import contextlib 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 migration_workflows as workflows import prepare_business_acceptance_batch as acceptance import prepare_feature_capability_batch as features import generate_feature_capability_registry as registry import prepare_implementation_business_batch as planner import prepare_module_implementation_packet as packets import run_freecad_web_scheduler as scheduler from report_migration_delivery import report class DeliveryTests(unittest.TestCase): def test_native_marker_recognizes_fully_qualified_desktop_api_id(self): source = ''' // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Assembly_CreateJointAngle") if (apiId == QStringLiteral("Assembly:desktop:Assembly_CreateJointAngle")) { result.insert(QStringLiteral("status"), QStringLiteral("exposed")); return true; } ''' self.assertIn("Assembly_CreateJointAngle", registry.implemented_desktop_commands(source)) def test_native_marker_recognizes_dispatcher_boolean_command_alias(self): source = ''' // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Assembly_SelectComponentsWithDoFs") const bool dofs = apiId == QStringLiteral("Assembly:desktop:Assembly_SelectComponentsWithDoFs"); if (dofs) { result.insert(QStringLiteral("status"), QStringLiteral("exposed")); } ''' self.assertIn("Assembly_SelectComponentsWithDoFs", registry.implemented_desktop_commands(source)) def test_native_marker_recognizes_name_value_alias_with_json_status(self): source = r''' // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("BIM_Nudge_Down") const bool nudge = familyValue == "desktop-command" && (nameValue == "BIM_Nudge_Down" || nameValue == "BIM_Nudge_Left"); if (nudge) { recordsJson += "\"status\":\"exposed\""; } ''' self.assertIn("BIM_Nudge_Down", registry.implemented_desktop_commands(source)) self.assertNotIn("BIM_Nudge_Right", registry.implemented_desktop_commands(source)) def test_native_marker_recognizes_shared_draft_snap_branch(self): source = r''' // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Draft_Snap_Grid") const int snapIndex = name.startsWith(QStringLiteral("Draft_Snap_")) ? 1 : -1; if (family == QStringLiteral("desktop-command") && snapIndex >= 0) { result.insert(QStringLiteral("status"), QStringLiteral("exposed")); } ''' self.assertIn("Draft_Snap_Grid", registry.implemented_desktop_commands(source)) def test_same_module_cases_for_different_native_owners_are_not_combined(self): rows = [{"id": "Part:create", "module": "Part", "command": "create"}, {"id": "Part:cylinder", "module": "Part", "command": "cylinder"}, {"id": "Part:sphere", "module": "Part", "command": "sphere"}] document = {"tree": "build", "target": "FreeCADWebDocumentProbe"} state = {"pending": rows, "rows": rows, "accepted": set(), "cooling": set(), "receipts": {}, "covered": {"Part": {row["id"] for row in rows}}, "buildOverrides": {"Part": {"Part:cylinder": document, "Part:sphere": document}}} groups = acceptance.candidates(state) self.assertEqual([len(group["rows"]) for group in groups], [1, 2]) self.assertEqual(len(state["pending"]), 3) contract = {"build": {"target": "PartBoxProbe"}, "buildOverrides": state["buildOverrides"]["Part"]} self.assertEqual(packets.acceptance_build_for_ids(contract, {"Part:cylinder", "Part:sphere"}), (document, True)) self.assertEqual(packets.acceptance_build_for_ids(contract, {"Part:create"}), (contract["build"], False)) self.assertEqual(packets.acceptance_build_for_ids(contract, set()), (contract["build"], False)) def state(self): rows = [{"id": key, "module": "Web", "command": key} for key, family in workflows.PRODUCT_WORKFLOWS.items() if family == "product-workspace"] rows += [{"id": "Part:save", "module": "Part", "command": "part.saveReopen", "category": "persistence"}, {"id": "TechDraw:desktop:View", "module": "TechDraw", "command": "View", "kind": "desktop-gui"}, {"id": "Import:inspect", "module": "Import", "command": "import.inspectSourceInventory", "category": "format"}] return {"rows": rows, "pending": rows, "accepted": set(), "receipts": {}, "cooling": set(), "covered": {row["module"]: set() for row in rows}} def test_core_workspace_precedes_desktop_and_support_routes_without_dropping_scope(self): state = self.state() batch = acceptance.plan(state, delivery_only=True) self.assertEqual(batch["selected"]["module"], "Web") self.assertEqual(batch["selected"]["recordCount"], 4) self.assertEqual(batch["businessWorkflow"], "product-workspace") self.assertEqual(batch["records"][0][0], "product:react-primary-shell") state["cooling"] = {row[0] for row in batch["records"]} batch = acceptance.plan(state, delivery_only=True) self.assertEqual([row[0] for row in batch["records"]], ["Part:save"]) self.assertEqual(len(state["pending"]), 7) self.assertFalse(workflows.delivery_route(state["rows"][-1])) def test_save_and_format_families_are_distinct_and_aliases_share_one_family(self): self.assertEqual(workflows.workflow_family({"id": "Part:save", "category": "persistence"}), "document-persistence") self.assertEqual(workflows.workflow_family({"id": "business-format:Import:import:.step"}), workflows.workflow_family({"id": "business-format:Import:export:.stp"})) def test_business_and_desktop_feature_families_never_mix(self): groups = [(('Part', 'module-business-document-persistence'), [{"id": 'save'}]), (('Part', 'desktop-command-command'), [{"id": 'gui'}])] _, family, rows, _ = features.select_group_batch(groups) self.assertEqual(family, 'module-business-document-persistence') self.assertEqual([row['id'] for row in rows], ['save']) _, _, rows, _ = features.select_group_batch(list(reversed(groups))) self.assertEqual([row['id'] for row in rows], ['gui']) def test_delivery_and_runtime_filters_preserve_original_inventory(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) paths = {key: root / f'{key}.json' for key in ('REGISTRY', 'ISSUES', 'STATE', 'OUTPUT', 'PIPELINE', 'PROGRESS')} rows = [{"id": "Sketcher:create", "module": "Sketcher", "command": "sketcher.createRecompute", "category": "modeling"}, {"id": "TechDraw:desktop:View", "module": "TechDraw", "command": "View", "kind": "desktop-gui"}] paths['REGISTRY'].write_text(json.dumps({'records': rows})) paths['ISSUES'].write_text(json.dumps({'issues': [{'id': 'bug', 'capabilityId': rows[1]['id'], 'status': 'open'}]})) paths['STATE'].write_text('{"active_task":"task"}') original = paths['REGISTRY'].read_bytes() with patch.multiple(features, **paths), patch.object(features, 'capability_source_fingerprint', return_value='stable'), \ patch.object(features, 'implementation_ready_ids', return_value=set()), patch.object(features, 'cooling_ids', return_value=set()), \ contextlib.redirect_stdout(io.StringIO()): features.main(['--delivery-only']) batch = json.loads(paths['OUTPUT'].read_text()) self.assertEqual([row[0] for row in batch['records']], ['Sketcher:create']) self.assertTrue(batch['selected']['family'].startswith('module-business-')) features.main(['--runtime-only']) self.assertEqual(json.loads(paths['OUTPUT'].read_text())['selected']['family'], 'runtime-issue') self.assertEqual(paths['REGISTRY'].read_bytes(), original) def test_delivery_handoff_is_not_interrupted_by_unrelated_historical_review(self): with tempfile.TemporaryDirectory() as directory: output = Path(directory) / 'batch.json' output.write_text('{"selected":{"module":"TechDraw","tier":"feature-workflow"}}') calls = [] def run(command, **kwargs): calls.append(command) chosen = command[1] == 'tools/prepare_business_acceptance_batch.py' and '--delivery-only' in command output.write_text(json.dumps({'status': 'ready', 'selectionScope': 'delivery', 'selected': {'module': 'Web'}} if chosen else {'status': 'no_ready_batch'})) return type('Result', (), {'returncode': 0, 'stdout': '', 'stderr': ''})() with patch.object(planner, 'BATCH', output), patch.object(planner, 'feature_phase_remaining', return_value=600), \ patch.object(planner.subprocess, 'run', side_effect=run), contextlib.redirect_stdout(io.StringIO()): self.assertEqual(planner.main(), 0) self.assertFalse(any('tools/prepare_failure_reconciliation_batch.py' in call for call in calls)) phases = planner.phase_order({}, 600) self.assertTrue(any(command == ['tools/prepare_feature_capability_batch.py'] for _, command in phases)) self.assertTrue(any(command == ['tools/prepare_business_acceptance_batch.py'] for _, command in phases)) def test_remaining_module_handoff_preserves_business_batch(self): for target in ("tools/prepare_feature_capability_batch.py", "tools/prepare_business_acceptance_batch.py"): with self.subTest(target=target), tempfile.TemporaryDirectory() as directory: output = Path(directory) / 'batch.json' output.write_text('{"selected":{"module":"Assembly","tier":"feature-workflow"}}') calls = [] selected = {"status": "ready", "batchId": "business-next", "selected": {"module": "Draft"}} def run(command, **kwargs): calls.append(command) chosen = command[1:] == [target] review = "tools/prepare_failure_reconciliation_batch.py" in command output.write_text(json.dumps(selected if chosen else {"status": "ready", "batchId": "historical-review"} if review else {"status": "no_ready_batch"})) return type('Result', (), {'returncode': 0, 'stdout': '', 'stderr': ''})() with patch.object(planner, 'BATCH', output), patch.object(planner, 'feature_phase_remaining', return_value=600), \ patch.object(planner.subprocess, 'run', side_effect=run), contextlib.redirect_stdout(io.StringIO()): self.assertEqual(planner.main(), 0) self.assertEqual(json.loads(output.read_text())['batchId'], 'business-next') self.assertFalse(any('tools/prepare_failure_reconciliation_batch.py' in call for call in calls)) def test_historical_review_remains_available_after_business_phases(self): phases = planner.phase_order({}, 600) self.assertEqual(phases[-1][1], ['tools/prepare_failure_reconciliation_batch.py']) self.assertIn(('P0-observed-runtime-repair', ['tools/prepare_feature_capability_batch.py', '--runtime-only']), phases) def test_persistence_uses_its_existing_document_worker_source(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) probes = root / 'probes' probes.mkdir() (probes / 'DocumentProbe.cpp').write_text('document source') (probes / 'CMakeLists.txt').write_text('add_executable(FreeCADWebDocumentProbe DocumentProbe.cpp)') manifest = root / 'web/public/modules/manifest.json' manifest.parent.mkdir(parents=True) manifest.write_text(json.dumps({'modules': {'Part': {'workers': {'part-save.worker.ts': {'runtimeUrl': '/kernel/FreeCADWebDocumentProbe.js'}}}}})) route = root / 'web/src/module-registry.generated.ts' route.parent.mkdir(parents=True) route.write_text('case "Part:part.saveReopen": return "part-save.worker.ts";') row = {'id': 'Part:save', 'command': 'part.saveReopen', 'category': 'persistence', 'worker': 'part.worker.ts'} with patch.multiple(packets, ROOT=root, PROBE_ROOT=probes): self.assertEqual(packets.workflow_worker_sources('Part', ['Part:save'], {'Part:save': row}), [probes / 'DocumentProbe.cpp']) row['kind'] = 'desktop-gui' self.assertEqual(packets.workflow_worker_sources('Part', ['Part:save'], {'Part:save': row}), []) def test_report_keeps_full_scope_and_does_not_count_receipts_as_acceptance(self): state = self.state() state['receipts']['Part:save'] = {'status': 'implementation_ready'} result = report(state) self.assertEqual(result['fullScope'], 7) self.assertEqual(result['deliverySubset'], 5) self.assertEqual(result['fullAccepted'], 0) self.assertEqual(sum(row['reusableReceipts'] for row in result['groups']), 1) self.assertEqual(len(state['pending']), 7) def test_product_acceptance_packet_targets_real_page_not_web_server_probe(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) current = root / 'docs/current' current.mkdir(parents=True) source = root / 'web/src/main.ts' source.parent.mkdir(parents=True) source.write_text('mountWorkspace();\n') (root / 'web/index.html').write_text('\n') (source.parent / 'web-facade.ts').write_text('unrelatedServerFixture();\n') (current / 'feature-capability-registry.json').write_text('{"records": []}') (current / 'runtime-issue-ledger.json').write_text('{"issues": []}') batch_path = current / 'batch.json' output_path = current / 'packet.json' batch_path.write_text(json.dumps({'status': 'ready', 'batchId': 'batch', 'selected': { 'module': 'Web', 'tier': 'business-acceptance', 'family': 'business-acceptance'}, 'records': [['product:react-primary-shell', 'React shell', 'acceptance_required', 'web/src/main.ts', 1]]})) with patch.multiple(packets, ROOT=root, BATCH_PATH=batch_path, OUTPUT_PATH=output_path, PROBE_ROOT=root / 'probes'), \ patch.object(packets, 'probe_sources', return_value=[]), patch.object(packets, 'reconcile_failures', return_value={}), \ patch.object(packets, 'module_context', return_value={}), contextlib.redirect_stdout(io.StringIO()): self.assertEqual(packets.main(), 0) packet = json.loads(output_path.read_text()) self.assertEqual(packet['build']['kind'], 'frontend') paths = {row['path'] for row in packet['editableExcerpts']} self.assertIn('web/src/main.ts', paths) self.assertIn('web/index.html', paths) self.assertNotIn('web/src/web-facade.ts', paths) if __name__ == '__main__': unittest.main()