@devrik-tools/claude-gates 0.1.2 → 0.2.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-plugin/marketplace.json +7 -1
- package/cli/__tests__/config.test.mjs +113 -101
- package/cli/__tests__/install.test.mjs +102 -0
- package/cli/__tests__/materialize.test.mjs +95 -0
- package/cli/__tests__/registry-gates-consistency.test.mjs +52 -44
- package/cli/__tests__/task.test.mjs +124 -0
- package/cli/__tests__/version-consistency.test.mjs +44 -0
- package/cli/config.mjs +124 -113
- package/cli/index.mjs +100 -97
- package/cli/init.mjs +259 -256
- package/cli/install.mjs +150 -80
- package/cli/materialize.mjs +102 -51
- package/cli/registry.mjs +136 -127
- package/cli/task.mjs +140 -0
- package/package.json +2 -1
- package/plugins/gates/.claude-plugin/plugin.json +8 -2
- package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +83 -0
- package/plugins/gates/hooks/__tests__/doctor.test.mjs +85 -0
- package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +65 -0
- package/plugins/gates/hooks/ask-adoption.mjs +147 -0
- package/plugins/gates/hooks/doctor.mjs +207 -0
- package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +88 -88
- package/plugins/gates/hooks/gates/audit-before-build/test.mjs +117 -92
- package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +104 -0
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +48 -45
- package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +165 -0
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +285 -268
- package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +151 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +265 -177
- package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +207 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +501 -456
- package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +161 -143
- package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +69 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +126 -118
- package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +68 -0
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +43 -49
- package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -100
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +119 -0
- package/plugins/gates/hooks/gates/force-parallel/test.mjs +88 -0
- package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +155 -0
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -112
- package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +66 -0
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -186
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -238
- package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +96 -0
- package/plugins/gates/hooks/gates/intent-flow/test.mjs +165 -136
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +149 -0
- package/plugins/gates/hooks/gates/lint-commit/test.mjs +85 -0
- package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +100 -0
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -166
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +79 -85
- package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +80 -0
- package/plugins/gates/hooks/gates/never-assume/index.mjs +58 -55
- package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +71 -0
- package/plugins/gates/hooks/gates/never-assume/test.mjs +95 -78
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +148 -142
- package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +109 -0
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +123 -120
- package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +91 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +144 -147
- package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +89 -64
- package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +102 -0
- package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +94 -0
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +159 -150
- package/plugins/gates/hooks/gates/risk-level/index.mjs +263 -203
- package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +116 -0
- package/plugins/gates/hooks/gates/risk-level/test.mjs +153 -125
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +56 -50
- package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +58 -0
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +116 -111
- package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +97 -0
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +184 -99
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +130 -0
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -251
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +159 -0
- package/plugins/gates/hooks/gates/stop-pending/test.mjs +114 -0
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -101
- package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +72 -0
- package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +63 -0
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -181
- package/plugins/gates/hooks/gates/test-matrix/test.mjs +114 -87
- package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +62 -0
- package/plugins/gates/hooks/gates/tool-map/index.mjs +143 -140
- package/plugins/gates/hooks/hooks.json +346 -286
- package/plugins/gates/hooks/lib/__tests__/config.test.mjs +173 -154
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +210 -154
- package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +114 -0
- package/plugins/gates/hooks/lib/config.mjs +172 -165
- package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +189 -0
- package/plugins/gates/hooks/lib/hook-io.mjs +318 -208
- package/plugins/gates/hooks/lib/signals.mjs +127 -0
- package/plugins/gates/hooks/wiring-check.mjs +227 -0
- package/plugins/tasks/.claude-plugin/plugin.json +14 -0
- package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +100 -0
- package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +95 -0
- package/plugins/tasks/hooks/hooks.json +26 -0
- package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +180 -132
- package/plugins/tasks/hooks/lib/task-store.mjs +197 -159
- package/plugins/tasks/hooks/register-requests.mjs +141 -108
- package/plugins/tasks/hooks/session-tasks.mjs +108 -0
- package/registry.json +793 -686
|
@@ -1,154 +1,210 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { test } from 'node:test';
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
5
|
-
import {
|
|
6
|
-
toolNamesFor,
|
|
7
|
-
matcherFor,
|
|
8
|
-
toolNameOf,
|
|
9
|
-
sessionIdOf,
|
|
10
|
-
toolInputOf,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
test('toolNamesFor expands
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
assert.
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('toolNamesFor
|
|
35
|
-
assert.deepEqual(toolNamesFor([
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test('
|
|
39
|
-
assert.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
assert.equal(
|
|
59
|
-
assert.equal(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
assert.equal(
|
|
65
|
-
assert.equal(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
assert.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
assert.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
assert.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
),
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
'
|
|
109
|
-
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
assert.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import {
|
|
6
|
+
toolNamesFor,
|
|
7
|
+
matcherFor,
|
|
8
|
+
toolNameOf,
|
|
9
|
+
sessionIdOf,
|
|
10
|
+
toolInputOf,
|
|
11
|
+
toolInGroups,
|
|
12
|
+
writtenContentOf,
|
|
13
|
+
writtenPathOf,
|
|
14
|
+
delegationPromptOf,
|
|
15
|
+
} from '../hook-io.mjs';
|
|
16
|
+
|
|
17
|
+
// deny/warn/allow/runGate call process.exit and cannot be invoked in-process without
|
|
18
|
+
// killing the test runner. They are exercised here as a child process instead; each
|
|
19
|
+
// gate's own tests additionally cover runGate's wiring through its real check function.
|
|
20
|
+
|
|
21
|
+
test('toolNamesFor expands a single group', () => {
|
|
22
|
+
assert.deepEqual(toolNamesFor(['question']), ['AskUserQuestion']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('toolNamesFor expands multiple groups and de-duplicates overlapping names', () => {
|
|
26
|
+
const names = toolNamesFor(['write', 'execution']);
|
|
27
|
+
// 'execution' repeats every 'write' tool plus Bash/run_command/mcp__ide__executeCode.
|
|
28
|
+
assert.equal(names.filter((name) => name === 'Write').length, 1);
|
|
29
|
+
assert.ok(names.includes('Write'));
|
|
30
|
+
assert.ok(names.includes('Bash'));
|
|
31
|
+
assert.ok(names.includes('mcp__ide__executeCode'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('toolNamesFor returns an empty list for an empty group set', () => {
|
|
35
|
+
assert.deepEqual(toolNamesFor([]), []);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('toolNamesFor ignores an unknown group name', () => {
|
|
39
|
+
assert.deepEqual(toolNamesFor(['not-a-real-group']), []);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('matcherFor joins native tool names with a pipe and always appends the mcp__.* clause', () => {
|
|
43
|
+
// The trailing `mcp__.*` makes Claude Code route EVERY MCP tool call to the hook, so the
|
|
44
|
+
// gate can classify it at runtime with toolInGroups. Without it, the hook is never even
|
|
45
|
+
// invoked for an MCP tool — the deepest layer of the MCP blind spot.
|
|
46
|
+
assert.equal(matcherFor(['question']), 'AskUserQuestion|mcp__.*');
|
|
47
|
+
assert.equal(matcherFor(['shell']), 'Bash|run_command|mcp__.*');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('matcherFor still matches MCP tools even for an empty native group set', () => {
|
|
51
|
+
// An empty group means "no native names", but MCP tools must still reach the hook.
|
|
52
|
+
assert.equal(matcherFor([]), 'mcp__.*');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('toolInGroups matches native names exactly and case-insensitively', () => {
|
|
56
|
+
assert.equal(toolInGroups('Write', ['write']), true);
|
|
57
|
+
assert.equal(toolInGroups('write', ['write']), true); // case-insensitive
|
|
58
|
+
assert.equal(toolInGroups('AskUserQuestion', ['question']), true);
|
|
59
|
+
assert.equal(toolInGroups('Bash', ['write']), false); // wrong group
|
|
60
|
+
assert.equal(toolInGroups('', ['write']), false); // empty tool name
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('toolInGroups classifies MCP tools by their action segment', () => {
|
|
64
|
+
assert.equal(toolInGroups('mcp__filesystem__write_file', ['write']), true);
|
|
65
|
+
assert.equal(toolInGroups('mcp__fs__edit_file', ['write']), true);
|
|
66
|
+
assert.equal(toolInGroups('mcp__shell__exec', ['shell']), true);
|
|
67
|
+
assert.equal(toolInGroups('mcp__x__ask_user_confirmation', ['question']), true);
|
|
68
|
+
assert.equal(toolInGroups('mcp__orchestrator__spawn_agent', ['delegation']), true);
|
|
69
|
+
// A read-only MCP tool must NOT match a write/shell group.
|
|
70
|
+
assert.equal(toolInGroups('mcp__fs__read_file', ['write', 'shell']), false);
|
|
71
|
+
// Malformed mcp name (no action segment) does not match.
|
|
72
|
+
assert.equal(toolInGroups('mcp__server', ['write']), false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('writtenContentOf reads content across native and MCP field shapes', () => {
|
|
76
|
+
assert.equal(writtenContentOf({ content: 'a' }), 'a'); // Write
|
|
77
|
+
assert.equal(writtenContentOf({ new_string: 'b' }), 'b'); // Edit
|
|
78
|
+
assert.equal(writtenContentOf({ new_source: 'c' }), 'c'); // NotebookEdit
|
|
79
|
+
assert.equal(writtenContentOf({ new_content: 'd' }), 'd'); // replace_file_content
|
|
80
|
+
assert.equal(writtenContentOf({ text: 'e' }), 'e'); // an MCP shape
|
|
81
|
+
assert.equal(
|
|
82
|
+
writtenContentOf({ edits: [{ new_string: 'x' }, { new_string: 'y' }] }),
|
|
83
|
+
'x\ny',
|
|
84
|
+
); // MultiEdit
|
|
85
|
+
assert.equal(writtenContentOf({}), '');
|
|
86
|
+
assert.equal(writtenContentOf(null), '');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('writtenPathOf reads the target path across field shapes', () => {
|
|
90
|
+
assert.equal(writtenPathOf({ file_path: 'a.js' }), 'a.js');
|
|
91
|
+
assert.equal(writtenPathOf({ path: 'b.js' }), 'b.js');
|
|
92
|
+
assert.equal(writtenPathOf({ notebook_path: 'n.ipynb' }), 'n.ipynb');
|
|
93
|
+
assert.equal(writtenPathOf({}), '');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('delegationPromptOf reads the brief across field shapes', () => {
|
|
97
|
+
assert.equal(delegationPromptOf({ prompt: 'a' }), 'a');
|
|
98
|
+
assert.equal(delegationPromptOf({ description: 'b' }), 'b'); // Task
|
|
99
|
+
assert.equal(delegationPromptOf({ instructions: 'c' }), 'c'); // an MCP shape
|
|
100
|
+
assert.equal(delegationPromptOf({}), '');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('toolNameOf reads tool_name, falls back to name, defaults to empty string', () => {
|
|
104
|
+
assert.equal(toolNameOf(JSON.stringify({ tool_name: 'Bash' })), 'Bash');
|
|
105
|
+
assert.equal(toolNameOf(JSON.stringify({ name: 'Edit' })), 'Edit');
|
|
106
|
+
assert.equal(
|
|
107
|
+
toolNameOf(JSON.stringify({ tool_name: 'Bash', name: 'Edit' })),
|
|
108
|
+
'Bash',
|
|
109
|
+
);
|
|
110
|
+
assert.equal(toolNameOf(JSON.stringify({})), '');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('toolNameOf returns null for unparseable payloads', () => {
|
|
114
|
+
assert.equal(toolNameOf('{not json'), null);
|
|
115
|
+
assert.equal(toolNameOf(undefined), null);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('sessionIdOf reads session_id or returns null when absent/unparseable', () => {
|
|
119
|
+
assert.equal(sessionIdOf(JSON.stringify({ session_id: 'abc123' })), 'abc123');
|
|
120
|
+
assert.equal(sessionIdOf(JSON.stringify({})), null);
|
|
121
|
+
assert.equal(sessionIdOf('{not json'), null);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('toolInputOf reads tool_input, falls back to input, defaults to {}', () => {
|
|
125
|
+
assert.deepEqual(
|
|
126
|
+
toolInputOf(JSON.stringify({ tool_input: { command: 'ls' } })),
|
|
127
|
+
{ command: 'ls' },
|
|
128
|
+
);
|
|
129
|
+
assert.deepEqual(toolInputOf(JSON.stringify({ input: { file: 'a' } })), {
|
|
130
|
+
file: 'a',
|
|
131
|
+
});
|
|
132
|
+
assert.deepEqual(
|
|
133
|
+
toolInputOf(JSON.stringify({ tool_input: { a: 1 }, input: { b: 2 } })),
|
|
134
|
+
{ a: 1 },
|
|
135
|
+
);
|
|
136
|
+
assert.deepEqual(toolInputOf(JSON.stringify({})), {});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('toolInputOf returns {} for unparseable payloads', () => {
|
|
140
|
+
assert.deepEqual(toolInputOf('{not json'), {});
|
|
141
|
+
assert.deepEqual(toolInputOf(undefined), {});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const hookIoModuleUrl = pathToFileURL(
|
|
145
|
+
new URL('../hook-io.mjs', import.meta.url).pathname.replace(
|
|
146
|
+
/^\/([A-Za-z]:)/,
|
|
147
|
+
'$1',
|
|
148
|
+
),
|
|
149
|
+
).href;
|
|
150
|
+
|
|
151
|
+
/** Runs `body` (a callback expression referencing `mod`) in a child process after
|
|
152
|
+
* dynamically importing hook-io.mjs there, so process.exit-calling exports can be
|
|
153
|
+
* exercised without killing the test runner. */
|
|
154
|
+
function runInChildProcess(body, execOptions) {
|
|
155
|
+
const script = `import('${hookIoModuleUrl}').then((mod) => { ${body} });`;
|
|
156
|
+
return execFileSync(process.execPath, ['--input-type=module', '-e', script], {
|
|
157
|
+
encoding: 'utf8',
|
|
158
|
+
...execOptions,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
test('readHookPayload reads whatever is piped on stdin, including empty input', () => {
|
|
163
|
+
const output = runInChildProcess(
|
|
164
|
+
'process.stdout.write(JSON.stringify(mod.readHookPayload()))',
|
|
165
|
+
{ input: '{"tool_name":"Bash"}' },
|
|
166
|
+
);
|
|
167
|
+
assert.equal(output, JSON.stringify('{"tool_name":"Bash"}'));
|
|
168
|
+
|
|
169
|
+
const emptyOutput = runInChildProcess(
|
|
170
|
+
'process.stdout.write(JSON.stringify(mod.readHookPayload()))',
|
|
171
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] },
|
|
172
|
+
);
|
|
173
|
+
// With no stdin available, readFileSync(0) resolves to an empty string rather than
|
|
174
|
+
// throwing on this platform, so readHookPayload returns '' — not null. The null
|
|
175
|
+
// path (an unreadable fd 0) is exercised indirectly by every gate's own tests via
|
|
176
|
+
// runGate, since forcing an EBADF/EAGAIN on fd 0 portably from node:test is not
|
|
177
|
+
// reliable across platforms.
|
|
178
|
+
assert.equal(emptyOutput, JSON.stringify(''));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('deny writes a permissionDecision:deny hookSpecificOutput and exits 0', () => {
|
|
182
|
+
const output = runInChildProcess("mod.deny('my-gate', 'because reasons')", {
|
|
183
|
+
input: '',
|
|
184
|
+
});
|
|
185
|
+
const parsed = JSON.parse(output);
|
|
186
|
+
assert.equal(parsed.hookSpecificOutput.hookEventName, 'PreToolUse');
|
|
187
|
+
assert.equal(parsed.hookSpecificOutput.permissionDecision, 'deny');
|
|
188
|
+
assert.equal(
|
|
189
|
+
parsed.hookSpecificOutput.permissionDecisionReason,
|
|
190
|
+
'[my-gate] because reasons',
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('warn writes additionalContext and exits 0', () => {
|
|
195
|
+
const output = runInChildProcess("mod.warn('my-gate', 'consider this')", {
|
|
196
|
+
input: '',
|
|
197
|
+
});
|
|
198
|
+
const parsed = JSON.parse(output);
|
|
199
|
+
assert.equal(parsed.hookSpecificOutput.hookEventName, 'PreToolUse');
|
|
200
|
+
assert.equal(
|
|
201
|
+
parsed.hookSpecificOutput.additionalContext,
|
|
202
|
+
'[my-gate] consider this',
|
|
203
|
+
);
|
|
204
|
+
assert.equal(parsed.hookSpecificOutput.permissionDecision, undefined);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('allow exits 0 with no stdout', () => {
|
|
208
|
+
const output = runInChildProcess('mod.allow()', { input: '' });
|
|
209
|
+
assert.equal(output, '');
|
|
210
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
RISK_SIGNAL,
|
|
5
|
+
MONEY_SIGNAL,
|
|
6
|
+
AUTH_SIGNAL,
|
|
7
|
+
DESTRUCTIVE_DEPLOY_SIGNAL,
|
|
8
|
+
CONJECTURE,
|
|
9
|
+
PERSISTENCE_VERB,
|
|
10
|
+
withUnicodeWordBoundary,
|
|
11
|
+
} from '../signals.mjs';
|
|
12
|
+
|
|
13
|
+
// ── RISK_SIGNAL ──────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
test('RISK_SIGNAL matches money terms in EN and ES', () => {
|
|
16
|
+
assert.ok(RISK_SIGNAL.test('update the payment flow'));
|
|
17
|
+
assert.ok(RISK_SIGNAL.test('actualizar el flujo de pago'));
|
|
18
|
+
assert.ok(RISK_SIGNAL.test('mover el saldo de una cuenta'));
|
|
19
|
+
assert.ok(RISK_SIGNAL.test('move the account balance'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('RISK_SIGNAL matches auth terms in EN and ES', () => {
|
|
23
|
+
assert.ok(RISK_SIGNAL.test('reset the user password'));
|
|
24
|
+
assert.ok(RISK_SIGNAL.test('resetear la contrasena del usuario'));
|
|
25
|
+
assert.ok(RISK_SIGNAL.test('resetear la contraseña del usuario'));
|
|
26
|
+
assert.ok(RISK_SIGNAL.test('cambiar la sesion activa'));
|
|
27
|
+
assert.ok(RISK_SIGNAL.test('cambiar la sesión activa'));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('RISK_SIGNAL matches destructive/deploy terms in EN and ES', () => {
|
|
31
|
+
assert.ok(RISK_SIGNAL.test('deploy to production'));
|
|
32
|
+
assert.ok(RISK_SIGNAL.test('desplegar a produccion'));
|
|
33
|
+
assert.ok(RISK_SIGNAL.test('truncate the orders table'));
|
|
34
|
+
assert.ok(RISK_SIGNAL.test('eliminar la tabla de pedidos'));
|
|
35
|
+
assert.ok(RISK_SIGNAL.test('run the migration'));
|
|
36
|
+
assert.ok(RISK_SIGNAL.test('correr la migracion'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('bilingual control: an ES risk brief matches exactly like its EN equivalent', () => {
|
|
40
|
+
const es = 'implementa el endpoint para mover el saldo de una cuenta bancaria';
|
|
41
|
+
const en = 'implement the endpoint to move the balance of a bank account';
|
|
42
|
+
assert.equal(RISK_SIGNAL.test(es), RISK_SIGNAL.test(en));
|
|
43
|
+
assert.ok(RISK_SIGNAL.test(es));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('RISK_SIGNAL does not fire on neutral text', () => {
|
|
47
|
+
assert.equal(RISK_SIGNAL.test('add a tooltip to the button'), false);
|
|
48
|
+
assert.equal(RISK_SIGNAL.test('agregar un tooltip al boton'), false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('RISK_SIGNAL false-positive guard: short terms require a full word', () => {
|
|
52
|
+
// "auth" inside "autor", "prod" inside "producto", "token" inside "tokenizer".
|
|
53
|
+
assert.equal(RISK_SIGNAL.test('the autor of this book'), false);
|
|
54
|
+
assert.equal(RISK_SIGNAL.test('el autor de este libro'), false);
|
|
55
|
+
assert.equal(RISK_SIGNAL.test('compra este producto'), false);
|
|
56
|
+
assert.equal(RISK_SIGNAL.test('buy this product'), false);
|
|
57
|
+
assert.equal(RISK_SIGNAL.test('the tokenizer splits words'), false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('MONEY_SIGNAL / AUTH_SIGNAL / DESTRUCTIVE_DEPLOY_SIGNAL are independently usable', () => {
|
|
61
|
+
assert.ok(MONEY_SIGNAL.test('pago'));
|
|
62
|
+
assert.equal(MONEY_SIGNAL.test('auth'), false);
|
|
63
|
+
assert.ok(AUTH_SIGNAL.test('token'));
|
|
64
|
+
assert.equal(AUTH_SIGNAL.test('pago'), false);
|
|
65
|
+
assert.ok(DESTRUCTIVE_DEPLOY_SIGNAL.test('produccion'));
|
|
66
|
+
assert.equal(DESTRUCTIVE_DEPLOY_SIGNAL.test('pago'), false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── CONJECTURE ───────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
test('CONJECTURE matches EN and ES conjecture phrasing', () => {
|
|
72
|
+
assert.ok(CONJECTURE.test('i assume the timezone is UTC'));
|
|
73
|
+
assert.ok(CONJECTURE.test('probably fine to skip this'));
|
|
74
|
+
assert.ok(CONJECTURE.test('supongo que esto funciona'));
|
|
75
|
+
assert.ok(CONJECTURE.test('asumo que el usuario ya esta logueado'));
|
|
76
|
+
assert.ok(CONJECTURE.test('deberia ser suficiente con esto'));
|
|
77
|
+
assert.ok(CONJECTURE.test('debería ser suficiente con esto'));
|
|
78
|
+
assert.ok(CONJECTURE.test('creo que esto anda bien'));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('bilingual control: ES and EN conjecture brief both trigger', () => {
|
|
82
|
+
const es = 'creo que el usuario ya esta autenticado, no lo verifique';
|
|
83
|
+
const en = 'i assume the user is already authenticated, did not verify it';
|
|
84
|
+
assert.equal(CONJECTURE.test(es), CONJECTURE.test(en));
|
|
85
|
+
assert.ok(CONJECTURE.test(es));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('CONJECTURE does not fire on neutral confirmed statements', () => {
|
|
89
|
+
assert.equal(CONJECTURE.test('verified: the timezone is UTC per config.json'), false);
|
|
90
|
+
assert.equal(CONJECTURE.test('confirmado: el usuario esta autenticado'), false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── PERSISTENCE_VERB ─────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
test('PERSISTENCE_VERB matches EN and ES persistence instructions', () => {
|
|
96
|
+
assert.ok(PERSISTENCE_VERB.test('save this to state.json'));
|
|
97
|
+
assert.ok(PERSISTENCE_VERB.test('guarda esto en decision.md'));
|
|
98
|
+
assert.ok(PERSISTENCE_VERB.test('persist the result in the database'));
|
|
99
|
+
assert.ok(PERSISTENCE_VERB.test('escribi esto en el archivo'));
|
|
100
|
+
assert.ok(PERSISTENCE_VERB.test('anota el resultado en el log'));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('PERSISTENCE_VERB does not fire on unrelated prose', () => {
|
|
104
|
+
assert.equal(PERSISTENCE_VERB.test('the button looks nice'), false);
|
|
105
|
+
assert.equal(PERSISTENCE_VERB.test('el boton se ve bien'), false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ── withUnicodeWordBoundary ──────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
test('withUnicodeWordBoundary rejects a substring match', () => {
|
|
111
|
+
const pattern = withUnicodeWordBoundary('auth');
|
|
112
|
+
assert.ok(pattern.test('auth required'));
|
|
113
|
+
assert.equal(pattern.test('autor'), false);
|
|
114
|
+
});
|