prizmkit 1.0.147 → 1.0.148
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/run.sh +54 -1
- package/bundled/dev-pipeline/scripts/update-feature-status.py +287 -7
- package/bundled/dev-pipeline/templates/feature-list-schema.json +1 -1
- package/bundled/dev-pipeline/tests/conftest.py +1 -0
- package/bundled/dev-pipeline/tests/test_auto_skip.py +446 -0
- package/bundled/skills/_metadata.json +9 -1
- package/bundled/skills/app-planner/SKILL.md +110 -28
- package/bundled/skills/app-planner/scripts/validate-and-generate.py +1 -1
- package/bundled/skills/prizm-kit/SKILL.md +3 -1
- package/bundled/skills/prizmkit-committer/SKILL.md +1 -1
- package/bundled/skills/prizmkit-deploy/SKILL.md +112 -0
- package/bundled/skills/prizmkit-deploy/assets/deploy-template.md +108 -0
- package/bundled/skills/prizmkit-plan/SKILL.md +30 -8
- package/bundled/skills/prizmkit-plan/assets/plan-template.md +19 -0
- package/bundled/skills/prizmkit-retrospective/SKILL.md +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""Tests for auto-skip and unskip functionality in update-feature-status.py."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from update_feature_status import (
|
|
7
|
+
auto_skip_blocked_features,
|
|
8
|
+
TERMINAL_STATUSES,
|
|
9
|
+
load_feature_status,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from utils import load_json_file, write_json_file
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Helpers
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
def _make_feature(fid, title="Feature", deps=None, status="pending"):
|
|
20
|
+
return {
|
|
21
|
+
"id": fid,
|
|
22
|
+
"title": title,
|
|
23
|
+
"description": "desc",
|
|
24
|
+
"priority": "medium",
|
|
25
|
+
"dependencies": deps or [],
|
|
26
|
+
"acceptance_criteria": ["ok"],
|
|
27
|
+
"status": status,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _make_feature_list(features):
|
|
32
|
+
return {
|
|
33
|
+
"$schema": "dev-pipeline-feature-list-v1",
|
|
34
|
+
"app_name": "test-app",
|
|
35
|
+
"features": features,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _init_state(tmp_path, feature_ids):
|
|
40
|
+
"""Create state dir with status.json for each feature."""
|
|
41
|
+
state_dir = str(tmp_path / "state")
|
|
42
|
+
for fid in feature_ids:
|
|
43
|
+
feature_dir = os.path.join(state_dir, "features", fid, "sessions")
|
|
44
|
+
os.makedirs(feature_dir, exist_ok=True)
|
|
45
|
+
status_path = os.path.join(state_dir, "features", fid, "status.json")
|
|
46
|
+
write_json_file(status_path, {
|
|
47
|
+
"feature_id": fid,
|
|
48
|
+
"status": "pending",
|
|
49
|
+
"retry_count": 0,
|
|
50
|
+
"max_retries": 3,
|
|
51
|
+
"sessions": [],
|
|
52
|
+
"last_session_id": None,
|
|
53
|
+
"resume_from_phase": None,
|
|
54
|
+
"created_at": "2026-01-01T00:00:00Z",
|
|
55
|
+
"updated_at": "2026-01-01T00:00:00Z",
|
|
56
|
+
})
|
|
57
|
+
return state_dir
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _write_fl(tmp_path, features):
|
|
61
|
+
"""Write feature-list.json and return its path."""
|
|
62
|
+
fl_path = str(tmp_path / "feature-list.json")
|
|
63
|
+
write_json_file(fl_path, _make_feature_list(features))
|
|
64
|
+
return fl_path
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _read_statuses(fl_path):
|
|
68
|
+
"""Return {feature_id: status} from feature-list.json."""
|
|
69
|
+
data, _ = load_json_file(fl_path)
|
|
70
|
+
return {f["id"]: f["status"] for f in data["features"]}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# TERMINAL_STATUSES
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
class TestTerminalStatuses:
|
|
78
|
+
def test_auto_skipped_is_terminal(self):
|
|
79
|
+
assert "auto_skipped" in TERMINAL_STATUSES
|
|
80
|
+
|
|
81
|
+
def test_split_is_terminal(self):
|
|
82
|
+
assert "split" in TERMINAL_STATUSES
|
|
83
|
+
|
|
84
|
+
def test_all_expected_statuses(self):
|
|
85
|
+
assert TERMINAL_STATUSES == {"completed", "failed", "skipped", "auto_skipped", "split"}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# auto_skip_blocked_features — linear chain
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
class TestAutoSkipLinearChain:
|
|
93
|
+
"""F-001 → F-002 → F-003 linear dependency chain."""
|
|
94
|
+
|
|
95
|
+
def test_failing_root_skips_all_downstream(self, tmp_path):
|
|
96
|
+
features = [
|
|
97
|
+
_make_feature("F-001", "Root", status="failed"),
|
|
98
|
+
_make_feature("F-002", "Mid", deps=["F-001"]),
|
|
99
|
+
_make_feature("F-003", "Leaf", deps=["F-002"]),
|
|
100
|
+
]
|
|
101
|
+
fl_path = _write_fl(tmp_path, features)
|
|
102
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003"])
|
|
103
|
+
|
|
104
|
+
result = auto_skip_blocked_features(fl_path, state_dir, "F-001")
|
|
105
|
+
|
|
106
|
+
assert len(result) == 2
|
|
107
|
+
skipped_ids = {r["feature_id"] for r in result}
|
|
108
|
+
assert skipped_ids == {"F-002", "F-003"}
|
|
109
|
+
|
|
110
|
+
statuses = _read_statuses(fl_path)
|
|
111
|
+
assert statuses["F-001"] == "failed"
|
|
112
|
+
assert statuses["F-002"] == "auto_skipped"
|
|
113
|
+
assert statuses["F-003"] == "auto_skipped"
|
|
114
|
+
|
|
115
|
+
def test_status_json_also_updated(self, tmp_path):
|
|
116
|
+
features = [
|
|
117
|
+
_make_feature("F-001", "Root", status="failed"),
|
|
118
|
+
_make_feature("F-002", "Mid", deps=["F-001"]),
|
|
119
|
+
]
|
|
120
|
+
fl_path = _write_fl(tmp_path, features)
|
|
121
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002"])
|
|
122
|
+
|
|
123
|
+
auto_skip_blocked_features(fl_path, state_dir, "F-001")
|
|
124
|
+
|
|
125
|
+
fs = load_feature_status(state_dir, "F-002")
|
|
126
|
+
assert fs["status"] == "auto_skipped"
|
|
127
|
+
|
|
128
|
+
def test_failing_mid_skips_only_downstream(self, tmp_path):
|
|
129
|
+
features = [
|
|
130
|
+
_make_feature("F-001", "Root", status="completed"),
|
|
131
|
+
_make_feature("F-002", "Mid", deps=["F-001"], status="failed"),
|
|
132
|
+
_make_feature("F-003", "Leaf", deps=["F-002"]),
|
|
133
|
+
]
|
|
134
|
+
fl_path = _write_fl(tmp_path, features)
|
|
135
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003"])
|
|
136
|
+
|
|
137
|
+
result = auto_skip_blocked_features(fl_path, state_dir, "F-002")
|
|
138
|
+
|
|
139
|
+
assert len(result) == 1
|
|
140
|
+
assert result[0]["feature_id"] == "F-003"
|
|
141
|
+
|
|
142
|
+
statuses = _read_statuses(fl_path)
|
|
143
|
+
assert statuses["F-001"] == "completed"
|
|
144
|
+
assert statuses["F-003"] == "auto_skipped"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# auto_skip_blocked_features — diamond dependency
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
class TestAutoSkipDiamond:
|
|
152
|
+
"""A → C, B → C diamond where only A fails."""
|
|
153
|
+
|
|
154
|
+
def test_diamond_skips_downstream(self, tmp_path):
|
|
155
|
+
features = [
|
|
156
|
+
_make_feature("F-001", "A", status="failed"),
|
|
157
|
+
_make_feature("F-002", "B", status="completed"),
|
|
158
|
+
_make_feature("F-003", "C", deps=["F-001", "F-002"]),
|
|
159
|
+
]
|
|
160
|
+
fl_path = _write_fl(tmp_path, features)
|
|
161
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003"])
|
|
162
|
+
|
|
163
|
+
result = auto_skip_blocked_features(fl_path, state_dir, "F-001")
|
|
164
|
+
|
|
165
|
+
assert len(result) == 1
|
|
166
|
+
assert result[0]["feature_id"] == "F-003"
|
|
167
|
+
|
|
168
|
+
def test_no_skip_when_all_deps_ok(self, tmp_path):
|
|
169
|
+
features = [
|
|
170
|
+
_make_feature("F-001", "A", status="completed"),
|
|
171
|
+
_make_feature("F-002", "B", status="completed"),
|
|
172
|
+
_make_feature("F-003", "C", deps=["F-001", "F-002"]),
|
|
173
|
+
]
|
|
174
|
+
fl_path = _write_fl(tmp_path, features)
|
|
175
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003"])
|
|
176
|
+
|
|
177
|
+
result = auto_skip_blocked_features(fl_path, state_dir, "F-001")
|
|
178
|
+
|
|
179
|
+
assert len(result) == 0
|
|
180
|
+
assert _read_statuses(fl_path)["F-003"] == "pending"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
# auto_skip_blocked_features — edge cases
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
class TestAutoSkipEdgeCases:
|
|
188
|
+
def test_no_dependencies_no_skip(self, tmp_path):
|
|
189
|
+
features = [
|
|
190
|
+
_make_feature("F-001", "A", status="failed"),
|
|
191
|
+
_make_feature("F-002", "B"), # independent
|
|
192
|
+
]
|
|
193
|
+
fl_path = _write_fl(tmp_path, features)
|
|
194
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002"])
|
|
195
|
+
|
|
196
|
+
result = auto_skip_blocked_features(fl_path, state_dir, "F-001")
|
|
197
|
+
|
|
198
|
+
assert len(result) == 0
|
|
199
|
+
assert _read_statuses(fl_path)["F-002"] == "pending"
|
|
200
|
+
|
|
201
|
+
def test_already_completed_not_skipped(self, tmp_path):
|
|
202
|
+
features = [
|
|
203
|
+
_make_feature("F-001", "A", status="failed"),
|
|
204
|
+
_make_feature("F-002", "B", deps=["F-001"], status="completed"),
|
|
205
|
+
]
|
|
206
|
+
fl_path = _write_fl(tmp_path, features)
|
|
207
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002"])
|
|
208
|
+
|
|
209
|
+
result = auto_skip_blocked_features(fl_path, state_dir, "F-001")
|
|
210
|
+
|
|
211
|
+
assert len(result) == 0
|
|
212
|
+
assert _read_statuses(fl_path)["F-002"] == "completed"
|
|
213
|
+
|
|
214
|
+
def test_already_auto_skipped_dep_propagates(self, tmp_path):
|
|
215
|
+
"""If a dep is already auto_skipped (from a previous failure),
|
|
216
|
+
new downstream features depending on it should also be skipped."""
|
|
217
|
+
features = [
|
|
218
|
+
_make_feature("F-001", "A", status="failed"),
|
|
219
|
+
_make_feature("F-002", "B", deps=["F-001"], status="auto_skipped"),
|
|
220
|
+
_make_feature("F-003", "C", deps=["F-002"]), # pending
|
|
221
|
+
]
|
|
222
|
+
fl_path = _write_fl(tmp_path, features)
|
|
223
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003"])
|
|
224
|
+
|
|
225
|
+
result = auto_skip_blocked_features(fl_path, state_dir, "F-001")
|
|
226
|
+
|
|
227
|
+
# F-002 is already terminal (auto_skipped), so not re-processed
|
|
228
|
+
# F-003 should be auto_skipped because its dep F-002 is auto_skipped
|
|
229
|
+
skipped_ids = {r["feature_id"] for r in result}
|
|
230
|
+
assert "F-003" in skipped_ids
|
|
231
|
+
assert _read_statuses(fl_path)["F-003"] == "auto_skipped"
|
|
232
|
+
|
|
233
|
+
def test_split_feature_not_skipped(self, tmp_path):
|
|
234
|
+
features = [
|
|
235
|
+
_make_feature("F-001", "A", status="failed"),
|
|
236
|
+
_make_feature("F-002", "B", deps=["F-001"], status="split"),
|
|
237
|
+
]
|
|
238
|
+
fl_path = _write_fl(tmp_path, features)
|
|
239
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002"])
|
|
240
|
+
|
|
241
|
+
result = auto_skip_blocked_features(fl_path, state_dir, "F-001")
|
|
242
|
+
|
|
243
|
+
assert len(result) == 0
|
|
244
|
+
assert _read_statuses(fl_path)["F-002"] == "split"
|
|
245
|
+
|
|
246
|
+
def test_skipped_dep_triggers_auto_skip(self, tmp_path):
|
|
247
|
+
"""When a dependency is manually skipped, downstream should be auto_skipped."""
|
|
248
|
+
features = [
|
|
249
|
+
_make_feature("F-001", "A", status="skipped"),
|
|
250
|
+
_make_feature("F-002", "B", deps=["F-001"]),
|
|
251
|
+
_make_feature("F-003", "C", deps=["F-002"]),
|
|
252
|
+
]
|
|
253
|
+
fl_path = _write_fl(tmp_path, features)
|
|
254
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003"])
|
|
255
|
+
|
|
256
|
+
result = auto_skip_blocked_features(fl_path, state_dir, "F-001")
|
|
257
|
+
|
|
258
|
+
assert len(result) == 2
|
|
259
|
+
statuses = _read_statuses(fl_path)
|
|
260
|
+
assert statuses["F-001"] == "skipped"
|
|
261
|
+
assert statuses["F-002"] == "auto_skipped"
|
|
262
|
+
assert statuses["F-003"] == "auto_skipped"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# action_unskip — via subprocess (tests the full CLI interface)
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
import subprocess
|
|
270
|
+
|
|
271
|
+
_SCRIPT = os.path.join(
|
|
272
|
+
os.path.dirname(__file__), "..", "scripts", "update-feature-status.py"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _run_unskip(fl_path, state_dir, feature_id=None):
|
|
277
|
+
cmd = [
|
|
278
|
+
"python3", _SCRIPT,
|
|
279
|
+
"--feature-list", fl_path,
|
|
280
|
+
"--state-dir", state_dir,
|
|
281
|
+
"--action", "unskip",
|
|
282
|
+
]
|
|
283
|
+
if feature_id:
|
|
284
|
+
cmd += ["--feature-id", feature_id]
|
|
285
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
286
|
+
assert result.returncode == 0, result.stderr
|
|
287
|
+
return json.loads(result.stdout)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _run_get_next(fl_path, state_dir):
|
|
291
|
+
cmd = [
|
|
292
|
+
"python3", _SCRIPT,
|
|
293
|
+
"--feature-list", fl_path,
|
|
294
|
+
"--state-dir", state_dir,
|
|
295
|
+
"--action", "get_next",
|
|
296
|
+
]
|
|
297
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
298
|
+
return result.stdout.strip()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class TestUnskipByFeatureId:
|
|
302
|
+
"""Unskip with --feature-id targets a specific failed feature + downstream."""
|
|
303
|
+
|
|
304
|
+
def test_unskip_failed_root_resets_all(self, tmp_path):
|
|
305
|
+
features = [
|
|
306
|
+
_make_feature("F-001", "Root", status="failed"),
|
|
307
|
+
_make_feature("F-002", "Mid", deps=["F-001"], status="auto_skipped"),
|
|
308
|
+
_make_feature("F-003", "Leaf", deps=["F-002"], status="auto_skipped"),
|
|
309
|
+
]
|
|
310
|
+
fl_path = _write_fl(tmp_path, features)
|
|
311
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003"])
|
|
312
|
+
|
|
313
|
+
result = _run_unskip(fl_path, state_dir, "F-001")
|
|
314
|
+
|
|
315
|
+
assert result["reset_count"] == 3
|
|
316
|
+
statuses = _read_statuses(fl_path)
|
|
317
|
+
assert all(s == "pending" for s in statuses.values())
|
|
318
|
+
|
|
319
|
+
def test_unskip_auto_skipped_leaf_resets_upstream(self, tmp_path):
|
|
320
|
+
"""C2 fix: unskip F-003 must also find and reset F-001 (failed root)."""
|
|
321
|
+
features = [
|
|
322
|
+
_make_feature("F-001", "Root", status="failed"),
|
|
323
|
+
_make_feature("F-002", "Mid", deps=["F-001"], status="auto_skipped"),
|
|
324
|
+
_make_feature("F-003", "Leaf", deps=["F-002"], status="auto_skipped"),
|
|
325
|
+
]
|
|
326
|
+
fl_path = _write_fl(tmp_path, features)
|
|
327
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003"])
|
|
328
|
+
|
|
329
|
+
result = _run_unskip(fl_path, state_dir, "F-003")
|
|
330
|
+
|
|
331
|
+
assert result["reset_count"] == 3
|
|
332
|
+
reset_ids = {f["feature_id"] for f in result["features"]}
|
|
333
|
+
assert "F-001" in reset_ids # transitive upstream reset
|
|
334
|
+
assert all(s == "pending" for s in _read_statuses(fl_path).values())
|
|
335
|
+
|
|
336
|
+
def test_unskip_preserves_completed_features(self, tmp_path):
|
|
337
|
+
features = [
|
|
338
|
+
_make_feature("F-001", "Done", status="completed"),
|
|
339
|
+
_make_feature("F-002", "Failed", deps=["F-001"], status="failed"),
|
|
340
|
+
_make_feature("F-003", "Skipped", deps=["F-002"], status="auto_skipped"),
|
|
341
|
+
]
|
|
342
|
+
fl_path = _write_fl(tmp_path, features)
|
|
343
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003"])
|
|
344
|
+
|
|
345
|
+
result = _run_unskip(fl_path, state_dir, "F-002")
|
|
346
|
+
|
|
347
|
+
assert result["reset_count"] == 2
|
|
348
|
+
statuses = _read_statuses(fl_path)
|
|
349
|
+
assert statuses["F-001"] == "completed" # not touched
|
|
350
|
+
assert statuses["F-002"] == "pending"
|
|
351
|
+
assert statuses["F-003"] == "pending"
|
|
352
|
+
|
|
353
|
+
def test_unskip_skipped_feature_resets_downstream(self, tmp_path):
|
|
354
|
+
"""Unskip a manually-skipped feature should also reset its auto_skipped downstream."""
|
|
355
|
+
features = [
|
|
356
|
+
_make_feature("F-001", "Skipped", status="skipped"),
|
|
357
|
+
_make_feature("F-002", "Child", deps=["F-001"], status="auto_skipped"),
|
|
358
|
+
]
|
|
359
|
+
fl_path = _write_fl(tmp_path, features)
|
|
360
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002"])
|
|
361
|
+
|
|
362
|
+
result = _run_unskip(fl_path, state_dir, "F-001")
|
|
363
|
+
|
|
364
|
+
assert result["reset_count"] == 2
|
|
365
|
+
statuses = _read_statuses(fl_path)
|
|
366
|
+
assert statuses["F-001"] == "pending"
|
|
367
|
+
assert statuses["F-002"] == "pending"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class TestUnskipAll:
|
|
371
|
+
"""Unskip without --feature-id resets all failed + auto_skipped."""
|
|
372
|
+
|
|
373
|
+
def test_resets_all_failed_and_auto_skipped(self, tmp_path):
|
|
374
|
+
features = [
|
|
375
|
+
_make_feature("F-001", "OK", status="completed"),
|
|
376
|
+
_make_feature("F-002", "Failed", status="failed"),
|
|
377
|
+
_make_feature("F-003", "Skipped", deps=["F-002"], status="auto_skipped"),
|
|
378
|
+
_make_feature("F-004", "Pending", status="pending"),
|
|
379
|
+
]
|
|
380
|
+
fl_path = _write_fl(tmp_path, features)
|
|
381
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003", "F-004"])
|
|
382
|
+
|
|
383
|
+
result = _run_unskip(fl_path, state_dir)
|
|
384
|
+
|
|
385
|
+
assert result["reset_count"] == 2
|
|
386
|
+
statuses = _read_statuses(fl_path)
|
|
387
|
+
assert statuses["F-001"] == "completed"
|
|
388
|
+
assert statuses["F-002"] == "pending"
|
|
389
|
+
assert statuses["F-003"] == "pending"
|
|
390
|
+
assert statuses["F-004"] == "pending"
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# ---------------------------------------------------------------------------
|
|
394
|
+
# Integration: auto-skip → get_next → unskip → get_next
|
|
395
|
+
# ---------------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
class TestAutoSkipIntegration:
|
|
398
|
+
"""Full lifecycle: fail → auto-skip → pipeline complete → unskip → resume."""
|
|
399
|
+
|
|
400
|
+
def test_full_lifecycle(self, tmp_path):
|
|
401
|
+
features = [
|
|
402
|
+
_make_feature("F-001", "Root", status="failed"),
|
|
403
|
+
_make_feature("F-002", "Child", deps=["F-001"]),
|
|
404
|
+
]
|
|
405
|
+
fl_path = _write_fl(tmp_path, features)
|
|
406
|
+
state_dir = _init_state(tmp_path, ["F-001", "F-002"])
|
|
407
|
+
|
|
408
|
+
# 1. Auto-skip cascades
|
|
409
|
+
auto_skip_blocked_features(fl_path, state_dir, "F-001")
|
|
410
|
+
assert _read_statuses(fl_path)["F-002"] == "auto_skipped"
|
|
411
|
+
|
|
412
|
+
# 2. get_next should return PIPELINE_COMPLETE
|
|
413
|
+
assert _run_get_next(fl_path, state_dir) == "PIPELINE_COMPLETE"
|
|
414
|
+
|
|
415
|
+
# 3. Unskip recovers everything
|
|
416
|
+
_run_unskip(fl_path, state_dir, "F-001")
|
|
417
|
+
statuses = _read_statuses(fl_path)
|
|
418
|
+
assert statuses["F-001"] == "pending"
|
|
419
|
+
assert statuses["F-002"] == "pending"
|
|
420
|
+
|
|
421
|
+
# 4. get_next should now return F-001
|
|
422
|
+
output = _run_get_next(fl_path, state_dir)
|
|
423
|
+
data = json.loads(output)
|
|
424
|
+
assert data["feature_id"] == "F-001"
|
|
425
|
+
|
|
426
|
+
def test_retry_count_reset_after_unskip(self, tmp_path):
|
|
427
|
+
features = [
|
|
428
|
+
_make_feature("F-001", "Root", status="failed"),
|
|
429
|
+
]
|
|
430
|
+
fl_path = _write_fl(tmp_path, features)
|
|
431
|
+
state_dir = _init_state(tmp_path, ["F-001"])
|
|
432
|
+
# Simulate retry_count = 3
|
|
433
|
+
fs_path = os.path.join(state_dir, "features", "F-001", "status.json")
|
|
434
|
+
with open(fs_path) as f:
|
|
435
|
+
fs = json.load(f)
|
|
436
|
+
fs["retry_count"] = 3
|
|
437
|
+
fs["status"] = "failed"
|
|
438
|
+
with open(fs_path, "w") as f:
|
|
439
|
+
json.dump(fs, f)
|
|
440
|
+
|
|
441
|
+
_run_unskip(fl_path, state_dir, "F-001")
|
|
442
|
+
|
|
443
|
+
with open(fs_path) as f:
|
|
444
|
+
fs = json.load(f)
|
|
445
|
+
assert fs["retry_count"] == 0
|
|
446
|
+
assert fs["status"] == "pending"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.
|
|
2
|
+
"version": "1.0.148",
|
|
3
3
|
"skills": {
|
|
4
4
|
"prizm-kit": {
|
|
5
5
|
"description": "Full-lifecycle dev toolkit. Covers spec-driven development, Prizm context docs, code quality, debugging, deployment, and knowledge management.",
|
|
@@ -78,6 +78,13 @@
|
|
|
78
78
|
"hasAssets": true,
|
|
79
79
|
"hasScripts": false
|
|
80
80
|
},
|
|
81
|
+
"prizmkit-deploy": {
|
|
82
|
+
"description": "Generate or update deployment documentation (DEPLOY.md) by scanning project state and deploy strategy. Fully autonomous.",
|
|
83
|
+
"tier": "1",
|
|
84
|
+
"category": "prizmkit-skill",
|
|
85
|
+
"hasAssets": true,
|
|
86
|
+
"hasScripts": false
|
|
87
|
+
},
|
|
81
88
|
"prizmkit-tool-tech-debt-tracker": {
|
|
82
89
|
"description": "Identify and track technical debt via code pattern analysis.",
|
|
83
90
|
"tier": "1",
|
|
@@ -252,6 +259,7 @@
|
|
|
252
259
|
"prizmkit-code-review",
|
|
253
260
|
"prizmkit-committer",
|
|
254
261
|
"prizmkit-retrospective",
|
|
262
|
+
"prizmkit-deploy",
|
|
255
263
|
"feature-workflow",
|
|
256
264
|
"refactor-workflow",
|
|
257
265
|
"app-planner",
|