fix(codex): resolve peer-review findings R1-R6 and T1-T2

- R1: use herdr send-keys C-c for graceful stop
- R2: preserve JSONL fallback discovery when database is empty
- R3: apply workspace-scoped SQLite query before bounding results
- R4: tighten artifact validation to fail closed on missing/invalid metadata
- R5: verify actual rollout presence before reporting resumable in status
- R6: remove unconditional drift-B backend command fallback
- T1: parameterize artifact metadata regression with verified finder fixture
- T2: seed unmanaged and managed sessions in mock herdr for drift-B testing
- Add final loop review reports (100% PASS)
This commit is contained in:
Godopu committed 2026-09-06 00:18:19 +09:00
1 parent 76f10b2096
commit 47640e2fd7
10 files changed
+504 -34

No files matched your search

@@ -0,0 +1,60 @@
# Job 155f5092 Final Report: Resolution of Follow-Up Findings T1 & T2
- **Job ID**: 155f5092
- **Reviewer Target**: `planner-reviewer-codex-01` (`ff721ed1`)
- **Status**: Completed (Both regression test findings T1 and T2 resolved and mutation-verified)
---
## 1. Summary of Changes
### T1 (P2): Invalid-metadata assertions only exercise "file not found"
- **Problem**: In `test_codex_verify_artifact_metadata_fail_closed`, the fixture file was placed outside the discovery path without the finder override being active for cases 1–5. Consequently, `verify_artifact` returned `False` on file existence before reaching metadata parsing, which allowed the old buggy verifier (from commit `76f10b2`) to pass the test.
- **Fix**:
- In `tests/test_a4_adapter_contract.py`, updated `test_codex_verify_artifact_metadata_fail_closed` to use pytest `monkeypatch`.
- Installed `adapter._find_rollout` monkeypatch override *before* running all assertions.
- Added an explicit assertion verifying that `adapter._find_rollout(uuid, ctx)` resolves the fixture file before evaluating malformed contents.
- Parameterized invalid metadata cases:
- `{}` (empty dict - R4 defect)
- `{"payload": {"cwd": ws}}` (missing `payload.id`)
- `{"payload": {"id": uuid}}` (missing `payload.cwd`)
- `{"payload": {"id": "other-id", "cwd": ws}}` (mismatched session ID)
- `{"payload": {"id": uuid, "cwd": "/wrong/path"}}` (mismatched workspace cwd)
- `"not-valid-json"` (malformed JSON syntax)
- `[]` (non-dict JSON)
- `{"payload": "not-a-dict"}` (non-dict payload)
- Added database/transcript metadata inconsistency test: DB has conflicting cwd, asserting fail-closed behavior even if rollout matches workspace.
- Retained a valid control case with matching DB cwd and transcript cwd, asserting `True`.
- **Mutation Verification**:
- Bound `old_verify_artifact` from commit `76f10b2` in memory: test **FAILED** with `AssertionError: Expected False for payload: {} (assert True is False)`.
- With current implementation: test **PASSED**.
### T2 (P2): Unmanaged-session test never supplies a session to drift-B
- **Problem**: `test_reconcile_drift_b_unmanaged_session_ignored` used a minimal custom mock script that did not support the `herdr ls -F "#{session_name}|#{session_created}"` or `herdr list-panes -F` query shapes used by `reconcile.sh`, meaning drift-B was never evaluated for the session.
- **Fix**:
- In `tests/test_tier1_unit.py`, updated `test_reconcile_drift_b_unmanaged_session_ignored` to use the contract-compatible `mock_herdr` fixture.
- Seeded `mock_herdr_state.json` with two sessions running `codex` in the workspace:
1. Unmanaged candidate: `"personal-session"` (unmanaged name, no `MAM_MANAGED` env marker).
2. Managed positive control: `"test-proj-creator-codex"` (standard managed name pattern).
- Asserted that both sessions are discoverable via `herdr ls -F "#{session_name}|#{session_created}"` and `herdr list-panes -t <name> -F "#{pane_pid}|#{pane_current_path}|#{pane_current_command}"` before running reconcile.
- Ran `reconcile.sh --once --emit-diff`.
- Asserted that `"personal-session"` is ignored (not present in `drifts` as class B, not in `agent-sessions.yaml`).
- Asserted that `"test-proj-creator-codex"` is registered as drift class B and added to `agent-sessions.yaml`.
- **Mutation Verification**:
- Temporarily restored the old fallback (`if not agent: if pm_check and pm_check.get("cmd") in ...: agent = ...`): test **FAILED** with `AssertionError: assert not any(d.get("name") == "personal-session" for d in drifts)` (because `"personal-session"` was incorrectly registered).
- With current implementation (fallback removed): test **PASSED**.
---
## 2. Validation & Verification
1. `git diff --check`: Clean (exit code 0).
2. T1 Mutation Check:
- Against `76f10b2` `verify_artifact`: FAILED (`AssertionError: Expected False for payload: {}`).
- Against current `verify_artifact`: PASSED.
3. T2 Mutation Check:
- Against `76f10b2` fallback in `reconcile.sh`: FAILED (`assert not True` on personal-session registration).
- Against current `reconcile.sh`: PASSED.
4. Focused Test Execution:
- `pytest tests/test_a4_adapter_contract.py tests/test_tier1_unit.py -q -k "codex or adapter or facts_bridge or delegate_agent or cli_bridge"`: **39 passed, 61 deselected in 46.23s**.
- `pytest tests/test_tier1_unit.py::test_reconcile_drift_b_unmanaged_session_ignored tests/test_version_consistency.py tests/test_tier2_component.py::test_comp_codex_create_and_reconcile_and_stop -q`: **4 passed in 40.15s**.
@@ -0,0 +1,48 @@
# Follow-up review — ca31d443
Reviewed the cumulative corrections on `support-codex`, HEAD `76f10b2`, with particular attention to T1 and T2 from job `ff721ed1`. Both remaining findings are resolved. No new blocking findings were identified in this review. No production files were edited by this reviewer.
## Findings closed
### T1: Invalid-metadata regression now exercises the verifier
The test installs the rollout finder override before every invalid case and explicitly verifies that the fixture resolves and exists. It covers missing identity/workspace metadata, mismatched identity/workspace, malformed JSON and payload types, database/transcript cwd disagreement, and a valid database/transcript control case. Pytest monkeypatch restores the singleton adapter after the test.
Independent mutation verification: loaded `CodexAgentAdapter.verify_artifact` from `76f10b2` and bound it to the registry adapter in memory, then ran the revised test in a temporary directory. The test correctly failed with `Expected False for payload: {}`. The same revised test passes with the current implementation. This directly closes the previous false-positive regression coverage.
### T2: Unmanaged-session regression now reaches drift-B
The revised test uses the existing mock Herdr fixture, seeds both unmanaged and managed agent records, asserts listing and pane metadata visibility, and checks both emitted drifts and resulting YAML. The managed positive control must register while the unmanaged session must remain absent.
Independent mutation verification: instantiated the actual sandbox and mock Herdr fixtures in a temporary directory, restored only that sandbox's reconcile script to `76f10b2`, and ran the revised test. It correctly failed at `tests/test_tier1_unit.py:1236`: `assert not any(d.get("name") == unmanaged_name for d in drifts)`. This confirms failure occurs at the intended unmanaged-session behavior, rather than during setup or visibility checks. The test passes with the current reconciler.
## Prior production findings
The four production correction files have the same Git blob hashes as in the prior review:
- Codex adapter: `1e7a5c74f968f56521108b95a707fbe07768a07e`
- Reconciler: `da806b7237e21ee55b9e1b5ea7bec1c72d570f79`
- Status: `d668bbdea0704ea6ddd5c33b3ddba3a8184b0f48`
- Stop: `d14ffeb7208caa299737cafc268cc338ff4addd6`
Accordingly, the prior acceptance of R1–R6 stands: control-key stop transport, empty/stale DB fallback, workspace-scoped discovery, metadata validation, stale-row status handling, and managed-session identification. T1/T2 now provide effective regression protection for the two previously uncovered cases.
## Validation
Current run:
`./.venv/bin/python -m pytest tests/test_a4_adapter_contract.py tests/test_tier1_unit.py::test_reconcile_drift_b_unmanaged_session_ignored tests/test_version_consistency.py -q`
Result: **36 passed in 10.71 seconds**, exit 0.
- Both independent old-code mutation checks failed at the intended defect assertions, as detailed above.
- `git diff --check main`: clean.
- Inspection confirmed the remaining cumulative changes match the previously reviewed fixes, with the two regression-test corrections described above.
Validation scope: the prior follow-up independently passed 43 focused adapter/lifecycle/status/version tests on the same production fixes. The initial review independently passed all 462 baseline tests on `76f10b2`. Those are historical results, not a claim that this review reran the full suite on the current working tree. This round reran the affected adapter tests, corrected reconciler test, and version checks; only the regression tests changed since the prior accepted production fixes.
## Disposition
T1 and T2 are closed; all findings raised by this reviewer are resolved. No planner escalation is needed. This verdict covers the reviewed working-tree changes and does not itself merge or commit them.
[VERDICT: PASS]
+75 -24
View File
@@ -73,17 +73,33 @@ class CodexAgentAdapter(BaseAgentAdapter):
return False return False
if ctx.epoch and os.path.getmtime(path) < ctx.epoch: if ctx.epoch and os.path.getmtime(path) < ctx.epoch:
return False return False
found_cwd = db_cwd
if not found_cwd: # Fail closed on invalid metadata or mismatched id / cwd (R4)
try: try:
with open(path) as f: with open(path) as f:
first = f.readline() first = f.readline().strip()
found_cwd = json.loads(first).get('payload', {}).get('cwd') meta = json.loads(first)
except Exception: if not isinstance(meta, dict):
return False return False
if found_cwd and workspace_key(found_cwd) != workspace_key(ctx.cwd): payload = meta.get('payload')
if not isinstance(payload, dict):
return False
file_id = payload.get('id')
file_cwd = payload.get('cwd')
if not file_id or file_id != uuid:
return False
if not file_cwd:
return False
if db_cwd and workspace_key(db_cwd) != workspace_key(file_cwd):
return False
target_cwd = ctx.cwd or ctx.workspace
if not target_cwd:
return False
if workspace_key(file_cwd) != workspace_key(target_cwd):
return False
return True
except Exception:
return False return False
return True
def purge_artifacts(self, uuid: str, ctx: DiscoveryContext) -> List[str]: def purge_artifacts(self, uuid: str, ctx: DiscoveryContext) -> List[str]:
purged = [] purged = []
@@ -126,7 +142,13 @@ class CodexAgentAdapter(BaseAgentAdapter):
return os.path.exists(f"{codex_home}/auth.json") or bool(os.environ.get('OPENAI_API_KEY')) return os.path.exists(f"{codex_home}/auth.json") or bool(os.environ.get('OPENAI_API_KEY'))
def discover(self, ctx: DiscoveryContext) -> List[str]: def discover(self, ctx: DiscoveryContext) -> List[str]:
target_ws = ctx.workspace or ctx.cwd
target_key = workspace_key(target_ws) if target_ws else ""
db = self._state_db(ctx) db = self._state_db(ctx)
candidates = []
seen = set()
if db and os.path.exists(db): if db and os.path.exists(db):
try: try:
conn = sqlite3.connect(db) conn = sqlite3.connect(db)
@@ -135,24 +157,53 @@ class CodexAgentAdapter(BaseAgentAdapter):
cols = {r[1] for r in cur.fetchall()} cols = {r[1] for r in cur.fetchall()}
order_col = "updated_at" if "updated_at" in cols else ("created_at" if "created_at" in cols else None) order_col = "updated_at" if "updated_at" in cols else ("created_at" if "created_at" in cols else None)
order_clause = f"ORDER BY {order_col} DESC" if order_col else "" order_clause = f"ORDER BY {order_col} DESC" if order_col else ""
rows = conn.execute(
f"SELECT id, cwd FROM threads {order_clause} LIMIT 50" # Query database with workspace scope before bounding (R3)
).fetchall() if "cwd" in cols and target_ws:
direct_cwds = list({target_ws, os.path.realpath(target_ws)})
placeholders = ",".join("?" * len(direct_cwds))
cur.execute(f"SELECT id, cwd FROM threads WHERE cwd IN ({placeholders}) {order_clause} LIMIT 50", direct_cwds)
for uuid, cwd in cur.fetchall():
if uuid and uuid not in seen and self.verify_artifact(uuid, ctx):
candidates.append(uuid)
seen.add(uuid)
# Page through batches if needed to catch symlink aliases / canonical paths
if len(candidates) < 50:
cur.execute(f"SELECT id, cwd FROM threads {order_clause}")
while True:
batch = cur.fetchmany(100)
if not batch:
break
for uuid, cwd in batch:
if uuid in seen:
continue
if target_key and cwd and workspace_key(cwd) != target_key:
continue
if uuid and self.verify_artifact(uuid, ctx):
candidates.append(uuid)
seen.add(uuid)
if len(candidates) >= 50:
break
if len(candidates) >= 50:
break
conn.close() conn.close()
candidates = []
for uuid, cwd in rows:
if ctx.workspace and cwd and workspace_key(cwd) != workspace_key(ctx.workspace):
continue
if uuid and self.verify_artifact(uuid, ctx):
candidates.append(uuid)
return candidates
except Exception: except Exception:
pass pass
# R2: If database yielded usable candidates, return them; otherwise fall through to transcript discovery
if candidates:
return candidates
root = self._codex_home(ctx) root = self._codex_home(ctx)
candidates = [] paths = sorted(glob.glob(f"{root}/sessions/*/*/*/rollout-*.jsonl"), key=os.path.getmtime, reverse=True)
paths = sorted(glob.glob(f"{root}/sessions/*/*/*/rollout-*.jsonl"), key=os.path.getmtime, reverse=True)[:50]
for path in paths: for path in paths:
uuid = os.path.basename(path)[:-6][-36:] base = os.path.basename(path)
if uuid and self.verify_artifact(uuid, ctx): if base.endswith(".jsonl") and len(base) >= 42:
candidates.append(uuid) uuid = base[:-6][-36:]
if uuid and uuid not in seen and self.verify_artifact(uuid, ctx):
candidates.append(uuid)
seen.add(uuid)
if len(candidates) >= 50:
break
return candidates return candidates
@@ -547,10 +547,6 @@ if herdr_confirmed:
except Exception: except Exception:
pass pass
if not agent:
if pm_check and pm_check.get('cmd') in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
agent = pm_check['cmd']
if not agent: if not agent:
continue continue
@@ -139,8 +139,6 @@ def resume_on_disk(s):
conn.close() conn.close()
if r and r[0] and os.path.exists(r[0]): if r and r[0] and os.path.exists(r[0]):
return 'yes' return 'yes'
if r:
return 'yes'
except Exception: except Exception:
pass pass
m = glob.glob(f"{codex_home}/sessions/*/*/*/rollout-*-{u}.jsonl") m = glob.glob(f"{codex_home}/sessions/*/*/*/rollout-*-{u}.jsonl")
@@ -340,8 +338,6 @@ def resume_on_disk(s):
conn.close() conn.close()
if r and r[0] and os.path.exists(r[0]): if r and r[0] and os.path.exists(r[0]):
return 'yes' return 'yes'
if r:
return 'yes'
except Exception: except Exception:
pass pass
m = glob.glob(f"{codex_home}/sessions/*/*/*/rollout-*-{u}.jsonl") m = glob.glob(f"{codex_home}/sessions/*/*/*/rollout-*-{u}.jsonl")
@@ -198,7 +198,11 @@ graceful_stop() {
pane_pid=$(herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null | head -1 || true) pane_pid=$(herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null | head -1 || true)
exitkey="$("$(_delegate_py_bin)" -m lib_py.agents exit-key "$AGENT" 2>/dev/null || echo "/exit")" exitkey="$("$(_delegate_py_bin)" -m lib_py.agents exit-key "$AGENT" 2>/dev/null || echo "/exit")"
echo "graceful: send-keys '$exitkey' to $SESSION_NAME" echo "graceful: send-keys '$exitkey' to $SESSION_NAME"
send_keys_safe "$SESSION_NAME" "$exitkey" "stop$$" || echo "graceful: safe delivery failed (rc=$?) — falling back to kill chain" if [ "$exitkey" = "Ctrl+C" ] || [ "$exitkey" = "C-c" ]; then
herdr send-keys -t "$SESSION_NAME" C-c 2>/dev/null || true
else
send_keys_safe "$SESSION_NAME" "$exitkey" "stop$$" || echo "graceful: safe delivery failed (rc=$?) — falling back to kill chain"
fi
_wait_session_gone "$SESSION_NAME" 5 || true _wait_session_gone "$SESSION_NAME" 5 || true
if ! herdr has-session -t "$SESSION_NAME" 2>/dev/null; then if ! herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
echo "graceful: exited cleanly" echo "graceful: exited cleanly"
+1 -1
View File
@@ -655,7 +655,7 @@ elif cmd1 == "agent":
rollout_file = os.path.join(sessions_dir, f"rollout-001-{session_uuid}.jsonl") rollout_file = os.path.join(sessions_dir, f"rollout-001-{session_uuid}.jsonl")
now_ts = int(time.time()) now_ts = int(time.time())
with open(rollout_file, "w") as rf: with open(rollout_file, "w") as rf:
rf.write(json.dumps({"payload": {"cwd": ws_abs, "id": session_uuid}}) + "\\\\n") rf.write(json.dumps({"payload": {"cwd": ws_abs, "id": session_uuid}}) + "\\n")
conn.execute("INSERT OR REPLACE INTO threads (id, cwd, rollout_path, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", (session_uuid, ws_abs, rollout_file, now_ts, now_ts)) conn.execute("INSERT OR REPLACE INTO threads (id, cwd, rollout_path, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", (session_uuid, ws_abs, rollout_file, now_ts, now_ts))
conn.commit() conn.commit()
conn.close() conn.close()
+165
View File
@@ -995,7 +995,172 @@ def test_codex_reconcile_full_block_integration(tmp_path):
assert valid_candidates == ['u-target'] assert valid_candidates == ['u-target']
def test_codex_verify_artifact_metadata_fail_closed(tmp_path, monkeypatch):
"""R4 / T1: Verify verify_artifact fails closed on missing/invalid metadata, id mismatch, or db inconsistency."""
import json, sqlite3
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
ws = str(tmp_path / "ws")
home = str(tmp_path / "home")
os.makedirs(ws, exist_ok=True)
os.makedirs(home, exist_ok=True)
adapter = get_adapter('codex')
ctx = DiscoveryContext(workspace=ws, agent_name='codex', home_dir=home)
uuid = "12345678-1234-1234-1234-123456789abc"
rollout_file = tmp_path / "rollout.jsonl"
rollout_file.write_text("{}\n")
# Install finder override using pytest monkeypatch before all cases
monkeypatch.setattr(adapter, "_find_rollout", lambda u, c: str(rollout_file))
# Assert finder resolves the fixture before testing malformed contents
resolved_path = adapter._find_rollout(uuid, ctx)
assert resolved_path == str(rollout_file)
assert os.path.exists(resolved_path)
# Parameterized invalid metadata cases
invalid_payloads = [
"{}", # Empty dict (R4 finding)
json.dumps({"payload": {"cwd": ws}}), # Missing payload.id
json.dumps({"payload": {"id": uuid}}), # Missing payload.cwd
json.dumps({"payload": {"id": "other-id", "cwd": ws}}), # Mismatched session id
json.dumps({"payload": {"id": uuid, "cwd": "/wrong/path"}}), # Mismatched workspace
"not-valid-json", # Non-JSON content
json.dumps([]), # Non-dict JSON
json.dumps({"payload": "not-a-dict"}), # Non-dict payload
]
for inv in invalid_payloads:
rollout_file.write_text(inv + "\n")
assert adapter.verify_artifact(uuid, ctx) is False, f"Expected False for payload: {inv}"
# Database / transcript metadata inconsistency check
db_path = tmp_path / "state_5.sqlite"
conn = sqlite3.connect(str(db_path))
conn.execute("CREATE TABLE threads (id TEXT PRIMARY KEY, cwd TEXT, rollout_path TEXT)")
conn.execute("INSERT INTO threads VALUES (?, ?, ?)", (uuid, "/conflicting/db/cwd", str(rollout_file)))
conn.commit()
conn.close()
monkeypatch.setattr(adapter, "_state_db", lambda c: str(db_path))
# Even if transcript has valid ws, DB cwd conflict must fail closed
rollout_file.write_text(json.dumps({"payload": {"id": uuid, "cwd": ws}}) + "\n")
assert adapter.verify_artifact(uuid, ctx) is False
# Valid control case (matching DB cwd and transcript cwd)
conn = sqlite3.connect(str(db_path))
conn.execute("UPDATE threads SET cwd=? WHERE id=?", (ws, uuid))
conn.commit()
conn.close()
assert adapter.verify_artifact(uuid, ctx) is True
def test_codex_discover_empty_stale_db_fallback(tmp_path):
"""R2: Verify discover falls back to JSONL transcripts when DB is empty or has stale rows."""
import json, sqlite3, time
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
ws = str(tmp_path / "ws")
home = str(tmp_path / "home")
os.makedirs(ws, exist_ok=True)
os.makedirs(home, exist_ok=True)
codex_home = tmp_path / "home" / ".codex"
os.makedirs(codex_home, exist_ok=True)
adapter = get_adapter('codex')
ctx = DiscoveryContext(workspace=ws, agent_name='codex', home_dir=home)
# Create a valid date-nested rollout on disk
uuid_valid = "11111111-1111-1111-1111-111111111111"
sessions_dir = codex_home / "sessions" / "2026" / "09" / "05"
sessions_dir.mkdir(parents=True, exist_ok=True)
valid_file = sessions_dir / f"rollout-2026-09-05T00-00-00-{uuid_valid}.jsonl"
valid_file.write_text(json.dumps({"payload": {"id": uuid_valid, "cwd": ws}}) + "\n")
# Empty DB: threads table exists with 0 rows
db_path = codex_home / "state_5.sqlite"
conn = sqlite3.connect(str(db_path))
conn.execute("CREATE TABLE threads (id TEXT PRIMARY KEY, cwd TEXT, rollout_path TEXT, updated_at INTEGER)")
conn.commit()
conn.close()
# Should fall through to JSONL file and discover uuid_valid
found = adapter.discover(ctx)
assert uuid_valid in found
# Stale DB: insert row pointing to nonexistent file
conn = sqlite3.connect(str(db_path))
conn.execute("INSERT INTO threads (id, cwd, rollout_path, updated_at) VALUES (?, ?, ?, ?)",
("stale-uuid", ws, str(codex_home / "nonexistent.jsonl"), int(time.time())))
conn.commit()
conn.close()
found_stale = adapter.discover(ctx)
assert uuid_valid in found_stale
def test_codex_discover_workspace_scoping_over_50_foreign_rows(tmp_path):
"""R3: Verify global LIMIT does not hide target-workspace threads behind >50 foreign threads."""
import json, sqlite3, time
from pathlib import Path
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
ws_target = str(tmp_path / "ws_target")
ws_foreign = str(tmp_path / "ws_foreign")
home = str(tmp_path / "home")
os.makedirs(ws_target, exist_ok=True)
os.makedirs(ws_foreign, exist_ok=True)
os.makedirs(home, exist_ok=True)
adapter = get_adapter('codex')
ctx_target = DiscoveryContext(workspace=ws_target, agent_name='codex', home_dir=home)
codex_home = Path(home) / ".codex"
sessions_dir = codex_home / "sessions" / "2026" / "09" / "05"
sessions_dir.mkdir(parents=True, exist_ok=True)
db_path = codex_home / "state_5.sqlite"
conn = sqlite3.connect(str(db_path))
conn.execute("CREATE TABLE IF NOT EXISTS threads (id TEXT PRIMARY KEY, cwd TEXT, rollout_path TEXT, updated_at INTEGER)")
now = int(time.time())
# Seed 1 valid target thread
target_uuid = "target-1234-1234-1234-123456789abc"
target_file = sessions_dir / f"rollout-001-{target_uuid}.jsonl"
target_file.write_text(json.dumps({"payload": {"id": target_uuid, "cwd": ws_target}}) + "\n")
conn.execute("INSERT INTO threads (id, cwd, rollout_path, updated_at) VALUES (?, ?, ?, ?)",
(target_uuid, ws_target, str(target_file), now - 100))
# Seed 60 newer foreign threads
for i in range(60):
f_uuid = f"foreign-{i:04d}-1234-1234-123456789abc"
f_file = sessions_dir / f"rollout-001-{f_uuid}.jsonl"
f_file.write_text(json.dumps({"payload": {"id": f_uuid, "cwd": ws_foreign}}) + "\n")
conn.execute("INSERT INTO threads (id, cwd, rollout_path, updated_at) VALUES (?, ?, ?, ?)",
(f_uuid, ws_foreign, str(f_file), now + i))
conn.commit()
conn.close()
candidates = adapter.discover(ctx_target)
assert target_uuid in candidates
def test_codex_discover_symlink_canonical_alias(tmp_path):
"""R3: Verify discover resolves symlink workspace aliases correctly."""
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
real_ws = tmp_path / "real_ws"
real_ws.mkdir()
symlink_ws = tmp_path / "symlink_ws"
symlink_ws.symlink_to(real_ws, target_is_directory=True)
home = str(tmp_path / "home")
os.makedirs(home, exist_ok=True)
adapter = get_adapter('codex')
ctx = DiscoveryContext(workspace=str(symlink_ws), agent_name='codex', home_dir=home)
uuid = "sym-1234-1234-1234-123456789abc"
_seed_codex_session(home, uuid, str(real_ws), 2000.0)
candidates = adapter.discover(ctx)
assert uuid in candidates
+148
View File
@@ -1092,8 +1092,156 @@ def test_orc_onboard_codex_flag_tolerant_regex():
assert res.stdout.strip() == "12345678-1234-1234-1234-123456789abc" assert res.stdout.strip() == "12345678-1234-1234-1234-123456789abc"
def test_stop_session_codex_sends_c_c_chord_not_text_prompt(mam_sandbox, mock_herdr, mock_agents):
"""R1: Verify graceful stop sends C-c key chord rather than text prompt for codex."""
import subprocess, json
# 1. Create a codex session
create_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
res = subprocess.run([
"bash", str(create_script),
"--workspace", str(mam_sandbox),
"--agent", "codex",
"--role", "creator"
], capture_output=True, text=True, cwd=str(mam_sandbox))
assert res.returncode == 0, res.stderr
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
import yaml
with open(yaml_path) as f:
data = yaml.safe_load(f)
session_name = data["herdr_sessions"][0]["name"]
# 2. Stop the session
stop_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
res_stop = subprocess.run([
"bash", str(stop_script),
"--session", session_name,
"--agent", "codex"
], capture_output=True, text=True, cwd=str(mam_sandbox))
assert res_stop.returncode == 0, res_stop.stderr
# 3. Verify mock_herdr recorded C-c send-keys call and did NOT record Ctrl+C prompt
with open(mock_herdr) as f:
state = json.load(f)
calls = state.get("calls", [])
pane_send_keys = [c for c in calls if "send-keys" in c and "C-c" in c]
agent_prompts = [c for c in calls if "prompt" in c and any("Ctrl+C" in str(arg) for arg in c)]
assert len(pane_send_keys) >= 1
assert len(agent_prompts) == 0
def test_status_resume_on_disk_codex_stale_and_valid(mam_sandbox):
"""R5: Verify status.sh resume_on_disk reports MISSING for stale DB rows and yes for existing files."""
import sqlite3, json, subprocess
codex_home = mam_sandbox / ".codex"
codex_home.mkdir(parents=True, exist_ok=True)
db_path = codex_home / "state_5.sqlite"
conn = sqlite3.connect(str(db_path))
conn.execute("CREATE TABLE threads (id TEXT PRIMARY KEY, cwd TEXT, rollout_path TEXT)")
# 1. Stale row: rollout_path does not exist
stale_uuid = "stale-1234-1234-1234-123456789abc"
conn.execute("INSERT INTO threads (id, cwd, rollout_path) VALUES (?, ?, ?)",
(stale_uuid, str(mam_sandbox), str(codex_home / "nonexistent.jsonl")))
# 2. Valid row: rollout_path exists
valid_uuid = "valid-1234-1234-1234-123456789abc"
valid_file = codex_home / f"rollout-{valid_uuid}.jsonl"
valid_file.write_text(json.dumps({"payload": {"id": valid_uuid, "cwd": str(mam_sandbox)}}) + "\n")
conn.execute("INSERT INTO threads (id, cwd, rollout_path) VALUES (?, ?, ?)",
(valid_uuid, str(mam_sandbox), str(valid_file)))
conn.commit()
conn.close()
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
yaml_path.write_text(f"""herdr_sessions:
- name: test-stale-codex
status: running
codex_session_id_own: {stale_uuid}
pane:
cwd: {mam_sandbox}
- name: test-valid-codex
status: running
codex_session_id_own: {valid_uuid}
pane:
cwd: {mam_sandbox}
""")
script_path = mam_sandbox / "skills" / "multi-agent-mux-status" / "scripts" / "status.sh"
env = {**os.environ, "CODEX_HOME": str(codex_home)}
res = subprocess.run(["bash", str(script_path), "--json"], capture_output=True, text=True, env=env)
assert res.returncode == 0
data = json.loads(res.stdout)
details = {d["name"]: d["resume_state"] for d in data["sessions_detail"]}
assert details["test-stale-codex"] == "MISSING"
assert details["test-valid-codex"] == "yes"
def test_reconcile_drift_b_unmanaged_session_ignored(mam_sandbox, mock_herdr):
"""R6 / T2: Verify drift-B ignores unmanaged in-workspace sessions while registering managed positive control."""
import subprocess, json, yaml
reconcile_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
assert reconcile_script.exists()
# Create dummy yaml with no sessions
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
yaml_path.parent.mkdir(parents=True, exist_ok=True)
yaml_path.write_text("herdr_sessions: []\n")
unmanaged_name = "personal-session"
managed_name = "test-proj-creator-codex"
# Seed mock_herdr with an unmanaged session and a managed control session running codex in workspace
state = {
"workspaces": [{"workspace_id": "w1", "label": "default", "cwd": str(mam_sandbox)}],
"agents": {
unmanaged_name: {
"name": unmanaged_name,
"status": "running",
"cwd": str(mam_sandbox),
"command": "codex",
"pid": 12345
},
managed_name: {
"name": managed_name,
"status": "running",
"cwd": str(mam_sandbox),
"command": "codex",
"pid": 54321
}
},
"calls": []
}
with open(mock_herdr, "w") as f:
json.dump(state, f)
# Assert candidate is visible to listing and pane metadata lookup before running reconcile
r_ls = subprocess.run(["herdr", "ls", "-F", "#{session_name}|#{session_created}"], capture_output=True, text=True)
assert r_ls.returncode == 0
assert unmanaged_name in r_ls.stdout
assert managed_name in r_ls.stdout
r_pm = subprocess.run(["herdr", "list-panes", "-t", unmanaged_name, "-F", "#{pane_pid}|#{pane_current_path}|#{pane_current_command}"], capture_output=True, text=True)
assert r_pm.returncode == 0
assert f"12345|{str(mam_sandbox)}|codex" in r_pm.stdout
res = subprocess.run(["bash", str(reconcile_script), "--once", "--emit-diff"],
capture_output=True, text=True, cwd=str(mam_sandbox),
env={**os.environ, "WORKSPACE_ROOT": str(mam_sandbox)})
assert res.returncode == 0, res.stderr
out = json.loads(res.stdout)
drifts = out.get("drifts", [])
# Personal session must NOT be registered as drift B
assert not any(d.get("name") == unmanaged_name for d in drifts)
# Managed positive control MUST be registered as drift B
b_registered = [d.get("name") for d in drifts if d.get("class") == "B"]
assert managed_name in b_registered
# Verify YAML updated with managed session but without unmanaged session
with open(yaml_path) as f:
data = yaml.safe_load(f)
registered_names = [s.get("name") for s in data.get("herdr_sessions", [])]
assert unmanaged_name not in registered_names
assert managed_name in registered_names
+2
View File
@@ -1252,6 +1252,8 @@ def test_comp_opencode_create_and_reconcile_and_stop(mam_sandbox, mock_herdr, mo
# Run reconcile.sh to trigger drift-C materialization # Run reconcile.sh to trigger drift-C materialization
reconcile_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh" reconcile_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
res_rec = subprocess.run(["bash", str(reconcile_script), "--once", "--emit-diff"], capture_output=True, text=True, cwd=str(mam_sandbox), env={**os.environ, "WORKSPACE_ROOT": str(mam_sandbox)}) res_rec = subprocess.run(["bash", str(reconcile_script), "--once", "--emit-diff"], capture_output=True, text=True, cwd=str(mam_sandbox), env={**os.environ, "WORKSPACE_ROOT": str(mam_sandbox)})
print("RECONCILE STDOUT:\n", res_rec.stdout)
print("RECONCILE STDERR:\n", res_rec.stderr)
assert res_rec.returncode == 0, res_rec.stderr assert res_rec.returncode == 0, res_rec.stderr
with open(yaml_path) as f: with open(yaml_path) as f: