feat(directives): harvest 8 directives from feature_flags/RAG/cache/knowledge styleguides + 4 new styleguides
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# cache_stable_to_volatile — v1
|
||||
|
||||
**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/cache_friendly_context.md` §"1. The 12-layer model" (lines 22-43) + §"2. The byte-comparison test" (lines 51-77). This is the baseline encoding — the layered-table-plus-contract-test style currently in production.
|
||||
Future variants will test alternative encodings (rule-only, before/after) against this baseline.
|
||||
|
||||
**Source:** `conductor/code_styleguides/cache_friendly_context.md:22-43 + 51-77`
|
||||
|
||||
---
|
||||
|
||||
## 1. The 12-layer model (the stable-to-volatile ordering)
|
||||
|
||||
| # | Layer | Stable across turns? | Source | SSDL |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Role instructions (model + provider) | yes | `_get_combined_system_prompt` | `[I]` |
|
||||
| 2 | Function-calling schema | yes | per provider | `[I]` |
|
||||
| 3 | Discovered tool descriptions | yes | `mcp_client.get_tool_schemas()` | `[I]` |
|
||||
| 4 | System prompt preset | yes | `app_state.ai_settings.system_prompt` | `[I]` |
|
||||
| 5 | Persona profile | yes | `app_state.active_persona` | `[I]` |
|
||||
| 6 | Project context (per `manual_slop.toml`) | yes | NEW (Candidate 14) | `[I]` |
|
||||
| 7 | Knowledge digest (per `knowledge/digest.md`) | yes (within a gc cycle) | NEW (Candidate 8) | `[I]` |
|
||||
| 8 | Discussion metadata (name, role count) | no (per turn) | `disc_entries[:1]` or `disc_meta` | `───` (data) |
|
||||
| 9 | Active preset (FileItem set) | no (per turn) | `self.context_files` | `───` (data) |
|
||||
| 10 | Per-file details (history, slices, notes) | no (per file) | per `FileItem` | `───` (data) |
|
||||
| 11 | Tool-call results from prior turns | no (per turn) | per `_reread_file_items` | `───` (data) |
|
||||
| 12 | The user message | no (per turn) | the input | `───` (data) |
|
||||
|
||||
**The cache boundary is at layer 7/8.** Layers 1-7 are byte-identical across turns of the same discussion (and across discussions of the same mode). Layers 8-12 change per turn.
|
||||
|
||||
---
|
||||
|
||||
## 2. The byte-comparison test (the design contract)
|
||||
|
||||
The design rule "stable prefix is byte-identical" must be testable. The test:
|
||||
|
||||
```python
|
||||
# In tests/test_aggregate_caching.py (NEW)
|
||||
def test_aggregate_stable_to_volatile_ordering():
|
||||
"""The first N characters of the context should be identical across turns
|
||||
of the same conversation, when no stable-layer inputs change."""
|
||||
ctrl = mock_app_controller()
|
||||
ctrl.ai_settings.system_prompt = "Test system prompt"
|
||||
ctrl.active_persona = mock_persona()
|
||||
|
||||
# Turn 1
|
||||
turn1 = aggregate.build_initial_context(ctrl, user_message="first prompt")
|
||||
|
||||
# Turn 2 (same stable inputs, different user message)
|
||||
turn2 = aggregate.build_initial_context(ctrl, user_message="second prompt")
|
||||
|
||||
# The first N characters should be identical (N = where the volatile layers start)
|
||||
N = aggregate.stable_prefix_length(ctrl)
|
||||
assert turn1[:N] == turn2[:N], f"Stable prefix mismatch: {turn1[:N]!r} != {turn2[:N]!r}"
|
||||
```
|
||||
|
||||
**The test is the contract.** If a new layer is added in the middle of the stack, this test fails; the agent must either move the layer to the stable position or update the test (with written justification).
|
||||
@@ -0,0 +1,72 @@
|
||||
# chroma_cache_path — v1
|
||||
|
||||
**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/chroma_cache.md` §"The Rule" + §"The Pre-Cleanup Pattern" (lines 5-50). This is the baseline encoding — the rule-plus-rationale-plus-anti-patterns style currently in production.
|
||||
Future variants will test alternative encodings (rule-only, before/after) against this baseline.
|
||||
|
||||
**Source:** `conductor/code_styleguides/chroma_cache.md:5-50`
|
||||
|
||||
---
|
||||
|
||||
# Chroma Cache Path Styleguide
|
||||
|
||||
## The Rule
|
||||
|
||||
The ChromaDB persistent vector cache lives at:
|
||||
|
||||
```
|
||||
<project_root>/tests/artifacts/.slop_cache/chroma_<collection_name>/
|
||||
```
|
||||
|
||||
**NOT** at the per-run `tests/artifacts/live_gui_workspace_<timestamp>/` subdir.
|
||||
|
||||
Tests that interact with RAG **MUST** pre-clean the cache to avoid persistent state from prior tests in the batched run.
|
||||
|
||||
## Why This Rule Exists
|
||||
|
||||
The chroma cache path is auto-derived from `RAGEngine._init_vector_store()` (`src/rag_engine.py:108-125`):
|
||||
|
||||
```python
|
||||
db_path = os.path.abspath(os.path.join(
|
||||
self.base_dir, ".slop_cache", f"chroma_{vs_config.collection_name}"
|
||||
))
|
||||
```
|
||||
|
||||
`self.base_dir` is computed as `Path(active_project_path).parent`. **The trailing-slash bug**: when the test config produces a project path ending in `/` (e.g., from `os.path.join` with a trailing `/`), `Path(p).parent` returns the directory ONE LEVEL HIGHER than expected. So the chroma cache lands at `tests/artifacts/.slop_cache/` (the parent of the per-run `live_gui_workspace_<timestamp>/` subdir) instead of inside the per-run subdir.
|
||||
|
||||
## The Pre-Cleanup Pattern
|
||||
|
||||
RAG tests should wipe the chroma cache BEFORE pushing RAG config. The pattern is in `tests/test_rag_phase4_final_verify.py`:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
def test_phase4_final_verify(live_gui):
|
||||
# Wipe any stale chroma from prior batched runs
|
||||
cache = Path("tests/artifacts/.slop_cache/chroma_test_final_verify")
|
||||
if cache.exists():
|
||||
shutil.rmtree(cache, ignore_errors=True)
|
||||
# ... rest of test
|
||||
```
|
||||
|
||||
`ignore_errors=True` is required because:
|
||||
- On Windows, the chroma client may still hold file handles; `rmtree` may fail with `WinError 32` (sharing violation).
|
||||
- If a parallel xdist worker is mid-write, the rmtree can race; `ignore_errors` lets the next worker's write retry.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
❌ **Assuming the cache is per-run:**
|
||||
|
||||
```python
|
||||
def test_rag(live_gui, live_gui_workspace):
|
||||
# WRONG: live_gui_workspace is a per-run subdir, but the chroma
|
||||
# cache is at tests/artifacts/.slop_cache/, NOT under live_gui_workspace
|
||||
cache = live_gui_workspace / ".slop_cache" / "chroma_test"
|
||||
if cache.exists():
|
||||
shutil.rmtree(cache) # Doesn't find the actual cache
|
||||
```
|
||||
|
||||
❌ **Not pre-cleaning at all:**
|
||||
```python
|
||||
def test_rag(live_gui):
|
||||
# WRONG: no pre-cleanup. If a prior batched run with a different
|
||||
@@ -0,0 +1,70 @@
|
||||
# config_state_owner — v1
|
||||
|
||||
**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/config_state_owner.md` (entire file, lines 1-82). This is the baseline encoding — the rule-plus-allowed/forbidden-lists style currently in production.
|
||||
Future variants will test alternative encodings (rule-only, before/after) against this baseline.
|
||||
|
||||
**Source:** `conductor/code_styleguides/config_state_owner.md:1-82`
|
||||
|
||||
---
|
||||
|
||||
# Config I/O State Ownership
|
||||
|
||||
**Rule:** The `AppController` is the single source of truth for the
|
||||
in-memory config (`self.config`) and the only authorized caller of
|
||||
the file I/O primitives (the config-load/save helpers, which moved out
|
||||
of `src/models.py` to `src/project_manager.py` per
|
||||
`module_taxonomy_refactor_20260627`).
|
||||
|
||||
## Why
|
||||
|
||||
1. **The controller owns the in-memory state.** If other modules
|
||||
write to `config.toml` directly, the controller's `self.config`
|
||||
silently drifts from disk. Tests can corrupt the user's TOML
|
||||
files; users lose data without warning.
|
||||
2. **Test isolation breaks.** When `models.save_config(...)` is
|
||||
called from anywhere in `src/`, tests cannot intercept the
|
||||
write without patching the I/O primitive. The test then
|
||||
couples to the file format, not the controller's behavior.
|
||||
3. **Path resolution can't be enforced.** The controller respects
|
||||
`SLOP_CONFIG` env var at call time. Direct calls to
|
||||
`models.save_config` would only respect it if the path is
|
||||
re-resolved (which it is in `_save_config_to_disk`, but only
|
||||
because someone remembered).
|
||||
|
||||
## What is Forbidden in `src/`
|
||||
|
||||
- `models.load_config(...)` (legacy public function)
|
||||
- `models.save_config(...)` (legacy public function)
|
||||
- `models._load_config_from_disk(...)` (private I/O primitive)
|
||||
- `models._save_config_to_disk(...)` (private I/O primitive)
|
||||
|
||||
The only allowed call sites are inside `AppController` itself
|
||||
(`load_config()` and `save_config()` methods).
|
||||
|
||||
## The Public API
|
||||
|
||||
```python
|
||||
# In AppController:
|
||||
def load_config(self) -> Dict[str, Any]:
|
||||
"""Re-read the global config.toml from disk and update self.config."""
|
||||
self.config = models._load_config_from_disk()
|
||||
return self.config
|
||||
|
||||
def save_config(self) -> None:
|
||||
"""Flush self.config to disk."""
|
||||
models._save_config_to_disk(self.config)
|
||||
```
|
||||
|
||||
Callers (including `gui_2.py`, `commands.py`, etc.) go through
|
||||
the controller:
|
||||
|
||||
```python
|
||||
# In App class methods (gui_2.py): __getattr__ delegates to controller
|
||||
self.save_config() # -> controller.save_config()
|
||||
app.save_config() # -> controller.save_config() (via __getattr__)
|
||||
app.load_config() # -> controller.load_config() (via __getattr__)
|
||||
|
||||
# In AppController:
|
||||
self.save_config() # direct
|
||||
self.load_config() # direct
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
# feature_flag_delete_to_turn_off — v1
|
||||
|
||||
**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/feature_flags.md` §"0. The two patterns (the one-glance table)" (lines 13-21) + §1 + §2 + §3 (lines 22-143). This is the baseline encoding — the one-glance-table-plus-rationale style currently in production.
|
||||
Future variants will test alternative encodings (rule-only, decision-tree) against this baseline.
|
||||
|
||||
**Source:** `conductor/code_styleguides/feature_flags.md:13-143`
|
||||
|
||||
---
|
||||
|
||||
## 0. The two patterns (the one-glance table)
|
||||
|
||||
| Pattern | How it works | How to turn off | How to turn on |
|
||||
|---|---|---|---|
|
||||
| **File presence** | The feature checks for the file's existence; the file is the switch | `rm <file>` | Touch the file (or run the generator that creates it) |
|
||||
| **Config flag** | The feature checks a setting in `[ai_settings.toml]` / `[manual_slop.toml]`; the GUI checkbox is the surface | Set `enabled = false` in the config; or uncheck the GUI box | Set `enabled = true`; or check the GUI box |
|
||||
| **CLI flag** (a sub-pattern of config) | The CLI accepts a flag like `--no-cache`; the default behavior is "on" | Pass `--no-cache` on the CLI | Omit the flag (use the default) |
|
||||
| **Feature flag in metadata** (a sub-pattern) | A `metadata.json` field for the feature's track declares `uses_rag: true` | Edit the metadata | Edit the metadata |
|
||||
|
||||
## 1. When to use file presence (the "delete to turn off" pattern)
|
||||
|
||||
**Use file presence when:**
|
||||
- The feature generates a *side artifact* that the user might want to *turn off* by deleting the artifact
|
||||
- The "off" state is *recoverable* — the artifact can be regenerated by running a command
|
||||
- The user *expects* to be able to manage the feature via the filesystem (the user is on the command line; they know `rm`)
|
||||
- The feature is *opt-in by default-off* (deleting the artifact means the feature is off; the absence of the file is the "off" state)
|
||||
|
||||
**Examples in Manual Slop:**
|
||||
|
||||
| Feature | The "on" state | The "off" state | The regeneration command |
|
||||
|---|---|---|---|
|
||||
| Knowledge digest injection | `~/.manual_slop/knowledge/digest.md` exists | File is deleted | `python -m src.knowledge_harvest --apply` |
|
||||
| Per-file knowledge for file X | `~/.manual_slop/knowledge/files/{file_id}.md` exists | File is deleted | (the next harvest regenerates) |
|
||||
| Saved conversations index | `~/.manual_slop/conversations/index-saved-conversations-*.json` exists | File is deleted | (n/a; user manually saves) |
|
||||
| RAG index for project | `~/.manual_slop/.slop_cache/chroma_<provider>/` exists | Directory is deleted | `python -m src.rag_engine --rebuild-index` |
|
||||
| Audit log | `~/.manual_slop/logs/sessions/<session>/comms.log` exists | File is deleted | (n/a; the log is auto-generated per turn) |
|
||||
|
||||
**The principle (per the data-oriented foundation):** *the data is the thing*. If the feature produces a file, the file is the switch. Deleting the file is the natural way to turn off the feature.
|
||||
|
||||
**The discovery surface:** the user can `ls ~/.manual_slop/knowledge/` and see `digest.md` (or not) and understand the state.
|
||||
|
||||
**The ux surface:** the GUI shows the file state and provides a `[Delete to turn off]` button that does the same `rm` underneath.
|
||||
|
||||
## 2. When to use config flags (the `[ai_settings.toml]` pattern)
|
||||
|
||||
**Use config flags when:**
|
||||
- The feature is *always on* by default; the flag is a way to *opt out* in special circumstances
|
||||
- The "off" state is *not recoverable* by a single command (it's a persistent preference)
|
||||
- The user *expects* to manage the feature via the GUI (they're not on the command line)
|
||||
- The feature's behavior is *complex* (multiple settings, not just on/off)
|
||||
- The setting is *user-specific* (different users might have different preferences)
|
||||
@@ -0,0 +1,39 @@
|
||||
# knowledge_harvest_pattern — v1
|
||||
|
||||
**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/knowledge_artifacts.md` §"0. The one-glance directory layout" (lines 9-26) + §1 (lines 28-44). This is the baseline encoding — the directory-tree-plus-rationale style currently in production.
|
||||
Future variants will test alternative encodings (rule-only, table-only) against this baseline.
|
||||
|
||||
**Source:** `conductor/code_styleguides/knowledge_artifacts.md:9-44`
|
||||
|
||||
---
|
||||
|
||||
## 0. The one-glance directory layout
|
||||
|
||||
```
|
||||
~/.manual_slop/knowledge/
|
||||
├── facts.md # - {statement} {provenance}
|
||||
├── decisions.md # - {statement, reason} {provenance}
|
||||
├── questions.md # - {question} {provenance}
|
||||
├── playbooks.md # - **{name}**: {steps} {provenance}
|
||||
├── tasks.md # ## Open / ## Done
|
||||
├── files/
|
||||
│ └── {file_id}.md # per-file notes (keyed by inode)
|
||||
├── digest.md # bounded 4KB; the projection; "delete to turn off"
|
||||
├── ledger.json # sha256-of-content audit log
|
||||
└── prompts/
|
||||
└── harvest-conversation.md # user-editable harvest prompt
|
||||
```
|
||||
|
||||
## 1. The category files (the source of truth)
|
||||
|
||||
### 1.1 `facts.md` (durable statements)
|
||||
|
||||
```markdown
|
||||
# Facts
|
||||
|
||||
- The MCP dispatch uses a flat if/elif chain. 4 places, 45 tools. [from: 2026-05-12-investigate-dispatch, 2026-05-12]
|
||||
- ai_client.py has 5 separate per-provider history lists, each with their own lock. Switching providers mid-session loses history. [from: 2026-05-13-state-mutation-matrix, 2026-05-13]
|
||||
- RAG is opt-in. Default-off in new projects. [from: 2026-06-12-rag-discipline, 2026-06-12]
|
||||
```
|
||||
|
||||
**The shape:** `- {statement} {provenance}`. Plain markdown. Append-only. User-editable.
|
||||
@@ -0,0 +1,19 @@
|
||||
# rag_six_rules — v1
|
||||
|
||||
**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/rag_integration_discipline.md` §"0. The 6 rules (the one-glance table)" (lines 11-20). This is the baseline encoding — the one-glance-table-with-why-column style currently in production.
|
||||
Future variants will test alternative encodings (rule-only, rationale-first) against this baseline.
|
||||
|
||||
**Source:** `conductor/code_styleguides/rag_integration_discipline.md:11-20`
|
||||
|
||||
---
|
||||
|
||||
## 0. The 6 rules (the one-glance table)
|
||||
|
||||
| # | Rule | Why |
|
||||
|---|---|---|
|
||||
| 1 | RAG is **opt-in**. Default-off in new projects | Most features don't need it; the cost of unnecessary RAG is the embedding-provider round trip + the storage cost |
|
||||
| 2 | RAG **complements**; it never **replaces** | Curation / Discussion / Knowledge are the durable, user-editable dimensions; RAG is the fuzzy, semantic search |
|
||||
| 3 | RAG results display with **provenance** | The user needs to know which file and which chunk produced the result |
|
||||
| 4 | RAG **never mutates state** | No auto-injection of RAG results into `disc_entries`; no auto-update of `FileItem`; no auto-write to disk |
|
||||
| 5 | RAG integration is **feature-gated** | A feature must explicitly request RAG in its scope; RAG is not the default for "give me context" |
|
||||
| 6 | RAG failure is **graceful** | A failed search returns `Result.empty` or an empty list; never crashes the request |
|
||||
@@ -0,0 +1,49 @@
|
||||
# test_sandbox — v1
|
||||
|
||||
**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/test_sandbox.md` §"The 4-Layer Model" + §"The `--config` CLI Flag" + §"The `--basetemp` Rule" (lines 5-58). This is the baseline encoding — the layered-table-plus-mechanism style currently in production.
|
||||
Future variants will test alternative encodings (rule-only, before/after) against this baseline.
|
||||
|
||||
**Source:** `conductor/code_styleguides/test_sandbox.md:5-58`
|
||||
|
||||
---
|
||||
|
||||
## The 4-Layer Model
|
||||
|
||||
| Layer | Mechanism | Where | Default-on? |
|
||||
|---|---|---|---|
|
||||
| Layer 1 | Python runtime file-I/O guard (`sys.addaudithook`) | `tests/conftest.py:_sandbox_audit_hook` | Yes |
|
||||
| Layer 2 | `isolate_workspace` autouse + `pyproject.toml --basetemp` | `tests/conftest.py` + `pyproject.toml` | Yes |
|
||||
| Layer 3 | OS-level restricted-token PowerShell wrapper | `scripts/run_tests_sandboxed.ps1` | **Opt-in** |
|
||||
| Layer 4 | Static audit script (CI gate) | `scripts/audit_test_sandbox_violations.py` | Yes (informational) / opt-in (`--strict`) |
|
||||
|
||||
Layer 1 + Layer 2 + Layer 4 are file-presence-on = enabled (delete the relevant file to disable). Layer 3 requires explicit invocation.
|
||||
|
||||
## The `--config` CLI Flag (replaces `SLOP_CONFIG`)
|
||||
|
||||
The historical `SLOP_CONFIG` env var has been removed from `src/paths.py`. The CLI flag `--config <path>` is now the ONLY supported mechanism for overriding the default `<project_root>/config.toml` location.
|
||||
|
||||
```bash
|
||||
# Use the default <project_root>/config.toml
|
||||
uv run python sloppy.py
|
||||
|
||||
# Override
|
||||
uv run python sloppy.py --config /path/to/your/config.toml
|
||||
```
|
||||
|
||||
`sloppy.py` calls `paths.set_config_override(Path(args.config).resolve())` AFTER `parse_args()` and BEFORE any `from src.gui_2 import App` import. This is the only way to override the config path in production.
|
||||
|
||||
## The `--basetemp` Rule
|
||||
|
||||
`pyproject.toml` sets `addopts = "--basetemp=tests/artifacts/_pytest_tmp"`. This redirects pytest's `tmp_path` and `tmp_path_factory` fixtures (which default to `%TEMP%\pytest-of-<user>\` on Windows) into `./tests/artifacts/`. This is what allows the Layer 1 allowlist to be a single rule: "anything under `./tests/` is allowed."
|
||||
|
||||
## Layer 1 Audit Hook Contract
|
||||
|
||||
`tests/conftest.py:_sandbox_audit_hook` is a `sys.addaudithook` callback. It fires on every `open()` call. Behavior:
|
||||
|
||||
- **Reads** (mode `r`, `rb`): pass through, no check
|
||||
- **Writes** (mode contains `w`, `a`, `x`, `+`): check path
|
||||
- **Allowed** if path resolves under:
|
||||
- `<project_root>/tests/`
|
||||
- Path contains `.pytest_cache`, `__pycache__`, `.coverage`, `.slop_cache`, or `.ruff_cache`
|
||||
- Original path string starts with `\\.\` (Windows device namespace) or `/dev/` (Unix device namespace)
|
||||
- **Blocked** otherwise: raises `RuntimeError("TEST_SANDBOX_VIOLATION: attempted to write to <path>...")`
|
||||
@@ -0,0 +1,82 @@
|
||||
# workspace_paths — v1
|
||||
|
||||
**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/workspace_paths.md` (lines 1-103). This is the baseline encoding — the rule-plus-forbidden-patterns-plus-rationale-history style currently in production.
|
||||
Future variants will test alternative encodings (rule-only, tabular) against this baseline.
|
||||
|
||||
**Source:** `conductor/code_styleguides/workspace_paths.md:1-103`
|
||||
|
||||
---
|
||||
|
||||
# Test Workspace Paths — Hard Rule
|
||||
|
||||
## TL;DR
|
||||
|
||||
Test workspaces live in the project tree under `tests/artifacts/`. Conftest creates them. No env vars. No CLI args. No `tmp_path_factory`. No `%TEMP%`. No runner changes. **The user must be able to find every test workspace by looking in `tests/artifacts/`.**
|
||||
|
||||
## The Rule
|
||||
|
||||
When creating a test workspace, fixture, or scratch directory for any test infrastructure:
|
||||
|
||||
```python
|
||||
# CORRECT — conftest creates the path
|
||||
from datetime import datetime
|
||||
_RUN_ID = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
_RUN_WORKSPACE = Path(f"tests/artifacts/live_gui_workspace_{_RUN_ID}")
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def live_gui(request):
|
||||
temp_workspace = _RUN_WORKSPACE
|
||||
...
|
||||
```
|
||||
|
||||
```python
|
||||
# WRONG — env vars
|
||||
import os
|
||||
WORKSPACE = os.environ.get("LIVE_GUI_WORKSPACE", "tests/artifacts/live_gui_workspace")
|
||||
|
||||
# WRONG — CLI args
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--workspace", action="store", default="tests/artifacts/live_gui_workspace")
|
||||
|
||||
# WRONG — tmp_path_factory (lives in %TEMP%, not in project tree)
|
||||
def live_gui(request, tmp_path_factory):
|
||||
temp_workspace = tmp_path_factory.mktemp("live_gui_workspace")
|
||||
# Creates: C:\Users\<user>\AppData\Local\Temp\pytest-of-<user>\pytest-N\live_gui_workspace0
|
||||
# User CANNOT FIND THIS from the project tree.
|
||||
```
|
||||
|
||||
## Why This Rule Exists
|
||||
|
||||
This rule was added 2026-06-09 after a 4-day agent churn on workspace paths. The chain of decisions:
|
||||
|
||||
1. Original conftest: `temp_workspace = Path("tests/artifacts/live_gui_workspace")`. Sims worked. User could find the workspace. **This was correct.**
|
||||
|
||||
2. Phase 3 of test_infrastructure_hardening_20260609: agent changed it to `tmp_path_factory.mktemp("live_gui_workspace")`. The user did not catch this for 2 days. It moved the workspace to `%TEMP%/pytest-of-<user>/...` which:
|
||||
- The user cannot find from the project tree
|
||||
- The sims (which compute `os.path.abspath("tests/artifacts/...")` from the project root) could not find the workspace either
|
||||
- Caused `test_extended_sims.py::test_context_sim_live` to fail with "stale ui - ops disabled" because the sim's project path didn't match the controller's active_project_path
|
||||
- The agent then spent 2 more days trying to fix the sim timing, the MMA state, the RAG state, the watchdog — none of which were the actual cause
|
||||
|
||||
3. The user caught the regression. Their feedback: "we should be using a folder in `./tests/`" — i.e., the project tree, not the system temp dir.
|
||||
|
||||
4. The agent tried `Path("tests/artifacts/live_gui_workspace")` (no timestamp). That solved the sim issue but was per-session, not per-run. Per-test pollution is desirable (it exposes fragility), so per-run isolation is what we want.
|
||||
|
||||
5. The user pushed back on adding CLI args: "have conftest make it, conftest is the right place." The agent then tried env vars as an indirection layer.
|
||||
|
||||
6. The user rejected env vars: "env vars are hidden global state, pass it to conftest directly." Conftest is the source of truth.
|
||||
|
||||
7. Final solution: conftest creates a per-run timestamped folder under `tests/artifacts/`. One source of truth. No indirection. The user must be able to find every test workspace by looking in `tests/artifacts/`.
|
||||
|
||||
## Forbidden Patterns (Hard Bans)
|
||||
|
||||
### 1. `tmp_path_factory` for test infrastructure workspaces
|
||||
|
||||
`tmp_path_factory` is for pytest's own test isolation (e.g., when a unit test needs a temp dir to write a file). It is **NOT** for test infrastructure workspaces (e.g., the `live_gui` subprocess's CWD).
|
||||
|
||||
### 2. Environment variables for test paths
|
||||
|
||||
Env vars are hidden global state. The user has explicitly banned them.
|
||||
|
||||
### 3. CLI args for test paths
|
||||
|
||||
The conftest is the right place. CLI args add a layer of indirection between the runner and the test, and they require the runner to be modified to pass them.
|
||||
Reference in New Issue
Block a user