from __future__ import annotations import importlib.util import json import sys import tempfile import unittest from pathlib import Path from types import ModuleType from unittest.mock import patch SCRIPT = Path(__file__).with_name("run-periodic-learner-e2e.py") def load_module() -> ModuleType: spec = importlib.util.spec_from_file_location("run_periodic_learner_e2e", SCRIPT) if spec is None or spec.loader is None: raise RuntimeError("cannot load periodic learner runner") module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module runner = load_module() class PeriodicRunnerStaticContractTests(unittest.TestCase): def test_browser_failures_are_not_retried_automatically(self) -> None: self.assertEqual(runner.PLAYWRIGHT_RETRIES, 0) class FakeController: def __init__(self, *, tracked_status: str = "") -> None: self.tracked_status = tracked_status self.commands: list[tuple[str, list[str]]] = [] self.starts: list[tuple[str, list[str]]] = [] self.stops: list[int] = [] def run( self, stage, argv, *, cwd, env=None, timeout, check=True, ): del cwd, env, timeout, check self.commands.append((stage, list(argv))) stdout = "" if stage == "source_head": stdout = "1" * 40 + "\n" elif stage == "source_tree": stdout = "2" * 40 + "\n" elif stage == "source_tracked_clean": stdout = self.tracked_status elif stage == "source_untracked_inventory": stdout = "?? apps/api/engine.err.log.bak\n" elif stage == "docker_context_show": stdout = "desktop-linux\n" elif stage == "docker_context_inspect": stdout = json.dumps( [ { "Endpoints": { "docker": { "Host": "npipe:////./pipe/dockerDesktopLinuxEngine" } } } ] ) return runner.CommandResult(0, stdout, "", 0.01) def start( self, stage, argv, *, cwd, env, stdout_path, stderr_path, ): del cwd, env, stdout_path, stderr_path self.starts.append((stage, list(argv))) return runner.ProcessHandle(4242) def stop_exact(self, handle, *, timeout): del timeout self.stops.append(handle.pid) return True def http_json(self, url, *, headers=None, timeout): del url, headers, timeout return {"ok": True, "status": "ok", "db": True, "engine": True} def http_text(self, url, *, timeout): del url, timeout return '
' def tcp_listening(self, host, port, *, timeout=0.25): del host, port, timeout return False def sleep(self, seconds): del seconds def config(receipt_path: Path) -> runner.RunnerConfig: return runner.RunnerConfig( receipt_path=receipt_path, python_exe="python.exe", node_exe="node.exe", docker_exe="docker.exe", execute=True, ) class SafetyContractTests(unittest.TestCase): def test_rejects_every_protected_port_from_argv_and_runtime_env(self) -> None: for port in sorted(runner.FORBIDDEN_PORTS): with self.subTest(port=port): with self.assertRaises(runner.GateError): runner.assert_safe_invocation([f"http://127.0.0.1:{port}"], {}) with self.assertRaises(runner.GateError): runner.assert_safe_invocation([], {"DATABASE_URL": f"postgresql://x@127.0.0.1:{port}/db"}) def test_rejects_public_and_nas_markers(self) -> None: values = [ "https://vignette.chanpaca.net", "https://api-vignette.chanpaca.net/health", "postgresql://app@100.116.83.60:55433/vignette", "vignette-preview-20260807", "vignette-dev-db", "/volume1/docker/vignette", ] for value in values: with self.subTest(value=value), self.assertRaises(runner.GateError): runner.assert_safe_value("contract", value) def test_rejects_inherited_runtime_targets_even_when_the_port_looks_local(self) -> None: for key, value in ( ("DATABASE_URL", "postgresql://app@127.0.0.1:55439/vignette"), ("ENGINE_URL", "http://127.0.0.1:9199"), ("PLAYWRIGHT_BASE_URL", "http://127.0.0.1:5199"), ("COMPOSE_PROJECT_NAME", "some-existing-project"), ): with self.subTest(key=key), self.assertRaises(runner.GateError): runner.assert_no_inherited_runtime_targets({key: value}) def test_runtime_identity_and_generated_environment_are_disposable(self) -> None: source = runner.SourceIdentity("1" * 40, "2" * 40) with patch.object(runner, "allocate_unique_ports", return_value=(18080, 18443, 15439, 19199)): runtime = runner.build_runtime_identity(source) runner.validate_runtime_identity(runtime) values = runner.build_stack_environment(runtime) self.assertEqual(len(set(runtime.ports)), 4) self.assertTrue(runner.PROJECT_RE.fullmatch(runtime.project)) self.assertEqual(values["HTTP_PORT"], "18080") self.assertEqual(values["DB_HOST_PORT"], "15439") self.assertEqual(values["ENGINE_URL"], "http://host.docker.internal:19199") self.assertNotIn("vignette.chanpaca.net", json.dumps(values)) self.assertFalse(set(runtime.ports) & runner.FORBIDDEN_PORTS) def test_compose_override_labels_every_resource_and_loopback_binds_database(self) -> None: with tempfile.TemporaryDirectory() as raw: path = Path(raw) / "override.yml" runner.write_compose_override(path) text = path.read_text(encoding="utf-8") self.assertEqual(text.count("com.vignette.periodic-e2e-sentinel"), 8) self.assertIn('127.0.0.1:${DB_HOST_PORT}:5432', text) self.assertIn("pgdata:", text) self.assertIn("apiuploads:", text) self.assertIn("caddydata:", text) self.assertIn("vignette:", text) def test_resolved_compose_contract_binds_only_loopback_and_internal_db(self) -> None: runtime = runner.RuntimeIdentity( "20260809T120000-abcdef12", "vignette-periodic-11111111-abcdef12", "periodic:" + "1" * 40 + ":" + "2" * 40 + ":run", 18080, 18443, 15439, 19199, ) stack_env = { "POSTGRES_DB": "vignette_periodic", } payload = { "services": { name: { "labels": { "com.vignette.periodic-e2e-sentinel": runtime.sentinel }, "ports": [], } for name in ("db", "api", "web", "proxy") }, "volumes": { name: { "labels": { "com.vignette.periodic-e2e-sentinel": runtime.sentinel } } for name in ("pgdata", "apiuploads", "caddydata") }, "networks": { "vignette": { "labels": { "com.vignette.periodic-e2e-sentinel": runtime.sentinel } } }, } payload["services"]["db"]["ports"] = [ {"host_ip": "127.0.0.1", "target": 5432, "published": "15439"} ] payload["services"]["proxy"]["ports"] = [ {"host_ip": "127.0.0.1", "target": 80, "published": "18080"}, {"host_ip": "127.0.0.1", "target": 443, "published": "18443"}, ] payload["services"]["api"]["environment"] = { "DATABASE_URL": "postgresql://app:secret@db:5432/vignette_periodic", "ENGINE_URL": "http://host.docker.internal:19199", "FRONTEND_BASE_URL": "http://127.0.0.1:18080", "OAUTH_REDIRECT_URI": "http://127.0.0.1:18080/api/auth/callback", } class ConfigFake(FakeController): def run(self, stage, argv, **kwargs): result = super().run(stage, argv, **kwargs) if stage == "resolved_compose_config": return runner.CommandResult(0, json.dumps(payload), "", 0.01) return result fake = ConfigFake() with tempfile.TemporaryDirectory() as raw: periodic = runner.PeriodicRunner(config(Path(raw) / "receipt.json"), fake) periodic.state.runtime = runtime periodic.state.docker_context = "desktop-linux" periodic.state.env_file = Path(raw) / "stack.env" periodic.state.override_file = Path(raw) / "override.yml" digest = periodic._validate_resolved_compose_config(stack_env) self.assertEqual(len(digest), 64) payload["services"]["proxy"]["ports"][0]["host_ip"] = "0.0.0.0" with tempfile.TemporaryDirectory() as raw: periodic = runner.PeriodicRunner(config(Path(raw) / "receipt.json"), ConfigFake()) periodic.state.runtime = runtime periodic.state.docker_context = "desktop-linux" periodic.state.env_file = Path(raw) / "stack.env" periodic.state.override_file = Path(raw) / "override.yml" with self.assertRaises(runner.GateError): periodic._validate_resolved_compose_config(stack_env) def test_dirty_source_fails_before_process_or_compose_mutation(self) -> None: fake = FakeController(tracked_status=" M apps/web/src/App.tsx\n") with tempfile.TemporaryDirectory() as raw: receipt = Path(raw) / "receipt.json" execution = runner.PeriodicRunner(config(receipt), fake).execute() stored = json.loads(receipt.read_text(encoding="utf-8")) self.assertEqual(execution["status"], "FAILED") self.assertEqual(stored["status"], "FAILED") self.assertIn("tracked worktree differs", execution["error"]) self.assertEqual(fake.starts, []) self.assertFalse(any("compose" in command for _, command in fake.commands)) def test_docker_context_is_pinned_to_local_windows_named_pipe(self) -> None: fake = FakeController() periodic = runner.PeriodicRunner(config(Path("receipt.json")), fake) proof = periodic._pin_local_docker_context() self.assertEqual(proof, {"context": "desktop-linux", "transport": "npipe", "remote": False}) self.assertEqual(periodic.state.docker_context, "desktop-linux") class RemoteDockerFake(FakeController): def run(self, stage, argv, **kwargs): result = super().run(stage, argv, **kwargs) if stage == "docker_context_inspect": return runner.CommandResult( 0, json.dumps( [ { "Endpoints": { "docker": {"Host": "tcp://127.0.0.1:2375"} } } ] ), "", 0.01, ) return result with self.assertRaises(runner.GateError): runner.PeriodicRunner( config(Path("receipt.json")), RemoteDockerFake() )._pin_local_docker_context() def test_cleanup_targets_only_exact_project_and_proves_listener_zero(self) -> None: fake = FakeController() with tempfile.TemporaryDirectory() as raw: periodic = runner.PeriodicRunner(config(Path(raw) / "receipt.json"), fake) periodic.state.source = runner.SourceIdentity("1" * 40, "2" * 40) periodic.state.runtime = runner.RuntimeIdentity( "20260809T120000-abcdef12", "vignette-periodic-11111111-abcdef12", "periodic:" + "1" * 40 + ":" + "2" * 40 + ":run", 18080, 18443, 15439, 19199, ) periodic.state.docker_context = "desktop-linux" periodic.state.env_file = Path(raw) / "stack.env" periodic.state.override_file = Path(raw) / "override.yml" periodic.state.stack_attempted = True periodic.state.engine = runner.ProcessHandle(4242) proof = periodic._cleanup() down = next(command for stage, command in fake.commands if stage == "compose_down") self.assertIn("vignette-periodic-11111111-abcdef12", down) self.assertIn("--remove-orphans", down) self.assertIn("--volumes", down) self.assertEqual(fake.stops, [4242]) self.assertEqual(proof["container_remainder"], 0) self.assertEqual(proof["volume_remainder"], 0) self.assertEqual(proof["network_remainder"], 0) self.assertEqual(set(proof["listener_counts"].values()), {0}) self.assertTrue(periodic._cleanup_green(proof)) def test_untracked_runtime_source_is_rejected_but_log_backup_is_not(self) -> None: fake = FakeController() periodic = runner.PeriodicRunner(config(Path("receipt.json")), fake) identity = periodic.source_identity(require_clean=True) self.assertEqual(identity.head, "1" * 40) class DangerousFake(FakeController): def run(self, stage, argv, **kwargs): result = super().run(stage, argv, **kwargs) if stage == "source_untracked_inventory": return runner.CommandResult( 0, "?? apps/web/e2e/uncommitted-runtime.spec.ts\n", "", 0.01, ) return result with self.assertRaises(runner.GateError): runner.PeriodicRunner( config(Path("receipt.json")), DangerousFake() ).source_identity(require_clean=True) if __name__ == "__main__": unittest.main()