@su-record/vibe 2.12.4 → 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.
- package/CLAUDE.md +10 -3
- package/README.en.md +11 -11
- package/README.md +7 -7
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +24 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/postinstall/claude-agents.d.ts +11 -1
- package/dist/cli/postinstall/claude-agents.d.ts.map +1 -1
- package/dist/cli/postinstall/claude-agents.js +22 -2
- package/dist/cli/postinstall/claude-agents.js.map +1 -1
- package/dist/cli/postinstall/constants.d.ts +18 -0
- package/dist/cli/postinstall/constants.d.ts.map +1 -1
- package/dist/cli/postinstall/constants.js +50 -0
- package/dist/cli/postinstall/constants.js.map +1 -1
- package/dist/cli/postinstall/fs-utils.d.ts +23 -0
- package/dist/cli/postinstall/fs-utils.d.ts.map +1 -1
- package/dist/cli/postinstall/fs-utils.js +71 -0
- package/dist/cli/postinstall/fs-utils.js.map +1 -1
- package/dist/cli/postinstall/fs-utils.test.js +69 -1
- package/dist/cli/postinstall/fs-utils.test.js.map +1 -1
- package/dist/cli/postinstall/index.d.ts +1 -1
- package/dist/cli/postinstall/index.d.ts.map +1 -1
- package/dist/cli/postinstall/index.js +1 -1
- package/dist/cli/postinstall/index.js.map +1 -1
- package/dist/cli/postinstall/main.d.ts.map +1 -1
- package/dist/cli/postinstall/main.js +15 -4
- package/dist/cli/postinstall/main.js.map +1 -1
- package/dist/cli/postinstall.d.ts +1 -1
- package/dist/cli/postinstall.d.ts.map +1 -1
- package/dist/cli/postinstall.js +1 -1
- package/dist/cli/postinstall.js.map +1 -1
- package/dist/cli/setup/CodexHooks.test.js +27 -0
- package/dist/cli/setup/CodexHooks.test.js.map +1 -1
- package/dist/cli/setup/ProjectSetup.js +2 -2
- package/dist/cli/setup/ProjectSetup.js.map +1 -1
- package/dist/infra/lib/ContextCompressor.d.ts +11 -2
- package/dist/infra/lib/ContextCompressor.d.ts.map +1 -1
- package/dist/infra/lib/ContextCompressor.js +26 -41
- package/dist/infra/lib/ContextCompressor.js.map +1 -1
- package/dist/infra/lib/ContextCompressor.test.d.ts +2 -0
- package/dist/infra/lib/ContextCompressor.test.d.ts.map +1 -0
- package/dist/infra/lib/ContextCompressor.test.js +25 -0
- package/dist/infra/lib/ContextCompressor.test.js.map +1 -0
- package/dist/infra/lib/DecisionTracer.d.ts +4 -0
- package/dist/infra/lib/DecisionTracer.d.ts.map +1 -1
- package/dist/infra/lib/DecisionTracer.js +4 -0
- package/dist/infra/lib/DecisionTracer.js.map +1 -1
- package/dist/infra/lib/LoopBreaker.d.ts +4 -0
- package/dist/infra/lib/LoopBreaker.d.ts.map +1 -1
- package/dist/infra/lib/LoopBreaker.js +4 -0
- package/dist/infra/lib/LoopBreaker.js.map +1 -1
- package/dist/infra/lib/ReviewRace.d.ts +4 -0
- package/dist/infra/lib/ReviewRace.d.ts.map +1 -1
- package/dist/infra/lib/ReviewRace.js +4 -0
- package/dist/infra/lib/ReviewRace.js.map +1 -1
- package/dist/infra/lib/SkillQualityGate.d.ts +4 -0
- package/dist/infra/lib/SkillQualityGate.d.ts.map +1 -1
- package/dist/infra/lib/SkillQualityGate.js +4 -0
- package/dist/infra/lib/SkillQualityGate.js.map +1 -1
- package/dist/infra/lib/UltraQA.d.ts +4 -0
- package/dist/infra/lib/UltraQA.d.ts.map +1 -1
- package/dist/infra/lib/UltraQA.js +4 -0
- package/dist/infra/lib/UltraQA.js.map +1 -1
- package/dist/infra/lib/VerificationLoop.d.ts +4 -0
- package/dist/infra/lib/VerificationLoop.d.ts.map +1 -1
- package/dist/infra/lib/VerificationLoop.js +4 -0
- package/dist/infra/lib/VerificationLoop.js.map +1 -1
- package/dist/infra/lib/embedding/VectorStore.d.ts +9 -2
- package/dist/infra/lib/embedding/VectorStore.d.ts.map +1 -1
- package/dist/infra/lib/embedding/VectorStore.js +42 -19
- package/dist/infra/lib/embedding/VectorStore.js.map +1 -1
- package/dist/infra/lib/memory/MemoryStorage.d.ts +12 -0
- package/dist/infra/lib/memory/MemoryStorage.d.ts.map +1 -1
- package/dist/infra/lib/memory/MemoryStorage.js +57 -0
- package/dist/infra/lib/memory/MemoryStorage.js.map +1 -1
- package/dist/infra/lib/memory/ReflectionStore.d.ts.map +1 -1
- package/dist/infra/lib/memory/ReflectionStore.js +8 -27
- package/dist/infra/lib/memory/ReflectionStore.js.map +1 -1
- package/dist/infra/orchestrator/LLMCluster.d.ts +4 -0
- package/dist/infra/orchestrator/LLMCluster.d.ts.map +1 -1
- package/dist/infra/orchestrator/LLMCluster.js +35 -8
- package/dist/infra/orchestrator/LLMCluster.js.map +1 -1
- package/dist/infra/orchestrator/index.d.ts.map +1 -1
- package/dist/infra/orchestrator/index.js +1 -3
- package/dist/infra/orchestrator/index.js.map +1 -1
- package/dist/infra/orchestrator/parallelResearch.d.ts.map +1 -1
- package/dist/infra/orchestrator/parallelResearch.js +1 -4
- package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
- package/dist/tools/convention/validateCodeQuality.d.ts.map +1 -1
- package/dist/tools/convention/validateCodeQuality.js +5 -4
- package/dist/tools/convention/validateCodeQuality.js.map +1 -1
- package/dist/tools/spec/traceabilityMatrix.d.ts +2 -0
- package/dist/tools/spec/traceabilityMatrix.d.ts.map +1 -1
- package/dist/tools/spec/traceabilityMatrix.js +50 -1
- package/dist/tools/spec/traceabilityMatrix.js.map +1 -1
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts +10 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts.map +1 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js +89 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js.map +1 -0
- package/dist/tools/spec/traceabilityMatrix.test.js +19 -0
- package/dist/tools/spec/traceabilityMatrix.test.js.map +1 -1
- package/hooks/hooks.json +3 -9
- package/hooks/scripts/__tests__/.vibe/command-log.txt +39 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db +0 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db-shm +0 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db-wal +0 -0
- package/hooks/scripts/__tests__/auto-test-debounce.test.js +145 -0
- package/hooks/scripts/__tests__/code-check-detectors.test.js +155 -0
- package/hooks/scripts/__tests__/dispatcher-inprocess.test.js +99 -0
- package/hooks/scripts/__tests__/post-edit-dispatcher.test.js +139 -0
- package/hooks/scripts/__tests__/pre-tool-guard.test.js +115 -1
- package/hooks/scripts/__tests__/run-ledger-verify-required.test.js +146 -0
- package/hooks/scripts/__tests__/run-ledger.test.js +330 -0
- package/hooks/scripts/__tests__/scope-from-spec.test.js +215 -0
- package/hooks/scripts/__tests__/sentinel-guard.test.js +79 -24
- package/hooks/scripts/__tests__/step-counter.test.js +95 -15
- package/hooks/scripts/__tests__/utils-npm-root.test.js +98 -0
- package/hooks/scripts/auto-commit.js +30 -11
- package/hooks/scripts/auto-format.js +96 -26
- package/hooks/scripts/auto-test.js +187 -37
- package/hooks/scripts/code-check.js +292 -99
- package/hooks/scripts/codex-hook-adapter.js +12 -1
- package/hooks/scripts/command-log.js +26 -16
- package/hooks/scripts/lib/dispatcher.js +38 -0
- package/hooks/scripts/lib/hook-context.js +101 -0
- package/hooks/scripts/lib/pr-gate-runner.js +62 -0
- package/hooks/scripts/lib/run-ledger.js +169 -0
- package/hooks/scripts/lib/scope-from-spec.js +40 -7
- package/hooks/scripts/post-edit-dispatcher.js +93 -20
- package/hooks/scripts/post-edit.js +40 -19
- package/hooks/scripts/pr-test-gate.js +8 -37
- package/hooks/scripts/pre-tool-dispatcher.js +18 -16
- package/hooks/scripts/pre-tool-guard.js +55 -52
- package/hooks/scripts/prompt-dispatcher.js +10 -0
- package/hooks/scripts/scope-guard.js +40 -39
- package/hooks/scripts/sentinel-guard.js +41 -41
- package/hooks/scripts/session-start.js +41 -16
- package/hooks/scripts/step-counter.js +100 -7
- package/hooks/scripts/stop-dispatcher.js +26 -0
- package/hooks/scripts/utils.js +96 -30
- package/hooks/scripts/verify-ledger.js +22 -0
- package/package.json +2 -2
- package/skills/arch-guard/SKILL.md +2 -2
- package/skills/characterization-test/SKILL.md +2 -2
- package/skills/exec-plan/SKILL.md +2 -2
- package/skills/spec/SKILL.md +6 -312
- package/skills/spec/references/askuser-examples.md +57 -0
- package/skills/spec/references/example-session.md +87 -0
- package/skills/spec/references/templates.md +194 -0
- package/skills/vibe.run/SKILL.md +145 -1682
- package/skills/vibe.run/references/brand-assets.md +59 -0
- package/skills/vibe.run/references/parallel-agents.md +326 -0
- package/skills/vibe.run/references/race-review.md +272 -0
- package/skills/vibe.run/references/ralph-loop.md +172 -0
- package/skills/vibe.run/references/ultrawork-mode.md +148 -0
- package/skills/vibe.trace/SKILL.md +25 -38
- package/skills/vibe.verify/SKILL.md +15 -0
- package/vibe/templates/claudemd-template.md +1 -1
- package/hooks/scripts/figma-guard.js +0 -219
package/hooks/hooks.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Dispatcher pattern — PreToolUse/PostToolUse/Stop는 단일 디스패처가 stdin을 한 번 읽어 순차 실행 (병렬 spawn 폭주 제거, cascade 격리, config.hooks.{name}.enabled 토글 지원). UserPromptSubmit은 VIBE_HOOK_DEPTH 재귀 가드 탑재된 prompt-dispatcher 사용.",
|
|
2
|
+
"_comment": "Dispatcher pattern — PreToolUse/PostToolUse/Stop는 단일 디스패처가 stdin을 한 번 읽어 순차 실행 (병렬 spawn 폭주 제거, cascade 격리, config.hooks.{name}.enabled 토글 지원). UserPromptSubmit은 VIBE_HOOK_DEPTH 재귀 가드 탑재된 prompt-dispatcher 사용. SessionStart matcher가 compact를 제외하는 이유: session-start.js가 컨텍스트 앞단에 현재 시각/24h 카운트 등 동적 값을 주입하므로, compact마다 재발화하면 압축 후 프리픽스가 매번 달라져 prefix cache를 무효화한다.",
|
|
3
3
|
"permissions": {
|
|
4
4
|
"allow": [],
|
|
5
5
|
"deny": [],
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"hooks": {
|
|
9
9
|
"SessionStart": [
|
|
10
10
|
{
|
|
11
|
+
"matcher": "startup|resume|clear",
|
|
11
12
|
"hooks": [
|
|
12
13
|
{
|
|
13
14
|
"type": "command",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
]
|
|
66
67
|
},
|
|
67
68
|
{
|
|
69
|
+
"matcher": "Edit|Write|Bash|Task|SlashCommand|NotebookEdit",
|
|
68
70
|
"hooks": [
|
|
69
71
|
{
|
|
70
72
|
"type": "command",
|
|
@@ -74,14 +76,6 @@
|
|
|
74
76
|
}
|
|
75
77
|
],
|
|
76
78
|
"UserPromptSubmit": [
|
|
77
|
-
{
|
|
78
|
-
"hooks": [
|
|
79
|
-
{
|
|
80
|
-
"type": "command",
|
|
81
|
-
"command": "echo '[INTERRUPT RULE] If this message follows a user interrupt (Ctrl+C/Escape), the previous task is CANCELLED. Do NOT resume interrupted work. Respond ONLY to this new message.'"
|
|
82
|
-
}
|
|
83
|
-
]
|
|
84
|
-
},
|
|
85
79
|
{
|
|
86
80
|
"hooks": [
|
|
87
81
|
{
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[2026-06-10T07:48:43.637Z] rm -rf /
|
|
2
|
+
[2026-06-10T07:48:43.682Z] ls -la
|
|
3
|
+
[2026-06-10T07:48:43.728Z] rm -rf src/infra/lib/autonomy/
|
|
4
|
+
[2026-06-10T07:48:46.450Z] rm -rf /
|
|
5
|
+
[2026-06-10T07:48:46.533Z] ls -la
|
|
6
|
+
[2026-06-10T07:48:46.615Z] rm -rf src/infra/lib/autonomy/
|
|
7
|
+
[2026-06-10T23:31:11.461Z] rm -rf /
|
|
8
|
+
[2026-06-10T23:31:11.532Z] ls -la
|
|
9
|
+
[2026-06-10T23:31:11.610Z] rm -rf src/infra/lib/autonomy/
|
|
10
|
+
[2026-06-11T00:54:20.384Z] rm -rf /
|
|
11
|
+
[2026-06-11T00:54:20.472Z] ls -la
|
|
12
|
+
[2026-06-11T00:54:20.544Z] rm -rf src/infra/lib/autonomy/
|
|
13
|
+
[2026-06-11T00:55:23.164Z] rm -rf /
|
|
14
|
+
[2026-06-11T00:55:23.258Z] ls -la
|
|
15
|
+
[2026-06-11T00:55:23.351Z] rm -rf src/infra/lib/autonomy/
|
|
16
|
+
[2026-06-11T01:04:45.163Z] rm -rf /
|
|
17
|
+
[2026-06-11T01:04:45.246Z] ls -la
|
|
18
|
+
[2026-06-11T01:04:45.326Z] rm -rf src/infra/lib/autonomy/
|
|
19
|
+
[2026-06-11T01:05:19.173Z] rm -rf /
|
|
20
|
+
[2026-06-11T01:05:19.280Z] ls -la
|
|
21
|
+
[2026-06-11T01:05:19.346Z] rm -rf src/infra/lib/autonomy/
|
|
22
|
+
[2026-06-11T01:20:55.438Z] rm -rf /
|
|
23
|
+
[2026-06-11T01:20:55.530Z] ls -la
|
|
24
|
+
[2026-06-11T01:20:55.622Z] rm -rf src/infra/lib/autonomy/
|
|
25
|
+
[2026-06-11T01:21:25.176Z] rm -rf /
|
|
26
|
+
[2026-06-11T01:21:25.271Z] ls -la
|
|
27
|
+
[2026-06-11T01:21:25.380Z] rm -rf src/infra/lib/evolution/
|
|
28
|
+
[2026-06-11T01:21:40.631Z] rm -rf /
|
|
29
|
+
[2026-06-11T01:21:40.715Z] ls -la
|
|
30
|
+
[2026-06-11T01:21:40.789Z] rm -rf src/infra/lib/evolution/
|
|
31
|
+
[2026-06-11T01:21:58.442Z] rm -rf /
|
|
32
|
+
[2026-06-11T01:21:58.547Z] ls -la
|
|
33
|
+
[2026-06-11T01:21:58.640Z] rm -rf src/infra/lib/evolution/
|
|
34
|
+
[2026-06-11T01:22:15.393Z] rm -rf /
|
|
35
|
+
[2026-06-11T01:22:15.536Z] ls -la
|
|
36
|
+
[2026-06-11T01:22:15.628Z] rm -rf src/infra/lib/evolution/
|
|
37
|
+
[2026-06-11T01:30:46.209Z] rm -rf /
|
|
38
|
+
[2026-06-11T01:30:46.279Z] ls -la
|
|
39
|
+
[2026-06-11T01:30:46.366Z] rm -rf src/infra/lib/evolution/
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auto-test.js debounce 테스트
|
|
3
|
+
*
|
|
4
|
+
* 검증 대상:
|
|
5
|
+
* - debounce 모드에서 쿨다운 내 + 소스 미변경 → 스킵
|
|
6
|
+
* - 쿨다운 만료 → 재실행
|
|
7
|
+
* - 소스 변경 → 재실행
|
|
8
|
+
* - mode='off' → 실행 안 함
|
|
9
|
+
* - mode='always' → debounce 없이 항상 실행 시도
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
import { createHash } from 'crypto';
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
let tmpDir;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-autotest-debounce-'));
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// debounce 로직 단위 테스트 (auto-test.js와 동일 알고리즘)
|
|
29
|
+
const DEBOUNCE_COOLDOWN_MS = 120_000;
|
|
30
|
+
|
|
31
|
+
function fileHash(filePath) {
|
|
32
|
+
try {
|
|
33
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
+
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
35
|
+
} catch {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeDebounceHelpers(stateFile) {
|
|
41
|
+
function readState() {
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(stateFile)) return JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
44
|
+
} catch { /* ignore */ }
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeState(state) {
|
|
49
|
+
try {
|
|
50
|
+
fs.mkdirSync(path.dirname(stateFile), { recursive: true });
|
|
51
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf-8');
|
|
52
|
+
} catch { /* ignore */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function shouldSkip(testFile, srcFile) {
|
|
56
|
+
try {
|
|
57
|
+
const state = readState();
|
|
58
|
+
const entry = state[testFile];
|
|
59
|
+
if (!entry) return false;
|
|
60
|
+
const elapsed = Date.now() - entry.lastRun;
|
|
61
|
+
if (elapsed > DEBOUNCE_COOLDOWN_MS) return false;
|
|
62
|
+
const currentHash = fileHash(srcFile);
|
|
63
|
+
if (currentHash !== entry.srcHash) return false;
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function updateState(testFile, srcFile) {
|
|
71
|
+
const state = readState();
|
|
72
|
+
state[testFile] = { lastRun: Date.now(), srcHash: fileHash(srcFile) };
|
|
73
|
+
writeState(state);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { shouldSkip, updateState, readState };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('auto-test debounce: 핵심 로직', () => {
|
|
80
|
+
it('첫 실행: state 없음 → 스킵 안 함', () => {
|
|
81
|
+
const stateFile = path.join(tmpDir, 'auto-test-state.json');
|
|
82
|
+
const { shouldSkip } = makeDebounceHelpers(stateFile);
|
|
83
|
+
const testFile = path.join(tmpDir, 'foo.test.ts');
|
|
84
|
+
const srcFile = path.join(tmpDir, 'foo.ts');
|
|
85
|
+
fs.writeFileSync(srcFile, 'const x = 1;\n', 'utf-8');
|
|
86
|
+
expect(shouldSkip(testFile, srcFile)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('state 업데이트 후 쿨다운 내 + 소스 미변경 → 스킵', () => {
|
|
90
|
+
const stateFile = path.join(tmpDir, 'auto-test-state.json');
|
|
91
|
+
const { shouldSkip, updateState } = makeDebounceHelpers(stateFile);
|
|
92
|
+
const testFile = path.join(tmpDir, 'foo.test.ts');
|
|
93
|
+
const srcFile = path.join(tmpDir, 'foo.ts');
|
|
94
|
+
fs.writeFileSync(srcFile, 'const x = 1;\n', 'utf-8');
|
|
95
|
+
updateState(testFile, srcFile);
|
|
96
|
+
// 소스 변경 없음 + 즉시 재호출 → 스킵
|
|
97
|
+
expect(shouldSkip(testFile, srcFile)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('소스 변경 후 → 스킵 안 함 (재실행)', () => {
|
|
101
|
+
const stateFile = path.join(tmpDir, 'auto-test-state.json');
|
|
102
|
+
const { shouldSkip, updateState } = makeDebounceHelpers(stateFile);
|
|
103
|
+
const testFile = path.join(tmpDir, 'foo.test.ts');
|
|
104
|
+
const srcFile = path.join(tmpDir, 'foo.ts');
|
|
105
|
+
fs.writeFileSync(srcFile, 'const x = 1;\n', 'utf-8');
|
|
106
|
+
updateState(testFile, srcFile);
|
|
107
|
+
// 소스 변경
|
|
108
|
+
fs.writeFileSync(srcFile, 'const x = 2;\n', 'utf-8');
|
|
109
|
+
expect(shouldSkip(testFile, srcFile)).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('쿨다운 만료 → 스킵 안 함 (재실행)', () => {
|
|
113
|
+
const stateFile = path.join(tmpDir, 'auto-test-state.json');
|
|
114
|
+
const { shouldSkip, updateState, readState } = makeDebounceHelpers(stateFile);
|
|
115
|
+
const testFile = path.join(tmpDir, 'foo.test.ts');
|
|
116
|
+
const srcFile = path.join(tmpDir, 'foo.ts');
|
|
117
|
+
fs.writeFileSync(srcFile, 'const x = 1;\n', 'utf-8');
|
|
118
|
+
updateState(testFile, srcFile);
|
|
119
|
+
|
|
120
|
+
// lastRun을 과거로 조작 (쿨다운 초과)
|
|
121
|
+
const state = readState();
|
|
122
|
+
state[testFile].lastRun = Date.now() - DEBOUNCE_COOLDOWN_MS - 1000;
|
|
123
|
+
fs.mkdirSync(path.dirname(stateFile), { recursive: true });
|
|
124
|
+
fs.writeFileSync(stateFile, JSON.stringify(state), 'utf-8');
|
|
125
|
+
|
|
126
|
+
expect(shouldSkip(testFile, srcFile)).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('state 파일 없어도 fail-open (스킵 안 함)', () => {
|
|
130
|
+
const stateFile = path.join(tmpDir, 'nonexistent', 'state.json');
|
|
131
|
+
const { shouldSkip } = makeDebounceHelpers(stateFile);
|
|
132
|
+
const testFile = path.join(tmpDir, 'foo.test.ts');
|
|
133
|
+
const srcFile = path.join(tmpDir, 'foo.ts');
|
|
134
|
+
expect(shouldSkip(testFile, srcFile)).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('auto-test: state 파일 경로 패턴', () => {
|
|
139
|
+
it('state 파일이 .vibe/metrics/ 하위에 위치', () => {
|
|
140
|
+
// 경로 패턴 검증 (실제 PROJECT_DIR은 다름)
|
|
141
|
+
const expectedPattern = /\.vibe[/\\]metrics[/\\]auto-test-state\.json$/;
|
|
142
|
+
const stateFile = path.join('/some/project', '.vibe', 'metrics', 'auto-test-state.json');
|
|
143
|
+
expect(expectedPattern.test(stateFile)).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* code-check.js 정밀 탐지기 테스트
|
|
3
|
+
*
|
|
4
|
+
* 검증 대상:
|
|
5
|
+
* - any 탐지: 단어 경계 기반 (false positive: 'company', 'anything' 등)
|
|
6
|
+
* - any 탐지: `: any`, `as any`, `@ts-ignore`, `<any>` (true positive)
|
|
7
|
+
* - console.log 정책: 경로별 허용/차단
|
|
8
|
+
* - run-ledger verifyRequired round-trip + pass 시 clear
|
|
9
|
+
* - auto-commit verifyRequired skip
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
// ─── any 탐지 정규식 (code-check.js와 동일) ──────────────────────────────
|
|
20
|
+
const P1_DETECTORS = [
|
|
21
|
+
/:\s*any\b/,
|
|
22
|
+
/\bas\s+any\b/,
|
|
23
|
+
/<any[\s,>]/,
|
|
24
|
+
/@ts-ignore\b/,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function detectsAny(line) {
|
|
28
|
+
return P1_DETECTORS.some(re => re.test(line));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('any 탐지: true positive', () => {
|
|
32
|
+
it(': any — 타입 어노테이션', () => {
|
|
33
|
+
expect(detectsAny('function foo(x: any): void {}')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it(': any — 공백 포함', () => {
|
|
37
|
+
expect(detectsAny('const x: any = 1;')).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('as any — 타입 캐스트', () => {
|
|
41
|
+
expect(detectsAny('const y = x as any;')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('<any> — 제네릭', () => {
|
|
45
|
+
expect(detectsAny('const z = foo<any>();')).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('<any, — 제네릭 다중 파라미터', () => {
|
|
49
|
+
expect(detectsAny('const m = new Map<any, string>();')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('@ts-ignore — 라인 직전', () => {
|
|
53
|
+
expect(detectsAny('// @ts-ignore')).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('@ts-ignore — 인라인', () => {
|
|
57
|
+
expect(detectsAny(' @ts-ignore ')).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('any 탐지: false negative (탐지 안 됨)', () => {
|
|
62
|
+
it('"company" 단어 포함 — 탐지 안 됨', () => {
|
|
63
|
+
expect(detectsAny('const company = "Acme";')).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('"anything" 식별자 — 탐지 안 됨', () => {
|
|
67
|
+
expect(detectsAny('if (anything) return;')).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('"manyThings" — 탐지 안 됨', () => {
|
|
71
|
+
expect(detectsAny('const manyThings = [];')).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('"fantasy" — 탐지 안 됨', () => {
|
|
75
|
+
expect(detectsAny('const fantasy = "world";')).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('문자열 리터럴 "any" — 정규식은 문자열 내부도 매칭할 수 있음 (알려진 한계, 문서화)', () => {
|
|
79
|
+
// NOTE: 현재 구현은 문자열 내 "any"를 완전히 제외하지 않음.
|
|
80
|
+
// 실제 코드에서 ": any" 패턴이 문자열로 존재하는 경우는 극히 드물어 허용.
|
|
81
|
+
// 이 테스트는 동작을 문서화하는 것이 목적.
|
|
82
|
+
const result = detectsAny('const msg = "type: any is bad";');
|
|
83
|
+
// 문자열 내 ": any" 도 탐지될 수 있음 — false positive지만 드문 케이스
|
|
84
|
+
expect(typeof result).toBe('boolean');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ─── console.log 경로 정책 ────────────────────────────────────────────────
|
|
89
|
+
describe('console.log 허용 경로 정책', () => {
|
|
90
|
+
// 허용 glob 패턴 (code-check.js 기본값과 동일)
|
|
91
|
+
const DEFAULT_ALLOW_GLOBS = [
|
|
92
|
+
'hooks/scripts/**',
|
|
93
|
+
'scripts/**',
|
|
94
|
+
'**/cli/**',
|
|
95
|
+
'**/*.test.*',
|
|
96
|
+
'**/*.spec.*',
|
|
97
|
+
'**/__tests__/**',
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
function globToRegExp(glob) {
|
|
101
|
+
const normalized = glob.replace(/\\/g, '/');
|
|
102
|
+
let out = '';
|
|
103
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
104
|
+
const c = normalized[i];
|
|
105
|
+
if (c === '*') {
|
|
106
|
+
if (normalized[i + 1] === '*') {
|
|
107
|
+
out += '.*';
|
|
108
|
+
i++;
|
|
109
|
+
if (normalized[i + 1] === '/') i++;
|
|
110
|
+
} else {
|
|
111
|
+
out += '[^/]*';
|
|
112
|
+
}
|
|
113
|
+
} else if (c === '?') {
|
|
114
|
+
out += '[^/]';
|
|
115
|
+
} else if ('.+^$()|{}[]\\'.includes(c)) {
|
|
116
|
+
out += '\\' + c;
|
|
117
|
+
} else {
|
|
118
|
+
out += c;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return new RegExp('^' + out + '$');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isConsoleAllowed(relPath) {
|
|
125
|
+
return DEFAULT_ALLOW_GLOBS.some(g => globToRegExp(g).test(relPath));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
it('hooks/scripts/auto-test.js → 허용', () => {
|
|
129
|
+
expect(isConsoleAllowed('hooks/scripts/auto-test.js')).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('hooks/scripts/lib/run-ledger.js → 허용', () => {
|
|
133
|
+
expect(isConsoleAllowed('hooks/scripts/lib/run-ledger.js')).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('src/cli/index.ts → 허용 (**/cli/**)', () => {
|
|
137
|
+
expect(isConsoleAllowed('src/cli/index.ts')).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('src/tools/foo.test.ts → 허용 (**/*.test.*)', () => {
|
|
141
|
+
expect(isConsoleAllowed('src/tools/foo.test.ts')).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('src/__tests__/bar.ts → 허용 (**/__tests__/**)', () => {
|
|
145
|
+
expect(isConsoleAllowed('src/__tests__/bar.ts')).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('src/tools/convention/validateCodeQuality.ts → 차단', () => {
|
|
149
|
+
expect(isConsoleAllowed('src/tools/convention/validateCodeQuality.ts')).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('src/infra/orchestrator/index.ts → 차단', () => {
|
|
153
|
+
expect(isConsoleAllowed('src/infra/orchestrator/index.ts')).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* in-process 디스패처 외부 계약 테스트
|
|
3
|
+
*
|
|
4
|
+
* SPEC: .vibe/specs/hook-dispatcher-inprocess.md
|
|
5
|
+
* 디스패처를 Claude Code와 동일하게 CLI로 spawn하여 검증한다 —
|
|
6
|
+
* 내부 구현(in-process)이 아니라 외부 계약(exit code, stdout 주입)이 대상.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { execFileSync } from 'child_process';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const PRE_DISPATCHER = path.resolve(__dirname, '..', 'pre-tool-dispatcher.js');
|
|
15
|
+
const POST_DISPATCHER = path.resolve(__dirname, '..', 'post-edit-dispatcher.js');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 디스패처를 stdin 페이로드와 함께 실행. { stdout, exitCode } 반환.
|
|
19
|
+
*/
|
|
20
|
+
function runDispatcher(script, args, payload) {
|
|
21
|
+
const input = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
22
|
+
try {
|
|
23
|
+
const stdout = execFileSync('node', [script, ...args], {
|
|
24
|
+
encoding: 'utf-8',
|
|
25
|
+
input,
|
|
26
|
+
timeout: 15000,
|
|
27
|
+
// scope.json이 없는 격리된 cwd에서 실행 (scope-guard no-op 보장)
|
|
28
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: __dirname },
|
|
29
|
+
});
|
|
30
|
+
return { stdout: stdout.trim(), exitCode: 0 };
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return { stdout: (err.stdout || '').trim(), exitCode: err.status };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('pre-tool-dispatcher (in-process)', () => {
|
|
37
|
+
it('AC-1: 위험 Bash 명령은 exit 2로 deny', () => {
|
|
38
|
+
const { exitCode } = runDispatcher(PRE_DISPATCHER, ['Bash'], {
|
|
39
|
+
tool_name: 'Bash',
|
|
40
|
+
tool_input: { command: 'rm -rf /' },
|
|
41
|
+
});
|
|
42
|
+
expect(exitCode).toBe(2);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('AC-2: sentinel 경로 Edit은 exit 2 + stdout에 block JSON 주입', () => {
|
|
46
|
+
// 실제 sentinel 경로: src/infra/lib/evolution/ (autonomy/ 는 팬텀 경로였음)
|
|
47
|
+
const { stdout, exitCode } = runDispatcher(PRE_DISPATCHER, ['Edit'], {
|
|
48
|
+
tool_name: 'Edit',
|
|
49
|
+
tool_input: { file_path: 'src/infra/lib/evolution/GuardAnalyzer.ts', old_string: 'a', new_string: 'b' },
|
|
50
|
+
});
|
|
51
|
+
expect(exitCode).toBe(2);
|
|
52
|
+
expect(stdout).toContain('"decision":"block"');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('AC-3: 안전한 Bash 명령은 exit 0', () => {
|
|
56
|
+
const { exitCode } = runDispatcher(PRE_DISPATCHER, ['Bash'], {
|
|
57
|
+
tool_name: 'Bash',
|
|
58
|
+
tool_input: { command: 'ls -la' },
|
|
59
|
+
});
|
|
60
|
+
expect(exitCode).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('AC-3: 안전한 Edit은 exit 0 (scope.json 없음 → scope-guard no-op)', () => {
|
|
64
|
+
const { exitCode } = runDispatcher(PRE_DISPATCHER, ['Edit'], {
|
|
65
|
+
tool_name: 'Edit',
|
|
66
|
+
tool_input: { file_path: 'src/cli/index.ts', old_string: 'a', new_string: 'b' },
|
|
67
|
+
});
|
|
68
|
+
expect(exitCode).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('AC-2 변형: 위험 명령이 sentinel 경로를 노리면 sentinel-guard가 차단', () => {
|
|
72
|
+
// 실제 sentinel 경로: src/infra/lib/evolution/
|
|
73
|
+
const { exitCode } = runDispatcher(PRE_DISPATCHER, ['Bash'], {
|
|
74
|
+
tool_name: 'Bash',
|
|
75
|
+
tool_input: { command: 'rm -rf src/infra/lib/evolution/' },
|
|
76
|
+
});
|
|
77
|
+
expect(exitCode).toBe(2);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('post-edit-dispatcher (in-process)', () => {
|
|
82
|
+
it('AC-4: 손상된 stdin(비JSON)에도 exit 0 (fail-open)', () => {
|
|
83
|
+
const { exitCode } = runDispatcher(POST_DISPATCHER, [], 'not-json');
|
|
84
|
+
expect(exitCode).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('AC-4: 빈 페이로드에도 exit 0', () => {
|
|
88
|
+
const { exitCode } = runDispatcher(POST_DISPATCHER, [], {});
|
|
89
|
+
expect(exitCode).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('존재하지 않는 파일 경로 페이로드에도 exit 0 (step 내부 격리)', () => {
|
|
93
|
+
const { exitCode } = runDispatcher(POST_DISPATCHER, [], {
|
|
94
|
+
tool_name: 'Edit',
|
|
95
|
+
tool_input: { file_path: '/nonexistent/__vibe_test__.xyz' },
|
|
96
|
+
});
|
|
97
|
+
expect(exitCode).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* post-edit-dispatcher 계약 테스트
|
|
3
|
+
*
|
|
4
|
+
* 검증 대상:
|
|
5
|
+
* - findings 있음 → stdout에 JSON hookSpecificOutput 출력, exit 0
|
|
6
|
+
* - findings 없음 → stdout 없음, exit 0
|
|
7
|
+
* - 손상된 stdin에도 exit 0 (fail-open)
|
|
8
|
+
* - additionalContext 형식 정확성
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { spawnSync } from 'child_process';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const DISPATCHER = path.resolve(__dirname, '..', 'post-edit-dispatcher.js');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 디스패처를 stdin 페이로드와 함께 실행.
|
|
22
|
+
* @param {object|string} payload
|
|
23
|
+
* @param {string} [projectDir]
|
|
24
|
+
* @returns {{ stdout: string, stderr: string, exitCode: number }}
|
|
25
|
+
*/
|
|
26
|
+
function runDispatcher(payload, projectDir) {
|
|
27
|
+
const input = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
28
|
+
const env = {
|
|
29
|
+
...process.env,
|
|
30
|
+
CLAUDE_PROJECT_DIR: projectDir || __dirname,
|
|
31
|
+
};
|
|
32
|
+
const result = spawnSync('node', [DISPATCHER], {
|
|
33
|
+
input,
|
|
34
|
+
encoding: 'utf-8',
|
|
35
|
+
timeout: 30000,
|
|
36
|
+
env,
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
stdout: (result.stdout || '').trim(),
|
|
40
|
+
stderr: (result.stderr || '').trim(),
|
|
41
|
+
exitCode: result.status ?? 0,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('post-edit-dispatcher: 출력 형식', () => {
|
|
46
|
+
it('빈 페이로드 → exit 0, stdout 없음 (findings 없음)', () => {
|
|
47
|
+
const { stdout, exitCode } = runDispatcher({});
|
|
48
|
+
expect(exitCode).toBe(0);
|
|
49
|
+
expect(stdout).toBe('');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('손상된 stdin(비JSON) → exit 0 (fail-open)', () => {
|
|
53
|
+
const { exitCode } = runDispatcher('not-json');
|
|
54
|
+
expect(exitCode).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('존재하지 않는 파일 경로 → exit 0', () => {
|
|
58
|
+
const { exitCode } = runDispatcher({
|
|
59
|
+
tool_name: 'Edit',
|
|
60
|
+
tool_input: { file_path: '/nonexistent/__vibe_test__.xyz' },
|
|
61
|
+
});
|
|
62
|
+
expect(exitCode).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('post-edit-dispatcher: JSON hookSpecificOutput 형식', () => {
|
|
67
|
+
let tmpDir;
|
|
68
|
+
let tsFile;
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-dispatcher-test-'));
|
|
72
|
+
tsFile = path.join(tmpDir, 'test-file.ts');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('P1 이슈가 있는 TS 파일 → JSON hookSpecificOutput 출력', () => {
|
|
80
|
+
// any 타입이 포함된 TS 파일 생성
|
|
81
|
+
fs.writeFileSync(tsFile, 'function foo(x: any): any { return x; }\n', 'utf-8');
|
|
82
|
+
|
|
83
|
+
const { stdout, exitCode } = runDispatcher({
|
|
84
|
+
tool_name: 'Edit',
|
|
85
|
+
tool_input: { file_path: tsFile },
|
|
86
|
+
}, tmpDir);
|
|
87
|
+
|
|
88
|
+
expect(exitCode).toBe(0);
|
|
89
|
+
if (stdout) {
|
|
90
|
+
// stdout이 있으면 JSON hookSpecificOutput 형식이어야 함
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(stdout);
|
|
94
|
+
} catch {
|
|
95
|
+
// plain text일 수도 있음 (code-check 모듈 로드 실패 시)
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (parsed.hookSpecificOutput) {
|
|
99
|
+
expect(parsed.hookSpecificOutput.hookEventName).toBe('PostToolUse');
|
|
100
|
+
expect(typeof parsed.hookSpecificOutput.additionalContext).toBe('string');
|
|
101
|
+
expect(parsed.hookSpecificOutput.additionalContext.length).toBeGreaterThan(0);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('findings 있으면 additionalContext에 요약 포함', () => {
|
|
107
|
+
fs.writeFileSync(tsFile, 'const x: any = 1;\n', 'utf-8');
|
|
108
|
+
|
|
109
|
+
const { stdout, exitCode } = runDispatcher({
|
|
110
|
+
tool_name: 'Edit',
|
|
111
|
+
tool_input: { file_path: tsFile },
|
|
112
|
+
}, tmpDir);
|
|
113
|
+
|
|
114
|
+
expect(exitCode).toBe(0);
|
|
115
|
+
if (stdout) {
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(stdout);
|
|
118
|
+
if (parsed.hookSpecificOutput?.additionalContext) {
|
|
119
|
+
// any 탐지 관련 내용이 포함되어야 함
|
|
120
|
+
expect(parsed.hookSpecificOutput.additionalContext).toContain('any');
|
|
121
|
+
}
|
|
122
|
+
} catch { /* JSON 아닐 수 있음 */ }
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('post-edit-dispatcher: additionalContext JSON 계약', () => {
|
|
128
|
+
it('JSON 구조가 올바른 hookSpecificOutput 스키마를 따름', () => {
|
|
129
|
+
// 직접 JSON 생성해 스키마 검증
|
|
130
|
+
const output = {
|
|
131
|
+
hookSpecificOutput: {
|
|
132
|
+
hookEventName: 'PostToolUse',
|
|
133
|
+
additionalContext: 'P1 any-type line 1: x: any',
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
expect(output.hookSpecificOutput.hookEventName).toBe('PostToolUse');
|
|
137
|
+
expect(typeof output.hookSpecificOutput.additionalContext).toBe('string');
|
|
138
|
+
});
|
|
139
|
+
});
|