177 lines
12 KiB
Python
177 lines
12 KiB
Python
import json
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
import business_acceptance as evidence
|
|
import prepare_business_acceptance_batch as batches
|
|
import prepare_implementation_business_batch as planner
|
|
import run_freecad_web_scheduler as scheduler
|
|
import implementation_first as implementation_policy
|
|
|
|
|
|
class ContinuousAcceptanceTests(unittest.TestCase):
|
|
def test_gap_priority_excludes_repeat_validation_without_changing_scope(self):
|
|
policy = {"continuousDevelopment": True, "completionFocus": "business-acceptance", "prioritizeCoverageGaps": True}
|
|
with patch.object(planner, "load", return_value=policy):
|
|
commands = [command for _, command in planner.phase_order({}, 0)]
|
|
self.assertEqual(commands[0], ["tools/prepare_business_acceptance_batch.py", "--uncovered-only"])
|
|
self.assertNotIn(["tools/prepare_business_acceptance_batch.py", "--priority-only"], commands)
|
|
self.assertNotIn(["tools/prepare_failure_reconciliation_batch.py"], commands)
|
|
rows = [{"id": name, "module": "Part", "command": name} for name in ("old", "gap", "new-case")]
|
|
state = {"pending": rows, "covered": {"Part": {"old", "new-case"}}, "cooling": set(), "receipts": {},
|
|
"deferredValidation": {"old"}}
|
|
selected = lambda **kwargs: {row["id"] for group in batches.candidates(state, **kwargs) for row in group["rows"]}
|
|
self.assertEqual(selected(uncovered_only=True), {"gap"})
|
|
self.assertEqual(selected(covered_only=True), {"new-case"})
|
|
self.assertEqual(state["pending"], rows)
|
|
|
|
def test_previous_pass_remains_deferred_after_failure_or_fingerprint_change(self):
|
|
records = {"old": {"status": "failed", "evidencePath": "old-pass.json", "fingerprint": "stale"},
|
|
"new": {"status": "failed", "lastFailureEvidencePath": "first-failure.json"}}
|
|
def read(path):
|
|
return {"deferPreviouslyPassedValidation": True} if path.name == "execution-policy.json" else {"records": records}
|
|
with patch.object(implementation_policy, "_read_json", side_effect=read):
|
|
self.assertEqual(implementation_policy.deferred_validation_ids(), {"old"})
|
|
with patch.object(implementation_policy, "_read_json", return_value={}):
|
|
self.assertEqual(implementation_policy.deferred_validation_ids(), set())
|
|
|
|
def test_only_selected_semantic_batch_requests_full_ifc_reverification(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
batch, pipeline, packet = (root / name for name in ("batch.json", "pipeline.json", "packet.json"))
|
|
with patch.object(scheduler, "continuous_acceptance_enabled", return_value=True), \
|
|
patch.object(scheduler, "MODULE_BATCH_PATH", batch), \
|
|
patch.object(scheduler, "MODULE_PIPELINE_PATH", pipeline), \
|
|
patch.object(scheduler, "IMPLEMENTATION_PACKET_PATH", packet), \
|
|
patch.object(evidence, "requirements", return_value=[]):
|
|
for selected_id, semantic in [("Part:desktop:Part_CheckGeometry", False),
|
|
("business-format:BIM:semantic-export:.ifc", True)]:
|
|
batch.write_text(json.dumps({"batchId": "current", "records": [[selected_id]]}))
|
|
prompt = scheduler._build_prompt_unbounded("task", failures=[])
|
|
self.assertEqual("Read the full semantic contract" in prompt, semantic)
|
|
self.assertEqual("The independent verifier runs separately" in prompt, semantic)
|
|
self.assertEqual("Reuse current IFC evidence" in prompt, not semantic)
|
|
self.assertIn("Do not modify the semantic contract", prompt)
|
|
self.assertIn("Keep all original requirements", prompt)
|
|
|
|
def test_two_refreshes_reserve_uncovered_work_without_preempting_runtime_repairs(self):
|
|
refresh = {"tier": "business-acceptance", "businessLane": "acceptance", "outcome": "progress", "firstBusinessPassIds": []}
|
|
history = [refresh, {"outcome": "provider_wait"}, refresh]
|
|
self.assertTrue(planner.continuous_coverage_due(history))
|
|
self.assertFalse(planner.continuous_coverage_due(history + [{"businessLane": "development"}]))
|
|
self.assertFalse(planner.continuous_coverage_due(history + [{"firstBusinessPassIds": ["new"]}]))
|
|
def load(path):
|
|
return {"continuousDevelopment": True, "completionFocus": "business-acceptance"} if path.name == "execution-policy.json" else {"history": history}
|
|
with patch.object(planner, "load", side_effect=load):
|
|
phases = [command for _, command in planner.phase_order({}, 0)]
|
|
self.assertEqual(phases[0][-1], "--priority-only")
|
|
self.assertEqual(phases[1][-1], "--runtime-only")
|
|
self.assertLess(phases.index(["tools/prepare_business_acceptance_batch.py", "--uncovered-only"]),
|
|
phases.index(["tools/prepare_business_acceptance_batch.py", "--covered-only"]))
|
|
|
|
def test_scope_cannot_shrink_or_drop_the_semantic_requirement(self):
|
|
def read(path):
|
|
if path.name == "feature-capability-registry.json":
|
|
return {"records": [{"id": "base", "module": "BIM"}]}
|
|
if path.name == "execution-policy.json":
|
|
return {"minimumOriginalRequirements": 2}
|
|
return {}
|
|
with patch.object(evidence, "read", side_effect=read):
|
|
with self.assertRaisesRegex(ValueError, "scope was reduced"):
|
|
evidence.requirements()
|
|
def missing_supplement(path):
|
|
if path.name == "execution-policy.json":
|
|
return {"requiredSupplementalIds": ["semantic"]}
|
|
return {}
|
|
with patch.object(evidence, "read", side_effect=missing_supplement):
|
|
with self.assertRaisesRegex(ValueError, "supplemental"):
|
|
evidence.requirements()
|
|
|
|
def test_partial_semantic_success_does_not_close_full_contract(self):
|
|
result = {"capabilityId": "semantic", "status": "passed", "cases": [
|
|
{"input": {"class": "IfcWall"}, "actual": {"class": "IfcWall"}, "covers": ["classes"],
|
|
"assertions": [{"name": "class preserved", "passed": True}]}]}
|
|
supplemental = {"records": [{"id": "semantic", "requiredProofAreas": ["classes", "materials"]}]}
|
|
with patch.object(evidence, "read", return_value=supplemental):
|
|
self.assertFalse(evidence.successful_result(result))
|
|
self.assertEqual(evidence.missing_proof_areas(result), ["materials"])
|
|
result["cases"].append({"input": {"material": "Concrete"}, "covers": ["materials"],
|
|
"assertions": [{"name": "material association", "passed": True}]})
|
|
self.assertFalse(evidence.successful_result(result))
|
|
result["cases"][-1]["actual"] = {"material": "Concrete", "relation": "IfcRelAssociatesMaterial"}
|
|
self.assertTrue(evidence.successful_result(result))
|
|
|
|
def test_semantic_requirement_keeps_original_scope_and_cannot_inherit_geometry_pass(self):
|
|
original = {"id": "business-format:BIM:export:.ifc", "module": "BIM", "command": "export IFC"}
|
|
semantic = {"id": "business-format:BIM:semantic-export:.ifc", "module": "BIM",
|
|
"command": "bim.formatWorkflow", "sourcePath": "native.cpp", "acceptanceRequired": True}
|
|
def read(path):
|
|
if path.name == "feature-capability-registry.json":
|
|
return {"records": [original]}
|
|
if path.name == "business-acceptance-supplement.json":
|
|
return {"records": [semantic]}
|
|
return {}
|
|
with patch.object(evidence, "read", side_effect=read):
|
|
rows = evidence.requirements()
|
|
self.assertEqual(rows, [original, semantic])
|
|
with patch.object(batches, "requirements", return_value=rows), \
|
|
patch.object(batches, "deferred_validation_ids", return_value=set()), \
|
|
patch.object(batches, "accepted_ids", return_value={original["id"]}), \
|
|
patch.object(batches, "load_registry", return_value={"records": {}}), \
|
|
patch.object(batches, "contract_ids", return_value=set()), \
|
|
patch.object(batches, "cooling_ids", return_value=set()):
|
|
state = batches.inventory()
|
|
self.assertEqual([row["id"] for row in state["pending"]], [semantic["id"]])
|
|
self.assertFalse(batches.plan(state)["acceptanceReady"])
|
|
|
|
def test_contract_changes_invalidate_semantic_fingerprint(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
doc = root / "contract.md"
|
|
doc.write_text("Preserve classes")
|
|
row = {"id": "semantic", "module": "BIM", "contractDocument": "contract.md", "requiredBehavior": ["classes"]}
|
|
with patch.object(evidence, "ROOT", root), patch.object(evidence, "module_inputs", return_value={}):
|
|
before = evidence.fingerprint(row)
|
|
doc.write_text("Preserve classes and material relationships")
|
|
evidence.digest.cache_clear()
|
|
self.assertNotEqual(before, evidence.fingerprint(row))
|
|
before = evidence.fingerprint(row)
|
|
self.assertNotEqual(before, evidence.fingerprint({**row, "requiredBehavior": ["classes", "materials"]}))
|
|
evidence.digest.cache_clear()
|
|
|
|
def test_priority_then_uncovered_work_does_not_stop_at_route_receipts(self):
|
|
with patch.object(planner, "load", return_value={"continuousDevelopment": True, "completionFocus": "business-acceptance"}):
|
|
phases = planner.phase_order({}, 0)
|
|
self.assertEqual(phases[0][1], ["tools/prepare_business_acceptance_batch.py", "--priority-only"])
|
|
self.assertIn(["tools/prepare_business_acceptance_batch.py", "--uncovered-only"], [item[1] for item in phases])
|
|
|
|
def test_authorized_profile_allows_focused_tests_but_not_legacy_full_gates(self):
|
|
with patch.object(scheduler, "continuous_acceptance_enabled", return_value=True):
|
|
for command in ("python3 tools/run_business_acceptance.py --module BIM --reuse",
|
|
"cmake --build freecad-source/build-web/configure --target FreeCADWebBimRuntimeProbe -j2",
|
|
"npm --prefix web run build"):
|
|
self.assertFalse(scheduler.forbidden_implementation_command(command, "build"))
|
|
self.assertTrue(scheduler.forbidden_implementation_command("python3 tools/validate_web_context.py", "validation"))
|
|
|
|
def test_prompt_rejects_a_foreign_batch_packet(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
batch, pipeline, packet = (root / name for name in ("batch.json", "pipeline.json", "packet.json"))
|
|
batch.write_text(json.dumps({"batchId": "current", "selected": {"module": "Part"}, "records": [["Part:one"]]}))
|
|
pipeline.write_text(json.dumps({"batchId": "foreign", "steps": [{"exitCode": 1, "stderr": "FOREIGN ERROR"}]}))
|
|
packet.write_text(json.dumps({"batchId": "foreign", "build": {"target": "FOREIGN TARGET"}, "editableExcerpts": ["FOREIGN SOURCE"]}))
|
|
with patch.object(scheduler, "continuous_acceptance_enabled", return_value=True), \
|
|
patch.object(scheduler, "MODULE_BATCH_PATH", batch), \
|
|
patch.object(scheduler, "MODULE_PIPELINE_PATH", pipeline), \
|
|
patch.object(scheduler, "IMPLEMENTATION_PACKET_PATH", packet), \
|
|
patch.object(evidence, "requirements", return_value=[]):
|
|
prompt = scheduler._build_prompt_unbounded("task", failures=[])
|
|
self.assertNotIn("FOREIGN", prompt)
|
|
self.assertIn("full IFC semantic export", prompt)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|