prizmkit 1.1.106 → 1.1.107
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/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +56 -5
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -8
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +9 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +278 -62
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +102 -17
- package/bundled/dev-pipeline/scripts/continuation.py +0 -4
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +7 -39
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +1 -31
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -31
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +5 -6
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +28 -28
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +36 -52
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +34 -54
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +2 -3
- package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +4 -9
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +4 -10
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -3
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +3 -3
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +49 -2
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +17 -0
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +50 -1
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +352 -1
- package/bundled/dev-pipeline/tests/test_unified_cli.py +207 -0
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +12 -13
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +10 -10
- package/package.json +1 -1
- package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +0 -7
- package/bundled/dev-pipeline/templates/agent-prompts/dev-resume.md +0 -5
- package/bundled/skills/prizmkit-code-review/references/dev-agent-prompt.md +0 -30
|
@@ -28,6 +28,20 @@ from generate_bootstrap_prompt import (
|
|
|
28
28
|
)
|
|
29
29
|
from continuation import append_continuation_handoff, checkpoint_cursor
|
|
30
30
|
|
|
31
|
+
REMOVED_MAX_LOG_ENV = "MAX_" + "LOG_SIZE"
|
|
32
|
+
REMOVED_MAX_LOG_HUMAN = REMOVED_MAX_LOG_ENV + "_HUMAN"
|
|
33
|
+
|
|
34
|
+
FORBIDDEN_DEV_FIX_PROMPT = "dev-" + "fix.md"
|
|
35
|
+
FORBIDDEN_DEV_RESUME_PROMPT = "dev-" + "resume.md"
|
|
36
|
+
FORBIDDEN_DEV_FIX_PLACEHOLDER = "{{AGENT_PROMPT_DEV_" + "FIX}}"
|
|
37
|
+
FORBIDDEN_DEV_RESUME_PLACEHOLDER = "{{AGENT_PROMPT_DEV_" + "RESUME}}"
|
|
38
|
+
FORBIDDEN_REVIEW_DEV_FIX = "Reviewer Agent → filter → " + "Dev Agent fix"
|
|
39
|
+
FORBIDDEN_REVIEWER_DEV_SPAWN = "Reviewer/" + "Dev agent spawn"
|
|
40
|
+
FORBIDDEN_EXPLORE_SUBAGENT = "subagent_type: " + "Explore"
|
|
41
|
+
FORBIDDEN_TEST_CMD_PLACEHOLDER = "(" + "$TEST_CMD" + ")"
|
|
42
|
+
FORBIDDEN_THREE_STRIKE = "3-" + "strike"
|
|
43
|
+
FORBIDDEN_TWENTY_STEP = "20-" + "step window"
|
|
44
|
+
|
|
31
45
|
|
|
32
46
|
def scoped_report_text(verdict="PASS", boundary_missing=0,
|
|
33
47
|
happy_path_only=0, completion_gate="yes",
|
|
@@ -222,6 +236,11 @@ class TestExplicitProjectRoot:
|
|
|
222
236
|
assert not replacements["{{SESSION_STATUS_PATH}}"].startswith(str(execution_root.resolve()))
|
|
223
237
|
assert replacements["{{PROJECT_BRIEF}}"] == "worktree brief"
|
|
224
238
|
assert replacements["{{DEV_SUBAGENT_PATH}}"].startswith(str(execution_root.resolve()))
|
|
239
|
+
assert replacements["{{SESSION_LOG_PATH}}"].endswith(
|
|
240
|
+
".prizmkit/state/features/F-001/sessions/session-1/logs/session.log"
|
|
241
|
+
)
|
|
242
|
+
assert "{{" + REMOVED_MAX_LOG_ENV + "}}" not in replacements
|
|
243
|
+
assert "{{" + REMOVED_MAX_LOG_HUMAN + "}}" not in replacements
|
|
225
244
|
|
|
226
245
|
|
|
227
246
|
def test_main_writes_checkpoint_under_cli_project_root_and_status_under_state_dir(self, tmp_path, monkeypatch):
|
|
@@ -779,11 +798,14 @@ class TestHeadlessRecoveryReframing:
|
|
|
779
798
|
def test_log_size_awareness_is_passive_checkpoint_guidance(self):
|
|
780
799
|
content = load_log_size_section(str(Path("dev-pipeline/scripts").resolve()))
|
|
781
800
|
|
|
782
|
-
assert "Session
|
|
801
|
+
assert "Session Checkpoint Awareness" in content
|
|
783
802
|
assert "passive checkpoint guidance" in content
|
|
784
803
|
assert "durable checkpoints and artifacts" in content
|
|
785
804
|
assert "fresh continuation session" in content
|
|
786
805
|
assert "optional interactive convenience only" in content
|
|
806
|
+
assert REMOVED_MAX_LOG_ENV not in content
|
|
807
|
+
assert ("maximum-" + "log-size") not in content
|
|
808
|
+
assert ("log size " + "limit") not in content.lower()
|
|
787
809
|
assert "COMPACT_NEEDED" not in content
|
|
788
810
|
assert "monitor-log" not in content
|
|
789
811
|
assert "background monitoring subagent" not in content
|
|
@@ -802,6 +824,10 @@ class TestHeadlessRecoveryReframing:
|
|
|
802
824
|
)
|
|
803
825
|
assert "Headless recovery must rely on durable checkpoints" in rendered
|
|
804
826
|
assert "fresh continuation session" in rendered
|
|
827
|
+
assert REMOVED_MAX_LOG_ENV not in rendered
|
|
828
|
+
assert REMOVED_MAX_LOG_HUMAN not in rendered
|
|
829
|
+
assert ("209" + "7152") not in rendered
|
|
830
|
+
assert ("2" + "MB") not in rendered
|
|
805
831
|
assert "monitor-log" not in rendered
|
|
806
832
|
assert "COMPACT_NEEDED" not in rendered
|
|
807
833
|
assert "log-size monitor" not in rendered
|
|
@@ -818,9 +844,16 @@ class TestDirectOrchestratorImplementationPrompts:
|
|
|
818
844
|
replacements = load_active_agent_prompts(str(templates_dir))
|
|
819
845
|
|
|
820
846
|
assert "dev-implement.md" not in ACTIVE_AGENT_PROMPTS
|
|
847
|
+
assert FORBIDDEN_DEV_FIX_PROMPT not in ACTIVE_AGENT_PROMPTS
|
|
848
|
+
assert FORBIDDEN_DEV_RESUME_PROMPT not in ACTIVE_AGENT_PROMPTS
|
|
821
849
|
assert "{{AGENT_PROMPT_DEV_IMPLEMENT}}" not in replacements
|
|
850
|
+
assert FORBIDDEN_DEV_FIX_PLACEHOLDER not in replacements
|
|
851
|
+
assert FORBIDDEN_DEV_RESUME_PLACEHOLDER not in replacements
|
|
822
852
|
assert "{{AGENT_PROMPT_CRITIC_PLAN_CHALLENGE}}" in replacements
|
|
853
|
+
assert "{{AGENT_PROMPT_REVIEWER_REVIEW}}" in replacements
|
|
823
854
|
assert (templates_dir / "agent-prompts" / "dev-implement.md").exists() is False
|
|
855
|
+
assert (templates_dir / "agent-prompts" / FORBIDDEN_DEV_FIX_PROMPT).exists() is False
|
|
856
|
+
assert (templates_dir / "agent-prompts" / FORBIDDEN_DEV_RESUME_PROMPT).exists() is False
|
|
824
857
|
|
|
825
858
|
def test_sections_all_modes_use_direct_orchestrator_implementation(self):
|
|
826
859
|
sections_dir = Path("dev-pipeline/templates/sections").resolve()
|
|
@@ -840,7 +873,13 @@ class TestDirectOrchestratorImplementationPrompts:
|
|
|
840
873
|
assert "Implement — Dev" not in rendered
|
|
841
874
|
assert "{{AGENT_PROMPT_DEV_IMPLEMENT}}" not in rendered
|
|
842
875
|
assert "dev-implement.md" not in rendered
|
|
843
|
-
assert
|
|
876
|
+
assert FORBIDDEN_REVIEW_DEV_FIX not in rendered
|
|
877
|
+
assert FORBIDDEN_REVIEWER_DEV_SPAWN not in rendered
|
|
878
|
+
assert FORBIDDEN_EXPLORE_SUBAGENT not in rendered
|
|
879
|
+
assert FORBIDDEN_TEST_CMD_PLACEHOLDER not in rendered
|
|
880
|
+
assert FORBIDDEN_THREE_STRIKE not in rendered
|
|
881
|
+
assert FORBIDDEN_TWENTY_STEP not in rendered
|
|
882
|
+
assert "Reviewer Agent → filter → orchestrator fix" in rendered
|
|
844
883
|
|
|
845
884
|
def test_legacy_tier_templates_use_direct_orchestrator_implementation(self):
|
|
846
885
|
for template_name in ("bootstrap-tier2.md", "bootstrap-tier3.md"):
|
|
@@ -853,6 +892,12 @@ class TestDirectOrchestratorImplementationPrompts:
|
|
|
853
892
|
assert "Implement — Dev" not in rendered
|
|
854
893
|
assert "{{AGENT_PROMPT_DEV_IMPLEMENT}}" not in rendered
|
|
855
894
|
assert "dev-implement.md" not in rendered
|
|
895
|
+
assert FORBIDDEN_REVIEW_DEV_FIX not in rendered
|
|
896
|
+
assert FORBIDDEN_REVIEWER_DEV_SPAWN not in rendered
|
|
897
|
+
assert FORBIDDEN_EXPLORE_SUBAGENT not in rendered
|
|
898
|
+
assert FORBIDDEN_TEST_CMD_PLACEHOLDER not in rendered
|
|
899
|
+
assert FORBIDDEN_THREE_STRIKE not in rendered
|
|
900
|
+
assert FORBIDDEN_TWENTY_STEP not in rendered
|
|
856
901
|
|
|
857
902
|
|
|
858
903
|
# ---------------------------------------------------------------------------
|
|
@@ -898,6 +943,8 @@ class TestContinuationHandoff:
|
|
|
898
943
|
assert ".prizmkit/state/features/F-123/sessions/F-123-new/logs/session.log" in rendered
|
|
899
944
|
assert ".prizmkit/state/features/F-123/sessions/F-123-new/logs/progress.json" in rendered
|
|
900
945
|
assert ".prizmkit/state/features/F-123/sessions/F-123-new/logs/ai.pid" in rendered
|
|
946
|
+
assert ("Log " + "monitor") not in rendered
|
|
947
|
+
assert ("log-" + "monitor") not in rendered
|
|
901
948
|
|
|
902
949
|
summary = tmp_path / ".prizmkit/specs/123-continuation/continuation-summary.md"
|
|
903
950
|
assert summary.exists()
|
|
@@ -16,6 +16,9 @@ from generate_bugfix_prompt import (
|
|
|
16
16
|
)
|
|
17
17
|
from continuation import append_continuation_handoff
|
|
18
18
|
|
|
19
|
+
REMOVED_MAX_LOG_ENV = "MAX_" + "LOG_SIZE"
|
|
20
|
+
REMOVED_MAX_LOG_HUMAN = REMOVED_MAX_LOG_ENV + "_HUMAN"
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
# ---------------------------------------------------------------------------
|
|
21
24
|
# find_bug
|
|
@@ -193,6 +196,11 @@ class TestExplicitProjectRoot:
|
|
|
193
196
|
)
|
|
194
197
|
assert not replacements["{{SESSION_STATUS_PATH}}"].startswith(str(execution_root.resolve()))
|
|
195
198
|
assert replacements["{{DEV_SUBAGENT_PATH}}"].startswith(str(execution_root.resolve()))
|
|
199
|
+
assert replacements["{{SESSION_LOG_PATH}}"].endswith(
|
|
200
|
+
".prizmkit/state/bugfix/B-001/sessions/session-1/logs/session.log"
|
|
201
|
+
)
|
|
202
|
+
assert "{{" + REMOVED_MAX_LOG_ENV + "}}" not in replacements
|
|
203
|
+
assert "{{" + REMOVED_MAX_LOG_HUMAN + "}}" not in replacements
|
|
196
204
|
|
|
197
205
|
|
|
198
206
|
# ---------------------------------------------------------------------------
|
|
@@ -225,6 +233,9 @@ class TestHeadlessRecoveryReframing:
|
|
|
225
233
|
assert "Headless recovery must rely on durable checkpoints" in content
|
|
226
234
|
assert "fresh continuation session" in content
|
|
227
235
|
assert "optional interactive convenience only" in content
|
|
236
|
+
assert REMOVED_MAX_LOG_ENV not in content
|
|
237
|
+
assert ("maximum-" + "log-size") not in content
|
|
238
|
+
assert ("log size " + "limit") not in content.lower()
|
|
228
239
|
assert "COMPACT_NEEDED" not in content
|
|
229
240
|
assert "monitor-log" not in content
|
|
230
241
|
assert "background monitoring subagent" not in content
|
|
@@ -235,6 +246,10 @@ class TestHeadlessRecoveryReframing:
|
|
|
235
246
|
content = Path("dev-pipeline/templates/bugfix-bootstrap-prompt.md").read_text(encoding="utf-8")
|
|
236
247
|
|
|
237
248
|
assert "Do not spawn persistent background subagents for headless recovery" in content
|
|
249
|
+
assert REMOVED_MAX_LOG_ENV not in content
|
|
250
|
+
assert REMOVED_MAX_LOG_HUMAN not in content
|
|
251
|
+
assert ("209" + "7152") not in content
|
|
252
|
+
assert ("2" + "MB") not in content
|
|
238
253
|
assert "log-size monitor" not in content
|
|
239
254
|
assert "monitor-log" not in content
|
|
240
255
|
assert "COMPACT_NEEDED" not in content
|
|
@@ -264,5 +279,7 @@ class TestBugfixContinuationHandoff:
|
|
|
264
279
|
assert prompt.startswith("BUGFIX PROMPT")
|
|
265
280
|
assert "This is an automatic continuation after context overflow." in prompt
|
|
266
281
|
assert "Do not attempt `/compact` as the recovery mechanism." in prompt
|
|
282
|
+
assert ("Log " + "monitor") not in prompt
|
|
283
|
+
assert ("log-" + "monitor") not in prompt
|
|
267
284
|
assert (tmp_path / ".prizmkit/specs/B-007/continuation-summary.md").exists()
|
|
268
285
|
assert (tmp_path / ".prizmkit/bugfix/B-007/continuation-summary.md").exists()
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
"""Tests for generate-refactor-prompt.py continuation behavior."""
|
|
2
2
|
|
|
3
3
|
from argparse import Namespace
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
from continuation import append_continuation_handoff
|
|
6
|
-
from generate_refactor_prompt import
|
|
7
|
+
from generate_refactor_prompt import (
|
|
8
|
+
build_replacements,
|
|
9
|
+
generate_refactor_checkpoint,
|
|
10
|
+
load_log_size_section,
|
|
11
|
+
merge_refactor_checkpoint_state,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
REMOVED_MAX_LOG_ENV = "MAX_" + "LOG_SIZE"
|
|
15
|
+
REMOVED_MAX_LOG_HUMAN = REMOVED_MAX_LOG_ENV + "_HUMAN"
|
|
7
16
|
|
|
8
17
|
|
|
9
18
|
class TestRefactorContinuationHandoff:
|
|
@@ -27,6 +36,8 @@ class TestRefactorContinuationHandoff:
|
|
|
27
36
|
assert prompt.startswith("REFACTOR PROMPT")
|
|
28
37
|
assert "This is a fresh headless AI CLI session for the same task." in prompt
|
|
29
38
|
assert "Do not attempt `/compact` as the recovery mechanism." in prompt
|
|
39
|
+
assert ("Log " + "monitor") not in prompt
|
|
40
|
+
assert ("log-" + "monitor") not in prompt
|
|
30
41
|
assert (tmp_path / ".prizmkit/specs/R-007/continuation-summary.md").exists()
|
|
31
42
|
assert (tmp_path / ".prizmkit/refactor/R-007/continuation-summary.md").exists()
|
|
32
43
|
|
|
@@ -41,3 +52,41 @@ class TestRefactorCheckpointMerge:
|
|
|
41
52
|
|
|
42
53
|
assert merged["steps"][0]["status"] == "skipped"
|
|
43
54
|
assert merged["steps"][1]["status"] == "pending"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestRefactorPromptMaxLogRemoval:
|
|
58
|
+
def test_refactor_log_size_section_is_checkpoint_guidance_without_max_log_env(self):
|
|
59
|
+
content = load_log_size_section(str(Path("dev-pipeline/scripts").resolve()))
|
|
60
|
+
|
|
61
|
+
assert "passive checkpoint guidance" in content
|
|
62
|
+
assert "Headless recovery must rely on durable checkpoints" in content
|
|
63
|
+
assert "fresh continuation session" in content
|
|
64
|
+
assert REMOVED_MAX_LOG_ENV not in content
|
|
65
|
+
assert REMOVED_MAX_LOG_HUMAN not in content
|
|
66
|
+
assert ("maximum-" + "log-size") not in content
|
|
67
|
+
assert ("log size " + "limit") not in content.lower()
|
|
68
|
+
|
|
69
|
+
def test_refactor_replacements_keep_session_log_without_max_log_placeholders(self):
|
|
70
|
+
args = Namespace(
|
|
71
|
+
refactor_id="R-001",
|
|
72
|
+
session_id="session-1",
|
|
73
|
+
run_id="run-1",
|
|
74
|
+
retry_count="0",
|
|
75
|
+
resume_phase="null",
|
|
76
|
+
state_dir=None,
|
|
77
|
+
output="out.md",
|
|
78
|
+
template=None,
|
|
79
|
+
mode=None,
|
|
80
|
+
critic=None,
|
|
81
|
+
)
|
|
82
|
+
refactor = {"id": "R-001", "title": "Restructure", "description": "d"}
|
|
83
|
+
|
|
84
|
+
replacements = build_replacements(
|
|
85
|
+
args, refactor, [refactor], {}, str(Path("dev-pipeline/scripts").resolve())
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
assert replacements["{{SESSION_LOG_PATH}}"].endswith(
|
|
89
|
+
".prizmkit/state/refactor/R-001/sessions/session-1/logs/session.log"
|
|
90
|
+
)
|
|
91
|
+
assert "{{" + REMOVED_MAX_LOG_ENV + "}}" not in replacements
|
|
92
|
+
assert "{{" + REMOVED_MAX_LOG_HUMAN + "}}" not in replacements
|
|
@@ -318,6 +318,7 @@ def test_runner_models_parse_legacy_args_and_environment(tmp_path):
|
|
|
318
318
|
assert env.stale_kill_threshold_seconds == 9
|
|
319
319
|
assert env.max_retries == 9
|
|
320
320
|
assert env.strict_behavior_check is False
|
|
321
|
+
assert not hasattr(env, "max_" + "log_size")
|
|
321
322
|
|
|
322
323
|
|
|
323
324
|
def test_runner_environment_disables_worktree_by_default_and_accepts_truthy_opt_in():
|
|
@@ -414,10 +415,67 @@ def test_stop_on_failure_does_not_stop_on_retryable_pending(monkeypatch, tmp_pat
|
|
|
414
415
|
|
|
415
416
|
result = runners._run_pipeline(family, invocation, paths)
|
|
416
417
|
|
|
418
|
+
assert processed == ["F-001", "F-002"]
|
|
419
|
+
assert result.completed is False
|
|
420
|
+
assert result.stopped_reason == "pipeline_complete_with_non_terminal_history"
|
|
421
|
+
assert result.last_status == "completed"
|
|
422
|
+
assert result.details == ("F-001:pending", "F-002:completed")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def test_pipeline_complete_rejects_retryable_detail(monkeypatch, tmp_path):
|
|
426
|
+
from prizmkit_runtime import runners
|
|
427
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
428
|
+
|
|
429
|
+
paths = _make_paths(tmp_path)
|
|
430
|
+
family = family_for("feature", paths)
|
|
431
|
+
invocation = parse_invocation(family, "run", ())
|
|
432
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
433
|
+
queue = [
|
|
434
|
+
SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-001"}), data={"feature_id": "F-001"}, text=""),
|
|
435
|
+
SimpleNamespace(ok=True, stdout="PIPELINE_COMPLETE\n", data=None, text="PIPELINE_COMPLETE"),
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
monkeypatch.setenv("STOP_ON_FAILURE", "0")
|
|
439
|
+
monkeypatch.setattr(runners, "get_next", lambda *args, **kwargs: queue.pop(0))
|
|
440
|
+
monkeypatch.setattr(runners, "_process_item", lambda *args, **kwargs: "retryable")
|
|
441
|
+
|
|
442
|
+
result = runners._run_pipeline(family, invocation, paths)
|
|
443
|
+
|
|
444
|
+
assert result.completed is False
|
|
445
|
+
assert result.stopped_reason == "pipeline_complete_with_non_terminal_history"
|
|
446
|
+
assert result.last_status == "retryable"
|
|
447
|
+
assert result.details == ("F-001:retryable",)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def test_full_list_continues_from_completed_item_to_next_pending(monkeypatch, tmp_path):
|
|
451
|
+
from prizmkit_runtime import runners
|
|
452
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
453
|
+
|
|
454
|
+
paths = _make_paths(tmp_path)
|
|
455
|
+
family = family_for("feature", paths)
|
|
456
|
+
invocation = parse_invocation(family, "run", ())
|
|
457
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
458
|
+
queue = [
|
|
459
|
+
SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-001"}), data={"feature_id": "F-001"}, text=""),
|
|
460
|
+
SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-002"}), data={"feature_id": "F-002"}, text=""),
|
|
461
|
+
SimpleNamespace(ok=True, stdout="PIPELINE_COMPLETE\n", data=None, text="PIPELINE_COMPLETE"),
|
|
462
|
+
]
|
|
463
|
+
processed = []
|
|
464
|
+
|
|
465
|
+
monkeypatch.setattr(runners, "get_next", lambda *args, **kwargs: queue.pop(0))
|
|
466
|
+
|
|
467
|
+
def fake_process(_family, _invocation, item_id, _paths, *, initial_metadata):
|
|
468
|
+
processed.append(item_id)
|
|
469
|
+
return "completed"
|
|
470
|
+
|
|
471
|
+
monkeypatch.setattr(runners, "_process_item", fake_process)
|
|
472
|
+
|
|
473
|
+
result = runners._run_pipeline(family, invocation, paths)
|
|
474
|
+
|
|
417
475
|
assert processed == ["F-001", "F-002"]
|
|
418
476
|
assert result.completed is True
|
|
419
477
|
assert result.stopped_reason == "pipeline_complete"
|
|
420
|
-
assert result.details == ("F-001:
|
|
478
|
+
assert result.details == ("F-001:completed", "F-002:completed")
|
|
421
479
|
|
|
422
480
|
|
|
423
481
|
def test_stop_on_failure_still_stops_on_terminal_status(monkeypatch, tmp_path):
|
|
@@ -878,6 +936,172 @@ def test_default_no_worktree_preserves_main_worktree_branch_launch(monkeypatch,
|
|
|
878
936
|
assert branch_events == [("create", "dev/F-001-test"), ("merge", "dev/F-001-test")]
|
|
879
937
|
|
|
880
938
|
|
|
939
|
+
def test_has_branch_completion_evidence_requires_task_branch_ahead_of_base(tmp_path):
|
|
940
|
+
from prizmkit_runtime.runner_classification import has_branch_completion_evidence
|
|
941
|
+
|
|
942
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
943
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
944
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
945
|
+
artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
|
|
946
|
+
artifact_dir.mkdir(parents=True)
|
|
947
|
+
(artifact_dir / "completion-summary.json").write_text("{}", encoding="utf-8")
|
|
948
|
+
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
949
|
+
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
950
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
951
|
+
subprocess.run(["git", "checkout", "-b", "dev/F-001"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
952
|
+
(tmp_path / "done.txt").write_text("done", encoding="utf-8")
|
|
953
|
+
subprocess.run(["git", "add", "done.txt"], cwd=tmp_path, check=True)
|
|
954
|
+
subprocess.run(["git", "commit", "-m", "done"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
955
|
+
|
|
956
|
+
assert has_branch_completion_evidence(tmp_path, "main", "dev/F-001", artifact_dir) is True
|
|
957
|
+
|
|
958
|
+
subprocess.run(["git", "checkout", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
959
|
+
(tmp_path / "main.txt").write_text("main", encoding="utf-8")
|
|
960
|
+
subprocess.run(["git", "add", "main.txt"], cwd=tmp_path, check=True)
|
|
961
|
+
subprocess.run(["git", "commit", "-m", "advance-main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
962
|
+
subprocess.run(["git", "branch", "stale/F-001", "HEAD~1"], cwd=tmp_path, check=True)
|
|
963
|
+
|
|
964
|
+
assert has_branch_completion_evidence(tmp_path, "main", "stale/F-001", artifact_dir) is False
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def test_no_worktree_late_runtime_error_success_updates_and_merges(monkeypatch, tmp_path):
|
|
968
|
+
from prizmkit_runtime import runners
|
|
969
|
+
from prizmkit_runtime.runner_models import SessionClassification, family_for, parse_invocation
|
|
970
|
+
|
|
971
|
+
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-late-error")
|
|
972
|
+
paths = _make_paths(tmp_path)
|
|
973
|
+
family = family_for("feature", paths)
|
|
974
|
+
invocation = parse_invocation(family, "run", ())
|
|
975
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
976
|
+
observed = _install_runner_session_fakes(monkeypatch, runners, statuses=("infra_error",))
|
|
977
|
+
events = []
|
|
978
|
+
|
|
979
|
+
def fake_classify(*args, **kwargs):
|
|
980
|
+
return SessionClassification("success", reason="completed_checkpoint_with_late_runtime_error")
|
|
981
|
+
|
|
982
|
+
def fake_run_git_plan(project_root, plan):
|
|
983
|
+
events.append(plan.name)
|
|
984
|
+
return SimpleNamespace(ok=True, plan=plan, results=())
|
|
985
|
+
|
|
986
|
+
monkeypatch.setattr(runners, "classify_session", fake_classify)
|
|
987
|
+
monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
|
|
988
|
+
|
|
989
|
+
status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
|
|
990
|
+
|
|
991
|
+
assert status == "completed"
|
|
992
|
+
assert observed.updates[-1][0] == "success"
|
|
993
|
+
assert "branch_merge" in events
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def test_no_worktree_recovers_completed_pending_branch_before_new_session(monkeypatch, tmp_path):
|
|
997
|
+
from prizmkit_runtime import runners
|
|
998
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
999
|
+
|
|
1000
|
+
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-recover")
|
|
1001
|
+
paths = _make_paths(tmp_path)
|
|
1002
|
+
family = family_for("feature", paths)
|
|
1003
|
+
invocation = parse_invocation(family, "run", ())
|
|
1004
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
1005
|
+
session_dir = paths.feature_state_dir / "F-001" / "sessions" / "F-001-session-old"
|
|
1006
|
+
session_dir.mkdir(parents=True)
|
|
1007
|
+
artifact_dir = paths.project_root / ".prizmkit" / "specs" / "001-low"
|
|
1008
|
+
artifact_dir.mkdir(parents=True)
|
|
1009
|
+
(artifact_dir / "workflow-checkpoint.json").write_text(
|
|
1010
|
+
json.dumps({"steps": [{"id": "S01", "skill": "done", "status": "completed"}]}),
|
|
1011
|
+
encoding="utf-8",
|
|
1012
|
+
)
|
|
1013
|
+
updates = []
|
|
1014
|
+
events = []
|
|
1015
|
+
|
|
1016
|
+
monkeypatch.setattr(
|
|
1017
|
+
runners,
|
|
1018
|
+
"load_runtime_config",
|
|
1019
|
+
lambda paths: SimpleNamespace(ai_client=SimpleNamespace(command="fake-ai", platform="codebuddy"), model="", effort=None),
|
|
1020
|
+
)
|
|
1021
|
+
monkeypatch.setattr(runners, "current_branch", lambda _root: "main")
|
|
1022
|
+
monkeypatch.setattr(runners, "start_item", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not restart completed work")))
|
|
1023
|
+
monkeypatch.setattr(runners, "generate_prompt", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not launch prompt")))
|
|
1024
|
+
monkeypatch.setattr(runners, "has_branch_completion_evidence", lambda *args, **kwargs: True)
|
|
1025
|
+
monkeypatch.setattr(runners, "commit_final_bookkeeping", lambda *args, **kwargs: None)
|
|
1026
|
+
monkeypatch.setattr(runners, "commit_task_branch_changes", lambda *args, **kwargs: None)
|
|
1027
|
+
|
|
1028
|
+
def fake_update(family, invocation, item_id, session_status, session_id, project_root, **metadata):
|
|
1029
|
+
updates.append((session_status, session_id, metadata))
|
|
1030
|
+
return SimpleNamespace(ok=True, text="", data={"new_status": "completed"})
|
|
1031
|
+
|
|
1032
|
+
def fake_run_git_plan(project_root, plan):
|
|
1033
|
+
events.append(plan.name)
|
|
1034
|
+
return SimpleNamespace(ok=True, plan=plan, results=())
|
|
1035
|
+
|
|
1036
|
+
monkeypatch.setattr(runners, "update_item", fake_update)
|
|
1037
|
+
monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
|
|
1038
|
+
|
|
1039
|
+
status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={"title": "Low"})
|
|
1040
|
+
|
|
1041
|
+
assert status == "completed"
|
|
1042
|
+
assert updates == [("success", "F-001-session-old", {"active_dev_branch": "dev/F-001-recover", "base_branch": "main"})]
|
|
1043
|
+
assert "branch_merge" in events
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def test_worktree_recovers_completed_pending_branch_from_base_session_history(monkeypatch, tmp_path):
|
|
1047
|
+
from prizmkit_runtime import runners
|
|
1048
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
1049
|
+
|
|
1050
|
+
monkeypatch.setenv("USE_WORKTREE", "1")
|
|
1051
|
+
paths = _make_paths(tmp_path)
|
|
1052
|
+
family = family_for("feature", paths)
|
|
1053
|
+
invocation = parse_invocation(family, "run", ())
|
|
1054
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
1055
|
+
base_session_dir = paths.feature_state_dir / "F-001" / "sessions" / "F-001-session-old"
|
|
1056
|
+
base_session_dir.mkdir(parents=True)
|
|
1057
|
+
(base_session_dir / "session-status.json").write_text(
|
|
1058
|
+
json.dumps({"base_branch": "main", "active_dev_branch": "dev/F-001-worktree"}),
|
|
1059
|
+
encoding="utf-8",
|
|
1060
|
+
)
|
|
1061
|
+
updates = []
|
|
1062
|
+
cleanup_calls = []
|
|
1063
|
+
merge_calls = []
|
|
1064
|
+
|
|
1065
|
+
monkeypatch.setattr(
|
|
1066
|
+
runners,
|
|
1067
|
+
"load_runtime_config",
|
|
1068
|
+
lambda paths: SimpleNamespace(ai_client=SimpleNamespace(command="fake-ai", platform="codebuddy"), model="", effort=None),
|
|
1069
|
+
)
|
|
1070
|
+
monkeypatch.setattr(runners, "current_branch", lambda _root: "main")
|
|
1071
|
+
monkeypatch.setattr(runners, "ensure_linked_worktree", lambda runtime: SimpleNamespace(ok=True, runtime=runtime, reason=""), raising=False)
|
|
1072
|
+
monkeypatch.setattr(runners, "materialize_worktree_support_assets", lambda runtime: SimpleNamespace(ok=True, reason=""), raising=False)
|
|
1073
|
+
monkeypatch.setattr(runners, "has_completed_artifact_path", lambda artifact: True)
|
|
1074
|
+
monkeypatch.setattr(runners, "has_branch_completion_evidence", lambda *args, **kwargs: True)
|
|
1075
|
+
monkeypatch.setattr(runners, "start_item", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not restart completed work")))
|
|
1076
|
+
monkeypatch.setattr(runners, "generate_prompt", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not launch prompt")))
|
|
1077
|
+
monkeypatch.setattr(runners, "commit_final_bookkeeping", lambda *args, **kwargs: None)
|
|
1078
|
+
monkeypatch.setattr(runners, "commit_task_branch_changes", lambda *args, **kwargs: None)
|
|
1079
|
+
|
|
1080
|
+
def fake_update(family, invocation, item_id, session_status, session_id, project_root, **metadata):
|
|
1081
|
+
updates.append((session_status, session_id, project_root, metadata))
|
|
1082
|
+
return SimpleNamespace(ok=True, text="", data={"new_status": "completed"})
|
|
1083
|
+
|
|
1084
|
+
def fake_merge(runtime, *, auto_push=False):
|
|
1085
|
+
merge_calls.append((runtime, auto_push))
|
|
1086
|
+
return SimpleNamespace(ok=True, reason="", runtime=runtime, results=())
|
|
1087
|
+
|
|
1088
|
+
def fake_cleanup(runtime, *, delete_branch=True):
|
|
1089
|
+
cleanup_calls.append((runtime, delete_branch))
|
|
1090
|
+
return SimpleNamespace(ok=True, reason="")
|
|
1091
|
+
|
|
1092
|
+
monkeypatch.setattr(runners, "update_item", fake_update)
|
|
1093
|
+
monkeypatch.setattr(runners, "merge_linked_worktree", fake_merge, raising=False)
|
|
1094
|
+
monkeypatch.setattr(runners, "guarded_worktree_remove", fake_cleanup, raising=False)
|
|
1095
|
+
|
|
1096
|
+
status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={"title": "Low"})
|
|
1097
|
+
|
|
1098
|
+
assert status == "completed"
|
|
1099
|
+
assert updates[0][0] == "success"
|
|
1100
|
+
assert updates[0][1] == "F-001-session-old"
|
|
1101
|
+
assert updates[0][2] != paths.project_root
|
|
1102
|
+
assert merge_calls and cleanup_calls[-1][1] is True
|
|
1103
|
+
|
|
1104
|
+
|
|
881
1105
|
def test_default_branch_names_keep_timestamp_for_each_runner_family(monkeypatch, tmp_path):
|
|
882
1106
|
from prizmkit_runtime import runners
|
|
883
1107
|
from prizmkit_runtime.runner_models import family_for
|
|
@@ -1108,6 +1332,133 @@ def test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outco
|
|
|
1108
1332
|
assert observed.cleanup_calls[0][1] is True
|
|
1109
1333
|
|
|
1110
1334
|
|
|
1335
|
+
def test_classification_accepts_late_infra_error_with_completed_checkpoint_and_commit(tmp_path):
|
|
1336
|
+
from prizmkit_runtime.runner_classification import classify_session
|
|
1337
|
+
from prizmkit_runtime.runner_models import PromptGenerationResult
|
|
1338
|
+
from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
|
|
1339
|
+
from prizmkit_runtime.status import ProgressSummary
|
|
1340
|
+
|
|
1341
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1342
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
1343
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
1344
|
+
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
1345
|
+
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
1346
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1347
|
+
base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
1348
|
+
(tmp_path / "done.txt").write_text("done", encoding="utf-8")
|
|
1349
|
+
subprocess.run(["git", "add", "done.txt"], cwd=tmp_path, check=True)
|
|
1350
|
+
subprocess.run(["git", "commit", "-m", "done"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1351
|
+
artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
|
|
1352
|
+
artifact_dir.mkdir(parents=True)
|
|
1353
|
+
checkpoint = artifact_dir / "workflow-checkpoint.json"
|
|
1354
|
+
checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "status": "completed"}]}), encoding="utf-8")
|
|
1355
|
+
log = tmp_path / "session.log"
|
|
1356
|
+
log.write_text("model quota exceeded; API error after commit", encoding="utf-8")
|
|
1357
|
+
raw = AISessionResult(
|
|
1358
|
+
command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
|
|
1359
|
+
exit_code=1,
|
|
1360
|
+
timed_out=False,
|
|
1361
|
+
termination_reason=None,
|
|
1362
|
+
stale_marker=None,
|
|
1363
|
+
fatal_error_code="api_error",
|
|
1364
|
+
progress_summary=ProgressSummary(terminal_result_text="quota exceeded"),
|
|
1365
|
+
session_log=log,
|
|
1366
|
+
backup_log=tmp_path / "backup.log",
|
|
1367
|
+
log_recovery=None,
|
|
1368
|
+
started_epoch=0,
|
|
1369
|
+
ended_epoch=1,
|
|
1370
|
+
)
|
|
1371
|
+
prompt = PromptGenerationResult(output_path=tmp_path / "prompt.md", checkpoint_path=checkpoint, artifact_path=artifact_dir)
|
|
1372
|
+
|
|
1373
|
+
classification = classify_session(raw, project_root=tmp_path, base_head=base_head, prompt=prompt)
|
|
1374
|
+
|
|
1375
|
+
assert classification.session_status == "success"
|
|
1376
|
+
assert classification.reason == "completed_checkpoint_with_late_runtime_error"
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def test_classification_rejects_late_infra_when_head_diff_has_no_commit_range(tmp_path):
|
|
1380
|
+
from prizmkit_runtime.runner_classification import classify_session
|
|
1381
|
+
from prizmkit_runtime.runner_models import PromptGenerationResult
|
|
1382
|
+
from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
|
|
1383
|
+
from prizmkit_runtime.status import ProgressSummary
|
|
1384
|
+
|
|
1385
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1386
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
1387
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
1388
|
+
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
1389
|
+
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
1390
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1391
|
+
subprocess.run(["git", "checkout", "-b", "side"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1392
|
+
(tmp_path / "side.txt").write_text("side", encoding="utf-8")
|
|
1393
|
+
subprocess.run(["git", "add", "side.txt"], cwd=tmp_path, check=True)
|
|
1394
|
+
subprocess.run(["git", "commit", "-m", "side"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1395
|
+
side_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
1396
|
+
subprocess.run(["git", "checkout", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1397
|
+
(tmp_path / "main.txt").write_text("main", encoding="utf-8")
|
|
1398
|
+
subprocess.run(["git", "add", "main.txt"], cwd=tmp_path, check=True)
|
|
1399
|
+
subprocess.run(["git", "commit", "-m", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1400
|
+
subprocess.run(["git", "checkout", "side"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1401
|
+
artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
|
|
1402
|
+
artifact_dir.mkdir(parents=True)
|
|
1403
|
+
(artifact_dir / "completion-summary.json").write_text("{}", encoding="utf-8")
|
|
1404
|
+
log = tmp_path / "session.log"
|
|
1405
|
+
log.write_text("quota exceeded", encoding="utf-8")
|
|
1406
|
+
raw = AISessionResult(
|
|
1407
|
+
command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
|
|
1408
|
+
exit_code=1,
|
|
1409
|
+
timed_out=False,
|
|
1410
|
+
termination_reason=None,
|
|
1411
|
+
stale_marker=None,
|
|
1412
|
+
fatal_error_code="api_error",
|
|
1413
|
+
progress_summary=ProgressSummary(terminal_result_text="quota exceeded"),
|
|
1414
|
+
session_log=log,
|
|
1415
|
+
backup_log=tmp_path / "backup.log",
|
|
1416
|
+
log_recovery=None,
|
|
1417
|
+
started_epoch=0,
|
|
1418
|
+
ended_epoch=1,
|
|
1419
|
+
)
|
|
1420
|
+
prompt = PromptGenerationResult(output_path=tmp_path / "prompt.md", artifact_path=artifact_dir)
|
|
1421
|
+
|
|
1422
|
+
classification = classify_session(raw, project_root=tmp_path, base_head=side_head, prompt=prompt)
|
|
1423
|
+
|
|
1424
|
+
assert classification.session_status == "infra_error"
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
def test_classification_keeps_pre_completion_provider_error_as_infra_error(tmp_path):
|
|
1428
|
+
from prizmkit_runtime.runner_classification import classify_session
|
|
1429
|
+
from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
|
|
1430
|
+
from prizmkit_runtime.status import ProgressSummary
|
|
1431
|
+
|
|
1432
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1433
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
1434
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
1435
|
+
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
1436
|
+
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
1437
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1438
|
+
base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
1439
|
+
log = tmp_path / "session.log"
|
|
1440
|
+
log.write_text("authentication failed", encoding="utf-8")
|
|
1441
|
+
raw = AISessionResult(
|
|
1442
|
+
command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
|
|
1443
|
+
exit_code=1,
|
|
1444
|
+
timed_out=False,
|
|
1445
|
+
termination_reason=None,
|
|
1446
|
+
stale_marker=None,
|
|
1447
|
+
fatal_error_code="authentication_error",
|
|
1448
|
+
progress_summary=ProgressSummary(terminal_result_text="authentication failed"),
|
|
1449
|
+
session_log=log,
|
|
1450
|
+
backup_log=tmp_path / "backup.log",
|
|
1451
|
+
log_recovery=None,
|
|
1452
|
+
started_epoch=0,
|
|
1453
|
+
ended_epoch=1,
|
|
1454
|
+
)
|
|
1455
|
+
|
|
1456
|
+
classification = classify_session(raw, project_root=tmp_path, base_head=base_head)
|
|
1457
|
+
|
|
1458
|
+
assert classification.session_status == "infra_error"
|
|
1459
|
+
assert classification.reason == "ai_runtime_or_provider_error"
|
|
1460
|
+
|
|
1461
|
+
|
|
1111
1462
|
def test_classification_accepts_terminal_success_with_completed_checkpoint_without_git_delta(tmp_path):
|
|
1112
1463
|
from prizmkit_runtime.runner_classification import classify_session
|
|
1113
1464
|
from prizmkit_runtime.runner_models import PromptGenerationResult
|