fix: address PR reviews, add scheduler Windows tasks and Pester coverage

This commit is contained in:
Sparsh :)
2026-07-13 21:20:30 +05:30
parent 3c45db3a90
commit 79d6117f6f
9 changed files with 408 additions and 66 deletions
+59 -12
View File
@@ -36,11 +36,12 @@ if exist "%SCRIPT_DIR%\..\skillopt_sleep" (
:found :found
) )
:: Find python >= 3.10 if not "%REPO_ROOT%"=="" (
set "PY=" :: Find python >= 3.10
if not "%SKILLOPT_SLEEP_PYTHON%"=="" ( set "PY="
if not "%SKILLOPT_SLEEP_PYTHON%"=="" (
set "PY=%SKILLOPT_SLEEP_PYTHON%" set "PY=%SKILLOPT_SLEEP_PYTHON%"
) else ( ) else (
for %%p in (python3.exe python.exe py.exe) do ( for %%p in (python3.exe python.exe py.exe) do (
where %%p >nul 2>nul where %%p >nul 2>nul
if !errorlevel! equ 0 ( if !errorlevel! equ 0 (
@@ -51,17 +52,63 @@ if not "%SKILLOPT_SLEEP_PYTHON%"=="" (
) )
) )
) )
) )
:py_found :py_found
if "%PY%"=="" ( if "%PY%"=="" (
echo [sleep] ERROR: need Python >= 3.10 (found none). >&2 echo [sleep] ERROR: need Python >= 3.10 (found none). >&2
exit /b 1 exit /b 1
)
cd /d "%REPO_ROOT%"
if "%~1"=="" (
"%PY%" -m skillopt_sleep status
) else (
"%PY%" -m skillopt_sleep %*
)
exit /b !errorlevel!
) )
cd /d "%REPO_ROOT%" :: No source checkout found — fall back to an installed engine.
if "%~1"=="" (
"%PY%" -m skillopt_sleep status :: Fallback 1: skillopt-sleep CLI on PATH (uv tool install / pipx / pip install).
) else ( where skillopt-sleep >nul 2>nul
"%PY%" -m skillopt_sleep %* if !errorlevel! equ 0 (
if "%~1"=="" (
skillopt-sleep status
) else (
skillopt-sleep %*
)
exit /b !errorlevel!
) )
:: Fallback 2: importable as a module (pip install into the active Python).
set "PY="
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 (
%%p -c "import skillopt_sleep" >nul 2>nul
if !errorlevel! equ 0 (
set "PY=%%p"
goto py_import_found
)
)
)
)
:py_import_found
if not "%PY%"=="" (
if "%~1"=="" (
"%PY%" -m skillopt_sleep status
) else (
"%PY%" -m skillopt_sleep %*
)
exit /b !errorlevel!
)
echo [sleep] ERROR: could not locate the skillopt_sleep package. >&2
echo [sleep] Install it with 'uv tool install skillopt' or 'pip install skillopt', >&2
echo [sleep] or set SKILLOPT_SLEEP_REPO to a clone of the SkillOpt repo. >&2
exit /b 1
+47 -15
View File
@@ -26,15 +26,16 @@ if (Test-Path (Join-Path $ScriptDir "..\skillopt_sleep")) {
} }
} }
if (-not $RepoRoot) { $argsList = @($args)
Write-Error "[sleep] ERROR: could not locate the skillopt_sleep package. Set SKILLOPT_SLEEP_REPO to the repo root." if ($argsList.Count -eq 0) {
exit 1 $argsList = @("status")
} }
$Py = "" if ($RepoRoot) {
if ($env:SKILLOPT_SLEEP_PYTHON) { $Py = ""
if ($env:SKILLOPT_SLEEP_PYTHON) {
$Py = $env:SKILLOPT_SLEEP_PYTHON $Py = $env:SKILLOPT_SLEEP_PYTHON
} else { } else {
foreach ($cand in @("python3", "python", "py")) { foreach ($cand in @("python3", "python", "py")) {
$cmd = Get-Command $cand -ErrorAction SilentlyContinue $cmd = Get-Command $cand -ErrorAction SilentlyContinue
if ($cmd) { if ($cmd) {
@@ -45,18 +46,49 @@ if ($env:SKILLOPT_SLEEP_PYTHON) {
} }
} }
} }
} }
if (-not $Py) { if (-not $Py) {
Write-Error "[sleep] ERROR: need Python >= 3.10 (found none)." [Console]::Error.WriteLine("[sleep] ERROR: need Python >= 3.10 (found none).")
exit 1 exit 1
}
Set-Location $RepoRoot
& $Py -m skillopt_sleep $argsList
exit $LASTEXITCODE
} }
$argsList = @($args) # No source checkout found — fall back to an installed engine.
if ($argsList.Count -eq 0) {
$argsList = @("status") # Fallback 1: skillopt-sleep CLI on PATH (uv tool install / pipx / pip install).
$cliCmd = Get-Command "skillopt-sleep" -ErrorAction SilentlyContinue
if ($cliCmd) {
& skillopt-sleep $argsList
exit $LASTEXITCODE
} }
Set-Location $RepoRoot # Fallback 2: importable as a module (pip install into the active Python).
# Run using the call operator $Py = ""
& $Py -m skillopt_sleep $argsList 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) {
$hasModule = & $cand -c "import skillopt_sleep" 2>$null
if ($LASTEXITCODE -eq 0) {
$Py = $cand
break
}
}
}
}
if ($Py) {
& $Py -m skillopt_sleep $argsList
exit $LASTEXITCODE
}
[Console]::Error.WriteLine("[sleep] ERROR: could not locate the skillopt_sleep package.")
[Console]::Error.WriteLine("[sleep] Install it with 'uv tool install skillopt' or 'pip install skillopt',")
[Console]::Error.WriteLine("[sleep] or set SKILLOPT_SLEEP_REPO to a clone of the SkillOpt repo.")
exit 1
+14 -6
View File
@@ -736,21 +736,29 @@ def resolve_codex_path(explicit: str = "") -> str:
if env: if env:
return env return env
import sys import sys
import shutil
candidates = [] candidates = []
# Try shutil.which("codex") first so PATH, pnpm, Volta, etc. work.
which_codex = shutil.which("codex")
if which_codex:
candidates.append(which_codex)
if sys.platform == "win32": if sys.platform == "win32":
import ntpath
appdata = os.environ.get("APPDATA") appdata = os.environ.get("APPDATA")
if appdata: if appdata:
candidates.append(os.path.join(appdata, "npm", "codex.cmd")) candidates.append(ntpath.join(appdata, "npm", "codex.cmd"))
userprofile = os.environ.get("USERPROFILE") userprofile = os.environ.get("USERPROFILE")
if userprofile: if userprofile:
candidates.append(os.path.join(userprofile, "AppData", "Roaming", "npm", "codex.cmd")) candidates.append(ntpath.join(userprofile, "AppData", "Roaming", "npm", "codex.cmd"))
nvm_home = os.environ.get("NVM_HOME") nvm_home = os.environ.get("NVM_HOME")
if nvm_home: if nvm_home:
candidates.append(os.path.join(nvm_home, "codex.cmd")) candidates.append(ntpath.join(nvm_home, "codex.cmd"))
else: else:
candidates = [ candidates.extend([
os.path.expanduser("~/.nvm/versions/node/v22.22.3/bin/codex"), os.path.expanduser("~/.nvm/versions/node/v22.22.3/bin/codex"),
] ])
# any nvm node version # any nvm node version
nvm = os.path.expanduser("~/.nvm/versions/node") nvm = os.path.expanduser("~/.nvm/versions/node")
if os.path.isdir(nvm): if os.path.isdir(nvm):
@@ -768,7 +776,7 @@ def resolve_codex_path(explicit: str = "") -> str:
except Exception: except Exception:
pass pass
return c return c
return "codex" # last resort (may be the wrapper) return "codex"
class CodexCliBackend(CliBackend): class CodexCliBackend(CliBackend):
+89 -19
View File
@@ -1,19 +1,7 @@
"""SkillOpt-Sleep — built-in nightly scheduler. """SkillOpt-Sleep — built-in nightly scheduler.
Installs/removes a crontab entry that runs the sleep cycle automatically, so the Installs/removes a crontab entry (on Unix) or a Scheduled Task (on Windows) that
user doesn't have to wire cron themselves. Idempotent: a managed block delimited runs the sleep cycle automatically, so the user doesn't have to wire it manually.
by marker comments is added/replaced/removed in the user's crontab.
Design choices:
* Off-:00 minute (3:17 local by default) so many users don't all hit the API
at the same instant.
* The entry runs `python -m skillopt_sleep run` for a specific project and
appends to <project>/.skillopt-sleep/cron.log.
* `schedule` is additive per project (keyed by project path); `unschedule`
removes the project's line (or the whole managed block with --all).
cron is the portable mechanism on Linux/macOS. On systems without `crontab`,
`schedule` prints the line and instructions instead of failing.
""" """
from __future__ import annotations from __future__ import annotations
@@ -65,14 +53,62 @@ def _split_managed(crontab: str) -> Tuple[str, List[str]]:
return "\n".join(outside).rstrip(), managed return "\n".join(outside).rstrip(), managed
def _have_schtasks() -> bool:
return shutil.which("schtasks") is not None
def _win_task_name(project: str) -> str:
project = os.path.abspath(project)
safe = project.replace(":\\", "_").replace("\\", "_").replace("/", "_").replace(" ", "_")
return f"SkillOpt-Sleep-{safe}"
def _create_win_task(task_name: str, command: str, hour: int, minute: int) -> bool:
try:
st = f"{hour:02d}:{minute:02d}"
cmd = ["schtasks", "/create", "/tn", task_name, "/tr", command, "/sc", "daily", "/st", st, "/f"]
proc = subprocess.run(cmd, capture_output=True, text=True)
return proc.returncode == 0
except Exception:
return False
def _delete_win_task(task_name: str) -> bool:
try:
cmd = ["schtasks", "/delete", "/tn", task_name, "/f"]
proc = subprocess.run(cmd, capture_output=True, text=True)
return proc.returncode == 0
except Exception:
return False
def _list_win_tasks() -> List[str]:
try:
proc = subprocess.run(["schtasks", "/query", "/fo", "csv"], capture_output=True, text=True)
if proc.returncode != 0:
return []
tasks = []
for line in proc.stdout.splitlines():
if not line.startswith('"'):
continue
parts = line.split(",")
if len(parts) > 0:
name = parts[0].strip('"')
if name.startswith("SkillOpt-Sleep-"):
tasks.append(name)
return tasks
except Exception:
return []
def _runner_cmd(project: str, backend: str, extra: str, python: str) -> str: def _runner_cmd(project: str, backend: str, extra: str, python: str) -> str:
logdir = os.path.join(project, ".skillopt-sleep") logdir = os.path.join(project, ".skillopt-sleep")
log = os.path.join(logdir, "cron.log") log = os.path.join(logdir, "cron.log")
# use absolute python + -m so cron's minimal env still works # use absolute python + -m so cron's/scheduler'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": if sys.platform == "win32":
return f'if not exist "{logdir}" mkdir "{logdir}" && cd /d "{_repo_root()}" && {cmd} >> "{log}" 2>&1' return f'cmd.exe /c "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'
@@ -89,12 +125,26 @@ def schedule(project: str, *, backend: str = "mock", hour: int = 3, minute: int
extra: str = "", python: Optional[str] = None) -> Tuple[bool, str]: extra: str = "", python: Optional[str] = None) -> Tuple[bool, str]:
"""Install (or replace) the nightly entry for ``project``. """Install (or replace) the nightly entry for ``project``.
Returns (installed, message). If crontab is unavailable, installed=False and Returns (installed, message). If the scheduler backend is unavailable, installed=False and
the message contains the line to add manually. the message contains instructions to add manually.
""" """
project = os.path.abspath(project) project = os.path.abspath(project)
python = python or sys.executable or "python3" python = python or sys.executable or "python3"
cron_line = f"{minute} {hour} * * * {_runner_cmd(project, backend, extra, python)} {_project_marker(project)}" runner_cmd = _runner_cmd(project, backend, extra, python)
if sys.platform == "win32":
if not _have_schtasks():
return False, "schtasks.exe not found on this system. Add this command to your scheduler manually:\n" + runner_cmd
tn = _win_task_name(project)
ok = _create_win_task(tn, runner_cmd, hour, minute)
if ok:
return True, (f"Scheduled nightly at {hour:02d}:{minute:02d} for {project} "
f"(backend={backend}) via Windows Task Scheduler. Task Name: {tn}\n"
f"Logs -> {project}/.skillopt-sleep/cron.log\n"
f"Runs `skillopt_sleep run`; it only STAGES a proposal — adopt is still manual.")
return False, f"Failed to write scheduled task. Command to run manually:\nschtasks /create /tn \"{tn}\" /tr \"{runner_cmd}\" /sc daily /st {hour:02d}:{minute:02d} /f"
cron_line = f"{minute} {hour} * * * {runner_cmd} {_project_marker(project)}"
if not _have_crontab(): if not _have_crontab():
return False, ("crontab not found on this system. Add this line to your " return False, ("crontab not found on this system. Add this line to your "
@@ -118,6 +168,22 @@ def schedule(project: str, *, backend: str = "mock", hour: int = 3, minute: int
def unschedule(project: Optional[str] = None, *, all_projects: bool = False) -> Tuple[bool, str]: def unschedule(project: Optional[str] = None, *, all_projects: bool = False) -> Tuple[bool, str]:
"""Remove the entry for ``project`` (or the whole managed block with all_projects).""" """Remove the entry for ``project`` (or the whole managed block with all_projects)."""
if sys.platform == "win32":
if not _have_schtasks():
return False, "schtasks.exe not found on this system."
if all_projects:
tasks = _list_win_tasks()
ok = True
for t in tasks:
if not _delete_win_task(t):
ok = False
return ok, ("Removed all scheduled tasks." if ok else "Failed to remove some tasks.")
elif project:
tn = _win_task_name(project)
ok = _delete_win_task(tn)
return ok, ("Removed." if ok else "Failed to remove scheduled task (does it exist?).")
return False, "No project specified to unschedule."
if not _have_crontab(): if not _have_crontab():
return False, "crontab not found; nothing to remove." return False, "crontab not found; nothing to remove."
outside, managed = _split_managed(_read_crontab()) outside, managed = _split_managed(_read_crontab())
@@ -136,5 +202,9 @@ def unschedule(project: Optional[str] = None, *, all_projects: bool = False) ->
def list_scheduled() -> List[str]: def list_scheduled() -> List[str]:
if sys.platform == "win32":
if not _have_schtasks():
return []
return _list_win_tasks()
_outside, managed = _split_managed(_read_crontab()) _outside, managed = _split_managed(_read_crontab())
return [ln for ln in managed if ln.strip()] return [ln for ln in managed if ln.strip()]
+1 -1
View File
@@ -9,7 +9,7 @@ def test_resolve_alfworld_gamefile_uses_alfworld_data_for_relative_paths(monkeyp
resolved = _resolve_alfworld_gamefile("json_2.1.1/valid_seen/task/game.tw-pddl") resolved = _resolve_alfworld_gamefile("json_2.1.1/valid_seen/task/game.tw-pddl")
assert resolved == os.path.join(str(data_root), "json_2.1.1/valid_seen/task/game.tw-pddl") assert resolved == os.path.normpath(os.path.join(str(data_root), "json_2.1.1/valid_seen/task/game.tw-pddl"))
def test_resolve_alfworld_gamefile_keeps_absolute_paths(monkeypatch, tmp_path): def test_resolve_alfworld_gamefile_keeps_absolute_paths(monkeypatch, tmp_path):
+3
View File
@@ -31,6 +31,7 @@ def _run(script, args, env, cwd):
return proc.returncode, proc.stdout, proc.stderr return proc.returncode, proc.stdout, proc.stderr
@unittest.skipIf(sys.platform == "win32", "Unix bash runner tests require POSIX/bash environment")
class TestRunnerSourceCheckout(unittest.TestCase): class TestRunnerSourceCheckout(unittest.TestCase):
"""When skillopt_sleep/ is found in the repo, the source-checkout path is used.""" """When skillopt_sleep/ is found in the repo, the source-checkout path is used."""
@@ -46,6 +47,7 @@ class TestRunnerSourceCheckout(unittest.TestCase):
self.assertIn("status", out) self.assertIn("status", out)
@unittest.skipIf(sys.platform == "win32", "Unix bash runner tests require POSIX/bash environment")
class TestRunnerCliFallback(unittest.TestCase): class TestRunnerCliFallback(unittest.TestCase):
"""When no source dir is found, fall back to the skillopt-sleep CLI on PATH.""" """When no source dir is found, fall back to the skillopt-sleep CLI on PATH."""
@@ -101,6 +103,7 @@ class TestRunnerCliFallback(unittest.TestCase):
self.assertIn("fake-cli invoked: status", out) self.assertIn("fake-cli invoked: status", out)
@unittest.skipIf(sys.platform == "win32", "Unix bash runner tests require POSIX/bash environment")
class TestRunnerNoEngine(unittest.TestCase): class TestRunnerNoEngine(unittest.TestCase):
"""When no source dir, no CLI on PATH, and no importable package, error cleanly.""" """When no source dir, no CLI on PATH, and no importable package, error cleanly."""
+118
View File
@@ -0,0 +1,118 @@
Describe "run-sleep.ps1 runner" {
BeforeAll {
$script = Join-Path $PSScriptRoot "..\plugins\run-sleep.ps1"
$tempDir = [System.IO.Path]::GetTempFileName()
Remove-Item $tempDir
New-Item -ItemType Directory -Path $tempDir | Out-Null
# Detect a working python to bypass Windows Store alias issues
if (-not $env:SKILLOPT_SLEEP_PYTHON) {
if (Test-Path "C:\Python314\python.exe") {
$env:SKILLOPT_SLEEP_PYTHON = "C:\Python314\python.exe"
} elseif (Test-Path "C:\Python313\python.exe") {
$env:SKILLOPT_SLEEP_PYTHON = "C:\Python313\python.exe"
} elseif (Test-Path "C:\Python312\python.exe") {
$env:SKILLOPT_SLEEP_PYTHON = "C:\Python312\python.exe"
} elseif (Test-Path "C:\Python311\python.exe") {
$env:SKILLOPT_SLEEP_PYTHON = "C:\Python311\python.exe"
} elseif (Test-Path "C:\Python310\python.exe") {
$env:SKILLOPT_SLEEP_PYTHON = "C:\Python310\python.exe"
}
}
}
AfterAll {
if (Test-Path $tempDir) {
Remove-Item -Recurse -Force $tempDir
}
}
It "runs successfully in source checkout mode" {
$env:SKILLOPT_SLEEP_REPO = Resolve-Path (Join-Path $PSScriptRoot "..")
# Run help
$result = powershell -File $script "--help" 2>&1
$LASTEXITCODE | Should Be 0
$result | Out-String | Should Match "skillopt_sleep"
}
It "falls back to CLI on PATH" {
# Create a temp dir for isolated testing
$sandbox = Join-Path $tempDir "cli_fallback"
New-Item -ItemType Directory -Path $sandbox | Out-Null
$scriptCopy = Join-Path $sandbox "run-sleep.ps1"
Copy-Item $script $scriptCopy
# Create fake CLI
$binDir = Join-Path $sandbox "bin"
New-Item -ItemType Directory -Path $binDir | Out-Null
$fakeCli = Join-Path $binDir "skillopt-sleep.cmd"
"@echo off`r`necho fake-cli invoked %*`r`nexit /b 0" | Out-File -FilePath $fakeCli -Encoding ascii
# Save existing env vars
$oldPath = $env:PATH
$oldRepo = $env:SKILLOPT_SLEEP_REPO
$oldPlugin = $env:CLAUDE_PLUGIN_ROOT
$env:PATH = "$binDir;$oldPath"
$env:SKILLOPT_SLEEP_REPO = $null
$env:CLAUDE_PLUGIN_ROOT = $null
$oldLocation = Get-Location
Set-Location $sandbox
try {
$result = powershell -File $scriptCopy "status" 2>&1
$LASTEXITCODE | Should Be 0
$result | Out-String | Should Match "fake-cli invoked status"
}
finally {
Set-Location $oldLocation
$env:PATH = $oldPath
$env:SKILLOPT_SLEEP_REPO = $oldRepo
$env:CLAUDE_PLUGIN_ROOT = $oldPlugin
}
}
It "propagates exit code on failure" {
$env:SKILLOPT_SLEEP_REPO = Resolve-Path (Join-Path $PSScriptRoot "..")
# Run a non-existent command to trigger python module failure
$result = powershell -File $script "non-existent-subcommand" 2>&1
$LASTEXITCODE | Should Not Be 0
}
It "handles paths containing spaces correctly" {
# Create path containing spaces
$spaceDir = Join-Path $tempDir "path with spaces"
New-Item -ItemType Directory -Path $spaceDir | Out-Null
$scriptCopy = Join-Path $spaceDir "run-sleep.ps1"
Copy-Item $script $scriptCopy
# Create fake CLI
$binDir = Join-Path $spaceDir "bin"
New-Item -ItemType Directory -Path $binDir | Out-Null
$fakeCli = Join-Path $binDir "skillopt-sleep.cmd"
"@echo off`r`necho fake-cli invoked %*`r`nexit /b 0" | Out-File -FilePath $fakeCli -Encoding ascii
# Save env
$oldPath = $env:PATH
$oldRepo = $env:SKILLOPT_SLEEP_REPO
$oldPlugin = $env:CLAUDE_PLUGIN_ROOT
$env:PATH = "$binDir;$oldPath"
$env:SKILLOPT_SLEEP_REPO = $null
$env:CLAUDE_PLUGIN_ROOT = $null
$oldLocation = Get-Location
Set-Location $spaceDir
try {
$result = powershell -File $scriptCopy "status" 2>&1
$LASTEXITCODE | Should Be 0
$result | Out-String | Should Match "fake-cli invoked status"
}
finally {
Set-Location $oldLocation
$env:PATH = $oldPath
$env:SKILLOPT_SLEEP_REPO = $oldRepo
$env:CLAUDE_PLUGIN_ROOT = $oldPlugin
}
}
}
+63
View File
@@ -0,0 +1,63 @@
import os
import sys
import unittest
from unittest import mock
class TestSchedulerWindows(unittest.TestCase):
@mock.patch("sys.platform", "win32")
@mock.patch("shutil.which", return_value="C:\\Windows\\System32\\schtasks.exe")
def test_schedule_windows(self, mock_which):
from skillopt_sleep.scheduler import schedule
calls = []
def fake_run(cmd, **kwargs):
calls.append(cmd)
class Proc:
returncode = 0
stdout = "SUCCESS: The scheduled task ... has successfully been created."
stderr = ""
return Proc()
with mock.patch("subprocess.run", side_effect=fake_run):
ok, msg = schedule("/p/my project", backend="mock", hour=3, minute=17)
self.assertTrue(ok)
self.assertIn("via Windows Task Scheduler", msg)
self.assertEqual(len(calls), 1)
cmd = calls[0]
self.assertEqual(cmd[0], "schtasks")
self.assertEqual(cmd[1], "/create")
self.assertEqual(cmd[2], "/tn")
self.assertTrue(cmd[3].startswith("SkillOpt-Sleep-"))
self.assertIn("my_project", cmd[3])
self.assertEqual(cmd[4], "/tr")
self.assertIn("cmd.exe", cmd[5])
self.assertEqual(cmd[6], "/sc")
self.assertEqual(cmd[7], "daily")
self.assertEqual(cmd[8], "/st")
self.assertEqual(cmd[9], "03:17")
self.assertEqual(cmd[10], "/f")
@mock.patch("sys.platform", "win32")
@mock.patch("shutil.which", return_value="C:\\Windows\\System32\\schtasks.exe")
def test_unschedule_windows(self, mock_which):
from skillopt_sleep.scheduler import unschedule
calls = []
def fake_run(cmd, **kwargs):
calls.append(cmd)
class Proc:
returncode = 0
stdout = "SUCCESS: The scheduled task ... was successfully deleted."
stderr = ""
return Proc()
with mock.patch("subprocess.run", side_effect=fake_run):
ok, msg = unschedule("/p/my project")
self.assertTrue(ok)
self.assertEqual(len(calls), 1)
cmd = calls[0]
self.assertEqual(cmd[0], "schtasks")
self.assertEqual(cmd[1], "/delete")
self.assertEqual(cmd[2], "/tn")
self.assertTrue(cmd[3].startswith("SkillOpt-Sleep-"))
self.assertEqual(cmd[4], "/f")
+1
View File
@@ -770,6 +770,7 @@ class TestCodexBackend(unittest.TestCase):
def test_codex_resolve_path_windows(self): def test_codex_resolve_path_windows(self):
from skillopt_sleep.backend import resolve_codex_path from skillopt_sleep.backend import resolve_codex_path
with mock.patch("sys.platform", "win32"), \ with mock.patch("sys.platform", "win32"), \
mock.patch("shutil.which", return_value=None), \
mock.patch.dict("os.environ", { mock.patch.dict("os.environ", {
"APPDATA": r"C:\Users\Sparsh\AppData\Roaming", "APPDATA": r"C:\Users\Sparsh\AppData\Roaming",
"USERPROFILE": r"C:\Users\Sparsh", "USERPROFILE": r"C:\Users\Sparsh",