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.
This commit is contained in:
Abhi
2026-02-06 12:39:16 -05:00
parent ed85166a41
commit fb3dfea925
3 changed files with 193 additions and 198 deletions
+39 -43
View File
@@ -163,12 +163,7 @@ describe('Hooks Agent Flow', () => {
}); });
it('should process clearContext in AfterAgent hook output', async () => { it('should process clearContext in AfterAgent hook output', async () => {
await rig.setup('should process clearContext in AfterAgent hook output', { await rig.setup('should process clearContext in AfterAgent hook output');
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.after-agent.responses',
),
});
// BeforeModel hook to track message counts across LLM calls // BeforeModel hook to track message counts across LLM calls
const messageCountFile = join( const messageCountFile = join(
@@ -207,7 +202,11 @@ describe('Hooks Agent Flow', () => {
).replace(/\\/g, '/'); ).replace(/\\/g, '/');
writeFileSync(afterAgentScriptPath, afterAgentScript); writeFileSync(afterAgentScriptPath, afterAgentScript);
await rig.setup('should process clearContext in AfterAgent hook output', { await rig.configure({
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.after-agent.responses',
),
settings: { settings: {
hooksConfig: { hooksConfig: {
enabled: true, enabled: true,
@@ -282,44 +281,41 @@ describe('Hooks Agent Flow', () => {
).replace(/\\/g, '/'); ).replace(/\\/g, '/');
writeFileSync(afterAgentScriptPath, afterAgentScript); writeFileSync(afterAgentScriptPath, afterAgentScript);
await rig.setup( await rig.configure({
'should fire BeforeAgent and AfterAgent exactly once per turn despite tool calls', fakeResponsesPath: join(
{ import.meta.dirname,
fakeResponsesPath: join( 'hooks-agent-flow-multistep.responses',
import.meta.dirname, ),
'hooks-agent-flow-multistep.responses', settings: {
), hooksConfig: {
settings: { enabled: true,
hooksConfig: { },
enabled: true, hooks: {
}, BeforeAgent: [
hooks: { {
BeforeAgent: [ hooks: [
{ {
hooks: [ type: 'command',
{ command: `node "${beforeAgentScriptPath}"`,
type: 'command', timeout: 5000,
command: `node "${beforeAgentScriptPath}"`, },
timeout: 5000, ],
}, },
], ],
}, AfterAgent: [
], {
AfterAgent: [ hooks: [
{ {
hooks: [ type: 'command',
{ command: `node "${afterAgentScriptPath}"`,
type: 'command', timeout: 5000,
command: `node "${afterAgentScriptPath}"`, },
timeout: 5000, ],
}, },
], ],
},
],
},
}, },
}, },
); });
await rig.run({ args: 'Do a multi-step task' }); await rig.run({ args: 'Do a multi-step task' });
+140 -155
View File
@@ -31,35 +31,32 @@ describe('Hooks System Integration', () => {
"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'}))",
); );
rig.setup( rig.configure({
'should block tool execution when hook returns block decision', fakeResponsesPath: join(
{ import.meta.dirname,
fakeResponsesPath: join( 'hooks-system.block-tool.responses',
import.meta.dirname, ),
'hooks-system.block-tool.responses', settings: {
), hooksConfig: {
settings: { enabled: true,
hooksConfig: { },
enabled: true, hooks: {
}, BeforeTool: [
hooks: { {
BeforeTool: [ matcher: 'write_file',
{ sequential: true,
matcher: 'write_file', hooks: [
sequential: true, {
hooks: [ type: 'command',
{ command: `node "${scriptPath}"`,
type: 'command', timeout: 5000,
command: `node "${scriptPath}"`, },
timeout: 5000, ],
}, },
], ],
},
],
},
}, },
}, },
); });
const result = await rig.run({ const result = await rig.run({
args: 'Create a file called test.txt with content "Hello World"', args: 'Create a file called test.txt with content "Hello World"',
@@ -93,35 +90,32 @@ describe('Hooks System Integration', () => {
"process.stderr.write('File writing blocked by security policy'); process.exit(2)", "process.stderr.write('File writing blocked by security policy'); process.exit(2)",
); );
rig.setup( rig.configure({
'should block tool execution and use stderr as reason when hook exits with code 2', fakeResponsesPath: join(
{ import.meta.dirname,
fakeResponsesPath: join( 'hooks-system.block-tool.responses',
import.meta.dirname, ),
'hooks-system.block-tool.responses', settings: {
), hooksConfig: {
settings: { enabled: true,
hooksConfig: { },
enabled: true, hooks: {
}, BeforeTool: [
hooks: { {
BeforeTool: [ matcher: 'write_file',
{ hooks: [
matcher: 'write_file', {
hooks: [ type: 'command',
{ // Exit with code 2 and write reason to stderr
type: 'command', command: `node "${scriptPath}"`,
// Exit with code 2 and write reason to stderr timeout: 5000,
command: `node "${scriptPath}"`, },
timeout: 5000, ],
}, },
], ],
},
],
},
}, },
}, },
); });
const result = await rig.run({ const result = await rig.run({
args: 'Create a file called test.txt with content "Hello World"', args: 'Create a file called test.txt with content "Hello World"',
@@ -158,34 +152,31 @@ describe('Hooks System Integration', () => {
"console.log(JSON.stringify({decision: 'allow', reason: 'File writing approved'}))", "console.log(JSON.stringify({decision: 'allow', reason: 'File writing approved'}))",
); );
rig.setup( rig.configure({
'should allow tool execution when hook returns allow decision', fakeResponsesPath: join(
{ import.meta.dirname,
fakeResponsesPath: join( 'hooks-system.allow-tool.responses',
import.meta.dirname, ),
'hooks-system.allow-tool.responses', settings: {
), hooksConfig: {
settings: { enabled: true,
hooksConfig: { },
enabled: true, hooks: {
}, BeforeTool: [
hooks: { {
BeforeTool: [ matcher: 'write_file',
{ hooks: [
matcher: 'write_file', {
hooks: [ type: 'command',
{ command: `node "${scriptPath}"`,
type: 'command', timeout: 5000,
command: `node "${scriptPath}"`, },
timeout: 5000, ],
}, },
], ],
},
],
},
}, },
}, },
); });
await rig.run({ await rig.run({
args: 'Create a file called approved.txt with content "Approved content"', args: 'Create a file called approved.txt with content "Approved content"',
@@ -214,7 +205,7 @@ describe('Hooks System Integration', () => {
"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}"`; const command = `node "${scriptPath}"`;
rig.setup('should add additional context from AfterTool hooks', { rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
import.meta.dirname, import.meta.dirname,
'hooks-system.after-tool-context.responses', 'hooks-system.after-tool-context.responses',
@@ -634,7 +625,7 @@ console.log(JSON.stringify({
); );
const hookCommand = `node "${scriptPath.replace(/\\/g, '/')}"`; const hookCommand = `node "${scriptPath.replace(/\\/g, '/')}"`;
rig.setup('should handle notification hooks for tool permissions', { rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
import.meta.dirname, import.meta.dirname,
'hooks-system.notification.responses', 'hooks-system.notification.responses',
@@ -742,7 +733,7 @@ console.log(JSON.stringify({
const hook1Command = `node "${script1Path.replace(/\\/g, '/')}"`; const hook1Command = `node "${script1Path.replace(/\\/g, '/')}"`;
const hook2Command = `node "${script2Path.replace(/\\/g, '/')}"`; const hook2Command = `node "${script2Path.replace(/\\/g, '/')}"`;
rig.setup('should execute hooks sequentially when configured', { rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
import.meta.dirname, import.meta.dirname,
'hooks-system.sequential-execution.responses', 'hooks-system.sequential-execution.responses',
@@ -876,36 +867,33 @@ try {
"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'}))",
); );
rig.setup( rig.configure({
'should treat mixed stdout (text + JSON) as system message and allow execution when exit code is 0', fakeResponsesPath: join(
{ import.meta.dirname,
fakeResponsesPath: join( 'hooks-system.allow-tool.responses',
import.meta.dirname, ),
'hooks-system.allow-tool.responses', settings: {
), hooksConfig: {
settings: { enabled: true,
hooksConfig: { },
enabled: true, hooks: {
}, BeforeTool: [
hooks: { {
BeforeTool: [ matcher: 'write_file',
{ hooks: [
matcher: 'write_file', {
hooks: [ type: 'command',
{ // Output plain text then JSON.
type: 'command', // This breaks JSON parsing, so it falls back to 'allow' with the whole stdout as systemMessage.
// Output plain text then JSON. command: `node "${scriptPath}"`,
// This breaks JSON parsing, so it falls back to 'allow' with the whole stdout as systemMessage. timeout: 5000,
command: `node "${scriptPath}"`, },
timeout: 5000, ],
}, },
], ],
},
],
},
}, },
}, },
); });
const result = await rig.run({ const result = await rig.run({
args: 'Create a file called approved.txt with content "Approved content"', args: 'Create a file called approved.txt with content "Approved content"',
@@ -945,7 +933,7 @@ try {
const afterToolCommand = `node "${afterToolScript.replace(/\\/g, '/')}"`; const afterToolCommand = `node "${afterToolScript.replace(/\\/g, '/')}"`;
const beforeAgentCommand = `node "${beforeAgentScript.replace(/\\/g, '/')}"`; const beforeAgentCommand = `node "${beforeAgentScript.replace(/\\/g, '/')}"`;
rig.setup('should handle hooks for all major event types', { rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
import.meta.dirname, import.meta.dirname,
'hooks-system.multiple-events.responses', 'hooks-system.multiple-events.responses',
@@ -1062,7 +1050,7 @@ try {
const failingCommand = `node "${failingScript.replace(/\\/g, '/')}"`; const failingCommand = `node "${failingScript.replace(/\\/g, '/')}"`;
const workingCommand = `node "${workingScript.replace(/\\/g, '/')}"`; const workingCommand = `node "${workingScript.replace(/\\/g, '/')}"`;
rig.setup('should handle hook failures gracefully', { rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
import.meta.dirname, import.meta.dirname,
'hooks-system.error-handling.responses', 'hooks-system.error-handling.responses',
@@ -1120,7 +1108,7 @@ try {
); );
const hookCommand = `node "${scriptPath.replace(/\\/g, '/')}"`; const hookCommand = `node "${scriptPath.replace(/\\/g, '/')}"`;
rig.setup('should generate telemetry events for hook executions', { rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
import.meta.dirname, import.meta.dirname,
'hooks-system.telemetry.responses', 'hooks-system.telemetry.responses',
@@ -1167,7 +1155,7 @@ try {
); );
const sessionStartCommand = `node "${scriptPath.replace(/\\/g, '/')}"`; const sessionStartCommand = `node "${scriptPath.replace(/\\/g, '/')}"`;
rig.setup('should fire SessionStart hook on app startup', { rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
import.meta.dirname, import.meta.dirname,
'hooks-system.session-startup.responses', 'hooks-system.session-startup.responses',
@@ -1405,46 +1393,43 @@ console.log(JSON.stringify({
const sessionEndCommand = `node "${endScriptPath.replace(/\\/g, '/')}"`; const sessionEndCommand = `node "${endScriptPath.replace(/\\/g, '/')}"`;
const sessionStartCommand = `node "${startScriptPath.replace(/\\/g, '/')}"`; const sessionStartCommand = `node "${startScriptPath.replace(/\\/g, '/')}"`;
rig.setup( rig.configure({
'should fire SessionEnd and SessionStart hooks on /clear command', fakeResponsesPath: join(
{ import.meta.dirname,
fakeResponsesPath: join( 'hooks-system.session-clear.responses',
import.meta.dirname, ),
'hooks-system.session-clear.responses', settings: {
), hooksConfig: {
settings: { enabled: true,
hooksConfig: { },
enabled: true, hooks: {
}, SessionEnd: [
hooks: { {
SessionEnd: [ matcher: '*',
{ hooks: [
matcher: '*', {
hooks: [ type: 'command',
{ command: sessionEndCommand,
type: 'command', timeout: 5000,
command: sessionEndCommand, },
timeout: 5000, ],
}, },
], ],
}, SessionStart: [
], {
SessionStart: [ matcher: '*',
{ hooks: [
matcher: '*', {
hooks: [ type: 'command',
{ command: sessionStartCommand,
type: 'command', timeout: 5000,
command: sessionStartCommand, },
timeout: 5000, ],
}, },
], ],
},
],
},
}, },
}, },
); });
const run = await rig.runInteractive(); const run = await rig.runInteractive();
@@ -1585,7 +1570,7 @@ console.log(JSON.stringify({
); );
const preCompressCommand = `node "${scriptPath.replace(/\\/g, '/')}"`; const preCompressCommand = `node "${scriptPath.replace(/\\/g, '/')}"`;
rig.setup('should fire PreCompress hook on automatic compression', { rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
import.meta.dirname, import.meta.dirname,
'hooks-system.compress-auto.responses', 'hooks-system.compress-auto.responses',
@@ -1659,7 +1644,7 @@ console.log(JSON.stringify({
); );
const sessionEndCommand = `node "${scriptPath.replace(/\\/g, '/')}"`; const sessionEndCommand = `node "${scriptPath.replace(/\\/g, '/')}"`;
rig.setup('should fire SessionEnd hook on graceful exit', { rig.configure({
fakeResponsesPath: join( fakeResponsesPath: join(
import.meta.dirname, import.meta.dirname,
'hooks-system.session-startup.responses', 'hooks-system.session-startup.responses',
+14
View File
@@ -361,6 +361,20 @@ export class TestRig {
this.homeDir = join(testFileDir, sanitizedName + '-home'); this.homeDir = join(testFileDir, sanitizedName + '-home');
mkdirSync(this.testDir, { recursive: true }); mkdirSync(this.testDir, { recursive: true });
mkdirSync(this.homeDir, { 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) { if (options.fakeResponsesPath) {
this.fakeResponsesPath = join(this.testDir, 'fake-responses.json'); this.fakeResponsesPath = join(this.testDir, 'fake-responses.json');
this.originalFakeResponsesPath = options.fakeResponsesPath; this.originalFakeResponsesPath = options.fakeResponsesPath;