Files
workinf_Blender_Wasm/my-tools/test_wbv2_task_dispatch.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

233 lines
12 KiB
Python

"""Automatic local intake and sequential dispatch using synthetic case receipts."""
import contextlib
import copy
import io
import json
import os
import threading
import time
import unittest
from unittest.mock import patch
import test_wbv2_local as fixtures
from wbv2_autopilot import POLICY, queue_context, sha, write_json
from wbv2_task_dispatch import TaskDispatch, input_signature, prepared_contract
class CandidateWakeupTests(unittest.TestCase):
def setUp(self):
import tempfile
from pathlib import Path
from types import SimpleNamespace
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
self.runner = SimpleNamespace(root=self.root, state={"jobs": {}})
self.context = {"parentManifest": "tests/golden/parent/manifest.json"}
self.job = {"id": "AUDIT", "ownedPaths": []}
self.candidate = self.root / "web/tests/e2e/audit.spec.ts"
self.candidate.parent.mkdir(parents=True)
self.candidate.write_text("before")
self.report = self.root / "tests/golden/WBV2/autopilot/preparation/AUDIT.json"
self.report.parent.mkdir(parents=True)
self.report.write_text(json.dumps({"job": "AUDIT", "candidateTests": ["web/tests/e2e/audit.spec.ts"]}))
def signature(self):
return input_signature(self.runner, self.context, [self.job])
def test_editing_existing_candidate_changes_signature_without_directory_change(self):
previous_time = self.candidate.stat().st_mtime_ns
directory_time = self.candidate.parent.stat().st_mtime_ns
first = self.signature()
self.candidate.write_text("after!")
os.utime(self.candidate, ns=(previous_time + 1000000000, previous_time + 1000000000))
self.assertEqual(directory_time, self.candidate.parent.stat().st_mtime_ns)
self.assertNotEqual(first, self.signature())
self.assertEqual(self.signature(), self.signature())
def test_unrelated_and_unsafe_candidate_edits_do_not_change_signature(self):
outside = self.root / "outside.spec.ts"
outside.write_text("before")
unrelated = self.candidate.with_name("unrelated.spec.ts")
unrelated.write_text("before")
linked = self.candidate.with_name("linked.spec.ts")
linked.symlink_to(outside)
self.report.write_text(json.dumps({"job": "AUDIT", "candidateTests": [
"web/tests/e2e/audit.spec.ts", "../outside.spec.ts", str(outside),
"outside.spec.ts", "web/tests/e2e/linked.spec.ts"]}))
first = self.signature()
outside.write_text("outside changed")
unrelated.write_text("unrelated changed")
self.assertEqual(first, self.signature())
def test_invalid_or_symlinked_preparation_report_is_ignored(self):
from wbv2_preparation import preparation_test_paths
self.report.write_text(json.dumps({"job": "OTHER", "candidateTests": ["web/tests/e2e/audit.spec.ts"]}))
self.assertEqual(preparation_test_paths(self.root, self.job), ())
self.report.write_text(" " * 131073)
self.assertEqual(preparation_test_paths(self.root, self.job), ())
self.report.unlink()
self.report.symlink_to(self.candidate)
self.assertEqual(preparation_test_paths(self.root, self.job), ())
self.assertEqual(preparation_test_paths(self.root, {"id": "../../outside"}), ())
class AutoDispatchTests(unittest.TestCase):
setUp = fixtures.LocalWorkflowTests.setUp
file = fixtures.LocalWorkflowTests.file
ready_result = fixtures.LocalWorkflowTests.ready_result
def configure(self):
self.policy.update(modelExecution="disabled", localWorkflows={}, preflight=[], priority=[], localPollSeconds=5)
write_json(self.root / POLICY, self.policy)
self.file("my-tools/wbv2_validation.py", "# synthetic module\n")
def jobs(self, count=2):
self.configure()
jobs = []
result = self.ready_result(self.runner.directory / "prepared")
for index in range(count):
job = {"id": "LOCAL-" + str(index), "ownedPaths": ["source.cpp"], "resultPath": f"prepared-{index}.json"}
write_json(self.root / job["resultPath"], copy.deepcopy(result))
jobs.append(job)
write_json(self.root / "tests/golden/WBV2/web-product-backlog.json", {"queueTask": self.task, "tasks": jobs})
return jobs
def run_local(self, cycles):
with patch.object(self.runner, "remember_baseline"), patch.object(self.runner, "worker", side_effect=AssertionError("no model allowed")), contextlib.redirect_stdout(io.StringIO()):
return self.runner.run(cycles)
def test_two_prepared_jobs_are_automatically_imported_claimed_and_verified(self):
jobs = self.jobs()
jobs[0]["dependsOn"] = [jobs[1]["id"]]
write_json(self.root / "tests/golden/WBV2/web-product-backlog.json", {"queueTask": self.task, "tasks": jobs})
self.assertEqual(0, self.run_local(2))
for job in jobs:
record = self.runner.state["jobs"][job["id"]]
self.assertEqual("verified", record["state"])
self.assertTrue(self.runner.receipt_valid(record))
claim = json.loads((self.runner.directory / "claims" / (job["id"] + ".json")).read_text())
self.assertEqual("verified", claim["state"])
self.assertEqual(self.context["parentSha256"], claim["parentSha256"])
events = [json.loads(line) for line in (self.runner.directory / "events.jsonl").read_text().splitlines()]
self.assertEqual([jobs[1]["id"], jobs[0]["id"]], [event["job"] for event in events if event["event"] == "task.claimed"])
self.assertEqual(0, self.runner.state["tokensUsed"])
self.assertEqual(self.task, queue_context(self.root)["task"])
def test_changed_failed_input_is_retried_automatically(self):
jobs = self.jobs(1)
original = (self.root / "check.py").read_text()
self.file("check.py", "raise SystemExit(1)\n")
self.assertEqual(2, self.run_local(1))
self.assertEqual("blocked", self.runner.state["jobs"][jobs[0]["id"]]["state"])
# An active daemon reaches preflight on its next polling cycle.
self.runner.state["state"] = "waiting_local_work"
self.file("check.py", original)
self.assertEqual(0, self.run_local(1))
self.assertEqual("verified", self.runner.state["jobs"][jobs[0]["id"]]["state"])
def test_invalid_contract_does_not_prevent_next_independent_job(self):
jobs = self.jobs()
destination = self.runner.directory / "local-plans" / (jobs[0]["id"] + ".json")
write_json(destination, {"task": self.task, "job": jobs[0]["id"], "parentSha256": "0" * 64})
self.assertEqual(0, self.run_local(2))
self.assertEqual("blocked", self.runner.state["jobs"][jobs[0]["id"]]["state"])
self.assertEqual("verified", self.runner.state["jobs"][jobs[1]["id"]]["state"])
def test_incomplete_implemented_metadata_is_not_executable(self):
jobs = self.jobs(1)
write_json(self.root / jobs[0]["resultPath"], {"status": "implemented", "changedFiles": ["source.cpp"]})
contract, findings = prepared_contract(self.root, self.context, jobs[0])
self.assertIsNone(contract)
self.assertIn("checks", findings[0]["missingFields"])
self.assertIn("cases", findings[0]["missingFields"])
def test_dependency_cycles_and_unknown_dependencies_are_not_selected(self):
jobs = self.jobs()
jobs[0]["dependsOn"] = [jobs[1]["id"]]
jobs[1]["dependsOn"] = [jobs[0]["id"]]
self.assertEqual("task dependency cycle", TaskDispatch(self.runner).dependency_issue(jobs[0], jobs))
jobs[0]["dependsOn"] = ["MISSING"]
self.assertIn("unknown task dependency", TaskDispatch(self.runner).dependency_issue(jobs[0], jobs))
def test_queue_change_prevents_claim(self):
jobs = self.jobs(1)
write_json(self.root / self.parent, {"status": "done", "nextTask": "OTHER"})
with self.assertRaises((InterruptedError, ValueError)):
TaskDispatch(self.runner).claim(self.context, jobs[0], jobs)
self.assertFalse((self.runner.directory / "claims").exists())
def test_new_contract_wakes_wait_without_full_poll_delay(self):
jobs = self.jobs(1)
self.runner.state.update(state="waiting_local_work", jobs={})
before = input_signature(self.runner, self.context, jobs)
destination = self.root / "my-tools/local-workflows" / (jobs[0]["id"] + ".json")
timer = threading.Timer(0.05, lambda: write_json(destination, {"status": "ready"}))
timer.start()
try:
started = time.monotonic()
with contextlib.redirect_stdout(io.StringIO()):
self.runner.wait_for_local_work(self.context, jobs)
self.assertLess(time.monotonic() - started, 2)
finally:
timer.join()
self.assertNotEqual(before, input_signature(self.runner, self.context, jobs))
def test_wrapped_contract_requires_exact_authority(self):
jobs = self.jobs(1)
result = json.loads((self.root / jobs[0]["resultPath"]).read_text())
write_json(self.root / jobs[0]["resultPath"], {"task": self.task, "job": jobs[0]["id"], "parentSha256": "0" * 64, "result": result})
contract, findings = prepared_contract(self.root, self.context, jobs[0])
self.assertIsNone(contract)
self.assertIn("mismatch", findings[0]["reason"])
def interrupted_fixture(self):
jobs = self.jobs(1)
contract, _ = prepared_contract(self.root, self.context, jobs[0])
relative = "tests/golden/WBV2/autopilot/local-plans/LOCAL-0.json"
write_json(self.root / relative, contract)
directory = self.runner.directory / "runs/LOCAL-0-interrupted"
with contextlib.redirect_stdout(io.StringIO()):
protected = self.runner.protected_hashes(self.context)
write_json(directory / "before-worker.json", {"protected": protected, "owned": {"source.cpp": sha(self.root / "source.cpp")}})
self.runner.state.update(state="validating", pid=900001, identity="old", executionMode="local_only", activeJob="LOCAL-0", jobs={"LOCAL-0": {"state": "validating", "runDirectory": str(directory.relative_to(self.root)), "localInputs": {"check.py": sha(self.root / "check.py")}}})
write_json(self.runner.directory / "claims/LOCAL-0.json", {"state": "claimed", "ownerPid": 900001, "task": self.task, "job": "LOCAL-0", "parentSha256": self.context["parentSha256"], "contract": relative, "contractSha256": sha(self.root / relative)})
return jobs
def test_dead_local_writer_recovers_when_all_bindings_are_unchanged(self):
self.interrupted_fixture()
with patch("wbv2_autopilot.alive", return_value=False), contextlib.redirect_stdout(io.StringIO()):
self.assertTrue(TaskDispatch(self.runner).recover())
self.assertEqual("idle", self.runner.state["state"])
self.assertEqual("retry_pending", self.runner.state["jobs"]["LOCAL-0"]["state"])
def test_explicit_pause_or_input_drift_prevents_recovery(self):
self.interrupted_fixture()
(self.runner.directory / "pause.request").touch()
self.assertFalse(TaskDispatch(self.runner).recover())
(self.runner.directory / "pause.request").unlink()
self.file("source.cpp", "changed locally\n")
with patch("wbv2_autopilot.alive", return_value=False):
self.assertFalse(TaskDispatch(self.runner).recover())
def test_surviving_process_group_prevents_recovery(self):
self.interrupted_fixture()
self.runner.state.update(childPid=900002, childIdentity="old-child")
with patch("wbv2_autopilot.alive", return_value=False), patch("wbv2_task_dispatch.os.killpg", return_value=None):
self.assertFalse(TaskDispatch(self.runner).recover())
def test_recovery_rejects_an_unowned_or_wrong_parent_claim(self):
self.interrupted_fixture()
path = self.runner.directory / "claims/LOCAL-0.json"
claim = json.loads(path.read_text())
claim["parentSha256"] = "0" * 64
write_json(path, claim)
with patch("wbv2_autopilot.alive", return_value=False):
self.assertFalse(TaskDispatch(self.runner).recover())
if __name__ == "__main__":
unittest.main()