mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-22 02:54:31 -07:00
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:
@@ -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' });
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user