"""Issue #63: same-cwd conversation-id resolution in stop/purge/resume and same-type creation ordering for post-discovered agents. """ from __future__ import annotations import json import os import sqlite3 import subprocess import sys import time from pathlib import Path from typing import List, Optional import pytest import yaml REPO = Path(__file__).resolve().parents[1] SKILLS = REPO / ".agents" / "skills" if str(SKILLS) not in sys.path: sys.path.insert(0, str(SKILLS)) UA = "a6300001-0000-4000-8000-000000000001" UB = "b6300002-0000-4000-8000-000000000002" UC = "c6300003-0000-4000-8000-000000000003" UX = "d6300004-0000-4000-8000-000000000004" pytestmark = pytest.mark.usefixtures("own_workspace_root") # --------------------------------------------------------------------------- helpers def _agy_conv(home: Path, uuid: str, mtime: Optional[float] = None) -> Path: db = Path(home) / ".gemini" / "antigravity-cli" / "conversations" / f"{uuid}.db" db.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(str(db)) conn.execute("CREATE TABLE steps (id INTEGER)") conn.execute("INSERT INTO steps VALUES (1)") conn.commit() conn.close() if mtime is not None: os.utime(db, (mtime, mtime)) return db def _agy_full_artifacts(home: Path, uuid: str) -> List[Path]: base = Path(home) / ".gemini" / "antigravity-cli" db = _agy_conv(home, uuid) wal = Path(str(db) + "-wal") shm = Path(str(db) + "-shm") wal.write_text("wal") shm.write_text("shm") brain = base / "brain" / uuid brain.mkdir(parents=True, exist_ok=True) (brain / "note.md").write_text("note") ann = base / "annotations" / f"{uuid}.pbtxt" ann.parent.mkdir(parents=True, exist_ok=True) ann.write_text("pbtxt") lock = base / "presence" / f"{uuid}.lock" lock.parent.mkdir(parents=True, exist_ok=True) lock.write_text("") return [db, wal, shm, brain, ann, lock] def _agy_cache(home: Path, mapping: dict) -> None: lc = Path(home) / ".gemini" / "antigravity-cli" / "cache" / "last_conversations.json" lc.parent.mkdir(parents=True, exist_ok=True) lc.write_text(json.dumps({str(k): v for k, v in mapping.items()})) def _claude_transcript(claude_dir: Path, ws: Path, uuid: str, mtime: Optional[float] = None) -> Path: from lib_py.verify_session import workspace_key pdir = Path(claude_dir) / workspace_key(str(ws)) pdir.mkdir(parents=True, exist_ok=True) f = pdir / f"{uuid}.jsonl" f.write_text(json.dumps({"sessionId": uuid, "cwd": str(ws), "type": "user"}) + "\n") if mtime is not None: os.utime(f, (mtime, mtime)) return f def _row(name: str, agent: str, cwd: Path, epoch: int, pid: int = 4242, **extra) -> dict: own = {"agy": "agy_conversation_id_own", "opencode": "opencode_session_id_own", "codex": "codex_session_id_own", "hermes": "hermes_conversation_id_own", "claude": "claude_session_id_own", "grok": "grok_session_id_own"}[agent] row = {"name": name, "status": "running", "role": "creator", "agent": agent, "herdr_session_epoch": epoch, own: None, "session_id_source": "pending-discovery", "session_id_verified": False, "last_visible_status": "unverified", "pane": {"cwd": str(cwd), "pid": pid, "cmd": agent}} row.update(extra) return row # --------------------------------------------------------------------------- 2.1 workspace_uuid def test_find_workspace_uuid_skips_exclude_ids(tmp_path): """rev 2 (B): find_workspace_uuid skips exclude_ids across sibling rows and discover().""" from lib_py.workspace_uuid import find_workspace_uuid home = tmp_path / "home" claude_dir = home / ".claude" / "projects" ws = tmp_path / "ws" ws.mkdir(parents=True) now = time.time() _claude_transcript(claude_dir, ws, UA, mtime=now) _claude_transcript(claude_dir, ws, UB, mtime=now - 10) state = { "herdr_sessions": [ _row("ws-creator-claude", "claude", ws, int(now) - 100, status="stopped", claude_session_id_own=UA, session_id_source="discovered", session_id_verified=True), _row("ws-reviewer-claude", "claude", ws, int(now) - 100, status="running", claude_session_id_own=None), ] } assert find_workspace_uuid(str(ws), "claude", "ws-reviewer-claude", state, str(home), str(claude_dir), exclude_ids=()) == UA assert find_workspace_uuid(str(ws), "claude", "ws-reviewer-claude", state, str(home), str(claude_dir), exclude_ids={UA}) == UB def test_find_workspace_uuid_uses_explicit_home_and_claude_dir(tmp_path, monkeypatch): """rev 2 (B-sub): explicit home and claude_dir are forwarded to tier-1 revalidate.""" from lib_py.workspace_uuid import find_workspace_uuid monkeypatch.setenv("HOME", str(tmp_path / "wrong-home")) monkeypatch.delenv("HOME_DIR", raising=False) monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False) home = tmp_path / "custom-home" claude_dir = tmp_path / "custom-claude" ws = tmp_path / "ws" ws.mkdir(parents=True) _agy_conv(home, UA) _claude_transcript(claude_dir, ws, UB) state = { "herdr_sessions": [ _row("ws-creator-agy", "agy", ws, 100, status="stopped", agy_conversation_id_own=UA, session_id_source="discovered", session_id_verified=True), _row("ws-creator-claude", "claude", ws, 100, status="stopped", claude_session_id_own=UB, session_id_source="discovered", session_id_verified=True), ] } assert find_workspace_uuid(str(ws), "agy", "ws-creator-agy", state, home=str(home), claude_dir=str(claude_dir)) == UA assert find_workspace_uuid(str(ws), "claude", "ws-creator-claude", state, home=str(home), claude_dir=str(claude_dir)) == UB