From 9484aae7a28ec426884a12b2d0b2a8a2a2a37ebd Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 19 Jun 2026 07:48:31 -0400 Subject: [PATCH] test+docs(sandbox): add FR3 invariant regression tests + tech-stack note --- conductor/tech-stack.md | 6 ++++ tests/test_test_sandbox.py | 56 +++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/conductor/tech-stack.md b/conductor/tech-stack.md index 7bf584bb..4e79325c 100644 --- a/conductor/tech-stack.md +++ b/conductor/tech-stack.md @@ -86,6 +86,12 @@ - **Thread-Local Context Isolation:** Utilizes `threading.local()` for managing per-thread AI client context (e.g., source tier tagging), ensuring thread safety during concurrent multi-agent execution. - **Asynchronous Tool Execution Engine:** Refactored MCP tool dispatch and AI client loops to use `asyncio.gather` and `asyncio.to_thread`, enabling parallel execution of independent tool calls within a single AI turn to reduce latency. +## pyproject.toml pytest addopts (added 2026-06-19, per test_sandbox_hardening_20260619) + +`[tool.pytest.ini_options].addopts = "--basetemp=tests/artifacts/_pytest_tmp"`. + +**Rationale:** Per `conductor/code_styleguides/workspace_paths.md`, ALL test infrastructure paths must live under `./tests/`. pytest's `tmp_path` and `tmp_path_factory` fixtures default to `%TEMP%\pytest-of-\` on Windows. This `addopts` redirects them under `./tests/` so the FR1 runtime guard's allowlist (also `./tests/`) is a single rule. + ## Architectural Patterns - **Centralized Registry Management:** Consolidation of critical application constants (e.g., `PROVIDERS`, `AGENT_TOOL_NAMES`) into `src/models.py` as a single source of truth, eliminating redundant list definitions across the UI and Controller. diff --git a/tests/test_test_sandbox.py b/tests/test_test_sandbox.py index 4073c68f..b309a45a 100644 --- a/tests/test_test_sandbox.py +++ b/tests/test_test_sandbox.py @@ -191,4 +191,58 @@ def test_sloppy_py_parses_config_flag() -> None: if isinstance(func, ast.Name) and func.id == "set_config_override": found_set_override_call = True assert found_config_arg, "sloppy.py must have a --config argparse argument" - assert found_set_override_call, "sloppy.py must call paths.set_config_override(args.config)" \ No newline at end of file + assert found_set_override_call, "sloppy.py must call paths.set_config_override(args.config)" + + +def test_pyproject_toml_basetemp_is_under_tests() -> None: + """pyproject.toml contains --basetemp=tests/artifacts/_pytest_tmp.""" + pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml" + text = pyproject.read_text(encoding="utf-8") + assert "--basetemp=tests/artifacts/_pytest_tmp" in text, ( + "pyproject.toml must set addopts = '--basetemp=tests/artifacts/_pytest_tmp' " + "so the FR1 runtime guard's allowlist can be a single rule." + ) + + +def test_isolate_workspace_does_not_use_tmp_path_factory_for_infra() -> None: + """isolate_workspace fixture does not use tmp_path_factory.mktemp.""" + import ast + conftest = Path(__file__).resolve().parent / "conftest.py" + tree = ast.parse(conftest.read_text(encoding="utf-8")) + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef) and node.name == "isolate_workspace": + body = node.body + if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant): + body = body[1:] + body_src = "\n".join(ast.unparse(stmt) for stmt in body) + assert "tmp_path_factory.mktemp" not in body_src, ( + "isolate_workspace must not use tmp_path_factory.mktemp; " + "use _ISOLATION_WORKSPACE under tests/artifacts/ instead." + ) + assert "_ISOLATION_WORKSPACE" in body_src, ( + "isolate_workspace should reference _ISOLATION_WORKSPACE" + ) + return + raise AssertionError("isolate_workspace fixture not found in conftest.py") + + +def test_appcontroller_init_does_not_load_config() -> None: + """AppController.__init__ must not call init_state() or load_config() — + fixtures apply before App.__init__; loading config in AppController.__init__ + would race against the autouse isolate_workspace.""" + import ast + app_controller = Path(__file__).resolve().parent.parent / "src" / "app_controller.py" + tree = ast.parse(app_controller.read_text(encoding="utf-8")) + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef) and node.name == "__init__": + src = ast.unparse(node) + assert "init_state()" not in src, ( + "AppController.__init__ must not call init_state() " + "(this would trigger config reads before fixtures apply)" + ) + assert "load_config()" not in src, ( + "AppController.__init__ must not call load_config() " + "(this would trigger config reads before fixtures apply)" + ) + return + raise AssertionError("AppController.__init__ not found") \ No newline at end of file