Private
Public Access
feat(directives): scavenge sweep 4/5 (tracks + commands + styleguides + todos): 18 batch-4 directives + concurrent worker batches
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# deterministic_signal_endpoint_pattern
|
||||
|
||||
## v1
|
||||
|
||||
**Why this iteration:** Lifted from `conductor/todos/TODO_test_full_live_workflow.md` §Task 1 + §Task 4 — the deterministic signal endpoint pattern (`/api/project_switch_status` with `in_progress` / `path` / `error` fields) replaces fragile polling of derived state. The pattern was SHIPPED (commit `6ecb31ea`) as the structural fix for the `test_full_live_workflow` race condition.
|
||||
**Source:** `conductor/todos/TODO_test_full_live_workflow.md` §1 + §4 (commit `6ecb31ea`, `a6605d98`, `b6972c31`)
|
||||
|
||||
---
|
||||
|
||||
**Lifted:** 2026-07-03 scavenge sweep batch 4/5: tracks + commands + styleguides + todos
|
||||
@@ -0,0 +1,76 @@
|
||||
# Tests that wait for asynchronous state MUST poll a purpose-built signal endpoint — NOT poll derived state
|
||||
|
||||
## What it says
|
||||
|
||||
When a test or external automation needs to wait for an asynchronous state change (project switch, file creation, hook completion, etc.), it MUST poll a **purpose-built signal endpoint** that returns the exact state-of-interest (e.g., `/api/project_switch_status` returning `{"in_progress": bool, "path": str | null, "error": str | null}`). Do NOT poll derived state (the project dict, the file list, the hook log) — derived state is stale until the originating state settles.
|
||||
|
||||
## Why
|
||||
|
||||
Polling derived state is fragile because:
|
||||
|
||||
1. **Derived state lags.** The project dict updates AFTER `_do_project_switch` finishes. Polling the project dict during the switch returns stale state from the previous test.
|
||||
2. **Polling doesn't surface failure reasons.** If the switch fails, the project dict returns the OLD state forever; the test times out without knowing why.
|
||||
3. **Polling is opaque.** The test cannot distinguish "switch in progress" from "switch failed and state is back to old" without parsing logs.
|
||||
|
||||
A purpose-built signal endpoint gives the test:
|
||||
|
||||
1. **A single source of truth** for the in-progress state.
|
||||
2. **A failure reason** surfaced via an `error` field.
|
||||
3. **A bounded poll** that times out with a clear message instead of looping forever.
|
||||
4. **An idempotent retry contract** — the test can re-poll safely without side effects.
|
||||
|
||||
## Pattern (the canonical example)
|
||||
|
||||
```python
|
||||
def wait_for_project_switch(
|
||||
self,
|
||||
expected_path: str,
|
||||
timeout: float = 30.0,
|
||||
interval: float = 0.5,
|
||||
) -> str:
|
||||
"""Poll /api/project_switch_status until the switch completes or times out.
|
||||
|
||||
Returns the final path. Raises ApiTimeoutError if timeout exceeded or
|
||||
the error field is non-null.
|
||||
"""
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
status = self.get_project_switch_status()
|
||||
if not status["in_progress"]:
|
||||
if status["error"] is not None:
|
||||
raise ApiTimeoutError(
|
||||
f"project switch failed: {status['error']}"
|
||||
)
|
||||
if status["path"] != expected_path:
|
||||
raise ApiTimeoutError(
|
||||
f"project switch wrong path: "
|
||||
f"expected={expected_path}, got={status['path']}"
|
||||
)
|
||||
return status["path"]
|
||||
time.sleep(interval)
|
||||
raise ApiTimeoutError(
|
||||
f"project switch did not complete within {timeout}s"
|
||||
)
|
||||
```
|
||||
|
||||
## The forbidden pattern
|
||||
|
||||
```python
|
||||
# WRONG: poll derived state
|
||||
while True:
|
||||
project = client.get_project_dict()
|
||||
if project.get("active_path") == expected_path:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
# FAILS: stale state; no failure reason; opaque timeout
|
||||
```
|
||||
|
||||
## When to add a new signal endpoint
|
||||
|
||||
When a test needs to wait for:
|
||||
|
||||
- a state transition (file created, project switched, hook completed, panel opened)
|
||||
- a result that can fail (workflow ran, AI responded, batch completed)
|
||||
- a per-turn event in a multi-step process
|
||||
|
||||
…add a `/api/<thing>_status` endpoint and a `client.wait_for_<thing>()` helper. Do NOT add a derived-state poll helper to the client; the source of truth is the controller's `_handle_<thing>` state, exposed via the new endpoint.
|
||||
Reference in New Issue
Block a user