prizmkit 1.1.86 → 1.1.88

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.
Files changed (45) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +462 -4
  3. package/bundled/dev-pipeline/scripts/prizmkit-test-gate.py +446 -0
  4. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
  5. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +14 -0
  6. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +18 -3
  7. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +18 -3
  8. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +2 -2
  9. package/bundled/dev-pipeline/templates/sections/phase-commit.md +2 -2
  10. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +39 -0
  11. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +5 -1
  12. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +5 -1
  13. package/bundled/dev-pipeline/tests/conftest.py +1 -0
  14. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +434 -0
  15. package/bundled/dev-pipeline/tests/test_prizmkit_test_gate.py +107 -0
  16. package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +462 -4
  17. package/bundled/dev-pipeline-windows/scripts/prizmkit-test-gate.py +446 -0
  18. package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +1 -1
  19. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +14 -0
  20. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +18 -3
  21. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +18 -3
  22. package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +2 -2
  23. package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +2 -2
  24. package/bundled/dev-pipeline-windows/templates/sections/phase-prizmkit-test.md +39 -0
  25. package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +5 -1
  26. package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +5 -1
  27. package/bundled/skills/_metadata.json +1 -1
  28. package/bundled/skills/prizmkit-code-review/SKILL.md +7 -2
  29. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +6 -2
  30. package/bundled/skills/prizmkit-test/SKILL.md +106 -14
  31. package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +220 -0
  32. package/bundled/skills/prizmkit-test/references/examples.md +82 -9
  33. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +68 -12
  34. package/bundled/skills/prizmkit-test/references/test-report-template.md +73 -9
  35. package/bundled/skills/prizmkit-test/scripts/validate_boundary_report.py +347 -0
  36. package/bundled/skills-windows/prizmkit-code-review/SKILL.md +7 -2
  37. package/bundled/skills-windows/prizmkit-code-review/references/reviewer-agent-prompt.md +6 -2
  38. package/bundled/skills-windows/prizmkit-test/SKILL.md +106 -14
  39. package/bundled/skills-windows/prizmkit-test/references/boundary-coverage-protocol.md +220 -0
  40. package/bundled/skills-windows/prizmkit-test/references/examples.md +82 -9
  41. package/bundled/skills-windows/prizmkit-test/references/service-boundary-test-catalog.md +4 -4
  42. package/bundled/skills-windows/prizmkit-test/references/test-generation-steps.md +68 -12
  43. package/bundled/skills-windows/prizmkit-test/references/test-report-template.md +73 -9
  44. package/bundled/skills-windows/prizmkit-test/scripts/validate_boundary_report.py +347 -0
  45. package/package.json +1 -1
@@ -1,6 +1,9 @@
1
1
  """Tests for generate-bootstrap-prompt.py core functions."""
2
2
 
3
+ import os
3
4
  import re
5
+ import time
6
+ from pathlib import Path
4
7
 
5
8
  from generate_bootstrap_prompt import (
6
9
  compute_feature_slug,
@@ -12,9 +15,75 @@ from generate_bootstrap_prompt import (
12
15
  determine_pipeline_mode,
13
16
  process_conditional_blocks,
14
17
  process_mode_blocks,
18
+ assemble_sections,
19
+ generate_checkpoint_definition,
20
+ merge_checkpoint_state,
15
21
  )
16
22
 
17
23
 
24
+ def scoped_report_text(verdict="PASS", boundary_missing=0,
25
+ happy_path_only=0, completion_gate="yes",
26
+ validator_result="passed"):
27
+ """Build a scoped report with a complete boundary contract."""
28
+ return "\n".join([
29
+ "# Test Report",
30
+ "",
31
+ "## Scope",
32
+ "- Mode: this-change",
33
+ "- Artifact Dir: .prizmkit/specs/123-scope-test/",
34
+ "- In-Scope Source Files:",
35
+ " - src/feature.js",
36
+ "- In-Scope Test Files:",
37
+ " - tests/feature.test.js",
38
+ "",
39
+ "## Boundary Matrix",
40
+ "| 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 |",
41
+ "|--------|-----------|--------------|------------|--------------------|-------------------------------|-------------------|-------------------------|-------------|-------------------|-------------------|-------------------|-------------|--------------|",
42
+ "| feature | POST /feature | API | covered | covered | covered | covered | covered | covered | covered | covered | covered | none | boundary-covered |",
43
+ "",
44
+ "## Boundary Completion Gate",
45
+ "- Interfaces total: 1",
46
+ "- Boundary-covered: 1",
47
+ "- Generated-needs-review: 0",
48
+ "- Skipped-with-reason: 0",
49
+ f"- Boundary-missing: {boundary_missing}",
50
+ f"- Happy-path-only: {happy_path_only}",
51
+ f"- Completion gate passed: {completion_gate}",
52
+ "",
53
+ "## Boundary Validation",
54
+ "- Validator command: `python3 validate_boundary_report.py`",
55
+ f"- Validator result: {validator_result}",
56
+ "- Validator output: Boundary report validation: PASS",
57
+ "",
58
+ "## Verdict",
59
+ verdict,
60
+ "",
61
+ "## Final Status",
62
+ "- All tests passing: yes",
63
+ f"- Boundary completion gate passed: {completion_gate}",
64
+ "- Ready for commit: yes",
65
+ "- Needs human review: None",
66
+ "",
67
+ ])
68
+
69
+
70
+ def write_fresh_scoped_report(project_root, report_path, report_text=None):
71
+ """Write marker, in-scope files, and a fresh scoped report."""
72
+ source = project_root / "src" / "feature.js"
73
+ test = project_root / "tests" / "feature.test.js"
74
+ marker = project_root / ".prizmkit" / "specs" / "123-scope-test" / ".prizmkit-test-started"
75
+ source.parent.mkdir(parents=True, exist_ok=True)
76
+ test.parent.mkdir(parents=True, exist_ok=True)
77
+ marker.parent.mkdir(parents=True, exist_ok=True)
78
+ report_path.parent.mkdir(parents=True, exist_ok=True)
79
+ source.write_text("export const feature = true;\n", encoding="utf-8")
80
+ test.write_text("test('feature', () => {});\n", encoding="utf-8")
81
+ marker.write_text("started\n", encoding="utf-8")
82
+ time.sleep(0.01)
83
+ report_path.write_text(report_text or scoped_report_text(), encoding="utf-8")
84
+ return source, test, marker
85
+
86
+
18
87
  # ---------------------------------------------------------------------------
19
88
  # compute_feature_slug
20
89
  # ---------------------------------------------------------------------------
@@ -222,3 +291,368 @@ class TestProcessModeBlocks:
222
291
  tpl = "{{IF_CRITIC_ENABLED}}critic{{END_IF_CRITIC_ENABLED}}"
223
292
  result = process_mode_blocks(tpl, "standard", init_done=True)
224
293
  assert "critic" not in result
294
+
295
+
296
+
297
+ # ---------------------------------------------------------------------------
298
+ # section assembly / checkpoint integration
299
+ # ---------------------------------------------------------------------------
300
+
301
+ class TestScopedFeatureTestGate:
302
+ def test_standard_sections_insert_prizmkit_test_between_implement_and_review(self):
303
+ sections_dir = Path(__file__).resolve().parents[1] / "templates" / "sections"
304
+ sections = assemble_sections(
305
+ "standard",
306
+ str(sections_dir),
307
+ init_done=True,
308
+ is_resume=False,
309
+ critic_enabled=False,
310
+ browser_enabled=False,
311
+ )
312
+ names = [name for name, _ in sections]
313
+
314
+ assert "phase-prizmkit-test" in names
315
+ assert names.index("phase-implement") < names.index("phase-prizmkit-test") < names.index("phase-review")
316
+
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
321
+ assert "/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/" in rendered
322
+ assert ".prizmkit/bugfix/" in rendered # explicitly forbidden in the gate wording
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
326
+
327
+ def test_lite_sections_include_prizmkit_test_before_commit(self):
328
+ sections_dir = Path(__file__).resolve().parents[1] / "templates" / "sections"
329
+ sections = assemble_sections(
330
+ "lite",
331
+ str(sections_dir),
332
+ init_done=True,
333
+ is_resume=False,
334
+ critic_enabled=False,
335
+ browser_enabled=False,
336
+ )
337
+ names = [name for name, _ in sections]
338
+
339
+ assert "phase-prizmkit-test" in names
340
+ assert "phase-review" not in names
341
+ assert names.index("phase-implement") < names.index("phase-prizmkit-test") < names.index("phase-commit")
342
+
343
+ def test_checkpoint_orders_prizmkit_test_between_implement_and_review(self):
344
+ sections_dir = Path(__file__).resolve().parents[1] / "templates" / "sections"
345
+ sections = assemble_sections(
346
+ "standard",
347
+ str(sections_dir),
348
+ init_done=True,
349
+ is_resume=False,
350
+ critic_enabled=False,
351
+ browser_enabled=False,
352
+ )
353
+ checkpoint = generate_checkpoint_definition(
354
+ sections,
355
+ pipeline_mode="standard",
356
+ workflow_type="feature-pipeline",
357
+ item_id="F-123",
358
+ item_slug="123-scope-test",
359
+ session_id="F-123-session",
360
+ init_done=True,
361
+ )
362
+ skills = [step["skill"] for step in checkpoint["steps"]]
363
+
364
+ assert skills.index("prizmkit-implement") < skills.index("prizmkit-test") < skills.index("prizmkit-code-review")
365
+ test_step = next(step for step in checkpoint["steps"] if step["skill"] == "prizmkit-test")
366
+ assert test_step["required_artifacts"] == [
367
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
368
+ ".prizmkit/test/*/test-report.md",
369
+ ]
370
+
371
+ def test_merge_resets_completed_prizmkit_test_without_report_artifact(self, tmp_path):
372
+ existing = {
373
+ "steps": [
374
+ {
375
+ "id": "S01",
376
+ "skill": "prizmkit-test",
377
+ "status": "completed",
378
+ "required_artifacts": [
379
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
380
+ ".prizmkit/test/*/test-report.md",
381
+ ],
382
+ },
383
+ {"id": "S02", "skill": "prizmkit-code-review", "status": "completed", "required_artifacts": []},
384
+ ],
385
+ }
386
+ fresh = {
387
+ "steps": [
388
+ {"id": "S01", "skill": "prizmkit-test", "status": "pending", "required_artifacts": [
389
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
390
+ ".prizmkit/test/*/test-report.md",
391
+ ]},
392
+ {"id": "S02", "skill": "prizmkit-code-review", "status": "pending", "required_artifacts": []},
393
+ ],
394
+ }
395
+ pointer = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
396
+ pointer.parent.mkdir(parents=True, exist_ok=True)
397
+ pointer.write_text(".prizmkit/test/missing/test-report.md\n", encoding="utf-8")
398
+
399
+ merged = merge_checkpoint_state(existing, fresh, str(tmp_path))
400
+
401
+ assert [step["status"] for step in merged["steps"]] == ["pending", "pending"]
402
+
403
+ def test_merge_keeps_completed_prizmkit_test_only_for_scoped_pass_report(self, tmp_path):
404
+ report = tmp_path / ".prizmkit" / "test" / "run" / "test-report.md"
405
+ write_fresh_scoped_report(tmp_path, report)
406
+ pointer = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
407
+ pointer.parent.mkdir(parents=True, exist_ok=True)
408
+ pointer.write_text(".prizmkit/test/run/test-report.md\n", encoding="utf-8")
409
+ existing = {
410
+ "steps": [
411
+ {"id": "S01", "skill": "prizmkit-test", "status": "completed", "required_artifacts": [
412
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
413
+ ".prizmkit/test/*/test-report.md",
414
+ ]},
415
+ {"id": "S02", "skill": "prizmkit-code-review", "status": "completed", "required_artifacts": []},
416
+ ],
417
+ }
418
+ fresh = {
419
+ "steps": [
420
+ {"id": "S01", "skill": "prizmkit-test", "status": "pending", "required_artifacts": [
421
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
422
+ ".prizmkit/test/*/test-report.md",
423
+ ]},
424
+ {"id": "S02", "skill": "prizmkit-code-review", "status": "pending", "required_artifacts": []},
425
+ ],
426
+ }
427
+
428
+ merged = merge_checkpoint_state(existing, fresh, str(tmp_path))
429
+
430
+ assert [step["status"] for step in merged["steps"]] == ["completed", "completed"]
431
+
432
+ def test_merge_resets_completed_prizmkit_test_for_needs_fixes_report(self, tmp_path):
433
+ report = tmp_path / ".prizmkit" / "test" / "run" / "test-report.md"
434
+ report.parent.mkdir(parents=True)
435
+ report.write_text(scoped_report_text(verdict="NEEDS_FIXES"), encoding="utf-8")
436
+ pointer = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
437
+ pointer.parent.mkdir(parents=True, exist_ok=True)
438
+ pointer.write_text(".prizmkit/test/run/test-report.md\n", encoding="utf-8")
439
+ existing = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "completed", "required_artifacts": [
440
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
441
+ ".prizmkit/test/*/test-report.md",
442
+ ]}]}
443
+ fresh = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "pending", "required_artifacts": [
444
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
445
+ ".prizmkit/test/*/test-report.md",
446
+ ]}]}
447
+
448
+ merged = merge_checkpoint_state(existing, fresh, str(tmp_path))
449
+
450
+ assert merged["steps"][0]["status"] == "pending"
451
+
452
+ def test_merge_resets_completed_prizmkit_test_for_absolute_report_outside_project(self, tmp_path):
453
+ project_root = tmp_path / "project"
454
+ external_root = tmp_path / "external"
455
+ in_project_report = project_root / ".prizmkit" / "test" / "run" / "test-report.md"
456
+ external_report = external_root / "report.md"
457
+ pointer = project_root / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
458
+ for path in (in_project_report.parent, external_report.parent, pointer.parent):
459
+ path.mkdir(parents=True, exist_ok=True)
460
+ report_text = "\n".join([
461
+ "# Test Report",
462
+ "",
463
+ "## Scope",
464
+ "- Mode: this-change",
465
+ "- Artifact Dir: .prizmkit/specs/123-scope-test/",
466
+ "",
467
+ "## Verdict",
468
+ "PASS",
469
+ "",
470
+ ])
471
+ in_project_report.write_text(report_text, encoding="utf-8")
472
+ external_report.write_text(report_text, encoding="utf-8")
473
+ pointer.write_text(f"{external_report}\n", encoding="utf-8")
474
+ existing = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "completed", "required_artifacts": [
475
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
476
+ ".prizmkit/test/*/test-report.md",
477
+ ]}]}
478
+ fresh = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "pending", "required_artifacts": [
479
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
480
+ ".prizmkit/test/*/test-report.md",
481
+ ]}]}
482
+
483
+ merged = merge_checkpoint_state(existing, fresh, str(project_root))
484
+
485
+ assert merged["steps"][0]["status"] == "pending"
486
+
487
+ def test_merge_resets_completed_prizmkit_test_for_sibling_workspace_report(self, tmp_path):
488
+ project_root = tmp_path / "project"
489
+ sibling_root = tmp_path / "project-sibling"
490
+ in_project_report = project_root / ".prizmkit" / "test" / "run" / "test-report.md"
491
+ sibling_report = sibling_root / ".prizmkit" / "test" / "run" / "test-report.md"
492
+ pointer = project_root / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
493
+ for path in (in_project_report.parent, sibling_report.parent, pointer.parent):
494
+ path.mkdir(parents=True, exist_ok=True)
495
+ report_text = scoped_report_text()
496
+ in_project_report.write_text(report_text, encoding="utf-8")
497
+ sibling_report.write_text(report_text, encoding="utf-8")
498
+ pointer.write_text(f"{sibling_report}\n", encoding="utf-8")
499
+ existing = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "completed", "required_artifacts": [
500
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
501
+ ".prizmkit/test/*/test-report.md",
502
+ ]}]}
503
+ fresh = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "pending", "required_artifacts": [
504
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
505
+ ".prizmkit/test/*/test-report.md",
506
+ ]}]}
507
+
508
+ merged = merge_checkpoint_state(existing, fresh, str(project_root))
509
+
510
+ assert merged["steps"][0]["status"] == "pending"
511
+
512
+ def test_merge_resets_completed_prizmkit_test_for_minimal_boundary_matrix(self, tmp_path):
513
+ report = tmp_path / ".prizmkit" / "test" / "run" / "test-report.md"
514
+ report.parent.mkdir(parents=True)
515
+ report.write_text(
516
+ "\n".join([
517
+ "# Test Report",
518
+ "",
519
+ "## Scope",
520
+ "- Mode: this-change",
521
+ "- Artifact Dir: .prizmkit/specs/123-scope-test/",
522
+ "",
523
+ "## Boundary Matrix",
524
+ "| Interface | Happy Path | N/A Reasons | Final Status |",
525
+ "|-----------|------------|-------------|--------------|",
526
+ "| POST /feature | covered | none | boundary-covered |",
527
+ "",
528
+ "## Boundary Completion Gate",
529
+ "- Interfaces total: 1",
530
+ "- Boundary-covered: 1",
531
+ "- Boundary-missing: 0",
532
+ "- Happy-path-only: 0",
533
+ "- Completion gate passed: yes",
534
+ "",
535
+ "## Boundary Validation",
536
+ "- Validator result: skipped",
537
+ "",
538
+ "## Verdict",
539
+ "PASS",
540
+ "",
541
+ ]),
542
+ encoding="utf-8",
543
+ )
544
+ pointer = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
545
+ pointer.parent.mkdir(parents=True, exist_ok=True)
546
+ pointer.write_text(".prizmkit/test/run/test-report.md\n", encoding="utf-8")
547
+ existing = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "completed", "required_artifacts": [
548
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
549
+ ".prizmkit/test/*/test-report.md",
550
+ ]}]}
551
+ fresh = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "pending", "required_artifacts": [
552
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
553
+ ".prizmkit/test/*/test-report.md",
554
+ ]}]}
555
+
556
+ merged = merge_checkpoint_state(existing, fresh, str(tmp_path))
557
+
558
+ assert merged["steps"][0]["status"] == "pending"
559
+
560
+ def test_merge_resets_completed_prizmkit_test_for_missing_boundary_metrics(self, tmp_path):
561
+ report = tmp_path / ".prizmkit" / "test" / "run" / "test-report.md"
562
+ report_text = scoped_report_text().replace("- Boundary-missing: 0\n", "").replace("- Happy-path-only: 0\n", "")
563
+ write_fresh_scoped_report(tmp_path, report, report_text)
564
+ pointer = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
565
+ pointer.parent.mkdir(parents=True, exist_ok=True)
566
+ pointer.write_text(".prizmkit/test/run/test-report.md\n", encoding="utf-8")
567
+ existing = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "completed", "required_artifacts": [
568
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
569
+ ".prizmkit/test/*/test-report.md",
570
+ ]}]}
571
+ fresh = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "pending", "required_artifacts": [
572
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
573
+ ".prizmkit/test/*/test-report.md",
574
+ ]}]}
575
+
576
+ merged = merge_checkpoint_state(existing, fresh, str(tmp_path))
577
+
578
+ assert merged["steps"][0]["status"] == "pending"
579
+
580
+ def test_merge_resets_completed_prizmkit_test_when_marker_newer_than_report(self, tmp_path):
581
+ report = tmp_path / ".prizmkit" / "test" / "run" / "test-report.md"
582
+ _source, _test, marker = write_fresh_scoped_report(tmp_path, report)
583
+ now = time.time()
584
+ os.utime(report, (now, now))
585
+ os.utime(marker, (now + 10, now + 10))
586
+ pointer = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
587
+ pointer.write_text(".prizmkit/test/run/test-report.md\n", encoding="utf-8")
588
+ existing = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "completed", "required_artifacts": [
589
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
590
+ ".prizmkit/test/*/test-report.md",
591
+ ]}]}
592
+ fresh = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "pending", "required_artifacts": [
593
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
594
+ ".prizmkit/test/*/test-report.md",
595
+ ]}]}
596
+
597
+ merged = merge_checkpoint_state(existing, fresh, str(tmp_path))
598
+
599
+ assert merged["steps"][0]["status"] == "pending"
600
+
601
+ def test_merge_resets_completed_prizmkit_test_when_in_scope_file_newer_than_report(self, tmp_path):
602
+ report = tmp_path / ".prizmkit" / "test" / "run" / "test-report.md"
603
+ source, _test, _marker = write_fresh_scoped_report(tmp_path, report)
604
+ now = time.time()
605
+ os.utime(report, (now, now))
606
+ os.utime(source, (now + 10, now + 10))
607
+ pointer = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
608
+ pointer.write_text(".prizmkit/test/run/test-report.md\n", encoding="utf-8")
609
+ existing = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "completed", "required_artifacts": [
610
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
611
+ ".prizmkit/test/*/test-report.md",
612
+ ]}]}
613
+ fresh = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "pending", "required_artifacts": [
614
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
615
+ ".prizmkit/test/*/test-report.md",
616
+ ]}]}
617
+
618
+ merged = merge_checkpoint_state(existing, fresh, str(tmp_path))
619
+
620
+ assert merged["steps"][0]["status"] == "pending"
621
+
622
+ def test_merge_resets_completed_prizmkit_test_without_in_scope_tests(self, tmp_path):
623
+ report = tmp_path / ".prizmkit" / "test" / "run" / "test-report.md"
624
+ report_text = scoped_report_text().replace("- In-Scope Test Files:\n - tests/feature.test.js\n", "- In-Scope Test Files:\n")
625
+ write_fresh_scoped_report(tmp_path, report, report_text)
626
+ pointer = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
627
+ pointer.write_text(".prizmkit/test/run/test-report.md\n", encoding="utf-8")
628
+ existing = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "completed", "required_artifacts": [
629
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
630
+ ".prizmkit/test/*/test-report.md",
631
+ ]}]}
632
+ fresh = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "pending", "required_artifacts": [
633
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
634
+ ".prizmkit/test/*/test-report.md",
635
+ ]}]}
636
+
637
+ merged = merge_checkpoint_state(existing, fresh, str(tmp_path))
638
+
639
+ assert merged["steps"][0]["status"] == "pending"
640
+
641
+ def test_merge_resets_completed_prizmkit_test_without_in_scope_sources(self, tmp_path):
642
+ report = tmp_path / ".prizmkit" / "test" / "run" / "test-report.md"
643
+ report_text = scoped_report_text().replace("- In-Scope Source Files:\n - src/feature.js\n", "- In-Scope Source Files:\n")
644
+ write_fresh_scoped_report(tmp_path, report, report_text)
645
+ pointer = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
646
+ pointer.write_text(".prizmkit/test/run/test-report.md\n", encoding="utf-8")
647
+ existing = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "completed", "required_artifacts": [
648
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
649
+ ".prizmkit/test/*/test-report.md",
650
+ ]}]}
651
+ fresh = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "pending", "required_artifacts": [
652
+ ".prizmkit/specs/123-scope-test/test-report-path.txt",
653
+ ".prizmkit/test/*/test-report.md",
654
+ ]}]}
655
+
656
+ merged = merge_checkpoint_state(existing, fresh, str(tmp_path))
657
+
658
+ assert merged["steps"][0]["status"] == "pending"
@@ -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"