@su-record/vibe 2.12.5 → 2.13.0

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 (110) hide show
  1. package/CLAUDE.md +8 -2
  2. package/README.en.md +11 -11
  3. package/README.md +7 -7
  4. package/dist/cli/postinstall/fs-utils.d.ts +23 -0
  5. package/dist/cli/postinstall/fs-utils.d.ts.map +1 -1
  6. package/dist/cli/postinstall/fs-utils.js +71 -0
  7. package/dist/cli/postinstall/fs-utils.js.map +1 -1
  8. package/dist/cli/postinstall/fs-utils.test.js +69 -1
  9. package/dist/cli/postinstall/fs-utils.test.js.map +1 -1
  10. package/dist/cli/postinstall/main.d.ts.map +1 -1
  11. package/dist/cli/postinstall/main.js +12 -2
  12. package/dist/cli/postinstall/main.js.map +1 -1
  13. package/dist/cli/setup/CodexHooks.test.js +27 -0
  14. package/dist/cli/setup/CodexHooks.test.js.map +1 -1
  15. package/dist/cli/setup/ProjectSetup.js +2 -2
  16. package/dist/cli/setup/ProjectSetup.js.map +1 -1
  17. package/dist/infra/lib/DecisionTracer.d.ts +4 -0
  18. package/dist/infra/lib/DecisionTracer.d.ts.map +1 -1
  19. package/dist/infra/lib/DecisionTracer.js +4 -0
  20. package/dist/infra/lib/DecisionTracer.js.map +1 -1
  21. package/dist/infra/lib/LoopBreaker.d.ts +4 -0
  22. package/dist/infra/lib/LoopBreaker.d.ts.map +1 -1
  23. package/dist/infra/lib/LoopBreaker.js +4 -0
  24. package/dist/infra/lib/LoopBreaker.js.map +1 -1
  25. package/dist/infra/lib/ReviewRace.d.ts +4 -0
  26. package/dist/infra/lib/ReviewRace.d.ts.map +1 -1
  27. package/dist/infra/lib/ReviewRace.js +4 -0
  28. package/dist/infra/lib/ReviewRace.js.map +1 -1
  29. package/dist/infra/lib/SkillQualityGate.d.ts +4 -0
  30. package/dist/infra/lib/SkillQualityGate.d.ts.map +1 -1
  31. package/dist/infra/lib/SkillQualityGate.js +4 -0
  32. package/dist/infra/lib/SkillQualityGate.js.map +1 -1
  33. package/dist/infra/lib/UltraQA.d.ts +4 -0
  34. package/dist/infra/lib/UltraQA.d.ts.map +1 -1
  35. package/dist/infra/lib/UltraQA.js +4 -0
  36. package/dist/infra/lib/UltraQA.js.map +1 -1
  37. package/dist/infra/lib/VerificationLoop.d.ts +4 -0
  38. package/dist/infra/lib/VerificationLoop.d.ts.map +1 -1
  39. package/dist/infra/lib/VerificationLoop.js +4 -0
  40. package/dist/infra/lib/VerificationLoop.js.map +1 -1
  41. package/dist/infra/orchestrator/index.d.ts.map +1 -1
  42. package/dist/infra/orchestrator/index.js +1 -3
  43. package/dist/infra/orchestrator/index.js.map +1 -1
  44. package/dist/infra/orchestrator/parallelResearch.d.ts.map +1 -1
  45. package/dist/infra/orchestrator/parallelResearch.js +1 -4
  46. package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
  47. package/dist/tools/convention/validateCodeQuality.d.ts.map +1 -1
  48. package/dist/tools/convention/validateCodeQuality.js +5 -4
  49. package/dist/tools/convention/validateCodeQuality.js.map +1 -1
  50. package/dist/tools/spec/traceabilityMatrix.d.ts +2 -0
  51. package/dist/tools/spec/traceabilityMatrix.d.ts.map +1 -1
  52. package/dist/tools/spec/traceabilityMatrix.js +50 -1
  53. package/dist/tools/spec/traceabilityMatrix.js.map +1 -1
  54. package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts +10 -0
  55. package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts.map +1 -0
  56. package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js +89 -0
  57. package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js.map +1 -0
  58. package/dist/tools/spec/traceabilityMatrix.test.js +19 -0
  59. package/dist/tools/spec/traceabilityMatrix.test.js.map +1 -1
  60. package/hooks/hooks.json +1 -0
  61. package/hooks/scripts/__tests__/.vibe/command-log.txt +39 -0
  62. package/hooks/scripts/__tests__/.vibe/memories/memories.db +0 -0
  63. package/hooks/scripts/__tests__/.vibe/memories/memories.db-shm +0 -0
  64. package/hooks/scripts/__tests__/.vibe/memories/memories.db-wal +0 -0
  65. package/hooks/scripts/__tests__/auto-test-debounce.test.js +145 -0
  66. package/hooks/scripts/__tests__/code-check-detectors.test.js +155 -0
  67. package/hooks/scripts/__tests__/dispatcher-inprocess.test.js +99 -0
  68. package/hooks/scripts/__tests__/post-edit-dispatcher.test.js +139 -0
  69. package/hooks/scripts/__tests__/pre-tool-guard.test.js +115 -1
  70. package/hooks/scripts/__tests__/run-ledger-verify-required.test.js +146 -0
  71. package/hooks/scripts/__tests__/run-ledger.test.js +330 -0
  72. package/hooks/scripts/__tests__/scope-from-spec.test.js +215 -0
  73. package/hooks/scripts/__tests__/sentinel-guard.test.js +79 -24
  74. package/hooks/scripts/__tests__/step-counter.test.js +95 -15
  75. package/hooks/scripts/__tests__/utils-npm-root.test.js +98 -0
  76. package/hooks/scripts/auto-commit.js +27 -1
  77. package/hooks/scripts/auto-format.js +85 -20
  78. package/hooks/scripts/auto-test.js +187 -37
  79. package/hooks/scripts/code-check.js +286 -90
  80. package/hooks/scripts/codex-hook-adapter.js +12 -1
  81. package/hooks/scripts/command-log.js +26 -16
  82. package/hooks/scripts/lib/dispatcher.js +38 -0
  83. package/hooks/scripts/lib/hook-context.js +101 -0
  84. package/hooks/scripts/lib/pr-gate-runner.js +62 -0
  85. package/hooks/scripts/lib/run-ledger.js +169 -0
  86. package/hooks/scripts/lib/scope-from-spec.js +40 -7
  87. package/hooks/scripts/post-edit-dispatcher.js +93 -20
  88. package/hooks/scripts/post-edit.js +40 -19
  89. package/hooks/scripts/pr-test-gate.js +8 -37
  90. package/hooks/scripts/pre-tool-dispatcher.js +18 -16
  91. package/hooks/scripts/pre-tool-guard.js +55 -52
  92. package/hooks/scripts/prompt-dispatcher.js +10 -0
  93. package/hooks/scripts/scope-guard.js +40 -39
  94. package/hooks/scripts/sentinel-guard.js +41 -41
  95. package/hooks/scripts/session-start.js +13 -1
  96. package/hooks/scripts/step-counter.js +100 -7
  97. package/hooks/scripts/stop-dispatcher.js +26 -0
  98. package/hooks/scripts/utils.js +63 -21
  99. package/hooks/scripts/verify-ledger.js +22 -0
  100. package/package.json +2 -2
  101. package/skills/spec/references/templates.md +11 -6
  102. package/skills/vibe.run/SKILL.md +144 -1681
  103. package/skills/vibe.run/references/brand-assets.md +59 -0
  104. package/skills/vibe.run/references/parallel-agents.md +326 -0
  105. package/skills/vibe.run/references/race-review.md +272 -0
  106. package/skills/vibe.run/references/ralph-loop.md +172 -0
  107. package/skills/vibe.run/references/ultrawork-mode.md +148 -0
  108. package/skills/vibe.trace/SKILL.md +25 -38
  109. package/skills/vibe.verify/SKILL.md +15 -0
  110. package/hooks/scripts/figma-guard.js +0 -219
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { spawnSync } from 'child_process';
3
3
  import path from 'path';
