prizmkit 1.1.152 → 1.1.154
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/README.md +100 -87
- package/bundled/dev-pipeline/assets/skill-subagent-integration.md +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +14 -0
- package/bundled/dev-pipeline/prizmkit_runtime/cli.py +192 -110
- package/bundled/dev-pipeline/prizmkit_runtime/commands.py +146 -111
- package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +3 -5
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/paths.py +0 -3
- package/bundled/dev-pipeline/prizmkit_runtime/reset.py +122 -15
- package/bundled/dev-pipeline/prizmkit_runtime/reset_preserve.py +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +19 -52
- package/bundled/dev-pipeline/prizmkit_runtime/status.py +106 -1
- package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +2 -2
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +10 -12
- package/bundled/dev-pipeline/scripts/update-bug-status.py +196 -67
- package/bundled/dev-pipeline/scripts/update-checkpoint.py +16 -3
- package/bundled/dev-pipeline/scripts/update-feature-status.py +45 -117
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +45 -119
- package/bundled/dev-pipeline/scripts/utils.py +119 -0
- package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +3 -2
- package/bundled/dev-pipeline/tests/test_auto_skip.py +111 -32
- package/bundled/dev-pipeline/tests/test_checkpoint_state.py +148 -12
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +0 -19
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +113 -188
- package/bundled/dev-pipeline/tests/test_recovery_workflow.py +211 -0
- package/bundled/dev-pipeline/tests/test_reset_modes.py +935 -0
- package/bundled/dev-pipeline/tests/test_reset_preserve.py +5 -5
- package/bundled/dev-pipeline/tests/test_unified_cli.py +544 -181
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/bug-planner/references/schema-validation.md +1 -1
- package/bundled/skills/bug-planner/scripts/validate-bug-list.py +1 -1
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -13
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +22 -14
- package/bundled/skills/recovery-workflow/SKILL.md +7 -5
- package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
- package/bundled/skills/recovery-workflow/references/detection.md +3 -3
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +1 -1
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +21 -13
- package/bundled/templates/project-memory-template.md +19 -11
- package/package.json +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +0 -228
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +0 -767
|
@@ -54,23 +54,16 @@ def _load_progress_parser_module():
|
|
|
54
54
|
return module
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def
|
|
57
|
+
def test_cli_help_exposes_only_operation_first_public_namespaces():
|
|
58
58
|
result = run_cli("--help")
|
|
59
59
|
|
|
60
60
|
assert result.returncode == 0
|
|
61
61
|
assert "canonical Python runtime CLI" in result.stdout
|
|
62
|
-
assert "
|
|
63
|
-
for group in [
|
|
64
|
-
"feature",
|
|
65
|
-
"bugfix",
|
|
66
|
-
"refactor",
|
|
67
|
-
"recovery",
|
|
68
|
-
"reset",
|
|
69
|
-
"daemon",
|
|
70
|
-
"status",
|
|
71
|
-
"diagnostics",
|
|
72
|
-
]:
|
|
62
|
+
assert "operation-first" in result.stdout
|
|
63
|
+
for group in ["run", "status", "reset", "daemon", "diagnostics", "helper"]:
|
|
73
64
|
assert group in result.stdout
|
|
65
|
+
for removed in ["feature", "bugfix", "refactor", "recovery"]:
|
|
66
|
+
assert removed not in result.stdout.lower()
|
|
74
67
|
assert "Traceback" not in result.stderr
|
|
75
68
|
|
|
76
69
|
|
|
@@ -79,7 +72,7 @@ def test_cli_help_preserves_runtime_identity_at_narrow_terminal_width():
|
|
|
79
72
|
|
|
80
73
|
assert result.returncode == 0
|
|
81
74
|
assert "canonical Python runtime CLI" in result.stdout
|
|
82
|
-
assert "
|
|
75
|
+
assert "operation-first" in result.stdout
|
|
83
76
|
|
|
84
77
|
|
|
85
78
|
def test_module_entrypoint_help_exposes_same_cli():
|
|
@@ -99,6 +92,45 @@ def test_module_entrypoint_help_exposes_same_cli():
|
|
|
99
92
|
assert "diagnostics" in result.stdout
|
|
100
93
|
|
|
101
94
|
|
|
95
|
+
@pytest.mark.parametrize(
|
|
96
|
+
"args",
|
|
97
|
+
[
|
|
98
|
+
("--help",),
|
|
99
|
+
("run", "--help"),
|
|
100
|
+
("run", "feature", "--help"),
|
|
101
|
+
("run", "bugfix", "--help"),
|
|
102
|
+
("run", "refactor", "--help"),
|
|
103
|
+
("status", "--help"),
|
|
104
|
+
("status", "feature", "--help"),
|
|
105
|
+
("status", "all", "--help"),
|
|
106
|
+
("reset", "--help"),
|
|
107
|
+
("reset", "feature", "--help"),
|
|
108
|
+
("daemon", "--help"),
|
|
109
|
+
("daemon", "feature", "--help"),
|
|
110
|
+
("daemon", "feature", "start", "--help"),
|
|
111
|
+
("daemon", "bugfix", "stop", "--help"),
|
|
112
|
+
("daemon", "refactor", "status", "--help"),
|
|
113
|
+
("daemon", "feature", "logs", "--help"),
|
|
114
|
+
("daemon", "feature", "restart", "--help"),
|
|
115
|
+
("diagnostics", "--help"),
|
|
116
|
+
("diagnostics", "ai-cli", "--help"),
|
|
117
|
+
("helper", "--help"),
|
|
118
|
+
],
|
|
119
|
+
)
|
|
120
|
+
def test_help_at_every_namespace_depth_is_parser_owned_and_never_dispatches(monkeypatch, args):
|
|
121
|
+
from prizmkit_runtime import cli as runtime_cli
|
|
122
|
+
|
|
123
|
+
dispatched = []
|
|
124
|
+
monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
|
|
125
|
+
monkeypatch.setattr(runtime_cli, "_dispatch", lambda parsed: dispatched.append(parsed))
|
|
126
|
+
|
|
127
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
128
|
+
runtime_cli.main(list(args))
|
|
129
|
+
|
|
130
|
+
assert exc_info.value.code == 0
|
|
131
|
+
assert dispatched == []
|
|
132
|
+
|
|
133
|
+
|
|
102
134
|
def test_invalid_arguments_report_usage_without_traceback():
|
|
103
135
|
result = run_cli("unknown-command")
|
|
104
136
|
|
|
@@ -108,6 +140,80 @@ def test_invalid_arguments_report_usage_without_traceback():
|
|
|
108
140
|
assert "Traceback" not in result.stderr
|
|
109
141
|
|
|
110
142
|
|
|
143
|
+
@pytest.mark.parametrize(
|
|
144
|
+
"args",
|
|
145
|
+
[
|
|
146
|
+
("recovery",),
|
|
147
|
+
("recovery", "detect"),
|
|
148
|
+
("recovery", "run"),
|
|
149
|
+
("recovery", "describe"),
|
|
150
|
+
("recovery", "run", "--dry-run"),
|
|
151
|
+
],
|
|
152
|
+
)
|
|
153
|
+
def test_former_recovery_invocations_are_unknown_commands(args):
|
|
154
|
+
result = run_cli(*args)
|
|
155
|
+
|
|
156
|
+
assert result.returncode == 2
|
|
157
|
+
assert "invalid choice: 'recovery'" in result.stderr
|
|
158
|
+
assert "Traceback" not in result.stderr
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_former_recovery_invocation_never_reaches_dispatch(monkeypatch):
|
|
162
|
+
from prizmkit_runtime import cli as runtime_cli
|
|
163
|
+
|
|
164
|
+
dispatched = []
|
|
165
|
+
monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
|
|
166
|
+
monkeypatch.setattr(runtime_cli, "_dispatch", lambda args: dispatched.append(args))
|
|
167
|
+
|
|
168
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
169
|
+
runtime_cli.main(["recovery", "run", "--dry-run"])
|
|
170
|
+
|
|
171
|
+
assert exc_info.value.code == 2
|
|
172
|
+
assert dispatched == []
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.parametrize(
|
|
176
|
+
"args",
|
|
177
|
+
[
|
|
178
|
+
("feature", "run"),
|
|
179
|
+
("feature", "status"),
|
|
180
|
+
("feature", "reset"),
|
|
181
|
+
("feature", "unskip"),
|
|
182
|
+
("feature", "test-cli"),
|
|
183
|
+
("feature", "describe"),
|
|
184
|
+
("bugfix", "run"),
|
|
185
|
+
("bugfix", "status"),
|
|
186
|
+
("bugfix", "reset"),
|
|
187
|
+
("bugfix", "unskip"),
|
|
188
|
+
("bugfix", "describe"),
|
|
189
|
+
("refactor", "run"),
|
|
190
|
+
("refactor", "status"),
|
|
191
|
+
("refactor", "reset"),
|
|
192
|
+
("refactor", "unskip"),
|
|
193
|
+
("refactor", "describe"),
|
|
194
|
+
("reset", "describe"),
|
|
195
|
+
("daemon", "feature", "describe"),
|
|
196
|
+
("diagnostics", "describe"),
|
|
197
|
+
("status", "all", "custom-plan.json"),
|
|
198
|
+
("run", "feature", "--unknown-option"),
|
|
199
|
+
("run", "feature", "F-001", "plan.json", "extra"),
|
|
200
|
+
("daemon", "feature", "status", "extra"),
|
|
201
|
+
],
|
|
202
|
+
)
|
|
203
|
+
def test_removed_or_invalid_forms_fail_in_parser_without_runtime_dispatch(monkeypatch, args):
|
|
204
|
+
from prizmkit_runtime import cli as runtime_cli
|
|
205
|
+
|
|
206
|
+
dispatched = []
|
|
207
|
+
monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
|
|
208
|
+
monkeypatch.setattr(runtime_cli, "_dispatch", lambda parsed: dispatched.append(parsed))
|
|
209
|
+
|
|
210
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
211
|
+
runtime_cli.main(list(args))
|
|
212
|
+
|
|
213
|
+
assert exc_info.value.code == 2
|
|
214
|
+
assert dispatched == []
|
|
215
|
+
|
|
216
|
+
|
|
111
217
|
def test_python_version_guard_mentions_required_version():
|
|
112
218
|
from prizmkit_runtime.compat import UnsupportedPythonVersion, ensure_supported_python
|
|
113
219
|
|
|
@@ -136,6 +242,7 @@ def test_runtime_paths_support_framework_source_layout():
|
|
|
136
242
|
assert paths.refactor_plan == paths.plans_dir / "refactor-list.json"
|
|
137
243
|
assert paths.feature_state_dir == paths.state_dir / "features"
|
|
138
244
|
assert paths.specs_dir == paths.prizmkit_dir / "specs"
|
|
245
|
+
assert not hasattr(paths, "recovery_state_dir")
|
|
139
246
|
assert all(isinstance(value, Path) for value in paths.path_values())
|
|
140
247
|
|
|
141
248
|
|
|
@@ -154,6 +261,22 @@ def test_runtime_paths_support_installed_layout(tmp_path):
|
|
|
154
261
|
assert paths.logs_dir == paths.state_dir / "logs"
|
|
155
262
|
|
|
156
263
|
|
|
264
|
+
def test_runtime_diagnostics_leave_historical_recovery_state_untouched(tmp_path):
|
|
265
|
+
project = tmp_path / "project"
|
|
266
|
+
historical = project / ".prizmkit" / "state" / "recovery" / "historical-session"
|
|
267
|
+
historical.mkdir(parents=True)
|
|
268
|
+
marker = historical / "marker.json"
|
|
269
|
+
marker.write_bytes(b'{"preserve": true}\n')
|
|
270
|
+
before = {path.relative_to(project).as_posix(): path.read_bytes() for path in historical.rglob("*") if path.is_file()}
|
|
271
|
+
|
|
272
|
+
result = run_cli("--project-root", str(project), "diagnostics", "paths")
|
|
273
|
+
|
|
274
|
+
after = {path.relative_to(project).as_posix(): path.read_bytes() for path in historical.rglob("*") if path.is_file()}
|
|
275
|
+
assert result.returncode == 0
|
|
276
|
+
assert before == after
|
|
277
|
+
assert marker.is_file()
|
|
278
|
+
|
|
279
|
+
|
|
157
280
|
def test_runtime_config_models_environment_and_ai_resolution():
|
|
158
281
|
from prizmkit_runtime.config import AIClientResolution, RuntimeEnvironment, load_runtime_config
|
|
159
282
|
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
@@ -298,59 +421,51 @@ def test_runtime_config_supplied_env_does_not_leak_ambient_path(tmp_path):
|
|
|
298
421
|
|
|
299
422
|
|
|
300
423
|
|
|
301
|
-
|
|
424
|
+
def test_runtime_command_registry_contains_only_executing_operation_first_entries():
|
|
425
|
+
from prizmkit_runtime.commands import find_runtime_command, list_runtime_commands
|
|
302
426
|
|
|
303
427
|
command_keys = {command.key for command in list_runtime_commands()}
|
|
304
|
-
|
|
305
428
|
expected = {
|
|
306
|
-
(
|
|
307
|
-
(
|
|
308
|
-
(
|
|
309
|
-
(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
(
|
|
313
|
-
|
|
314
|
-
(
|
|
315
|
-
("refactor", None, "run"),
|
|
316
|
-
("refactor", None, "status"),
|
|
317
|
-
("refactor", None, "unskip"),
|
|
318
|
-
("refactor", None, "describe"),
|
|
319
|
-
("recovery", None, "run"),
|
|
320
|
-
("recovery", None, "detect"),
|
|
321
|
-
("recovery", None, "describe"),
|
|
322
|
-
("reset", "feature", "reset"),
|
|
323
|
-
("reset", "bugfix", "reset"),
|
|
324
|
-
("reset", "refactor", "reset"),
|
|
325
|
-
("reset", None, "describe"),
|
|
326
|
-
("status", "feature", "status"),
|
|
327
|
-
("status", "bugfix", "status"),
|
|
328
|
-
("status", "refactor", "status"),
|
|
329
|
-
("status", "all", "status"),
|
|
330
|
-
("diagnostics", None, "paths"),
|
|
331
|
-
("diagnostics", None, "entrypoints"),
|
|
332
|
-
("diagnostics", None, "version"),
|
|
429
|
+
*(('run', family, 'run') for family in ('feature', 'bugfix', 'refactor')),
|
|
430
|
+
*(('status', family, 'status') for family in ('feature', 'bugfix', 'refactor', 'all')),
|
|
431
|
+
*(('reset', family, 'reset') for family in ('feature', 'bugfix', 'refactor')),
|
|
432
|
+
*(('daemon', family, action)
|
|
433
|
+
for family in ('feature', 'bugfix', 'refactor')
|
|
434
|
+
for action in ('start', 'stop', 'status', 'logs', 'restart')),
|
|
435
|
+
*(('diagnostics', None, action)
|
|
436
|
+
for action in ('paths', 'entrypoints', 'version', 'ai-cli')),
|
|
437
|
+
('helper', None, 'run'),
|
|
333
438
|
}
|
|
334
|
-
for target in ["feature", "bugfix", "refactor"]:
|
|
335
|
-
for action in ["start", "stop", "status", "logs", "restart", "describe"]:
|
|
336
|
-
expected.add(("daemon", target, action))
|
|
337
|
-
|
|
338
|
-
assert expected.issubset(command_keys)
|
|
339
439
|
|
|
440
|
+
assert command_keys == expected
|
|
441
|
+
assert all(command.current_entrypoint for command in list_runtime_commands())
|
|
442
|
+
assert all(command.action != "describe" for command in list_runtime_commands())
|
|
443
|
+
assert all(command.group not in {"feature", "bugfix", "refactor", "recovery"} for command in list_runtime_commands())
|
|
444
|
+
for removed in [
|
|
445
|
+
("feature", "run", None),
|
|
446
|
+
("feature", "test-cli", None),
|
|
447
|
+
("reset", "describe", None),
|
|
448
|
+
("daemon", "describe", "feature"),
|
|
449
|
+
("recovery", "detect", None),
|
|
450
|
+
]:
|
|
451
|
+
with pytest.raises(KeyError, match="Unknown runtime command"):
|
|
452
|
+
find_runtime_command(removed[0], removed[1], removed[2])
|
|
340
453
|
|
|
341
|
-
def test_python_runtime_command_output_describes_canonical_boundary():
|
|
342
|
-
result = run_cli("feature", "describe")
|
|
343
454
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
455
|
+
def test_removed_describe_commands_are_rejected():
|
|
456
|
+
for args in [
|
|
457
|
+
("feature", "describe"),
|
|
458
|
+
("reset", "describe"),
|
|
459
|
+
("daemon", "feature", "describe"),
|
|
460
|
+
("diagnostics", "describe"),
|
|
461
|
+
]:
|
|
462
|
+
result = run_cli(*args)
|
|
463
|
+
assert result.returncode == 2
|
|
464
|
+
assert "invalid choice" in result.stderr
|
|
465
|
+
assert "Python runtime command contract" not in result.stdout
|
|
351
466
|
|
|
352
467
|
|
|
353
|
-
def
|
|
468
|
+
def test_operation_first_runtime_arguments_are_validated_then_forwarded_exactly(monkeypatch):
|
|
354
469
|
from prizmkit_runtime import cli as runtime_cli
|
|
355
470
|
|
|
356
471
|
captured = []
|
|
@@ -362,8 +477,16 @@ def test_option_first_runtime_arguments_are_forwarded(monkeypatch):
|
|
|
362
477
|
monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
|
|
363
478
|
monkeypatch.setattr(runtime_cli, "handle_runtime_command", fake_handle)
|
|
364
479
|
|
|
365
|
-
|
|
366
|
-
|
|
480
|
+
run_args = [
|
|
481
|
+
"run", "feature", "F-001", "custom.json", "--features", "F-001:F-005",
|
|
482
|
+
"--max-retries", "5", "--max-infra-retries", "4", "--mode", "full",
|
|
483
|
+
"--resume-phase", "6", "--dry-run",
|
|
484
|
+
]
|
|
485
|
+
assert runtime_cli.main(run_args) == 0
|
|
486
|
+
assert captured[-1] == ("run", "run", "feature", tuple(run_args[2:]))
|
|
487
|
+
|
|
488
|
+
assert runtime_cli.main(["status", "bugfix", "bugs.json"]) == 0
|
|
489
|
+
assert captured[-1] == ("status", "status", "bugfix", ("bugs.json",))
|
|
367
490
|
|
|
368
491
|
assert runtime_cli.main(["daemon", "feature", "start", "--mode", "standard"]) == 0
|
|
369
492
|
assert captured[-1] == ("daemon", "start", "feature", ("--mode", "standard"))
|
|
@@ -371,67 +494,104 @@ def test_option_first_runtime_arguments_are_forwarded(monkeypatch):
|
|
|
371
494
|
assert runtime_cli.main(["daemon", "feature", "logs", "--follow"]) == 0
|
|
372
495
|
assert captured[-1] == ("daemon", "logs", "feature", ("--follow",))
|
|
373
496
|
|
|
374
|
-
assert runtime_cli.main(["reset", "feature", "--failed"]) == 0
|
|
375
|
-
assert captured[-1] == ("reset", "reset", "feature", ("--failed",))
|
|
497
|
+
assert runtime_cli.main(["reset", "feature", "--failed", "--preserve-runtime"]) == 0
|
|
498
|
+
assert captured[-1] == ("reset", "reset", "feature", ("--failed", "--preserve-runtime"))
|
|
499
|
+
|
|
500
|
+
interspersed_reset = ["reset", "feature", "F-001", "--fresh-checkout", "custom.json"]
|
|
501
|
+
assert runtime_cli.main(interspersed_reset) == 0
|
|
502
|
+
assert captured[-1] == (
|
|
503
|
+
"reset", "reset", "feature", ("F-001", "--fresh-checkout", "custom.json")
|
|
504
|
+
)
|
|
376
505
|
|
|
377
|
-
|
|
378
|
-
assert
|
|
506
|
+
interspersed_daemon = ["daemon", "feature", "start", "--mode", "full", "custom.json"]
|
|
507
|
+
assert runtime_cli.main(interspersed_daemon) == 0
|
|
508
|
+
assert captured[-1] == (
|
|
509
|
+
"daemon", "start", "feature", ("--mode", "full", "custom.json")
|
|
510
|
+
)
|
|
379
511
|
|
|
512
|
+
assert runtime_cli.main(["--project-root", "feature", "run", "feature", "--dry-run"]) == 0
|
|
513
|
+
assert captured[-1] == ("run", "run", "feature", ("--dry-run",))
|
|
380
514
|
|
|
381
|
-
def test_unskip_dispatches_status_updater_not_contract(monkeypatch):
|
|
382
|
-
from prizmkit_runtime import runners
|
|
383
|
-
from prizmkit_runtime.commands import handle_runtime_command
|
|
384
|
-
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
385
515
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
516
|
+
@pytest.mark.parametrize(
|
|
517
|
+
("family", "task_id", "extra_args"),
|
|
518
|
+
[
|
|
519
|
+
("feature", "F-007", ("--features", "F-007", "--dry-run")),
|
|
520
|
+
("bugfix", "B-007", ("--mode", "full", "--max-retries", "4")),
|
|
521
|
+
("refactor", "R-007", ("--resume-phase", "6", "--max-infra-retries", "5")),
|
|
522
|
+
],
|
|
523
|
+
)
|
|
524
|
+
def test_run_family_dispatch_preserves_each_family_argument_tail(
|
|
525
|
+
monkeypatch, family, task_id, extra_args
|
|
526
|
+
):
|
|
527
|
+
from prizmkit_runtime import cli as runtime_cli
|
|
390
528
|
|
|
391
529
|
captured = []
|
|
530
|
+
monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
|
|
531
|
+
monkeypatch.setattr(
|
|
532
|
+
runtime_cli,
|
|
533
|
+
"handle_runtime_command",
|
|
534
|
+
lambda group, action, **kwargs: captured.append(
|
|
535
|
+
(group, action, kwargs["target"], kwargs["legacy_args"])
|
|
536
|
+
) or runtime_cli.CommandResult(0, "ok", "ok"),
|
|
537
|
+
)
|
|
538
|
+
argv = ["run", family, task_id, *extra_args]
|
|
392
539
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return FakeStatus()
|
|
540
|
+
assert runtime_cli.main(argv) == 0
|
|
541
|
+
assert captured == [("run", "run", family, (task_id, *extra_args))]
|
|
396
542
|
|
|
397
|
-
monkeypatch.setattr(runners, "run_status_action", fake_run_status_action)
|
|
398
|
-
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT)
|
|
399
543
|
|
|
400
|
-
|
|
544
|
+
@pytest.mark.parametrize("family", ("feature", "bugfix", "refactor"))
|
|
545
|
+
@pytest.mark.parametrize("legacy_action", ("reset", "un" + "skip"))
|
|
546
|
+
def test_family_level_reset_aliases_are_not_public_commands(family, legacy_action):
|
|
547
|
+
result = run_cli(family, legacy_action)
|
|
401
548
|
|
|
402
|
-
assert result.
|
|
403
|
-
assert
|
|
404
|
-
assert "unskipped" in result.render()
|
|
405
|
-
assert "Python runtime command contract" not in result.render()
|
|
406
|
-
assert captured == [("feature", "unskip", "F-005", "F-005", REPO_ROOT.resolve())]
|
|
549
|
+
assert result.returncode == 2
|
|
550
|
+
assert "invalid choice" in result.stderr
|
|
407
551
|
|
|
408
552
|
|
|
409
|
-
def
|
|
410
|
-
from prizmkit_runtime import
|
|
411
|
-
from prizmkit_runtime.commands import
|
|
553
|
+
def test_diagnostics_ai_cli_reports_family_neutral_resolution(monkeypatch):
|
|
554
|
+
from prizmkit_runtime import config
|
|
555
|
+
from prizmkit_runtime.commands import handle_diagnostics
|
|
412
556
|
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
413
557
|
|
|
414
558
|
class FakeAIClient:
|
|
415
559
|
command = "claude"
|
|
416
560
|
platform = "claude"
|
|
417
561
|
source = "test"
|
|
418
|
-
notes = ()
|
|
562
|
+
notes = ("configured globally",)
|
|
419
563
|
|
|
420
564
|
class FakeConfig:
|
|
421
565
|
ai_client = FakeAIClient()
|
|
422
566
|
runtime_mode = "python-runtime-canonical"
|
|
423
567
|
model = "claude-sonnet-4.6"
|
|
424
568
|
|
|
425
|
-
monkeypatch.setattr(
|
|
569
|
+
monkeypatch.setattr(config, "load_runtime_config", lambda paths: FakeConfig())
|
|
426
570
|
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT)
|
|
427
571
|
|
|
428
|
-
result =
|
|
572
|
+
result = handle_diagnostics("ai-cli", paths)
|
|
429
573
|
rendered = result.render()
|
|
430
574
|
|
|
431
575
|
assert result.exit_code == 0
|
|
432
|
-
assert result.title == "Python
|
|
576
|
+
assert result.title == "Python runtime AI CLI diagnostics"
|
|
433
577
|
assert "detected_cli: claude" in rendered
|
|
434
|
-
assert "
|
|
578
|
+
assert "platform: claude" in rendered
|
|
579
|
+
assert "source: test" in rendered
|
|
580
|
+
assert "runtime_mode: python-runtime-canonical" in rendered
|
|
581
|
+
assert "model: claude-sonnet-4.6" in rendered
|
|
582
|
+
assert "note: configured globally" in rendered
|
|
583
|
+
assert "feature" not in result.title.lower()
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def test_diagnostics_help_exposes_only_real_diagnostic_actions():
|
|
587
|
+
result = run_cli("diagnostics", "--help")
|
|
588
|
+
|
|
589
|
+
assert result.returncode == 0
|
|
590
|
+
for action in ("paths", "entrypoints", "version", "ai-cli"):
|
|
591
|
+
assert action in result.stdout
|
|
592
|
+
assert "describe" not in result.stdout
|
|
593
|
+
assert "commands" not in result.stdout
|
|
594
|
+
assert "test-cli" not in result.stdout
|
|
435
595
|
|
|
436
596
|
|
|
437
597
|
def test_diagnostics_paths_and_entrypoints_are_standard_library_cli_actions():
|
|
@@ -444,8 +604,180 @@ def test_diagnostics_paths_and_entrypoints_are_standard_library_cli_actions():
|
|
|
444
604
|
assert entrypoints_result.returncode == 0
|
|
445
605
|
assert "Python runtime entrypoint inventory" in entrypoints_result.stdout
|
|
446
606
|
assert "runtime:cli:" in entrypoints_result.stdout
|
|
607
|
+
assert "command:run:feature: python3 dev-pipeline/cli.py run feature" in entrypoints_result.stdout
|
|
608
|
+
assert "command:status:all: python3 dev-pipeline/cli.py status all" in entrypoints_result.stdout
|
|
609
|
+
assert "command:diagnostics:ai-cli:" in entrypoints_result.stdout
|
|
610
|
+
assert " describe" not in entrypoints_result.stdout
|
|
611
|
+
assert "test-cli" not in entrypoints_result.stdout
|
|
447
612
|
assert "run-feature.sh" not in entrypoints_result.stdout
|
|
448
613
|
assert "generate-bootstrap-prompt.py" in entrypoints_result.stdout
|
|
614
|
+
assert "generate-recovery-prompt.py" not in entrypoints_result.stdout
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _write_family_status_plan(paths, family, task_id):
|
|
618
|
+
plan_path, key = {
|
|
619
|
+
"feature": (paths.feature_plan, "features"),
|
|
620
|
+
"bugfix": (paths.bugfix_plan, "bugs"),
|
|
621
|
+
"refactor": (paths.refactor_plan, "refactors"),
|
|
622
|
+
}[family]
|
|
623
|
+
plan_path.parent.mkdir(parents=True, exist_ok=True)
|
|
624
|
+
plan_path.write_text(json.dumps({key: [{"id": task_id, "status": "pending"}]}), encoding="utf-8")
|
|
625
|
+
return plan_path
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _file_bytes_under(root):
|
|
629
|
+
if not root.exists():
|
|
630
|
+
return {}
|
|
631
|
+
return {
|
|
632
|
+
path.relative_to(root).as_posix(): path.read_bytes()
|
|
633
|
+
for path in root.rglob("*")
|
|
634
|
+
if path.is_file()
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def test_status_all_aggregates_configured_families_in_fixed_order_and_missing_is_nonfatal(
|
|
639
|
+
tmp_path, monkeypatch
|
|
640
|
+
):
|
|
641
|
+
from prizmkit_runtime import runners
|
|
642
|
+
from prizmkit_runtime.commands import CommandResult, handle_runtime_command
|
|
643
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
644
|
+
|
|
645
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
|
|
646
|
+
_write_family_status_plan(paths, "feature", "F-001")
|
|
647
|
+
_write_family_status_plan(paths, "refactor", "R-001")
|
|
648
|
+
before = _file_bytes_under(tmp_path)
|
|
649
|
+
calls = []
|
|
650
|
+
|
|
651
|
+
def fake_status(family, action, legacy_args, runtime_paths):
|
|
652
|
+
calls.append((family, action, legacy_args, runtime_paths))
|
|
653
|
+
return CommandResult(0, f"Python {family} status", f"normal {family} status")
|
|
654
|
+
|
|
655
|
+
monkeypatch.setattr(runners, "run_pipeline_command", fake_status)
|
|
656
|
+
result = handle_runtime_command("status", "status", target="all", paths=paths)
|
|
657
|
+
rendered = result.render()
|
|
658
|
+
|
|
659
|
+
assert result.exit_code == 0
|
|
660
|
+
assert rendered.index("FEATURE\n=======") < rendered.index("BUGFIX\n======")
|
|
661
|
+
assert rendered.index("BUGFIX\n======") < rendered.index("REFACTOR\n========")
|
|
662
|
+
assert "normal feature status" in rendered
|
|
663
|
+
assert "normal refactor status" in rendered
|
|
664
|
+
assert "BUGFIX\n======\nnot configured: plan list not found" in rendered
|
|
665
|
+
assert [(family, action, args) for family, action, args, _paths in calls] == [
|
|
666
|
+
("feature", "status", ()),
|
|
667
|
+
("refactor", "status", ()),
|
|
668
|
+
]
|
|
669
|
+
assert _file_bytes_under(tmp_path) == before
|
|
670
|
+
assert not paths.feature_state_dir.exists()
|
|
671
|
+
assert not paths.bugfix_state_dir.exists()
|
|
672
|
+
assert not paths.refactor_state_dir.exists()
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def test_status_all_uses_real_family_status_handlers_without_filesystem_mutation(tmp_path):
|
|
676
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
677
|
+
|
|
678
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
|
|
679
|
+
feature_plan = _write_family_status_plan(paths, "feature", "F-001")
|
|
680
|
+
refactor_plan = _write_family_status_plan(paths, "refactor", "R-001")
|
|
681
|
+
before = {
|
|
682
|
+
path.relative_to(tmp_path).as_posix(): (path.read_bytes(), path.stat().st_mtime_ns)
|
|
683
|
+
for path in (feature_plan, refactor_plan)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
result = run_cli("--project-root", str(tmp_path), "status", "all")
|
|
687
|
+
|
|
688
|
+
after = {
|
|
689
|
+
path.relative_to(tmp_path).as_posix(): (path.read_bytes(), path.stat().st_mtime_ns)
|
|
690
|
+
for path in (feature_plan, refactor_plan)
|
|
691
|
+
}
|
|
692
|
+
assert result.returncode == 0, result.stderr
|
|
693
|
+
assert "FEATURE\n=======" in result.stdout
|
|
694
|
+
assert "Python feature status" in result.stdout
|
|
695
|
+
assert "BUGFIX\n======\nnot configured: plan list not found" in result.stdout
|
|
696
|
+
assert "REFACTOR\n========" in result.stdout
|
|
697
|
+
assert "Python refactor status" in result.stdout
|
|
698
|
+
assert after == before
|
|
699
|
+
assert not paths.feature_state_dir.exists()
|
|
700
|
+
assert not paths.bugfix_state_dir.exists()
|
|
701
|
+
assert not paths.refactor_state_dir.exists()
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def test_status_all_reports_plan_and_handler_errors_but_renders_unaffected_sections(
|
|
705
|
+
tmp_path, monkeypatch
|
|
706
|
+
):
|
|
707
|
+
from prizmkit_runtime import runners
|
|
708
|
+
from prizmkit_runtime.commands import CommandResult, handle_runtime_command
|
|
709
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
710
|
+
|
|
711
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
|
|
712
|
+
_write_family_status_plan(paths, "feature", "F-001")
|
|
713
|
+
paths.bugfix_plan.parent.mkdir(parents=True, exist_ok=True)
|
|
714
|
+
paths.bugfix_plan.write_text("{malformed", encoding="utf-8")
|
|
715
|
+
_write_family_status_plan(paths, "refactor", "R-001")
|
|
716
|
+
|
|
717
|
+
def fake_status(family, action, legacy_args, runtime_paths):
|
|
718
|
+
assert action == "status"
|
|
719
|
+
if family == "refactor":
|
|
720
|
+
raise RuntimeError("family handler exploded")
|
|
721
|
+
return CommandResult(0, f"Python {family} status", f"normal {family} status")
|
|
722
|
+
|
|
723
|
+
monkeypatch.setattr(runners, "run_pipeline_command", fake_status)
|
|
724
|
+
result = handle_runtime_command("status", "status", target="all", paths=paths)
|
|
725
|
+
rendered = result.render()
|
|
726
|
+
|
|
727
|
+
assert result.exit_code != 0
|
|
728
|
+
assert "FEATURE\n=======\nPython feature status\nnormal feature status" in rendered
|
|
729
|
+
assert "BUGFIX\n======\nerror: malformed plan list:" in rendered
|
|
730
|
+
assert "REFACTOR\n========\nerror: family handler exploded" in rendered
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def test_status_all_reports_malformed_runtime_unparseable_and_nonzero_results(
|
|
734
|
+
tmp_path, monkeypatch
|
|
735
|
+
):
|
|
736
|
+
from prizmkit_runtime import runners
|
|
737
|
+
from prizmkit_runtime.commands import CommandResult, handle_runtime_command
|
|
738
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
739
|
+
|
|
740
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
|
|
741
|
+
_write_family_status_plan(paths, "feature", "F-001")
|
|
742
|
+
_write_family_status_plan(paths, "bugfix", "B-001")
|
|
743
|
+
_write_family_status_plan(paths, "refactor", "R-001")
|
|
744
|
+
malformed_state = paths.feature_state_dir / "F-001" / "status.json"
|
|
745
|
+
malformed_state.parent.mkdir(parents=True, exist_ok=True)
|
|
746
|
+
malformed_state.write_text("not-json", encoding="utf-8")
|
|
747
|
+
|
|
748
|
+
def fake_status(family, action, legacy_args, runtime_paths):
|
|
749
|
+
if family == "bugfix":
|
|
750
|
+
return object()
|
|
751
|
+
return CommandResult(7 if family == "refactor" else 0, f"Python {family} status", "status output")
|
|
752
|
+
|
|
753
|
+
monkeypatch.setattr(runners, "run_pipeline_command", fake_status)
|
|
754
|
+
result = handle_runtime_command("status", "status", target="all", paths=paths)
|
|
755
|
+
rendered = result.render()
|
|
756
|
+
|
|
757
|
+
assert result.exit_code != 0
|
|
758
|
+
assert "FEATURE\n=======\nerror: malformed runtime state F-001/status.json" in rendered
|
|
759
|
+
assert "BUGFIX\n======\nerror: unparseable configured family result" in rendered
|
|
760
|
+
assert "REFACTOR\n========\nerror: Python refactor status\nstatus output" in rendered
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def test_status_all_direct_api_rejects_family_specific_arguments_without_calling_handlers(
|
|
764
|
+
tmp_path, monkeypatch
|
|
765
|
+
):
|
|
766
|
+
from prizmkit_runtime import runners
|
|
767
|
+
from prizmkit_runtime.commands import handle_runtime_command
|
|
768
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
769
|
+
|
|
770
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
|
|
771
|
+
called = []
|
|
772
|
+
monkeypatch.setattr(runners, "run_pipeline_command", lambda *args: called.append(args))
|
|
773
|
+
|
|
774
|
+
result = handle_runtime_command(
|
|
775
|
+
"status", "status", target="all", paths=paths, legacy_args=("custom.json",)
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
assert result.exit_code == 2
|
|
779
|
+
assert "does not accept a plan path" in result.render()
|
|
780
|
+
assert called == []
|
|
449
781
|
|
|
450
782
|
|
|
451
783
|
def test_interoperability_preserves_existing_utility_mapping():
|
|
@@ -481,9 +813,55 @@ def test_interoperability_preserves_existing_utility_mapping():
|
|
|
481
813
|
assert actual_scripts == mapped_scripts | explicitly_library_only
|
|
482
814
|
assert explicitly_library_only == {"continuation.py", "prompt_framework.py", "utils.py"}
|
|
483
815
|
assert "parse-stream-progress.py" in mapped_scripts
|
|
816
|
+
assert "generate-recovery-prompt" not in UTILITY_ENTRYPOINTS
|
|
484
817
|
assert "prizmkit-test-gate.py" not in mapped_scripts
|
|
485
818
|
|
|
486
819
|
|
|
820
|
+
def test_obsolete_runtime_recovery_ai_session_sources_are_absent():
|
|
821
|
+
assert not (PIPELINE_ROOT / "prizmkit_runtime" / "runner_recovery.py").exists()
|
|
822
|
+
assert not (PIPELINE_ROOT / "scripts" / "generate-recovery-prompt.py").exists()
|
|
823
|
+
|
|
824
|
+
forbidden_by_source = {
|
|
825
|
+
"prizmkit_runtime/cli.py": ('"recovery"',),
|
|
826
|
+
"prizmkit_runtime/commands.py": ('"recovery": ("run"', 'group == "recovery"'),
|
|
827
|
+
"prizmkit_runtime/runners.py": ("runner_recovery", "run_recovery_command"),
|
|
828
|
+
"prizmkit_runtime/paths.py": ("recovery_state_dir",),
|
|
829
|
+
"prizmkit_runtime/interoperability.py": ("generate-recovery-prompt",),
|
|
830
|
+
}
|
|
831
|
+
for relative_path, forbidden_tokens in forbidden_by_source.items():
|
|
832
|
+
content = (PIPELINE_ROOT / relative_path).read_text(encoding="utf-8")
|
|
833
|
+
for token in forbidden_tokens:
|
|
834
|
+
assert token not in content, f"{relative_path} still contains {token}"
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
@pytest.mark.parametrize(
|
|
838
|
+
("path", "ordinary_command"),
|
|
839
|
+
[
|
|
840
|
+
(REPO_ROOT / "README.md", "python3 ./.prizmkit/dev-pipeline/cli.py run feature F-003"),
|
|
841
|
+
(
|
|
842
|
+
REPO_ROOT / "core" / "templates" / "project-memory-template.md",
|
|
843
|
+
"python3 ./.prizmkit/dev-pipeline/cli.py run <family> <task-id>",
|
|
844
|
+
),
|
|
845
|
+
(
|
|
846
|
+
REPO_ROOT
|
|
847
|
+
/ "core"
|
|
848
|
+
/ "skills"
|
|
849
|
+
/ "orchestration-skill"
|
|
850
|
+
/ "workflows"
|
|
851
|
+
/ "recovery-workflow"
|
|
852
|
+
/ "SKILL.md",
|
|
853
|
+
"python3 ./.prizmkit/dev-pipeline/cli.py run feature F-001",
|
|
854
|
+
),
|
|
855
|
+
],
|
|
856
|
+
)
|
|
857
|
+
def test_canonical_interrupted_task_guidance_uses_recovery_workflow(path, ordinary_command):
|
|
858
|
+
content = path.read_text(encoding="utf-8")
|
|
859
|
+
|
|
860
|
+
assert "recovery-workflow" in content
|
|
861
|
+
assert ordinary_command in content
|
|
862
|
+
assert "cli.py recovery" not in content
|
|
863
|
+
|
|
864
|
+
|
|
487
865
|
def test_boundary_dataclasses_are_cohesive_and_frozen():
|
|
488
866
|
from prizmkit_runtime.gitops import BranchContext, WorktreePolicy
|
|
489
867
|
from prizmkit_runtime.heartbeat import HeartbeatConfig, HeartbeatState
|
|
@@ -1017,6 +1395,31 @@ def test_progress_parser_keeps_pi_length_as_nonfatal_diagnostic_metadata():
|
|
|
1017
1395
|
assert data["terminal_success_at"]
|
|
1018
1396
|
|
|
1019
1397
|
|
|
1398
|
+
@pytest.mark.parametrize("role", ("assistant", "toolResult"))
|
|
1399
|
+
def test_progress_parser_does_not_promote_pi_diagnostic_content_to_fatal_error(role):
|
|
1400
|
+
module = _load_progress_parser_module()
|
|
1401
|
+
tracker = module.ProgressTracker()
|
|
1402
|
+
tracker.process_event({"type": "session", "version": 3, "id": "pi-session", "cwd": "/tmp/project"})
|
|
1403
|
+
tracker.process_event({"type": "agent_start"})
|
|
1404
|
+
tracker.process_event({
|
|
1405
|
+
"type": "message_end",
|
|
1406
|
+
"message": {
|
|
1407
|
+
"role": role,
|
|
1408
|
+
"content": [{
|
|
1409
|
+
"type": "text",
|
|
1410
|
+
"text": 'continuation_reason = "context_overflow"\nerror = _error_text(result)',
|
|
1411
|
+
}],
|
|
1412
|
+
"stopReason": "toolUse",
|
|
1413
|
+
},
|
|
1414
|
+
})
|
|
1415
|
+
|
|
1416
|
+
data = tracker.to_dict()
|
|
1417
|
+
|
|
1418
|
+
assert data["api_error_code"] == ""
|
|
1419
|
+
assert data["fatal_error_code"] == ""
|
|
1420
|
+
assert data["last_result_is_error"] is False
|
|
1421
|
+
|
|
1422
|
+
|
|
1020
1423
|
def test_progress_parser_recovers_from_retrying_pi_attempt_before_final_success():
|
|
1021
1424
|
module = _load_progress_parser_module()
|
|
1022
1425
|
tracker = module.ProgressTracker()
|
|
@@ -1677,6 +2080,57 @@ def test_session_persists_normalized_stream_without_raw_history(tmp_path, monkey
|
|
|
1677
2080
|
assert not backup_log.exists()
|
|
1678
2081
|
|
|
1679
2082
|
|
|
2083
|
+
def test_pi_session_diagnostic_tool_output_does_not_trigger_fatal_termination(tmp_path):
|
|
2084
|
+
import prizmkit_runtime.sessions as sessions
|
|
2085
|
+
|
|
2086
|
+
fake_cli = tmp_path / "pi_diagnostic_cli.py"
|
|
2087
|
+
fake_cli.write_text(
|
|
2088
|
+
"#!/usr/bin/env python3\n"
|
|
2089
|
+
"import json, sys, time\n"
|
|
2090
|
+
"_prompt = sys.stdin.read()\n"
|
|
2091
|
+
"source = 'continuation_reason = \\\"context_overflow\\\"\\nerror = _error_text(result)'\n"
|
|
2092
|
+
"events = [\n"
|
|
2093
|
+
" {'type':'session','id':'s1'},\n"
|
|
2094
|
+
" {'type':'agent_start'},\n"
|
|
2095
|
+
" {'type':'turn_start'},\n"
|
|
2096
|
+
" {'type':'tool_execution_start','toolCallId':'c1','toolName':'read','args':{'path':'runtime.py'}},\n"
|
|
2097
|
+
" {'type':'tool_execution_end','toolCallId':'c1','toolName':'read','result':{'content':[{'type':'text','text':source}]},'isError':False},\n"
|
|
2098
|
+
" {'type':'message_end','message':{'role':'toolResult','toolCallId':'c1','content':[{'type':'text','text':source}],'stopReason':'toolUse'}},\n"
|
|
2099
|
+
"]\n"
|
|
2100
|
+
"for event in events: print(json.dumps(event), flush=True)\n"
|
|
2101
|
+
"time.sleep(1.5)\n"
|
|
2102
|
+
"message = {'role':'assistant','content':[{'type':'text','text':'Done'}],'stopReason':'stop'}\n"
|
|
2103
|
+
"print(json.dumps({'type':'message_end','message':message}), flush=True)\n"
|
|
2104
|
+
"print(json.dumps({'type':'agent_end','willRetry':False,'messages':[message]}), flush=True)\n",
|
|
2105
|
+
encoding="utf-8",
|
|
2106
|
+
)
|
|
2107
|
+
fake_cli.chmod(0o755)
|
|
2108
|
+
prompt = tmp_path / "prompt.md"
|
|
2109
|
+
prompt.write_text("hello", encoding="utf-8")
|
|
2110
|
+
session_log = tmp_path / "state" / "sessions" / "sess-diagnostic" / "logs" / "session.log"
|
|
2111
|
+
progress = session_log.with_name("progress.json")
|
|
2112
|
+
|
|
2113
|
+
result = sessions.AISessionLauncher(
|
|
2114
|
+
sessions.AISessionConfig(
|
|
2115
|
+
cli_command=str(fake_cli),
|
|
2116
|
+
platform="pi",
|
|
2117
|
+
model=None,
|
|
2118
|
+
prompt_path=prompt,
|
|
2119
|
+
cwd=tmp_path,
|
|
2120
|
+
log_path=session_log,
|
|
2121
|
+
progress_path=progress,
|
|
2122
|
+
)
|
|
2123
|
+
).run()
|
|
2124
|
+
|
|
2125
|
+
progress_data = json.loads(progress.read_text(encoding="utf-8"))
|
|
2126
|
+
assert result.exit_code == 0
|
|
2127
|
+
assert result.fatal_error_code == ""
|
|
2128
|
+
assert result.progress_summary.result_subtype == "success"
|
|
2129
|
+
assert progress_data["fatal_error_code"] == ""
|
|
2130
|
+
assert not session_log.with_name("fatal-error.json").exists()
|
|
2131
|
+
assert not session_log.with_name("stale-kill.json").exists()
|
|
2132
|
+
|
|
2133
|
+
|
|
1680
2134
|
def test_custom_session_launch_writes_existing_logs_and_progress(tmp_path):
|
|
1681
2135
|
from prizmkit_runtime.launch_profiles import HeadlessLaunchProfile
|
|
1682
2136
|
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
|
|
@@ -2122,97 +2576,6 @@ def test_compact_live_heartbeat_stays_out_of_primary_and_backup_logs(tmp_path, c
|
|
|
2122
2576
|
assert "stale:" not in backup_text.get("content", "")
|
|
2123
2577
|
|
|
2124
2578
|
|
|
2125
|
-
def test_recovery_sessions_receive_live_heartbeat_configuration(tmp_path, monkeypatch):
|
|
2126
|
-
import prizmkit_runtime.runner_recovery as runner_recovery
|
|
2127
|
-
|
|
2128
|
-
project = tmp_path / "project"
|
|
2129
|
-
pipeline = tmp_path / "pipeline"
|
|
2130
|
-
recovery_state = tmp_path / "recovery"
|
|
2131
|
-
project.mkdir()
|
|
2132
|
-
pipeline.mkdir()
|
|
2133
|
-
recovery_state.mkdir()
|
|
2134
|
-
paths = type(
|
|
2135
|
-
"Paths",
|
|
2136
|
-
(),
|
|
2137
|
-
{"project_root": project, "pipeline_root": pipeline, "recovery_state_dir": recovery_state},
|
|
2138
|
-
)()
|
|
2139
|
-
captured_config = {}
|
|
2140
|
-
|
|
2141
|
-
monkeypatch.setattr(
|
|
2142
|
-
runner_recovery,
|
|
2143
|
-
"run_recovery_detect",
|
|
2144
|
-
lambda _paths: (0, "{}", "", {"detected": True, "workflow_type": "feature"}),
|
|
2145
|
-
)
|
|
2146
|
-
monkeypatch.setattr(
|
|
2147
|
-
runner_recovery,
|
|
2148
|
-
"load_runtime_config",
|
|
2149
|
-
lambda _paths: type(
|
|
2150
|
-
"Config",
|
|
2151
|
-
(),
|
|
2152
|
-
{
|
|
2153
|
-
"ai_client": type("Client", (), {"command": "fake-cli", "platform": "codebuddy"})(),
|
|
2154
|
-
"model": "model",
|
|
2155
|
-
"effort": "high",
|
|
2156
|
-
},
|
|
2157
|
-
)(),
|
|
2158
|
-
)
|
|
2159
|
-
monkeypatch.setattr(
|
|
2160
|
-
runner_recovery,
|
|
2161
|
-
"RunnerEnvironment",
|
|
2162
|
-
type(
|
|
2163
|
-
"EnvFactory",
|
|
2164
|
-
(),
|
|
2165
|
-
{
|
|
2166
|
-
"from_env": staticmethod(
|
|
2167
|
-
lambda: type(
|
|
2168
|
-
"Env",
|
|
2169
|
-
(),
|
|
2170
|
-
{
|
|
2171
|
-
"verbose": True,
|
|
2172
|
-
"live_output": True,
|
|
2173
|
-
"heartbeat_interval_seconds": 7,
|
|
2174
|
-
"heartbeat_stale_threshold_seconds": 21,
|
|
2175
|
-
"stale_kill_threshold_seconds": 42,
|
|
2176
|
-
},
|
|
2177
|
-
)()
|
|
2178
|
-
)
|
|
2179
|
-
},
|
|
2180
|
-
),
|
|
2181
|
-
)
|
|
2182
|
-
monkeypatch.setattr(runner_recovery, "detect_stream_json_support", lambda *_args: type("Support", (), {"enabled": True})())
|
|
2183
|
-
monkeypatch.setattr(
|
|
2184
|
-
runner_recovery.subprocess,
|
|
2185
|
-
"run",
|
|
2186
|
-
lambda *_args, **_kwargs: type("Completed", (), {"returncode": 0, "stdout": "0", "stderr": ""})(),
|
|
2187
|
-
)
|
|
2188
|
-
monkeypatch.setattr(runner_recovery, "_recovery_outcome", lambda *_args, **_kwargs: ("no_changes", {}))
|
|
2189
|
-
|
|
2190
|
-
class FakeLauncher:
|
|
2191
|
-
def __init__(self, config):
|
|
2192
|
-
captured_config["config"] = config
|
|
2193
|
-
|
|
2194
|
-
def run(self):
|
|
2195
|
-
return type("Result", (), {"exit_code": 0})()
|
|
2196
|
-
|
|
2197
|
-
monkeypatch.setattr(runner_recovery, "AISessionLauncher", FakeLauncher)
|
|
2198
|
-
|
|
2199
|
-
code, stdout, stderr = runner_recovery.run_recovery(paths)
|
|
2200
|
-
config = captured_config["config"]
|
|
2201
|
-
|
|
2202
|
-
assert code == 0
|
|
2203
|
-
assert stdout == "no_changes\n"
|
|
2204
|
-
assert stderr == ""
|
|
2205
|
-
assert config.live_output is True
|
|
2206
|
-
assert config.verbose is True
|
|
2207
|
-
assert config.use_stream_json is True
|
|
2208
|
-
assert config.live_progress_interval_seconds == 7
|
|
2209
|
-
assert config.heartbeat_stale_threshold_seconds == 21
|
|
2210
|
-
assert config.stale_kill_threshold_seconds == 42
|
|
2211
|
-
assert config.live_banner is False
|
|
2212
|
-
assert config.suppress_stream_output is True
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
2579
|
def test_session_summary_reports_normalized_subagents_and_child_liveness(tmp_path, capsys):
|
|
2217
2580
|
import prizmkit_runtime.runners as runners
|
|
2218
2581
|
from prizmkit_runtime.runner_models import SessionPaths
|