🚩 gremlins: Windows-only gate in where, init and launches

This commit is contained in:
2026-10-16 20:39:15 +02:00
parent 25d03587b4
commit e9f1b0615a
2 changed files with 289 additions and 13 deletions
@@ -71,6 +71,12 @@ def child_env():
return env
def gate_warning(out: dict) -> list[str]:
"""The warning `where` adds when the platform gate is overridden (C13 on a closed platform;
never on Windows), so that the exact `warnings` assertions hold on every host."""
return [gremlins.GATE_OVERRIDE_WARNING] if out["platform"]["override"] else []
class Base(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.mkdtemp(prefix="gremlins-test-")
@@ -79,6 +85,9 @@ class Base(unittest.TestCase):
env_patch.start()
self.addCleanup(env_patch.stop)
os.environ.pop(gremlins.CHILD_MARKER, None)
# C13: the suite runs with the platform gate overridden, so it exercises the POSIX code on
# the porting hosts; on Windows (supported) the variable changes nothing. GateTests drop it.
os.environ[gremlins.UNSUPPORTED_OK_ENV] = "1"
def assert_one_json_line(self, raw: str):
self.assertTrue(raw.endswith("\n"), repr(raw[-40:]))
@@ -1086,15 +1095,51 @@ class CliTests(Base):
code, out, raw = call(["where", "--binary", stub])
self.assertEqual(code, 0, out)
self.assert_one_json_line(raw)
self.assertEqual(out["warnings"], [])
self.assertEqual(out["warnings"], gate_warning(out))
os.environ["ANTHROPIC_BASE_URL"] = "https://llm-gateway.example.internal/anthropic"
code, out, raw = call(["where", "--binary", stub])
self.assertEqual(code, 0, out)
self.assert_one_json_line(raw)
self.assertTrue(out["ok"])
self.assertEqual(len(out["warnings"]), 1, out["warnings"])
self.assertEqual(len(out["warnings"]), 1 + len(gate_warning(out)), out["warnings"])
self.assertIn("ANTHROPIC_BASE_URL", out["warnings"][0])
self.assertIn("llm-gateway.example.internal", out["warnings"][0])
self.assertEqual(out["warnings"][1:], gate_warning(out))
def test_where_platform(self):
"""Digest §3.6/§4, A12: `where.platform` is structured; `override` is false on a supported
platform even though the suite sets the override env (C13)."""
stub = make_stub_binary(self.tmp)
self.assertEqual(os.environ.get(gremlins.UNSUPPORTED_OK_ENV), "1")
code, out, raw = call(["where", "--binary", stub])
self.assertEqual(code, 0, out)
self.assert_one_json_line(raw)
self.assertNotIn("spawn", out)
pf = out["platform"]
self.assertEqual(set(pf), {"name", "sys_platform", "wsl", "supported", "detach", "override"})
self.assertEqual(pf["sys_platform"], sys.platform)
self.assertEqual(pf["override"], not gremlins.PLATFORM.supported)
if WIN_PLATFORM:
self.assertEqual(pf, {"name": "windows", "sys_platform": sys.platform, "wsl": False,
"supported": True, "detach": "cim-hidden", "override": False})
self.assertEqual(out["warnings"], [])
def test_where_refused_binary_path(self):
"""A11: a `--binary` path form this platform refuses lands in `binary.error` and the
reasons (exit 2); the rest of the report is still filled."""
bad = "/mnt/c/x/claude.exe" if WIN_PLATFORM else "C:\\x\\claude.exe"
code, out, raw = call(["where", "--binary", bad])
self.assertEqual(code, 2, out)
self.assert_one_json_line(raw)
self.assertFalse(out["ok"])
self.assertIsNone(out["binary"]["path"])
self.assertEqual(out["binary"]["how"], "--binary")
self.assertEqual(out["binary"]["given"], bad)
self.assertIn("path form", out["binary"]["error"])
self.assertIn(out["binary"]["error"], out["reason"])
self.assertEqual(out["error"], out["reason"])
for k in ("script", "python", "uv", "git", "scrub", "platform", "warnings"):
self.assertIn(k, out)
def test_child_refuses_kill_expire_init(self):
"""M4: a gremlin may not run kill, expire or init."""
@@ -1142,6 +1187,175 @@ class CliTests(Base):
self.assertIn("error", out)
# --------------------------------------------------------------------------------------
def closed_platform():
"""B1: a fake closed platform. It subclasses the host's own platform class (on Windows a
`WindowsPlatform`), so path handling stays the host's and `--binary`, `--run` and
`script_path()` work; it reports itself as WSL Linux and unsupported."""
host = type(gremlins.PLATFORM)
class ClosedPlatform(host):
supported = False
def is_wsl(self):
return True
def describe(self):
return {"name": "posix", "sys_platform": "linux", "wsl": True, "supported": False,
"detach": "setsid"}
return ClosedPlatform()
class GateTests(Base):
"""Digest §4 with amendments B1, A1, A7, A12: the Windows-only gate."""
def setUp(self):
super().setUp()
os.environ.pop(gremlins.UNSUPPORTED_OK_ENV, None) # C13: the gate tests run without it
self.cwd = os.path.join(self.tmp, "proj")
os.makedirs(self.cwd)
os.makedirs(os.path.join(self.tmp, "reports"))
self.run_log = os.path.join(self.tmp, "reports", "2026-10-16-20-g-run.md")
self.state = gremlins.state_dir_for(self.run_log)
p = mock.patch.object(gremlins, "USER_SETTINGS_PATH", os.path.join(self.tmp, "home", "settings.json"))
p.start()
self.addCleanup(p.stop)
self.stub = make_stub_binary(self.tmp)
def close_gate(self):
p = mock.patch.object(gremlins, "PLATFORM", closed_platform())
p.start()
self.addCleanup(p.stop)
def init(self):
return call(["init", "--run", self.run_log, "--model", "claude-opus-5-5[1m]", "--cwd", self.cwd,
"--binary", self.stub])
def test_supported_flags(self):
self.assertIs(gremlins.WindowsPlatform().supported, True)
for sp in ("linux", "darwin"):
with self.subTest(sys_platform=sp), mock.patch.object(sys, "platform", sp):
pp = gremlins.PosixPlatform()
self.assertIs(pp.supported, False)
self.assertIs(pp.describe()["supported"], False) # A12: read from `supported`
self.assertEqual(pp.describe()["sys_platform"], sp)
def test_is_wsl(self):
pp = gremlins.PosixPlatform()
os.environ["WSL_DISTRO_NAME"] = "Ubuntu-20.04"
self.assertTrue(pp.is_wsl())
os.environ.pop("WSL_DISTRO_NAME")
with mock.patch("builtins.open", mock.mock_open(read_data=b"5.15.90.1-microsoft-standard-WSL2")):
self.assertTrue(pp.is_wsl())
with mock.patch("builtins.open", mock.mock_open(read_data=b"6.8.0-45-generic")):
self.assertFalse(pp.is_wsl())
with mock.patch("builtins.open", side_effect=OSError("no /proc")):
self.assertFalse(pp.is_wsl())
def test_gate_reason(self):
"""A7: no version number; names the platform and WSL; A12: read from describe()."""
if gremlins.PLATFORM.supported:
self.assertIsNone(gremlins.platform_gate())
self.close_gate()
self.assertEqual(gremlins.platform_gate(),
"spire:gremlins is not tested on linux, WSL yet; use fresh agents for every "
"block (see SKILL.md, Platforms).")
os.environ[gremlins.UNSUPPORTED_OK_ENV] = "1"
self.assertIsNone(gremlins.platform_gate())
self.assertTrue(gremlins.gate_overridden())
def test_host_where(self):
"""On the real host without the override env: Windows is supported, no override."""
code, out, raw = call(["where", "--binary", self.stub])
self.assert_one_json_line(raw)
if WIN_PLATFORM:
self.assertEqual(code, 0, out)
self.assertTrue(out["platform"]["supported"])
self.assertFalse(out["platform"]["override"])
self.assertEqual(out["warnings"], [])
def test_closed_where(self):
self.close_gate()
code, out, raw = call(["where", "--binary", self.stub])
self.assertEqual(code, 2, out)
self.assert_one_json_line(raw)
self.assertFalse(out["ok"])
self.assertIn("not tested on linux, WSL", out["reason"])
self.assertIn("fresh agents", out["reason"])
self.assertEqual(out["error"], out["reason"])
self.assertEqual(out["platform"], {"name": "posix", "sys_platform": "linux", "wsl": True,
"supported": False, "detach": "setsid", "override": False})
# everything else is still filled (the porting hosts need it)
self.assertEqual(out["binary"]["version"], STUB_VERSION)
self.assertNotIn("error", out["binary"])
for k in ("script", "python", "uv", "git", "scrub", "warnings"):
self.assertIn(k, out)
self.assertEqual(out["warnings"], [])
def test_closed_init(self):
self.close_gate()
code, out, raw = self.init()
self.assertEqual(code, 2, out)
self.assert_one_json_line(raw)
self.assertEqual(out["error"], "platform not supported")
self.assertIn("not tested on linux, WSL", out["reason"])
self.assertEqual(out["platform"]["supported"], False)
self.assertEqual(out["platform"]["override"], False)
self.assertFalse(os.path.exists(self.state))
# B1: the gate comes before any path handling (a bad --run is not even looked at)
code, out, _ = call(["init", "--run", os.path.join(self.tmp, "not-a-run.txt"), "--model",
"claude-opus-5-5[1m]", "--cwd", self.cwd, "--binary", self.stub])
self.assertEqual((code, out["error"]), (2, "platform not supported"))
def test_override(self):
self.close_gate()
os.environ[gremlins.UNSUPPORTED_OK_ENV] = "1"
code, out, raw = call(["where", "--binary", self.stub])
self.assertEqual(code, 0, out)
self.assert_one_json_line(raw)
self.assertTrue(out["ok"])
self.assertTrue(out["platform"]["override"])
self.assertFalse(out["platform"]["supported"])
self.assertEqual(out["warnings"], [gremlins.GATE_OVERRIDE_WARNING])
self.assertIn(gremlins.UNSUPPORTED_OK_ENV, out["warnings"][0])
code, out, _ = self.init()
self.assertEqual(code, 0, out)
self.assertTrue(out["created"])
self.assertTrue(os.path.isfile(gremlins.registry_path(self.state)))
def test_closed_gate_on_existing_registry(self):
"""A1: a registry made with the gate open; with it closed, nothing launches and no job
entry is written; the tick launches no ping (it reads nothing)."""
code, out, _ = self.init()
self.assertEqual(code, 0, out)
before = gremlins.read_registry(self.state)
brief = os.path.join(self.tmp, "b.md")
with open(brief, "w") as f:
f.write("x\n")
self.close_gate()
for argv in (["spawn", "--base", "B1", "--block", "1.1", "--brief", brief],
["train", "--base", "B1", "--brief", brief],
["continue", "--job", "j0001", "--brief", brief],
["rebase", "--base", "B1", "--as", "B2", "--brief", brief],
["tick"]):
with self.subTest(cmd=argv[0]):
code, out, raw = call([argv[0], "--run", self.run_log, *argv[1:]])
self.assertEqual(code, 2, out)
self.assert_one_json_line(raw)
self.assertEqual(out["error"], "platform not supported")
self.assertIn("not tested on linux, WSL", out["reason"])
self.assertEqual(out["platform"]["supported"], False)
after = gremlins.read_registry(self.state)
self.assertEqual(after["jobs"], {})
self.assertEqual(after, before)
with mock.patch.object(gremlins, "read_registry", side_effect=AssertionError("tick read the registry")):
self.assertEqual(gremlins.tick_pass(self.state), {"pinged": [], "bases": []})
# list and wait still work on a closed gate
self.assertEqual(call(["list", "--run", self.run_log])[0], 0)
code, out, _ = call(["wait", "--run", self.run_log, "--all", "--timeout", "1"])
self.assertEqual((code, out["pings"]), (0, []))
# --------------------------------------------------------------------------------------
@unittest.skipUnless(shutil.which("uv"), "uv not on PATH")
class SubprocessTests(Base):