4
4
  import { fileURLToPath } from 'url';
@@ -406,4 +406,118 @@ describe('pre-tool-guard', () => {
406
406
  expect(result.stdout).toContain('Editing sensitive file');
407
407
  });
408
408
  });
409
+
410
+ // ══════════════════════════════════════════════════
411
+ // REQ-012: 64KB 초과 대용량 페이로드 파싱
412
+ // hook-context.js readStdinSync가 EOF까지 읽는지 검증
413
+ // ══════════════════════════════════════════════════
414
+ describe('large stdin payload (REQ-012: >64KB)', () => {
415
+ it('should parse tool_name from payload larger than 64KB', () => {
416
+ // 80KB content field로 페이로드 크기를 64KB 이상으로 만든다
417
+ const largeContent = 'x'.repeat(80 * 1024);
418
+ const result = runGuardWithStdin({
419
+ tool_name: 'Write',
420
+ tool_input: {
421
+ file_path: 'src/output.ts',
422
+ content: largeContent,
423
+ },
424
+ });
425
+ // Write to safe path: no blocking, tool_name parsed correctly (exit 0)
426
+ expect(result.exitCode).toBe(0);
427
+ });
428
+
429
+ it('should correctly detect dangerous command in large Bash payload', () => {
430
+ const padding = 'a'.repeat(80 * 1024);
431
+ const result = runGuardWithStdin({
432
+ tool_name: 'Bash',
433
+ tool_input: { command: 'rm -rf /', _padding: padding },
434
+ });
435
+ expect(result.exitCode).toBe(2);
436
+ expect(result.stdout).toContain('BLOCKED');
437
+ });
438
+
439
+ it('should allow safe command in large Bash payload', () => {
440
+ const padding = 'b'.repeat(80 * 1024);
441
+ const result = runGuardWithStdin({
442
+ tool_name: 'Bash',
443
+ tool_input: { command: 'ls -la', _padding: padding },
444
+ });
445
+ expect(result.exitCode).toBe(0);
446
+ expect(result.stdout).toBe('');
447
+ });
448
+ });
449
+
450
+ // ══════════════════════════════════════════════════
451
+ // REQ-013: gh pr create Bash 경로 테스트 게이트
452
+ // pr-gate-runner 헬퍼를 mock하여 테스트러너 실행 없이 검증
453
+ // ══════════════════════════════════════════════════
454
+ describe('gh pr create gate (REQ-013)', () => {
455
+ it('should allow gh pr create when tests pass (argv mode)', () => {
456
+ // 테스트 환경에서는 package.json test 스크립트가 존재하지만
457
+ // 여기서는 실제 테스트를 실행하지 않도록 PROJECT_DIR을 빈 디렉토리로 설정
458
+ const tmpDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '.vibe');
459
+ const result = spawnSync(
460
+ 'node',
461
+ [SCRIPT, 'Bash', 'gh pr create --title "feat" --body "desc"'],
462
+ {
463
+ encoding: 'utf-8',
464
+ timeout: 10000,
465
+ // 빈 디렉토리 → detectTestCommand가 null 반환 → 통과
466
+ env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
467
+ }
468
+ );
469
+ const combined = `${result.stdout || ''}${result.stderr || ''}`.trim();
470
+ // 테스트 커맨드 없음 → PR 허용 (exit 0)
471
+ expect(result.status).toBe(0);
472
+ expect(combined).not.toContain('PR-GATE');
473
+ });
474
+
475
+ it('should detect gh pr create in stdin payload', () => {
476
+ const tmpDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '.vibe');
477
+ const json = JSON.stringify({
478
+ tool_name: 'Bash',
479
+ tool_input: { command: 'gh pr create --title "feat" --body "body"' },
480
+ });
481
+ const result = spawnSync(
482
+ 'node',
483
+ [SCRIPT],
484
+ {
485
+ input: json,
486
+ encoding: 'utf-8',
487
+ timeout: 10000,
488
+ env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
489
+ }
490
+ );
491
+ // 테스트 커맨드 없음 → exit 0
492
+ expect(result.status).toBe(0);
493
+ });
494
+
495
+ it('should NOT trigger gate for unrelated gh commands', () => {
496
+ const result = runGuard({ args: ['Bash', 'gh issue list'] });
497
+ expect(result.exitCode).toBe(0);
498
+ expect(result.stdout).toBe('');
499
+ });
500
+
501
+ it('should NOT trigger gate for gh pr view', () => {
502
+ const result = runGuard({ args: ['Bash', 'gh pr view 123'] });
503
+ expect(result.exitCode).toBe(0);
504
+ expect(result.stdout).toBe('');
505
+ });
506
+
507
+ it('should match gh pr create with flags before/after', () => {
508
+ // 플래그 포함 명령에도 정규식 매칭이 되어야 함
509
+ // (테스트 없는 PROJECT_DIR이므로 exit 0)
510
+ const tmpDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '.vibe');
511
+ const result = spawnSync(
512
+ 'node',
513
+ [SCRIPT, 'Bash', 'gh pr create -t "title" -b "body" --draft'],
514
+ {
515
+ encoding: 'utf-8',
516
+ timeout: 10000,
517
+ env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
518
+ }
519
+ );
520
+ expect(result.status).toBe(0);
521
+ });
522
+ });
409
523
  });
