prizmkit 1.1.105 → 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/gitops.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +83 -12
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +93 -6
- 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 +421 -61
- 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 +560 -1
- package/bundled/dev-pipeline/tests/test_unified_cli.py +208 -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
|
@@ -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):
|
|
@@ -847,12 +905,24 @@ def test_default_no_worktree_preserves_main_worktree_branch_launch(monkeypatch,
|
|
|
847
905
|
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
848
906
|
observed = _install_runner_session_fakes(monkeypatch, runners)
|
|
849
907
|
plan_names = []
|
|
908
|
+
branch_events = []
|
|
909
|
+
lifecycle_order = []
|
|
850
910
|
|
|
851
911
|
def fake_run_git_plan(project_root, plan):
|
|
852
912
|
plan_names.append(plan.name)
|
|
913
|
+
if plan.name == "branch_create":
|
|
914
|
+
lifecycle_order.append("branch_create")
|
|
915
|
+
branch_events.append(("create", plan.commands[1].args[2]))
|
|
916
|
+
if plan.name == "branch_merge":
|
|
917
|
+
branch_events.append(("merge", plan.commands[5].args[2]))
|
|
853
918
|
return SimpleNamespace(ok=True, plan=plan, results=())
|
|
854
919
|
|
|
920
|
+
def fake_start_item(*args, **kwargs):
|
|
921
|
+
lifecycle_order.append("start_item")
|
|
922
|
+
return SimpleNamespace(ok=True, text="", data={"new_status": "in_progress"})
|
|
923
|
+
|
|
855
924
|
monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
|
|
925
|
+
monkeypatch.setattr(runners, "start_item", fake_start_item)
|
|
856
926
|
monkeypatch.setattr(runners, "ensure_linked_worktree", lambda runtime: (_ for _ in ()).throw(AssertionError("worktree setup should not run")), raising=False)
|
|
857
927
|
|
|
858
928
|
status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
|
|
@@ -862,6 +932,228 @@ def test_default_no_worktree_preserves_main_worktree_branch_launch(monkeypatch,
|
|
|
862
932
|
assert observed.prompt_calls[0].get("execution_root") == paths.project_root
|
|
863
933
|
assert "branch_create" in plan_names
|
|
864
934
|
assert "branch_merge" in plan_names
|
|
935
|
+
assert lifecycle_order[:2] == ["branch_create", "start_item"]
|
|
936
|
+
assert branch_events == [("create", "dev/F-001-test"), ("merge", "dev/F-001-test")]
|
|
937
|
+
|
|
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
|
+
|
|
1105
|
+
def test_default_branch_names_keep_timestamp_for_each_runner_family(monkeypatch, tmp_path):
|
|
1106
|
+
from prizmkit_runtime import runners
|
|
1107
|
+
from prizmkit_runtime.runner_models import family_for
|
|
1108
|
+
|
|
1109
|
+
monkeypatch.setattr(runners.time, "strftime", lambda fmt: "20260707010203")
|
|
1110
|
+
paths = _make_paths(tmp_path)
|
|
1111
|
+
|
|
1112
|
+
assert runners._default_branch_name(family_for("feature", paths), "F-001") == "dev/F-001-20260707010203"
|
|
1113
|
+
assert runners._default_branch_name(family_for("bugfix", paths), "B-001") == "bugfix/B-001-20260707010203"
|
|
1114
|
+
assert runners._default_branch_name(family_for("refactor", paths), "R-001") == "refactor/R-001-20260707010203"
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
@pytest.mark.parametrize(
|
|
1118
|
+
("kind", "item_id", "branch"),
|
|
1119
|
+
[
|
|
1120
|
+
("feature", "F-001", "dev/F-001-20260707010203"),
|
|
1121
|
+
("bugfix", "B-001", "bugfix/B-001-20260707010203"),
|
|
1122
|
+
("refactor", "R-001", "refactor/R-001-20260707010203"),
|
|
1123
|
+
],
|
|
1124
|
+
)
|
|
1125
|
+
def test_no_worktree_failure_stays_on_task_branch_without_merge_for_all_families(monkeypatch, tmp_path, kind, item_id, branch):
|
|
1126
|
+
from prizmkit_runtime import runners
|
|
1127
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
1128
|
+
|
|
1129
|
+
monkeypatch.setenv("DEV_BRANCH", branch)
|
|
1130
|
+
paths = _make_paths(tmp_path / kind)
|
|
1131
|
+
family = family_for(kind, paths)
|
|
1132
|
+
invocation = parse_invocation(family, "run", ())
|
|
1133
|
+
plan_path = {"feature": paths.feature_plan, "bugfix": paths.bugfix_plan, "refactor": paths.refactor_plan}[kind]
|
|
1134
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": plan_path})
|
|
1135
|
+
_install_runner_session_fakes(monkeypatch, runners, statuses=("crashed",), update_status="pending")
|
|
1136
|
+
events = []
|
|
1137
|
+
final_commits = []
|
|
1138
|
+
wip_commits = []
|
|
1139
|
+
|
|
1140
|
+
def fake_run_git_plan(project_root, plan):
|
|
1141
|
+
events.append(plan.name)
|
|
1142
|
+
return SimpleNamespace(ok=True, plan=plan, results=())
|
|
1143
|
+
|
|
1144
|
+
monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
|
|
1145
|
+
monkeypatch.setattr(runners, "branch_ensure_return", lambda project_root, base, task_branch: events.append(("return", base, task_branch)))
|
|
1146
|
+
monkeypatch.setattr(runners, "commit_final_bookkeeping", lambda project_root, family, item_id, status: final_commits.append((project_root, item_id, status)))
|
|
1147
|
+
monkeypatch.setattr(runners, "commit_wip_changes", lambda project_root, family, item_id, status: wip_commits.append((project_root, item_id, status)))
|
|
1148
|
+
|
|
1149
|
+
status = runners._process_item(family, invocation, item_id, paths, initial_metadata={})
|
|
1150
|
+
|
|
1151
|
+
assert status == "pending"
|
|
1152
|
+
assert events[0] == "branch_create"
|
|
1153
|
+
assert "branch_merge" not in events
|
|
1154
|
+
assert events[-1] == ("return", "main", branch)
|
|
1155
|
+
assert final_commits == [(paths.project_root, item_id, "pending")]
|
|
1156
|
+
assert wip_commits == [(paths.project_root, item_id, "pending")]
|
|
865
1157
|
|
|
866
1158
|
|
|
867
1159
|
def test_no_worktree_runner_ignores_recovery_side_effect_branch(monkeypatch, tmp_path):
|
|
@@ -956,6 +1248,63 @@ def test_no_worktree_interrupt_saves_wip_and_returns_to_base(monkeypatch, tmp_pa
|
|
|
956
1248
|
assert ensure_calls == [("main", "dev/F-001-interrupt")]
|
|
957
1249
|
|
|
958
1250
|
|
|
1251
|
+
def test_bookkeeping_commits_only_runner_owned_paths(tmp_path):
|
|
1252
|
+
from prizmkit_runtime.runner_bookkeeping import commit_final_bookkeeping
|
|
1253
|
+
from prizmkit_runtime.runner_models import RunnerFamily
|
|
1254
|
+
|
|
1255
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1256
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
1257
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
1258
|
+
plans = tmp_path / ".prizmkit" / "plans"
|
|
1259
|
+
state = tmp_path / ".prizmkit" / "state" / "features"
|
|
1260
|
+
item_state = state / "F-001"
|
|
1261
|
+
plans.mkdir(parents=True)
|
|
1262
|
+
item_state.mkdir(parents=True)
|
|
1263
|
+
plan = plans / "feature-list.json"
|
|
1264
|
+
plan.write_text('{"features": []}', encoding="utf-8")
|
|
1265
|
+
(item_state / "status.json").write_text("{}", encoding="utf-8")
|
|
1266
|
+
source = tmp_path / "src.txt"
|
|
1267
|
+
source.write_text("base", encoding="utf-8")
|
|
1268
|
+
subprocess.run(["git", "add", ".prizmkit/plans/feature-list.json", ".prizmkit/state/features/F-001/status.json", "src.txt"], cwd=tmp_path, check=True)
|
|
1269
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1270
|
+
plan.write_text('{"features": [{"id": "F-001", "status": "completed"}]}', encoding="utf-8")
|
|
1271
|
+
(item_state / "status.json").write_text('{"last_session_id": "s1"}', encoding="utf-8")
|
|
1272
|
+
source.write_text("dirty source change", encoding="utf-8")
|
|
1273
|
+
family = RunnerFamily(
|
|
1274
|
+
kind="feature",
|
|
1275
|
+
id_prefix="F-",
|
|
1276
|
+
list_key="features",
|
|
1277
|
+
list_arg="--feature-list",
|
|
1278
|
+
item_id_arg="--feature-id",
|
|
1279
|
+
state_dir=state,
|
|
1280
|
+
plan_path=plan,
|
|
1281
|
+
updater_script=tmp_path / "update-feature-status.py",
|
|
1282
|
+
prompt_script=tmp_path / "generate-bootstrap-prompt.py",
|
|
1283
|
+
branch_prefix="dev",
|
|
1284
|
+
artifact_root_name="specs",
|
|
1285
|
+
)
|
|
1286
|
+
|
|
1287
|
+
result = commit_final_bookkeeping(tmp_path, family, "F-001", "completed")
|
|
1288
|
+
committed_files = subprocess.run(
|
|
1289
|
+
["git", "show", "--name-only", "--format=", "HEAD"],
|
|
1290
|
+
cwd=tmp_path,
|
|
1291
|
+
check=True,
|
|
1292
|
+
stdout=subprocess.PIPE,
|
|
1293
|
+
text=True,
|
|
1294
|
+
).stdout.splitlines()
|
|
1295
|
+
dirty_files = subprocess.run(
|
|
1296
|
+
["git", "status", "--porcelain", "--", "src.txt"],
|
|
1297
|
+
cwd=tmp_path,
|
|
1298
|
+
check=True,
|
|
1299
|
+
stdout=subprocess.PIPE,
|
|
1300
|
+
text=True,
|
|
1301
|
+
).stdout
|
|
1302
|
+
|
|
1303
|
+
assert result.committed is True
|
|
1304
|
+
assert committed_files == [".prizmkit/plans/feature-list.json", ".prizmkit/state/features/F-001/status.json"]
|
|
1305
|
+
assert dirty_files.startswith(" M src.txt")
|
|
1306
|
+
|
|
1307
|
+
|
|
959
1308
|
def test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outcome(monkeypatch, tmp_path):
|
|
960
1309
|
from prizmkit_runtime import runners
|
|
961
1310
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
@@ -983,6 +1332,179 @@ def test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outco
|
|
|
983
1332
|
assert observed.cleanup_calls[0][1] is True
|
|
984
1333
|
|
|
985
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
|
+
|
|
1462
|
+
def test_classification_accepts_terminal_success_with_completed_checkpoint_without_git_delta(tmp_path):
|
|
1463
|
+
from prizmkit_runtime.runner_classification import classify_session
|
|
1464
|
+
from prizmkit_runtime.runner_models import PromptGenerationResult
|
|
1465
|
+
from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
|
|
1466
|
+
from prizmkit_runtime.status import ProgressSummary
|
|
1467
|
+
|
|
1468
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1469
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
1470
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
1471
|
+
(tmp_path / ".gitignore").write_text(".prizmkit/*\n", encoding="utf-8")
|
|
1472
|
+
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
1473
|
+
subprocess.run(["git", "add", ".gitignore", "base.txt"], cwd=tmp_path, check=True)
|
|
1474
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1475
|
+
base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
1476
|
+
artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
|
|
1477
|
+
artifact_dir.mkdir(parents=True)
|
|
1478
|
+
checkpoint = artifact_dir / "workflow-checkpoint.json"
|
|
1479
|
+
checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "status": "completed"}]}), encoding="utf-8")
|
|
1480
|
+
(artifact_dir / "completion-summary.json").write_text("{}", encoding="utf-8")
|
|
1481
|
+
log = tmp_path / "session.log"
|
|
1482
|
+
log.write_text("terminal success", encoding="utf-8")
|
|
1483
|
+
raw = AISessionResult(
|
|
1484
|
+
command=AISessionCommand(("fake",), None, "claude", "stdin"),
|
|
1485
|
+
exit_code=0,
|
|
1486
|
+
timed_out=False,
|
|
1487
|
+
termination_reason=None,
|
|
1488
|
+
stale_marker=None,
|
|
1489
|
+
fatal_error_code="",
|
|
1490
|
+
progress_summary=ProgressSummary(result_subtype="success", terminal_result_text="Completed; working tree clean"),
|
|
1491
|
+
session_log=log,
|
|
1492
|
+
backup_log=tmp_path / "backup.log",
|
|
1493
|
+
log_recovery=None,
|
|
1494
|
+
started_epoch=0,
|
|
1495
|
+
ended_epoch=1,
|
|
1496
|
+
)
|
|
1497
|
+
prompt = PromptGenerationResult(
|
|
1498
|
+
output_path=tmp_path / "prompt.md",
|
|
1499
|
+
checkpoint_path=checkpoint,
|
|
1500
|
+
artifact_path=artifact_dir,
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
classification = classify_session(raw, project_root=tmp_path, base_head=base_head, prompt=prompt)
|
|
1504
|
+
assert classification.session_status == "success"
|
|
1505
|
+
assert classification.reason == "terminal_success_with_completed_checkpoint"
|
|
1506
|
+
|
|
1507
|
+
|
|
986
1508
|
def test_classification_preserves_success_before_context_overflow(tmp_path):
|
|
987
1509
|
from prizmkit_runtime.runner_classification import classify_session
|
|
988
1510
|
from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
|
|
@@ -1021,6 +1543,43 @@ def test_classification_preserves_success_before_context_overflow(tmp_path):
|
|
|
1021
1543
|
assert classification.reason == "commit_since_base"
|
|
1022
1544
|
|
|
1023
1545
|
|
|
1546
|
+
def test_classification_crash_with_commit_since_base_does_not_become_success(tmp_path):
|
|
1547
|
+
from prizmkit_runtime.runner_classification import classify_session
|
|
1548
|
+
from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
|
|
1549
|
+
from prizmkit_runtime.status import ProgressSummary
|
|
1550
|
+
|
|
1551
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1552
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
1553
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
1554
|
+
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
1555
|
+
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
1556
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1557
|
+
base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
1558
|
+
(tmp_path / "done.txt").write_text("done", encoding="utf-8")
|
|
1559
|
+
subprocess.run(["git", "add", "done.txt"], cwd=tmp_path, check=True)
|
|
1560
|
+
subprocess.run(["git", "commit", "-m", "done"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1561
|
+
log = tmp_path / "session.log"
|
|
1562
|
+
log.write_text("hard failure", encoding="utf-8")
|
|
1563
|
+
raw = AISessionResult(
|
|
1564
|
+
command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
|
|
1565
|
+
exit_code=1,
|
|
1566
|
+
timed_out=False,
|
|
1567
|
+
termination_reason=None,
|
|
1568
|
+
stale_marker=None,
|
|
1569
|
+
fatal_error_code="",
|
|
1570
|
+
progress_summary=ProgressSummary(terminal_result_text="hard failure"),
|
|
1571
|
+
session_log=log,
|
|
1572
|
+
backup_log=tmp_path / "backup.log",
|
|
1573
|
+
log_recovery=None,
|
|
1574
|
+
started_epoch=0,
|
|
1575
|
+
ended_epoch=1,
|
|
1576
|
+
)
|
|
1577
|
+
|
|
1578
|
+
classification = classify_session(raw, project_root=tmp_path, base_head=base_head)
|
|
1579
|
+
assert classification.session_status == "crashed"
|
|
1580
|
+
assert classification.reason == "exit_code=1"
|
|
1581
|
+
|
|
1582
|
+
|
|
1024
1583
|
def test_classification_stalls_after_repeated_no_progress_context_overflow(tmp_path):
|
|
1025
1584
|
from prizmkit_runtime.runner_classification import classify_session, progress_fingerprint
|
|
1026
1585
|
from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
|