Files
FreeCADWasmDev/tools/test_acceptance_priority.py
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

281 lines
19 KiB
Python

"""Acceptance must reuse ready work without starving implementation or lying about it."""
import contextlib
import hashlib
import io
import json
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parent))
import business_acceptance as evidence
import prepare_business_acceptance_batch as acceptance
import prepare_implementation_business_batch as planner
import run_freecad_web_scheduler as scheduler
import run_module_batch_pipeline as pipeline
import preflight_current_module_batch as preflight
import continue_web_task_cycle as continuation
class PriorityTests(unittest.TestCase):
def test_coverage_development_survives_feature_and_reconciliation_handoffs(self):
feature = {"tier": "feature-workflow", "outcome": "no_progress"}
refresh = {"tier": "business-acceptance", "outcome": "progress", "fileChanges": 0,
"firstBusinessPassIds": [], "refreshedBusinessIds": ["Web:old"]}
history = [feature, {"tier": "failure-reconciliation", "outcome": "progress"},
{**feature, "outcome": "source_edits_pending_outcome", "fileChanges": 1}, refresh, feature]
self.assertTrue(planner.coverage_development_due(history))
previous = {"selected": {"module": "Assembly", "tier": "feature-workflow"}}
with patch.object(planner, "load", return_value={"history": history}):
self.assertEqual(planner.phase_order(previous, 600)[0],
("P1-uncovered-business-development", ["tools/prepare_business_acceptance_batch.py", "--uncovered-only", "--exclude-module", "Assembly"]))
for reset in ({"businessLane": "development"}, {"firstBusinessPassIds": ["Part:new"]}):
self.assertFalse(planner.coverage_development_due([*history, reset]))
self.assertFalse(planner.coverage_development_due([feature, refresh, {"outcome": "provider_wait"}]))
def test_preserved_feature_yields_to_due_uncovered_development(self):
history = [{"tier": "feature-workflow", "outcome": "no_progress"}] * 3
state = self.state()
state["covered"]["Draft"] = set()
batch = {"schedulingPolicy": planner.POLICY, "selectionScope": "delivery", "selected": {"tier": "feature-workflow"}}
with patch.object(acceptance, "inventory", return_value=state), patch.object(planner, "load", return_value={"history": history}):
self.assertTrue(planner.acceptance_preempts(batch))
def test_missing_acceptance_cases_keep_case_authoring_context_on_resume(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
paths = {key: root / f"{key}.json" for key in ("MODULE_BATCH_PATH", "MODULE_PIPELINE_PATH", "IMPLEMENTATION_PACKET_PATH")}
paths["MODULE_BATCH_PATH"].write_text(json.dumps({"status": "ready", "batchId": "b", "selected": {"tier": "business-acceptance", "module": "Part"}}))
paths["MODULE_PIPELINE_PATH"].write_text('{}')
paths["IMPLEMENTATION_PACKET_PATH"].write_text(json.dumps({"batchId": "b", "acceptanceContract": "contract.json",
"acceptanceSetup": {"ready": False, "missingIds": ["Part:missing"], "caseScripts": ["cases.mjs"]},
"editableExcerpts": [{"path": "cases.mjs", "excerpt": "EXISTING_CASE_CODE"}]}))
with patch.multiple(scheduler, **paths):
first = scheduler.build_prompt("task", failures=[])
resumed = scheduler.build_prompt("task", failures=[], resumed=True)
self.assertIn("EXISTING_CASE_CODE", first)
self.assertIn("Part:missing", resumed)
self.assertIn("cases.mjs", resumed)
self.assertNotIn("status=exposed", resumed)
self.assertNotIn("EXISTING_CASE_CODE", resumed)
def test_uncovered_acceptance_stops_before_build_and_preserves_caches(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "cases.py").write_text("pass\n")
contract = root / "contract.json"
contract.write_text(json.dumps({"command": ["python3", "cases.py"], "coveredIds": ["Part:ready"]}))
batch = {"batchId": "b", "selected": {"module": "Part", "tier": "business-acceptance"},
"records": [["Part:ready"], ["Part:missing"]]}
packet = {"batchId": "b", "outerPipelineAvailable": True, "acceptanceContract": "contract.json",
"acceptanceSetup": {"ready": True}, "build": {"target": "probe", "tree": "build"}}
paths = {key: root / (key + ".json") for key in ("BATCH_PATH", "PACKET_PATH", "OUTPUT_PATH", "BUILD_CACHE_PATH", "PENDING_BUILD_PATH")}
paths["BATCH_PATH"].write_text(json.dumps(batch))
paths["PACKET_PATH"].write_text(json.dumps(packet))
for key in ("BUILD_CACHE_PATH", "PENDING_BUILD_PATH"):
paths[key].write_text('{"existing":"preserve"}')
with patch.multiple(pipeline, ROOT=root, **paths), patch.object(pipeline, "run_step") as run, \
patch.object(pipeline.subprocess, "run") as subprocess_run, contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(pipeline.main(), 78)
run.assert_not_called()
subprocess_run.assert_not_called()
result = json.loads(paths["OUTPUT_PATH"].read_text())
self.assertEqual(result["failedStage"], "business-acceptance")
self.assertEqual(json.loads(result["steps"][0]["stdout"])["missingIds"], ["Part:missing"])
self.assertEqual(scheduler.compile_failure_steps(result, "b"), [])
for key in ("BUILD_CACHE_PATH", "PENDING_BUILD_PATH"):
self.assertEqual(paths[key].read_text(), '{"existing":"preserve"}')
with patch.object(pipeline, "ROOT", root):
contract.write_text(json.dumps({"command": ["python3", "cases.py"], "coveredIds": ["Part:ready", "Part:missing"]}))
self.assertIsNone(pipeline.acceptance_contract_gap(batch, packet))
(root / "cases.py").unlink()
self.assertIsNotNone(pipeline.acceptance_contract_gap(batch, packet))
def state(self):
rows = [{"id": "Part:save", "module": "Part", "command": "part.save", "category": "persistence"},
{"id": "Draft:format", "module": "Draft", "command": "draft.format", "category": "format"},
{"id": "Draft:other", "module": "Draft", "command": "draft.other"}]
return {"pending": rows, "rows": rows, "accepted": set(), "cooling": set(), "receipts": {},
"covered": {"Part": set(), "Draft": {"Draft:format"}}}
def test_executable_work_precedes_module_order_and_never_mixes_uncovered_ids(self):
batch = acceptance.plan(self.state())
self.assertEqual([row[0] for row in batch["records"]], ["Draft:format"])
self.assertTrue(batch["acceptanceReady"])
selected = batch["selected"]
parts = preflight.batch_identity_parts(batch, selected["module"], selected["tier"], selected["family"], [row[0] for row in batch["records"]])
self.assertEqual(batch["batchId"], hashlib.sha256("|".join(parts).encode()).hexdigest()[:24])
state = self.state()
state["cooling"] = {"Draft:format"}
self.assertEqual(acceptance.plan(state)["selected"]["module"], "Part")
self.assertEqual(acceptance.candidates(state, covered_only=True), [])
def test_workflow_groups_preserve_document_and_desktop_boundaries(self):
state = self.state()
state["covered"]["Draft"] = set()
state["pending"].extend([
{"id": "Part:import", "module": "Part", "command": "part.import", "category": "format"},
{"id": "Part:desktop:Box", "module": "Part", "command": "Box", "kind": "desktop-gui"},
])
selected = acceptance.plan(state)
self.assertEqual({row[0] for row in selected["records"]}, {"Part:save"})
self.assertEqual(selected["businessWorkflow"], "document-persistence")
self.assertTrue(any(row["family"] == "document-import-export" for row in selected["queue"]))
def test_context_queue_is_bounded_without_discarding_pending_totals(self):
state = self.state()
for index in range(20):
module = f"Module{index}"
state["pending"].append({"id": f"{module}:create", "module": module, "command": "create"})
state["covered"][module] = set()
batch = acceptance.plan(state)
self.assertEqual(len(batch["queue"]), 8)
self.assertEqual(batch["queueGroupCount"], 23)
self.assertEqual(batch["pendingBusinessCount"], 23)
def test_ready_receipt_with_missing_route_is_not_an_acceptance_candidate(self):
rows = [{"id": "Part:good", "module": "Part", "command": "good", "implementationStatus": "routed"},
{"id": "Part:gap", "module": "Part", "command": "gap", "implementationStatus": "implementation_required"}]
receipts = {row["id"]: {"status": "implementation_ready"} for row in rows}
with patch.object(acceptance, "requirements", return_value=rows), patch.object(acceptance, "accepted_ids", return_value=set()), \
patch.object(acceptance, "load_registry", return_value={"records": receipts}), patch.object(acceptance, "cooling_ids", return_value=set()), \
patch.object(acceptance, "contract_ids", return_value=set()):
self.assertEqual([row["id"] for row in acceptance.inventory()["pending"]], ["Part:good"])
def test_executable_first_and_development_alternates_with_implementation(self):
for previous, expected in (("feature-workflow", "P2-delivery-acceptance"),
("business-acceptance", "P2-delivery-implementation")):
with patch.object(planner, "load", return_value={"history": []}):
phases = planner.phase_order({"selected": {"tier": previous}}, 900)
self.assertEqual(phases[0][0], "P0-observed-runtime-repair")
self.assertEqual(phases[1][0], "P0-executable-business-acceptance")
self.assertEqual(phases[2][0], expected)
def test_two_failed_recovery_turns_reserve_new_business_despite_evidence_refresh(self):
failed = {"tier": "business-acceptance", "outcome": "source_edits_pending_outcome", "fileChanges": 1}
history = [failed, {"tier": "business-acceptance", "outcome": "progress", "fileChanges": 0}, failed]
previous = {"selected": {"module": "TechDraw", "tier": "business-acceptance"}}
with patch.object(planner, "load", return_value={"history": history}):
phases = planner.phase_order(previous, 600)
self.assertEqual(phases[0], ("P1-uncovered-business-development", ["tools/prepare_business_acceptance_batch.py", "--uncovered-only", "--exclude-module", "TechDraw"]))
self.assertIn(("P0-executable-business-acceptance", ["tools/prepare_business_acceptance_batch.py", "--covered-only"]), phases)
self.assertTrue(any(command == ["tools/prepare_feature_capability_batch.py"] for _, command in phases))
history.append({**failed, "businessLane": "development"})
self.assertFalse(planner.recovery_budget_spent(history))
def test_uncovered_lane_preserves_scope_and_selects_only_other_modules(self):
state = self.state()
original = list(state["pending"])
batch = acceptance.plan(state, uncovered_only=True, exclude_module="Part")
self.assertEqual([row[0] for row in batch["records"]], ["Draft:other"])
self.assertEqual(batch["businessLane"], "development")
self.assertFalse(batch["acceptanceReady"])
self.assertEqual(state["pending"], original)
state["cooling"] = {"Draft:other"}
with patch.object(acceptance, "retry_details", return_value={}) as retry:
batch = acceptance.plan(state, uncovered_only=True, exclude_module="Part")
self.assertEqual(batch["status"], "no_ready_batch")
retry.assert_called_once_with({"Draft:other"})
def test_provider_wait_and_one_failure_do_not_trigger_development_rotation(self):
failed = {"tier": "business-acceptance", "outcome": "no_progress"}
self.assertFalse(planner.recovery_budget_spent([failed, {"outcome": "provider_wait"}]))
self.assertFalse(planner.recovery_budget_spent([failed, failed, {"tier": "feature-workflow", "outcome": "no_progress"}]))
def test_preserved_implementation_yields_after_two_turns(self):
state = self.state()
state["covered"]["Draft"] = set()
batch = {"schedulingPolicy": planner.POLICY, "selectionScope": "delivery", "selected": {"tier": "feature-workflow"}}
history = [{"tier": "feature-workflow", "outcome": "source_edits_pending_outcome"}]
with patch.object(acceptance, "inventory", return_value=state), patch.object(planner, "load", return_value={"history": history}):
self.assertFalse(planner.acceptance_preempts(batch))
history.append(dict(history[0]))
self.assertTrue(planner.acceptance_preempts(batch))
history.append({"tier": "business-acceptance", "outcome": "progress"})
self.assertFalse(planner.acceptance_preempts(batch))
def test_executable_acceptance_can_run_before_model_and_checks_script(self):
with tempfile.TemporaryDirectory() as directory, patch.object(scheduler, "ROOT", Path(directory)):
root = Path(directory)
contract = root / "docs/business-acceptance/Draft.json"
contract.parent.mkdir(parents=True)
contract.write_text(json.dumps({"command": ["node", "cases.mjs"], "coveredIds": ["Draft:format"]}))
batch = {"selected": {"tier": "business-acceptance", "module": "Draft"}, "records": [["Draft:format"]]}
self.assertFalse(scheduler.runtime_pipeline_ready(batch))
(root / "cases.mjs").write_text("export {};\n")
self.assertTrue(scheduler.runtime_pipeline_ready(batch))
batch["records"].append(["Draft:other"])
self.assertFalse(scheduler.runtime_pipeline_ready(batch))
def test_format_string_routes_are_fingerprinted(self):
with patch.object(evidence, "module_inputs", return_value={}), patch.object(evidence, "digest", return_value="source"):
row = {"id": "business-format:Import:import:.step", "module": "Import", "route": "importStep"}
before = evidence.fingerprint(row)
row["route"] = "other"
self.assertNotEqual(before, evidence.fingerprint(row))
def test_unattempted_acceptance_survives_continuation_then_yields_after_a_turn(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
current = root / "docs/current"
current.mkdir(parents=True)
batch = acceptance.plan(self.state())
batch["generatedAt"] = "2026-09-08T00:00:00+00:00"
(current / "module-batch-manifest.json").write_text(json.dumps(batch))
state, checkpoint = current / "task-state.json", current / "continuation-state.json"
state.write_text('{"active_task":"task"}')
checkpoint.write_text('{"activeTask":"task"}')
with patch.multiple(continuation, ROOT=root, STATE=state, CHECKPOINT=checkpoint), \
patch.object(continuation, "implementation_first_enabled", return_value=True), \
patch.object(continuation, "batch_retry_details", return_value={}), \
patch.object(acceptance, "inventory", return_value=self.state()):
self.assertFalse(continuation.batch_requires_refresh())
history = [{"batchId": batch["batchId"], "tier": "business-acceptance", "outcome": "source_edits_pending_outcome", "at": 1788900000}]
(current / "scheduler-progress.json").write_text(json.dumps({"history": history}))
self.assertTrue(continuation.batch_requires_refresh())
def test_acceptance_failure_without_build_retains_recovery_stage(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
batch, packet = root / "batch.json", root / "packet.json"
batch.write_text(json.dumps({"batchId": "b", "selected": {"tier": "business-acceptance"}}))
packet.write_text(json.dumps({"batchId": "b", "outerPipelineAvailable": True}))
with patch.multiple(pipeline, BATCH_PATH=batch, PACKET_PATH=packet), \
patch.object(pipeline, "run_step", return_value={"exitCode": 1}) as run, \
patch.object(pipeline, "save") as save, patch.object(pipeline.subprocess, "run"), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(pipeline.main(), 1)
self.assertEqual(run.call_args.args[0], "business-acceptance")
self.assertEqual(save.call_args.args[0]["failedStage"], "business-acceptance")
def test_native_acceptance_builds_and_syncs_before_cases_without_rewriting_receipts(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
tree = root / "build"
tree.mkdir()
(tree / "probe.js").write_text("runtime")
(tree / "probe.wasm").write_text("wasm")
(root / "source.cpp").write_text("native")
paths = {key: root / f"{key}.json" for key in ("BATCH_PATH", "PACKET_PATH", "OUTPUT_PATH", "BUILD_CACHE_PATH", "PENDING_BUILD_PATH")}
paths["BATCH_PATH"].write_text(json.dumps({"batchId": "b", "selected": {"tier": "business-acceptance", "module": "Part"}, "records": [["Part:create"]]}))
paths["PACKET_PATH"].write_text(json.dumps({"batchId": "b", "module": "Part", "outerPipelineAvailable": True,
"build": {"target": "probe", "tree": "build", "runtime": "build/probe.js", "wasm": "build/probe.wasm"},
"supportFiles": [{"path": "source.cpp", "sha256": "native"}]}))
calls = []
def run(name, command, environment, timeout):
calls.append(name)
return {"name": name, "command": command, "exitCode": 0, "seconds": 0.01}
with patch.multiple(pipeline, ROOT=root, **paths), patch.object(pipeline, "run_step", side_effect=run), \
patch.object(pipeline, "record_implementation_batch", side_effect=AssertionError("reuse prior receipt")), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(pipeline.main(), 0)
self.assertLess(calls.index("build"), calls.index("artifact-sync"))
self.assertLess(calls.index("artifact-sync"), calls.index("business-acceptance"))
result = json.loads(paths["OUTPUT_PATH"].read_text())
self.assertEqual(result["status"], "pass")
self.assertFalse(result["validationDeferred"])
if __name__ == "__main__":
unittest.main()