@@ -0,0 +1,146 @@
1
+ /**
2
+ * run-ledger verifyRequired 확장 테스트
3
+ *
4
+ * 검증 대상:
5
+ * - recordVerifyRequired: verifyRequired=true, reason 설정
6
+ * - recordVerify(pass) → verifyRequired 클리어
7
+ * - recordVerify(fail) → verifyRequired 유지
8
+ * - auto-commit 게이트: verifyRequired=true 시 skip
9
+ */
10
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
11
+ import { fileURLToPath } from 'url';
12
+ import path from 'path';
13
+ import fs from 'fs';
14
+ import os from 'os';
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+
18
+ let tmpDir;
19
+ beforeEach(() => {
20
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-verify-required-'));
21
+ });
22
+ afterEach(() => {
23
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
24
+ });
25
+
26
+ async function importLedger() {
27
+ const ledgerPath = path.resolve(__dirname, '..', 'lib', 'run-ledger.js');
28
+ return import(ledgerPath);
29
+ }
30
+
31
+ describe('run-ledger: verifyRequired', () => {
32
+ it('초기 상태: verifyRequired 없음', async () => {
33
+ const { recordRunStart, readLedger } = await importLedger();
34
+ recordRunStart(tmpDir, 'feat');
35
+ const ledger = readLedger(tmpDir);
36
+ // verifyRequired는 undefined 또는 false여야 함
37
+ expect(ledger.verifyRequired).toBeFalsy();
38
+ });
39
+
40
+ it('recordVerifyRequired → verifyRequired=true, reason 설정', async () => {
41
+ const { recordRunStart, recordVerifyRequired, readLedger } = await importLedger();
42
+ recordRunStart(tmpDir, 'feat');
43
+ recordVerifyRequired(tmpDir, 'P1 any-type line 5');
44
+ const ledger = readLedger(tmpDir);
45
+ expect(ledger.verifyRequired).toBe(true);
46
+ expect(ledger.verifyRequiredReason).toBe('P1 any-type line 5');
47
+ });
48
+
49
+ it('recordVerify(pass) → verifyRequired 클리어', async () => {
50
+ const { recordRunStart, recordVerifyRequired, recordVerify, readLedger } = await importLedger();
51
+ recordRunStart(tmpDir, 'feat');
52
+ recordVerifyRequired(tmpDir, 'P1 console.log found');
53
+ recordVerify(tmpDir, true);
54
+ const ledger = readLedger(tmpDir);
55
+ expect(ledger.verifyRequired).toBe(false);
56
+ expect(ledger.verifyRequiredReason).toBeNull();
57
+ expect(ledger.verifyPassed).toBe(true);
58
+ });
59
+
60
+ it('recordVerify(fail) → verifyRequired 유지됨', async () => {
61
+ const { recordRunStart, recordVerifyRequired, recordVerify, readLedger } = await importLedger();
62
+ recordRunStart(tmpDir, 'feat');
63
+ recordVerifyRequired(tmpDir, 'P1 issue');
64
+ recordVerify(tmpDir, false);
65
+ const ledger = readLedger(tmpDir);
66
+ // fail 시 verifyRequired는 유지되어야 함
67
+ expect(ledger.verifyRequired).toBe(true);
68
+ expect(ledger.verifyPassed).toBe(false);
69
+ });
70
+
71
+ it('reason 없이 recordVerifyRequired → 기본 reason 설정', async () => {
72
+ const { recordRunStart, recordVerifyRequired, readLedger } = await importLedger();
73
+ recordRunStart(tmpDir, 'feat');
74
+ recordVerifyRequired(tmpDir, '');
75
+ const ledger = readLedger(tmpDir);
76
+ expect(ledger.verifyRequired).toBe(true);
77
+ expect(typeof ledger.verifyRequiredReason).toBe('string');
78
+ });
79
+
80
+ it('recordVerifyRequired fail-open (레저 없이도 작동)', async () => {
81
+ const { recordVerifyRequired, readLedger } = await importLedger();
82
+ // runStart 없이 호출
83
+ recordVerifyRequired(tmpDir, 'standalone P1');
84
+ const ledger = readLedger(tmpDir);
85
+ expect(ledger.verifyRequired).toBe(true);
86
+ });
87
+ });
88
+
89
+ describe('auto-commit: verifyRequired 게이트 로직', () => {
90
+ function autoCommitGatePass(ledger) {
91
+ if (!ledger) return true;
92
+
93
+ // verify 게이트 (기존)
94
+ if (ledger.runStarted) {
95
+ const verifyOk = ledger.verifyPassed === true
96
+ && ledger.verifyAt
97
+ && ledger.verifyAt > ledger.runStarted;
98
+ if (!verifyOk) return false;
99
+ }
100
+
101
+ // verifyRequired 게이트 (신규)
102
+ if (ledger.verifyRequired === true) return false;
103
+
104
+ return true;
105
+ }
106
+
107
+ it('verifyRequired=true → 차단', () => {
108
+ expect(autoCommitGatePass({
109
+ runStarted: null,
110
+ verifyRequired: true,
111
+ verifyRequiredReason: 'P1 any-type',
112
+ })).toBe(false);
113
+ });
114
+
115
+ it('verifyRequired=false → 통과 (verifyRequired만 확인 시)', () => {
116
+ expect(autoCommitGatePass({
117
+ runStarted: null,
118
+ verifyRequired: false,
119
+ })).toBe(true);
120
+ });
121
+
122
+ it('verifyRequired=undefined → 통과 (기존 레저 호환)', () => {
123
+ expect(autoCommitGatePass({
124
+ runStarted: null,
125
+ verifyRequired: undefined,
126
+ })).toBe(true);
127
+ });
128
+
129
+ it('runStarted + verifyPassed + verifyRequired=true → 차단 (verifyRequired 우선)', () => {
130
+ expect(autoCommitGatePass({
131
+ runStarted: '2026-01-01T10:00:00.000Z',
132
+ verifyPassed: true,
133
+ verifyAt: '2026-01-01T10:05:00.000Z',
134
+ verifyRequired: true,
135
+ })).toBe(false);
136
+ });
137
+
138
+ it('runStarted + verifyPassed + verifyRequired=false → 통과', () => {
139
+ expect(autoCommitGatePass({
140
+ runStarted: '2026-01-01T10:00:00.000Z',
141
+ verifyPassed: true,
142
+ verifyAt: '2026-01-01T10:05:00.000Z',
143
+ verifyRequired: false,
144
+ })).toBe(true);
145
+ });
146
+ });
@@ -0,0 +1,330 @@
1
+ /**
2
+ * run-ledger 라이브러리 테스트
3
+ *
4
+ * REQ-harness-remediation-005, 006, 007, 008 커버리지:
5
+ * - round-trip 읽기/쓰기
6
+ * - verifyAt > runStarted 로직
7
+ * - extractRunFeature / isVibeRunPrompt 헬퍼
8
+ * - recordRunStart → verifyPassed 리셋
9
+ * - recordVerify pass/fail
10
+ * - prompt-dispatcher 에서의 vibe.run 감지 (CLI 실행)
11
+ */
12
+
13
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import os from 'os';
17
+ import { execFileSync, spawnSync } from 'child_process';
18
+ import { fileURLToPath } from 'url';
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+
22
+ // 테스트 격리용 임시 디렉토리
23
+ let tmpDir;
24
+ beforeEach(() => {
25
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-ledger-test-'));
26
+ });
27
+ afterEach(() => {
28
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
29
+ });
30
+
31
+ // 모듈 임포트 (ESM 동적)
32
+ async function importLedger() {
33
+ const ledgerPath = path.resolve(__dirname, '..', 'lib', 'run-ledger.js');
34
+ // 캐시 버스팅 불필요 — 순수 함수, 상태 없음
35
+ return import(ledgerPath);
36
+ }
37
+
38
+ // ──────────────────────────────────────────────────────────────────
39
+ // readLedger / recordRunStart / recordVerify round-trip
40
+ // ──────────────────────────────────────────────────────────────────
41
+ describe('run-ledger: round-trip', () => {
42
+ it('존재하지 않는 레저 → null 반환', async () => {
43
+ const { readLedger } = await importLedger();
44
+ const result = readLedger(tmpDir);
45
+ expect(result).toBeNull();
46
+ });
47
+
48
+ it('recordRunStart 후 읽으면 runStarted가 ISO 문자열', async () => {
49
+ const { recordRunStart, readLedger } = await importLedger();
50
+ const before = new Date().toISOString();
51
+ recordRunStart(tmpDir, 'my-feature');
52
+ const ledger = readLedger(tmpDir);
53
+ expect(ledger).not.toBeNull();
54
+ expect(ledger.runStarted).toBeDefined();
55
+ expect(ledger.runStarted >= before).toBe(true);
56
+ expect(ledger.runFeature).toBe('my-feature');
57
+ });
58
+
59
+ it('recordRunStart는 verifyPassed를 false로 리셋', async () => {
60
+ const { recordRunStart, recordVerify, readLedger } = await importLedger();
61
+ recordRunStart(tmpDir, 'f1');
62
+ recordVerify(tmpDir, true);
63
+ // 두 번째 run — 리셋
64
+ recordRunStart(tmpDir, 'f2');
65
+ const ledger = readLedger(tmpDir);
66
+ expect(ledger.verifyPassed).toBe(false);
67
+ expect(ledger.verifyAt).toBeNull();
68
+ expect(ledger.stopWarned).toBe(false);
69
+ });
70
+
71
+ it('recordVerify(pass) → verifyPassed=true, verifyAt 설정', async () => {
72
+ const { recordRunStart, recordVerify, readLedger } = await importLedger();
73
+ recordRunStart(tmpDir, 'feat');
74
+ const before = new Date().toISOString();
75
+ recordVerify(tmpDir, true);
76
+ const ledger = readLedger(tmpDir);
77
+ expect(ledger.verifyPassed).toBe(true);
78
+ expect(ledger.verifyAt).toBeDefined();
79
+ expect(ledger.verifyAt >= before).toBe(true);
80
+ });
81
+
82
+ it('recordVerify(fail) → verifyPassed=false', async () => {
83
+ const { recordRunStart, recordVerify, readLedger } = await importLedger();
84
+ recordRunStart(tmpDir, 'feat');
85
+ recordVerify(tmpDir, false);
86
+ const ledger = readLedger(tmpDir);
87
+ expect(ledger.verifyPassed).toBe(false);
88
+ });
89
+ });
90
+
91
+ // ──────────────────────────────────────────────────────────────────
92
+ // verifyAt > runStarted 관계 검증
93
+ // ──────────────────────────────────────────────────────────────────
94
+ describe('run-ledger: verifyAt > runStarted', () => {
95
+ it('recordVerify 후 verifyAt > runStarted', async () => {
96
+ const { recordRunStart, recordVerify, readLedger } = await importLedger();
97
+ recordRunStart(tmpDir, 'f');
98
+ // 동일 밀리초 내 실행 가능성을 피하기 위해 runStarted를 과거로 조작
99
+ const ledger = readLedger(tmpDir);
100
+ const p = path.join(tmpDir, '.vibe', 'metrics', 'run-ledger.json');
101
+ const patched = { ...ledger, runStarted: new Date(Date.now() - 1000).toISOString() };
102
+ fs.writeFileSync(p, JSON.stringify(patched), 'utf-8');
103
+
104
+ recordVerify(tmpDir, true);
105
+ const after = readLedger(tmpDir);
106
+ expect(after.verifyAt > after.runStarted).toBe(true);
107
+ expect(after.verifyPassed).toBe(true);
108
+ });
109
+
110
+ it('verifyAt이 runStarted보다 이르면 gate 불충족 (simulate)', async () => {
111
+ const { readLedger } = await importLedger();
112
+ const p = path.join(tmpDir, '.vibe', 'metrics', 'run-ledger.json');
113
+ fs.mkdirSync(path.dirname(p), { recursive: true });
114
+ const now = new Date();
115
+ // runStarted가 verifyAt보다 나중
116
+ const data = {
117
+ runStarted: new Date(now.getTime() + 5000).toISOString(),
118
+ runFeature: 'f',
119
+ verifyPassed: true,
120
+ verifyAt: now.toISOString(),
121
+ stopWarned: false,
122
+ };
123
+ fs.writeFileSync(p, JSON.stringify(data), 'utf-8');
124
+ const ledger = readLedger(tmpDir);
125
+ // auto-commit 게이트 로직: verifyPassed=true 이지만 verifyAt < runStarted
126
+ const gatePass = ledger.verifyPassed === true
127
+ && ledger.verifyAt
128
+ && ledger.verifyAt > ledger.runStarted;
129
+ expect(gatePass).toBe(false);
130
+ });
131
+ });
132
+
133
+ // ──────────────────────────────────────────────────────────────────
134
+ // isVibeRunPrompt / extractRunFeature
135
+ // ──────────────────────────────────────────────────────────────────
136
+ describe('run-ledger: vibe.run 감지 헬퍼', () => {
137
+ it('/vibe.run 감지', async () => {
138
+ const { isVibeRunPrompt } = await importLedger();
139
+ expect(isVibeRunPrompt('/vibe.run my-feature')).toBe(true);
140
+ });
141
+
142
+ it('$vibe.run 감지', async () => {
143
+ const { isVibeRunPrompt } = await importLedger();
144
+ expect(isVibeRunPrompt('$vibe.run my-feature')).toBe(true);
145
+ });
146
+
147
+ it('대소문자 무관', async () => {
148
+ const { isVibeRunPrompt } = await importLedger();
149
+ expect(isVibeRunPrompt('/VIBE.RUN feature')).toBe(true);
150
+ });
151
+
152
+ it('부분 매칭 방지 — vibe.runs 는 미감지', async () => {
153
+ const { isVibeRunPrompt } = await importLedger();
154
+ // "vibe.runs"는 \b로 경계 처리됨
155
+ expect(isVibeRunPrompt('vibe.runs everything')).toBe(false);
156
+ });
157
+
158
+ it('단어 내 포함은 미감지', async () => {
159
+ const { isVibeRunPrompt } = await importLedger();
160
+ expect(isVibeRunPrompt('I was talking about vibe.runner')).toBe(false);
161
+ });
162
+
163
+ it('extractRunFeature: 기능명 추출', async () => {
164
+ const { extractRunFeature } = await importLedger();
165
+ expect(extractRunFeature('/vibe.run my-feature')).toBe('my-feature');
166
+ });
167
+
168
+ it('extractRunFeature: $vibe.run에서 추출', async () => {
169
+ const { extractRunFeature } = await importLedger();
170
+ expect(extractRunFeature('$vibe.run auth-login ultrawork')).toBe('auth-login');
171
+ });
172
+
173
+ it('extractRunFeature: 기능명 없으면 null', async () => {
174
+ const { extractRunFeature } = await importLedger();
175
+ expect(extractRunFeature('/vibe.run --phase 1')).toBeNull();
176
+ });
177
+
178
+ it('extractRunFeature: /vibe.run만 있으면 null', async () => {
179
+ const { extractRunFeature } = await importLedger();
180
+ expect(extractRunFeature('/vibe.run')).toBeNull();
181
+ });
182
+ });
183
+
184
+ // ──────────────────────────────────────────────────────────────────
185
+ // markStopWarned
186
+ // ──────────────────────────────────────────────────────────────────
187
+ describe('run-ledger: stopWarned', () => {
188
+ it('markStopWarned → stopWarned=true', async () => {
189
+ const { recordRunStart, markStopWarned, readLedger } = await importLedger();
190
+ recordRunStart(tmpDir, 'f');
191
+ markStopWarned(tmpDir);
192
+ const ledger = readLedger(tmpDir);
193
+ expect(ledger.stopWarned).toBe(true);
194
+ });
195
+ });
196
+
197
+ // ──────────────────────────────────────────────────────────────────
198
+ // verify-ledger.js CLI
199
+ // ──────────────────────────────────────────────────────────────────
200
+ describe('verify-ledger CLI', () => {
201
+ const CLI = path.resolve(__dirname, '..', 'verify-ledger.js');
202
+
203
+ it('pass 인자 → verifyPassed=true, exit 0', async () => {
204
+ const { recordRunStart } = await importLedger();
205
+ recordRunStart(tmpDir, 'feat');
206
+
207
+ const result = spawnSync('node', [CLI, 'pass'], {
208
+ encoding: 'utf-8',
209
+ timeout: 5000,
210
+ env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
211
+ });
212
+ expect(result.status).toBe(0);
213
+ expect(result.stdout).toContain('verifyPassed=true');
214
+
215
+ const { readLedger } = await importLedger();
216
+ const ledger = readLedger(tmpDir);
217
+ expect(ledger.verifyPassed).toBe(true);
218
+ });
219
+
220
+ it('fail 인자 → verifyPassed=false, exit 0', async () => {
221
+ const { recordRunStart } = await importLedger();
222
+ recordRunStart(tmpDir, 'feat');
223
+
224
+ const result = spawnSync('node', [CLI, 'fail'], {
225
+ encoding: 'utf-8',
226
+ timeout: 5000,
227
+ env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
228
+ });
229
+ expect(result.status).toBe(0);
230
+ expect(result.stdout).toContain('verifyPassed=false');
231
+
232
+ const { readLedger } = await importLedger();
233
+ const ledger = readLedger(tmpDir);
234
+ expect(ledger.verifyPassed).toBe(false);
235
+ });
236
+
237
+ it('인자 없이도 exit 0 (fail-open)', () => {
238
+ const result = spawnSync('node', [CLI], {
239
+ encoding: 'utf-8',
240
+ timeout: 5000,
241
+ env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
242
+ });
243
+ expect(result.status).toBe(0);
244
+ });
245
+ });
246
+
247
+ // ──────────────────────────────────────────────────────────────────
248
+ // prompt-dispatcher vibe.run 감지 (sanity — ledger 파일 생성 확인)
249
+ // ──────────────────────────────────────────────────────────────────
250
+ describe('prompt-dispatcher: vibe.run 감지 → ledger 파일 생성', () => {
251
+ const DISPATCHER = path.resolve(__dirname, '..', 'prompt-dispatcher.js');
252
+
253
+ it('/vibe.run 프롬프트 → run-ledger.json 생성', () => {
254
+ const payload = JSON.stringify({ prompt: '/vibe.run test-feature' });
255
+ const result = spawnSync('node', [DISPATCHER], {
256
+ input: payload,
257
+ encoding: 'utf-8',
258
+ timeout: 10000,
259
+ env: {
260
+ ...process.env,
261
+ CLAUDE_PROJECT_DIR: tmpDir,
262
+ VIBE_HOOK_DEPTH: undefined,
263
+ // keyword-detector 등 자식 프로세스 타임아웃 내 완료
264
+ },
265
+ });
266
+ expect(result.status).toBe(0);
267
+ const ledgerFile = path.join(tmpDir, '.vibe', 'metrics', 'run-ledger.json');
268
+ expect(fs.existsSync(ledgerFile)).toBe(true);
269
+ const ledger = JSON.parse(fs.readFileSync(ledgerFile, 'utf-8'));
270
+ expect(ledger.runStarted).toBeDefined();
271
+ expect(ledger.runFeature).toBe('test-feature');
272
+ expect(ledger.verifyPassed).toBe(false);
273
+ });
274
+
275
+ it('일반 프롬프트 → ledger 파일 미생성', () => {
276
+ const payload = JSON.stringify({ prompt: 'implement the login form' });
277
+ spawnSync('node', [DISPATCHER], {
278
+ input: payload,
279
+ encoding: 'utf-8',
280
+ timeout: 10000,
281
+ env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
282
+ });
283
+ const ledgerFile = path.join(tmpDir, '.vibe', 'metrics', 'run-ledger.json');
284
+ expect(fs.existsSync(ledgerFile)).toBe(false);
285
+ });
286
+ });
287
+
288
+ // ──────────────────────────────────────────────────────────────────
289
+ // auto-commit 게이트 로직 (단위 수준 — 레저 상태별 gate 검증)
290
+ // ──────────────────────────────────────────────────────────────────
291
+ describe('auto-commit: verify gate 로직 (레저 상태 시뮬레이션)', () => {
292
+ function gatePass(ledger) {
293
+ if (!ledger || !ledger.runStarted) return true; // 레저 없으면 통과 (기존 동작)
294
+ return ledger.verifyPassed === true
295
+ && ledger.verifyAt
296
+ && ledger.verifyAt > ledger.runStarted;
297
+ }
298
+
299
+ it('레저 없음 → 통과 (기존 동작 유지)', () => {
300
+ expect(gatePass(null)).toBe(true);
301
+ });
302
+
303
+ it('runStarted 없음 → 통과', () => {
304
+ expect(gatePass({ verifyPassed: false, verifyAt: null })).toBe(true);
305
+ });
306
+
307
+ it('verifyPassed=false → 차단', () => {
308
+ expect(gatePass({
309
+ runStarted: '2026-01-01T10:00:00.000Z',
310
+ verifyPassed: false,
311
+ verifyAt: null,
312
+ })).toBe(false);
313
+ });
314
+
315
+ it('verifyPassed=true + verifyAt > runStarted → 통과', () => {
316
+ expect(gatePass({
317
+ runStarted: '2026-01-01T10:00:00.000Z',
318
+ verifyPassed: true,
319
+ verifyAt: '2026-01-01T10:05:00.000Z',
320
+ })).toBe(true);
321
+ });
322
+
323
+ it('verifyPassed=true 이지만 verifyAt < runStarted → 차단', () => {
324
+ expect(gatePass({
325
+ runStarted: '2026-01-01T10:05:00.000Z',
326
+ verifyPassed: true,
327
+ verifyAt: '2026-01-01T10:00:00.000Z',
328
+ })).toBe(false);
329
+ });
330
+ });