diff --git a/just/dev.just b/just/dev.just index f44e26ae4..7bfbe180f 100644 --- a/just/dev.just +++ b/just/dev.just @@ -92,7 +92,7 @@ test kind="all": case "{{kind}}" in fast) ROBOCLAWS_PYTEST_CLEAR_PROVIDER_ENV=1 {{standalone_pytest}} \ - -m "not integration" -q --durations=5 + -m "not integration and not local" -q --durations=5 ;; unit) diff --git a/roboclaws/operator_console/runtime_inventory.py b/roboclaws/operator_console/runtime_inventory.py index 392480077..26bd3d5e6 100644 --- a/roboclaws/operator_console/runtime_inventory.py +++ b/roboclaws/operator_console/runtime_inventory.py @@ -55,7 +55,7 @@ def runtime_inventory_payload( """Return a redacted inventory of repo-relevant local background tasks.""" root = root.resolve() - port_list = _dedupe_ints([*(ports or []), DEFAULT_MCP_PORT]) + port_list = _dedupe_ints([DEFAULT_MCP_PORT] if ports is None else ports) tasks: list[dict[str, Any]] = [] tasks.extend(_operator_console_tasks(root, include_recent_terminal=include_recent_terminal)) tasks.extend(_eval_harness_tasks(root, include_recent_terminal=include_recent_terminal)) diff --git a/roboclaws/operator_console/server.py b/roboclaws/operator_console/server.py index 6c8ca9c45..4b25c4fd0 100644 --- a/roboclaws/operator_console/server.py +++ b/roboclaws/operator_console/server.py @@ -366,7 +366,7 @@ def _serve_runtime_tasks(self, query_string: str) -> None: ports.append(int(value)) except ValueError: continue - self._json(runtime_blockers_payload(self.repo_root, ports=ports)) + self._json(runtime_blockers_payload(self.repo_root, ports=ports or None)) def _serve_route_readiness(self, query_string: str) -> None: try: diff --git a/tests/conftest.py b/tests/conftest.py index 2a17cdd48..db1d2736d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,48 @@ "test_code_mcp_binding_smoke.py", } +LOCAL_ASSET_MODULES = { + # These validate private B1 / Agibot map exports or robot-data-lab scene + # assets that are present on local workstations but not in GitHub checkout. + "test_b1_map12_alignment_fit_cli.py", + "test_b1_map12_base_navigation_map.py", + "test_b1_map12_base_navigation_sidecar.py", + "test_b1_map12_correspondence_review_cli.py", + "test_b1_map12_label_tool.py", + "test_b1_map12_manual_alignment_overlay_cli.py", + "test_b1_map12_verified_alignment.py", + "test_b1_scene_topdown_diagnostic.py", + "test_molmospaces_source_pin.py", +} + +LOCAL_ASSET_TESTS = { + "test_agibot_map_context_scripts.py": { + "test_agibot_nav_json_artifact_source_rejects_invalid_payloads", + "test_agibot_nav_raw_map_source_rejects_malformed_gzip_json", + "test_agibot_nav_raw_map_source_rejects_non_object_gzip_json", + "test_agibot_nav_raw_map_source_rejects_plain_json_file", + }, + "test_base_waypoint_builder.py": { + "test_base_waypoint_builder_preserves_b1_map12_waypoints", + }, + "test_check_molmo_realworld_cleanup_result.py": { + "test_checker_accepts_b1_robot_consumption_proof_without_rby1m_readiness", + "test_checker_rejects_b1_robot_consumption_manifest_drift", + "test_checker_rejects_b1_robot_consumption_without_manifest", + "test_checker_rejects_b1_robot_consumption_without_verified_navigation", + }, + "test_cross_environment_semantic_map_parity.py": { + "test_b1_uses_dt_room_reference_and_alignment_correspondence_manifest", + }, + "test_nav2_map_bundle_contract.py": { + "test_base_navigation_map_v1_validation_accepts_b1_bundle", + "test_base_navigation_map_v1_validation_rejects_contract_violations", + }, + "test_scene_room_semantic_overlay.py": { + "test_b1_base_navigation_map_materializes_review_labels_without_retargeting_map", + }, +} + LAYER_DIRS = ("local", "slow", "integration", "contract", "regression", "unit") REGRESSION_NAME_PARTS = ( @@ -39,6 +81,7 @@ EXPLICIT_LAYER_MARKERS = ("local", "slow", "integration", "contract", "regression", "unit") +@pytest.hookimpl(tryfirst=True) def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None: for item in items: layer = _layer_for_item(item) @@ -51,20 +94,30 @@ def _layer_for_item(item: pytest.Item) -> str: return marker_name path = Path(str(item.path)) - try: - relative_parts = path.relative_to(Path(str(item.config.rootpath)) / "tests").parts - except ValueError: - relative_parts = () - if relative_parts and relative_parts[0] in LAYER_DIRS: - return relative_parts[0] - filename = path.name stem = filename.removeprefix("test_").removesuffix(".py") + if filename in LOCAL_ASSET_MODULES: + return "local" + if item.name.split("[", 1)[0] in LOCAL_ASSET_TESTS.get(filename, set()): + return "local" if filename in INTEGRATION_MODULES: return "integration" + directory_layer = _directory_layer(item, path) + if directory_layer: + return directory_layer if any(part in stem for part in CONTRACT_NAME_PARTS): return "contract" if any(part in stem for part in REGRESSION_NAME_PARTS): return "regression" return "unit" + + +def _directory_layer(item: pytest.Item, path: Path) -> str: + try: + relative_parts = path.relative_to(Path(str(item.config.rootpath)) / "tests").parts + except ValueError: + return "" + if relative_parts and relative_parts[0] in LAYER_DIRS: + return relative_parts[0] + return "" diff --git a/tests/unit/operator_console/test_runtime_inventory.py b/tests/unit/operator_console/test_runtime_inventory.py index f0cd7d8cc..4b854b175 100644 --- a/tests/unit/operator_console/test_runtime_inventory.py +++ b/tests/unit/operator_console/test_runtime_inventory.py @@ -200,8 +200,8 @@ def fake_run(command: list[str], **kwargs) -> SimpleNamespace: # noqa: ANN003 return SimpleNamespace(returncode=1, stdout="") with patch("roboclaws.operator_console.runtime_inventory.subprocess.run", side_effect=fake_run): - payload = runtime_inventory_payload(tmp_path) - blockers = runtime_blockers_payload(tmp_path) + payload = runtime_inventory_payload(tmp_path, ports=[]) + blockers = runtime_blockers_payload(tmp_path, ports=[]) task = next(item for item in payload["tasks"] if item["owner"] == "docker") assert task["id"] == "source-error:docker:container-a:mounts" @@ -373,7 +373,10 @@ def test_runtime_inventory_surfaces_invalid_nested_runtime_json_resources( assert blockers["summary"]["active"] == 0 -def test_runtime_inventory_marks_dead_eval_harness_live_row_stale(tmp_path: Path) -> None: +def test_runtime_inventory_marks_dead_eval_harness_live_row_stale( + tmp_path: Path, + free_tcp_port: int, +) -> None: row_dir = tmp_path / "output" / "eval-harness" / "focused" / "rows" / "codex-cleanup-live" run_dir = row_dir / "run" / "0615_1225" / "seed-7" run_dir.mkdir(parents=True) @@ -384,7 +387,7 @@ def test_runtime_inventory_marks_dead_eval_harness_live_row_stale(tmp_path: Path (run_dir / "tmux_session.txt").write_text("roboclaws-molmo-codex-dead\n", encoding="utf-8") (run_dir / "server.pid").write_text("99999999\n", encoding="utf-8") (run_dir / "visual_backend_slot.json").write_text( - json.dumps({"slot_id": 1, "pid": 99999999, "port": 18788}), + json.dumps({"slot_id": 1, "pid": 99999999, "port": free_tcp_port}), encoding="utf-8", ) (tmp_path / "output" / "eval-harness" / "focused" / "eval_harness.json").write_text( @@ -411,7 +414,7 @@ def test_runtime_inventory_marks_dead_eval_harness_live_row_stale(tmp_path: Path encoding="utf-8", ) - payload = runtime_inventory_payload(tmp_path, ports=[18788]) + payload = runtime_inventory_payload(tmp_path, ports=[free_tcp_port]) task = next(item for item in payload["tasks"] if item["id"] == "eval-row:codex-cleanup-live") assert task["status"] == "stale"