|
16 | 16 | from loopx.control_plane.testing.canary_harness import run_json_cli_result |
17 | 17 | from loopx.control_plane.todos import completion_validation |
18 | 18 | from loopx.todos import complete_goal_todo |
| 19 | +from loopx.control_plane.todos.todo_index import build_todo_index |
| 20 | +from loopx.control_plane.runtime.public_safety import public_safe_compact_text |
| 21 | +from tests.control_plane.canonical_authority_fixture import ( |
| 22 | + initialize_canonical_authority, isolate_sqlite_runtime, |
| 23 | +) |
| 24 | +from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection |
| 25 | +from loopx.control_plane.effect_runtime import restart_effect_runtime |
| 26 | + |
| 27 | + |
| 28 | +@pytest.mark.parametrize("status", ["open", "blocked", "deferred", "done"]) |
| 29 | +def test_rollout_audit_cannot_overwrite_current_todo_state_or_claim(tmp_path, status): |
| 30 | + current = {"todo_id": "todo_current", "text": "Current authority work", |
| 31 | + "status": status, "done": status in ("deferred", "done"), "claimed_by": None} |
| 32 | + events = [{"goal_id": "goal-a", "todo_id": "todo_current", "event_kind": "todo_update", |
| 33 | + "status": "open" if status != "open" else "done", "agent_id": "old-actor", |
| 34 | + "summary": "Historical update", "recorded_at": "2026-01-01T00:00:00Z"}] |
| 35 | + result = build_todo_index(queue={"items": [{"goal_id": "goal-a", |
| 36 | + "agent_todos": {"items": [current]}}]}, history={"goals": [{"id": "goal-a"}]}, |
| 37 | + runtime_root=tmp_path, public_safe_compact_text=public_safe_compact_text, |
| 38 | + events_for_goal=lambda goal_id, **kwargs: events) |
| 39 | + row = result["items"][0] |
| 40 | + assert row["status"] == status |
| 41 | + assert row["done"] is current["done"] |
| 42 | + assert row.get("agent_id") is None |
| 43 | + assert row["latest_event_status"] == events[0]["status"] |
| 44 | + assert row["latest_event_summary"] == "Historical update" |
| 45 | + assert row["event_count"] == 1 |
19 | 46 |
|
20 | 47 | @pytest.mark.parametrize("alias", [None, "state_event_log", "state_events_file", "event_log"]) |
21 | 48 | @pytest.mark.parametrize("contents", ["{broken\n", '{"event_type":"todo_added"}\n']) |
@@ -175,3 +202,101 @@ def test_http_status_preserves_healthy_goal_when_retired_source_appears(tmp_path |
175 | 202 | finally: |
176 | 203 | server.terminate() |
177 | 204 | server.wait(timeout=10) |
| 205 | + |
| 206 | + |
| 207 | +@pytest.mark.parametrize("provider", ["file", "sqlite"]) |
| 208 | +def test_real_directory_lifecycle_uses_canonical_state_despite_stale_audit(tmp_path, monkeypatch, provider): |
| 209 | + isolate_sqlite_runtime(tmp_path, monkeypatch) |
| 210 | + state = tmp_path / "ACTIVE_GOAL_STATE.md" |
| 211 | + state.write_text("# Goal\n\n## Agent Todo\n\n") |
| 212 | + runtime = tmp_path / "runtime" |
| 213 | + registry = tmp_path / "registry.json" |
| 214 | + registry.write_text(json.dumps({"schema_version": 1, "common_runtime_root": str(runtime), |
| 215 | + "goals": [{"id": "goal-a", "repo": str(tmp_path), "state_file": str(state), |
| 216 | + "domain": "software", "status": "active", |
| 217 | + "adapter": {"kind": "read_only_project_map_v0", "status": "connected"}, |
| 218 | + "coordination": {"registered_agents": ["agent-a", "agent-b", "agent-c"]}}]})) |
| 219 | + initialize_canonical_authority(runtime, "goal-a", |
| 220 | + build_todo_runtime_shadow_projection(goal_id="goal-a", todos=[], handoff_mode="hard_lease"), |
| 221 | + state_path=state, provider=provider) |
| 222 | + |
| 223 | + def cli(*args, success=True): |
| 224 | + code, payload = run_json_cli_result(*args, "--goal-id", "goal-a", registry_path=registry) |
| 225 | + if success: |
| 226 | + assert code == 0 and payload.get("ok") is True, payload |
| 227 | + return payload |
| 228 | + |
| 229 | + def readback(expected, lease_status=None, *, agent="agent-a", target=None): |
| 230 | + target = target or todo_id |
| 231 | + before = cli("todo", "list") |
| 232 | + current = next(row for row in before["todos"] if row["todo_id"] == target) |
| 233 | + assert current["status"] == expected |
| 234 | + payload = cli("status") |
| 235 | + indexed = next(row for row in payload["todo_index"]["items"] if row["todo_id"] == target) |
| 236 | + assert indexed["status"] == expected |
| 237 | + directory = cli("agent-directory", "--agent-id", agent) |
| 238 | + work = next(row for row in directory["rows"] if row["agent_id"] == agent)["work"] |
| 239 | + if expected == "done": |
| 240 | + assert work is None |
| 241 | + else: |
| 242 | + assert work["todo_id"] == target and work["todo_status"] == expected |
| 243 | + assert work["claimed_by"] == agent |
| 244 | + assert work.get("lease_status") == lease_status |
| 245 | + after = cli("todo", "list") |
| 246 | + assert before["authority_read"]["provider_revision"] == after["authority_read"]["provider_revision"] |
| 247 | + assert before["todos"] == after["todos"] |
| 248 | + |
| 249 | + try: |
| 250 | + added = cli("todo", "add", "--role", "agent", "--text", "Long canonical task " + "work " * 140, |
| 251 | + "--claimed-by", "agent-a") |
| 252 | + todo_id = added["todo_id"] |
| 253 | + lease = cli("task-lease", "acquire", "--todo-id", todo_id, "--owner", "agent-a", |
| 254 | + "--idempotency-key", "directory-live-lease") |
| 255 | + readback("open", "active") |
| 256 | + cli("task-lease", "release", "--todo-id", todo_id, "--owner", "agent-a", |
| 257 | + "--idempotency-key", "directory-live-lease", "--expected-version", str(lease["lease"]["version"])) |
| 258 | + before = cli("todo", "list") |
| 259 | + cli("todo", "update", "--todo-id", todo_id, "--agent-id", "agent-a", |
| 260 | + "--status", "blocked", "--reason", "Fixture lifecycle transition", |
| 261 | + "--update-operation-id", "directory-blocked", "--clear-resume-when", |
| 262 | + "--update-expected-provider-revision", before["authority_read"]["provider_revision"]) |
| 263 | + readback("blocked", "released") |
| 264 | + deferred = cli("todo", "add", "--role", "agent", "--text", "Wait for an explicit decision", |
| 265 | + "--status", "deferred", "--resume-when", "todo_done:" + todo_id, "--claimed-by", "agent-b") |
| 266 | + readback("deferred", agent="agent-b", target=deferred["todo_id"]) |
| 267 | + finished = cli("todo", "add", "--role", "agent", "--text", "Validate the terminal readback", |
| 268 | + "--claimed-by", "agent-c") |
| 269 | + terminal_lease = cli("task-lease", "acquire", "--todo-id", finished["todo_id"], "--owner", "agent-c", |
| 270 | + "--idempotency-key", "directory-terminal-lease") |
| 271 | + cli("todo", "complete", "--todo-id", finished["todo_id"], "--agent-id", "agent-c", |
| 272 | + "--evidence", "validation://directory-lifecycle", "--no-follow-up", |
| 273 | + "--task-lease-idempotency-key", "directory-terminal-lease", "--task-lease-expected-version", |
| 274 | + str(terminal_lease["lease"]["version"])) |
| 275 | + readback("done", agent="agent-c", target=finished["todo_id"]) |
| 276 | + # A readable stale display and rollout history cannot rescue a lost provider. |
| 277 | + state.write_text("## Agent Todo\n- [ ] Old work\n" |
| 278 | + f" <!-- loopx:todo todo_id={todo_id} status=open claimed_by=agent-a -->\n") |
| 279 | + authority = runtime / "authority" / f"{provider}-v0" |
| 280 | + unavailable = runtime / "unavailable-provider" |
| 281 | + authority.rename(unavailable) |
| 282 | + for command in ("status", "agent-directory"): |
| 283 | + failure = subprocess.run([sys.executable, "-m", "loopx.cli", "--registry", str(registry), |
| 284 | + "--runtime-root", str(runtime), "--format", "json", command, "--goal-id", "goal-a"], |
| 285 | + capture_output=True, text=True, timeout=60) |
| 286 | + if failure.stdout.strip(): |
| 287 | + failed = json.loads(failure.stdout) |
| 288 | + if command == "status": |
| 289 | + assert not failed["ok"] and not failed.get("todo_index", {}).get("items") |
| 290 | + else: |
| 291 | + assert all(row["work"] is None for row in failed.get("rows", [])) |
| 292 | + else: |
| 293 | + # Some source-loss paths raise the owning unavailable error. |
| 294 | + # That failure is not a successful empty or legacy work view. |
| 295 | + assert failure.returncode != 0 |
| 296 | + assert "LocalCoordinationAuthorityUnavailable:" in failure.stderr |
| 297 | + unavailable.rename(authority) |
| 298 | + readback("blocked", "released") |
| 299 | + readback("deferred", agent="agent-b", target=deferred["todo_id"]) |
| 300 | + readback("done", agent="agent-c", target=finished["todo_id"]) |
| 301 | + finally: |
| 302 | + restart_effect_runtime() |
0 commit comments