chore: fix session browser test and skip hook system tests (#14099)

This commit is contained in:
Jack Wotherspoon
2025-11-28 16:07:54 -05:00
committed by GitHub
parent 7a4280a482
commit 576fda18eb
2 changed files with 223 additions and 211 deletions

View File

@@ -260,15 +260,17 @@ echo '{
}); });
describe('AfterModel Hooks - LLM Response Modification', () => { describe('AfterModel Hooks - LLM Response Modification', () => {
it('should modify LLM responses with AfterModel hooks', async () => { it.skipIf(process.platform === 'win32')(
await rig.setup('should modify LLM responses with AfterModel hooks', { 'should modify LLM responses with AfterModel hooks',
fakeResponsesPath: join( async () => {
import.meta.dirname, await rig.setup('should modify LLM responses with AfterModel hooks', {
'hooks-system.after-model.responses', fakeResponsesPath: join(
), import.meta.dirname,
}); 'hooks-system.after-model.responses',
// Create a hook script that modifies the LLM response ),
const hookScript = `#!/bin/bash });
// Create a hook script that modifies the LLM response
const hookScript = `#!/bin/bash
echo '{ echo '{
"hookSpecificOutput": { "hookSpecificOutput": {
"hookEventName": "AfterModel", "hookEventName": "AfterModel",
@@ -288,44 +290,45 @@ echo '{
} }
}'`; }'`;
const scriptPath = join(rig.testDir!, 'after_model_hook.sh'); const scriptPath = join(rig.testDir!, 'after_model_hook.sh');
writeFileSync(scriptPath, hookScript); writeFileSync(scriptPath, hookScript);
const { execSync } = await import('node:child_process'); const { execSync } = await import('node:child_process');
execSync(`chmod +x "${scriptPath}"`); execSync(`chmod +x "${scriptPath}"`);
await rig.setup('should modify LLM responses with AfterModel hooks', { await rig.setup('should modify LLM responses with AfterModel hooks', {
settings: { settings: {
tools: { tools: {
enableHooks: true, enableHooks: true,
},
hooks: {
AfterModel: [
{
hooks: [
{
type: 'command',
command: scriptPath,
timeout: 5000,
},
],
},
],
},
}, },
hooks: { });
AfterModel: [
{
hooks: [
{
type: 'command',
command: scriptPath,
timeout: 5000,
},
],
},
],
},
},
});
const prompt = 'What is 2 + 2?'; const prompt = 'What is 2 + 2?';
const result = await rig.run(prompt); const result = await rig.run(prompt);
// The hook should have replaced the model response // The hook should have replaced the model response
expect(result).toContain( expect(result).toContain(
'[FILTERED] Response has been filtered for security compliance', '[FILTERED] Response has been filtered for security compliance',
); );
// Should generate hook telemetry // Should generate hook telemetry
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call'); const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy(); expect(hookTelemetryFound).toBeTruthy();
}); },
);
}); });
describe('BeforeToolSelection Hooks - Tool Configuration', () => { describe('BeforeToolSelection Hooks - Tool Configuration', () => {
@@ -523,10 +526,12 @@ echo '{
describe('Sequential Hook Execution', () => { describe('Sequential Hook Execution', () => {
// Note: This test checks telemetry for hook context in API requests, // Note: This test checks telemetry for hook context in API requests,
// which behaves differently with mocked responses. Keeping real LLM calls. // which behaves differently with mocked responses. Keeping real LLM calls.
it('should execute hooks sequentially when configured', async () => { it.skipIf(process.platform === 'win32')(
await rig.setup('should execute hooks sequentially when configured'); 'should execute hooks sequentially when configured',
// Create two hooks that modify the input sequentially async () => {
const hook1Script = `#!/bin/bash await rig.setup('should execute hooks sequentially when configured');
// Create two hooks that modify the input sequentially
const hook1Script = `#!/bin/bash
echo '{ echo '{
"decision": "allow", "decision": "allow",
"hookSpecificOutput": { "hookSpecificOutput": {
@@ -535,7 +540,7 @@ echo '{
} }
}'`; }'`;
const hook2Script = `#!/bin/bash const hook2Script = `#!/bin/bash
echo '{ echo '{
"decision": "allow", "decision": "allow",
"hookSpecificOutput": { "hookSpecificOutput": {
@@ -544,74 +549,75 @@ echo '{
} }
}'`; }'`;
const script1Path = join(rig.testDir!, 'sequential_hook1.sh'); const script1Path = join(rig.testDir!, 'sequential_hook1.sh');
const script2Path = join(rig.testDir!, 'sequential_hook2.sh'); const script2Path = join(rig.testDir!, 'sequential_hook2.sh');
writeFileSync(script1Path, hook1Script); writeFileSync(script1Path, hook1Script);
writeFileSync(script2Path, hook2Script); writeFileSync(script2Path, hook2Script);
const { execSync } = await import('node:child_process'); const { execSync } = await import('node:child_process');
execSync(`chmod +x "${script1Path}"`); execSync(`chmod +x "${script1Path}"`);
execSync(`chmod +x "${script2Path}"`); execSync(`chmod +x "${script2Path}"`);
await rig.setup('should execute hooks sequentially when configured', { await rig.setup('should execute hooks sequentially when configured', {
settings: { settings: {
tools: { tools: {
enableHooks: true, enableHooks: true,
},
hooks: {
BeforeAgent: [
{
sequential: true,
hooks: [
{
type: 'command',
command: script1Path,
timeout: 5000,
},
{
type: 'command',
command: script2Path,
timeout: 5000,
},
],
},
],
},
}, },
hooks: { });
BeforeAgent: [
{
sequential: true,
hooks: [
{
type: 'command',
command: script1Path,
timeout: 5000,
},
{
type: 'command',
command: script2Path,
timeout: 5000,
},
],
},
],
},
},
});
const prompt = 'Hello, please help me with a task'; const prompt = 'Hello, please help me with a task';
await rig.run(prompt); await rig.run(prompt);
// Should generate hook telemetry // Should generate hook telemetry
let hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call'); let hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy(); expect(hookTelemetryFound).toBeTruthy();
hookTelemetryFound = await rig.waitForTelemetryEvent('api_request'); hookTelemetryFound = await rig.waitForTelemetryEvent('api_request');
const apiRequests = rig.readAllApiRequest(); const apiRequests = rig.readAllApiRequest();
const apiRequestsTexts = apiRequests const apiRequestsTexts = apiRequests
?.filter( ?.filter(
(request) => (request) =>
'attributes' in request && 'attributes' in request &&
typeof request['attributes'] === 'object' && typeof request['attributes'] === 'object' &&
request['attributes'] !== null && request['attributes'] !== null &&
'request_text' in request['attributes'] && 'request_text' in request['attributes'] &&
typeof request['attributes']['request_text'] === 'string', typeof request['attributes']['request_text'] === 'string',
) )
.map((request) => request['attributes']['request_text']); .map((request) => request['attributes']['request_text']);
expect(apiRequestsTexts).toBeDefined(); expect(apiRequestsTexts).toBeDefined();
let hasBeforeAgentHookContext = false; let hasBeforeAgentHookContext = false;
let hasAfterToolHookContext = false; let hasAfterToolHookContext = false;
for (const requestText of apiRequestsTexts) { for (const requestText of apiRequestsTexts) {
if (requestText.includes('Step 1: Initial validation passed')) { if (requestText.includes('Step 1: Initial validation passed')) {
hasBeforeAgentHookContext = true; hasBeforeAgentHookContext = true;
}
if (requestText.includes('Step 2: Security check completed')) {
hasAfterToolHookContext = true;
}
} }
if (requestText.includes('Step 2: Security check completed')) { expect(hasBeforeAgentHookContext).toBeTruthy();
hasAfterToolHookContext = true; expect(hasAfterToolHookContext).toBeTruthy();
} },
} );
expect(hasBeforeAgentHookContext).toBeTruthy();
expect(hasAfterToolHookContext).toBeTruthy();
});
}); });
describe('Hook Input/Output Validation', () => { describe('Hook Input/Output Validation', () => {
@@ -682,124 +688,127 @@ fi`;
describe('Multiple Event Types', () => { describe('Multiple Event Types', () => {
// Note: This test checks telemetry for hook context in API requests, // Note: This test checks telemetry for hook context in API requests,
// which behaves differently with mocked responses. Keeping real LLM calls. // which behaves differently with mocked responses. Keeping real LLM calls.
it('should handle hooks for all major event types', async () => { it.skipIf(process.platform === 'win32')(
await rig.setup('should handle hooks for all major event types'); 'should handle hooks for all major event types',
// Create hook scripts for different events async () => {
const beforeToolScript = `#!/bin/bash await rig.setup('should handle hooks for all major event types');
// Create hook scripts for different events
const beforeToolScript = `#!/bin/bash
echo '{"decision": "allow", "systemMessage": "BeforeTool: File operation logged"}'`; echo '{"decision": "allow", "systemMessage": "BeforeTool: File operation logged"}'`;
const afterToolScript = `#!/bin/bash const afterToolScript = `#!/bin/bash
echo '{"hookSpecificOutput": {"hookEventName": "AfterTool", "additionalContext": "AfterTool: Operation completed successfully"}}'`; echo '{"hookSpecificOutput": {"hookEventName": "AfterTool", "additionalContext": "AfterTool: Operation completed successfully"}}'`;
const beforeAgentScript = `#!/bin/bash const beforeAgentScript = `#!/bin/bash
echo '{"decision": "allow", "hookSpecificOutput": {"hookEventName": "BeforeAgent", "additionalContext": "BeforeAgent: User request processed"}}'`; echo '{"decision": "allow", "hookSpecificOutput": {"hookEventName": "BeforeAgent", "additionalContext": "BeforeAgent: User request processed"}}'`;
const beforeToolPath = join(rig.testDir!, 'before_tool.sh'); const beforeToolPath = join(rig.testDir!, 'before_tool.sh');
const afterToolPath = join(rig.testDir!, 'after_tool.sh'); const afterToolPath = join(rig.testDir!, 'after_tool.sh');
const beforeAgentPath = join(rig.testDir!, 'before_agent.sh'); const beforeAgentPath = join(rig.testDir!, 'before_agent.sh');
writeFileSync(beforeToolPath, beforeToolScript); writeFileSync(beforeToolPath, beforeToolScript);
writeFileSync(afterToolPath, afterToolScript); writeFileSync(afterToolPath, afterToolScript);
writeFileSync(beforeAgentPath, beforeAgentScript); writeFileSync(beforeAgentPath, beforeAgentScript);
const { execSync } = await import('node:child_process'); const { execSync } = await import('node:child_process');
execSync(`chmod +x "${beforeToolPath}"`); execSync(`chmod +x "${beforeToolPath}"`);
execSync(`chmod +x "${afterToolPath}"`); execSync(`chmod +x "${afterToolPath}"`);
execSync(`chmod +x "${beforeAgentPath}"`); execSync(`chmod +x "${beforeAgentPath}"`);
await rig.setup('should handle hooks for all major event types', { await rig.setup('should handle hooks for all major event types', {
settings: { settings: {
tools: { tools: {
enableHooks: true, enableHooks: true,
},
hooks: {
BeforeAgent: [
{
hooks: [
{
type: 'command',
command: beforeAgentPath,
timeout: 5000,
},
],
},
],
BeforeTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
command: beforeToolPath,
timeout: 5000,
},
],
},
],
AfterTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
command: afterToolPath,
timeout: 5000,
},
],
},
],
},
}, },
hooks: { });
BeforeAgent: [
{
hooks: [
{
type: 'command',
command: beforeAgentPath,
timeout: 5000,
},
],
},
],
BeforeTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
command: beforeToolPath,
timeout: 5000,
},
],
},
],
AfterTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
command: afterToolPath,
timeout: 5000,
},
],
},
],
},
},
});
const prompt = const prompt =
'Create a file called multi-event-test.txt with content ' + 'Create a file called multi-event-test.txt with content ' +
'"testing multiple events", and then please reply with ' + '"testing multiple events", and then please reply with ' +
'everything I say just after this:"'; 'everything I say just after this:"';
const result = await rig.run(prompt); const result = await rig.run(prompt);
// Should execute write_file tool // Should execute write_file tool
const foundWriteFile = await rig.waitForToolCall('write_file'); const foundWriteFile = await rig.waitForToolCall('write_file');
expect(foundWriteFile).toBeTruthy(); expect(foundWriteFile).toBeTruthy();
// File should be created // File should be created
const fileContent = rig.readFile('multi-event-test.txt'); const fileContent = rig.readFile('multi-event-test.txt');
expect(fileContent).toContain('testing multiple events'); expect(fileContent).toContain('testing multiple events');
// Result should contain context from all hooks // Result should contain context from all hooks
expect(result).toContain('BeforeTool: File operation logged'); expect(result).toContain('BeforeTool: File operation logged');
// Should generate hook telemetry // Should generate hook telemetry
let hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call'); let hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy(); expect(hookTelemetryFound).toBeTruthy();
hookTelemetryFound = await rig.waitForTelemetryEvent('api_request'); hookTelemetryFound = await rig.waitForTelemetryEvent('api_request');
const apiRequests = rig.readAllApiRequest(); const apiRequests = rig.readAllApiRequest();
const apiRequestsTexts = apiRequests const apiRequestsTexts = apiRequests
?.filter( ?.filter(
(request) => (request) =>
'attributes' in request && 'attributes' in request &&
typeof request['attributes'] === 'object' && typeof request['attributes'] === 'object' &&
request['attributes'] !== null && request['attributes'] !== null &&
'request_text' in request['attributes'] && 'request_text' in request['attributes'] &&
typeof request['attributes']['request_text'] === 'string', typeof request['attributes']['request_text'] === 'string',
) )
.map((request) => request['attributes']['request_text']); .map((request) => request['attributes']['request_text']);
expect(apiRequestsTexts).toBeDefined(); expect(apiRequestsTexts).toBeDefined();
let hasBeforeAgentHookContext = false; let hasBeforeAgentHookContext = false;
let hasAfterToolHookContext = false; let hasAfterToolHookContext = false;
for (const requestText of apiRequestsTexts) { for (const requestText of apiRequestsTexts) {
if (requestText.includes('BeforeAgent: User request processed')) { if (requestText.includes('BeforeAgent: User request processed')) {
hasBeforeAgentHookContext = true; hasBeforeAgentHookContext = true;
}
if (
requestText.includes('AfterTool: Operation completed successfully')
) {
hasAfterToolHookContext = true;
}
} }
if ( expect(hasBeforeAgentHookContext).toBeTruthy();
requestText.includes('AfterTool: Operation completed successfully') expect(hasAfterToolHookContext).toBeTruthy();
) { },
hasAfterToolHookContext = true; );
}
}
expect(hasBeforeAgentHookContext).toBeTruthy();
expect(hasAfterToolHookContext).toBeTruthy();
});
}); });
describe('Hook Error Handling', () => { describe('Hook Error Handling', () => {

View File

@@ -138,11 +138,14 @@ const createSession = (overrides: Partial<SessionInfo>): SessionInfo => ({
describe('SessionBrowser component', () => { describe('SessionBrowser component', () => {
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2025-11-01T12:00:00Z'));
keypressHandlers.length = 0; keypressHandlers.length = 0;
vi.clearAllMocks(); vi.clearAllMocks();
}); });
afterEach(() => { afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });