feat(codex): add Windows support and compatibility for Codex plugin
This commit is contained in:
@@ -21,6 +21,7 @@ rules. The shared runner remains a plain shell entrypoint that the skill calls.
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
On Linux/macOS:
|
||||||
```bash
|
```bash
|
||||||
git clone <repo-url> SkillOpt-Sleep
|
git clone <repo-url> SkillOpt-Sleep
|
||||||
cd SkillOpt-Sleep
|
cd SkillOpt-Sleep
|
||||||
@@ -28,6 +29,14 @@ bash plugins/codex/install.sh # installs the skill
|
|||||||
export SKILLOPT_SLEEP_REPO="$(pwd)" # so the runner is found from anywhere
|
export SKILLOPT_SLEEP_REPO="$(pwd)" # so the runner is found from anywhere
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On Windows (PowerShell):
|
||||||
|
```powershell
|
||||||
|
git clone <repo-url> SkillOpt-Sleep
|
||||||
|
cd SkillOpt-Sleep
|
||||||
|
powershell -File plugins/codex/install.ps1
|
||||||
|
[System.Environment]::SetEnvironmentVariable("SKILLOPT_SLEEP_REPO", "$(pwd)", "User")
|
||||||
|
```
|
||||||
|
|
||||||
If a previous install created `~/.codex/prompts/sleep.md`, the installer moves
|
If a previous install created `~/.codex/prompts/sleep.md`, the installer moves
|
||||||
that deprecated prompt aside with a `.skillopt-legacy*.bak` suffix.
|
that deprecated prompt aside with a `.skillopt-legacy*.bak` suffix.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Install the SkillOpt-Sleep Codex integration as a user-level Codex skill on Windows.
|
||||||
|
# Idempotent; prints what it does.
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
|
||||||
|
$CodexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $env:USERPROFILE ".codex" }
|
||||||
|
$AgentsSkills = Join-Path $env:USERPROFILE ".agents\skills"
|
||||||
|
$LegacyPrompt = Join-Path $CodexHome "prompts\sleep.md"
|
||||||
|
|
||||||
|
Write-Output "[install] repo: $RepoRoot"
|
||||||
|
|
||||||
|
# 1) user-level skill
|
||||||
|
$SkillDir = Join-Path $AgentsSkills "skillopt-sleep"
|
||||||
|
if (-not (Test-Path $SkillDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $SkillDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
Copy-Item (Join-Path $RepoRoot "plugins\codex\skills\skillopt-sleep\SKILL.md") (Join-Path $SkillDir "SKILL.md") -Force
|
||||||
|
Write-Output "[install] skill -> $(Join-Path $SkillDir 'SKILL.md')"
|
||||||
|
|
||||||
|
# 2) retire the old custom prompt entrypoint from previous installs
|
||||||
|
if (Test-Path $LegacyPrompt) {
|
||||||
|
$Backup = "${LegacyPrompt}.skillopt-legacy.bak"
|
||||||
|
if (Test-Path $Backup) {
|
||||||
|
$DateStr = Get-Date -Format "yyyyMMddHHmmss"
|
||||||
|
$Backup = "${LegacyPrompt}.skillopt-legacy.${DateStr}.bak"
|
||||||
|
}
|
||||||
|
Move-Item $LegacyPrompt $Backup -Force
|
||||||
|
Write-Output "[install] legacy prompt -> $Backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3) record the repo location so the runner is found from anywhere
|
||||||
|
Write-Output "[install] add to your environment variables:"
|
||||||
|
Write-Output " [System.Environment]::SetEnvironmentVariable('SKILLOPT_SLEEP_REPO', '$RepoRoot', 'User')"
|
||||||
|
Write-Output " Or set it via System Properties."
|
||||||
|
|
||||||
|
# 4) optional: append an AGENTS.md hint (only if the user opts in)
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "[install] Optional — add this to ~/.codex/AGENTS.md so Codex always knows the tool:"
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output " ## SkillOpt-Sleep"
|
||||||
|
Write-Output " Use the skillopt-sleep skill when I ask to run a sleep/dream/offline"
|
||||||
|
Write-Output " self-improvement cycle. The runner is:"
|
||||||
|
Write-Output " \`powershell -File `"$RepoRoot\plugins\run-sleep.ps1`" status --project `"\$(pwd)\`"\`."
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "Done. Try asking Codex:"
|
||||||
|
Write-Output " Use the skillopt-sleep skill to run status for this project."
|
||||||
@@ -52,6 +52,18 @@ bash "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" run --project "$(pwd)" --source
|
|||||||
bash "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" adopt --project "$(pwd)"
|
bash "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" adopt --project "$(pwd)"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On Windows (CMD / PowerShell):
|
||||||
|
```cmd
|
||||||
|
:: CMD
|
||||||
|
set SKILLOPT_SLEEP_REPO=C:\path\to\SkillOpt-Sleep
|
||||||
|
"%SKILLOPT_SLEEP_REPO%\plugins\run-sleep.cmd" status --project "%CD%"
|
||||||
|
```
|
||||||
|
```powershell
|
||||||
|
# PowerShell
|
||||||
|
$env:SKILLOPT_SLEEP_REPO = "C:\path\to\SkillOpt-Sleep"
|
||||||
|
powershell -File "$env:SKILLOPT_SLEEP_REPO\plugins\run-sleep.ps1" status --project "$(pwd)"
|
||||||
|
```
|
||||||
|
|
||||||
Actions are `status`, `harvest`, `dry-run`, `run`, `adopt`, `schedule`, and `unschedule`.
|
Actions are `status`, `harvest`, `dry-run`, `run`, `adopt`, `schedule`, and `unschedule`.
|
||||||
|
|
||||||
- Default backend is `mock`, which is deterministic and spends no API budget.
|
- Default backend is `mock`, which is deterministic and spends no API budget.
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
:: Resolve REPO_ROOT
|
||||||
|
set "SCRIPT_DIR=%~dp0"
|
||||||
|
:: Strip trailing backslash
|
||||||
|
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
|
||||||
|
|
||||||
|
if exist "%SCRIPT_DIR%\..\skillopt_sleep" (
|
||||||
|
cd /d "%SCRIPT_DIR%\.."
|
||||||
|
set "REPO_ROOT=%CD%"
|
||||||
|
) else if not "%CLAUDE_PLUGIN_ROOT%"=="" if exist "%CLAUDE_PLUGIN_ROOT%\..\..\skillopt_sleep" (
|
||||||
|
cd /d "%CLAUDE_PLUGIN_ROOT%\..\.."
|
||||||
|
set "REPO_ROOT=%CD%"
|
||||||
|
) else if not "%SKILLOPT_SLEEP_REPO%"=="" if exist "%SKILLOPT_SLEEP_REPO%\skillopt_sleep" (
|
||||||
|
set "REPO_ROOT=%SKILLOPT_SLEEP_REPO%"
|
||||||
|
) else (
|
||||||
|
:: Search upward from current directory
|
||||||
|
set "d=%CD%"
|
||||||
|
set "REPO_ROOT="
|
||||||
|
:loop
|
||||||
|
if exist "!d!\skillopt_sleep" (
|
||||||
|
set "REPO_ROOT=!d!"
|
||||||
|
goto found
|
||||||
|
)
|
||||||
|
for %%I in ("!d!") do set "parent=%%~dpI"
|
||||||
|
:: Strip trailing backslash from parent if it's not root
|
||||||
|
if "!parent!"=="!d!" goto notfound
|
||||||
|
set "parent=!parent:~0,-1!"
|
||||||
|
if "!parent!"=="" goto notfound
|
||||||
|
set "d=!parent!"
|
||||||
|
goto loop
|
||||||
|
:notfound
|
||||||
|
echo [sleep] ERROR: could not locate the skillopt_sleep package. Set SKILLOPT_SLEEP_REPO to the repo root. >&2
|
||||||
|
exit /b 1
|
||||||
|
:found
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Find python >= 3.10
|
||||||
|
set "PY="
|
||||||
|
if not "%SKILLOPT_SLEEP_PYTHON%"=="" (
|
||||||
|
set "PY=%SKILLOPT_SLEEP_PYTHON%"
|
||||||
|
) else (
|
||||||
|
for %%p in (python3.exe python.exe py.exe) do (
|
||||||
|
where %%p >nul 2>nul
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
%%p -c "import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)" >nul 2>nul
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
set "PY=%%p"
|
||||||
|
goto py_found
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
:py_found
|
||||||
|
|
||||||
|
if "%PY%"=="" (
|
||||||
|
echo [sleep] ERROR: need Python >= 3.10 (found none). >&2
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
cd /d "%REPO_ROOT%"
|
||||||
|
if "%~1"=="" (
|
||||||
|
"%PY%" -m skillopt_sleep status
|
||||||
|
) else (
|
||||||
|
"%PY%" -m skillopt_sleep %*
|
||||||
|
)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# SkillOpt-Sleep shared runner for Windows PowerShell
|
||||||
|
# Resolves the repo root, picks Python >= 3.10, and runs the engine CLI.
|
||||||
|
#
|
||||||
|
# Usage: .\run-sleep.ps1 [status|run|dry-run|adopt|...] [args...]
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$ScriptDir = $PSScriptRoot
|
||||||
|
|
||||||
|
$RepoRoot = $null
|
||||||
|
if (Test-Path (Join-Path $ScriptDir "..\skillopt_sleep")) {
|
||||||
|
$RepoRoot = Resolve-Path (Join-Path $ScriptDir "..")
|
||||||
|
} elseif ($env:CLAUDE_PLUGIN_ROOT -and (Test-Path (Join-Path $env:CLAUDE_PLUGIN_ROOT "..\..\skillopt_sleep"))) {
|
||||||
|
$RepoRoot = Resolve-Path (Join-Path $env:CLAUDE_PLUGIN_ROOT "..\..")
|
||||||
|
} elseif ($env:SKILLOPT_SLEEP_REPO -and (Test-Path (Join-Path $env:SKILLOPT_SLEEP_REPO "skillopt_sleep"))) {
|
||||||
|
$RepoRoot = Resolve-Path $env:SKILLOPT_SLEEP_REPO
|
||||||
|
} else {
|
||||||
|
# search upward from current location
|
||||||
|
$d = Get-Item .
|
||||||
|
while ($d -and $d.FullName -ne $d.Root.FullName) {
|
||||||
|
if (Test-Path (Join-Path $d.FullName "skillopt_sleep")) {
|
||||||
|
$RepoRoot = $d.FullName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
$d = Split-Path -Parent $d.FullName -ErrorAction SilentlyContinue | Get-Item -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $RepoRoot) {
|
||||||
|
Write-Error "[sleep] ERROR: could not locate the skillopt_sleep package. Set SKILLOPT_SLEEP_REPO to the repo root."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$Py = ""
|
||||||
|
if ($env:SKILLOPT_SLEEP_PYTHON) {
|
||||||
|
$Py = $env:SKILLOPT_SLEEP_PYTHON
|
||||||
|
} else {
|
||||||
|
foreach ($cand in @("python3", "python", "py")) {
|
||||||
|
$cmd = Get-Command $cand -ErrorAction SilentlyContinue
|
||||||
|
if ($cmd) {
|
||||||
|
$ver = & $cand -c "import sys; print('%d%d' % sys.version_info[:2])" 2>$null
|
||||||
|
if ($ver -and [int]$ver -ge 310) {
|
||||||
|
$Py = $cand
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $Py) {
|
||||||
|
Write-Error "[sleep] ERROR: need Python >= 3.10 (found none)."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$argsList = @($args)
|
||||||
|
if ($argsList.Count -eq 0) {
|
||||||
|
$argsList = @("status")
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-Location $RepoRoot
|
||||||
|
# Run using the call operator
|
||||||
|
& $Py -m skillopt_sleep $argsList
|
||||||
+59
-24
@@ -735,14 +735,27 @@ def resolve_codex_path(explicit: str = "") -> str:
|
|||||||
env = os.environ.get("SKILLOPT_SLEEP_CODEX_PATH")
|
env = os.environ.get("SKILLOPT_SLEEP_CODEX_PATH")
|
||||||
if env:
|
if env:
|
||||||
return env
|
return env
|
||||||
candidates = [
|
import sys
|
||||||
os.path.expanduser("~/.nvm/versions/node/v22.22.3/bin/codex"),
|
candidates = []
|
||||||
]
|
if sys.platform == "win32":
|
||||||
# any nvm node version
|
appdata = os.environ.get("APPDATA")
|
||||||
nvm = os.path.expanduser("~/.nvm/versions/node")
|
if appdata:
|
||||||
if os.path.isdir(nvm):
|
candidates.append(os.path.join(appdata, "npm", "codex.cmd"))
|
||||||
for ver in sorted(os.listdir(nvm), reverse=True):
|
userprofile = os.environ.get("USERPROFILE")
|
||||||
candidates.append(os.path.join(nvm, ver, "bin", "codex"))
|
if userprofile:
|
||||||
|
candidates.append(os.path.join(userprofile, "AppData", "Roaming", "npm", "codex.cmd"))
|
||||||
|
nvm_home = os.environ.get("NVM_HOME")
|
||||||
|
if nvm_home:
|
||||||
|
candidates.append(os.path.join(nvm_home, "codex.cmd"))
|
||||||
|
else:
|
||||||
|
candidates = [
|
||||||
|
os.path.expanduser("~/.nvm/versions/node/v22.22.3/bin/codex"),
|
||||||
|
]
|
||||||
|
# any nvm node version
|
||||||
|
nvm = os.path.expanduser("~/.nvm/versions/node")
|
||||||
|
if os.path.isdir(nvm):
|
||||||
|
for ver in sorted(os.listdir(nvm), reverse=True):
|
||||||
|
candidates.append(os.path.join(nvm, ver, "bin", "codex"))
|
||||||
for c in candidates:
|
for c in candidates:
|
||||||
if not c or not os.path.exists(c):
|
if not c or not os.path.exists(c):
|
||||||
continue
|
continue
|
||||||
@@ -886,23 +899,45 @@ class CodexCliBackend(CliBackend):
|
|||||||
work = tempfile.mkdtemp(prefix="skillopt_sleep_codextools_")
|
work = tempfile.mkdtemp(prefix="skillopt_sleep_codextools_")
|
||||||
calllog = os.path.join(work, "_tool_calls.log")
|
calllog = os.path.join(work, "_tool_calls.log")
|
||||||
out_path = os.path.join(work, "_last.txt")
|
out_path = os.path.join(work, "_last.txt")
|
||||||
|
tool_names = tools or ["search"]
|
||||||
|
is_windows = os.name == "nt"
|
||||||
try:
|
try:
|
||||||
for tname in (tools or ["search"]):
|
for tname in tool_names:
|
||||||
shim = os.path.join(work, tname)
|
if is_windows:
|
||||||
with open(shim, "w") as f:
|
shim = os.path.join(work, f"{tname}.cmd")
|
||||||
f.write(
|
with open(shim, "w") as f:
|
||||||
"#!/usr/bin/env bash\n"
|
f.write(
|
||||||
f'echo "{tname}" >> "{calllog}"\n'
|
"@echo off\n"
|
||||||
'echo "(search results: 3 relevant notes found; use them to answer)"\n'
|
f'echo %~n0>>"{calllog}"\n'
|
||||||
)
|
"echo (search results: 3 relevant notes found; use them to answer)\n"
|
||||||
os.chmod(shim, os.stat(shim).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
)
|
||||||
tool_hint = (
|
else:
|
||||||
"Shell tools are available in the working directory: "
|
shim = os.path.join(work, tname)
|
||||||
+ ", ".join(f"./{t}" for t in (tools or ["search"]))
|
with open(shim, "w") as f:
|
||||||
+ ". When the skill says to look something up or search before "
|
f.write(
|
||||||
"answering, you MUST actually run the tool (e.g. `./search \"query\"`) "
|
"#!/usr/bin/env bash\n"
|
||||||
"before giving your final answer."
|
f'echo "{tname}" >> "{calllog}"\n'
|
||||||
)
|
'echo "(search results: 3 relevant notes found; use them to answer)"\n'
|
||||||
|
)
|
||||||
|
os.chmod(shim, os.stat(shim).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
||||||
|
if is_windows:
|
||||||
|
tool_hint = (
|
||||||
|
"Shell tools are available in the working directory: "
|
||||||
|
+ ", ".join(f"{t}.cmd" for t in tool_names)
|
||||||
|
+ " (each callable as `" + tool_names[0] + "` or `.\\"
|
||||||
|
+ tool_names[0] + "`). When the skill says to look something "
|
||||||
|
"up or search before answering, you MUST actually run the "
|
||||||
|
"tool (e.g. `" + tool_names[0] + " \"query\"`) before giving "
|
||||||
|
"your final answer."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
tool_hint = (
|
||||||
|
"Shell tools are available in the working directory: "
|
||||||
|
+ ", ".join(f"./{t}" for t in tool_names)
|
||||||
|
+ ". When the skill says to look something up or search before "
|
||||||
|
"answering, you MUST actually run the tool (e.g. `./search \"query\"`) "
|
||||||
|
"before giving your final answer."
|
||||||
|
)
|
||||||
prompt = (
|
prompt = (
|
||||||
"Complete the task. Apply the skill and memory rules EXACTLY, "
|
"Complete the task. Apply the skill and memory rules EXACTLY, "
|
||||||
"including any rule about searching before answering. Treat a "
|
"including any rule about searching before answering. Treat a "
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ def _runner_cmd(project: str, backend: str, extra: str, python: str) -> str:
|
|||||||
# use absolute python + -m so cron's minimal env still works
|
# use absolute python + -m so cron's minimal env still works
|
||||||
cmd = (f'{python} -m skillopt_sleep run --project "{project}" '
|
cmd = (f'{python} -m skillopt_sleep run --project "{project}" '
|
||||||
f'--scope invoked --backend {backend} {extra}'.rstrip())
|
f'--scope invoked --backend {backend} {extra}'.rstrip())
|
||||||
|
if sys.platform == "win32":
|
||||||
|
return f'if not exist "{logdir}" mkdir "{logdir}" && cd /d "{_repo_root()}" && {cmd} >> "{log}" 2>&1'
|
||||||
return f'mkdir -p "{logdir}"; cd "{_repo_root()}" && {cmd} >> "{log}" 2>&1'
|
return f'mkdir -p "{logdir}"; cd "{_repo_root()}" && {cmd} >> "{log}" 2>&1'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -767,6 +767,59 @@ class TestCodexBackend(unittest.TestCase):
|
|||||||
self.assertIn("exited 1", be.last_call_error) # failure surfaced for diagnostics
|
self.assertIn("exited 1", be.last_call_error) # failure surfaced for diagnostics
|
||||||
self.assertEqual(called, []) # no tool actually ran
|
self.assertEqual(called, []) # no tool actually ran
|
||||||
|
|
||||||
|
def test_codex_resolve_path_windows(self):
|
||||||
|
from skillopt_sleep.backend import resolve_codex_path
|
||||||
|
with mock.patch("sys.platform", "win32"), \
|
||||||
|
mock.patch.dict("os.environ", {
|
||||||
|
"APPDATA": r"C:\Users\Sparsh\AppData\Roaming",
|
||||||
|
"USERPROFILE": r"C:\Users\Sparsh",
|
||||||
|
"NVM_HOME": r"C:\Users\Sparsh\nvm"
|
||||||
|
}), \
|
||||||
|
mock.patch("os.path.exists", return_value=True):
|
||||||
|
path = resolve_codex_path("")
|
||||||
|
self.assertEqual(path, r"C:\Users\Sparsh\AppData\Roaming\npm\codex.cmd")
|
||||||
|
|
||||||
|
def test_codex_attempt_with_tools_windows(self):
|
||||||
|
from skillopt_sleep.backend import CodexCliBackend
|
||||||
|
be = CodexCliBackend(codex_path="codex")
|
||||||
|
task = TaskRecord(id="t", project="/p", intent="answer the question",
|
||||||
|
reference_kind="rule",
|
||||||
|
judge={"checks": [{"op": "tool_called", "arg": "search"}]})
|
||||||
|
calls = []
|
||||||
|
def fake_run(cmd, **kwargs):
|
||||||
|
calls.append((cmd, kwargs))
|
||||||
|
class Proc:
|
||||||
|
returncode = 0
|
||||||
|
stdout = ""
|
||||||
|
stderr = ""
|
||||||
|
return Proc()
|
||||||
|
|
||||||
|
with mock.patch("os.name", "nt"), \
|
||||||
|
mock.patch("shutil.rmtree"), \
|
||||||
|
mock.patch("skillopt_sleep.backend.subprocess.run", side_effect=fake_run):
|
||||||
|
orig_mkdtemp = tempfile.mkdtemp
|
||||||
|
temp_dirs = []
|
||||||
|
def fake_mkdtemp(*args, **kwargs):
|
||||||
|
d = orig_mkdtemp(*args, **kwargs)
|
||||||
|
temp_dirs.append(d)
|
||||||
|
return d
|
||||||
|
with mock.patch("tempfile.mkdtemp", side_effect=fake_mkdtemp):
|
||||||
|
be.attempt_with_tools(task, "", "", ["search"])
|
||||||
|
|
||||||
|
self.assertEqual(len(temp_dirs), 1)
|
||||||
|
work_dir = temp_dirs[0]
|
||||||
|
shim_path = os.path.join(work_dir, "search.cmd")
|
||||||
|
try:
|
||||||
|
self.assertTrue(os.path.exists(shim_path))
|
||||||
|
with open(shim_path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
self.assertIn("@echo off", content)
|
||||||
|
self.assertIn("%~n0", content)
|
||||||
|
finally:
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(work_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestMultiRolloutAndBudget(unittest.TestCase):
|
class TestMultiRolloutAndBudget(unittest.TestCase):
|
||||||
def test_rolloutset_stats(self):
|
def test_rolloutset_stats(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user