prizmkit 1.1.148 → 1.1.150
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 +20 -17
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +77 -0
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +234 -39
- package/bundled/dev-pipeline/prizmkit_runtime/reset.py +57 -30
- package/bundled/dev-pipeline/prizmkit_runtime/reset_preserve.py +822 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +28 -11
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +71 -28
- package/bundled/dev-pipeline/prizmkit_runtime/task_checkout.py +18 -0
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +67 -13
- package/bundled/dev-pipeline/tests/test_reset_preserve.py +959 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +325 -5
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/app-planner/SKILL.md +27 -27
- package/bundled/skills/app-planner/references/architecture-decisions.md +5 -11
- package/bundled/skills/app-planner/references/frontend-design-guide.md +1 -1
- package/bundled/skills/app-planner/references/generated-plan-review.md +2 -2
- package/bundled/skills/app-planner/references/infrastructure-convention-discovery.md +2 -2
- package/bundled/skills/app-planner/references/project-conventions-discovery.md +1 -1
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -9
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +21 -9
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +21 -9
- package/bundled/templates/project-memory-template.md +38 -0
- package/package.json +1 -1
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
"""Regression tests for non-destructive failed-chain reset behavior."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shlex
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
12
|
+
PIPELINE_ROOT = REPO_ROOT / "dev-pipeline"
|
|
13
|
+
CLI_PATH = PIPELINE_ROOT / "cli.py"
|
|
14
|
+
if str(PIPELINE_ROOT) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(PIPELINE_ROOT))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _make_paths(tmp_path):
|
|
19
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
20
|
+
|
|
21
|
+
project = tmp_path / "project"
|
|
22
|
+
(project / ".prizmkit" / "plans").mkdir(parents=True)
|
|
23
|
+
return resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=project)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _write_family_plan(paths, kind, items):
|
|
27
|
+
plan_config = {
|
|
28
|
+
"feature": (paths.feature_plan, "features", "dev-pipeline-feature-list-v1"),
|
|
29
|
+
"bugfix": (paths.bugfix_plan, "bugs", "dev-pipeline-bug-fix-list-v1"),
|
|
30
|
+
"refactor": (paths.refactor_plan, "refactors", "dev-pipeline-refactor-list-v1"),
|
|
31
|
+
}
|
|
32
|
+
plan_path, list_key, schema = plan_config[kind]
|
|
33
|
+
plan_path.write_text(
|
|
34
|
+
json.dumps({"$schema": schema, "project_name": "Fake", list_key: items}),
|
|
35
|
+
encoding="utf-8",
|
|
36
|
+
)
|
|
37
|
+
return plan_path
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _write_semantic_completion_checkpoint(project_root, artifact_dir, workflow_type="feature-pipeline"):
|
|
41
|
+
contracts = {
|
|
42
|
+
"feature-pipeline": (
|
|
43
|
+
"feature_state",
|
|
44
|
+
[
|
|
45
|
+
"prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
|
|
46
|
+
"prizmkit-retrospective", "prizmkit-committer", "completion-summary",
|
|
47
|
+
],
|
|
48
|
+
),
|
|
49
|
+
"bugfix-pipeline": (
|
|
50
|
+
"bugfix_state",
|
|
51
|
+
[
|
|
52
|
+
"prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
|
|
53
|
+
"prizmkit-retrospective", "prizmkit-committer", "bug-report",
|
|
54
|
+
],
|
|
55
|
+
),
|
|
56
|
+
"refactor-pipeline": (
|
|
57
|
+
"refactor_state",
|
|
58
|
+
[
|
|
59
|
+
"prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
|
|
60
|
+
"prizmkit-retrospective", "prizmkit-committer", "refactor-report",
|
|
61
|
+
"completion-summary",
|
|
62
|
+
],
|
|
63
|
+
),
|
|
64
|
+
}
|
|
65
|
+
state_key, skills = contracts[workflow_type]
|
|
66
|
+
steps = []
|
|
67
|
+
previous = None
|
|
68
|
+
for index, skill in enumerate(skills, start=1):
|
|
69
|
+
step = {
|
|
70
|
+
"id": f"S{index:02d}",
|
|
71
|
+
"skill": skill,
|
|
72
|
+
"name": skill,
|
|
73
|
+
"status": "completed",
|
|
74
|
+
"required_artifacts": [],
|
|
75
|
+
"depends_on": previous,
|
|
76
|
+
}
|
|
77
|
+
if skill == "prizmkit-implement":
|
|
78
|
+
step["stage_result"] = "IMPLEMENTED"
|
|
79
|
+
if skill == "prizmkit-code-review":
|
|
80
|
+
step["stage_result"] = "REVIEW_PASS"
|
|
81
|
+
if skill == "prizmkit-test":
|
|
82
|
+
step["stage_result"] = "TEST_PASS"
|
|
83
|
+
relative_artifact = artifact_dir.relative_to(project_root)
|
|
84
|
+
step["required_artifacts"] = [
|
|
85
|
+
str(relative_artifact / "test-report.md"),
|
|
86
|
+
str(relative_artifact / "test-result.json"),
|
|
87
|
+
]
|
|
88
|
+
if skill == "prizmkit-retrospective":
|
|
89
|
+
step["stage_result"] = "RETRO_COMPLETE"
|
|
90
|
+
if skill == "prizmkit-committer":
|
|
91
|
+
step["stage_result"] = "COMMITTED"
|
|
92
|
+
steps.append(step)
|
|
93
|
+
previous = step["id"]
|
|
94
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
(artifact_dir / "test-report.md").write_text(
|
|
96
|
+
"# Test Report\n\n## Final Result\n\n- Result: TEST_PASS\n",
|
|
97
|
+
encoding="utf-8",
|
|
98
|
+
)
|
|
99
|
+
(artifact_dir / "test-result.json").write_text(
|
|
100
|
+
json.dumps({
|
|
101
|
+
"schema_version": 1,
|
|
102
|
+
"result": "TEST_PASS",
|
|
103
|
+
"report": "test-report.md",
|
|
104
|
+
"main_review_rounds": 1,
|
|
105
|
+
"independent_review": {
|
|
106
|
+
"status": "downgraded",
|
|
107
|
+
"responses": 0,
|
|
108
|
+
"downgrade_reason": "read-only capability unavailable",
|
|
109
|
+
"final_state_rechecked": False,
|
|
110
|
+
},
|
|
111
|
+
"repair_rounds": 0,
|
|
112
|
+
"production_changed": False,
|
|
113
|
+
"review_required": False,
|
|
114
|
+
"review_scope": None,
|
|
115
|
+
"unresolved_items": [],
|
|
116
|
+
}),
|
|
117
|
+
encoding="utf-8",
|
|
118
|
+
)
|
|
119
|
+
checkpoint = artifact_dir / "workflow-checkpoint.json"
|
|
120
|
+
checkpoint.write_text(
|
|
121
|
+
json.dumps({
|
|
122
|
+
"version": 1,
|
|
123
|
+
"workflow_type": workflow_type,
|
|
124
|
+
"steps": steps,
|
|
125
|
+
state_key: {
|
|
126
|
+
"repair_round": 0,
|
|
127
|
+
"terminal_status": "WORKFLOW_COMPLETED",
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
encoding="utf-8",
|
|
131
|
+
)
|
|
132
|
+
return checkpoint
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _init_preserve_runtime_repo(paths):
|
|
136
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=paths.project_root, check=True, stdout=subprocess.DEVNULL)
|
|
137
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=paths.project_root, check=True)
|
|
138
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=paths.project_root, check=True)
|
|
139
|
+
(paths.project_root / "base.txt").write_text("base\n", encoding="utf-8")
|
|
140
|
+
subprocess.run(["git", "add", "base.txt"], cwd=paths.project_root, check=True)
|
|
141
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=paths.project_root, check=True, stdout=subprocess.DEVNULL)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _preserve_family_contract(kind):
|
|
145
|
+
return {
|
|
146
|
+
"feature": ("F-", "dev", "feature-pipeline", "specs"),
|
|
147
|
+
"bugfix": ("B-", "bugfix", "bugfix-pipeline", "bugfix"),
|
|
148
|
+
"refactor": ("R-", "refactor", "refactor-pipeline", "refactor"),
|
|
149
|
+
}[kind]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _preserve_item_id(prefix, number):
|
|
153
|
+
return f"{prefix}{number:03d}"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _preserve_items(kind):
|
|
157
|
+
prefix, _branch_prefix, _workflow_type, _artifact_root = _preserve_family_contract(kind)
|
|
158
|
+
statuses = [
|
|
159
|
+
(1, "failed", []),
|
|
160
|
+
(2, "auto_skipped", [_preserve_item_id(prefix, 1)]),
|
|
161
|
+
(3, "auto_skipped", [_preserve_item_id(prefix, 2)]),
|
|
162
|
+
(4, "completed", []),
|
|
163
|
+
(5, "skipped", []),
|
|
164
|
+
(6, "auto_skipped", [_preserve_item_id(prefix, 5)]),
|
|
165
|
+
(7, "needs_info", []),
|
|
166
|
+
(8, "pending", []),
|
|
167
|
+
(9, "split", []),
|
|
168
|
+
]
|
|
169
|
+
return [
|
|
170
|
+
{
|
|
171
|
+
"id": _preserve_item_id(prefix, number),
|
|
172
|
+
"title": "Root" if number == 1 else f"Item {number}",
|
|
173
|
+
"description": "preserve runtime",
|
|
174
|
+
"status": status,
|
|
175
|
+
"priority": "medium",
|
|
176
|
+
"dependencies": dependencies,
|
|
177
|
+
}
|
|
178
|
+
for number, status, dependencies in statuses
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _write_preserve_runtime_state(paths, kind, item_id, branch, *, base_branch="main"):
|
|
183
|
+
from prizmkit_runtime.runner_models import family_for
|
|
184
|
+
from prizmkit_runtime.task_checkout import TaskCheckout, persist_task_checkout
|
|
185
|
+
|
|
186
|
+
family = family_for(kind, paths)
|
|
187
|
+
family.state_dir.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
(family.state_dir / "pipeline.json").write_text(json.dumps({"status": "failed"}), encoding="utf-8")
|
|
189
|
+
item_state = family.state_dir / item_id
|
|
190
|
+
sessions = item_state / "sessions" / "old-session" / "logs"
|
|
191
|
+
sessions.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
(sessions / "session.log").write_text("historical log\n", encoding="utf-8")
|
|
193
|
+
status = {
|
|
194
|
+
"status": "failed",
|
|
195
|
+
"retry_count": 7,
|
|
196
|
+
"infra_error_count": 4,
|
|
197
|
+
"sessions": ["old-session"],
|
|
198
|
+
"last_session_id": "old-session",
|
|
199
|
+
"previous_session_id": "old-session",
|
|
200
|
+
"last_infra_error_session_id": "old-session",
|
|
201
|
+
"continuation_pending": False,
|
|
202
|
+
"continuation_reason": "code_error",
|
|
203
|
+
"continuation_summary_path": "keep-this-path",
|
|
204
|
+
"last_progress_fingerprint": {"head": "abc", "checkpoint": "S03"},
|
|
205
|
+
"no_progress_count": 2,
|
|
206
|
+
"fatal_error_code": "provider_error",
|
|
207
|
+
"active_dev_branch": branch,
|
|
208
|
+
"base_branch": base_branch,
|
|
209
|
+
"custom_diagnostic": {"reason": "preserve me"},
|
|
210
|
+
}
|
|
211
|
+
status_path = item_state / "status.json"
|
|
212
|
+
status_path.write_text(json.dumps(status, indent=2) + "\n", encoding="utf-8")
|
|
213
|
+
checkout = TaskCheckout.active(kind, item_id, branch, base_branch)
|
|
214
|
+
persist_task_checkout(family.state_dir, checkout)
|
|
215
|
+
return family, status_path, status, checkout
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@pytest.mark.parametrize("kind", ["feature", "bugfix", "refactor"])
|
|
219
|
+
def test_preserve_runtime_restores_failed_chain_and_preserves_existing_checkout_history(tmp_path, kind):
|
|
220
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
221
|
+
from prizmkit_runtime.task_checkout import load_task_checkout
|
|
222
|
+
|
|
223
|
+
paths = _make_paths(tmp_path / kind)
|
|
224
|
+
_init_preserve_runtime_repo(paths)
|
|
225
|
+
prefix, branch_prefix, workflow_type, artifact_root = _preserve_family_contract(kind)
|
|
226
|
+
root_id = _preserve_item_id(prefix, 1)
|
|
227
|
+
plan = _write_family_plan(paths, kind, _preserve_items(kind))
|
|
228
|
+
branch = f"{branch_prefix}/{root_id}-preserved"
|
|
229
|
+
subprocess.run(["git", "branch", branch, "main"], cwd=paths.project_root, check=True)
|
|
230
|
+
family, status_path, before_status, checkout = _write_preserve_runtime_state(paths, kind, root_id, branch)
|
|
231
|
+
descendant_status = family.state_dir / _preserve_item_id(prefix, 2) / "status.json"
|
|
232
|
+
descendant_status.parent.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
descendant_status.write_text(
|
|
234
|
+
json.dumps({"status": "auto_skipped", "caused_by": root_id, "sessions": ["child-history"]}, indent=2) + "\n",
|
|
235
|
+
encoding="utf-8",
|
|
236
|
+
)
|
|
237
|
+
descendant_status_before = descendant_status.read_bytes()
|
|
238
|
+
artifact_name = "001-root" if kind == "feature" else root_id
|
|
239
|
+
artifact = paths.project_root / ".prizmkit" / artifact_root / artifact_name
|
|
240
|
+
checkpoint = _write_semantic_completion_checkpoint(paths.project_root, artifact, workflow_type)
|
|
241
|
+
checkpoint_before = checkpoint.read_bytes()
|
|
242
|
+
session_log = family.state_dir / root_id / "sessions" / "old-session" / "logs" / "session.log"
|
|
243
|
+
session_before = session_log.read_bytes()
|
|
244
|
+
branch_head_before = subprocess.run(
|
|
245
|
+
["git", "rev-parse", branch], cwd=paths.project_root, check=True, stdout=subprocess.PIPE, text=True
|
|
246
|
+
).stdout.strip()
|
|
247
|
+
|
|
248
|
+
result = run_reset_command(kind, ("--failed", "--preserve-runtime"), paths)
|
|
249
|
+
|
|
250
|
+
assert result.exit_code == 0, result.render()
|
|
251
|
+
updated_plan = json.loads(plan.read_text(encoding="utf-8"))
|
|
252
|
+
statuses = {item["id"]: item["status"] for item in updated_plan[family.list_key]}
|
|
253
|
+
assert statuses[root_id] == "pending"
|
|
254
|
+
assert statuses[_preserve_item_id(prefix, 2)] == "pending"
|
|
255
|
+
assert statuses[_preserve_item_id(prefix, 3)] == "pending"
|
|
256
|
+
assert statuses[_preserve_item_id(prefix, 4)] == "completed"
|
|
257
|
+
assert statuses[_preserve_item_id(prefix, 5)] == "skipped"
|
|
258
|
+
assert statuses[_preserve_item_id(prefix, 6)] == "auto_skipped"
|
|
259
|
+
assert statuses[_preserve_item_id(prefix, 7)] == "needs_info"
|
|
260
|
+
assert statuses[_preserve_item_id(prefix, 8)] == "pending"
|
|
261
|
+
assert statuses[_preserve_item_id(prefix, 9)] == "split"
|
|
262
|
+
expected_status = dict(before_status)
|
|
263
|
+
expected_status.update(retry_count=0, infra_error_count=0, continuation_pending=True)
|
|
264
|
+
assert json.loads(status_path.read_text(encoding="utf-8")) == expected_status
|
|
265
|
+
assert load_task_checkout(family.state_dir, kind, root_id) == checkout
|
|
266
|
+
assert descendant_status.read_bytes() == descendant_status_before
|
|
267
|
+
assert checkpoint.read_bytes() == checkpoint_before
|
|
268
|
+
assert session_log.read_bytes() == session_before
|
|
269
|
+
assert subprocess.run(
|
|
270
|
+
["git", "rev-parse", branch], cwd=paths.project_root, check=True, stdout=subprocess.PIPE, text=True
|
|
271
|
+
).stdout.strip() == branch_head_before
|
|
272
|
+
assert "Active checkouts reused" in result.render()
|
|
273
|
+
assert "Reset did not start a runner" in result.render()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@pytest.mark.parametrize("kind", ["feature", "bugfix", "refactor"])
|
|
277
|
+
def test_preserve_runtime_no_failed_root_is_a_noop_without_checkout_state(tmp_path, kind):
|
|
278
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
279
|
+
from prizmkit_runtime.runner_models import family_for
|
|
280
|
+
|
|
281
|
+
paths = _make_paths(tmp_path / kind)
|
|
282
|
+
items = _preserve_items(kind)
|
|
283
|
+
items[0]["status"] = "completed"
|
|
284
|
+
plan = _write_family_plan(paths, kind, items)
|
|
285
|
+
family = family_for(kind, paths)
|
|
286
|
+
before = plan.read_bytes()
|
|
287
|
+
|
|
288
|
+
result = run_reset_command(kind, ("--failed", "--preserve-runtime"), paths)
|
|
289
|
+
|
|
290
|
+
assert result.exit_code == 0
|
|
291
|
+
assert "No failed" in result.render()
|
|
292
|
+
assert plan.read_bytes() == before
|
|
293
|
+
assert not any((family.state_dir / item["id"] / "checkout.json").exists() for item in items)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_preserve_runtime_option_boundaries_and_removed_reset_run_do_not_mutate_state(tmp_path):
|
|
297
|
+
from prizmkit_runtime.reset import ResetOptions, run_reset_command
|
|
298
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
299
|
+
|
|
300
|
+
paths = _make_paths(tmp_path)
|
|
301
|
+
plan = _write_family_plan(paths, "feature", _preserve_items("feature"))
|
|
302
|
+
family = family_for("feature", paths)
|
|
303
|
+
family.state_dir.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
(family.state_dir / "pipeline.json").write_text("{}\n", encoding="utf-8")
|
|
305
|
+
before = plan.read_bytes()
|
|
306
|
+
invalid_cases = [
|
|
307
|
+
(("F-001", "--run"), "no longer accepts --run"),
|
|
308
|
+
(("--preserve-runtime",), "requires --failed"),
|
|
309
|
+
(("--failed", "--preserve-runtime", "--clean"), "mutually exclusive"),
|
|
310
|
+
(("F-001", "--failed", "--preserve-runtime"), "cannot target an ID or range"),
|
|
311
|
+
(("F-001:F-003", "--failed", "--preserve-runtime"), "cannot target an ID or range"),
|
|
312
|
+
(("--failed", "--preserve-runtime", "--stalled"), "Unsupported reset arguments"),
|
|
313
|
+
(("--failed", "--preserve-runtime", "--unknown"), "Unsupported reset arguments"),
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
for args, message in invalid_cases:
|
|
317
|
+
result = run_reset_command("feature", args, paths)
|
|
318
|
+
assert result.exit_code != 0
|
|
319
|
+
assert message in result.render()
|
|
320
|
+
assert plan.read_bytes() == before
|
|
321
|
+
assert "run_after" not in ResetOptions.__dataclass_fields__
|
|
322
|
+
invocation = parse_invocation(
|
|
323
|
+
family,
|
|
324
|
+
"run",
|
|
325
|
+
("F-001", "--max-retries", "6", "--max-infra-retries", "7"),
|
|
326
|
+
)
|
|
327
|
+
assert invocation.action == "run"
|
|
328
|
+
assert invocation.item_id == "F-001"
|
|
329
|
+
assert invocation.max_retries == 6
|
|
330
|
+
assert invocation.max_infra_retries == 7
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_preserve_runtime_restores_every_failed_root_in_one_successful_pass(tmp_path):
|
|
334
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
335
|
+
|
|
336
|
+
paths = _make_paths(tmp_path)
|
|
337
|
+
_init_preserve_runtime_repo(paths)
|
|
338
|
+
items = [
|
|
339
|
+
{"id": "F-001", "title": "Root One", "status": "failed", "dependencies": []},
|
|
340
|
+
{"id": "F-002", "title": "Child One", "status": "auto_skipped", "dependencies": ["F-001"]},
|
|
341
|
+
{"id": "F-003", "title": "Root Two", "status": "failed", "dependencies": []},
|
|
342
|
+
{"id": "F-004", "title": "Child Two", "status": "auto_skipped", "dependencies": ["F-003"]},
|
|
343
|
+
{"id": "F-005", "title": "Done", "status": "completed", "dependencies": []},
|
|
344
|
+
]
|
|
345
|
+
plan = _write_family_plan(paths, "feature", items)
|
|
346
|
+
before_statuses = {}
|
|
347
|
+
for item_id in ("F-001", "F-003"):
|
|
348
|
+
branch = f"dev/{item_id}-existing"
|
|
349
|
+
subprocess.run(["git", "branch", branch, "main"], cwd=paths.project_root, check=True)
|
|
350
|
+
_family, status_path, status_data, _checkout = _write_preserve_runtime_state(
|
|
351
|
+
paths, "feature", item_id, branch
|
|
352
|
+
)
|
|
353
|
+
before_statuses[item_id] = (status_path, status_data)
|
|
354
|
+
|
|
355
|
+
result = run_reset_command("feature", ("--failed", "--preserve-runtime"), paths)
|
|
356
|
+
|
|
357
|
+
assert result.exit_code == 0, result.render()
|
|
358
|
+
statuses = {
|
|
359
|
+
item["id"]: item["status"]
|
|
360
|
+
for item in json.loads(plan.read_text(encoding="utf-8"))["features"]
|
|
361
|
+
}
|
|
362
|
+
assert statuses == {
|
|
363
|
+
"F-001": "pending",
|
|
364
|
+
"F-002": "pending",
|
|
365
|
+
"F-003": "pending",
|
|
366
|
+
"F-004": "pending",
|
|
367
|
+
"F-005": "completed",
|
|
368
|
+
}
|
|
369
|
+
for status_path, before in before_statuses.values():
|
|
370
|
+
expected = dict(before)
|
|
371
|
+
expected.update(retry_count=0, infra_error_count=0, continuation_pending=True)
|
|
372
|
+
assert json.loads(status_path.read_text(encoding="utf-8")) == expected
|
|
373
|
+
assert "Failed roots restored: F-001, F-003" in result.render()
|
|
374
|
+
|
|
375
|
+
plan_after = plan.read_bytes()
|
|
376
|
+
runtime_after = {
|
|
377
|
+
item_id: status_path.read_bytes()
|
|
378
|
+
for item_id, (status_path, _before) in before_statuses.items()
|
|
379
|
+
}
|
|
380
|
+
second = run_reset_command("feature", ("--failed", "--preserve-runtime"), paths)
|
|
381
|
+
assert second.exit_code == 0
|
|
382
|
+
assert "No failed" in second.render()
|
|
383
|
+
assert plan.read_bytes() == plan_after
|
|
384
|
+
assert {
|
|
385
|
+
item_id: status_path.read_bytes()
|
|
386
|
+
for item_id, (status_path, _before) in before_statuses.items()
|
|
387
|
+
} == runtime_after
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def test_preserve_runtime_respects_existing_task_guard_without_mutation(tmp_path):
|
|
391
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
392
|
+
from prizmkit_runtime.task_checkout import task_checkout_guard
|
|
393
|
+
|
|
394
|
+
paths = _make_paths(tmp_path)
|
|
395
|
+
_init_preserve_runtime_repo(paths)
|
|
396
|
+
plan = _write_family_plan(
|
|
397
|
+
paths,
|
|
398
|
+
"feature",
|
|
399
|
+
[{"id": "F-001", "title": "Root", "status": "failed", "dependencies": []}],
|
|
400
|
+
)
|
|
401
|
+
subprocess.run(["git", "branch", "dev/F-001-existing", "main"], cwd=paths.project_root, check=True)
|
|
402
|
+
family, status_path, _status, _checkout = _write_preserve_runtime_state(
|
|
403
|
+
paths, "feature", "F-001", "dev/F-001-existing"
|
|
404
|
+
)
|
|
405
|
+
plan_before = plan.read_bytes()
|
|
406
|
+
status_before = status_path.read_bytes()
|
|
407
|
+
|
|
408
|
+
with task_checkout_guard(family.state_dir, "F-001"):
|
|
409
|
+
result = run_reset_command("feature", ("--failed", "--preserve-runtime"), paths)
|
|
410
|
+
|
|
411
|
+
assert result.exit_code == 1
|
|
412
|
+
assert "checkout lifecycle is busy" in result.render()
|
|
413
|
+
assert plan.read_bytes() == plan_before
|
|
414
|
+
assert status_path.read_bytes() == status_before
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@pytest.mark.parametrize(
|
|
418
|
+
("kind", "item_id"),
|
|
419
|
+
[("feature", "F-001"), ("bugfix", "B-001"), ("refactor", "R-001")],
|
|
420
|
+
)
|
|
421
|
+
def test_public_reset_cli_hard_rejects_run_for_every_family_without_state_change(tmp_path, kind, item_id):
|
|
422
|
+
project = tmp_path / kind
|
|
423
|
+
project.mkdir()
|
|
424
|
+
|
|
425
|
+
completed = subprocess.run(
|
|
426
|
+
[
|
|
427
|
+
sys.executable,
|
|
428
|
+
str(CLI_PATH),
|
|
429
|
+
"--project-root",
|
|
430
|
+
str(project),
|
|
431
|
+
"reset",
|
|
432
|
+
kind,
|
|
433
|
+
item_id,
|
|
434
|
+
"--run",
|
|
435
|
+
],
|
|
436
|
+
cwd=REPO_ROOT,
|
|
437
|
+
stdout=subprocess.PIPE,
|
|
438
|
+
stderr=subprocess.PIPE,
|
|
439
|
+
text=True,
|
|
440
|
+
check=False,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
assert completed.returncode == 2
|
|
444
|
+
assert "Reset no longer accepts --run" in completed.stdout + completed.stderr
|
|
445
|
+
assert not (project / ".prizmkit").exists()
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@pytest.mark.parametrize(
|
|
449
|
+
("items", "expected_error"),
|
|
450
|
+
[
|
|
451
|
+
(
|
|
452
|
+
[{"id": "F-001", "title": "Root", "status": "failed", "dependencies": ["F-999"]}],
|
|
453
|
+
"unknown dependency F-999",
|
|
454
|
+
),
|
|
455
|
+
(
|
|
456
|
+
[
|
|
457
|
+
{"id": "F-001", "title": "One", "status": "failed", "dependencies": ["F-002"]},
|
|
458
|
+
{"id": "F-002", "title": "Two", "status": "auto_skipped", "dependencies": ["F-001"]},
|
|
459
|
+
],
|
|
460
|
+
"dependency cycle",
|
|
461
|
+
),
|
|
462
|
+
(
|
|
463
|
+
[
|
|
464
|
+
{"id": "F-001", "title": "One", "status": "failed", "dependencies": []},
|
|
465
|
+
{"id": "F-001", "title": "Duplicate", "status": "failed", "dependencies": []},
|
|
466
|
+
],
|
|
467
|
+
"duplicate item id: F-001",
|
|
468
|
+
),
|
|
469
|
+
],
|
|
470
|
+
)
|
|
471
|
+
def test_preserve_runtime_rejects_invalid_dependency_graph_without_state_mutation(
|
|
472
|
+
tmp_path,
|
|
473
|
+
items,
|
|
474
|
+
expected_error,
|
|
475
|
+
):
|
|
476
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
477
|
+
|
|
478
|
+
paths = _make_paths(tmp_path)
|
|
479
|
+
plan = _write_family_plan(paths, "feature", items)
|
|
480
|
+
before = plan.read_bytes()
|
|
481
|
+
|
|
482
|
+
result = run_reset_command("feature", ("--failed", "--preserve-runtime"), paths)
|
|
483
|
+
|
|
484
|
+
assert result.exit_code == 1
|
|
485
|
+
assert expected_error in result.render()
|
|
486
|
+
assert plan.read_bytes() == before
|
|
487
|
+
assert not paths.feature_state_dir.exists()
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def test_preserve_runtime_success_quotes_list_path_in_separate_run_instruction(tmp_path):
|
|
491
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
492
|
+
|
|
493
|
+
paths = _make_paths(tmp_path / "parent with spaces")
|
|
494
|
+
_init_preserve_runtime_repo(paths)
|
|
495
|
+
plan = _write_family_plan(
|
|
496
|
+
paths,
|
|
497
|
+
"feature",
|
|
498
|
+
[{"id": "F-001", "title": "Root", "status": "failed", "dependencies": []}],
|
|
499
|
+
)
|
|
500
|
+
subprocess.run(["git", "branch", "dev/F-001-existing", "main"], cwd=paths.project_root, check=True)
|
|
501
|
+
_write_preserve_runtime_state(paths, "feature", "F-001", "dev/F-001-existing")
|
|
502
|
+
|
|
503
|
+
result = run_reset_command("feature", ("--failed", "--preserve-runtime"), paths)
|
|
504
|
+
|
|
505
|
+
assert result.exit_code == 0
|
|
506
|
+
assert f"feature run {shlex.quote(str(plan.resolve()))}" in result.render()
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def test_preserve_runtime_requires_recorded_base_to_be_a_local_branch(tmp_path):
|
|
510
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
511
|
+
|
|
512
|
+
paths = _make_paths(tmp_path)
|
|
513
|
+
_init_preserve_runtime_repo(paths)
|
|
514
|
+
subprocess.run(["git", "tag", "base-tag", "main"], cwd=paths.project_root, check=True)
|
|
515
|
+
plan = _write_family_plan(
|
|
516
|
+
paths,
|
|
517
|
+
"feature",
|
|
518
|
+
[{"id": "F-001", "title": "Root", "status": "failed", "dependencies": []}],
|
|
519
|
+
)
|
|
520
|
+
_family, status_path, _before_status, _checkout = _write_preserve_runtime_state(
|
|
521
|
+
paths,
|
|
522
|
+
"feature",
|
|
523
|
+
"F-001",
|
|
524
|
+
"dev/F-001-missing",
|
|
525
|
+
base_branch="base-tag",
|
|
526
|
+
)
|
|
527
|
+
plan_before = plan.read_bytes()
|
|
528
|
+
status_before = status_path.read_bytes()
|
|
529
|
+
|
|
530
|
+
result = run_reset_command("feature", ("--failed", "--preserve-runtime"), paths)
|
|
531
|
+
|
|
532
|
+
assert result.exit_code == 1
|
|
533
|
+
assert "recorded base branch is missing: base-tag" in result.render()
|
|
534
|
+
assert plan.read_bytes() == plan_before
|
|
535
|
+
assert status_path.read_bytes() == status_before
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def test_preserve_runtime_preflights_all_failed_roots_before_any_state_change(tmp_path):
|
|
539
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
540
|
+
|
|
541
|
+
paths = _make_paths(tmp_path)
|
|
542
|
+
_init_preserve_runtime_repo(paths)
|
|
543
|
+
items = [
|
|
544
|
+
{"id": "F-001", "title": "One", "status": "failed", "dependencies": []},
|
|
545
|
+
{"id": "F-002", "title": "Two", "status": "failed", "dependencies": []},
|
|
546
|
+
{"id": "F-003", "title": "Three", "status": "failed", "dependencies": []},
|
|
547
|
+
]
|
|
548
|
+
plan = _write_family_plan(paths, "feature", items)
|
|
549
|
+
subprocess.run(["git", "branch", "dev/F-001-existing", "main"], cwd=paths.project_root, check=True)
|
|
550
|
+
family, status_one, _data_one, _checkout_one = _write_preserve_runtime_state(
|
|
551
|
+
paths, "feature", "F-001", "dev/F-001-existing"
|
|
552
|
+
)
|
|
553
|
+
_family, status_two, _data_two, _checkout_two = _write_preserve_runtime_state(
|
|
554
|
+
paths, "feature", "F-002", "dev/F-002-missing", base_branch="missing-base-two"
|
|
555
|
+
)
|
|
556
|
+
_family, status_three, _data_three, _checkout_three = _write_preserve_runtime_state(
|
|
557
|
+
paths, "feature", "F-003", "dev/F-003-missing", base_branch="missing-base-three"
|
|
558
|
+
)
|
|
559
|
+
plan_before = plan.read_bytes()
|
|
560
|
+
status_one_before = status_one.read_bytes()
|
|
561
|
+
status_two_before = status_two.read_bytes()
|
|
562
|
+
status_three_before = status_three.read_bytes()
|
|
563
|
+
checkout_one_before = (family.state_dir / "F-001" / "checkout.json").read_bytes()
|
|
564
|
+
branches_before = subprocess.run(
|
|
565
|
+
["git", "branch", "--format=%(refname:short)"],
|
|
566
|
+
cwd=paths.project_root,
|
|
567
|
+
check=True,
|
|
568
|
+
stdout=subprocess.PIPE,
|
|
569
|
+
text=True,
|
|
570
|
+
).stdout
|
|
571
|
+
|
|
572
|
+
result = run_reset_command("feature", ("--failed", "--preserve-runtime"), paths)
|
|
573
|
+
|
|
574
|
+
assert result.exit_code == 1
|
|
575
|
+
assert "recorded base branch is missing: missing-base-two" in result.render()
|
|
576
|
+
assert "recorded base branch is missing: missing-base-three" in result.render()
|
|
577
|
+
assert plan.read_bytes() == plan_before
|
|
578
|
+
assert status_one.read_bytes() == status_one_before
|
|
579
|
+
assert status_two.read_bytes() == status_two_before
|
|
580
|
+
assert status_three.read_bytes() == status_three_before
|
|
581
|
+
assert (family.state_dir / "F-001" / "checkout.json").read_bytes() == checkout_one_before
|
|
582
|
+
assert subprocess.run(
|
|
583
|
+
["git", "branch", "--format=%(refname:short)"],
|
|
584
|
+
cwd=paths.project_root,
|
|
585
|
+
check=True,
|
|
586
|
+
stdout=subprocess.PIPE,
|
|
587
|
+
text=True,
|
|
588
|
+
).stdout == branches_before
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_preserve_runtime_invalid_existing_checkpoint_blocks_before_mutation(tmp_path):
|
|
592
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
593
|
+
|
|
594
|
+
paths = _make_paths(tmp_path)
|
|
595
|
+
_init_preserve_runtime_repo(paths)
|
|
596
|
+
plan = _write_family_plan(
|
|
597
|
+
paths,
|
|
598
|
+
"feature",
|
|
599
|
+
[{"id": "F-001", "title": "Root", "status": "failed", "dependencies": []}],
|
|
600
|
+
)
|
|
601
|
+
subprocess.run(["git", "branch", "dev/F-001-existing", "main"], cwd=paths.project_root, check=True)
|
|
602
|
+
_family, status_path, _before_status, _checkout = _write_preserve_runtime_state(
|
|
603
|
+
paths, "feature", "F-001", "dev/F-001-existing"
|
|
604
|
+
)
|
|
605
|
+
artifact = paths.project_root / ".prizmkit" / "specs" / "001-root"
|
|
606
|
+
artifact.mkdir(parents=True)
|
|
607
|
+
checkpoint = artifact / "workflow-checkpoint.json"
|
|
608
|
+
checkpoint.write_text("{\"steps\": \"invalid\"}\n", encoding="utf-8")
|
|
609
|
+
plan_before = plan.read_bytes()
|
|
610
|
+
status_before = status_path.read_bytes()
|
|
611
|
+
checkout_before = (
|
|
612
|
+
paths.feature_state_dir / "F-001" / "checkout.json"
|
|
613
|
+
).read_bytes()
|
|
614
|
+
|
|
615
|
+
result = run_reset_command("feature", ("--failed", "--preserve-runtime"), paths)
|
|
616
|
+
|
|
617
|
+
assert result.exit_code == 1
|
|
618
|
+
assert "invalid checkpoint" in result.render()
|
|
619
|
+
assert plan.read_bytes() == plan_before
|
|
620
|
+
assert status_path.read_bytes() == status_before
|
|
621
|
+
assert (paths.feature_state_dir / "F-001" / "checkout.json").read_bytes() == checkout_before
|
|
622
|
+
assert checkpoint.read_text(encoding="utf-8") == "{\"steps\": \"invalid\"}\n"
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _add_completed_plan_step(checkpoint):
|
|
626
|
+
payload = json.loads(checkpoint.read_text(encoding="utf-8"))
|
|
627
|
+
payload["steps"].insert(
|
|
628
|
+
0,
|
|
629
|
+
{
|
|
630
|
+
"id": "S00",
|
|
631
|
+
"skill": "prizmkit-plan",
|
|
632
|
+
"name": "plan",
|
|
633
|
+
"status": "completed",
|
|
634
|
+
"stage_result": "PLAN_READY",
|
|
635
|
+
"depends_on": None,
|
|
636
|
+
"required_artifacts": [],
|
|
637
|
+
},
|
|
638
|
+
)
|
|
639
|
+
payload["steps"][1]["depends_on"] = "S00"
|
|
640
|
+
state_key = {
|
|
641
|
+
"feature-pipeline": "feature_state",
|
|
642
|
+
"bugfix-pipeline": "bugfix_state",
|
|
643
|
+
"refactor-pipeline": "refactor_state",
|
|
644
|
+
}[payload["workflow_type"]]
|
|
645
|
+
payload[state_key]["completed_stages"] = ["plan", "implement", "code-review", "test"]
|
|
646
|
+
checkpoint.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def test_preserve_runtime_missing_branch_creates_replacement_and_rewinds_checkpoint(tmp_path):
|
|
650
|
+
from prizmkit_runtime.gitops import local_branch_exists
|
|
651
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
652
|
+
from prizmkit_runtime.runner_models import family_for
|
|
653
|
+
from prizmkit_runtime.task_checkout import load_task_checkout
|
|
654
|
+
|
|
655
|
+
paths = _make_paths(tmp_path)
|
|
656
|
+
_init_preserve_runtime_repo(paths)
|
|
657
|
+
plan = _write_family_plan(
|
|
658
|
+
paths,
|
|
659
|
+
"feature",
|
|
660
|
+
[{"id": "F-001", "title": "Root", "status": "failed", "dependencies": []}],
|
|
661
|
+
)
|
|
662
|
+
family, status_path, before_status, old_checkout = _write_preserve_runtime_state(
|
|
663
|
+
paths, "feature", "F-001", "dev/F-001-deleted"
|
|
664
|
+
)
|
|
665
|
+
artifact = paths.project_root / ".prizmkit" / "specs" / "001-root"
|
|
666
|
+
checkpoint = _write_semantic_completion_checkpoint(paths.project_root, artifact)
|
|
667
|
+
_add_completed_plan_step(checkpoint)
|
|
668
|
+
(artifact / "implementation-notes.md").write_text("keep context\n", encoding="utf-8")
|
|
669
|
+
session_log = family.state_dir / "F-001" / "sessions" / "old-session" / "logs" / "session.log"
|
|
670
|
+
session_before = session_log.read_bytes()
|
|
671
|
+
base_head = subprocess.run(
|
|
672
|
+
["git", "rev-parse", "main"], cwd=paths.project_root, check=True, stdout=subprocess.PIPE, text=True
|
|
673
|
+
).stdout.strip()
|
|
674
|
+
|
|
675
|
+
result = run_reset_command("feature", ("--failed", "--preserve-runtime"), paths)
|
|
676
|
+
|
|
677
|
+
assert result.exit_code == 0, result.render()
|
|
678
|
+
checkout = load_task_checkout(family.state_dir, "feature", "F-001")
|
|
679
|
+
assert checkout is not None and checkout.is_active and not checkout.is_reset
|
|
680
|
+
assert checkout.active_dev_branch != old_checkout.active_dev_branch
|
|
681
|
+
assert checkout.active_dev_branch.startswith("dev/F-001-resume-")
|
|
682
|
+
assert checkout.base_branch == "main"
|
|
683
|
+
assert local_branch_exists(paths.project_root, checkout.active_dev_branch)
|
|
684
|
+
assert subprocess.run(
|
|
685
|
+
["git", "rev-parse", checkout.active_dev_branch],
|
|
686
|
+
cwd=paths.project_root,
|
|
687
|
+
check=True,
|
|
688
|
+
stdout=subprocess.PIPE,
|
|
689
|
+
text=True,
|
|
690
|
+
).stdout.strip() == base_head
|
|
691
|
+
persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
|
|
692
|
+
assert persisted["steps"][0]["status"] == "completed"
|
|
693
|
+
assert persisted["steps"][0]["stage_result"] == "PLAN_READY"
|
|
694
|
+
assert all(step["status"] == "pending" for step in persisted["steps"][1:])
|
|
695
|
+
assert all("stage_result" not in step for step in persisted["steps"][1:])
|
|
696
|
+
assert persisted["feature_state"]["stage"] == "implement"
|
|
697
|
+
assert persisted["feature_state"]["current_stage"] == "prizmkit-implement"
|
|
698
|
+
assert persisted["feature_state"]["completed_stages"] == ["plan"]
|
|
699
|
+
assert (artifact / "implementation-notes.md").read_text(encoding="utf-8") == "keep context\n"
|
|
700
|
+
expected_status = dict(before_status)
|
|
701
|
+
expected_status.update(
|
|
702
|
+
retry_count=0,
|
|
703
|
+
infra_error_count=0,
|
|
704
|
+
continuation_pending=True,
|
|
705
|
+
active_dev_branch=checkout.active_dev_branch,
|
|
706
|
+
base_branch="main",
|
|
707
|
+
)
|
|
708
|
+
assert json.loads(status_path.read_text(encoding="utf-8")) == expected_status
|
|
709
|
+
assert session_log.read_bytes() == session_before
|
|
710
|
+
assert json.loads(plan.read_text(encoding="utf-8"))["features"][0]["status"] == "pending"
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
@pytest.mark.parametrize(
|
|
714
|
+
("kind", "item_id", "branch_prefix", "workflow_type", "artifact_root", "state_key"),
|
|
715
|
+
[
|
|
716
|
+
("bugfix", "B-001", "bugfix", "bugfix-pipeline", "bugfix", "bugfix_state"),
|
|
717
|
+
("refactor", "R-001", "refactor", "refactor-pipeline", "refactor", "refactor_state"),
|
|
718
|
+
],
|
|
719
|
+
)
|
|
720
|
+
def test_missing_branch_checkpoint_rewind_has_cross_family_parity(
|
|
721
|
+
tmp_path,
|
|
722
|
+
kind,
|
|
723
|
+
item_id,
|
|
724
|
+
branch_prefix,
|
|
725
|
+
workflow_type,
|
|
726
|
+
artifact_root,
|
|
727
|
+
state_key,
|
|
728
|
+
):
|
|
729
|
+
from prizmkit_runtime.gitops import local_branch_exists
|
|
730
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
731
|
+
from prizmkit_runtime.runner_models import family_for
|
|
732
|
+
from prizmkit_runtime.task_checkout import load_task_checkout
|
|
733
|
+
|
|
734
|
+
paths = _make_paths(tmp_path / kind)
|
|
735
|
+
_init_preserve_runtime_repo(paths)
|
|
736
|
+
plan = _write_family_plan(
|
|
737
|
+
paths,
|
|
738
|
+
kind,
|
|
739
|
+
[{"id": item_id, "title": "Root", "status": "failed", "dependencies": []}],
|
|
740
|
+
)
|
|
741
|
+
family, _status_path, _before_status, old_checkout = _write_preserve_runtime_state(
|
|
742
|
+
paths, kind, item_id, f"{branch_prefix}/{item_id}-deleted"
|
|
743
|
+
)
|
|
744
|
+
artifact = paths.project_root / ".prizmkit" / artifact_root / item_id
|
|
745
|
+
checkpoint = _write_semantic_completion_checkpoint(
|
|
746
|
+
paths.project_root,
|
|
747
|
+
artifact,
|
|
748
|
+
workflow_type,
|
|
749
|
+
)
|
|
750
|
+
_add_completed_plan_step(checkpoint)
|
|
751
|
+
|
|
752
|
+
result = run_reset_command(kind, ("--failed", "--preserve-runtime"), paths)
|
|
753
|
+
|
|
754
|
+
assert result.exit_code == 0, result.render()
|
|
755
|
+
checkout = load_task_checkout(family.state_dir, kind, item_id)
|
|
756
|
+
assert checkout is not None and checkout.is_active
|
|
757
|
+
assert checkout.active_dev_branch != old_checkout.active_dev_branch
|
|
758
|
+
assert checkout.active_dev_branch.startswith(f"{branch_prefix}/{item_id}-resume-")
|
|
759
|
+
assert local_branch_exists(paths.project_root, checkout.active_dev_branch)
|
|
760
|
+
persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
|
|
761
|
+
assert persisted["steps"][0]["status"] == "completed"
|
|
762
|
+
assert all(step["status"] == "pending" for step in persisted["steps"][1:])
|
|
763
|
+
assert persisted[state_key]["stage"] == "implement"
|
|
764
|
+
assert persisted[state_key]["current_stage"] == "prizmkit-implement"
|
|
765
|
+
collection = {"bugfix": "bugs", "refactor": "refactors"}[kind]
|
|
766
|
+
assert json.loads(plan.read_text(encoding="utf-8"))[collection][0]["status"] == "pending"
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def test_preserve_runtime_reuses_registered_worktree_without_cleanup(tmp_path):
|
|
770
|
+
from prizmkit_runtime.gitops import (
|
|
771
|
+
BranchContext,
|
|
772
|
+
WorktreePolicy,
|
|
773
|
+
ensure_linked_worktree,
|
|
774
|
+
registered_worktrees,
|
|
775
|
+
worktree_runtime_context,
|
|
776
|
+
)
|
|
777
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
778
|
+
from prizmkit_runtime.runner_models import family_for
|
|
779
|
+
from prizmkit_runtime.task_checkout import (
|
|
780
|
+
CHECKOUT_MODE_WORKTREE,
|
|
781
|
+
TaskCheckout,
|
|
782
|
+
load_task_checkout,
|
|
783
|
+
persist_task_checkout,
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
paths = _make_paths(tmp_path)
|
|
787
|
+
_init_preserve_runtime_repo(paths)
|
|
788
|
+
plan = _write_family_plan(
|
|
789
|
+
paths,
|
|
790
|
+
"feature",
|
|
791
|
+
[{"id": "F-001", "title": "Root", "status": "failed", "dependencies": []}],
|
|
792
|
+
)
|
|
793
|
+
family, status_path, _before_status, _old_checkout = _write_preserve_runtime_state(
|
|
794
|
+
paths, "feature", "F-001", "dev/F-001-worktree"
|
|
795
|
+
)
|
|
796
|
+
runtime = worktree_runtime_context(
|
|
797
|
+
BranchContext("main", "dev/F-001-worktree", paths.project_root),
|
|
798
|
+
WorktreePolicy(True, True, True, tmp_path / "worktrees"),
|
|
799
|
+
)
|
|
800
|
+
assert ensure_linked_worktree(runtime).ok
|
|
801
|
+
worktree_checkout = TaskCheckout.active(
|
|
802
|
+
"feature",
|
|
803
|
+
"F-001",
|
|
804
|
+
"dev/F-001-worktree",
|
|
805
|
+
"main",
|
|
806
|
+
checkout_mode=CHECKOUT_MODE_WORKTREE,
|
|
807
|
+
worktree_path=str(runtime.worktree_path),
|
|
808
|
+
)
|
|
809
|
+
persist_task_checkout(family.state_dir, worktree_checkout)
|
|
810
|
+
artifact = runtime.worktree_path / ".prizmkit" / "specs" / "001-root"
|
|
811
|
+
artifact.mkdir(parents=True)
|
|
812
|
+
marker = artifact / "partial.md"
|
|
813
|
+
marker.write_text("worktree WIP\n", encoding="utf-8")
|
|
814
|
+
registrations_before = registered_worktrees(paths.project_root)
|
|
815
|
+
|
|
816
|
+
result = run_reset_command("feature", ("--failed", "--preserve-runtime"), paths)
|
|
817
|
+
|
|
818
|
+
assert result.exit_code == 0, result.render()
|
|
819
|
+
assert load_task_checkout(family.state_dir, "feature", "F-001") == worktree_checkout
|
|
820
|
+
assert runtime.worktree_path.is_dir()
|
|
821
|
+
assert marker.read_text(encoding="utf-8") == "worktree WIP\n"
|
|
822
|
+
assert registered_worktrees(paths.project_root) == registrations_before
|
|
823
|
+
assert json.loads(status_path.read_text(encoding="utf-8"))["continuation_pending"] is True
|
|
824
|
+
assert json.loads(plan.read_text(encoding="utf-8"))["features"][0]["status"] == "pending"
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def test_replacement_worktree_skips_registered_path_collision_even_when_path_is_absent(monkeypatch, tmp_path):
|
|
828
|
+
from prizmkit_runtime import reset_preserve
|
|
829
|
+
from prizmkit_runtime.gitops import (
|
|
830
|
+
BranchContext,
|
|
831
|
+
RegisteredWorktree,
|
|
832
|
+
WorktreePolicy,
|
|
833
|
+
worktree_runtime_context,
|
|
834
|
+
)
|
|
835
|
+
from prizmkit_runtime.runner_models import family_for
|
|
836
|
+
from prizmkit_runtime.task_checkout import CHECKOUT_MODE_WORKTREE, TaskCheckout
|
|
837
|
+
|
|
838
|
+
paths = _make_paths(tmp_path)
|
|
839
|
+
_init_preserve_runtime_repo(paths)
|
|
840
|
+
family = family_for("feature", paths)
|
|
841
|
+
worktree_root = tmp_path / "worktrees"
|
|
842
|
+
old_runtime = worktree_runtime_context(
|
|
843
|
+
BranchContext("main", "dev/F-001-old", paths.project_root),
|
|
844
|
+
WorktreePolicy(True, True, True, worktree_root),
|
|
845
|
+
)
|
|
846
|
+
checkout = TaskCheckout.active(
|
|
847
|
+
"feature",
|
|
848
|
+
"F-001",
|
|
849
|
+
"dev/F-001-old",
|
|
850
|
+
"main",
|
|
851
|
+
checkout_mode=CHECKOUT_MODE_WORKTREE,
|
|
852
|
+
worktree_path=str(old_runtime.worktree_path),
|
|
853
|
+
)
|
|
854
|
+
item = reset_preserve.PreservePlanItem(
|
|
855
|
+
"F-001", "Root", "001-root", "failed", (), 0
|
|
856
|
+
)
|
|
857
|
+
monkeypatch.setattr(reset_preserve.time, "strftime", lambda *_args: "20260725120000")
|
|
858
|
+
colliding_runtime = worktree_runtime_context(
|
|
859
|
+
BranchContext(
|
|
860
|
+
"main",
|
|
861
|
+
"dev/F-001-resume-20260725120000",
|
|
862
|
+
paths.project_root,
|
|
863
|
+
),
|
|
864
|
+
WorktreePolicy(True, True, True, worktree_root),
|
|
865
|
+
)
|
|
866
|
+
registration = RegisteredWorktree(
|
|
867
|
+
colliding_runtime.worktree_path,
|
|
868
|
+
"different/branch-with-same-normalized-path",
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
branch, runtime = reset_preserve._replacement_identity(
|
|
872
|
+
family,
|
|
873
|
+
item,
|
|
874
|
+
checkout,
|
|
875
|
+
paths.project_root,
|
|
876
|
+
(registration,),
|
|
877
|
+
old_runtime,
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
assert branch == "dev/F-001-resume-20260725120000-1"
|
|
881
|
+
assert runtime is not None
|
|
882
|
+
assert runtime.worktree_path != colliding_runtime.worktree_path
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def test_preserve_runtime_missing_worktree_copies_reference_artifacts_without_deleting_old_site(tmp_path):
|
|
886
|
+
from prizmkit_runtime.gitops import BranchContext, WorktreePolicy, worktree_runtime_context
|
|
887
|
+
from prizmkit_runtime.reset import run_reset_command
|
|
888
|
+
from prizmkit_runtime.runner_models import family_for
|
|
889
|
+
from prizmkit_runtime.task_checkout import (
|
|
890
|
+
CHECKOUT_MODE_WORKTREE,
|
|
891
|
+
TaskCheckout,
|
|
892
|
+
load_task_checkout,
|
|
893
|
+
persist_task_checkout,
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
paths = _make_paths(tmp_path)
|
|
897
|
+
_init_preserve_runtime_repo(paths)
|
|
898
|
+
plan = _write_family_plan(
|
|
899
|
+
paths,
|
|
900
|
+
"feature",
|
|
901
|
+
[{"id": "F-001", "title": "Root", "status": "failed", "dependencies": []}],
|
|
902
|
+
)
|
|
903
|
+
family, status_path, before_status, _branch_checkout = _write_preserve_runtime_state(
|
|
904
|
+
paths, "feature", "F-001", "dev/F-001-deleted-worktree"
|
|
905
|
+
)
|
|
906
|
+
old_runtime = worktree_runtime_context(
|
|
907
|
+
BranchContext("main", "dev/F-001-deleted-worktree", paths.project_root),
|
|
908
|
+
WorktreePolicy(True, True, True, tmp_path / "worktrees"),
|
|
909
|
+
)
|
|
910
|
+
old_runtime.worktree_path.mkdir(parents=True)
|
|
911
|
+
old_checkout = TaskCheckout.active(
|
|
912
|
+
"feature",
|
|
913
|
+
"F-001",
|
|
914
|
+
"dev/F-001-deleted-worktree",
|
|
915
|
+
"main",
|
|
916
|
+
checkout_mode=CHECKOUT_MODE_WORKTREE,
|
|
917
|
+
worktree_path=str(old_runtime.worktree_path),
|
|
918
|
+
)
|
|
919
|
+
persist_task_checkout(family.state_dir, old_checkout)
|
|
920
|
+
old_artifact = old_runtime.worktree_path / ".prizmkit" / "specs" / "001-root"
|
|
921
|
+
old_checkpoint = _write_semantic_completion_checkpoint(old_runtime.worktree_path, old_artifact)
|
|
922
|
+
_add_completed_plan_step(old_checkpoint)
|
|
923
|
+
old_marker = old_artifact / "partial.md"
|
|
924
|
+
old_marker.write_text("old reference\n", encoding="utf-8")
|
|
925
|
+
external_reference = tmp_path / "external-reference.txt"
|
|
926
|
+
external_reference.write_text("do not copy through symlink\n", encoding="utf-8")
|
|
927
|
+
old_link = old_artifact / "external-reference"
|
|
928
|
+
old_link.symlink_to(external_reference)
|
|
929
|
+
old_checkpoint_before = old_checkpoint.read_bytes()
|
|
930
|
+
|
|
931
|
+
result = run_reset_command("feature", ("--failed", "--preserve-runtime"), paths)
|
|
932
|
+
|
|
933
|
+
assert result.exit_code == 0, result.render()
|
|
934
|
+
checkout = load_task_checkout(family.state_dir, "feature", "F-001")
|
|
935
|
+
assert checkout is not None and checkout.is_active and checkout.uses_worktree
|
|
936
|
+
assert checkout.active_dev_branch.startswith("dev/F-001-resume-")
|
|
937
|
+
assert Path(checkout.worktree_path) != old_runtime.worktree_path
|
|
938
|
+
assert Path(checkout.worktree_path).is_dir()
|
|
939
|
+
assert old_runtime.worktree_path.is_dir()
|
|
940
|
+
assert old_checkpoint.read_bytes() == old_checkpoint_before
|
|
941
|
+
assert old_marker.read_text(encoding="utf-8") == "old reference\n"
|
|
942
|
+
copied_artifact = Path(checkout.worktree_path) / ".prizmkit" / "specs" / "001-root"
|
|
943
|
+
assert (copied_artifact / "partial.md").read_text(encoding="utf-8") == "old reference\n"
|
|
944
|
+
copied_link = copied_artifact / "external-reference"
|
|
945
|
+
assert copied_link.is_symlink()
|
|
946
|
+
assert copied_link.resolve() == external_reference.resolve()
|
|
947
|
+
copied_checkpoint = json.loads((copied_artifact / "workflow-checkpoint.json").read_text(encoding="utf-8"))
|
|
948
|
+
assert copied_checkpoint["steps"][0]["status"] == "completed"
|
|
949
|
+
assert all(step["status"] == "pending" for step in copied_checkpoint["steps"][1:])
|
|
950
|
+
expected_status = dict(before_status)
|
|
951
|
+
expected_status.update(
|
|
952
|
+
retry_count=0,
|
|
953
|
+
infra_error_count=0,
|
|
954
|
+
continuation_pending=True,
|
|
955
|
+
active_dev_branch=checkout.active_dev_branch,
|
|
956
|
+
base_branch="main",
|
|
957
|
+
)
|
|
958
|
+
assert json.loads(status_path.read_text(encoding="utf-8")) == expected_status
|
|
959
|
+
assert json.loads(plan.read_text(encoding="utf-8"))["features"][0]["status"] == "pending"
|