Compare commits

...

5 Commits

Author SHA1 Message Date
Abhi 0fab129453 test(integration): remove redundant path normalization
Removes unnecessary .replace(/\\/g, '/') calls in hooks-system.test.ts since
the createHookScript helper now handles path normalization internally.
2026-02-06 13:42:09 -05:00
Abhi e2b96018bd 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.
2026-02-06 13:19:31 -05:00
Abhi e1d8cc78d7 refactor(test): complete rig.setup/configure refactor and path normalization
- Refactor all remaining rig.setup calls to use the new setup/configure pattern.
- Consistently normalize all script paths to forward slashes for Windows compatibility.
- Ensure proper ordering of rig.setup and rig.configure in all tests.
2026-02-06 12:57:33 -05:00
Abhi fb3dfea925 refactor(test): separate directory initialization from configuration in TestRig
- Refactor TestRig.setup to handle only directory creation by default.
- Add TestRig.configure to apply settings and fake responses.
- Update hook integration tests to use the new setup/configure pattern,
  avoiding brittle double setup calls.
2026-02-06 12:39:16 -05:00
Abhi ed85166a41 test(integration): fix windows integration issues for hook tests
Fixes several issues preventing integration tests from passing on Windows:
- Replaces brittle inline 'node -e' commands with script files to avoid shell quoting issues in PowerShell.
- Normalizes paths to forward slashes to prevent escape sequence misinterpretation in generated scripts.
- Ensures rig.setup() is called before accessing rig.testDir to properly initialize test environment.
- Corrects hooks settings structure (enabled moved to hooksConfig) to match schema validation.
2026-02-06 12:15:16 -05:00
3 changed files with 528 additions and 385 deletions
+92 -56
View File
@@ -7,7 +7,6 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';
import { writeFileSync } from 'node:fs';
describe('Hooks Agent Flow', () => {
let rig: TestRig;
@@ -24,7 +23,8 @@ describe('Hooks Agent Flow', () => {
describe('BeforeAgent Hooks', () => {
it('should inject additional context via BeforeAgent hook', async () => {
await rig.setup('should inject additional context via BeforeAgent hook', {
await rig.setup('should inject additional context via BeforeAgent hook');
await rig.configure({
fakeResponsesPath: join(
import.meta.dirname,
'hooks-agent-flow.responses',
@@ -48,10 +48,12 @@ describe('Hooks Agent Flow', () => {
console.error('DEBUG: BeforeAgent hook executed');
`;
const scriptPath = join(rig.testDir!, 'before_agent_context.cjs');
writeFileSync(scriptPath, hookScript);
const scriptPath = rig.createHookScript(
'before_agent_context.cjs',
hookScript,
);
await rig.setup('should inject additional context via BeforeAgent hook', {
await rig.configure({
settings: {
hooksConfig: {
enabled: true,
@@ -94,7 +96,8 @@ describe('Hooks Agent Flow', () => {
describe('AfterAgent Hooks', () => {
it('should receive prompt and response in AfterAgent hook', async () => {
await rig.setup('should receive prompt and response in AfterAgent hook', {
await rig.setup('should receive prompt and response in AfterAgent hook');
await rig.configure({
fakeResponsesPath: join(
import.meta.dirname,
'hooks-agent-flow.responses',
@@ -113,10 +116,12 @@ describe('Hooks Agent Flow', () => {
}
`;
const scriptPath = join(rig.testDir!, 'after_agent_verify.cjs');
writeFileSync(scriptPath, hookScript);
const scriptPath = rig.createHookScript(
'after_agent_verify.cjs',
hookScript,
);
await rig.setup('should receive prompt and response in AfterAgent hook', {
await rig.configure({
settings: {
hooksConfig: {
enabled: true,
@@ -157,15 +162,13 @@ describe('Hooks Agent Flow', () => {
});
it('should process clearContext in AfterAgent hook output', async () => {
await rig.setup('should process clearContext in AfterAgent hook output', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.after-agent.responses',
),
});
await rig.setup('should process clearContext in AfterAgent hook output');
// BeforeModel hook to track message counts across LLM calls
const messageCountFile = join(rig.testDir!, 'message-counts.json');
const messageCountFile = join(
rig.testDir!,
'message-counts.json',
).replace(/\\/g, '/');
const beforeModelScript = `
const fs = require('fs');
const input = JSON.parse(fs.readFileSync(0, 'utf-8'));
@@ -176,16 +179,36 @@ describe('Hooks Agent Flow', () => {
fs.writeFileSync('${messageCountFile}', JSON.stringify(counts));
console.log(JSON.stringify({ decision: 'allow' }));
`;
const beforeModelScriptPath = join(
rig.testDir!,
const beforeModelScriptPath = rig.createHookScript(
'before_model_counter.cjs',
beforeModelScript,
);
writeFileSync(beforeModelScriptPath, beforeModelScript);
await rig.setup('should process clearContext in AfterAgent hook output', {
const afterAgentScript = `
console.log(JSON.stringify({
decision: 'block',
reason: 'Security policy triggered',
hookSpecificOutput: {
hookEventName: 'AfterAgent',
clearContext: true
}
}));
`;
const afterAgentScriptPath = rig.createHookScript(
'after_agent_clear.cjs',
afterAgentScript,
);
await rig.configure({
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.after-agent.responses',
),
settings: {
hooks: {
hooksConfig: {
enabled: true,
},
hooks: {
BeforeModel: [
{
hooks: [
@@ -202,7 +225,7 @@ describe('Hooks Agent Flow', () => {
hooks: [
{
type: 'command',
command: `node -e "console.log(JSON.stringify({decision: 'block', reason: 'Security policy triggered', hookSpecificOutput: {hookEventName: 'AfterAgent', clearContext: true}}))"`,
command: `node "${afterAgentScriptPath}"`,
timeout: 5000,
},
],
@@ -239,42 +262,55 @@ describe('Hooks Agent Flow', () => {
it('should fire BeforeAgent and AfterAgent exactly once per turn despite tool calls', async () => {
await rig.setup(
'should fire BeforeAgent and AfterAgent exactly once per turn despite tool calls',
{
fakeResponsesPath: join(
import.meta.dirname,
'hooks-agent-flow-multistep.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
BeforeAgent: [
{
hooks: [
{
type: 'command',
command: `node -e "console.log('BeforeAgent Fired')"`,
timeout: 5000,
},
],
},
],
AfterAgent: [
{
hooks: [
{
type: 'command',
command: `node -e "console.log('AfterAgent Fired')"`,
timeout: 5000,
},
],
},
],
},
);
const beforeAgentScript = "console.log('BeforeAgent Fired')";
const beforeAgentScriptPath = rig.createHookScript(
'before_agent_loop.cjs',
beforeAgentScript,
);
const afterAgentScript = "console.log('AfterAgent Fired')";
const afterAgentScriptPath = rig.createHookScript(
'after_agent_loop.cjs',
afterAgentScript,
);
await rig.configure({
fakeResponsesPath: join(
import.meta.dirname,
'hooks-agent-flow-multistep.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
BeforeAgent: [
{
hooks: [
{
type: 'command',
command: `node "${beforeAgentScriptPath}"`,
timeout: 5000,
},
],
},
],
AfterAgent: [
{
hooks: [
{
type: 'command',
command: `node "${afterAgentScriptPath}"`,
timeout: 5000,
},
],
},
],
},
},
);
});
await rig.run({ args: 'Do a multi-step task' });
File diff suppressed because it is too large Load Diff
+27
View File
@@ -361,6 +361,20 @@ export class TestRig {
this.homeDir = join(testFileDir, sanitizedName + '-home');
mkdirSync(this.testDir, { recursive: true });
mkdirSync(this.homeDir, { recursive: true });
if (options.settings || options.fakeResponsesPath) {
this.configure(options);
}
}
configure(options: {
settings?: Record<string, unknown>;
fakeResponsesPath?: string;
}) {
if (!this.testDir || !this.homeDir) {
throw new Error('TestRig must be setup before calling configure');
}
if (options.fakeResponsesPath) {
this.fakeResponsesPath = join(this.testDir, 'fake-responses.json');
this.originalFakeResponsesPath = options.fakeResponsesPath;
@@ -373,6 +387,19 @@ export class TestRig {
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>) {
const projectGeminiDir = join(this.testDir!, GEMINI_DIR);
mkdirSync(projectGeminiDir, { recursive: true });