978 lines
54 KiB
Python
978 lines
54 KiB
Python
#!/usr/bin/env python3
|
|
"""Regression tests for scheduler optimizations that must preserve gate behavior."""
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "tools"))
|
|
|
|
|
|
def load_module(name: str, relative: str):
|
|
spec = importlib.util.spec_from_file_location(name, ROOT / relative)
|
|
if spec is None or spec.loader is None:
|
|
raise RuntimeError(relative)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
packet = load_module("implementation_packet", "tools/prepare_module_implementation_packet.py")
|
|
pipeline = load_module("module_pipeline", "tools/run_module_batch_pipeline.py")
|
|
deferred_flush = load_module("deferred_build_flush", "tools/flush_deferred_module_builds.py")
|
|
planner = load_module("module_planner", "tools/prepare_full_parity_module_batch.py")
|
|
scheduler = load_module("scheduler", "tools/run_freecad_web_scheduler.py")
|
|
preflight = load_module("batch_preflight", "tools/preflight_current_module_batch.py")
|
|
feature_planner = load_module("feature_planner", "tools/prepare_feature_capability_batch.py")
|
|
capability_generator = load_module("capability_generator", "tools/generate_feature_capability_registry.py")
|
|
implementation_first = load_module("implementation_first_state", "tools/implementation_first.py")
|
|
continuation = load_module("continuation", "tools/continue_web_task_cycle.py")
|
|
|
|
|
|
class GateSelectionTests(unittest.TestCase):
|
|
def assert_gate(self, module: str, tier: str, family: str, expected: str) -> None:
|
|
slug = "".join(value for value in module.lower() if value.isalnum())
|
|
selected = packet.select_module_gate(module, slug, tier, family)
|
|
self.assertIsNotNone(selected)
|
|
self.assertEqual(selected.name, expected)
|
|
|
|
def test_draft_generic_gate(self) -> None:
|
|
self.assert_gate("Draft", "tier-3-stateful", "document-graph", "run_p6_draft_module_batch_gate.mjs")
|
|
|
|
def test_material_nonstandard_gate_name(self) -> None:
|
|
for family in ("generic-object", "property", "serialization"):
|
|
self.assert_gate("Material", "tier-3-stateful", family, "run_p6_material_object_module_batch_gate.mjs")
|
|
|
|
def test_selector_rejects_incompatible_mesh_object_gate(self) -> None:
|
|
self.assertIsNone(packet.select_module_gate("Mesh", "mesh", "tier-2-pure", "pure-function"))
|
|
|
|
def test_feature_batch_selects_split_desktop_target(self) -> None:
|
|
hint = packet.build_hint("Part", packet.probe_sources("Part"), "feature-workflow", "desktop-command-command")
|
|
self.assertEqual(hint.get("target"), "FreeCADWebPartDesktopCommandProbe")
|
|
|
|
def test_mesh_desktop_batch_prefers_runtime_target(self) -> None:
|
|
hint = packet.build_hint("Mesh", packet.probe_sources("Mesh"), "feature-workflow", "desktop-command-command")
|
|
self.assertEqual(hint.get("target"), "FreeCADWebMeshRuntimeProbe")
|
|
|
|
def test_packet_derives_configured_build_tree_before_first_artifact(self) -> None:
|
|
hint = packet.build_hint("MeshPart", packet.probe_sources("MeshPart"), "feature-workflow", "desktop-command-mixed")
|
|
self.assertEqual(hint.get("target"), "FreeCADWebMeshPartRuntimeProbe")
|
|
self.assertEqual(hint.get("tree"), "freecad-source/build-web/configure")
|
|
self.assertTrue(hint.get("runtime", "").endswith("FreeCADWebMeshPartRuntimeProbe.js"))
|
|
|
|
|
|
class BuildReuseTests(unittest.TestCase):
|
|
def test_deferred_build_flush_syncs_before_receipt(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
(root / "build/bin").mkdir(parents=True)
|
|
previous = (
|
|
deferred_flush.ROOT,
|
|
deferred_flush.PENDING_PATH,
|
|
deferred_flush.OUTPUT_PATH,
|
|
deferred_flush.run,
|
|
deferred_flush.record_implementation_batch,
|
|
)
|
|
calls: list[str] = []
|
|
|
|
def fake_run(command: list[str], _timeout: int) -> dict[str, object]:
|
|
if command[0] == "cmake":
|
|
(root / "build/bin/target.js").write_text("js", encoding="ascii")
|
|
(root / "build/bin/target.wasm").write_bytes(b"wasm")
|
|
calls.append("build")
|
|
elif command[0] == "node":
|
|
calls.append("sync")
|
|
elif command[-1].endswith("generate_web_business_completion_audit.py"):
|
|
calls.append("audit")
|
|
else:
|
|
calls.append("preflight")
|
|
return {"command": command, "seconds": 0.0, "exitCode": 0, "stdout": "", "stderr": ""}
|
|
|
|
def fake_receipt(*_args: object, **_kwargs: object) -> dict[str, object]:
|
|
calls.append("receipt")
|
|
return {}
|
|
|
|
try:
|
|
deferred_flush.ROOT = root
|
|
deferred_flush.PENDING_PATH = root / "pending.json"
|
|
deferred_flush.OUTPUT_PATH = root / "report.json"
|
|
deferred_flush.run = fake_run
|
|
deferred_flush.record_implementation_batch = fake_receipt
|
|
deferred_flush.PENDING_PATH.write_text(json.dumps({
|
|
"schema": "freecad-web-module-build-pending/v1",
|
|
"targets": {"build:target": [{"batchId": "b1"}]},
|
|
}), encoding="utf-8")
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertEqual(deferred_flush.main([]), 0)
|
|
self.assertEqual(calls, ["preflight", "build", "sync", "receipt", "audit"])
|
|
state = json.loads(deferred_flush.PENDING_PATH.read_text(encoding="utf-8"))
|
|
self.assertEqual(state["targets"], {})
|
|
finally:
|
|
(
|
|
deferred_flush.ROOT,
|
|
deferred_flush.PENDING_PATH,
|
|
deferred_flush.OUTPUT_PATH,
|
|
deferred_flush.run,
|
|
deferred_flush.record_implementation_batch,
|
|
) = previous
|
|
|
|
def test_deferred_build_flush_honors_retry_backoff_without_subprocess(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
previous_pending = deferred_flush.PENDING_PATH
|
|
previous_output = deferred_flush.OUTPUT_PATH
|
|
previous_run = deferred_flush.run
|
|
try:
|
|
deferred_flush.PENDING_PATH = Path(directory) / "pending.json"
|
|
deferred_flush.OUTPUT_PATH = Path(directory) / "report.json"
|
|
deferred_flush.PENDING_PATH.write_text(json.dumps({
|
|
"schema": "freecad-web-module-build-pending/v1",
|
|
"targets": {"build:target": [{"batchId": "b1", "nextAttemptAt": 32503680000}]},
|
|
}), encoding="utf-8")
|
|
deferred_flush.run = lambda *_args, **_kwargs: self.fail("backoff target launched a subprocess")
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertEqual(deferred_flush.main([]), 0)
|
|
report = json.loads(deferred_flush.OUTPUT_PATH.read_text(encoding="utf-8"))
|
|
self.assertEqual(report["status"], "retry_backoff")
|
|
self.assertEqual(report["skippedTargets"], 1)
|
|
finally:
|
|
deferred_flush.PENDING_PATH = previous_pending
|
|
deferred_flush.OUTPUT_PATH = previous_output
|
|
deferred_flush.run = previous_run
|
|
|
|
def test_scheduler_flushes_deferred_builds_before_idle(self) -> None:
|
|
source = (ROOT / "tools/run_freecad_web_scheduler.py").read_text(encoding="utf-8")
|
|
flush = source.index('run_local("tools/flush_deferred_module_builds.py", "--json")')
|
|
wait = source.index('status="waiting_for_batch"', flush)
|
|
self.assertLess(flush, wait)
|
|
|
|
def test_feature_link_batching_only_defers_when_same_family_remains(self) -> None:
|
|
batch = {
|
|
"selected": {"module": "Sketcher", "family": "desktop-command-command", "recordCount": 16},
|
|
"queue": [{"module": "Sketcher", "family": "desktop-command-command", "unresolved": 30}],
|
|
}
|
|
self.assertTrue(pipeline.feature_has_more_same_family(batch))
|
|
self.assertTrue(pipeline.should_defer_feature_build(batch, implementation_first=True, pending_count=0, artifacts_exist=True))
|
|
self.assertFalse(pipeline.should_defer_feature_build(batch, implementation_first=True, pending_count=1, artifacts_exist=True))
|
|
self.assertFalse(pipeline.should_defer_feature_build(batch, implementation_first=True, pending_count=0, artifacts_exist=False))
|
|
batch["selected"]["family"] = "runtime-issue"
|
|
self.assertFalse(pipeline.should_defer_feature_build(batch, implementation_first=True, pending_count=0, artifacts_exist=True))
|
|
batch["selected"]["family"] = "desktop-command-command"
|
|
batch["queue"][0]["unresolved"] = 16
|
|
self.assertFalse(pipeline.feature_has_more_same_family(batch))
|
|
|
|
def test_pending_build_state_round_trips(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
previous = pipeline.PENDING_BUILD_PATH
|
|
try:
|
|
pipeline.PENDING_BUILD_PATH = Path(directory) / "pending.json"
|
|
state = {"schema": "freecad-web-module-build-pending/v1", "targets": {"tree:target": [{"batchId": "b1"}]}}
|
|
pipeline.save_pending_builds(state)
|
|
self.assertEqual(pipeline.load_pending_builds()["targets"]["tree:target"][0]["batchId"], "b1")
|
|
finally:
|
|
pipeline.PENDING_BUILD_PATH = previous
|
|
|
|
def test_pending_build_status_advances_planner_without_closing_runtime_issue(self) -> None:
|
|
source = (ROOT / "tools/implementation_first.py").read_text(encoding="utf-8")
|
|
self.assertIn('"implementation_pending_build"', source)
|
|
self.assertIn('status == "implementation_ready"', source)
|
|
|
|
def test_file_signature_reuses_metadata_but_detects_content_change(self) -> None:
|
|
with tempfile.TemporaryDirectory(dir=ROOT / "docs/current") as directory:
|
|
path = Path(directory) / "input.txt"
|
|
path.write_text("same", encoding="ascii")
|
|
first, first_hit = pipeline.file_signature(path)
|
|
second, second_hit = pipeline.file_signature(path, first)
|
|
self.assertFalse(first_hit)
|
|
self.assertTrue(second_hit)
|
|
self.assertEqual(first["sha256"], second["sha256"])
|
|
path.write_text("diff", encoding="ascii")
|
|
os.utime(path, ns=(int(first["mtimeNs"]) + 1, int(first["mtimeNs"]) + 1))
|
|
changed, changed_hit = pipeline.file_signature(path, first)
|
|
self.assertFalse(changed_hit)
|
|
self.assertNotEqual(first["sha256"], changed["sha256"])
|
|
|
|
def test_failed_step_compaction_preserves_error_tail(self) -> None:
|
|
value = "configure\n" + ("noise\n" * 1000) + "fatal compiler error"
|
|
compacted = pipeline.compact(value, 400, preserve_tail=True)
|
|
self.assertIn("configure", compacted)
|
|
self.assertIn("fatal compiler error", compacted)
|
|
self.assertIn("failure tail follows", compacted)
|
|
|
|
def test_draft_direct_link_closure_is_tracked(self) -> None:
|
|
tree = ROOT / "freecad-source/build-web/configure"
|
|
dependencies = pipeline.direct_link_dependencies(tree, "FreeCADWebDraftRuntimeProbe")
|
|
self.assertGreaterEqual(len(dependencies), 40)
|
|
self.assertTrue(all(path.suffix == ".a" and path.is_file() for path in dependencies))
|
|
|
|
def test_part_desktop_cache_tracks_only_target_sources(self) -> None:
|
|
tree = ROOT / "freecad-source/build-web/configure"
|
|
sources = pipeline.target_source_dependencies(tree, "FreeCADWebPartDesktopCommandProbe")
|
|
names = {path.name for path in sources}
|
|
self.assertTrue({"PartDesktopCommandProbe.cpp", "PartDesktopCommands.cpp"}.issubset(names))
|
|
self.assertNotIn("PartBoxProbe.cpp", names)
|
|
|
|
def test_incomplete_feature_batch_is_not_build_ready(self) -> None:
|
|
batch = {"records": [["Part:desktop:A"], ["Part:desktop:B"]]}
|
|
capabilities = {"records": [
|
|
{"id": "Part:desktop:A", "implementationStatus": "routed"},
|
|
{"id": "Part:desktop:B", "implementationStatus": "implementation_required"},
|
|
]}
|
|
readiness = pipeline.static_readiness(batch, capabilities)
|
|
self.assertEqual(readiness["ready"], 1)
|
|
self.assertEqual(readiness["missingIds"], ["Part:desktop:B"])
|
|
|
|
def test_rejected_native_marker_reason_reaches_static_readiness(self) -> None:
|
|
batch = {
|
|
"selected": {"module": "Mesh"},
|
|
"records": [["Mesh:desktop:A", "A"], ["Mesh:desktop:B", "B"]],
|
|
}
|
|
capabilities = {
|
|
"records": [
|
|
{"id": "Mesh:desktop:A", "implementationStatus": "implementation_required"},
|
|
{"id": "Mesh:desktop:B", "implementationStatus": "implementation_required"},
|
|
],
|
|
"diagnostics": {
|
|
"rejectedDesktopMarkers": {"Mesh": ["A"]},
|
|
"rejectedMarkerReason": "requires command-specific exposed branch",
|
|
},
|
|
}
|
|
readiness = pipeline.static_readiness(batch, capabilities)
|
|
self.assertEqual(readiness["rejectedMarkers"], [{
|
|
"id": "Mesh:desktop:A",
|
|
"command": "A",
|
|
"reason": "requires command-specific exposed branch",
|
|
}])
|
|
|
|
def test_pipeline_allows_real_partial_batch_but_does_not_defer_its_build(self) -> None:
|
|
source = (ROOT / "tools/run_module_batch_pipeline.py").read_text(encoding="utf-8")
|
|
self.assertIn('readiness["missing"] and not readiness["ready"]', source)
|
|
self.assertIn('and not readiness.get("missing")', source)
|
|
|
|
|
|
class DesktopCommandArchitectureTests(unittest.TestCase):
|
|
def test_probe_module_resolution_uses_canonical_longest_prefix(self) -> None:
|
|
known = {"Fem", "Mesh", "MeshPart", "Part", "PartDesign"}
|
|
self.assertEqual(capability_generator.canonical_probe_module("FemGenericFamilyProbe", known), "Fem")
|
|
self.assertEqual(capability_generator.canonical_probe_module("MeshPartRuntimeProbe", known), "MeshPart")
|
|
self.assertEqual(capability_generator.canonical_probe_module("PartDesignRuntimeProbe", known), "PartDesign")
|
|
|
|
def test_implemented_marker_requires_command_specific_exposed_branch(self) -> None:
|
|
source = '''
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Real")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("First")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("QtStyle")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CommandVariable")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Combined")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Boundary")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("GenericOnly")
|
|
if (FC_WEB_DESKTOP_COMMAND("Real")) {
|
|
result.insert("status", "not_implemented");
|
|
}
|
|
if (FC_WEB_DESKTOP_COMMAND("First")) {
|
|
result.insert("status", "exposed");
|
|
}
|
|
else if (name == QStringLiteral("QtStyle")) {
|
|
result.insert(QStringLiteral("status"), QStringLiteral("exposed"));
|
|
}
|
|
else if (command == QStringLiteral("CommandVariable")) {
|
|
result.insert(QStringLiteral("status"), QStringLiteral("exposed"));
|
|
}
|
|
else if (commandName == QStringLiteral("Combined") || commandName == QStringLiteral("Sibling")) {
|
|
result.insert(QStringLiteral("status"), QStringLiteral("exposed"));
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Real")) {
|
|
result.insert("status", "exposed");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Boundary")) {
|
|
result.insert("status", "not_implemented");
|
|
}
|
|
'''
|
|
self.assertEqual(capability_generator.implemented_desktop_commands(source), {"Combined", "CommandVariable", "First", "QtStyle", "Real"})
|
|
|
|
def test_part_desktop_commands_are_parameter_driven_and_split(self) -> None:
|
|
workbench = (ROOT / "web/src/workbench-shell.tsx").read_text(encoding="utf-8")
|
|
dispatcher = (ROOT / "web/src/desktop-command-dispatcher.ts").read_text(encoding="utf-8")
|
|
cmake = (ROOT / "freecad-source/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt").read_text(encoding="utf-8")
|
|
native = (ROOT / "freecad-source/src/WebAssembly/FreeCADCoreProbe/PartDesktopCommands.cpp").read_text(encoding="utf-8")
|
|
sync = (ROOT / "web/tools/sync-assets.mjs").read_text(encoding="utf-8")
|
|
self.assertNotIn('"ShapeA"', workbench)
|
|
self.assertNotIn('"ShapeB"', workbench)
|
|
self.assertIn("DesktopCommandDispatcher", workbench)
|
|
self.assertIn("record.sourcePath", dispatcher)
|
|
self.assertIn("PartDesktopCommands.cpp", cmake)
|
|
self.assertIn("args.shapes", native)
|
|
self.assertIn("FC_WEB_DESKTOP_COMMAND", native)
|
|
self.assertIn("FreeCADWeb[A-Za-z0-9]+DesktopCommandProbe", sync)
|
|
|
|
def test_desktop_routes_are_generated_from_native_ownership(self) -> None:
|
|
routes = json.loads((ROOT / "web/public/api/desktop-command-routes.json").read_text(encoding="utf-8"))
|
|
by_id = {row["id"]: row for row in routes["records"]}
|
|
self.assertEqual(by_id["PartDesign:desktop:PartDesign_AdditiveHelix"]["transport"], "direct-command")
|
|
self.assertEqual(by_id["PartDesign:desktop:PartDesign_AdditiveHelix"]["worker"], "partdesign-python.worker.ts")
|
|
self.assertEqual(by_id["Part:desktop:Boolean"]["transport"], "ui-container")
|
|
self.assertEqual(by_id["Part:desktop:Part_ColorPerFace"]["transport"], "family-batch")
|
|
self.assertEqual(by_id["Sketcher:desktop:Sketcher_CarbonCopy"]["module"], "Sketcher")
|
|
self.assertNotIn("PARTDESIGN_DESKTOP_DEFAULT_ARGS", (ROOT / "web/src/workbench-shell.tsx").read_text(encoding="utf-8"))
|
|
|
|
def test_registry_generator_has_no_module_specific_desktop_branch(self) -> None:
|
|
source = (ROOT / "tools/generate_web_module_registry.py").read_text(encoding="utf-8")
|
|
self.assertNotIn('if module == "PartDesign"', source)
|
|
self.assertIn("native_desktop_commands(module)", source)
|
|
|
|
def test_feature_registry_scans_implemented_markers_in_split_adapter(self) -> None:
|
|
source = (ROOT / "tools/generate_feature_capability_registry.py").read_text(encoding="utf-8")
|
|
desktop_loop = source[source.index('glob("*DesktopCommands.cpp")'):source.index('glob("*Probe.cpp")')]
|
|
self.assertIn("implemented_desktop_commands(text)", desktop_loop)
|
|
|
|
def test_feature_registry_scans_implemented_markers_in_inventory_api(self) -> None:
|
|
source = (ROOT / "tools/generate_feature_capability_registry.py").read_text(encoding="utf-8")
|
|
self.assertIn('glob("*InventoryApi.cpp")', source)
|
|
inventory = (ROOT / "freecad-source/src/WebAssembly/FreeCADCoreProbe/RobotInventoryApi.cpp").read_text(encoding="utf-8")
|
|
self.assertTrue(
|
|
{
|
|
"Robot_CreateTrajectory",
|
|
"Robot_Edge2Trac",
|
|
"Robot_InsertWaypoint",
|
|
"Robot_InsertWaypointPreselect",
|
|
}.issubset(capability_generator.implemented_desktop_commands(inventory))
|
|
)
|
|
|
|
def test_implemented_marker_accepts_desktop_name_branch(self) -> None:
|
|
source = '''
|
|
if (desktopName == QStringLiteral("MeshPart_Mesher")) {
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("MeshPart_Mesher");
|
|
result.insert(QStringLiteral("status"), QStringLiteral("exposed"));
|
|
}
|
|
'''
|
|
self.assertEqual(
|
|
capability_generator.implemented_desktop_commands(source),
|
|
{"MeshPart_Mesher"},
|
|
)
|
|
|
|
def test_implemented_marker_accepts_record_status_object(self) -> None:
|
|
source = '''
|
|
if (name == QStringLiteral("TemplatePyMod_Cmd1")) {
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("TemplatePyMod_Cmd1");
|
|
record.insert(QStringLiteral("status"), QStringLiteral("exposed"));
|
|
}
|
|
'''
|
|
self.assertEqual(
|
|
capability_generator.implemented_desktop_commands(source),
|
|
{"TemplatePyMod_Cmd1"},
|
|
)
|
|
|
|
def test_implemented_marker_accepts_start_ostringstream_status(self) -> None:
|
|
source = (ROOT / "freecad-source/src/WebAssembly/FreeCADCoreProbe/StartRuntimeProbe.cpp").read_text(encoding="utf-8")
|
|
self.assertIn("Start_Start", capability_generator.implemented_desktop_commands(source))
|
|
|
|
def test_implemented_marker_accepts_command_specific_native_execution_helper(self) -> None:
|
|
source = '''
|
|
#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(name)
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_Analysis")
|
|
if (command == QStringLiteral("FEM_Analysis")) {
|
|
return executeObjectCommand(command, QStringLiteral("Fem::FemAnalysis"), args, result);
|
|
}
|
|
'''
|
|
self.assertEqual(capability_generator.implemented_desktop_commands(source), {"FEM_Analysis"})
|
|
|
|
def test_implemented_marker_accepts_single_statement_native_execution_helper(self) -> None:
|
|
source = '''
|
|
#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(name)
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_SolverZ88")
|
|
if (command == QStringLiteral("FEM_SolverZ88")) return executeObjectCommand(command, QStringLiteral("Fem::FemSolverObject"), args, result);
|
|
'''
|
|
self.assertEqual(capability_generator.implemented_desktop_commands(source), {"FEM_SolverZ88"})
|
|
|
|
def test_start_direct_worker_synthesizes_route_without_duplicate_registry_entry(self) -> None:
|
|
source = (ROOT / "tools/generate_feature_capability_registry.py").read_text(encoding="utf-8")
|
|
self.assertIn('if exact is None and module not in desktop_adapter_modules:', source)
|
|
self.assertIn('"uiStatus": "generic-command-entry"', source)
|
|
routes = json.loads((ROOT / "web/public/api/desktop-command-routes.json").read_text(encoding="utf-8"))
|
|
start = next((row for row in routes.get("records", []) if row.get("id") == "Start:desktop:Start_Start"), None)
|
|
if start is not None:
|
|
self.assertEqual(start.get("transport"), "direct-command")
|
|
self.assertEqual(start.get("worker"), "start.worker.ts")
|
|
|
|
def test_templatepymod_desktop_contract_includes_final_command(self) -> None:
|
|
source = (ROOT / "web/src/templatepymod-python-family-facade.ts").read_text(encoding="utf-8")
|
|
declaration = next(line for line in source.splitlines() if "TEMPLATEPYMOD_DESKTOP_COMMANDS" in line)
|
|
self.assertIn('"TemplatePyMod_Cmd5"', declaration)
|
|
|
|
def test_workbench_groups_are_ui_containers_but_leaf_commands_are_not(self) -> None:
|
|
self.assertTrue(capability_generator.is_ui_container({
|
|
"command": "Constraints",
|
|
"sourcePath": "src/Mod/Sketcher/Gui/Workbench.cpp",
|
|
"sourceLine": 82,
|
|
}))
|
|
self.assertTrue(capability_generator.is_ui_container({
|
|
"command": "Boolean",
|
|
"sourcePath": "src/Mod/Part/Gui/Workbench.cpp",
|
|
"sourceLine": 93,
|
|
}))
|
|
self.assertFalse(capability_generator.is_ui_container({
|
|
"command": "Part_ColorPerFace",
|
|
"sourcePath": "src/Mod/Part/Gui/Workbench.cpp",
|
|
"sourceLine": 165,
|
|
}))
|
|
self.assertTrue(capability_generator.is_ui_container({
|
|
"command": "BIM_ArcTools",
|
|
"sourcePath": "src/Mod/BIM/InitGui.py",
|
|
"sourceLine": 390,
|
|
}))
|
|
self.assertTrue(capability_generator.is_ui_container({
|
|
"command": "Arch_RebarTools",
|
|
"sourcePath": "src/Mod/BIM/InitGui.py",
|
|
"sourceLine": 424,
|
|
}))
|
|
|
|
def test_desktop_transport_does_not_inflate_business_inventory(self) -> None:
|
|
registry = json.loads((ROOT / "docs/current/feature-capability-registry.json").read_text(encoding="utf-8"))
|
|
self.assertFalse(any(row["id"].endswith(".desktopCommand") for row in registry["records"]))
|
|
|
|
def test_implementation_packet_has_multiple_native_edit_anchors(self) -> None:
|
|
path = ROOT / "freecad-source/src/WebAssembly/FreeCADCoreProbe/SketcherProbe.cpp"
|
|
regions = packet.editable_excerpts(path, path.read_text(encoding="utf-8"), ["command-not-yet-registered"])
|
|
roles = {row["role"] for row in regions}
|
|
self.assertIn("desktop-handler", roles)
|
|
self.assertIn("command-registration", roles)
|
|
self.assertIn("unified-dispatch", roles)
|
|
|
|
def test_feature_packet_focuses_native_edits_on_dispatch_owner(self) -> None:
|
|
source = (ROOT / "tools/prepare_module_implementation_packet.py").read_text(encoding="utf-8")
|
|
self.assertIn("unified_dispatch_paths", source)
|
|
self.assertIn("path not in unified_dispatch_paths", source)
|
|
packet_payload = json.loads((ROOT / "docs/current/module-implementation-packet.json").read_text(encoding="utf-8"))
|
|
if packet_payload.get("module") == "Surface" and packet_payload.get("family", "").startswith("desktop-command"):
|
|
omitted = set(packet_payload.get("omittedStableSupport", []))
|
|
self.assertIn("freecad-source/src/WebAssembly/FreeCADCoreProbe/SurfaceRuntimeProbe.cpp", omitted)
|
|
|
|
def test_module_without_desktop_handler_keeps_family_or_abi_anchor(self) -> None:
|
|
path = ROOT / "freecad-source/src/WebAssembly/FreeCADCoreProbe/AssemblyRuntimeProbe.cpp"
|
|
regions = packet.editable_excerpts(path, path.read_text(encoding="utf-8"), ["Assembly_CommandNotRegistered"])
|
|
roles = {row["role"] for row in regions}
|
|
self.assertTrue(roles & {"family-record-handler", "family-dispatch", "unified-dispatch"})
|
|
|
|
def test_probe_discovery_is_case_insensitive_for_bim(self) -> None:
|
|
self.assertIn("BimRuntimeProbe.cpp", {path.name for path in packet.probe_sources("BIM")})
|
|
|
|
def test_probe_discovery_includes_suffixed_robot_native_units(self) -> None:
|
|
names = {path.name for path in packet.probe_sources("Robot")}
|
|
self.assertIn("RobotRuntimeProbeFixed.cpp", names)
|
|
self.assertIn("RobotInventoryApi.cpp", names)
|
|
|
|
def test_cpp_command_excerpt_resolves_activated_semantics(self) -> None:
|
|
path = ROOT / "freecad-source/src/Mod/Sketcher/Gui/CommandSketcherBSpline.cpp"
|
|
line, excerpt = packet.cpp_command_excerpt(path, "Sketcher_BSplineConvertToNURBS", 112)
|
|
self.assertGreater(line, 112)
|
|
self.assertIn("CmdSketcherConvertToNURBS::activated", excerpt)
|
|
self.assertIn("convertToNURBS", excerpt)
|
|
|
|
|
|
class BatchPreflightTests(unittest.TestCase):
|
|
def test_feature_batch_uses_feature_identity_and_ids(self) -> None:
|
|
batch = {
|
|
"schema": preflight.FEATURE_SCHEMA,
|
|
"strategy": "feature-first-deferred-validation",
|
|
}
|
|
ids = ["Sketcher:desktop:Sketcher_EditSketch"]
|
|
self.assertEqual(
|
|
preflight.batch_identity_parts(batch, "Sketcher", "feature-workflow", "desktop-command-command", ids),
|
|
["Sketcher", "desktop-command-command", *ids],
|
|
)
|
|
self.assertTrue(preflight.record_belongs_to_batch(batch, "Sketcher", ids[0]))
|
|
|
|
def test_legacy_batch_retains_tier_and_prefixed_ids(self) -> None:
|
|
batch = {"schema": "freecad-web-module-batch/v1"}
|
|
ids = ["py:Draft:Vector.add"]
|
|
self.assertEqual(
|
|
preflight.batch_identity_parts(batch, "Draft", "tier-2-pure", "geometry-value", ids),
|
|
["Draft", "tier-2-pure", "geometry-value", *ids],
|
|
)
|
|
self.assertTrue(preflight.record_belongs_to_batch(batch, "Draft", ids[0]))
|
|
self.assertFalse(preflight.record_belongs_to_batch(batch, "Part", ids[0]))
|
|
|
|
|
|
class AdaptiveBatchTests(unittest.TestCase):
|
|
def test_routed_record_without_receipt_remains_schedulable(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
previous = (
|
|
feature_planner.REGISTRY,
|
|
feature_planner.ISSUES,
|
|
feature_planner.STATE,
|
|
feature_planner.OUTPUT,
|
|
feature_planner.PIPELINE,
|
|
feature_planner.capability_source_fingerprint,
|
|
feature_planner.implementation_ready_ids,
|
|
)
|
|
try:
|
|
feature_planner.REGISTRY = root / "capabilities.json"
|
|
feature_planner.ISSUES = root / "issues.json"
|
|
feature_planner.STATE = root / "state.json"
|
|
feature_planner.OUTPUT = root / "batch.json"
|
|
feature_planner.PIPELINE = root / "pipeline.json"
|
|
feature_planner.capability_source_fingerprint = lambda: "fingerprint"
|
|
feature_planner.implementation_ready_ids = lambda: set()
|
|
feature_planner.REGISTRY.write_text(json.dumps({"records": [{
|
|
"id": "Start:desktop:Start_Start",
|
|
"module": "Start",
|
|
"command": "Start_Start",
|
|
"implementationStatus": "routed",
|
|
"uiStatus": "generic-command-entry",
|
|
"sourcePath": "src/Mod/Start/Gui/Manipulator.cpp",
|
|
"sourceLine": 42,
|
|
}]}), encoding="utf-8")
|
|
feature_planner.ISSUES.write_text('{"issues":[]}', encoding="utf-8")
|
|
feature_planner.STATE.write_text('{"active_task":"FW-P6-FPA-Y"}', encoding="utf-8")
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertEqual(feature_planner.main(), 0)
|
|
batch = json.loads(feature_planner.OUTPUT.read_text(encoding="utf-8"))
|
|
self.assertEqual(batch["status"], "ready")
|
|
self.assertEqual(batch["records"][0][0], "Start:desktop:Start_Start")
|
|
finally:
|
|
(
|
|
feature_planner.REGISTRY,
|
|
feature_planner.ISSUES,
|
|
feature_planner.STATE,
|
|
feature_planner.OUTPUT,
|
|
feature_planner.PIPELINE,
|
|
feature_planner.capability_source_fingerprint,
|
|
feature_planner.implementation_ready_ids,
|
|
) = previous
|
|
|
|
def test_feature_batch_size_is_bounded_and_runtime_issues_stay_small(self) -> None:
|
|
self.assertGreater(feature_planner.FEATURE_BATCH_SIZE, 8)
|
|
self.assertLessEqual(feature_planner.FEATURE_BATCH_SIZE, 32)
|
|
self.assertLessEqual(feature_planner.RUNTIME_ISSUE_BATCH_SIZE, feature_planner.FEATURE_BATCH_SIZE)
|
|
|
|
def test_small_desktop_families_are_packed_within_one_module(self) -> None:
|
|
ordered = [
|
|
(("Sketcher", "desktop-command-a"), [{"id": "a1"}]),
|
|
(("Sketcher", "desktop-command-b"), [{"id": "b1"}, {"id": "b2"}]),
|
|
(("TechDraw", "desktop-command-c"), [{"id": "c1"}]),
|
|
]
|
|
module, family, rows, source_families = feature_planner.select_group_batch(ordered)
|
|
self.assertEqual(module, "Sketcher")
|
|
self.assertEqual(family, "desktop-command-mixed")
|
|
self.assertEqual([row["id"] for row in rows], ["a1", "b1", "b2"])
|
|
self.assertEqual(source_families, ["desktop-command-a", "desktop-command-b"])
|
|
|
|
def test_mixed_feature_batch_caps_heterogeneous_records(self) -> None:
|
|
ordered = [
|
|
(("CAM", "desktop-command-a"), [{"id": f"a{index}"} for index in range(4)]),
|
|
(("CAM", "desktop-command-b"), [{"id": f"b{index}"} for index in range(8)]),
|
|
]
|
|
module, family, rows, source_families = feature_planner.select_group_batch(ordered)
|
|
self.assertEqual(module, "CAM")
|
|
self.assertEqual(family, "desktop-command-mixed")
|
|
self.assertEqual(len(rows), feature_planner.MIXED_FEATURE_BATCH_SIZE)
|
|
self.assertEqual(source_families, ["desktop-command-a", "desktop-command-b"])
|
|
|
|
def test_runtime_issue_batch_is_never_mixed(self) -> None:
|
|
ordered = [
|
|
(("Part", "runtime-issue"), [{"id": "issue"}]),
|
|
(("Part", "desktop-command-a"), [{"id": "a1"}]),
|
|
]
|
|
_, family, rows, source_families = feature_planner.select_group_batch(ordered)
|
|
self.assertEqual(family, "runtime-issue")
|
|
self.assertEqual([row["id"] for row in rows], ["issue"])
|
|
self.assertEqual(source_families, ["runtime-issue"])
|
|
|
|
def test_feature_batch_deduplicates_records_at_final_boundary(self) -> None:
|
|
ordered = [
|
|
(("Part", "runtime-issue"), [{"id": "same"}, {"id": "same"}, {"id": "first"}]),
|
|
(("Part", "desktop-command-a"), [{"id": "same"}, {"id": "second"}]),
|
|
]
|
|
_, family, rows, _ = feature_planner.select_group_batch(ordered, 4)
|
|
self.assertEqual(family, "runtime-issue")
|
|
self.assertEqual([row["id"] for row in rows], ["same", "first"])
|
|
|
|
def test_stalled_zero_ready_feature_batch_shrinks_to_four(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
previous_output = feature_planner.OUTPUT
|
|
previous_pipeline = feature_planner.PIPELINE
|
|
previous_continuation_pipeline = continuation.MODULE_PIPELINE
|
|
previous_override = os.environ.get("FREECAD_IMPLEMENTATION_FIRST")
|
|
try:
|
|
feature_planner.OUTPUT = root / "batch.json"
|
|
feature_planner.PIPELINE = root / "pipeline.json"
|
|
continuation.MODULE_PIPELINE = feature_planner.PIPELINE
|
|
os.environ["FREECAD_IMPLEMENTATION_FIRST"] = "1"
|
|
old = "2020-01-01T00:00:00+00:00"
|
|
batch = {
|
|
"status": "ready",
|
|
"strategy": "feature-first-deferred-validation",
|
|
"batchId": "stalled",
|
|
"generatedAt": old,
|
|
"selected": {"recordCount": 16},
|
|
}
|
|
pipeline_state = {
|
|
"status": "implementation_incomplete",
|
|
"batchId": "stalled",
|
|
"staticReadiness": {"ready": 0, "missing": 16},
|
|
}
|
|
feature_planner.OUTPUT.write_text(json.dumps(batch), encoding="utf-8")
|
|
feature_planner.PIPELINE.write_text(json.dumps(pipeline_state), encoding="utf-8")
|
|
self.assertEqual(feature_planner.stalled_batch_limit(), 4)
|
|
self.assertEqual(continuation.feature_batch_replan_size(batch), 4)
|
|
ordered = [(('Mesh', 'desktop-command-command'), [{"id": str(index)} for index in range(16)])]
|
|
self.assertEqual(len(feature_planner.select_group_batch(ordered, 4)[2]), 4)
|
|
pipeline_state["staticReadiness"]["ready"] = 1
|
|
feature_planner.PIPELINE.write_text(json.dumps(pipeline_state), encoding="utf-8")
|
|
self.assertIsNone(feature_planner.stalled_batch_limit())
|
|
self.assertIsNone(continuation.feature_batch_replan_size(batch))
|
|
finally:
|
|
feature_planner.OUTPUT = previous_output
|
|
feature_planner.PIPELINE = previous_pipeline
|
|
continuation.MODULE_PIPELINE = previous_continuation_pipeline
|
|
if previous_override is None:
|
|
os.environ.pop("FREECAD_IMPLEMENTATION_FIRST", None)
|
|
else:
|
|
os.environ["FREECAD_IMPLEMENTATION_FIRST"] = previous_override
|
|
|
|
def test_existing_oversized_mixed_batch_replans_immediately(self) -> None:
|
|
previous_override = os.environ.get("FREECAD_IMPLEMENTATION_FIRST")
|
|
try:
|
|
os.environ["FREECAD_IMPLEMENTATION_FIRST"] = "1"
|
|
batch = {
|
|
"status": "ready",
|
|
"strategy": "feature-first-deferred-validation",
|
|
"selected": {"family": "desktop-command-mixed", "recordCount": 16},
|
|
}
|
|
self.assertEqual(continuation.feature_batch_replan_size(batch), continuation.MIXED_FEATURE_BATCH_SIZE)
|
|
finally:
|
|
if previous_override is None:
|
|
os.environ.pop("FREECAD_IMPLEMENTATION_FIRST", None)
|
|
else:
|
|
os.environ["FREECAD_IMPLEMENTATION_FIRST"] = previous_override
|
|
|
|
def setUp(self) -> None:
|
|
self.directory = tempfile.TemporaryDirectory()
|
|
planner.SCHEDULER_TIMING = Path(self.directory.name) / "timing.json"
|
|
planner.SCHEDULER_STATUS = Path(self.directory.name) / "status.json"
|
|
planner.SCHEDULER_STATUS.write_text("{}", encoding="ascii")
|
|
|
|
def tearDown(self) -> None:
|
|
self.directory.cleanup()
|
|
|
|
def write_samples(self, durations: list[int], failed: bool = False) -> None:
|
|
planner.SCHEDULER_TIMING.write_text(json.dumps({"cycles": [
|
|
{"effectiveCycleSeconds": duration, "batch": {"module": "Draft", "tier": "tier-2-pure", "family": "geometry-value", "records": 24}, "activity": {"commandFailures": 1 if failed and index == 0 else 0}}
|
|
for index, duration in enumerate(durations)
|
|
]}), encoding="utf-8")
|
|
|
|
def test_fast_family_grows_within_cap(self) -> None:
|
|
self.write_samples([60, 65, 70])
|
|
self.assertEqual(planner.adaptive_target_size(None, "tier-2-pure", "Draft", "geometry-value")[0], 32)
|
|
|
|
def test_failure_halves_batch(self) -> None:
|
|
self.write_samples([100, 110, 120], failed=True)
|
|
self.assertEqual(planner.adaptive_target_size(None, "tier-2-pure", "Draft", "geometry-value")[0], 12)
|
|
|
|
def test_slow_family_shrinks_gradually(self) -> None:
|
|
self.write_samples([450, 460, 470])
|
|
self.assertEqual(planner.adaptive_target_size(None, "tier-2-pure", "Draft", "geometry-value")[0], 20)
|
|
|
|
|
|
class SessionTests(unittest.TestCase):
|
|
def test_prompt_context_has_a_hard_byte_limit(self) -> None:
|
|
prompt = "rules\n" + ("source excerpt 数据\n" * 10_000) + "batchId=current-batch\n"
|
|
compacted = scheduler.bound_prompt_context(prompt, 12_000)
|
|
self.assertLessEqual(len(compacted.encode("utf-8")), 12_000)
|
|
self.assertIn("[CONTEXT_COMPACTED by unattended scheduler:", compacted)
|
|
self.assertIn("batchId=current-batch", compacted)
|
|
|
|
def test_provider_failure_discards_resumed_session(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
previous_path = scheduler.SESSION_CACHE_PATH
|
|
try:
|
|
scheduler.SESSION_CACHE_PATH = Path(directory) / "session.json"
|
|
scheduler.SESSION_CACHE_PATH.write_text(json.dumps({
|
|
"module": "Part",
|
|
"threadId": "old-thread",
|
|
"lastBatchId": "batch",
|
|
}), encoding="utf-8")
|
|
metadata = {
|
|
"threadId": "old-thread",
|
|
"streamFailed": True,
|
|
"providerFailureReason": "servers are currently overloaded",
|
|
}
|
|
scheduler.save_session_cache("Part", "batch", metadata, None, False)
|
|
self.assertFalse(scheduler.SESSION_CACHE_PATH.exists())
|
|
self.assertTrue(metadata["contextRotatedAfterProviderFailure"])
|
|
finally:
|
|
scheduler.SESSION_CACHE_PATH = previous_path
|
|
|
|
def test_partial_native_routing_replans_only_remaining_feature_records(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
previous_capability = continuation.CAPABILITY_REGISTRY
|
|
try:
|
|
continuation.CAPABILITY_REGISTRY = Path(directory) / "capability.json"
|
|
continuation.CAPABILITY_REGISTRY.write_text(json.dumps({
|
|
"records": [
|
|
{"id": "ReverseEngineering:desktop:Implemented", "implementationStatus": "routed"},
|
|
{"id": "ReverseEngineering:desktop:Remaining", "implementationStatus": "implementation_required"},
|
|
]
|
|
}), encoding="utf-8")
|
|
batch = {
|
|
"strategy": "feature-first-deferred-validation",
|
|
"records": [
|
|
["ReverseEngineering:desktop:Implemented", "Implemented"],
|
|
["ReverseEngineering:desktop:Remaining", "Remaining"],
|
|
],
|
|
}
|
|
self.assertEqual(
|
|
continuation.selected_routed_ids(batch),
|
|
{"ReverseEngineering:desktop:Implemented"},
|
|
)
|
|
finally:
|
|
continuation.CAPABILITY_REGISTRY = previous_capability
|
|
|
|
def test_stale_receipt_is_not_ready_when_capability_route_is_revoked(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
previous_registry = implementation_first.REGISTRY_PATH
|
|
previous_capability = implementation_first.CAPABILITY_PATH
|
|
previous_override = os.environ.get("FREECAD_IMPLEMENTATION_FIRST")
|
|
try:
|
|
implementation_first.REGISTRY_PATH = root / "registry.json"
|
|
implementation_first.CAPABILITY_PATH = root / "capability.json"
|
|
os.environ["FREECAD_IMPLEMENTATION_FIRST"] = "1"
|
|
implementation_first.REGISTRY_PATH.write_text(json.dumps({
|
|
"schema": "freecad-web-implementation-first-registry/v1",
|
|
"records": {
|
|
"Mesh:desktop:Real": {"status": "implementation_ready"},
|
|
"Mesh:desktop:Revoked": {"status": "implementation_ready"},
|
|
},
|
|
}), encoding="utf-8")
|
|
implementation_first.CAPABILITY_PATH.write_text(json.dumps({"records": [
|
|
{"id": "Mesh:desktop:Real", "implementationStatus": "routed"},
|
|
{"id": "Mesh:desktop:Revoked", "implementationStatus": "implementation_required"},
|
|
]}), encoding="utf-8")
|
|
self.assertEqual(implementation_first.implementation_ready_ids(), {"Mesh:desktop:Real"})
|
|
finally:
|
|
implementation_first.REGISTRY_PATH = previous_registry
|
|
implementation_first.CAPABILITY_PATH = previous_capability
|
|
if previous_override is None:
|
|
os.environ.pop("FREECAD_IMPLEMENTATION_FIRST", None)
|
|
else:
|
|
os.environ["FREECAD_IMPLEMENTATION_FIRST"] = previous_override
|
|
|
|
def test_rg_no_match_is_not_a_scheduler_failure(self) -> None:
|
|
command = "/bin/bash -lc \"rg -n 'Missing|StillMissing' file.cpp\""
|
|
self.assertTrue(scheduler.exploratory_no_match(command, 1, ""))
|
|
self.assertTrue(scheduler.exploratory_no_match('/bin/bash -lc "sed -n 1,20p file.cpp; rg Missing file.cpp"', 1, "source excerpt"))
|
|
self.assertFalse(scheduler.exploratory_no_match(command, 2, ""))
|
|
self.assertFalse(scheduler.exploratory_no_match('/bin/bash -lc "rg Missing file.cpp && build"', 1, ""))
|
|
|
|
def test_old_rg_no_match_failure_is_normalized(self) -> None:
|
|
state = {"failures": {"old": {"command": "rg Missing file.cpp", "lastExitCode": 1, "firstError": "", "resolved": False}}}
|
|
self.assertEqual(scheduler.normalize_exploratory_no_match_failures(state), 1)
|
|
self.assertFalse(state["failures"]["old"]["resolved"])
|
|
self.assertEqual(state["failures"]["old"]["diagnosticReview"], "rg_no_match")
|
|
scope_state = {"failures": {"scope": {"command": "rg Pattern web/src", "lastExitCode": 64, "firstError": "SCHEDULER_SEARCH_SCOPE_REJECTED", "resolved": False}}}
|
|
self.assertEqual(scheduler.normalize_exploratory_no_match_failures(scope_state), 1)
|
|
self.assertFalse(scope_state["failures"]["scope"]["resolved"])
|
|
self.assertEqual(scope_state["failures"]["scope"]["diagnosticReview"], "rg_scope")
|
|
compat_state = {"failures": {"compat": {"command": "python3 tools/fast_source_search.py P --paths web", "lastExitCode": 2, "firstError": "unrecognized arguments: --paths web", "resolved": False}}}
|
|
self.assertEqual(scheduler.normalize_exploratory_no_match_failures(compat_state), 1)
|
|
self.assertFalse(compat_state["failures"]["compat"]["resolved"])
|
|
self.assertEqual(compat_state["failures"]["compat"]["diagnosticReview"], "tool_compat")
|
|
|
|
def test_scheduler_rg_rejects_directories_and_normalizes_no_match(self) -> None:
|
|
source = (ROOT / "tools/scheduler-bin/rg").read_text(encoding="utf-8")
|
|
self.assertIn('-d "$argument"', source)
|
|
self.assertIn("SCHEDULER_RG_NO_MATCH", source)
|
|
self.assertIn("SCHEDULER_SEARCH_SCOPE_SKIPPED", source)
|
|
self.assertIn("--max-columns 240 --max-columns-preview", source)
|
|
self.assertIn("SCHEDULER_SOURCE_BUDGET_REACHED", source)
|
|
scheduler_source = (ROOT / "tools/run_freecad_web_scheduler.py").read_text(encoding="utf-8")
|
|
self.assertIn('environment["BASH_ENV"]', scheduler_source)
|
|
fast_source = (ROOT / "tools/fast_source_search.py").read_text(encoding="utf-8")
|
|
self.assertIn("output_lines[:global_limit]", fast_source)
|
|
self.assertIn('"--max-count", "--max-results"', fast_source)
|
|
self.assertIn('os.environ.get("FREECAD_SOURCE_BUDGET_MARKER"', fast_source)
|
|
|
|
def test_feature_read_budget_is_soft_until_edit_or_hard_limit(self) -> None:
|
|
self.assertFalse(scheduler.source_read_should_stop(4, 0, 4, 5))
|
|
self.assertTrue(scheduler.source_read_should_stop(5, 0, 4, 5))
|
|
self.assertTrue(scheduler.source_read_should_stop(5, 1, 4, 5))
|
|
|
|
def test_source_budget_marks_search_boundary(self) -> None:
|
|
self.assertTrue(scheduler.source_read_should_stop(5, 0, 4, 5))
|
|
self.assertTrue(scheduler.source_read_should_stop(6, 0, 4, 5))
|
|
self.assertFalse(scheduler.source_read_should_stop(3, 0, 4, 5))
|
|
|
|
def test_token_prediction_rotates_before_limit(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
scheduler.SESSION_CACHE_PATH = Path(directory) / "session.json"
|
|
first_metadata = {"threadId": "thread", "usage": {"input_tokens": 150_000, "cached_input_tokens": 100_000}}
|
|
scheduler.save_session_cache("Draft", "b1", first_metadata, None, True)
|
|
first = scheduler.load_session_cache()
|
|
self.assertEqual(first["predictedNextInputTokens"], 300_000)
|
|
second_metadata = {"threadId": "thread", "usage": {"input_tokens": 330_000, "cached_input_tokens": 200_000}}
|
|
scheduler.save_session_cache("Draft", "b2", second_metadata, first, True)
|
|
second = scheduler.load_session_cache()
|
|
self.assertEqual(second["predictedNextInputTokens"], 510_000)
|
|
self.assertIsNotNone(scheduler.resumable_session("Draft"))
|
|
second["predictedNextInputTokens"] = scheduler.SESSION_INPUT_TOKEN_LIMIT
|
|
scheduler.SESSION_CACHE_PATH.write_text(json.dumps(second), encoding="utf-8")
|
|
self.assertIsNone(scheduler.resumable_session("Draft"))
|
|
|
|
def test_pipeline_policy_keeps_budget_stopped_edits_eligible(self) -> None:
|
|
source = (ROOT / "tools/run_freecad_web_scheduler.py").read_text(encoding="utf-8")
|
|
self.assertIn("pipeline_eligible = return_code == 0 and not any((timed_out, stream_failed, agent_idle_stopped))", source)
|
|
self.assertIn("pipeline_eligible and session_clean if feature_turn", source)
|
|
self.assertIn('status="batch_static_pipeline_complete"', source)
|
|
|
|
def test_feature_session_reuses_only_same_batch(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
scheduler.SESSION_CACHE_PATH = Path(directory) / "session.json"
|
|
metadata = {"threadId": "feature-thread", "usage": {"input_tokens": 10_000}}
|
|
scheduler.save_session_cache("PartDesign", "batch-a", metadata, None, True)
|
|
self.assertIsNotNone(scheduler.resumable_session("PartDesign", "batch-a", same_batch_only=True))
|
|
self.assertIsNone(scheduler.resumable_session("PartDesign", "batch-b", same_batch_only=True))
|
|
|
|
def test_compile_recovery_reuses_same_batch_session(self) -> None:
|
|
source = (ROOT / "tools/run_freecad_web_scheduler.py").read_text(encoding="utf-8")
|
|
self.assertIn("same_batch_only=compile_recovery or feature_turn", source)
|
|
self.assertGreaterEqual(scheduler.COMPILE_RECOVERY_IDLE_LIMIT_SECONDS, 20.0)
|
|
self.assertGreaterEqual(scheduler.POST_EDIT_FINALIZE_SECONDS, 15.0)
|
|
|
|
def test_implementation_turn_blocks_wrapped_validation_commands(self) -> None:
|
|
self.assertTrue(scheduler.forbidden_implementation_command(
|
|
"/bin/bash -lc 'git diff --check -- file && npx --prefix web tsc --noEmit -p web/tsconfig.json'",
|
|
"source/read",
|
|
))
|
|
self.assertTrue(scheduler.forbidden_implementation_command(
|
|
"npm --prefix web run build",
|
|
"source/read",
|
|
))
|
|
self.assertTrue(scheduler.forbidden_implementation_command(
|
|
"git status --short -- web/src/module.ts",
|
|
"source/read",
|
|
))
|
|
|
|
def test_failed_pipeline_skip_requires_unchanged_support_files(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
pipeline_path = root / "pipeline.json"
|
|
packet_path = root / "packet.json"
|
|
support = root / "Probe.cpp"
|
|
support.write_text("source", encoding="ascii")
|
|
pipeline_path.write_text(json.dumps({
|
|
"status": "failed",
|
|
"batchId": "batch-a",
|
|
"failedStage": "build",
|
|
}), encoding="ascii")
|
|
packet_path.write_text(json.dumps({
|
|
"batchId": "batch-a",
|
|
"supportFiles": [{"path": "Probe.cpp"}],
|
|
}), encoding="ascii")
|
|
previous_pipeline = scheduler.MODULE_PIPELINE_PATH
|
|
previous_root = scheduler.ROOT
|
|
try:
|
|
scheduler.MODULE_PIPELINE_PATH = pipeline_path
|
|
scheduler.ROOT = root
|
|
self.assertTrue(scheduler.failed_pipeline_is_unchanged("batch-a", json.loads(packet_path.read_text(encoding="ascii"))))
|
|
support.write_text("changed", encoding="ascii")
|
|
os.utime(support, ns=(pipeline_path.stat().st_mtime_ns + 1, pipeline_path.stat().st_mtime_ns + 1))
|
|
self.assertFalse(scheduler.failed_pipeline_is_unchanged("batch-a", json.loads(packet_path.read_text(encoding="ascii"))))
|
|
finally:
|
|
scheduler.MODULE_PIPELINE_PATH = previous_pipeline
|
|
scheduler.ROOT = previous_root
|
|
|
|
def test_resumed_feature_turn_uses_low_reasoning(self) -> None:
|
|
source = (ROOT / "tools/run_freecad_web_scheduler.py").read_text(encoding="utf-8")
|
|
self.assertIn('"low" if feature_turn or batch_has_completed_evidence(batch)', source)
|
|
self.assertLess(scheduler.FEATURE_SOURCE_READS_PER_TURN, scheduler.MAX_SOURCE_READS_PER_TURN)
|
|
self.assertGreater(scheduler.FEATURE_SOURCE_READS_HARD_LIMIT, scheduler.FEATURE_SOURCE_READS_PER_TURN)
|
|
self.assertEqual(scheduler.FEATURE_PATCH_CONFLICT_LIMIT, 1)
|
|
|
|
def test_packet_resolves_plain_module_worker(self) -> None:
|
|
source = (ROOT / "tools/prepare_module_implementation_packet.py").read_text(encoding="utf-8")
|
|
self.assertIn('f"web/src/{slug}.worker.ts"', source)
|
|
self.assertIn("relative in worker_paths", source)
|
|
self.assertIn('glob(f"{slug}*python*family*facade.ts")', source)
|
|
self.assertIn('r"PythonFamily(?:Batch)?Request"', source)
|
|
self.assertIn("relative in facade_paths", source)
|
|
|
|
def test_packet_recommends_splitting_oversized_desktop_adapter(self) -> None:
|
|
source = (ROOT / "tools/prepare_module_implementation_packet.py").read_text(encoding="utf-8")
|
|
self.assertIn("DESKTOP_ADAPTER_SPLIT_BYTES", source)
|
|
self.assertIn('"desktopAdapterRecommended"', source)
|
|
self.assertIn('"oversizedNativeSources"', source)
|
|
self.assertIn('"desktopAdapterAction"', source)
|
|
self.assertIn('"desktopAdapterActive"', source)
|
|
self.assertIn("path != desktop_adapter", source)
|
|
|
|
def test_resumed_feature_prompt_reuses_thread_packet(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
previous_batch = scheduler.MODULE_BATCH_PATH
|
|
previous_pipeline = scheduler.MODULE_PIPELINE_PATH
|
|
try:
|
|
scheduler.MODULE_BATCH_PATH = Path(directory) / "batch.json"
|
|
scheduler.MODULE_PIPELINE_PATH = Path(directory) / "pipeline.json"
|
|
batch = {
|
|
"status": "ready",
|
|
"batchId": "resume-batch",
|
|
"selected": {"module": "Start", "family": "desktop-command", "recordCount": 1},
|
|
"records": [["Start:desktop:Start_Start", "Start_Start"]],
|
|
}
|
|
scheduler.MODULE_BATCH_PATH.write_text(json.dumps(batch), encoding="utf-8")
|
|
scheduler.MODULE_PIPELINE_PATH.write_text("{}", encoding="utf-8")
|
|
prompt = scheduler.build_prompt("FW-P6-FPA-Y", failures=[], resumed=True)
|
|
self.assertIn(str(batch["batchId"]), prompt)
|
|
self.assertIn("上一轮的完整工作包", prompt)
|
|
self.assertNotIn("official source excerpts", prompt)
|
|
self.assertNotIn("editable excerpts:", prompt)
|
|
self.assertLess(len(prompt.encode("utf-8")), 4_000)
|
|
finally:
|
|
scheduler.MODULE_BATCH_PATH = previous_batch
|
|
scheduler.MODULE_PIPELINE_PATH = previous_pipeline
|
|
|
|
def test_conflicted_feature_session_is_not_reused(self) -> None:
|
|
source = (ROOT / "tools/run_freecad_web_scheduler.py").read_text(encoding="utf-8")
|
|
self.assertIn('not int(turn_metadata.get("patchConflicts", 0) or 0)', source)
|
|
self.assertIn('"custom tool call output is missing" in lowered', source)
|
|
self.assertIn('session_clean = bool(turn_metadata.get("turnCompleted"))', source)
|
|
self.assertIn("source_budget_reached_event.set()", source)
|
|
self.assertNotIn('turn_metadata.get("safeBudgetStop")', source)
|
|
|
|
def test_status_liveness_checks_process_identity(self) -> None:
|
|
status_source = (ROOT / "tools/show_freecad_web_scheduler.py").read_text(encoding="utf-8")
|
|
self.assertIn("def process_matches", status_source)
|
|
self.assertIn("observedState: stale", status_source)
|
|
|
|
|
|
class EvidenceTests(unittest.TestCase):
|
|
def test_draft_compile_cache_does_not_replace_native_gate(self) -> None:
|
|
candidates = []
|
|
for path in (ROOT / "docs/p6").glob("draft-module-batch-*-runtime.json"):
|
|
try:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError):
|
|
continue
|
|
if isinstance(value.get("gateTiming"), dict) and value["gateTiming"].get("compileCacheHit") is True:
|
|
candidates.append(value)
|
|
self.assertTrue(candidates)
|
|
evidence = candidates[-1]
|
|
self.assertIn(evidence.get("status"), {"pass", "pass_with_boundaries"})
|
|
self.assertTrue(evidence.get("resources", {}).get("resourcesReleased"))
|
|
self.assertGreater(float(evidence["gateTiming"].get("workerReadySeconds", 0)), 0)
|
|
self.assertGreater(float(evidence["gateTiming"].get("dispatchSeconds", 0)), 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|