vignette/scripts/test_run_periodic_learner_e2e.py

360 lines
14 KiB
Python

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 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 '<div id="root"></div>'
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()