Files
FreeCADWasmDev/tools/test_reconciliation_efficiency.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

156 lines
9.4 KiB
Python

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 failure_reconciliation as failures
import scheduler_progress as progress
import prepare_module_implementation_packet as packets
import run_freecad_web_scheduler as scheduler
from inspect_web_module_artifacts import inspect
import test_failure_reconciliation as fixtures
class ReconciliationEfficiencyTests(unittest.TestCase):
setUp = fixtures.ReconciliationTests.setUp
tearDown = fixtures.ReconciliationTests.tearDown
save_source = fixtures.ReconciliationTests.save_source
def test_inline_inspection_parse_error_is_not_a_cad_runtime_error(self):
entry = {"command": "node --input-type=module -e 'inspect FreeCADWebDraftRuntimeProbe)'",
"lastError": "file:///project/[eval1]:1 inspect() ) ^ SyntaxError: Unexpected token ')' at compileSourceTextModule"}
self.assertEqual(failures.classify(entry)[0], "diagnostic_tool")
entry["lastError"] = "file:///project/[eval1]:1 new Function(code) ^ SyntaxError: Unexpected token at new Function"
self.assertEqual(failures.classify(entry)[0], "business_runtime")
def test_scope_guard_is_reconciled_without_changing_raw_failures(self):
self.entry.update(command="/bin/bash -lc 'git status --short'", lastExitCode=64,
firstError="SCHEDULER_GIT_SCOPE_REJECTED: git status requires a pathspec")
self.save_source(self.entry)
before = failures.SOURCE.read_bytes()
result = failures.synchronize()
self.assertEqual(result["records"]["one"]["status"], "not_applicable")
self.assertEqual(failures.SOURCE.read_bytes(), before)
self.assertTrue(failures.summary_current()["complete"])
first_proof = result["records"]["one"]["resolution"]["evidencePath"]
self.entry["count"] = 2
self.save_source(self.entry)
repeated = failures.synchronize()
self.assertNotEqual(repeated["records"]["one"]["resolution"]["evidencePath"], first_proof)
self.assertTrue((self.root / first_proof).is_file())
def test_guard_rule_does_not_archive_mixed_commands_or_other_errors(self):
entry = {"command": "git status --short && node case.mjs", "lastExitCode": 64,
"firstError": "SCHEDULER_GIT_SCOPE_REJECTED"}
self.assertIsNone(failures.diagnostic_disposition(entry))
entry.update(command="git status --short", lastExitCode=1)
self.assertIsNone(failures.diagnostic_disposition(entry))
entry.update(lastExitCode=64, firstError="repository unavailable")
self.assertIsNone(failures.diagnostic_disposition(entry))
def test_readonly_tail_after_rejected_git_is_reconciled_without_replay(self):
for command in ["git status --short && ls src/WebAssembly && ls src/WebAssembly/FreeCADCoreProbe",
"git diff --stat && git diff -- web/src/cam.worker.ts | sed -n '1,160p'"]:
with self.subTest(command=command):
self.entry.update(command=command, lastExitCode=64, firstError="SCHEDULER_GIT_SCOPE_REJECTED")
self.save_source(self.entry)
raw = failures.SOURCE.read_bytes()
result = failures.synchronize()
self.assertEqual(result["records"]["one"]["status"], "not_applicable")
self.assertEqual(failures.SOURCE.read_bytes(), raw)
def test_shell_side_effects_and_unknown_commands_are_never_auto_closed(self):
for command in ["git status --short; cmake --build tree", "git status --short || node case.mjs",
"git status --short && sed -i 's/a/b/' source.cpp", "git status --short && ls $(touch file)",
"git status --short && ls `touch file`", "git status --short && ls > output",
"git status --short | sed -n '1p' source", "git status --short && git diff --output=out",
"git status --short && python3 case.py", "git status --short && ls\nnode case.mjs"]:
with self.subTest(command=command):
self.assertIsNone(failures.diagnostic_disposition({"command": command, "lastExitCode": 64,
"firstError": "SCHEDULER_GIT_SCOPE_REJECTED"}))
def test_attached_shell_operators_cannot_bypass_readonly_check(self):
self.assertTrue(failures.guarded_inspection_chain("git status --short&&ls x"))
for command in ["git status --short && ls x||node case.mjs", "git status --short && ls x;node case.mjs",
"git status --short && ls x&node case.mjs", "bash -lc 'git status --short' ; node case.mjs",
"git status --short && sed -n 1p -e '1w output' source"]:
self.assertFalse(failures.guarded_inspection_chain(command))
def test_warm_reconciliation_keeps_valid_snapshot_but_rechecks_proof_sources(self):
self.entry.update(command="sed -n 1p old.json")
self.save_source(self.entry)
source = self.root / "checker.py"
source.write_text("version = 1\n")
failures.review(["one"], "superseded", "replacement checked", [source])
before = failures.OUTPUT.read_bytes()
failures.synchronize()
self.assertEqual(failures.OUTPUT.read_bytes(), before)
source.write_text("version = 2\n")
self.assertEqual(failures.synchronize()["records"]["one"]["status"], "pending")
class ReviewSchedulingTests(unittest.TestCase):
def test_unresolved_review_yields_without_cooling_business_module(self):
with tempfile.TemporaryDirectory() as directory, patch.object(progress, "PROGRESS_PATH", Path(directory) / "progress.json"):
for cycle in range(1, 8):
batch = {"batchId": f"review-{cycle}", "selected": {"module": "Draft", "tier": "failure-reconciliation"},
"records": [[f"failure:Draft:key{cycle}"]]}
result = progress.record_outcome(batch, {}, {}, cycle=cycle, now=1000 + cycle)
self.assertEqual(result["deferredIds"], [f"failure:Draft:key{cycle}"])
self.assertEqual(progress.cooling_modules(now=1010), {})
self.assertNotIn("Draft:new-business", progress.cooling_ids(now=1010, candidates={"Draft:new-business"}))
self.assertTrue(progress.read(progress.PROGRESS_PATH)["history"])
def test_packet_excludes_already_closed_failure_records(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
batch = root / "batch.json"
output = root / "packet.json"
batch.write_text(json.dumps({"status": "ready", "batchId": "b", "selected": {"module": "Draft", "tier": "failure-reconciliation"},
"records": [["failure:Draft:open"], ["failure:Draft:closed"]], "failureKeys": ["open", "closed"]}))
row = {"key": "open", "category": "diagnostic_tool", "modules": ["Draft"], "failureFingerprint": "one", "status": "pending", "sourceSnapshot": {}}
ledger = {"records": {"open": row, "closed": {**row, "key": "closed", "status": "resolved"}}}
with patch.object(packets, "BATCH_PATH", batch), patch.object(packets, "OUTPUT_PATH", output), \
patch.object(packets, "reconcile_failures", return_value=ledger), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(packets.main(), 0)
value = json.loads(output.read_text())
self.assertEqual(value["recordCount"], 1)
self.assertEqual(value["failureReconciliation"]["records"][0]["key"], "open")
def test_fully_reconciled_batch_needs_no_model_turn(self):
batch = {"selected": {"tier": "failure-reconciliation"}, "failureKeys": ["one"]}
with patch.object(failures, "pending_keys", return_value=[]):
self.assertTrue(scheduler.batch_has_completed_evidence(batch))
with patch.object(failures, "pending_keys", return_value=["one"]):
self.assertFalse(scheduler.batch_has_completed_evidence(batch))
class ArtifactInspectionTests(unittest.TestCase):
def test_published_files_and_hashes_are_checked_without_loading_wasm(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
public = root / "web/public"
(public / "modules").mkdir(parents=True)
(public / "kernel").mkdir()
artifact = {"runtimeUrl": "/kernel/probe.js", "wasmUrl": "/kernel/probe.wasm", "pthreadWorkerUrl": "/kernel/probe.worker.js", "hashes": {}}
for kind, file in (("runtime", "probe.js"), ("wasm", "probe.wasm"), ("pthreadWorker", "probe.worker.js")):
content = f"test {kind}".encode()
(public / "kernel" / file).write_bytes(content)
artifact["hashes"][kind] = hashlib.sha256(content).hexdigest()
(public / "modules/manifest.json").write_text(json.dumps({"modules": {"Draft": {"workers": {"draft.worker.ts": artifact}}}}))
self.assertEqual(inspect("Draft", root)["status"], "passed")
(public / "kernel/probe.worker.js").unlink()
self.assertEqual(inspect("Draft", root)["status"], "failed")
(public / "kernel/probe.worker.js").write_text("wrong content")
self.assertEqual(inspect("Draft", root)["status"], "failed")
if __name__ == "__main__":
unittest.main()