Skip to content

Commit 910232e

Browse files
Trecekclaude
andcommitted
fix: resolve test failures from stale RUNNING dispatch recovery implementation
- Extract find_locked_dispatch helper to fleet/_reset.py domain layer to satisfy REQ-CNST-008 (no comprehensions in tool handlers) - Hoist CampaignStateMutator import to module level to satisfy IL-3 cross-layer import hoisting and cross-package submodule rules - Fix test_reset_dispatch_running_dead_process_succeeds to mock resolve_stale_running returning False (dead) and assert success - Fix test_reset_dispatch_running_alive_process_still_blocked to mock resolve_stale_running returning True (alive) and assert block - Update _LEGACY_JSON_WRITES line numbers for tools_kitchen.py shifts - Bump tools_kitchen.py line limit exemption to 1290 for reaper call Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 80f6551 commit 910232e

6 files changed

Lines changed: 31 additions & 37 deletions

File tree

src/autoskillit/fleet/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from ._reset import (
2525
ResetReport,
2626
find_dispatch_in_campaigns,
27+
find_locked_dispatch,
2728
format_resettable_statuses,
2829
reset_dispatch_artifacts,
2930
resolve_worktrees_dir,
@@ -181,6 +182,7 @@
181182
"ResetReport",
182183
"compute_reset_labels",
183184
"find_dispatch_in_campaigns",
185+
"find_locked_dispatch",
184186
"format_resettable_statuses",
185187
"reset_dispatch_artifacts",
186188
"resolve_worktrees_dir",

src/autoskillit/fleet/_reset.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
__all__ = [
3636
"ResetReport",
3737
"find_dispatch_in_campaigns",
38+
"find_locked_dispatch",
3839
"compute_reset_labels",
3940
"format_resettable_statuses",
4041
"reset_dispatch_artifacts",
@@ -80,6 +81,16 @@ def find_dispatch_in_campaigns(
8081
return None
8182

8283

84+
def find_locked_dispatch(dispatch_id: str, mutator: CampaignStateMutator) -> DispatchRecord | None:
85+
"""Locate a dispatch inside a locked mutator's state by dispatch_id or name."""
86+
if mutator.state is None:
87+
return None
88+
for d in mutator.state.dispatches:
89+
if (d.dispatch_id and d.dispatch_id == dispatch_id) or d.name == dispatch_id:
90+
return d
91+
return None
92+
93+
8394
def compute_reset_labels(target_state: IssueLabelState) -> tuple[list[str], list[str]]:
8495
label_def = LABEL_LIFECYCLE_REGISTRY[target_state]
8596
remove = sorted(s.value for s in label_def.removes_on_entry | {IssueLabelState.IN_PROGRESS})

src/autoskillit/server/tools/tools_fleet_reset.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
from autoskillit.core import FleetErrorCode, IssueLabelState, fleet_error, get_logger
1414
from autoskillit.fleet import (
1515
_RESETTABLE_STATUSES,
16+
CampaignStateMutator,
1617
DispatchStatus,
1718
ResetReport,
1819
cleanup_orphaned_labels,
1920
compute_reset_labels,
2021
discover_campaign_state_files,
2122
find_dispatch_in_campaigns,
23+
find_locked_dispatch,
2224
format_resettable_statuses,
2325
reset_dispatch_artifacts,
2426
resolve_stale_running,
@@ -124,23 +126,8 @@ async def reset_dispatch(
124126
dispatch, state_path = result
125127

126128
if dispatch.status == DispatchStatus.RUNNING:
127-
from autoskillit.fleet.state import CampaignStateMutator # circular-break
128-
129129
with CampaignStateMutator(state_path) as m:
130-
if m.state is None:
131-
return fleet_error(
132-
FleetErrorCode.FLEET_RESET_NOT_FOUND,
133-
f"No dispatch found matching {dispatch_id!r} in any campaign state file.",
134-
)
135-
locked_dispatch = next(
136-
(
137-
d
138-
for d in m.state.dispatches
139-
if (d.dispatch_id and d.dispatch_id == dispatch_id)
140-
or d.name == dispatch_id
141-
),
142-
None,
143-
)
130+
locked_dispatch = find_locked_dispatch(dispatch_id, m)
144131
if locked_dispatch is None:
145132
return fleet_error(
146133
FleetErrorCode.FLEET_RESET_NOT_FOUND,

tests/arch/test_subpackage_isolation.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -967,7 +967,7 @@ def test_data_directories_are_not_python_packages() -> None:
967967
"co-located with the execution engine that calls them",
968968
),
969969
"tools_kitchen.py": (
970-
1280,
970+
1290,
971971
"REQ-CNST-010-E7: kitchen tool handlers — open_kitchen and lock_ingredients require "
972972
"inline validation helpers (_check_override_keys, _build_ingredient_key_suggestions) "
973973
"for ingredient key validation; splitting would cross import-layer boundaries; "
@@ -980,7 +980,9 @@ def test_data_directories_are_not_python_packages() -> None:
980980
"_dispatch_infeasible_response helper for DOA pipeline refusal"
981981
"; capability admission control dispatch_feasible gating on deferred-recall and "
982982
"get_recipe paths (+22 net lines)"
983-
"; supports_quota_check bool at call sites replaces resolve_provider (+3 net lines)",
983+
"; supports_quota_check bool at call sites replaces resolve_provider (+3 net lines)"
984+
"; reap_stale_dispatches_async call in _open_kitchen_handler for interactive session "
985+
"dispatch recovery (+8 net lines)",
984986
),
985987
"tools_execution.py": (
986988
1200,

tests/infra/test_schema_version_convention.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,11 @@ def _is_yaml_dump(node: ast.expr) -> bool:
119119
# _lifespan.py — hooks.json self-heal on startup drift (co-owned with Claude plugin system)
120120
("src/autoskillit/server/_lifespan.py", 90),
121121
# tools_kitchen.py — hook config, quota guard, git_ops_policy, ingredient locks overlay
122-
("src/autoskillit/server/tools/tools_kitchen.py", 200),
123-
("src/autoskillit/server/tools/tools_kitchen.py", 219),
124-
("src/autoskillit/server/tools/tools_kitchen.py", 253),
125-
("src/autoskillit/server/tools/tools_kitchen.py", 998),
126-
("src/autoskillit/server/tools/tools_kitchen.py", 1058),
122+
("src/autoskillit/server/tools/tools_kitchen.py", 204),
123+
("src/autoskillit/server/tools/tools_kitchen.py", 223),
124+
("src/autoskillit/server/tools/tools_kitchen.py", 257),
125+
("src/autoskillit/server/tools/tools_kitchen.py", 1013),
126+
("src/autoskillit/server/tools/tools_kitchen.py", 1073),
127127
# tools_pipeline_tracker.py — tracker_data dict
128128
("src/autoskillit/server/tools/tools_pipeline_tracker.py", 166),
129129
# tools_status.py — mcp_data dict

tests/server/test_tools_fleet_reset.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -266,18 +266,20 @@ async def test_reset_dispatch_running_dead_process_succeeds(
266266
tool_ctx = build_ctx_open()
267267
_setup_tool(tool_ctx, monkeypatch, state_path)
268268

269-
from autoskillit.fleet import resolve_stale_running
269+
def _mock_resolve_dead(d, _m, **_kw):
270+
d.status = DispatchStatus.INTERRUPTED
271+
return False
270272

271273
monkeypatch.setattr(
272274
"autoskillit.server.tools.tools_fleet_reset.resolve_stale_running",
273-
resolve_stale_running,
275+
_mock_resolve_dead,
274276
)
275277

276278
from autoskillit.server.tools.tools_fleet_reset import reset_dispatch
277279

278280
raw = await reset_dispatch(dispatch_id="d-abc123")
279281
result = json.loads(raw)
280-
assert result["error"] != "fleet_reset_still_running"
282+
assert result["success"] is True
281283

282284
@pytest.mark.anyio
283285
async def test_reset_dispatch_running_alive_process_still_blocked(
@@ -288,21 +290,11 @@ async def test_reset_dispatch_running_alive_process_still_blocked(
288290
tool_ctx = build_ctx_open()
289291
_setup_tool(tool_ctx, monkeypatch, state_path)
290292

291-
from autoskillit.fleet import resolve_stale_running
292-
293293
monkeypatch.setattr(
294294
"autoskillit.server.tools.tools_fleet_reset.resolve_stale_running",
295-
resolve_stale_running,
295+
lambda _d, _m, **_kw: True,
296296
)
297297

298-
state_path_raw = state_path.read_text()
299-
import json as _json
300-
301-
raw_obj = _json.loads(state_path_raw)
302-
raw_obj["dispatches"][0]["dispatched_pid"] = 999999999
303-
raw_obj["dispatches"][0]["dispatched_boot_id"] = "nonexistent-boot-id"
304-
state_path.write_text(_json.dumps(raw_obj))
305-
306298
from autoskillit.server.tools.tools_fleet_reset import reset_dispatch
307299

308300
raw = await reset_dispatch(dispatch_id="d-abc123")

0 commit comments

Comments
 (0)