prizmkit 1.1.87 → 1.1.89
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-bugfix.sh +0 -26
- package/bundled/dev-pipeline/run-feature.sh +0 -15
- package/bundled/dev-pipeline/run-refactor.sh +0 -26
- package/bundled/dev-pipeline/scripts/prizmkit-test-gate.py +446 -0
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +11 -232
- package/bundled/dev-pipeline/tests/conftest.py +1 -0
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +5 -0
- package/bundled/dev-pipeline/tests/test_prizmkit_test_gate.py +107 -0
- package/bundled/dev-pipeline-windows/scripts/prizmkit-test-gate.py +446 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-prizmkit-test.md +11 -158
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
### Scoped Feature Test Gate — PrizmKit Test
|
|
2
2
|
|
|
3
|
-
**Goal**: Generate and verify tests for this feature's changed scope before code review.
|
|
3
|
+
**Goal**: Generate and verify tests for this feature's changed scope before code review without embedding the full gate implementation in this prompt.
|
|
4
4
|
|
|
5
5
|
Create a start marker immediately before invoking the skill so the gate can reject stale reports from older sessions:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
touch "$test_gate_marker"
|
|
8
|
+
python3 "$PIPELINE_DIR/scripts/prizmkit-test-gate.py" start \
|
|
9
|
+
--feature-slug "{{FEATURE_SLUG}}"
|
|
11
10
|
```
|
|
12
11
|
|
|
13
12
|
Run `/prizmkit-test` with the feature artifact directory:
|
|
@@ -24,237 +23,17 @@ Rules:
|
|
|
24
23
|
- If failures are baseline or unrelated to this feature, document them in the report and continue only when no in-scope failure remains unexplained.
|
|
25
24
|
|
|
26
25
|
**Gate Check — Test Report**:
|
|
27
|
-
After `/prizmkit-test` returns,
|
|
28
|
-
```bash
|
|
29
|
-
feature_artifact_dir=".prizmkit/specs/{{FEATURE_SLUG}}/"
|
|
30
|
-
latest_test_report=$(python3 - "$test_gate_marker" <<'PY'
|
|
31
|
-
import os
|
|
32
|
-
import sys
|
|
33
|
-
|
|
34
|
-
marker = sys.argv[1]
|
|
35
|
-
try:
|
|
36
|
-
marker_mtime = os.path.getmtime(marker)
|
|
37
|
-
except OSError:
|
|
38
|
-
sys.exit(0)
|
|
39
|
-
|
|
40
|
-
matches = []
|
|
41
|
-
for root, _dirs, files in os.walk(".prizmkit/test"):
|
|
42
|
-
if "test-report.md" not in files:
|
|
43
|
-
continue
|
|
44
|
-
path = os.path.join(root, "test-report.md")
|
|
45
|
-
try:
|
|
46
|
-
report_mtime = os.path.getmtime(path)
|
|
47
|
-
except OSError:
|
|
48
|
-
continue
|
|
49
|
-
if report_mtime >= marker_mtime:
|
|
50
|
-
matches.append((report_mtime, path))
|
|
51
|
-
|
|
52
|
-
if matches:
|
|
53
|
-
print(sorted(matches)[-1][1])
|
|
54
|
-
PY
|
|
55
|
-
)
|
|
56
|
-
if [ -n "$latest_test_report" ]; then
|
|
57
|
-
report_mode=$(awk '
|
|
58
|
-
/^## Scope[[:space:]]*$/ { in_scope=1; next }
|
|
59
|
-
/^## / && in_scope { exit }
|
|
60
|
-
in_scope && /^- Mode:/ { sub(/^- Mode:[[:space:]]*/, ""); print; exit }
|
|
61
|
-
' "$latest_test_report")
|
|
62
|
-
report_artifact_dir=$(awk '
|
|
63
|
-
/^## Scope[[:space:]]*$/ { in_scope=1; next }
|
|
64
|
-
/^## / && in_scope { exit }
|
|
65
|
-
in_scope && /^- Artifact Dir:/ { sub(/^- Artifact Dir:[[:space:]]*/, ""); print; exit }
|
|
66
|
-
' "$latest_test_report")
|
|
67
|
-
report_verdict=$(awk '
|
|
68
|
-
/^## Verdict[[:space:]]*$/ { in_verdict=1; next }
|
|
69
|
-
in_verdict && NF { gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print; exit }
|
|
70
|
-
' "$latest_test_report")
|
|
71
|
-
boundary_gate_passed=$(awk '
|
|
72
|
-
/^## Boundary Completion Gate[[:space:]]*$/ { in_gate=1; next }
|
|
73
|
-
/^## / && in_gate { exit }
|
|
74
|
-
in_gate && /^-?[[:space:]]*Completion gate passed:[[:space:]]*yes[[:space:]]*$/ { print "yes"; exit }
|
|
75
|
-
' "$latest_test_report")
|
|
76
|
-
boundary_missing=$(awk '
|
|
77
|
-
/^## Boundary Completion Gate[[:space:]]*$/ { in_gate=1; next }
|
|
78
|
-
/^## / && in_gate { exit }
|
|
79
|
-
in_gate && /^-?[[:space:]]*Boundary-missing:/ { sub(/^.*:[[:space:]]*/, ""); print; exit }
|
|
80
|
-
' "$latest_test_report")
|
|
81
|
-
happy_path_only=$(awk '
|
|
82
|
-
/^## Boundary Completion Gate[[:space:]]*$/ { in_gate=1; next }
|
|
83
|
-
/^## / && in_gate { exit }
|
|
84
|
-
in_gate && /^-?[[:space:]]*Happy-path-only:/ { sub(/^.*:[[:space:]]*/, ""); print; exit }
|
|
85
|
-
' "$latest_test_report")
|
|
86
|
-
boundary_columns_ok=$(awk '
|
|
87
|
-
/^## Boundary Matrix[[:space:]]*$/ { in_matrix=1; next }
|
|
88
|
-
/^## / && in_matrix { exit }
|
|
89
|
-
in_matrix && /^\|/ {
|
|
90
|
-
if ($0 ~ /\|[[:space:]]*Module[[:space:]]*\|/ &&
|
|
91
|
-
$0 ~ /\|[[:space:]]*Interface[[:space:]]*\|/ &&
|
|
92
|
-
$0 ~ /\|[[:space:]]*Service Type[[:space:]]*\|/ &&
|
|
93
|
-
$0 ~ /\|[[:space:]]*Happy Path[[:space:]]*\|/ &&
|
|
94
|
-
$0 ~ /\|[[:space:]]*Request Validation[[:space:]]*\|/ &&
|
|
95
|
-
$0 ~ /\|[[:space:]]*Auth \/ Permission \/ Ownership[[:space:]]*\|/ &&
|
|
96
|
-
$0 ~ /\|[[:space:]]*Domain Invariants[[:space:]]*\|/ &&
|
|
97
|
-
$0 ~ /\|[[:space:]]*Collection \/ Pagination[[:space:]]*\|/ &&
|
|
98
|
-
$0 ~ /\|[[:space:]]*Date \/ Time[[:space:]]*\|/ &&
|
|
99
|
-
$0 ~ /\|[[:space:]]*State Transitions[[:space:]]*\|/ &&
|
|
100
|
-
$0 ~ /\|[[:space:]]*Dependency Failure[[:space:]]*\|/ &&
|
|
101
|
-
$0 ~ /\|[[:space:]]*Response Contract[[:space:]]*\|/ &&
|
|
102
|
-
$0 ~ /\|[[:space:]]*N\/A Reasons[[:space:]]*\|/ &&
|
|
103
|
-
$0 ~ /\|[[:space:]]*Final Status[[:space:]]*\|/) { print "yes"; exit }
|
|
104
|
-
}
|
|
105
|
-
' "$latest_test_report")
|
|
106
|
-
boundary_incomplete=$(awk '
|
|
107
|
-
/^## Boundary Matrix[[:space:]]*$/ { in_matrix=1; next }
|
|
108
|
-
/^## / && in_matrix { exit }
|
|
109
|
-
in_matrix && /boundary-missing|happy-path-only|unresolved/ { print "yes"; exit }
|
|
110
|
-
' "$latest_test_report")
|
|
111
|
-
boundary_validation_result=$(awk '
|
|
112
|
-
/^## Boundary Validation[[:space:]]*$/ { in_validation=1; next }
|
|
113
|
-
/^## / && in_validation { exit }
|
|
114
|
-
in_validation && /^-?[[:space:]]*Validator result:/ { sub(/^.*:[[:space:]]*/, ""); print; exit }
|
|
115
|
-
' "$latest_test_report" | tr '[:upper:]' '[:lower:]')
|
|
116
|
-
openapi_file=""
|
|
117
|
-
for openapi_candidate in \
|
|
118
|
-
openapi.yaml openapi.yml swagger.yaml swagger.yml \
|
|
119
|
-
docs/openapi.yaml docs/openapi.yml docs/swagger.yaml docs/swagger.yml \
|
|
120
|
-
api/openapi.yaml api/openapi.yml api/swagger.yaml api/swagger.yml
|
|
121
|
-
do
|
|
122
|
-
if [ -f "$openapi_candidate" ]; then
|
|
123
|
-
openapi_file="$openapi_candidate"
|
|
124
|
-
break
|
|
125
|
-
fi
|
|
126
|
-
done
|
|
127
|
-
boundary_validator_passed=0
|
|
128
|
-
for boundary_validator in \
|
|
129
|
-
.claude/command-assets/prizmkit-test/scripts/validate_boundary_report.py \
|
|
130
|
-
.codebuddy/skills/prizmkit-test/scripts/validate_boundary_report.py \
|
|
131
|
-
.agents/skills/prizmkit-test/scripts/validate_boundary_report.py
|
|
132
|
-
do
|
|
133
|
-
if [ -f "$boundary_validator" ]; then
|
|
134
|
-
boundary_validator_args=(--report "$latest_test_report")
|
|
135
|
-
if [ -n "$openapi_file" ]; then
|
|
136
|
-
boundary_validator_args+=(--openapi "$openapi_file")
|
|
137
|
-
fi
|
|
138
|
-
if python3 "$boundary_validator" "${boundary_validator_args[@]}" >/dev/null 2>&1; then
|
|
139
|
-
boundary_validator_passed=1
|
|
140
|
-
break
|
|
141
|
-
fi
|
|
142
|
-
fi
|
|
143
|
-
done
|
|
144
|
-
|
|
145
|
-
scope_fresh_ok=$(python3 - "$latest_test_report" "$test_gate_marker" <<'PY'
|
|
146
|
-
import os
|
|
147
|
-
import sys
|
|
148
|
-
|
|
149
|
-
report_path, marker_path = sys.argv[1], sys.argv[2]
|
|
150
|
-
try:
|
|
151
|
-
report_mtime = os.path.getmtime(report_path)
|
|
152
|
-
marker_mtime = os.path.getmtime(marker_path)
|
|
153
|
-
except OSError:
|
|
154
|
-
print("0")
|
|
155
|
-
sys.exit(0)
|
|
156
|
-
if report_mtime < marker_mtime:
|
|
157
|
-
print("0")
|
|
158
|
-
sys.exit(0)
|
|
159
|
-
try:
|
|
160
|
-
with open(report_path, "r", encoding="utf-8") as report_file:
|
|
161
|
-
report_text = report_file.read()
|
|
162
|
-
except OSError:
|
|
163
|
-
print("0")
|
|
164
|
-
sys.exit(0)
|
|
165
|
-
|
|
166
|
-
def scope_list(name):
|
|
167
|
-
in_scope = False
|
|
168
|
-
in_list = False
|
|
169
|
-
marker = f"- {name}:"
|
|
170
|
-
values = []
|
|
171
|
-
for raw_line in report_text.splitlines():
|
|
172
|
-
line = raw_line.strip()
|
|
173
|
-
if line == "## Scope":
|
|
174
|
-
in_scope = True
|
|
175
|
-
continue
|
|
176
|
-
if in_scope and line.startswith("## "):
|
|
177
|
-
break
|
|
178
|
-
if not in_scope:
|
|
179
|
-
continue
|
|
180
|
-
if line == marker:
|
|
181
|
-
in_list = True
|
|
182
|
-
continue
|
|
183
|
-
if in_list:
|
|
184
|
-
if raw_line.startswith(" -") or raw_line.startswith("\t-"):
|
|
185
|
-
value = line[2:].strip().strip("`")
|
|
186
|
-
if value and value not in {"{...}", "..."}:
|
|
187
|
-
values.append(value)
|
|
188
|
-
continue
|
|
189
|
-
if line.startswith("- "):
|
|
190
|
-
break
|
|
191
|
-
return values
|
|
192
|
-
|
|
193
|
-
source_files = scope_list("In-Scope Source Files")
|
|
194
|
-
test_files = scope_list("In-Scope Test Files")
|
|
195
|
-
if not source_files or not test_files:
|
|
196
|
-
print("0")
|
|
197
|
-
sys.exit(0)
|
|
26
|
+
After `/prizmkit-test` returns, run the gate script to locate the newest current-run report, validate scope/freshness/boundary coverage, write `test-report-path.txt`, and mark the `prizmkit-test` checkpoint completed only on `GATE:PASS`:
|
|
198
27
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
print("0")
|
|
205
|
-
sys.exit(0)
|
|
206
|
-
candidate = normalized if os.path.isabs(normalized) else os.path.join(os.getcwd(), normalized)
|
|
207
|
-
candidate_real = os.path.realpath(candidate)
|
|
208
|
-
try:
|
|
209
|
-
if os.path.commonpath([project_root, candidate_real]) != project_root:
|
|
210
|
-
print("0")
|
|
211
|
-
sys.exit(0)
|
|
212
|
-
except ValueError:
|
|
213
|
-
print("0")
|
|
214
|
-
sys.exit(0)
|
|
215
|
-
if not os.path.isfile(candidate_real):
|
|
216
|
-
print("0")
|
|
217
|
-
sys.exit(0)
|
|
218
|
-
if os.path.getmtime(candidate_real) > report_mtime:
|
|
219
|
-
print("0")
|
|
220
|
-
sys.exit(0)
|
|
221
|
-
print("1")
|
|
222
|
-
PY
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
artifact_ok=0
|
|
226
|
-
case "$report_artifact_dir" in
|
|
227
|
-
"$feature_artifact_dir"|"${feature_artifact_dir%/}") artifact_ok=1 ;;
|
|
228
|
-
esac
|
|
229
|
-
|
|
230
|
-
boundary_ok=0
|
|
231
|
-
if [ "$boundary_columns_ok" = "yes" ] && [ -z "$boundary_incomplete" ] && [ "$boundary_gate_passed" = "yes" ] && [ "$boundary_missing" = "0" ] && [ "$happy_path_only" = "0" ] && [ "$boundary_validator_passed" = "1" ] && [ "$scope_fresh_ok" = "1" ]; then
|
|
232
|
-
case "$boundary_validation_result" in
|
|
233
|
-
passed) boundary_ok=1 ;;
|
|
234
|
-
esac
|
|
235
|
-
fi
|
|
236
|
-
|
|
237
|
-
if [ "$report_mode" = "this-change" ] && [ "$artifact_ok" = "1" ] && [ "$report_verdict" = "PASS" ] && [ "$boundary_ok" = "1" ]; then
|
|
238
|
-
printf "%s\n" "$latest_test_report" > .prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt
|
|
239
|
-
echo "GATE:PASS $latest_test_report"
|
|
240
|
-
else
|
|
241
|
-
echo "GATE:FAIL verdict=$report_verdict mode=$report_mode artifact_dir=$report_artifact_dir boundary_gate=$boundary_gate_passed boundary_missing=$boundary_missing happy_path_only=$happy_path_only boundary_validation=$boundary_validation_result validator_passed=$boundary_validator_passed scope_fresh=$scope_fresh_ok openapi=$openapi_file report=$latest_test_report"
|
|
242
|
-
fi
|
|
243
|
-
else
|
|
244
|
-
echo "GATE:MISSING"
|
|
245
|
-
fi
|
|
28
|
+
```bash
|
|
29
|
+
python3 "$PIPELINE_DIR/scripts/prizmkit-test-gate.py" check \
|
|
30
|
+
--feature-slug "{{FEATURE_SLUG}}" \
|
|
31
|
+
--artifact-dir ".prizmkit/specs/{{FEATURE_SLUG}}/" \
|
|
32
|
+
--checkpoint-path "{{CHECKPOINT_PATH}}"
|
|
246
33
|
```
|
|
247
34
|
|
|
248
35
|
Gate outcomes:
|
|
249
|
-
- `GATE:PASS` → append the report path and a 3-5 bullet summary to `context-snapshot.md` under `## PrizmKit Test Gate`, then
|
|
36
|
+
- `GATE:PASS` → append the report path and a 3-5 bullet summary to `context-snapshot.md` under `## PrizmKit Test Gate`, then proceed to the next checkpoint step. The gate script already wrote `test-report-path.txt` and marked `prizmkit-test` completed.
|
|
250
37
|
- `GATE:FAIL` with `NEEDS_FIXES` → read the report's `In-Scope Failures`, fix the feature implementation or generated tests within this feature scope, rerun `/prizmkit-test`, and repeat the gate check. Do not proceed to code review while the verdict is not `PASS`.
|
|
251
|
-
- `GATE:FAIL` with `BLOCKED`, wrong mode, wrong artifact dir,
|
|
38
|
+
- `GATE:FAIL` with `BLOCKED`, wrong mode, wrong artifact dir, unreadable report, stale scope, boundary failure, validator failure, or checkpoint update failure → write `.prizmkit/specs/{{FEATURE_SLUG}}/failure-log.md` with the scoped test failure and stop for recovery.
|
|
252
39
|
- `GATE:MISSING` → perform one bounded status check: inspect `/prizmkit-test` output and `.prizmkit/test/` for a report directory. If no current-run report exists, write `failure-log.md` and stop for recovery.
|
|
253
|
-
|
|
254
|
-
Only after `GATE:PASS`, run the update script to set step `prizmkit-test` to `"completed"`:
|
|
255
|
-
```bash
|
|
256
|
-
python3 $PIPELINE_DIR/scripts/update-checkpoint.py \
|
|
257
|
-
--checkpoint-path {{CHECKPOINT_PATH}} \
|
|
258
|
-
--step prizmkit-test \
|
|
259
|
-
--status completed
|
|
260
|
-
```
|
|
@@ -24,4 +24,5 @@ def _load_hyphenated_module(module_name, filename):
|
|
|
24
24
|
# Register hyphenated script files so tests can import them with underscores.
|
|
25
25
|
_load_hyphenated_module("generate_bootstrap_prompt", "generate-bootstrap-prompt.py")
|
|
26
26
|
_load_hyphenated_module("generate_bugfix_prompt", "generate-bugfix-prompt.py")
|
|
27
|
+
_load_hyphenated_module("prizmkit_test_gate", "prizmkit-test-gate.py")
|
|
27
28
|
_load_hyphenated_module("update_feature_status", "update-feature-status.py")
|
|
@@ -315,9 +315,14 @@ class TestScopedFeatureTestGate:
|
|
|
315
315
|
assert names.index("phase-implement") < names.index("phase-prizmkit-test") < names.index("phase-review")
|
|
316
316
|
|
|
317
317
|
rendered = "\n".join(content for _, content in sections)
|
|
318
|
+
assert "prizmkit-test-gate.py" in rendered
|
|
319
|
+
assert "prizmkit-test-gate.py\" start" in rendered
|
|
320
|
+
assert "prizmkit-test-gate.py\" check" in rendered
|
|
318
321
|
assert "/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/" in rendered
|
|
319
322
|
assert ".prizmkit/bugfix/" in rendered # explicitly forbidden in the gate wording
|
|
320
323
|
assert ".prizmkit/refactor/" in rendered # explicitly forbidden in the gate wording
|
|
324
|
+
assert "latest_test_report=$(python3" not in rendered
|
|
325
|
+
assert "boundary_columns_ok=$(awk" not in rendered
|
|
321
326
|
|
|
322
327
|
def test_lite_sections_include_prizmkit_test_before_commit(self):
|
|
323
328
|
sections_dir = Path(__file__).resolve().parents[1] / "templates" / "sections"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Tests for prizmkit-test-gate.py."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from prizmkit_test_gate import check_gate, start_gate
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def scoped_report_text():
|
|
12
|
+
return "\n".join([
|
|
13
|
+
"# Test Report",
|
|
14
|
+
"",
|
|
15
|
+
"## Scope",
|
|
16
|
+
"- Mode: this-change",
|
|
17
|
+
"- Artifact Dir: .prizmkit/specs/123-scope-test/",
|
|
18
|
+
"- In-Scope Source Files:",
|
|
19
|
+
" - src/feature.js",
|
|
20
|
+
"- In-Scope Test Files:",
|
|
21
|
+
" - tests/feature.test.js",
|
|
22
|
+
"",
|
|
23
|
+
"## Boundary Matrix",
|
|
24
|
+
"| Module | Interface | Service Type | Happy Path | Request Validation | Auth / Permission / Ownership | Domain Invariants | Collection / Pagination | Date / Time | State Transitions | Dependency Failure | Response Contract | N/A Reasons | Final Status |",
|
|
25
|
+
"|--------|-----------|--------------|------------|--------------------|-------------------------------|-------------------|-------------------------|-------------|-------------------|-------------------|-------------------|-------------|--------------|",
|
|
26
|
+
"| feature | POST /feature | API | covered | covered | covered | covered | covered | covered | covered | covered | covered | none | boundary-covered |",
|
|
27
|
+
"",
|
|
28
|
+
"## Boundary Completion Gate",
|
|
29
|
+
"- Interfaces total: 1",
|
|
30
|
+
"- Boundary-covered: 1",
|
|
31
|
+
"- Boundary-missing: 0",
|
|
32
|
+
"- Happy-path-only: 0",
|
|
33
|
+
"- Completion gate passed: yes",
|
|
34
|
+
"",
|
|
35
|
+
"## Boundary Validation",
|
|
36
|
+
"- Validator command: `python3 validate_boundary_report.py`",
|
|
37
|
+
"- Validator result: passed",
|
|
38
|
+
"- Validator output: Boundary report validation: PASS",
|
|
39
|
+
"",
|
|
40
|
+
"## Verdict",
|
|
41
|
+
"PASS",
|
|
42
|
+
"",
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def prepare_project(tmp_path):
|
|
47
|
+
(tmp_path / "src").mkdir()
|
|
48
|
+
(tmp_path / "tests").mkdir()
|
|
49
|
+
(tmp_path / "src" / "feature.js").write_text("export const feature = true;\n", encoding="utf-8")
|
|
50
|
+
(tmp_path / "tests" / "feature.test.js").write_text("test('feature', () => {});\n", encoding="utf-8")
|
|
51
|
+
|
|
52
|
+
validator = tmp_path / ".claude" / "command-assets" / "prizmkit-test" / "scripts" / "validate_boundary_report.py"
|
|
53
|
+
validator.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
validator.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
55
|
+
|
|
56
|
+
checkpoint = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "workflow-checkpoint.json"
|
|
57
|
+
checkpoint.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
checkpoint.write_text(
|
|
59
|
+
json.dumps({"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "pending"}]}),
|
|
60
|
+
encoding="utf-8",
|
|
61
|
+
)
|
|
62
|
+
return checkpoint
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def write_report(tmp_path):
|
|
66
|
+
report = tmp_path / ".prizmkit" / "test" / "run" / "test-report.md"
|
|
67
|
+
report.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
time.sleep(0.01)
|
|
69
|
+
report.write_text(scoped_report_text(), encoding="utf-8")
|
|
70
|
+
return report
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_gate_pass_writes_pointer_and_updates_checkpoint(tmp_path, monkeypatch):
|
|
74
|
+
monkeypatch.chdir(tmp_path)
|
|
75
|
+
checkpoint = prepare_project(tmp_path)
|
|
76
|
+
start = start_gate("123-scope-test")
|
|
77
|
+
report = write_report(tmp_path)
|
|
78
|
+
|
|
79
|
+
result = check_gate(
|
|
80
|
+
"123-scope-test",
|
|
81
|
+
".prizmkit/specs/123-scope-test/",
|
|
82
|
+
str(checkpoint),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
assert start["gate"] == "GATE:START"
|
|
86
|
+
assert result["status"] == "PASS"
|
|
87
|
+
assert result["report"] == os.path.join(".prizmkit", "test", "run", "test-report.md")
|
|
88
|
+
pointer = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
|
|
89
|
+
assert pointer.read_text(encoding="utf-8").strip() == str(report.relative_to(tmp_path))
|
|
90
|
+
checkpoint_data = json.loads(checkpoint.read_text(encoding="utf-8"))
|
|
91
|
+
assert checkpoint_data["steps"][0]["status"] == "completed"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_gate_fails_when_in_scope_file_is_newer_than_report(tmp_path, monkeypatch):
|
|
95
|
+
monkeypatch.chdir(tmp_path)
|
|
96
|
+
prepare_project(tmp_path)
|
|
97
|
+
start_gate("123-scope-test")
|
|
98
|
+
report = write_report(tmp_path)
|
|
99
|
+
now = time.time()
|
|
100
|
+
os.utime(report, (now, now))
|
|
101
|
+
os.utime(tmp_path / "src" / "feature.js", (now + 10, now + 10))
|
|
102
|
+
|
|
103
|
+
result = check_gate("123-scope-test", ".prizmkit/specs/123-scope-test/", "")
|
|
104
|
+
|
|
105
|
+
assert result["status"] == "FAIL"
|
|
106
|
+
assert result["reason"] == "scope_not_fresh"
|
|
107
|
+
assert result["scope_fresh"] == "0"
|