refactor(test): finish createHookScript refactor and cleanup

- Ensure all hook tests use rig.createHookScript for consistent path normalization.
- Remove unused writeFileSync and readFileSync imports from integration tests.
- Fix assertions in 'input override' test to match new .cjs extension and be more robust.
This commit is contained in:
Abhi
2026-02-06 13:19:31 -05:00
parent e1d8cc78d7
commit e2b96018bd
3 changed files with 151 additions and 155 deletions
+18 -25
View File
@@ -7,7 +7,6 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js'; import { TestRig } from './test-helper.js';
import { join } from 'node:path'; import { join } from 'node:path';
import { writeFileSync } from 'node:fs';
describe('Hooks Agent Flow', () => { describe('Hooks Agent Flow', () => {
let rig: TestRig; let rig: TestRig;
@@ -49,11 +48,10 @@ describe('Hooks Agent Flow', () => {
console.error('DEBUG: BeforeAgent hook executed'); console.error('DEBUG: BeforeAgent hook executed');
`; `;
const scriptPath = join(rig.testDir!, 'before_agent_context.cjs').replace( const scriptPath = rig.createHookScript(
/\\/g, 'before_agent_context.cjs',
'/', hookScript,
); );
writeFileSync(scriptPath, hookScript);
await rig.configure({ await rig.configure({
settings: { settings: {
@@ -118,11 +116,10 @@ describe('Hooks Agent Flow', () => {
} }
`; `;
const scriptPath = join(rig.testDir!, 'after_agent_verify.cjs').replace( const scriptPath = rig.createHookScript(
/\\/g, 'after_agent_verify.cjs',
'/', hookScript,
); );
writeFileSync(scriptPath, hookScript);
await rig.configure({ await rig.configure({
settings: { settings: {
@@ -182,11 +179,10 @@ describe('Hooks Agent Flow', () => {
fs.writeFileSync('${messageCountFile}', JSON.stringify(counts)); fs.writeFileSync('${messageCountFile}', JSON.stringify(counts));
console.log(JSON.stringify({ decision: 'allow' })); console.log(JSON.stringify({ decision: 'allow' }));
`; `;
const beforeModelScriptPath = join( const beforeModelScriptPath = rig.createHookScript(
rig.testDir!,
'before_model_counter.cjs', 'before_model_counter.cjs',
).replace(/\\/g, '/'); beforeModelScript,
writeFileSync(beforeModelScriptPath, beforeModelScript); );
const afterAgentScript = ` const afterAgentScript = `
console.log(JSON.stringify({ console.log(JSON.stringify({
@@ -198,11 +194,10 @@ describe('Hooks Agent Flow', () => {
} }
})); }));
`; `;
const afterAgentScriptPath = join( const afterAgentScriptPath = rig.createHookScript(
rig.testDir!,
'after_agent_clear.cjs', 'after_agent_clear.cjs',
).replace(/\\/g, '/'); afterAgentScript,
writeFileSync(afterAgentScriptPath, afterAgentScript); );
await rig.configure({ await rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
@@ -270,18 +265,16 @@ describe('Hooks Agent Flow', () => {
); );
const beforeAgentScript = "console.log('BeforeAgent Fired')"; const beforeAgentScript = "console.log('BeforeAgent Fired')";
const beforeAgentScriptPath = join( const beforeAgentScriptPath = rig.createHookScript(
rig.testDir!,
'before_agent_loop.cjs', 'before_agent_loop.cjs',
).replace(/\\/g, '/'); beforeAgentScript,
writeFileSync(beforeAgentScriptPath, beforeAgentScript); );
const afterAgentScript = "console.log('AfterAgent Fired')"; const afterAgentScript = "console.log('AfterAgent Fired')";
const afterAgentScriptPath = join( const afterAgentScriptPath = rig.createHookScript(
rig.testDir!,
'after_agent_loop.cjs', 'after_agent_loop.cjs',
).replace(/\\/g, '/'); afterAgentScript,
writeFileSync(afterAgentScriptPath, afterAgentScript); );
await rig.configure({ await rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
+120 -130
View File
@@ -7,7 +7,6 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig, poll } from './test-helper.js'; import { TestRig, poll } from './test-helper.js';
import { join } from 'node:path'; import { join } from 'node:path';
import { writeFileSync } from 'node:fs';
describe('Hooks System Integration', () => { describe('Hooks System Integration', () => {
let rig: TestRig; let rig: TestRig;
@@ -25,9 +24,8 @@ describe('Hooks System Integration', () => {
describe('Command Hooks - Blocking Behavior', () => { describe('Command Hooks - Blocking Behavior', () => {
it('should block tool execution when hook returns block decision', async () => { it('should block tool execution when hook returns block decision', async () => {
rig.setup('should block tool execution when hook returns block decision'); rig.setup('should block tool execution when hook returns block decision');
const scriptPath = join(rig.testDir!, 'block_tool.cjs'); const scriptPath = rig.createHookScript(
writeFileSync( 'block_tool.cjs',
scriptPath,
"console.log(JSON.stringify({decision: 'block', reason: 'File writing blocked by security policy'}))", "console.log(JSON.stringify({decision: 'block', reason: 'File writing blocked by security policy'}))",
); );
@@ -84,9 +82,8 @@ describe('Hooks System Integration', () => {
rig.setup( rig.setup(
'should block tool execution and use stderr as reason when hook exits with code 2', 'should block tool execution and use stderr as reason when hook exits with code 2',
); );
const scriptPath = join(rig.testDir!, 'block_tool_stderr.cjs'); const scriptPath = rig.createHookScript(
writeFileSync( 'block_tool_stderr.cjs',
scriptPath,
"process.stderr.write('File writing blocked by security policy'); process.exit(2)", "process.stderr.write('File writing blocked by security policy'); process.exit(2)",
); );
@@ -146,9 +143,8 @@ describe('Hooks System Integration', () => {
it('should allow tool execution when hook returns allow decision', async () => { it('should allow tool execution when hook returns allow decision', async () => {
rig.setup('should allow tool execution when hook returns allow decision'); rig.setup('should allow tool execution when hook returns allow decision');
const scriptPath = join(rig.testDir!, 'allow_tool.cjs'); const scriptPath = rig.createHookScript(
writeFileSync( 'allow_tool.cjs',
scriptPath,
"console.log(JSON.stringify({decision: 'allow', reason: 'File writing approved'}))", "console.log(JSON.stringify({decision: 'allow', reason: 'File writing approved'}))",
); );
@@ -199,12 +195,11 @@ describe('Hooks System Integration', () => {
describe('Command Hooks - Additional Context', () => { describe('Command Hooks - Additional Context', () => {
it('should add additional context from AfterTool hooks', async () => { it('should add additional context from AfterTool hooks', async () => {
rig.setup('should add additional context from AfterTool hooks'); rig.setup('should add additional context from AfterTool hooks');
const scriptPath = join(rig.testDir!, 'after_tool_context.cjs'); const scriptPath = rig.createHookScript(
writeFileSync( 'after_tool_context.cjs',
scriptPath,
"console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'AfterTool', additionalContext: 'Security scan: File content appears safe'}}))", "console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'AfterTool', additionalContext: 'Security scan: File content appears safe'}}))",
); );
const command = `node "${scriptPath.replace(/\\/g, '/')}"`; const command = `node "${scriptPath}"`;
rig.configure({ rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
import.meta.dirname, import.meta.dirname,
@@ -282,8 +277,10 @@ console.log(JSON.stringify({
} }
}));`; }));`;
const scriptPath = join(rig.testDir!, 'before_model_hook.cjs'); const scriptPath = rig.createHookScript(
writeFileSync(scriptPath, hookScript); 'before_model_hook.cjs',
hookScript,
);
rig.configure({ rig.configure({
settings: { settings: {
@@ -296,7 +293,7 @@ console.log(JSON.stringify({
hooks: [ hooks: [
{ {
type: 'command', type: 'command',
command: `node "${scriptPath.replace(/\\/g, '/')}"`, command: `node "${scriptPath}"`,
timeout: 5000, timeout: 5000,
}, },
], ],
@@ -341,8 +338,10 @@ console.log(JSON.stringify({
decision: "deny", decision: "deny",
reason: "Model execution blocked by security policy" reason: "Model execution blocked by security policy"
}));`; }));`;
const scriptPath = join(rig.testDir!, 'before_model_deny_hook.cjs'); const scriptPath = rig.createHookScript(
writeFileSync(scriptPath, hookScript); 'before_model_deny_hook.cjs',
hookScript,
);
rig.configure({ rig.configure({
settings: { settings: {
@@ -355,7 +354,7 @@ console.log(JSON.stringify({
hooks: [ hooks: [
{ {
type: 'command', type: 'command',
command: `node "${scriptPath.replace(/\\/g, '/')}"`, command: `node "${scriptPath}"`,
timeout: 5000, timeout: 5000,
}, },
], ],
@@ -383,8 +382,10 @@ console.log(JSON.stringify({
decision: "block", decision: "block",
reason: "Model execution blocked by security policy" reason: "Model execution blocked by security policy"
}));`; }));`;
const scriptPath = join(rig.testDir!, 'before_model_block_hook.cjs'); const scriptPath = rig.createHookScript(
writeFileSync(scriptPath, hookScript); 'before_model_block_hook.cjs',
hookScript,
);
rig.configure({ rig.configure({
settings: { settings: {
@@ -397,7 +398,7 @@ console.log(JSON.stringify({
hooks: [ hooks: [
{ {
type: 'command', type: 'command',
command: `node "${scriptPath.replace(/\\/g, '/')}"`, command: `node "${scriptPath}"`,
timeout: 5000, timeout: 5000,
}, },
], ],
@@ -450,8 +451,10 @@ console.log(JSON.stringify({
} }
}));`; }));`;
const scriptPath = join(rig.testDir!, 'after_model_hook.cjs'); const scriptPath = rig.createHookScript(
writeFileSync(scriptPath, hookScript); 'after_model_hook.cjs',
hookScript,
);
rig.configure({ rig.configure({
settings: { settings: {
@@ -464,7 +467,7 @@ console.log(JSON.stringify({
hooks: [ hooks: [
{ {
type: 'command', type: 'command',
command: `node "${scriptPath.replace(/\\/g, '/')}"`, command: `node "${scriptPath}"`,
timeout: 5000, timeout: 5000,
}, },
], ],
@@ -508,8 +511,10 @@ console.log(JSON.stringify({
} }
} }
}));`; }));`;
const scriptPath = join(rig.testDir!, 'before_tool_selection_hook.cjs'); const scriptPath = rig.createHookScript(
writeFileSync(scriptPath, hookScript); 'before_tool_selection_hook.cjs',
hookScript,
);
rig.configure({ rig.configure({
settings: { settings: {
@@ -523,7 +528,7 @@ console.log(JSON.stringify({
hooks: [ hooks: [
{ {
type: 'command', type: 'command',
command: `node "${scriptPath.replace(/\\/g, '/')}"`, command: `node "${scriptPath}"`,
timeout: 5000, timeout: 5000,
}, },
], ],
@@ -577,8 +582,10 @@ console.log(JSON.stringify({
} }
}));`; }));`;
const scriptPath = join(rig.testDir!, 'before_agent_hook.cjs'); const scriptPath = rig.createHookScript(
writeFileSync(scriptPath, hookScript); 'before_agent_hook.cjs',
hookScript,
);
rig.configure({ rig.configure({
settings: { settings: {
@@ -591,7 +598,7 @@ console.log(JSON.stringify({
hooks: [ hooks: [
{ {
type: 'command', type: 'command',
command: `node "${scriptPath.replace(/\\/g, '/')}"`, command: `node "${scriptPath}"`,
timeout: 5000, timeout: 5000,
}, },
], ],
@@ -616,12 +623,11 @@ console.log(JSON.stringify({
it('should handle notification hooks for tool permissions', async () => { it('should handle notification hooks for tool permissions', async () => {
rig.setup('should handle notification hooks for tool permissions'); rig.setup('should handle notification hooks for tool permissions');
// Create script for hook (works on both Unix and Windows) // Create script for hook (works on both Unix and Windows)
const scriptPath = join(rig.testDir!, 'notification_hook.cjs'); const scriptPath = rig.createHookScript(
writeFileSync( 'notification_hook.cjs',
scriptPath,
"console.log(JSON.stringify({suppressOutput: false, systemMessage: 'Permission request logged by security hook'}))", "console.log(JSON.stringify({suppressOutput: false, systemMessage: 'Permission request logged by security hook'}))",
); );
const hookCommand = `node "${scriptPath.replace(/\\/g, '/')}"`; const hookCommand = `node "${scriptPath}"`;
rig.configure({ rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
@@ -717,19 +723,17 @@ console.log(JSON.stringify({
it('should execute hooks sequentially when configured', async () => { it('should execute hooks sequentially when configured', async () => {
rig.setup('should execute hooks sequentially when configured'); rig.setup('should execute hooks sequentially when configured');
// Create script for hooks (works on both Unix and Windows) // Create script for hooks (works on both Unix and Windows)
const script1Path = join(rig.testDir!, 'hook1.cjs'); const script1Path = rig.createHookScript(
writeFileSync( 'hook1.cjs',
script1Path,
"console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'Step 1: Initial validation passed.'}}))", "console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'Step 1: Initial validation passed.'}}))",
); );
const script2Path = join(rig.testDir!, 'hook2.cjs'); const script2Path = rig.createHookScript(
writeFileSync( 'hook2.cjs',
script2Path,
"console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'Step 2: Security check completed.'}}))", "console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'Step 2: Security check completed.'}}))",
); );
const hook1Command = `node "${script1Path.replace(/\\/g, '/')}"`; const hook1Command = `node "${script1Path}"`;
const hook2Command = `node "${script2Path.replace(/\\/g, '/')}"`; const hook2Command = `node "${script2Path}"`;
rig.configure({ rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
@@ -815,8 +819,10 @@ try {
console.log(JSON.stringify({decision: "block", reason: "Invalid JSON"})); console.log(JSON.stringify({decision: "block", reason: "Invalid JSON"}));
}`; }`;
const scriptPath = join(rig.testDir!, 'input_validation_hook.cjs'); const scriptPath = rig.createHookScript(
writeFileSync(scriptPath, hookScript); 'input_validation_hook.cjs',
hookScript,
);
rig.configure({ rig.configure({
settings: { settings: {
@@ -829,7 +835,7 @@ try {
hooks: [ hooks: [
{ {
type: 'command', type: 'command',
command: `node "${scriptPath.replace(/\\/g, '/')}"`, command: `node "${scriptPath}"`,
timeout: 5000, timeout: 5000,
}, },
], ],
@@ -860,9 +866,8 @@ try {
rig.setup( rig.setup(
'should treat mixed stdout (text + JSON) as system message and allow execution when exit code is 0', 'should treat mixed stdout (text + JSON) as system message and allow execution when exit code is 0',
); );
const scriptPath = join(rig.testDir!, 'mixed_stdout.cjs'); const scriptPath = rig.createHookScript(
writeFileSync( 'mixed_stdout.cjs',
scriptPath,
"console.log('Pollution'); console.log(JSON.stringify({decision: 'deny', reason: 'Should be ignored'}))", "console.log('Pollution'); console.log(JSON.stringify({decision: 'deny', reason: 'Should be ignored'}))",
); );
@@ -912,25 +917,22 @@ try {
it('should handle hooks for all major event types', async () => { it('should handle hooks for all major event types', async () => {
rig.setup('should handle hooks for all major event types'); rig.setup('should handle hooks for all major event types');
// Create scripts for hooks (works on both Unix and Windows) // Create scripts for hooks (works on both Unix and Windows)
const beforeToolScript = join(rig.testDir!, 'before_tool_all.cjs'); const beforeToolScript = rig.createHookScript(
writeFileSync( 'before_tool_all.cjs',
beforeToolScript,
"console.log(JSON.stringify({decision: 'allow', systemMessage: 'BeforeTool: File operation logged'}))", "console.log(JSON.stringify({decision: 'allow', systemMessage: 'BeforeTool: File operation logged'}))",
); );
const afterToolScript = join(rig.testDir!, 'after_tool_all.cjs'); const afterToolScript = rig.createHookScript(
writeFileSync( 'after_tool_all.cjs',
afterToolScript,
"console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'AfterTool', additionalContext: 'AfterTool: Operation completed successfully'}}))", "console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'AfterTool', additionalContext: 'AfterTool: Operation completed successfully'}}))",
); );
const beforeAgentScript = join(rig.testDir!, 'before_agent_all.cjs'); const beforeAgentScript = rig.createHookScript(
writeFileSync( 'before_agent_all.cjs',
beforeAgentScript,
"console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'BeforeAgent: User request processed'}}))", "console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'BeforeAgent: User request processed'}}))",
); );
const beforeToolCommand = `node "${beforeToolScript.replace(/\\/g, '/')}"`; const beforeToolCommand = `node "${beforeToolScript}"`;
const afterToolCommand = `node "${afterToolScript.replace(/\\/g, '/')}"`; const afterToolCommand = `node "${afterToolScript}"`;
const beforeAgentCommand = `node "${beforeAgentScript.replace(/\\/g, '/')}"`; const beforeAgentCommand = `node "${beforeAgentScript}"`;
rig.configure({ rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
@@ -1038,16 +1040,17 @@ try {
describe('Hook Error Handling', () => { describe('Hook Error Handling', () => {
it('should handle hook failures gracefully', async () => { it('should handle hook failures gracefully', async () => {
rig.setup('should handle hook failures gracefully'); rig.setup('should handle hook failures gracefully');
const failingScript = join(rig.testDir!, 'failing_hook.cjs'); const failingScript = rig.createHookScript(
writeFileSync(failingScript, 'process.exit(1)'); 'failing_hook.cjs',
const workingScript = join(rig.testDir!, 'working_hook.cjs'); 'process.exit(1)',
writeFileSync( );
workingScript, const workingScript = rig.createHookScript(
'working_hook.cjs',
"console.log(JSON.stringify({decision: 'allow', reason: 'Working hook succeeded'}))", "console.log(JSON.stringify({decision: 'allow', reason: 'Working hook succeeded'}))",
); );
const failingCommand = `node "${failingScript.replace(/\\/g, '/')}"`; const failingCommand = `node "${failingScript}"`;
const workingCommand = `node "${workingScript.replace(/\\/g, '/')}"`; const workingCommand = `node "${workingScript}"`;
rig.configure({ rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
@@ -1100,12 +1103,11 @@ try {
describe('Hook Telemetry and Observability', () => { describe('Hook Telemetry and Observability', () => {
it('should generate telemetry events for hook executions', async () => { it('should generate telemetry events for hook executions', async () => {
rig.setup('should generate telemetry events for hook executions'); rig.setup('should generate telemetry events for hook executions');
const scriptPath = join(rig.testDir!, 'telemetry_hook.cjs'); const scriptPath = rig.createHookScript(
writeFileSync( 'telemetry_hook.cjs',
scriptPath,
"console.log(JSON.stringify({decision: 'allow', reason: 'Telemetry test hook'}))", "console.log(JSON.stringify({decision: 'allow', reason: 'Telemetry test hook'}))",
); );
const hookCommand = `node "${scriptPath.replace(/\\/g, '/')}"`; const hookCommand = `node "${scriptPath}"`;
rig.configure({ rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
@@ -1147,12 +1149,11 @@ try {
describe('Session Lifecycle Hooks', () => { describe('Session Lifecycle Hooks', () => {
it('should fire SessionStart hook on app startup', async () => { it('should fire SessionStart hook on app startup', async () => {
rig.setup('should fire SessionStart hook on app startup'); rig.setup('should fire SessionStart hook on app startup');
const scriptPath = join(rig.testDir!, 'session_start.cjs'); const scriptPath = rig.createHookScript(
writeFileSync( 'session_start.cjs',
scriptPath,
"console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session starting on startup'}))", "console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session starting on startup'}))",
); );
const sessionStartCommand = `node "${scriptPath.replace(/\\/g, '/')}"`; const sessionStartCommand = `node "${scriptPath}"`;
rig.configure({ rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
@@ -1229,8 +1230,10 @@ console.log(JSON.stringify({
), ),
}); });
const scriptPath = join(rig.testDir!, 'session_start_context_hook.cjs'); const scriptPath = rig.createHookScript(
writeFileSync(scriptPath, hookScript); 'session_start_context_hook.cjs',
hookScript,
);
rig.configure({ rig.configure({
settings: { settings: {
@@ -1244,7 +1247,7 @@ console.log(JSON.stringify({
hooks: [ hooks: [
{ {
type: 'command', type: 'command',
command: `node "${scriptPath.replace(/\\/g, '/')}"`, command: `node "${scriptPath}"`,
timeout: 5000, timeout: 5000,
}, },
], ],
@@ -1309,11 +1312,10 @@ console.log(JSON.stringify({
), ),
}); });
const scriptPath = join( const scriptPath = rig.createHookScript(
rig.testDir!,
'session_start_interactive_hook.cjs', 'session_start_interactive_hook.cjs',
hookScript,
); );
writeFileSync(scriptPath, hookScript);
rig.configure({ rig.configure({
settings: { settings: {
@@ -1327,7 +1329,7 @@ console.log(JSON.stringify({
hooks: [ hooks: [
{ {
type: 'command', type: 'command',
command: `node "${scriptPath.replace(/\\/g, '/')}"`, command: `node "${scriptPath}"`,
timeout: 5000, timeout: 5000,
}, },
], ],
@@ -1376,19 +1378,17 @@ console.log(JSON.stringify({
'should fire SessionEnd and SessionStart hooks on /clear command', 'should fire SessionEnd and SessionStart hooks on /clear command',
); );
// Create script for hooks (works on both Unix and Windows) // Create script for hooks (works on both Unix and Windows)
const endScriptPath = join(rig.testDir!, 'session_end_clear.cjs'); const endScriptPath = rig.createHookScript(
writeFileSync( 'session_end_clear.cjs',
endScriptPath,
"console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session ending due to clear'}))", "console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session ending due to clear'}))",
); );
const startScriptPath = join(rig.testDir!, 'session_start_clear.cjs'); const startScriptPath = rig.createHookScript(
writeFileSync( 'session_start_clear.cjs',
startScriptPath,
"console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session starting after clear'}))", "console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session starting after clear'}))",
); );
const sessionEndCommand = `node "${endScriptPath.replace(/\\/g, '/')}"`; const sessionEndCommand = `node "${endScriptPath}"`;
const sessionStartCommand = `node "${startScriptPath.replace(/\\/g, '/')}"`; const sessionStartCommand = `node "${startScriptPath}"`;
rig.configure({ rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
@@ -1560,12 +1560,11 @@ console.log(JSON.stringify({
describe('Compression Hooks', () => { describe('Compression Hooks', () => {
it('should fire PreCompress hook on automatic compression', async () => { it('should fire PreCompress hook on automatic compression', async () => {
rig.setup('should fire PreCompress hook on automatic compression'); rig.setup('should fire PreCompress hook on automatic compression');
const scriptPath = join(rig.testDir!, 'pre_compress.cjs'); const scriptPath = rig.createHookScript(
writeFileSync( 'pre_compress.cjs',
scriptPath,
"console.log(JSON.stringify({decision: 'allow', systemMessage: 'PreCompress hook executed for automatic compression'}))", "console.log(JSON.stringify({decision: 'allow', systemMessage: 'PreCompress hook executed for automatic compression'}))",
); );
const preCompressCommand = `node "${scriptPath.replace(/\\/g, '/')}"`; const preCompressCommand = `node "${scriptPath}"`;
rig.configure({ rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
@@ -1634,12 +1633,11 @@ console.log(JSON.stringify({
rig.setup( rig.setup(
'should fire SessionEnd hook on graceful exit in non-interactive mode', 'should fire SessionEnd hook on graceful exit in non-interactive mode',
); );
const scriptPath = join(rig.testDir!, 'session_end_exit.cjs'); const scriptPath = rig.createHookScript(
writeFileSync( 'session_end_exit.cjs',
scriptPath,
"console.log(JSON.stringify({decision: 'allow', systemMessage: 'SessionEnd hook executed on exit'}))", "console.log(JSON.stringify({decision: 'allow', systemMessage: 'SessionEnd hook executed on exit'}))",
); );
const sessionEndCommand = `node "${scriptPath.replace(/\\/g, '/')}"`; const sessionEndCommand = `node "${scriptPath}"`;
rig.configure({ rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
@@ -1740,18 +1738,15 @@ console.log(JSON.stringify({decision: "allow", systemMessage: "Enabled hook exec
const disabledHookScript = `const fs = require('fs'); const disabledHookScript = `const fs = require('fs');
console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook should not execute", reason: "This hook should be disabled"}));`; console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook should not execute", reason: "This hook should be disabled"}));`;
const enabledPath = join(rig.testDir!, 'enabled_hook.cjs').replace( const enabledPath = rig.createHookScript(
/\\/g, 'enabled_hook.cjs',
'/', enabledHookScript,
); );
const disabledPath = join(rig.testDir!, 'disabled_hook.cjs').replace( const disabledPath = rig.createHookScript(
/\\/g, 'disabled_hook.cjs',
'/', disabledHookScript,
); );
writeFileSync(enabledPath, enabledHookScript);
writeFileSync(disabledPath, disabledHookScript);
rig.configure({ rig.configure({
settings: { settings: {
hooksConfig: { hooksConfig: {
@@ -1824,18 +1819,15 @@ console.log(JSON.stringify({decision: "allow", systemMessage: "Active hook execu
const disabledHookScript = `const fs = require('fs'); const disabledHookScript = `const fs = require('fs');
console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook should not execute", reason: "This hook is disabled"}));`; console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook should not execute", reason: "This hook is disabled"}));`;
const activePath = join(rig.testDir!, 'active_hook.cjs').replace( const activePath = rig.createHookScript(
/\\/g, 'active_hook.cjs',
'/', activeHookScript,
); );
const disabledPath = join(rig.testDir!, 'disabled_hook.cjs').replace( const disabledPath = rig.createHookScript(
/\\/g, 'disabled_hook.cjs',
'/', disabledHookScript,
); );
writeFileSync(activePath, activeHookScript);
writeFileSync(disabledPath, disabledHookScript);
rig.configure({ rig.configure({
settings: { settings: {
hooksConfig: { hooksConfig: {
@@ -1930,13 +1922,10 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho
hookOutput, hookOutput,
)}));`; )}));`;
const scriptPath = join(rig.testDir!, 'input_override_hook.js'); const commandPath = rig.createHookScript(
writeFileSync(scriptPath, hookScript); 'input_override_hook.cjs',
hookScript,
// Ensure path is properly escaped for command line usage on all platforms );
// On Windows, backslashes in the command string need to be handled carefully
// Using forward slashes works well with Node.js on all platforms
const commandPath = scriptPath.replace(/\\/g, '/');
// 2. Full setup with settings and fake responses // 2. Full setup with settings and fake responses
rig.configure({ rig.configure({
@@ -1990,9 +1979,9 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho
expect(hookTelemetryFound).toBeTruthy(); expect(hookTelemetryFound).toBeTruthy();
const hookLogs = rig.readHookLogs(); const hookLogs = rig.readHookLogs();
expect(hookLogs.length).toBe(1); expect(hookLogs.length).toBeGreaterThanOrEqual(1);
expect(hookLogs[0].hookCall.hook_name).toContain( expect(hookLogs[0].hookCall.hook_name).toContain(
'input_override_hook.js', 'input_override_hook.cjs',
); );
// 4. Verify that the agent didn't try to work-around the hook input change // 4. Verify that the agent didn't try to work-around the hook input change
@@ -2021,9 +2010,10 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho
)}));`; )}));`;
rig.setup('should stop agent execution via BeforeTool hook'); rig.setup('should stop agent execution via BeforeTool hook');
const scriptPath = join(rig.testDir!, 'before_tool_stop_hook.js'); const commandPath = rig.createHookScript(
writeFileSync(scriptPath, hookScript); 'before_tool_stop_hook.cjs',
const commandPath = scriptPath.replace(/\\/g, '/'); hookScript,
);
rig.configure({ rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
+13
View File
@@ -387,6 +387,19 @@ export class TestRig {
this._createSettingsFile(options.settings); this._createSettingsFile(options.settings);
} }
/**
* Creates a hook script file and returns a normalized path suitable for cross-platform execution.
*/
createHookScript(fileName: string, content: string): string {
if (!this.testDir) {
throw new Error('TestRig must be setup before calling createHookScript');
}
const scriptPath = join(this.testDir, fileName);
writeFileSync(scriptPath, content);
// Return a path normalized for use in shell commands across platforms.
return scriptPath.replace(/\\/g, '/');
}
private _createSettingsFile(overrideSettings?: Record<string, unknown>) { private _createSettingsFile(overrideSettings?: Record<string, unknown>) {
const projectGeminiDir = join(this.testDir!, GEMINI_DIR); const projectGeminiDir = join(this.testDir!, GEMINI_DIR);
mkdirSync(projectGeminiDir, { recursive: true }); mkdirSync(projectGeminiDir, { recursive: true });