Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
268 lines
13 KiB
Python
268 lines
13 KiB
Python
import hashlib
|
|
import json
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock, patch
|
|
|
|
from wbv2_local_preparation import prepare_local_contracts
|
|
from wbv2_task_dispatch import input_signature
|
|
|
|
|
|
class LocalPreparationTests(unittest.TestCase):
|
|
def setUp(self):
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
self.root = Path(temporary.name)
|
|
(self.root / "my-tools").mkdir()
|
|
(self.root / "parent.json").write_text("{}")
|
|
self.context = {"task": "TASK", "parentSha256": "a" * 64, "parentManifest": "parent.json"}
|
|
self.jobs = [{"id": "MESH-EXTENDED-AUTHORING", "ownedPaths": []}]
|
|
self.script = self.root / "my-tools/prepare_wbv2_mesh_extended_workflow.py"
|
|
self.script.write_text('JOB = "MESH-EXTENDED-AUTHORING"\n')
|
|
self.runner = SimpleNamespace(root=self.root, directory=self.root / "control", stop_requested=False,
|
|
policy={"checkTimeoutSeconds": 120}, state={"jobs": {}},
|
|
local_contract_path=Mock(return_value=None), event=Mock(),
|
|
run_process=Mock(return_value={"exitCode": 0, "interrupted": False,
|
|
"timedOut": False, "logLimit": False}))
|
|
authority = patch("wbv2_autopilot.queue_context", return_value=self.context)
|
|
self.authority_patch = authority
|
|
self.authority = authority.start()
|
|
self.addCleanup(authority.stop)
|
|
admission = patch("wbv2_task_dispatch.prepared_contract", return_value=({"result": {}}, []))
|
|
self.admission_patch = admission
|
|
self.admission = admission.start()
|
|
self.addCleanup(admission.stop)
|
|
|
|
def prepare(self):
|
|
return prepare_local_contracts(self.runner, self.context, self.jobs)
|
|
|
|
def test_literal_job_matches_existing_nonstandard_filename(self):
|
|
self.assertEqual([self.jobs[0]["id"]], self.prepare())
|
|
self.assertEqual(["python3", str(self.script.relative_to(self.root))],
|
|
self.runner.run_process.call_args.args[0])
|
|
|
|
def test_failure_is_not_retried_until_input_changes(self):
|
|
self.runner.run_process.return_value["exitCode"] = 1
|
|
self.assertEqual([], self.prepare())
|
|
self.assertEqual([], self.prepare())
|
|
self.assertEqual(1, self.runner.run_process.call_count)
|
|
self.script.write_text(self.script.read_text() + "# repair\n")
|
|
self.prepare()
|
|
self.assertEqual(2, self.runner.run_process.call_count)
|
|
|
|
def test_dependency_change_wakes_and_retries_failed_preparation(self):
|
|
dependency = self.root / "input.json"
|
|
dependency.write_text("1")
|
|
self.jobs[0]["ownedPaths"] = ["input.json"]
|
|
self.runner.run_process.return_value["exitCode"] = 1
|
|
self.prepare()
|
|
before = input_signature(self.runner, self.context, self.jobs)
|
|
dependency.write_text("2")
|
|
self.assertNotEqual(before, input_signature(self.runner, self.context, self.jobs))
|
|
self.prepare()
|
|
self.assertEqual(2, self.runner.run_process.call_count)
|
|
|
|
def test_new_recipe_and_edit_wake_waiter(self):
|
|
self.script.unlink()
|
|
before = input_signature(self.runner, self.context, self.jobs)
|
|
self.script.write_text('JOB = "MESH-EXTENDED-AUTHORING"\n')
|
|
added = input_signature(self.runner, self.context, self.jobs)
|
|
self.assertNotEqual(before, added)
|
|
self.script.write_text(self.script.read_text() + "# changed\n")
|
|
self.assertNotEqual(added, input_signature(self.runner, self.context, self.jobs))
|
|
|
|
def test_declared_dependency_change_wakes_and_retries(self):
|
|
self.script.write_text(self.script.read_text() + 'PREPARATION_INPUTS = ["fixture.json"]\n')
|
|
dependency = self.root / "fixture.json"
|
|
dependency.write_text("1")
|
|
self.runner.run_process.return_value["exitCode"] = 1
|
|
self.prepare()
|
|
before = input_signature(self.runner, self.context, self.jobs)
|
|
dependency.write_text("2")
|
|
self.assertNotEqual(before, input_signature(self.runner, self.context, self.jobs))
|
|
self.prepare()
|
|
self.assertEqual(2, self.runner.run_process.call_count)
|
|
|
|
def test_existing_contract_is_never_overwritten(self):
|
|
path = self.root / "my-tools/local-workflows/MESH-EXTENDED-AUTHORING.json"
|
|
path.parent.mkdir()
|
|
path.write_text('{"status":"being-edited"}')
|
|
self.assertEqual([], self.prepare())
|
|
self.runner.run_process.assert_not_called()
|
|
|
|
def test_invalid_generated_contract_is_not_prepared(self):
|
|
self.admission.return_value = None, [{"reason": "parent mismatch"}]
|
|
self.assertEqual([], self.prepare())
|
|
self.prepare()
|
|
self.assertEqual(1, self.runner.run_process.call_count)
|
|
record = json.loads(next((self.runner.directory / "preparation-attempts").glob("*.json")).read_text())
|
|
self.assertEqual("blocked", record["state"])
|
|
self.assertGreaterEqual(record["seconds"], 0)
|
|
|
|
def real_recipe(self, payload=None):
|
|
output = f"my-tools/local-workflows/{self.jobs[0]['id']}.json"
|
|
value = "Path('recipe-input.json').read_text()" if payload is None else repr(payload)
|
|
self.script.write_text(
|
|
f"JOB = {self.jobs[0]['id']!r}\n"
|
|
"PREPARATION_INPUTS = ['recipe-input.json']\n"
|
|
"from pathlib import Path\n"
|
|
f"output = Path({output!r})\n"
|
|
"output.parent.mkdir(parents=True, exist_ok=True)\n"
|
|
"assert not output.exists(), 'existing output must be protected'\n"
|
|
f"output.write_text({value})\n"
|
|
)
|
|
return self.root / output
|
|
|
|
def use_real_preparation(self):
|
|
self.admission_patch.stop()
|
|
|
|
def execute(argv, cwd, log, timeout, **kwargs):
|
|
proc = subprocess.run(argv, cwd=cwd, capture_output=True, timeout=timeout)
|
|
log.parent.mkdir(parents=True, exist_ok=True)
|
|
log.write_bytes(proc.stdout + proc.stderr)
|
|
return {"exitCode": proc.returncode, "interrupted": False,
|
|
"timedOut": False, "logLimit": False}
|
|
|
|
self.runner.run_process.side_effect = execute
|
|
|
|
def valid_preparation_payload(self):
|
|
return json.dumps({
|
|
"task": self.context["task"], "job": self.jobs[0]["id"],
|
|
"parentSha256": self.context["parentSha256"],
|
|
"result": {
|
|
"status": "ready", "workflow": "Local preparation admission fixture",
|
|
"artifacts": [], "changedFiles": [], "dependencies": [],
|
|
"routeIds": [], "sourceReferences": [], "sourceUnits": [],
|
|
"checks": [{"id": "focused"}], "cases": [{"id": "positive"}],
|
|
},
|
|
})
|
|
|
|
def test_owned_invalid_json_retries_after_recipe_repair(self):
|
|
self.use_real_preparation()
|
|
origin = self.real_recipe("{")
|
|
self.assertEqual([], self.prepare())
|
|
record_path = self.runner.directory / "preparation-attempts" / f"{self.jobs[0]['id']}.json"
|
|
record = json.loads(record_path.read_text())
|
|
self.assertEqual("blocked", record["state"])
|
|
self.assertEqual({"path": str(origin.relative_to(self.root)),
|
|
"sha256": hashlib.sha256(origin.read_bytes()).hexdigest()},
|
|
record["generatedOrigin"])
|
|
self.assertEqual([], self.prepare())
|
|
self.assertEqual(1, self.runner.run_process.call_count)
|
|
payload = self.valid_preparation_payload()
|
|
self.real_recipe(payload)
|
|
self.assertEqual([self.jobs[0]["id"]], self.prepare())
|
|
self.assertEqual(payload, origin.read_text())
|
|
self.assertEqual(2, self.runner.run_process.call_count)
|
|
self.assertEqual("prepared", json.loads(record_path.read_text())["state"])
|
|
self.assertEqual([], self.prepare())
|
|
self.assertEqual(2, self.runner.run_process.call_count)
|
|
|
|
def test_owned_invalid_json_retries_after_declared_input_repair(self):
|
|
self.use_real_preparation()
|
|
dependency = self.root / "recipe-input.json"
|
|
dependency.write_text("{")
|
|
origin = self.real_recipe()
|
|
self.assertEqual([], self.prepare())
|
|
self.assertEqual([], self.prepare())
|
|
self.assertEqual(1, self.runner.run_process.call_count)
|
|
dependency.write_text(self.valid_preparation_payload())
|
|
self.assertEqual([self.jobs[0]["id"]], self.prepare())
|
|
self.assertEqual(dependency.read_text(), origin.read_text())
|
|
self.assertEqual(2, self.runner.run_process.call_count)
|
|
|
|
def test_owned_failed_output_edited_by_user_is_not_overwritten(self):
|
|
self.use_real_preparation()
|
|
origin = self.real_recipe("{")
|
|
self.assertEqual([], self.prepare())
|
|
origin.write_text('{"manual": true}')
|
|
self.real_recipe(self.valid_preparation_payload())
|
|
self.assertEqual([], self.prepare())
|
|
self.assertEqual('{"manual": true}', origin.read_text())
|
|
self.assertEqual(1, self.runner.run_process.call_count)
|
|
|
|
def test_preexisting_output_is_protected_after_recipe_change(self):
|
|
self.use_real_preparation()
|
|
origin = self.real_recipe("{")
|
|
origin.parent.mkdir(parents=True)
|
|
origin.write_text('{"manual": true}')
|
|
self.assertEqual([], self.prepare())
|
|
self.real_recipe(self.valid_preparation_payload())
|
|
self.assertEqual([], self.prepare())
|
|
self.assertEqual('{"manual": true}', origin.read_text())
|
|
self.runner.run_process.assert_not_called()
|
|
|
|
def test_failed_output_without_ownership_record_is_not_overwritten(self):
|
|
self.use_real_preparation()
|
|
origin = self.real_recipe("{")
|
|
self.assertEqual([], self.prepare())
|
|
record_path = self.runner.directory / "preparation-attempts" / f"{self.jobs[0]['id']}.json"
|
|
record = json.loads(record_path.read_text())
|
|
del record["generatedOrigin"]
|
|
record_path.write_text(json.dumps(record))
|
|
self.real_recipe(self.valid_preparation_payload())
|
|
self.assertEqual([], self.prepare())
|
|
self.assertEqual("{", origin.read_text())
|
|
self.assertEqual(1, self.runner.run_process.call_count)
|
|
|
|
def test_conflicting_recipes_do_not_execute(self):
|
|
(self.root / "my-tools/prepare_wbv2_duplicate_workflow.py").write_text(self.script.read_text())
|
|
self.assertEqual([], self.prepare())
|
|
self.runner.run_process.assert_not_called()
|
|
|
|
def test_symlink_and_computed_job_are_not_executed(self):
|
|
outside = self.root / "other.py"
|
|
outside.write_text(self.script.read_text())
|
|
self.script.unlink()
|
|
self.script.symlink_to(outside)
|
|
self.assertEqual([], self.prepare())
|
|
self.script.unlink()
|
|
self.script.write_text('JOB = "MESH" + "-EXTENDED-AUTHORING"\n')
|
|
self.assertEqual([], self.prepare())
|
|
self.runner.run_process.assert_not_called()
|
|
|
|
def test_authority_change_interrupts_before_publication(self):
|
|
self.authority.side_effect = [self.context, {**self.context, "task": "OTHER"}]
|
|
with self.assertRaises(InterruptedError):
|
|
self.prepare()
|
|
self.admission.assert_not_called()
|
|
|
|
def test_independent_job_continues_after_preparation_failure(self):
|
|
self.jobs.append({"id": "SECOND", "ownedPaths": []})
|
|
(self.root / "my-tools/prepare_wbv2_second_workflow.py").write_text('JOB = "SECOND"\n')
|
|
self.runner.run_process.side_effect = [{"exitCode": 1}, {"exitCode": 0}]
|
|
self.assertEqual(["SECOND"], self.prepare())
|
|
|
|
def test_preparation_is_admitted_and_verified_in_the_same_cycle(self):
|
|
from test_wbv2_task_dispatch import AutoDispatchTests
|
|
self.authority_patch.stop()
|
|
self.admission_patch.stop()
|
|
fixture = AutoDispatchTests()
|
|
fixture.setUp()
|
|
try:
|
|
jobs = fixture.jobs(1)
|
|
job = jobs[0]
|
|
(fixture.root / job["resultPath"]).replace(fixture.root / "recipe-input.json")
|
|
fixture.file("my-tools/prepare_wbv2_seed_workflow.py", (
|
|
f"JOB = {job['id']!r}\n"
|
|
"PREPARATION_INPUTS = ['recipe-input.json']\n"
|
|
"from pathlib import Path\n"
|
|
"import shutil\n"
|
|
"target = Path('my-tools/local-workflows') / (JOB + '.json')\n"
|
|
"target.parent.mkdir(parents=True, exist_ok=True)\n"
|
|
"shutil.copyfile('recipe-input.json', target)\n"
|
|
))
|
|
self.assertEqual(0, fixture.run_local(1))
|
|
self.assertEqual("verified", fixture.runner.state["jobs"][job["id"]]["state"])
|
|
attempt = json.loads((fixture.runner.directory / "preparation-attempts" / f"{job['id']}.json").read_text())
|
|
self.assertEqual("prepared", attempt["state"])
|
|
finally:
|
|
fixture.doCleanups()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|