Files
workinf_Blender_Wasm/my-tools/test_wbv2_local_preparation.py
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
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>
2026-11-10 11:18:01 +08:00

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()