@team-agent/installer 0.1.4 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/team_agent/runtime.py +25 -15
- package/src/team_agent/state.py +15 -5
- package/tests/run_tests.py +210 -33
package/package.json
CHANGED
|
@@ -35,7 +35,15 @@ from team_agent.providers import ResumeUnavailable, get_adapter, shell_command_f
|
|
|
35
35
|
from team_agent.routing import route_task
|
|
36
36
|
from team_agent.simple_yaml import dumps
|
|
37
37
|
from team_agent.spec import load_spec, validate_result_envelope, workspace_from_spec
|
|
38
|
-
from team_agent.state import
|
|
38
|
+
from team_agent.state import (
|
|
39
|
+
SESSION_CAPTURE_FIELDS,
|
|
40
|
+
SESSION_STATE_FIELDS,
|
|
41
|
+
load_runtime_state,
|
|
42
|
+
normalize_agent_session_state,
|
|
43
|
+
runtime_state_path,
|
|
44
|
+
save_runtime_state,
|
|
45
|
+
write_team_state,
|
|
46
|
+
)
|
|
39
47
|
from team_agent.task_graph import ready_tasks, update_task_status
|
|
40
48
|
from team_agent.task_graph import TASK_STATUSES
|
|
41
49
|
|
|
@@ -520,10 +528,7 @@ def _load_snapshot_state(path: Path) -> dict[str, Any] | None:
|
|
|
520
528
|
state = json.loads(path.read_text(encoding="utf-8"))
|
|
521
529
|
except (OSError, json.JSONDecodeError):
|
|
522
530
|
return None
|
|
523
|
-
|
|
524
|
-
if isinstance(agent_state, dict):
|
|
525
|
-
for field in ["session_id", "rollout_path", "captured_at", "captured_via", "attribution_confidence", "spawn_cwd"]:
|
|
526
|
-
agent_state.setdefault(field, None)
|
|
531
|
+
normalize_agent_session_state(state)
|
|
527
532
|
return state
|
|
528
533
|
|
|
529
534
|
|
|
@@ -1570,8 +1575,7 @@ def _capture_agent_session(
|
|
|
1570
1575
|
result = adapter.capture_session_id(agent_id, spawn_context, timeout_s=timeout_s)
|
|
1571
1576
|
if not isinstance(result, dict) or not result.get("session_id"):
|
|
1572
1577
|
return None
|
|
1573
|
-
|
|
1574
|
-
agent_state[key] = result.get(key)
|
|
1578
|
+
_copy_session_metadata(agent_state, result)
|
|
1575
1579
|
agent_state.pop("_pending_session_id", None)
|
|
1576
1580
|
event_log.write(
|
|
1577
1581
|
"session.captured",
|
|
@@ -1585,6 +1589,16 @@ def _capture_agent_session(
|
|
|
1585
1589
|
return result
|
|
1586
1590
|
|
|
1587
1591
|
|
|
1592
|
+
def _copy_session_metadata(target: dict[str, Any], source: dict[str, Any]) -> None:
|
|
1593
|
+
for key in SESSION_STATE_FIELDS:
|
|
1594
|
+
target[key] = source.get(key)
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
def _clear_session_capture_fields(target: dict[str, Any]) -> None:
|
|
1598
|
+
for key in SESSION_CAPTURE_FIELDS:
|
|
1599
|
+
target[key] = None
|
|
1600
|
+
|
|
1601
|
+
|
|
1588
1602
|
def _attach_profile_resume_root(workspace: Path, command_agent: dict[str, Any], previous: dict[str, Any]) -> dict[str, Any]:
|
|
1589
1603
|
profile_launch = command_agent.get("_provider_profile") or prepare_agent_profile_launch(workspace, command_agent)
|
|
1590
1604
|
if not profile_launch:
|
|
@@ -1631,8 +1645,7 @@ def _prepare_resume_state(
|
|
|
1631
1645
|
if not repaired:
|
|
1632
1646
|
repaired = adapter.recover_session_id(agent_id, prepared, workspace, exclude_session_ids or set())
|
|
1633
1647
|
if repaired:
|
|
1634
|
-
|
|
1635
|
-
prepared[key] = repaired.get(key)
|
|
1648
|
+
_copy_session_metadata(prepared, repaired)
|
|
1636
1649
|
event_log.write(
|
|
1637
1650
|
"resume.session_repaired",
|
|
1638
1651
|
agent_id=agent_id,
|
|
@@ -1657,8 +1670,7 @@ def _prepare_resume_state(
|
|
|
1657
1670
|
f"Cannot resume agent {agent_id}: stored session {session_id} is not available. "
|
|
1658
1671
|
"Use --allow-fresh only if losing that worker context is acceptable."
|
|
1659
1672
|
)
|
|
1660
|
-
|
|
1661
|
-
prepared[key] = None
|
|
1673
|
+
_clear_session_capture_fields(prepared)
|
|
1662
1674
|
event_log.write(
|
|
1663
1675
|
"resume.session_unavailable",
|
|
1664
1676
|
agent_id=agent_id,
|
|
@@ -1972,8 +1984,7 @@ def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None)
|
|
|
1972
1984
|
if profile_launch.get("claude_projects_root"):
|
|
1973
1985
|
agent_state["claude_projects_root"] = profile_launch["claude_projects_root"]
|
|
1974
1986
|
if restart_mode == "fresh":
|
|
1975
|
-
|
|
1976
|
-
agent_state[key] = None
|
|
1987
|
+
_clear_session_capture_fields(agent_state)
|
|
1977
1988
|
if command_agent.get("_session_id"):
|
|
1978
1989
|
agent_state["_pending_session_id"] = command_agent["_session_id"]
|
|
1979
1990
|
_capture_agent_session(
|
|
@@ -2231,8 +2242,7 @@ def _start_agent_unlocked(workspace: Path, agent_id: str, force: bool, open_disp
|
|
|
2231
2242
|
if profile_launch.get("claude_projects_root"):
|
|
2232
2243
|
agent_state["claude_projects_root"] = profile_launch["claude_projects_root"]
|
|
2233
2244
|
if start_mode == "fresh":
|
|
2234
|
-
|
|
2235
|
-
agent_state[key] = None
|
|
2245
|
+
_clear_session_capture_fields(agent_state)
|
|
2236
2246
|
if command_agent.get("_session_id"):
|
|
2237
2247
|
agent_state["_pending_session_id"] = command_agent["_session_id"]
|
|
2238
2248
|
_capture_agent_session(workspace, agent_id, agent_state, event_log, timeout_s=1.5, exclude_session_ids=known_session_ids)
|
package/src/team_agent/state.py
CHANGED
|
@@ -10,12 +10,15 @@ from team_agent.paths import runtime_dir
|
|
|
10
10
|
from team_agent.simple_yaml import dumps
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
SESSION_CAPTURE_FIELDS = [
|
|
14
14
|
"session_id",
|
|
15
15
|
"rollout_path",
|
|
16
16
|
"captured_at",
|
|
17
17
|
"captured_via",
|
|
18
18
|
"attribution_confidence",
|
|
19
|
+
]
|
|
20
|
+
SESSION_STATE_FIELDS = [
|
|
21
|
+
*SESSION_CAPTURE_FIELDS,
|
|
19
22
|
"spawn_cwd",
|
|
20
23
|
]
|
|
21
24
|
|
|
@@ -24,15 +27,22 @@ def runtime_state_path(workspace: Path) -> Path:
|
|
|
24
27
|
return runtime_dir(workspace) / "state.json"
|
|
25
28
|
|
|
26
29
|
|
|
30
|
+
def normalize_agent_session_state(state: dict[str, Any]) -> None:
|
|
31
|
+
agents = state.get("agents", {})
|
|
32
|
+
if not isinstance(agents, dict):
|
|
33
|
+
return
|
|
34
|
+
for agent_state in agents.values():
|
|
35
|
+
if isinstance(agent_state, dict):
|
|
36
|
+
for field in SESSION_STATE_FIELDS:
|
|
37
|
+
agent_state.setdefault(field, None)
|
|
38
|
+
|
|
39
|
+
|
|
27
40
|
def load_runtime_state(workspace: Path) -> dict[str, Any]:
|
|
28
41
|
path = runtime_state_path(workspace)
|
|
29
42
|
if not path.exists():
|
|
30
43
|
return {"agents": {}, "tasks": [], "session_name": None}
|
|
31
44
|
state = json.loads(path.read_text(encoding="utf-8"))
|
|
32
|
-
|
|
33
|
-
if isinstance(agent_state, dict):
|
|
34
|
-
for field in SESSION_STATE_FIELDS:
|
|
35
|
-
agent_state.setdefault(field, None)
|
|
45
|
+
normalize_agent_session_state(state)
|
|
36
46
|
return state
|
|
37
47
|
|
|
38
48
|
|
package/tests/run_tests.py
CHANGED
|
@@ -701,6 +701,7 @@ class RuntimeTests(unittest.TestCase):
|
|
|
701
701
|
spec_path.write_text(dumps(spec), encoding="utf-8")
|
|
702
702
|
with (
|
|
703
703
|
patch("team_agent.runtime.shutil_which", return_value="/usr/bin/tmux"),
|
|
704
|
+
patch("team_agent.providers.shutil.which", return_value="/usr/bin/codex"),
|
|
704
705
|
patch("team_agent.runtime._tmux_session_exists", return_value=False),
|
|
705
706
|
patch("team_agent.runtime._tmux_current_client_pane_info", return_value=None),
|
|
706
707
|
patch("team_agent.runtime._tmux_list_panes", return_value=[]),
|
|
@@ -709,6 +710,21 @@ class RuntimeTests(unittest.TestCase):
|
|
|
709
710
|
runtime.launch(spec_path, auto_approve=True)
|
|
710
711
|
self.assertIn("could not locate a tmux-managed leader pane", str(ctx.exception))
|
|
711
712
|
|
|
713
|
+
def test_launch_blocks_missing_provider_command(self) -> None:
|
|
714
|
+
with tempfile.TemporaryDirectory(prefix="team-agent-provider-missing-") as tmp:
|
|
715
|
+
workspace = Path(tmp)
|
|
716
|
+
spec = _fake_spec(workspace)
|
|
717
|
+
spec["agents"][0]["provider"] = "codex"
|
|
718
|
+
spec_path = workspace / "team.spec.yaml"
|
|
719
|
+
spec_path.write_text(dumps(spec), encoding="utf-8")
|
|
720
|
+
with (
|
|
721
|
+
patch("team_agent.runtime.shutil_which", return_value="/usr/bin/tmux"),
|
|
722
|
+
patch("team_agent.providers.shutil.which", return_value=None),
|
|
723
|
+
self.assertRaises(TeamAgentRuntimeError) as ctx,
|
|
724
|
+
):
|
|
725
|
+
runtime.launch(spec_path, auto_approve=True)
|
|
726
|
+
self.assertIn("Provider codex command 'codex' not found", str(ctx.exception))
|
|
727
|
+
|
|
712
728
|
def test_worker_to_leader_direct_injection_uses_standard_payload_and_does_not_route_task(self) -> None:
|
|
713
729
|
if not shutil.which("tmux"):
|
|
714
730
|
self.skipTest("tmux not installed")
|
|
@@ -1310,7 +1326,8 @@ Implement bounded tasks and report result_envelope_v1.
|
|
|
1310
1326
|
)
|
|
1311
1327
|
compiled = compile_team(team, workspace / "team.spec.yaml")["spec"]
|
|
1312
1328
|
self.assertIsNone(compiled["agents"][0]["model"])
|
|
1313
|
-
|
|
1329
|
+
with patch("team_agent.runtime.shutil_which", return_value="/usr/bin/tmux"):
|
|
1330
|
+
preflight = runtime.preflight(team)
|
|
1314
1331
|
self.assertTrue(preflight["ok"])
|
|
1315
1332
|
profiles = next(check for check in preflight["checks"] if check["name"] == "profiles")
|
|
1316
1333
|
implementer = next(item for item in profiles["checks"] if item["agent_id"] == "implementer")
|
|
@@ -3126,7 +3143,11 @@ Handle fake tasks.
|
|
|
3126
3143
|
cwd = os.getcwd()
|
|
3127
3144
|
os.chdir(workspace)
|
|
3128
3145
|
try:
|
|
3129
|
-
with
|
|
3146
|
+
with (
|
|
3147
|
+
patch("team_agent.runtime.shutil_which", return_value="/usr/bin/tmux"),
|
|
3148
|
+
patch("team_agent.runtime._model_checks_for_agents", return_value=[]),
|
|
3149
|
+
self.assertRaises(runtime.RuntimeError) as ctx,
|
|
3150
|
+
):
|
|
3130
3151
|
runtime.quick_start(team)
|
|
3131
3152
|
finally:
|
|
3132
3153
|
os.chdir(cwd)
|
|
@@ -3137,7 +3158,10 @@ Handle fake tasks.
|
|
|
3137
3158
|
workspace = Path(tmp)
|
|
3138
3159
|
state = {
|
|
3139
3160
|
"session_name": "team-old",
|
|
3140
|
-
"agents": {
|
|
3161
|
+
"agents": {
|
|
3162
|
+
"fake_impl": {"status": "running", "provider": "fake", "window": "fake_impl"},
|
|
3163
|
+
"legacy_bad": "untouched",
|
|
3164
|
+
},
|
|
3141
3165
|
"tasks": [],
|
|
3142
3166
|
}
|
|
3143
3167
|
save_runtime_state(workspace, state)
|
|
@@ -3146,6 +3170,98 @@ Handle fake tasks.
|
|
|
3146
3170
|
for key in ["session_id", "rollout_path", "captured_at", "captured_via", "attribution_confidence", "spawn_cwd"]:
|
|
3147
3171
|
self.assertIn(key, agent)
|
|
3148
3172
|
self.assertIsNone(agent[key])
|
|
3173
|
+
self.assertEqual(loaded["agents"]["legacy_bad"], "untouched")
|
|
3174
|
+
|
|
3175
|
+
def test_runtime_state_missing_file_default_is_literal(self) -> None:
|
|
3176
|
+
with tempfile.TemporaryDirectory(prefix="team-agent-missing-state-") as tmp:
|
|
3177
|
+
self.assertEqual(load_runtime_state(Path(tmp)), {"agents": {}, "tasks": [], "session_name": None})
|
|
3178
|
+
|
|
3179
|
+
def test_snapshot_state_session_fields_are_backward_compatible(self) -> None:
|
|
3180
|
+
with tempfile.TemporaryDirectory(prefix="team-agent-old-snapshot-") as tmp:
|
|
3181
|
+
state_path = Path(tmp) / "state.json"
|
|
3182
|
+
state_path.write_text(
|
|
3183
|
+
json.dumps(
|
|
3184
|
+
{
|
|
3185
|
+
"session_name": "team-old",
|
|
3186
|
+
"agents": {
|
|
3187
|
+
"fake_impl": {"status": "stopped", "provider": "fake", "window": "fake_impl"},
|
|
3188
|
+
"legacy_bad": None,
|
|
3189
|
+
},
|
|
3190
|
+
"tasks": [],
|
|
3191
|
+
}
|
|
3192
|
+
),
|
|
3193
|
+
encoding="utf-8",
|
|
3194
|
+
)
|
|
3195
|
+
loaded = runtime._load_snapshot_state(state_path)
|
|
3196
|
+
self.assertIsNotNone(loaded)
|
|
3197
|
+
agent = loaded["agents"]["fake_impl"]
|
|
3198
|
+
for key in ["session_id", "rollout_path", "captured_at", "captured_via", "attribution_confidence", "spawn_cwd"]:
|
|
3199
|
+
self.assertIn(key, agent)
|
|
3200
|
+
self.assertIsNone(agent[key])
|
|
3201
|
+
self.assertIsNone(loaded["agents"]["legacy_bad"])
|
|
3202
|
+
|
|
3203
|
+
def test_snapshot_state_malformed_json_returns_none(self) -> None:
|
|
3204
|
+
with tempfile.TemporaryDirectory(prefix="team-agent-bad-snapshot-") as tmp:
|
|
3205
|
+
state_path = Path(tmp) / "state.json"
|
|
3206
|
+
state_path.write_text("{bad json", encoding="utf-8")
|
|
3207
|
+
self.assertIsNone(runtime._load_snapshot_state(state_path))
|
|
3208
|
+
|
|
3209
|
+
def test_session_state_normalization_ignores_non_dict_agents_container(self) -> None:
|
|
3210
|
+
with tempfile.TemporaryDirectory(prefix="team-agent-odd-agents-") as tmp:
|
|
3211
|
+
workspace = Path(tmp)
|
|
3212
|
+
state_path = runtime.runtime_state_path(workspace)
|
|
3213
|
+
state_path.parent.mkdir(parents=True)
|
|
3214
|
+
state_path.write_text(json.dumps({"session_name": "team-old", "agents": "legacy-bad", "tasks": []}), encoding="utf-8")
|
|
3215
|
+
|
|
3216
|
+
loaded = load_runtime_state(workspace)
|
|
3217
|
+
self.assertEqual(loaded["agents"], "legacy-bad")
|
|
3218
|
+
|
|
3219
|
+
snapshot = runtime._load_snapshot_state(state_path)
|
|
3220
|
+
self.assertIsNotNone(snapshot)
|
|
3221
|
+
self.assertEqual(snapshot["agents"], "legacy-bad")
|
|
3222
|
+
|
|
3223
|
+
def test_copy_session_metadata_copies_known_fields_only(self) -> None:
|
|
3224
|
+
full_source = {
|
|
3225
|
+
"session_id": "session-123",
|
|
3226
|
+
"rollout_path": "rollout.jsonl",
|
|
3227
|
+
"captured_at": "2026-05-16T00:00:00+00:00",
|
|
3228
|
+
"captured_via": "fs_watch",
|
|
3229
|
+
"attribution_confidence": "high",
|
|
3230
|
+
"spawn_cwd": "/tmp/workspace",
|
|
3231
|
+
"extra": "ignored",
|
|
3232
|
+
}
|
|
3233
|
+
target = {"preexisting": "kept"}
|
|
3234
|
+
|
|
3235
|
+
runtime._copy_session_metadata(target, full_source)
|
|
3236
|
+
|
|
3237
|
+
for key in runtime.SESSION_STATE_FIELDS:
|
|
3238
|
+
self.assertEqual(target[key], full_source[key])
|
|
3239
|
+
self.assertEqual(target["preexisting"], "kept")
|
|
3240
|
+
self.assertNotIn("extra", target)
|
|
3241
|
+
|
|
3242
|
+
missing_source = {"session_id": "session-456"}
|
|
3243
|
+
runtime._copy_session_metadata(target, missing_source)
|
|
3244
|
+
|
|
3245
|
+
self.assertEqual(target["session_id"], "session-456")
|
|
3246
|
+
for key in runtime.SESSION_STATE_FIELDS:
|
|
3247
|
+
if key != "session_id":
|
|
3248
|
+
self.assertIsNone(target[key])
|
|
3249
|
+
|
|
3250
|
+
def test_clear_session_capture_fields_preserves_spawn_cwd(self) -> None:
|
|
3251
|
+
target = {
|
|
3252
|
+
"session_id": "session-123",
|
|
3253
|
+
"rollout_path": "rollout.jsonl",
|
|
3254
|
+
"captured_at": "2026-05-16T00:00:00+00:00",
|
|
3255
|
+
"captured_via": "fs_watch",
|
|
3256
|
+
"attribution_confidence": "high",
|
|
3257
|
+
"spawn_cwd": "/tmp/workspace",
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
runtime._clear_session_capture_fields(target)
|
|
3261
|
+
|
|
3262
|
+
for key in runtime.SESSION_CAPTURE_FIELDS:
|
|
3263
|
+
self.assertIsNone(target[key])
|
|
3264
|
+
self.assertEqual(target["spawn_cwd"], "/tmp/workspace")
|
|
3149
3265
|
|
|
3150
3266
|
def test_claude_adapter_predetermines_session_id_and_resume_command(self) -> None:
|
|
3151
3267
|
adapter = get_adapter("claude_code")
|
|
@@ -3249,6 +3365,65 @@ Handle fake tasks.
|
|
|
3249
3365
|
self.assertTrue(any(e["event"] == "resume.session_unverified" for e in events))
|
|
3250
3366
|
self.assertTrue(any(e["event"] == "resume.session_repaired" for e in events))
|
|
3251
3367
|
|
|
3368
|
+
def test_prepare_resume_state_copies_adapter_repaired_session_metadata(self) -> None:
|
|
3369
|
+
with tempfile.TemporaryDirectory(prefix="team-agent-adapter-repair-") as tmp:
|
|
3370
|
+
workspace = Path(tmp)
|
|
3371
|
+
event_log = runtime.EventLog(workspace)
|
|
3372
|
+
adapter = Mock()
|
|
3373
|
+
adapter.session_is_resumable.return_value = False
|
|
3374
|
+
adapter.recover_session_id.return_value = {
|
|
3375
|
+
"session_id": "repaired-session",
|
|
3376
|
+
"rollout_path": "repaired.jsonl",
|
|
3377
|
+
"captured_at": "2026-05-16T00:00:00+00:00",
|
|
3378
|
+
"captured_via": "fs_repair",
|
|
3379
|
+
"attribution_confidence": "high",
|
|
3380
|
+
"spawn_cwd": str(workspace / "repaired-cwd"),
|
|
3381
|
+
}
|
|
3382
|
+
previous = {
|
|
3383
|
+
"status": "stopped",
|
|
3384
|
+
"provider": "claude_code",
|
|
3385
|
+
"spawn_cwd": str(workspace / "old-cwd"),
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
repaired = runtime._prepare_resume_state(workspace, "analyst", previous, adapter, event_log)
|
|
3389
|
+
|
|
3390
|
+
self.assertEqual(repaired["session_id"], "repaired-session")
|
|
3391
|
+
self.assertEqual(repaired["rollout_path"], "repaired.jsonl")
|
|
3392
|
+
self.assertEqual(repaired["captured_at"], "2026-05-16T00:00:00+00:00")
|
|
3393
|
+
self.assertEqual(repaired["captured_via"], "fs_repair")
|
|
3394
|
+
self.assertEqual(repaired["attribution_confidence"], "high")
|
|
3395
|
+
self.assertEqual(repaired["spawn_cwd"], str(workspace / "repaired-cwd"))
|
|
3396
|
+
|
|
3397
|
+
def test_prepare_resume_state_allow_fresh_clears_capture_fields_and_preserves_spawn_cwd(self) -> None:
|
|
3398
|
+
with tempfile.TemporaryDirectory(prefix="team-agent-resume-allow-fresh-") as tmp:
|
|
3399
|
+
workspace = Path(tmp)
|
|
3400
|
+
adapter = Mock()
|
|
3401
|
+
adapter.session_is_resumable.return_value = False
|
|
3402
|
+
adapter.recover_session_id.return_value = None
|
|
3403
|
+
previous = {
|
|
3404
|
+
"status": "stopped",
|
|
3405
|
+
"provider": "claude_code",
|
|
3406
|
+
"session_id": "stale-session",
|
|
3407
|
+
"rollout_path": "stale.jsonl",
|
|
3408
|
+
"captured_at": "2026-05-16T00:00:00+00:00",
|
|
3409
|
+
"captured_via": "fs_watch",
|
|
3410
|
+
"attribution_confidence": "high",
|
|
3411
|
+
"spawn_cwd": str(workspace / "old-cwd"),
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
prepared = runtime._prepare_resume_state(
|
|
3415
|
+
workspace,
|
|
3416
|
+
"analyst",
|
|
3417
|
+
previous,
|
|
3418
|
+
adapter,
|
|
3419
|
+
runtime.EventLog(workspace),
|
|
3420
|
+
allow_fresh_on_resume_failure=True,
|
|
3421
|
+
)
|
|
3422
|
+
|
|
3423
|
+
for key in runtime.SESSION_CAPTURE_FIELDS:
|
|
3424
|
+
self.assertIsNone(prepared[key])
|
|
3425
|
+
self.assertEqual(prepared["spawn_cwd"], str(workspace / "old-cwd"))
|
|
3426
|
+
|
|
3252
3427
|
def test_prepare_resume_state_repairs_claude_session_from_pending_isolated_id(self) -> None:
|
|
3253
3428
|
adapter = get_adapter("claude_code")
|
|
3254
3429
|
with tempfile.TemporaryDirectory(prefix="team-agent-claude-pending-repair-") as tmp:
|
|
@@ -3318,12 +3493,19 @@ Handle fake tasks.
|
|
|
3318
3493
|
"spawn_cwd": str(workspace),
|
|
3319
3494
|
"spawned_at": "2026-05-16T00:00:00+00:00",
|
|
3320
3495
|
"claude_projects_root": str(projects_root),
|
|
3496
|
+
"_pending_session_id": "pending-session",
|
|
3321
3497
|
}
|
|
3322
3498
|
with patch("team_agent.runtime.get_adapter", return_value=adapter):
|
|
3323
3499
|
result = runtime._capture_agent_session(workspace, "analyst", agent_state, runtime.EventLog(workspace), timeout_s=0)
|
|
3324
3500
|
|
|
3325
3501
|
self.assertEqual(result["session_id"], "session-123")
|
|
3326
3502
|
self.assertEqual(seen["claude_projects_root"], str(projects_root))
|
|
3503
|
+
for key in runtime.SESSION_STATE_FIELDS:
|
|
3504
|
+
self.assertEqual(agent_state[key], result[key])
|
|
3505
|
+
self.assertNotIn("_pending_session_id", agent_state)
|
|
3506
|
+
event = _events(workspace)[-1]
|
|
3507
|
+
self.assertEqual(event["event"], "session.captured")
|
|
3508
|
+
self.assertEqual(event["session_id"], "session-123")
|
|
3327
3509
|
|
|
3328
3510
|
def test_startup_verify_rejects_window_that_disappears_immediately(self) -> None:
|
|
3329
3511
|
with tempfile.TemporaryDirectory(prefix="team-agent-startup-stability-") as tmp:
|
|
@@ -3551,16 +3733,7 @@ Handle fake tasks.
|
|
|
3551
3733
|
save_runtime_state(workspace, state)
|
|
3552
3734
|
|
|
3553
3735
|
started_windows: set[str] = set()
|
|
3554
|
-
|
|
3555
|
-
def fake_run_cmd(args: list[str], timeout: int = 20):
|
|
3556
|
-
proc = Mock(returncode=1 if args[:2] == ["tmux", "has-session"] else 0, stdout="", stderr="")
|
|
3557
|
-
if args[:3] == ["tmux", "new-session", "-d"]:
|
|
3558
|
-
started_windows.add(args[6])
|
|
3559
|
-
elif args[:2] == ["tmux", "new-window"]:
|
|
3560
|
-
started_windows.add(args[5])
|
|
3561
|
-
elif args[:3] == ["tmux", "list-windows", "-t"]:
|
|
3562
|
-
proc.stdout = "\n".join(sorted(started_windows))
|
|
3563
|
-
return proc
|
|
3736
|
+
fake_run_cmd = _make_fake_tmux_window_run_cmd(started_windows)
|
|
3564
3737
|
|
|
3565
3738
|
with patch("team_agent.runtime.run_cmd", side_effect=fake_run_cmd), patch(
|
|
3566
3739
|
"team_agent.runtime.start_coordinator", return_value={"ok": True, "pid": 321, "status": "started"}
|
|
@@ -3578,6 +3751,10 @@ Handle fake tasks.
|
|
|
3578
3751
|
):
|
|
3579
3752
|
fresh = runtime.restart(workspace)
|
|
3580
3753
|
self.assertEqual(fresh["agents"][0]["restart_mode"], "fresh")
|
|
3754
|
+
fresh_agent = load_runtime_state(workspace)["agents"]["fake_impl"]
|
|
3755
|
+
for key in runtime.SESSION_CAPTURE_FIELDS:
|
|
3756
|
+
self.assertIsNone(fresh_agent[key])
|
|
3757
|
+
self.assertEqual(fresh_agent["spawn_cwd"], str(workspace))
|
|
3581
3758
|
self.assertTrue(any(e["event"] == "restart.fresh_spawn" for e in _events(workspace)))
|
|
3582
3759
|
|
|
3583
3760
|
def test_restart_requires_team_selector_when_multiple_snapshots_exist(self) -> None:
|
|
@@ -3616,16 +3793,7 @@ Handle fake tasks.
|
|
|
3616
3793
|
self.assertIn("team-beta", str(ctx.exception))
|
|
3617
3794
|
|
|
3618
3795
|
started_windows: set[str] = set()
|
|
3619
|
-
|
|
3620
|
-
def fake_run_cmd(args: list[str], timeout: int = 20):
|
|
3621
|
-
proc = Mock(returncode=1 if args[:2] == ["tmux", "has-session"] else 0, stdout="", stderr="")
|
|
3622
|
-
if args[:3] == ["tmux", "new-session", "-d"]:
|
|
3623
|
-
started_windows.add(args[6])
|
|
3624
|
-
elif args[:2] == ["tmux", "new-window"]:
|
|
3625
|
-
started_windows.add(args[5])
|
|
3626
|
-
elif args[:3] == ["tmux", "list-windows", "-t"]:
|
|
3627
|
-
proc.stdout = "\n".join(sorted(started_windows))
|
|
3628
|
-
return proc
|
|
3796
|
+
fake_run_cmd = _make_fake_tmux_window_run_cmd(started_windows)
|
|
3629
3797
|
|
|
3630
3798
|
with patch("team_agent.runtime.run_cmd", side_effect=fake_run_cmd), patch(
|
|
3631
3799
|
"team_agent.runtime.start_coordinator", return_value={"ok": True, "pid": 321, "status": "started"}
|
|
@@ -3663,16 +3831,7 @@ Handle fake tasks.
|
|
|
3663
3831
|
)
|
|
3664
3832
|
started_windows: set[str] = set()
|
|
3665
3833
|
display_snapshots: list[set[str]] = []
|
|
3666
|
-
|
|
3667
|
-
def fake_run_cmd(args: list[str], timeout: int = 20):
|
|
3668
|
-
proc = Mock(returncode=1 if args[:2] == ["tmux", "has-session"] else 0, stdout="", stderr="")
|
|
3669
|
-
if args[:3] == ["tmux", "new-session", "-d"]:
|
|
3670
|
-
started_windows.add(args[6])
|
|
3671
|
-
elif args[:2] == ["tmux", "new-window"]:
|
|
3672
|
-
started_windows.add(args[5])
|
|
3673
|
-
elif args[:3] == ["tmux", "list-windows", "-t"]:
|
|
3674
|
-
proc.stdout = "\n".join(sorted(started_windows))
|
|
3675
|
-
return proc
|
|
3834
|
+
fake_run_cmd = _make_fake_tmux_window_run_cmd(started_windows)
|
|
3676
3835
|
|
|
3677
3836
|
def fake_open_display(workspace_arg, session_name, window_name, agent, event_log):
|
|
3678
3837
|
display_snapshots.append(set(started_windows))
|
|
@@ -3892,6 +4051,10 @@ Handle fake tasks.
|
|
|
3892
4051
|
self.assertTrue(result["ok"])
|
|
3893
4052
|
self.assertEqual(result["start_mode"], "fresh")
|
|
3894
4053
|
self.assertEqual(start_modes, ["resumed", "fresh"])
|
|
4054
|
+
agent_state = load_runtime_state(workspace)["agents"]["fake_impl"]
|
|
4055
|
+
for key in runtime.SESSION_CAPTURE_FIELDS:
|
|
4056
|
+
self.assertIsNone(agent_state[key])
|
|
4057
|
+
self.assertEqual(agent_state["spawn_cwd"], str(workspace))
|
|
3895
4058
|
events = _events(workspace)
|
|
3896
4059
|
self.assertTrue(any(e["event"] == "start_agent.window_missing_after_start" for e in events))
|
|
3897
4060
|
self.assertTrue(any(e["event"] == "start_agent.resume_window_missing_fallback_fresh" for e in events))
|
|
@@ -5300,6 +5463,20 @@ def _fake_spec(workspace: Path) -> dict:
|
|
|
5300
5463
|
return spec
|
|
5301
5464
|
|
|
5302
5465
|
|
|
5466
|
+
def _make_fake_tmux_window_run_cmd(started_windows: set[str]):
|
|
5467
|
+
def fake_run_cmd(args: list[str], timeout: int = 20):
|
|
5468
|
+
proc = Mock(returncode=1 if args[:2] == ["tmux", "has-session"] else 0, stdout="", stderr="")
|
|
5469
|
+
if args[:3] == ["tmux", "new-session", "-d"]:
|
|
5470
|
+
started_windows.add(args[6])
|
|
5471
|
+
elif args[:2] == ["tmux", "new-window"]:
|
|
5472
|
+
started_windows.add(args[5])
|
|
5473
|
+
elif args[:3] == ["tmux", "list-windows", "-t"]:
|
|
5474
|
+
proc.stdout = "\n".join(sorted(started_windows))
|
|
5475
|
+
return proc
|
|
5476
|
+
|
|
5477
|
+
return fake_run_cmd
|
|
5478
|
+
|
|
5479
|
+
|
|
5303
5480
|
def _write_doc_team(workspace: Path) -> Path:
|
|
5304
5481
|
team = workspace / ".team" / "current"
|
|
5305
5482
|
(team / "agents").mkdir(parents=True, exist_ok=True)
|