prizmkit 1.1.153 → 1.1.155
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/adapters/codex/skill-adapter.js +1 -1
- package/bundled/adapters/pi/skill-adapter.js +2 -1
- package/bundled/dev-pipeline/.env.example +2 -6
- package/bundled/dev-pipeline/README.md +120 -113
- package/bundled/dev-pipeline/assets/skill-subagent-integration.md +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +355 -15
- package/bundled/dev-pipeline/prizmkit_runtime/cli.py +189 -110
- package/bundled/dev-pipeline/prizmkit_runtime/commands.py +146 -111
- package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +4 -20
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +48 -14
- 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_classification.py +50 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +1 -10
- package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +139 -74
- package/bundled/dev-pipeline/prizmkit_runtime/runtime_commit.py +482 -0
- package/bundled/dev-pipeline/prizmkit_runtime/status.py +106 -1
- package/bundled/dev-pipeline/prizmkit_runtime/test_result.py +64 -11
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +145 -367
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +91 -62
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +105 -69
- package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +2 -2
- package/bundled/dev-pipeline/scripts/prompt_framework.py +36 -41
- package/bundled/dev-pipeline/scripts/update-bug-status.py +197 -71
- package/bundled/dev-pipeline/scripts/update-checkpoint.py +77 -16
- package/bundled/dev-pipeline/scripts/update-feature-status.py +46 -121
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +46 -123
- package/bundled/dev-pipeline/scripts/utils.py +119 -0
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +22 -27
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +71 -104
- package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +3 -2
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +59 -111
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +37 -72
- package/bundled/dev-pipeline/templates/sections/artifact-manifest.md +39 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-critical-paths.md +3 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +8 -25
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +3 -3
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-implement.md +2 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +3 -3
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +2 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +4 -4
- package/bundled/dev-pipeline/templates/sections/bugfix-session-context.md +0 -1
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +7 -4
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +14 -27
- package/bundled/dev-pipeline/templates/sections/feature-completion-summary.md +20 -0
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +8 -71
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +7 -7
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +3 -3
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +4 -4
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +5 -17
- package/bundled/dev-pipeline/templates/sections/refactor-critical-paths.md +3 -1
- package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +10 -23
- package/bundled/dev-pipeline/templates/sections/refactor-phase-implement.md +2 -2
- package/bundled/dev-pipeline/templates/sections/refactor-phase-plan.md +2 -2
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +3 -3
- package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +2 -2
- package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +7 -7
- package/bundled/dev-pipeline/templates/sections/refactor-session-context.md +0 -1
- package/bundled/dev-pipeline/templates/sections/retrospective.md +12 -0
- package/bundled/dev-pipeline/templates/sections/runtime-commit-handoff.md +35 -0
- package/bundled/dev-pipeline/templates/session-status-schema.json +45 -62
- package/bundled/dev-pipeline/tests/test_auto_skip.py +111 -32
- package/bundled/dev-pipeline/tests/test_checkpoint_state.py +617 -30
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +262 -194
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +62 -23
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +76 -28
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +281 -206
- package/bundled/dev-pipeline/tests/test_recovery_workflow.py +322 -0
- package/bundled/dev-pipeline/tests/test_reset_modes.py +935 -0
- package/bundled/dev-pipeline/tests/test_reset_preserve.py +5 -7
- package/bundled/dev-pipeline/tests/test_runtime_commit.py +608 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +617 -188
- package/bundled/skills/_metadata.json +9 -9
- package/bundled/skills/app-planner/SKILL.md +3 -3
- package/bundled/skills/app-planner/references/generated-plan-review.md +2 -1
- package/bundled/skills/app-planner/references/rules/backend/derivation-rules.md +56 -64
- package/bundled/skills/app-planner/references/rules/backend/fixed-rules.md +85 -240
- package/bundled/skills/app-planner/references/rules/backend/question-bank.md +25 -25
- package/bundled/skills/app-planner/references/rules/backend/question-manifest.json +16 -7
- package/bundled/skills/app-planner/references/rules/backend/template.md +74 -92
- package/bundled/skills/app-planner/references/rules/database/derivation-rules.md +93 -93
- package/bundled/skills/app-planner/references/rules/database/fixed-rules.md +66 -173
- package/bundled/skills/app-planner/references/rules/database/question-bank.md +31 -26
- package/bundled/skills/app-planner/references/rules/database/question-manifest.json +13 -4
- package/bundled/skills/app-planner/references/rules/database/template.md +76 -98
- package/bundled/skills/app-planner/references/rules/frontend/derivation-rules.md +68 -95
- package/bundled/skills/app-planner/references/rules/frontend/fixed-rules.md +93 -166
- package/bundled/skills/app-planner/references/rules/frontend/question-bank.md +46 -36
- package/bundled/skills/app-planner/references/rules/frontend/question-manifest.json +11 -2
- package/bundled/skills/app-planner/references/rules/frontend/template.md +79 -257
- package/bundled/skills/app-planner/references/rules/mobile/derivation-rules.md +91 -99
- package/bundled/skills/app-planner/references/rules/mobile/fixed-rules.md +73 -246
- package/bundled/skills/app-planner/references/rules/mobile/question-bank.md +17 -17
- package/bundled/skills/app-planner/references/rules/mobile/question-manifest.json +17 -2
- package/bundled/skills/app-planner/references/rules/mobile/template.md +73 -85
- package/bundled/skills/app-planner/references/rules-configuration.md +133 -65
- package/bundled/skills/app-planner/scripts/validate-rules-configuration.py +291 -0
- 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/prizmkit/SKILL.md +17 -18
- package/bundled/skills/prizmkit/references/workflow-state-protocol.md +22 -18
- package/bundled/skills/prizmkit-code-review/SKILL.md +22 -58
- package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +6 -6
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +2 -2
- package/bundled/skills/prizmkit-committer/SKILL.md +82 -132
- package/bundled/skills/prizmkit-deploy/SKILL.md +27 -11
- package/bundled/skills/prizmkit-deploy/assets/deploy-document-template.md +89 -0
- package/bundled/skills/prizmkit-deploy/references/ci-cd-workflows.md +44 -14
- package/bundled/skills/prizmkit-deploy/references/cloud-platform-deploy.md +19 -3
- package/bundled/skills/prizmkit-deploy/references/database-setup.md +22 -12
- package/bundled/skills/prizmkit-deploy/references/deploy-config-schema.md +3 -1
- package/bundled/skills/prizmkit-deploy/references/deploy-history-schema.md +22 -6
- package/bundled/skills/prizmkit-deploy/references/deploy-metadata-schema.json +49 -0
- package/bundled/skills/prizmkit-deploy/references/live-validation-notes.md +1 -1
- package/bundled/skills/prizmkit-deploy/references/pending-input-schema.json +164 -0
- package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +9 -8
- package/bundled/skills/prizmkit-deploy/references/ssh-execution-flow.md +6 -6
- package/bundled/skills/prizmkit-implement/SKILL.md +30 -66
- package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +4 -4
- package/bundled/skills/prizmkit-init/SKILL.md +4 -0
- package/bundled/skills/prizmkit-plan/SKILL.md +21 -64
- package/bundled/skills/prizmkit-plan/references/artifact-identity.md +38 -0
- package/bundled/skills/prizmkit-plan/references/independent-plan-review.md +4 -4
- package/bundled/skills/prizmkit-plan/references/review-plan-spec-loop.md +5 -5
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +11 -1
- package/bundled/skills/prizmkit-retrospective/SKILL.md +63 -77
- package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +3 -3
- package/bundled/skills/prizmkit-retrospective/references/retrospective-result-schema.json +138 -0
- package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +17 -23
- package/bundled/skills/prizmkit-test/SKILL.md +64 -51
- package/bundled/skills/prizmkit-test/references/boundary-contract-and-test-double-guidance.md +206 -0
- package/bundled/skills/prizmkit-test/references/independent-test-review.md +17 -9
- package/bundled/skills/prizmkit-test/references/test-coverage-model.md +45 -21
- package/bundled/skills/prizmkit-test/references/test-report-template.md +20 -13
- package/bundled/skills/prizmkit-workflow/SKILL.md +35 -37
- package/bundled/skills/prizmkit-workflow/references/artifact-identity.md +34 -0
- package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +22 -18
- package/bundled/skills/recovery-workflow/SKILL.md +9 -6
- package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
- package/bundled/skills/recovery-workflow/references/detection.md +7 -6
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +16 -6
- 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/src/scaffold.js +33 -22
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +0 -228
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +0 -767
- package/bundled/dev-pipeline/templates/sections/bugfix-artifacts.md +0 -16
- package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +0 -31
- package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -7
- package/bundled/dev-pipeline/templates/sections/critical-paths-lite.md +0 -7
- package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +0 -9
- package/bundled/dev-pipeline/templates/sections/directory-convention-lite.md +0 -6
- package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +0 -15
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +0 -61
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +0 -24
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +0 -12
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +0 -44
- package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +0 -25
- package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +0 -20
- package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +0 -20
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +0 -20
- package/bundled/dev-pipeline/templates/sections/refactor-artifacts.md +0 -17
- package/bundled/dev-pipeline/templates/sections/refactor-session-status.md +0 -28
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +0 -52
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery-lite.md +0 -52
- package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +0 -174
- package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +0 -174
- package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +0 -174
- package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +0 -174
- package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +0 -174
- package/bundled/skills/prizmkit-test/references/external-contract-mock-guidance.md +0 -119
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
"""Focused tests for explicit reset modes and causal state recovery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from utils import causal_reset_selection
|
|
13
|
+
|
|
14
|
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
15
|
+
PIPELINE_ROOT = REPO_ROOT / "dev-pipeline"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _item(item_id, status="pending", dependencies=None, **extra):
|
|
19
|
+
return {
|
|
20
|
+
"id": item_id,
|
|
21
|
+
"title": item_id,
|
|
22
|
+
"status": status,
|
|
23
|
+
"dependencies": list(dependencies or []),
|
|
24
|
+
**extra,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.parametrize(
|
|
29
|
+
("prefix", "root_statuses"),
|
|
30
|
+
(
|
|
31
|
+
("F-", {"failed", "skipped"}),
|
|
32
|
+
("B-", {"failed", "skipped", "needs_info"}),
|
|
33
|
+
("R-", {"failed", "skipped"}),
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
def test_causal_selection_single_root_recovers_only_complete_chain(prefix, root_statuses):
|
|
37
|
+
items = [
|
|
38
|
+
_item(f"{prefix}001", "failed"),
|
|
39
|
+
_item(f"{prefix}002", "auto_skipped", [f"{prefix}001"]),
|
|
40
|
+
_item(f"{prefix}003", "auto_skipped", [f"{prefix}002"]),
|
|
41
|
+
_item(f"{prefix}004", "completed", [f"{prefix}001"], marker="preserve"),
|
|
42
|
+
_item(f"{prefix}005", "failed"),
|
|
43
|
+
_item(f"{prefix}006", "auto_skipped", [f"{prefix}005"]),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
selected = causal_reset_selection(
|
|
47
|
+
items,
|
|
48
|
+
selected_id=f"{prefix}001",
|
|
49
|
+
blocking_root_statuses=root_statuses,
|
|
50
|
+
item_label="Task",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
assert selected == (f"{prefix}001", f"{prefix}002", f"{prefix}003")
|
|
54
|
+
assert items[3]["status"] == "completed"
|
|
55
|
+
assert items[3]["marker"] == "preserve"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.parametrize("prefix", ("F-", "B-", "R-"))
|
|
59
|
+
def test_causal_selection_keeps_multi_root_descendant_blocked(prefix):
|
|
60
|
+
root_statuses = {"failed", "skipped", "needs_info"} if prefix == "B-" else {"failed", "skipped"}
|
|
61
|
+
items = [
|
|
62
|
+
_item(f"{prefix}001", "failed"),
|
|
63
|
+
_item(f"{prefix}002", "skipped"),
|
|
64
|
+
_item(f"{prefix}003", "auto_skipped", [f"{prefix}001"]),
|
|
65
|
+
_item(f"{prefix}004", "auto_skipped", [f"{prefix}003", f"{prefix}002"]),
|
|
66
|
+
_item(f"{prefix}005", "auto_skipped", [f"{prefix}002"]),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
selected = causal_reset_selection(
|
|
70
|
+
items,
|
|
71
|
+
selected_id=f"{prefix}001",
|
|
72
|
+
blocking_root_statuses=root_statuses,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
assert selected == (f"{prefix}001", f"{prefix}003")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_causal_selection_does_not_cross_an_orphan_auto_skipped_blocker():
|
|
79
|
+
items = [
|
|
80
|
+
_item("F-001", "failed"),
|
|
81
|
+
_item("F-002", "auto_skipped"),
|
|
82
|
+
_item("F-003", "auto_skipped", ["F-001", "F-002"]),
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
selected = causal_reset_selection(
|
|
86
|
+
items,
|
|
87
|
+
selected_id="F-001",
|
|
88
|
+
blocking_root_statuses={"failed", "skipped"},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
assert selected == ("F-001",)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.mark.parametrize("prefix", ("F-", "B-", "R-"))
|
|
95
|
+
def test_selecting_auto_skipped_leaf_includes_every_required_root(prefix):
|
|
96
|
+
root_statuses = {"failed", "skipped", "needs_info"} if prefix == "B-" else {"failed", "skipped"}
|
|
97
|
+
second_root_status = "needs_info" if prefix == "B-" else "skipped"
|
|
98
|
+
items = [
|
|
99
|
+
_item(f"{prefix}001", "failed"),
|
|
100
|
+
_item(f"{prefix}002", second_root_status),
|
|
101
|
+
_item(f"{prefix}003", "auto_skipped", [f"{prefix}001"]),
|
|
102
|
+
_item(f"{prefix}004", "auto_skipped", [f"{prefix}002"]),
|
|
103
|
+
_item(f"{prefix}005", "auto_skipped", [f"{prefix}003", f"{prefix}004"]),
|
|
104
|
+
_item(f"{prefix}006", "completed"),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
selected = causal_reset_selection(
|
|
108
|
+
items,
|
|
109
|
+
selected_id=f"{prefix}005",
|
|
110
|
+
blocking_root_statuses=root_statuses,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
assert selected == (
|
|
114
|
+
f"{prefix}001",
|
|
115
|
+
f"{prefix}002",
|
|
116
|
+
f"{prefix}003",
|
|
117
|
+
f"{prefix}004",
|
|
118
|
+
f"{prefix}005",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.parametrize(
|
|
123
|
+
("prefix", "root_statuses", "root_status"),
|
|
124
|
+
(
|
|
125
|
+
("F-", {"failed", "skipped"}, "skipped"),
|
|
126
|
+
("B-", {"failed", "skipped", "needs_info"}, "needs_info"),
|
|
127
|
+
("R-", {"failed", "skipped"}, "skipped"),
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
def test_causal_selection_all_recovers_every_supported_chain(prefix, root_statuses, root_status):
|
|
131
|
+
items = [
|
|
132
|
+
_item(f"{prefix}001", "failed"),
|
|
133
|
+
_item(f"{prefix}002", root_status),
|
|
134
|
+
_item(f"{prefix}003", "auto_skipped", [f"{prefix}001", f"{prefix}002"]),
|
|
135
|
+
_item(f"{prefix}004", "completed"),
|
|
136
|
+
_item(f"{prefix}005", "pending"),
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
selected = causal_reset_selection(
|
|
140
|
+
items,
|
|
141
|
+
selected_id=None,
|
|
142
|
+
blocking_root_statuses=root_statuses,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
assert selected == (f"{prefix}001", f"{prefix}002", f"{prefix}003")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_causal_selection_rejects_missing_invalid_and_orphan_targets():
|
|
149
|
+
items = [
|
|
150
|
+
_item("F-001", "failed"),
|
|
151
|
+
_item("F-002", "pending"),
|
|
152
|
+
_item("F-003", "auto_skipped"),
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
with pytest.raises(ValueError, match="not found"):
|
|
156
|
+
causal_reset_selection(items, selected_id="F-999", blocking_root_statuses={"failed", "skipped"})
|
|
157
|
+
with pytest.raises(ValueError, match="not recoverable"):
|
|
158
|
+
causal_reset_selection(items, selected_id="F-002", blocking_root_statuses={"failed", "skipped"})
|
|
159
|
+
with pytest.raises(ValueError, match="unresolved auto_skipped blocker"):
|
|
160
|
+
causal_reset_selection(items, selected_id="F-003", blocking_root_statuses={"failed", "skipped"})
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_causal_selection_rejects_invalid_dependency_graph_before_selection():
|
|
164
|
+
items = [
|
|
165
|
+
_item("F-001", "failed", ["F-002"]),
|
|
166
|
+
_item("F-002", "auto_skipped", ["F-001"]),
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
with pytest.raises(ValueError, match="Dependency cycle"):
|
|
170
|
+
causal_reset_selection(items, selected_id="F-001", blocking_root_statuses={"failed", "skipped"})
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
FAMILY_CONTRACTS = {
|
|
174
|
+
"feature": {
|
|
175
|
+
"prefix": "F-",
|
|
176
|
+
"list_key": "features",
|
|
177
|
+
"list_arg": "--feature-list",
|
|
178
|
+
"id_arg": "--feature-id",
|
|
179
|
+
"schema": "dev-pipeline-feature-list-v1",
|
|
180
|
+
"script": "update-feature-status.py",
|
|
181
|
+
"branch_prefix": "dev",
|
|
182
|
+
"artifact_root": "specs",
|
|
183
|
+
},
|
|
184
|
+
"bugfix": {
|
|
185
|
+
"prefix": "B-",
|
|
186
|
+
"list_key": "bugs",
|
|
187
|
+
"list_arg": "--bug-list",
|
|
188
|
+
"id_arg": "--bug-id",
|
|
189
|
+
"schema": "dev-pipeline-bug-fix-list-v1",
|
|
190
|
+
"script": "update-bug-status.py",
|
|
191
|
+
"branch_prefix": "bugfix",
|
|
192
|
+
"artifact_root": "bugfix",
|
|
193
|
+
},
|
|
194
|
+
"refactor": {
|
|
195
|
+
"prefix": "R-",
|
|
196
|
+
"list_key": "refactors",
|
|
197
|
+
"list_arg": "--refactor-list",
|
|
198
|
+
"id_arg": "--refactor-id",
|
|
199
|
+
"schema": "dev-pipeline-refactor-list-v1",
|
|
200
|
+
"script": "update-refactor-status.py",
|
|
201
|
+
"branch_prefix": "refactor",
|
|
202
|
+
"artifact_root": "refactor",
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _make_paths(tmp_path):
|
|
208
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
209
|
+
|
|
210
|
+
project_root = tmp_path / "project"
|
|
211
|
+
(project_root / ".prizmkit" / "plans").mkdir(parents=True)
|
|
212
|
+
return resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=project_root)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _family_and_plan(paths, kind, items):
|
|
216
|
+
from prizmkit_runtime.runner_models import family_for
|
|
217
|
+
|
|
218
|
+
family = family_for(kind, paths)
|
|
219
|
+
contract = FAMILY_CONTRACTS[kind]
|
|
220
|
+
family.plan_path.write_text(
|
|
221
|
+
json.dumps(
|
|
222
|
+
{
|
|
223
|
+
"$schema": contract["schema"],
|
|
224
|
+
"project_name": "Reset Test",
|
|
225
|
+
contract["list_key"]: items,
|
|
226
|
+
},
|
|
227
|
+
indent=2,
|
|
228
|
+
)
|
|
229
|
+
+ "\n",
|
|
230
|
+
encoding="utf-8",
|
|
231
|
+
)
|
|
232
|
+
return family, family.plan_path
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _write_runtime_status(family, item_id, **overrides):
|
|
236
|
+
status_dir = family.state_dir / item_id
|
|
237
|
+
status_dir.mkdir(parents=True, exist_ok=True)
|
|
238
|
+
status = {
|
|
239
|
+
f"{family.kind if family.kind != 'bugfix' else 'bug'}_id": item_id,
|
|
240
|
+
"retry_count": 4,
|
|
241
|
+
"infra_error_count": 3,
|
|
242
|
+
"last_infra_error_session_id": "old-session",
|
|
243
|
+
"sessions": ["old-session"],
|
|
244
|
+
"last_session_id": "old-session",
|
|
245
|
+
"last_failed_session_id": "old-session",
|
|
246
|
+
"resume_from_phase": 4,
|
|
247
|
+
"continuation_pending": True,
|
|
248
|
+
"continuation_reason": "context_overflow",
|
|
249
|
+
"continuation_count": 2,
|
|
250
|
+
"context_overflow_count": 2,
|
|
251
|
+
"previous_session_id": "old-session",
|
|
252
|
+
"custom_diagnostic": {"keep": True},
|
|
253
|
+
**overrides,
|
|
254
|
+
}
|
|
255
|
+
status_path = status_dir / "status.json"
|
|
256
|
+
status_path.write_text(json.dumps(status, indent=2) + "\n", encoding="utf-8")
|
|
257
|
+
return status_path, status
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _tree_snapshot(root):
|
|
261
|
+
if not root.exists():
|
|
262
|
+
return {}
|
|
263
|
+
return {
|
|
264
|
+
str(path.relative_to(root)): path.read_bytes()
|
|
265
|
+
for path in sorted(root.rglob("*"))
|
|
266
|
+
if path.is_file()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _git(project_root, *args):
|
|
271
|
+
return subprocess.run(
|
|
272
|
+
["git", *args],
|
|
273
|
+
cwd=project_root,
|
|
274
|
+
text=True,
|
|
275
|
+
stdout=subprocess.PIPE,
|
|
276
|
+
stderr=subprocess.PIPE,
|
|
277
|
+
check=True,
|
|
278
|
+
).stdout.strip()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _init_git_repository(project_root):
|
|
282
|
+
subprocess.run(
|
|
283
|
+
["git", "init", "-b", "main"],
|
|
284
|
+
cwd=project_root,
|
|
285
|
+
stdout=subprocess.DEVNULL,
|
|
286
|
+
check=True,
|
|
287
|
+
)
|
|
288
|
+
_git(project_root, "config", "user.email", "reset@example.com")
|
|
289
|
+
_git(project_root, "config", "user.name", "Reset Test")
|
|
290
|
+
source = project_root / "source.txt"
|
|
291
|
+
source.write_text("committed\n", encoding="utf-8")
|
|
292
|
+
_git(project_root, "add", "source.txt")
|
|
293
|
+
_git(project_root, "commit", "-m", "base")
|
|
294
|
+
source.write_text("committed\nlocal work\n", encoding="utf-8")
|
|
295
|
+
return source
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@pytest.mark.parametrize("kind", ("feature", "bugfix", "refactor"))
|
|
299
|
+
def test_state_only_resets_exact_causal_records_and_preserves_checkout_git_and_artifacts(tmp_path, kind):
|
|
300
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
301
|
+
from prizmkit_runtime.task_checkout import (
|
|
302
|
+
CHECKOUT_MODE_WORKTREE,
|
|
303
|
+
TaskCheckout,
|
|
304
|
+
persist_task_checkout,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
paths = _make_paths(tmp_path / kind)
|
|
308
|
+
contract = FAMILY_CONTRACTS[kind]
|
|
309
|
+
prefix = contract["prefix"]
|
|
310
|
+
root_id = f"{prefix}001"
|
|
311
|
+
child_id = f"{prefix}002"
|
|
312
|
+
other_root_id = f"{prefix}003"
|
|
313
|
+
shared_id = f"{prefix}004"
|
|
314
|
+
completed_id = f"{prefix}005"
|
|
315
|
+
unrelated_child_id = f"{prefix}006"
|
|
316
|
+
items = [
|
|
317
|
+
_item(root_id, "failed", marker="root-field"),
|
|
318
|
+
_item(child_id, "auto_skipped", [root_id], marker="child-field"),
|
|
319
|
+
_item(other_root_id, "failed", marker="other-root-field"),
|
|
320
|
+
_item(shared_id, "auto_skipped", [child_id, other_root_id], marker="shared-field"),
|
|
321
|
+
_item(completed_id, "completed", marker="completed-field"),
|
|
322
|
+
_item(unrelated_child_id, "auto_skipped", [other_root_id], marker="unrelated-field"),
|
|
323
|
+
]
|
|
324
|
+
family, plan_path = _family_and_plan(paths, kind, items)
|
|
325
|
+
source_path = _init_git_repository(paths.project_root)
|
|
326
|
+
branch_name = f"{contract['branch_prefix']}/{root_id}-preserved"
|
|
327
|
+
_git(paths.project_root, "branch", branch_name, "main")
|
|
328
|
+
|
|
329
|
+
worktree_path = paths.project_root / ".task-worktrees" / root_id
|
|
330
|
+
worktree_path.mkdir(parents=True)
|
|
331
|
+
worktree_marker = worktree_path / "keep.txt"
|
|
332
|
+
worktree_marker.write_text("worktree remains\n", encoding="utf-8")
|
|
333
|
+
checkout = TaskCheckout.active(
|
|
334
|
+
kind,
|
|
335
|
+
root_id,
|
|
336
|
+
branch_name,
|
|
337
|
+
"main",
|
|
338
|
+
checkout_mode=CHECKOUT_MODE_WORKTREE,
|
|
339
|
+
worktree_path=str(worktree_path),
|
|
340
|
+
)
|
|
341
|
+
persist_task_checkout(family.state_dir, checkout)
|
|
342
|
+
checkout_path = family.state_dir / root_id / "checkout.json"
|
|
343
|
+
|
|
344
|
+
root_status_path, _root_before = _write_runtime_status(
|
|
345
|
+
family,
|
|
346
|
+
root_id,
|
|
347
|
+
active_dev_branch=branch_name,
|
|
348
|
+
base_branch="main",
|
|
349
|
+
)
|
|
350
|
+
child_status_path, _child_before = _write_runtime_status(family, child_id)
|
|
351
|
+
other_status_path, _other_before = _write_runtime_status(family, other_root_id)
|
|
352
|
+
completed_status_path, _completed_before = _write_runtime_status(family, completed_id)
|
|
353
|
+
|
|
354
|
+
session_log = family.state_dir / root_id / "sessions" / "old-session" / "logs" / "session.log"
|
|
355
|
+
session_log.parent.mkdir(parents=True)
|
|
356
|
+
session_log.write_text("session history\n", encoding="utf-8")
|
|
357
|
+
artifact_name = "001-root" if kind == "feature" else root_id
|
|
358
|
+
artifact_dir = paths.project_root / ".prizmkit" / contract["artifact_root"] / artifact_name
|
|
359
|
+
artifact_dir.mkdir(parents=True)
|
|
360
|
+
checkpoint = artifact_dir / "workflow-checkpoint.json"
|
|
361
|
+
checkpoint.write_text('{"sentinel":"checkpoint"}\n', encoding="utf-8")
|
|
362
|
+
artifact = artifact_dir / "spec.md"
|
|
363
|
+
artifact.write_text("artifact remains\n", encoding="utf-8")
|
|
364
|
+
|
|
365
|
+
protected_bytes = {
|
|
366
|
+
"checkout": checkout_path.read_bytes(),
|
|
367
|
+
"session": session_log.read_bytes(),
|
|
368
|
+
"checkpoint": checkpoint.read_bytes(),
|
|
369
|
+
"artifact": artifact.read_bytes(),
|
|
370
|
+
"worktree": worktree_marker.read_bytes(),
|
|
371
|
+
"other_status": other_status_path.read_bytes(),
|
|
372
|
+
"completed_status": completed_status_path.read_bytes(),
|
|
373
|
+
"source": source_path.read_bytes(),
|
|
374
|
+
}
|
|
375
|
+
branch_refs_before = _git(paths.project_root, "show-ref", "--heads")
|
|
376
|
+
branch_before = _git(paths.project_root, "branch", "--show-current")
|
|
377
|
+
source_diff_before = _git(paths.project_root, "diff", "--", "source.txt")
|
|
378
|
+
|
|
379
|
+
result = run_reset_command(
|
|
380
|
+
kind,
|
|
381
|
+
(root_id, "--state-only", str(plan_path)),
|
|
382
|
+
paths,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
assert result.exit_code == 0, result.render()
|
|
386
|
+
payload = json.loads(result.render())
|
|
387
|
+
assert payload["action"] == "reset_state"
|
|
388
|
+
assert payload["reset_count"] == 2
|
|
389
|
+
updated = json.loads(plan_path.read_text(encoding="utf-8"))[contract["list_key"]]
|
|
390
|
+
statuses = {item["id"]: item["status"] for item in updated}
|
|
391
|
+
assert statuses == {
|
|
392
|
+
root_id: "pending",
|
|
393
|
+
child_id: "pending",
|
|
394
|
+
other_root_id: "failed",
|
|
395
|
+
shared_id: "auto_skipped",
|
|
396
|
+
completed_id: "completed",
|
|
397
|
+
unrelated_child_id: "auto_skipped",
|
|
398
|
+
}
|
|
399
|
+
assert {item["id"]: item["marker"] for item in updated} == {
|
|
400
|
+
root_id: "root-field",
|
|
401
|
+
child_id: "child-field",
|
|
402
|
+
other_root_id: "other-root-field",
|
|
403
|
+
shared_id: "shared-field",
|
|
404
|
+
completed_id: "completed-field",
|
|
405
|
+
unrelated_child_id: "unrelated-field",
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for selected_status_path in (root_status_path, child_status_path):
|
|
409
|
+
selected_status = json.loads(selected_status_path.read_text(encoding="utf-8"))
|
|
410
|
+
assert selected_status["retry_count"] == 0
|
|
411
|
+
assert selected_status["infra_error_count"] == 0
|
|
412
|
+
assert selected_status["last_infra_error_session_id"] is None
|
|
413
|
+
assert selected_status["sessions"] == []
|
|
414
|
+
assert selected_status["last_session_id"] is None
|
|
415
|
+
assert selected_status["last_failed_session_id"] is None
|
|
416
|
+
assert selected_status["resume_from_phase"] is None
|
|
417
|
+
assert selected_status["custom_diagnostic"] == {"keep": True}
|
|
418
|
+
for continuation_key in (
|
|
419
|
+
"continuation_pending",
|
|
420
|
+
"continuation_reason",
|
|
421
|
+
"continuation_count",
|
|
422
|
+
"context_overflow_count",
|
|
423
|
+
"previous_session_id",
|
|
424
|
+
):
|
|
425
|
+
assert continuation_key not in selected_status
|
|
426
|
+
|
|
427
|
+
root_status = json.loads(root_status_path.read_text(encoding="utf-8"))
|
|
428
|
+
assert root_status["active_dev_branch"] == branch_name
|
|
429
|
+
assert root_status["base_branch"] == "main"
|
|
430
|
+
assert checkout_path.read_bytes() == protected_bytes["checkout"]
|
|
431
|
+
assert session_log.read_bytes() == protected_bytes["session"]
|
|
432
|
+
assert checkpoint.read_bytes() == protected_bytes["checkpoint"]
|
|
433
|
+
assert artifact.read_bytes() == protected_bytes["artifact"]
|
|
434
|
+
assert worktree_marker.read_bytes() == protected_bytes["worktree"]
|
|
435
|
+
assert other_status_path.read_bytes() == protected_bytes["other_status"]
|
|
436
|
+
assert completed_status_path.read_bytes() == protected_bytes["completed_status"]
|
|
437
|
+
assert source_path.read_bytes() == protected_bytes["source"]
|
|
438
|
+
assert _git(paths.project_root, "show-ref", "--heads") == branch_refs_before
|
|
439
|
+
assert _git(paths.project_root, "branch", "--show-current") == branch_before
|
|
440
|
+
assert _git(paths.project_root, "diff", "--", "source.txt") == source_diff_before
|
|
441
|
+
assert not (family.state_dir / "pipeline.json").exists()
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@pytest.mark.parametrize(
|
|
445
|
+
("kind", "second_root_status"),
|
|
446
|
+
(("feature", "skipped"), ("bugfix", "needs_info"), ("refactor", "skipped")),
|
|
447
|
+
)
|
|
448
|
+
def test_state_only_all_recovers_all_supported_chains_without_pipeline_state(tmp_path, kind, second_root_status):
|
|
449
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
450
|
+
|
|
451
|
+
paths = _make_paths(tmp_path / kind)
|
|
452
|
+
contract = FAMILY_CONTRACTS[kind]
|
|
453
|
+
prefix = contract["prefix"]
|
|
454
|
+
items = [
|
|
455
|
+
_item(f"{prefix}001", "failed"),
|
|
456
|
+
_item(f"{prefix}002", second_root_status),
|
|
457
|
+
_item(f"{prefix}003", "auto_skipped", [f"{prefix}001", f"{prefix}002"]),
|
|
458
|
+
_item(f"{prefix}004", "completed", marker="done"),
|
|
459
|
+
_item(f"{prefix}005", "pending", marker="unrelated"),
|
|
460
|
+
]
|
|
461
|
+
family, plan_path = _family_and_plan(paths, kind, items)
|
|
462
|
+
completed_status_path, _ = _write_runtime_status(family, f"{prefix}004")
|
|
463
|
+
completed_before = completed_status_path.read_bytes()
|
|
464
|
+
|
|
465
|
+
result = run_reset_command(
|
|
466
|
+
kind,
|
|
467
|
+
("--state-only", "--all", str(plan_path)),
|
|
468
|
+
paths,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
assert result.exit_code == 0, result.render()
|
|
472
|
+
payload = json.loads(result.render())
|
|
473
|
+
assert payload["action"] == "reset_state"
|
|
474
|
+
assert payload["reset_count"] == 3
|
|
475
|
+
updated = json.loads(plan_path.read_text(encoding="utf-8"))[contract["list_key"]]
|
|
476
|
+
assert [item["status"] for item in updated] == [
|
|
477
|
+
"pending",
|
|
478
|
+
"pending",
|
|
479
|
+
"pending",
|
|
480
|
+
"completed",
|
|
481
|
+
"pending",
|
|
482
|
+
]
|
|
483
|
+
assert completed_status_path.read_bytes() == completed_before
|
|
484
|
+
assert not (family.state_dir / "pipeline.json").exists()
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@pytest.mark.parametrize("kind", ("feature", "bugfix", "refactor"))
|
|
488
|
+
def test_reset_without_mode_fails_before_any_mutation(tmp_path, kind):
|
|
489
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
490
|
+
|
|
491
|
+
paths = _make_paths(tmp_path / kind)
|
|
492
|
+
prefix = FAMILY_CONTRACTS[kind]["prefix"]
|
|
493
|
+
family, plan_path = _family_and_plan(paths, kind, [_item(f"{prefix}001", "failed")])
|
|
494
|
+
marker = family.state_dir / "sentinel.bin"
|
|
495
|
+
marker.parent.mkdir(parents=True)
|
|
496
|
+
marker.write_bytes(b"runtime-sentinel")
|
|
497
|
+
before_plan = plan_path.read_bytes()
|
|
498
|
+
before_state = _tree_snapshot(family.state_dir)
|
|
499
|
+
|
|
500
|
+
result = run_reset_command(kind, (f"{prefix}001", str(plan_path)), paths)
|
|
501
|
+
|
|
502
|
+
assert result.exit_code != 0
|
|
503
|
+
assert "Exactly one reset mode is required" in result.render()
|
|
504
|
+
assert "--state-only" in result.render()
|
|
505
|
+
assert plan_path.read_bytes() == before_plan
|
|
506
|
+
assert _tree_snapshot(family.state_dir) == before_state
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@pytest.mark.parametrize(
|
|
510
|
+
"mode_pair",
|
|
511
|
+
(
|
|
512
|
+
("--state-only", "--fresh-checkout"),
|
|
513
|
+
("--state-only", "--clean"),
|
|
514
|
+
("--state-only", "--preserve-runtime"),
|
|
515
|
+
("--fresh-checkout", "--clean"),
|
|
516
|
+
("--fresh-checkout", "--preserve-runtime"),
|
|
517
|
+
("--clean", "--preserve-runtime"),
|
|
518
|
+
),
|
|
519
|
+
)
|
|
520
|
+
def test_conflicting_reset_modes_fail_before_mutation(tmp_path, mode_pair):
|
|
521
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
522
|
+
|
|
523
|
+
paths = _make_paths(tmp_path)
|
|
524
|
+
family, plan_path = _family_and_plan(paths, "feature", [_item("F-001", "failed")])
|
|
525
|
+
marker = family.state_dir / "sentinel.bin"
|
|
526
|
+
marker.parent.mkdir(parents=True)
|
|
527
|
+
marker.write_bytes(b"runtime-sentinel")
|
|
528
|
+
before_plan = plan_path.read_bytes()
|
|
529
|
+
before_state = _tree_snapshot(family.state_dir)
|
|
530
|
+
|
|
531
|
+
result = run_reset_command(
|
|
532
|
+
"feature",
|
|
533
|
+
("F-001", *mode_pair, "--failed", str(plan_path)),
|
|
534
|
+
paths,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
assert result.exit_code != 0
|
|
538
|
+
assert "Exactly one reset mode is required" in result.render()
|
|
539
|
+
assert plan_path.read_bytes() == before_plan
|
|
540
|
+
assert _tree_snapshot(family.state_dir) == before_state
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
@pytest.mark.parametrize(
|
|
544
|
+
("args", "message"),
|
|
545
|
+
(
|
|
546
|
+
(("F-001", "--standard"), "Unsupported reset arguments"),
|
|
547
|
+
(("--state-only",), "requires exactly one item ID or explicit --all"),
|
|
548
|
+
(("F-001", "--state-only", "--all"), "requires exactly one item ID or explicit --all"),
|
|
549
|
+
(("F-001:F-002", "--state-only"), "ranges and status filters are unsupported"),
|
|
550
|
+
(("--failed", "--state-only"), "ranges and status filters are unsupported"),
|
|
551
|
+
(("--fresh-checkout",), "requires exactly one item ID, range, or supported status filter"),
|
|
552
|
+
(("--fresh-checkout", "--all"), "--all is supported only with --state-only"),
|
|
553
|
+
(("--clean",), "requires exactly one item ID, range, or supported status filter"),
|
|
554
|
+
(("--clean", "--all"), "--all is supported only with --state-only"),
|
|
555
|
+
(("--preserve-runtime",), "requires --failed"),
|
|
556
|
+
(("F-001", "--failed", "--preserve-runtime"), "cannot target an ID, range, or --all"),
|
|
557
|
+
(("--failed", "--preserve-runtime", "--all"), "cannot target an ID, range, or --all"),
|
|
558
|
+
),
|
|
559
|
+
)
|
|
560
|
+
def test_reset_selector_matrix_rejects_unsupported_forms_before_mutation(tmp_path, args, message):
|
|
561
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
562
|
+
|
|
563
|
+
paths = _make_paths(tmp_path)
|
|
564
|
+
family, plan_path = _family_and_plan(
|
|
565
|
+
paths,
|
|
566
|
+
"feature",
|
|
567
|
+
[_item("F-001", "failed"), _item("F-002", "auto_skipped", ["F-001"])],
|
|
568
|
+
)
|
|
569
|
+
marker = family.state_dir / "sentinel.bin"
|
|
570
|
+
marker.parent.mkdir(parents=True)
|
|
571
|
+
marker.write_bytes(b"runtime-sentinel")
|
|
572
|
+
before_plan = plan_path.read_bytes()
|
|
573
|
+
before_state = _tree_snapshot(family.state_dir)
|
|
574
|
+
|
|
575
|
+
result = run_reset_command("feature", (*args, str(plan_path)), paths)
|
|
576
|
+
|
|
577
|
+
assert result.exit_code != 0
|
|
578
|
+
assert message in result.render()
|
|
579
|
+
assert plan_path.read_bytes() == before_plan
|
|
580
|
+
assert _tree_snapshot(family.state_dir) == before_state
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
@pytest.mark.parametrize(
|
|
584
|
+
("items", "args", "message"),
|
|
585
|
+
(
|
|
586
|
+
([_item("F-001", "failed")], ("F-999", "--state-only"), "not found"),
|
|
587
|
+
([_item("F-001", "pending")], ("F-001", "--state-only"), "not recoverable"),
|
|
588
|
+
([_item("F-001", "auto_skipped")], ("F-001", "--state-only"), "unresolved auto_skipped blocker"),
|
|
589
|
+
([_item("F-001", "completed")], ("--state-only", "--all"), "No recoverable blocking roots"),
|
|
590
|
+
(
|
|
591
|
+
[_item("F-001", "failed", ["F-002"]), _item("F-002", "auto_skipped", ["F-001"])],
|
|
592
|
+
("F-001", "--state-only"),
|
|
593
|
+
"Dependency cycle",
|
|
594
|
+
),
|
|
595
|
+
),
|
|
596
|
+
)
|
|
597
|
+
def test_state_only_invalid_target_or_graph_is_fail_closed(tmp_path, items, args, message):
|
|
598
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
599
|
+
|
|
600
|
+
paths = _make_paths(tmp_path)
|
|
601
|
+
family, plan_path = _family_and_plan(paths, "feature", items)
|
|
602
|
+
marker = family.state_dir / "sentinel.bin"
|
|
603
|
+
marker.parent.mkdir(parents=True)
|
|
604
|
+
marker.write_bytes(b"runtime-sentinel")
|
|
605
|
+
before_plan = plan_path.read_bytes()
|
|
606
|
+
before_state = _tree_snapshot(family.state_dir)
|
|
607
|
+
|
|
608
|
+
result = run_reset_command("feature", (*args, str(plan_path)), paths)
|
|
609
|
+
|
|
610
|
+
assert result.exit_code != 0
|
|
611
|
+
assert message in result.render()
|
|
612
|
+
assert plan_path.read_bytes() == before_plan
|
|
613
|
+
assert _tree_snapshot(family.state_dir) == before_state
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
@pytest.mark.parametrize("kind", ("feature", "bugfix", "refactor"))
|
|
617
|
+
@pytest.mark.parametrize("mode", ("--fresh-checkout", "--clean"))
|
|
618
|
+
@pytest.mark.parametrize(
|
|
619
|
+
("selector", "expected_numbers"),
|
|
620
|
+
(
|
|
621
|
+
(("X-003",), (3,)),
|
|
622
|
+
(("X-001:X-002",), (1, 2)),
|
|
623
|
+
(("--failed",), (1,)),
|
|
624
|
+
),
|
|
625
|
+
)
|
|
626
|
+
def test_fresh_checkout_and_clean_keep_id_range_and_filter_selection(
|
|
627
|
+
monkeypatch,
|
|
628
|
+
tmp_path,
|
|
629
|
+
kind,
|
|
630
|
+
mode,
|
|
631
|
+
selector,
|
|
632
|
+
expected_numbers,
|
|
633
|
+
):
|
|
634
|
+
from prizmkit_runtime import reset as reset_runtime
|
|
635
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
636
|
+
|
|
637
|
+
paths = _make_paths(tmp_path)
|
|
638
|
+
prefix = FAMILY_CONTRACTS[kind]["prefix"]
|
|
639
|
+
normalized_selector = tuple(value.replace("X-", prefix) for value in selector)
|
|
640
|
+
family, plan_path = _family_and_plan(
|
|
641
|
+
paths,
|
|
642
|
+
kind,
|
|
643
|
+
[
|
|
644
|
+
_item(f"{prefix}001", "failed"),
|
|
645
|
+
_item(f"{prefix}002", "auto_skipped", [f"{prefix}001"]),
|
|
646
|
+
_item(f"{prefix}003", "pending"),
|
|
647
|
+
],
|
|
648
|
+
)
|
|
649
|
+
family.state_dir.mkdir(parents=True, exist_ok=True)
|
|
650
|
+
(family.state_dir / "pipeline.json").write_text("{}\n", encoding="utf-8")
|
|
651
|
+
captured = []
|
|
652
|
+
|
|
653
|
+
def fake_reset_one(received_family, item, options, project_root, return_branch, lines):
|
|
654
|
+
captured.append((received_family.kind, item.item_id, options.fresh_checkout, options.clean))
|
|
655
|
+
return True
|
|
656
|
+
|
|
657
|
+
monkeypatch.setattr(reset_runtime, "_reset_one", fake_reset_one)
|
|
658
|
+
monkeypatch.setattr(reset_runtime, "default_branch", lambda _root: "main")
|
|
659
|
+
|
|
660
|
+
result = run_reset_command(
|
|
661
|
+
kind,
|
|
662
|
+
(*normalized_selector, mode, str(plan_path)),
|
|
663
|
+
paths,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
assert result.exit_code == 0, result.render()
|
|
667
|
+
expected_ids = [f"{prefix}{number:03d}" for number in expected_numbers]
|
|
668
|
+
assert [entry[1] for entry in captured] == expected_ids
|
|
669
|
+
assert all(entry[2] is (mode == "--fresh-checkout") for entry in captured)
|
|
670
|
+
assert all(entry[3] is (mode == "--clean") for entry in captured)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _valid_bug(bug_id, status="pending", dependencies=None):
|
|
674
|
+
return {
|
|
675
|
+
"id": bug_id,
|
|
676
|
+
"title": bug_id,
|
|
677
|
+
"description": "Bug description",
|
|
678
|
+
"severity": "high",
|
|
679
|
+
"error_source": {"type": "failed_test", "content": "failure"},
|
|
680
|
+
"verification_type": "automated",
|
|
681
|
+
"acceptance_criteria": ["Regression is fixed"],
|
|
682
|
+
"status": status,
|
|
683
|
+
"dependencies": list(dependencies or []),
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _run_bug_updater(plan_path, state_dir, *args):
|
|
688
|
+
return subprocess.run(
|
|
689
|
+
[
|
|
690
|
+
sys.executable,
|
|
691
|
+
str(PIPELINE_ROOT / "scripts" / "update-bug-status.py"),
|
|
692
|
+
"--bug-list",
|
|
693
|
+
str(plan_path),
|
|
694
|
+
"--state-dir",
|
|
695
|
+
str(state_dir),
|
|
696
|
+
"--project-root",
|
|
697
|
+
str(plan_path.parent),
|
|
698
|
+
*args,
|
|
699
|
+
],
|
|
700
|
+
cwd=REPO_ROOT,
|
|
701
|
+
text=True,
|
|
702
|
+
stdout=subprocess.PIPE,
|
|
703
|
+
stderr=subprocess.PIPE,
|
|
704
|
+
check=False,
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
@pytest.mark.parametrize(
|
|
709
|
+
("session_status", "extra_args", "expected_root_status"),
|
|
710
|
+
(
|
|
711
|
+
("failed", ("--max-retries", "1"), "failed"),
|
|
712
|
+
("workflow_skipped", (), "skipped"),
|
|
713
|
+
("workflow_blocked", (), "needs_info"),
|
|
714
|
+
),
|
|
715
|
+
)
|
|
716
|
+
def test_bugfix_terminal_roots_auto_skip_pending_descendants(
|
|
717
|
+
tmp_path,
|
|
718
|
+
session_status,
|
|
719
|
+
extra_args,
|
|
720
|
+
expected_root_status,
|
|
721
|
+
):
|
|
722
|
+
plan_path = tmp_path / "bug-fix-list.json"
|
|
723
|
+
state_dir = tmp_path / "state"
|
|
724
|
+
completed_status = state_dir / "B-004" / "status.json"
|
|
725
|
+
completed_status.parent.mkdir(parents=True)
|
|
726
|
+
completed_status.write_text('{"custom":"unchanged"}\n', encoding="utf-8")
|
|
727
|
+
completed_before = completed_status.read_bytes()
|
|
728
|
+
plan_path.write_text(
|
|
729
|
+
json.dumps(
|
|
730
|
+
{
|
|
731
|
+
"$schema": "dev-pipeline-bug-fix-list-v1",
|
|
732
|
+
"project_name": "Bug Cascade",
|
|
733
|
+
"bugs": [
|
|
734
|
+
_valid_bug("B-001", "in_progress"),
|
|
735
|
+
_valid_bug("B-002", "pending", ["B-001"]),
|
|
736
|
+
_valid_bug("B-003", "pending", ["B-002"]),
|
|
737
|
+
_valid_bug("B-004", "completed"),
|
|
738
|
+
],
|
|
739
|
+
}
|
|
740
|
+
),
|
|
741
|
+
encoding="utf-8",
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
result = _run_bug_updater(
|
|
745
|
+
plan_path,
|
|
746
|
+
state_dir,
|
|
747
|
+
"--action",
|
|
748
|
+
"update",
|
|
749
|
+
"--bug-id",
|
|
750
|
+
"B-001",
|
|
751
|
+
"--session-status",
|
|
752
|
+
session_status,
|
|
753
|
+
*extra_args,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
assert result.returncode == 0, (result.stdout, result.stderr)
|
|
757
|
+
summary = json.loads(result.stdout)
|
|
758
|
+
assert summary["auto_skipped"] == ["B-002", "B-003"]
|
|
759
|
+
statuses = {
|
|
760
|
+
bug["id"]: bug["status"]
|
|
761
|
+
for bug in json.loads(plan_path.read_text(encoding="utf-8"))["bugs"]
|
|
762
|
+
}
|
|
763
|
+
assert statuses == {
|
|
764
|
+
"B-001": expected_root_status,
|
|
765
|
+
"B-002": "auto_skipped",
|
|
766
|
+
"B-003": "auto_skipped",
|
|
767
|
+
"B-004": "completed",
|
|
768
|
+
}
|
|
769
|
+
assert completed_status.read_bytes() == completed_before
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def test_existing_bugfix_list_lazily_normalizes_blocked_pending_descendants(tmp_path):
|
|
773
|
+
plan_path = tmp_path / "bug-fix-list.json"
|
|
774
|
+
state_dir = tmp_path / "state"
|
|
775
|
+
data = {
|
|
776
|
+
"$schema": "dev-pipeline-bug-fix-list-v1",
|
|
777
|
+
"project_name": "Legacy Bug List",
|
|
778
|
+
"bugs": [
|
|
779
|
+
_valid_bug("B-001", "failed"),
|
|
780
|
+
_valid_bug("B-002", "pending", ["B-001"]),
|
|
781
|
+
_valid_bug("B-003", "pending", ["B-002"]),
|
|
782
|
+
_valid_bug("B-004", "completed"),
|
|
783
|
+
],
|
|
784
|
+
}
|
|
785
|
+
plan_path.write_text(json.dumps(data), encoding="utf-8")
|
|
786
|
+
|
|
787
|
+
result = _run_bug_updater(plan_path, state_dir, "--action", "get_next")
|
|
788
|
+
|
|
789
|
+
assert result.returncode == 0, (result.stdout, result.stderr)
|
|
790
|
+
assert result.stdout.strip() == "PIPELINE_COMPLETE"
|
|
791
|
+
updated = json.loads(plan_path.read_text(encoding="utf-8"))
|
|
792
|
+
statuses = {bug["id"]: bug["status"] for bug in updated["bugs"]}
|
|
793
|
+
assert statuses == {
|
|
794
|
+
"B-001": "failed",
|
|
795
|
+
"B-002": "auto_skipped",
|
|
796
|
+
"B-003": "auto_skipped",
|
|
797
|
+
"B-004": "completed",
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
status_result = _run_bug_updater(plan_path, state_dir, "--action", "status")
|
|
801
|
+
assert status_result.returncode == 0
|
|
802
|
+
assert "Auto-skipped: 2" in status_result.stdout
|
|
803
|
+
assert "[⊘]" in status_result.stdout
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def test_bugfix_auto_skip_write_failure_is_nonzero_without_partial_state(monkeypatch, tmp_path, capsys):
|
|
807
|
+
plan_path = tmp_path / "bug-fix-list.json"
|
|
808
|
+
state_dir = tmp_path / "state"
|
|
809
|
+
data = {
|
|
810
|
+
"$schema": "dev-pipeline-bug-fix-list-v1",
|
|
811
|
+
"project_name": "Write Failure",
|
|
812
|
+
"bugs": [
|
|
813
|
+
_valid_bug("B-001", "failed"),
|
|
814
|
+
_valid_bug("B-002", "pending", ["B-001"]),
|
|
815
|
+
],
|
|
816
|
+
}
|
|
817
|
+
plan_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
818
|
+
before = plan_path.read_bytes()
|
|
819
|
+
module = _load_source_module(
|
|
820
|
+
"f060_update_bug_status_write_failure",
|
|
821
|
+
PIPELINE_ROOT / "scripts" / "update-bug-status.py",
|
|
822
|
+
)
|
|
823
|
+
monkeypatch.setattr(module, "write_json_file", lambda *_args, **_kwargs: "permission denied")
|
|
824
|
+
|
|
825
|
+
with pytest.raises(SystemExit):
|
|
826
|
+
module.auto_skip_blocked_bugs(str(plan_path), str(state_dir), "B-001")
|
|
827
|
+
|
|
828
|
+
output = capsys.readouterr().out
|
|
829
|
+
assert "Failed to write auto-skipped bugs" in output
|
|
830
|
+
assert plan_path.read_bytes() == before
|
|
831
|
+
assert _tree_snapshot(state_dir) == {}
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _load_source_module(name, path):
|
|
835
|
+
import importlib.util
|
|
836
|
+
|
|
837
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
838
|
+
module = importlib.util.module_from_spec(spec)
|
|
839
|
+
assert spec.loader is not None
|
|
840
|
+
spec.loader.exec_module(module)
|
|
841
|
+
return module
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def test_bugfix_auto_skipped_schema_validators_and_aggregation_are_consistent():
|
|
845
|
+
from prizmkit_runtime.daemon import PROGRESS_COUNTS_BY_KIND
|
|
846
|
+
from prizmkit_runtime.runner_models import family_for
|
|
847
|
+
from prizmkit_runtime.runners import _completion_compatible_statuses
|
|
848
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
849
|
+
|
|
850
|
+
schema = json.loads(
|
|
851
|
+
(PIPELINE_ROOT / "templates" / "bug-fix-list-schema.json").read_text(encoding="utf-8")
|
|
852
|
+
)
|
|
853
|
+
status_enum = schema["properties"]["bugs"]["items"]["properties"]["status"]["enum"]
|
|
854
|
+
assert "auto_skipped" in status_enum
|
|
855
|
+
|
|
856
|
+
init_module = _load_source_module(
|
|
857
|
+
"f060_init_bugfix",
|
|
858
|
+
PIPELINE_ROOT / "scripts" / "init-bugfix-pipeline.py",
|
|
859
|
+
)
|
|
860
|
+
planner_module = _load_source_module(
|
|
861
|
+
"f060_validate_bug_list",
|
|
862
|
+
REPO_ROOT
|
|
863
|
+
/ "core"
|
|
864
|
+
/ "skills"
|
|
865
|
+
/ "orchestration-skill"
|
|
866
|
+
/ "pipelines"
|
|
867
|
+
/ "bug-planner"
|
|
868
|
+
/ "scripts"
|
|
869
|
+
/ "validate-bug-list.py",
|
|
870
|
+
)
|
|
871
|
+
assert "auto_skipped" in init_module.VALID_STATUSES
|
|
872
|
+
assert "auto_skipped" in init_module.TERMINAL_STATUSES
|
|
873
|
+
assert "auto_skipped" in planner_module.VALID_STATUSES
|
|
874
|
+
|
|
875
|
+
bugs = [
|
|
876
|
+
_valid_bug("B-001", "failed"),
|
|
877
|
+
_valid_bug("B-002", "pending", ["B-001"]),
|
|
878
|
+
_valid_bug("B-003", "auto_skipped", ["B-001"]),
|
|
879
|
+
]
|
|
880
|
+
init_errors, _seen = init_module.validate_bugs(bugs)
|
|
881
|
+
assert init_errors == []
|
|
882
|
+
planner_result = planner_module.validate(
|
|
883
|
+
{
|
|
884
|
+
"$schema": "dev-pipeline-bug-fix-list-v1",
|
|
885
|
+
"project_name": "Validator Test",
|
|
886
|
+
"bugs": bugs,
|
|
887
|
+
}
|
|
888
|
+
)
|
|
889
|
+
assert planner_result["valid"] is True, planner_result["errors"]
|
|
890
|
+
|
|
891
|
+
assert "auto_skipped" in PROGRESS_COUNTS_BY_KIND["bugfix"]
|
|
892
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=REPO_ROOT)
|
|
893
|
+
bug_family = family_for("bugfix", paths)
|
|
894
|
+
assert "auto_skipped" in _completion_compatible_statuses(bug_family)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def test_all_updaters_expose_only_reset_state_internal_recovery_action():
|
|
898
|
+
retired_action = "un" + "skip"
|
|
899
|
+
for contract in FAMILY_CONTRACTS.values():
|
|
900
|
+
source = (PIPELINE_ROOT / "scripts" / contract["script"]).read_text(encoding="utf-8")
|
|
901
|
+
assert retired_action not in source
|
|
902
|
+
assert '"reset_state"' in source
|
|
903
|
+
assert '"action": "reset_state"' in source
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def test_canonical_reset_guidance_documents_every_explicit_mode_without_generated_outputs():
|
|
907
|
+
launcher_root = (
|
|
908
|
+
REPO_ROOT
|
|
909
|
+
/ "core"
|
|
910
|
+
/ "skills"
|
|
911
|
+
/ "orchestration-skill"
|
|
912
|
+
/ "pipelines"
|
|
913
|
+
)
|
|
914
|
+
launchers = {
|
|
915
|
+
"feature-pipeline-launcher": ("feature", "F-001"),
|
|
916
|
+
"bugfix-pipeline-launcher": ("bugfix", "B-001"),
|
|
917
|
+
"refactor-pipeline-launcher": ("refactor", "R-001"),
|
|
918
|
+
}
|
|
919
|
+
for launcher, (family, item_id) in launchers.items():
|
|
920
|
+
source = (launcher_root / launcher / "SKILL.md").read_text(encoding="utf-8")
|
|
921
|
+
assert f"reset {family} {item_id} --state-only" in source
|
|
922
|
+
assert f"reset {family} {item_id} --fresh-checkout" in source
|
|
923
|
+
assert f"reset {family} {item_id} --clean" in source
|
|
924
|
+
assert f"reset {family} --failed --preserve-runtime" in source
|
|
925
|
+
assert "exactly one mode" in source.lower()
|
|
926
|
+
|
|
927
|
+
memory_source = (REPO_ROOT / "core" / "templates" / "project-memory-template.md").read_text(
|
|
928
|
+
encoding="utf-8"
|
|
929
|
+
)
|
|
930
|
+
assert "reset <family> <task-id> --state-only" in memory_source
|
|
931
|
+
assert "reset <family> --state-only --all" in memory_source
|
|
932
|
+
assert "reset <family> <task-id-or-range> --fresh-checkout" in memory_source
|
|
933
|
+
assert "reset <family> <task-id-or-range> --clean" in memory_source
|
|
934
|
+
assert "reset <family> --failed --preserve-runtime" in memory_source
|
|
935
|
+
assert "<family> " + "un" + "skip" not in memory_source
|