Reduce boilerplate.

This commit is contained in:
Christian Gunderman
2026-03-06 13:48:40 -08:00
parent 9a71caa781
commit 489e7bfbef
21 changed files with 230 additions and 151 deletions

View File

@@ -7,7 +7,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig, poll, normalizePath } from './test-helper.js';
import { join } from 'node:path';
import { writeFileSync } from 'node:fs';
import { writeFileSync, existsSync, mkdirSync } from 'node:fs';
import os from 'node:os';
describe('Hooks System Integration', () => {
@@ -2247,10 +2247,10 @@ console.log(JSON.stringify({
describe('Hooks "ask" Decision Integration', () => {
it(
'should force confirmation prompt when hook returns "ask" decision even in YOLO mode',
{ timeout: 20000 },
{ timeout: 60000 },
async () => {
const testName =
'should force confirmation prompt when hook returns "ask" decision';
'should force confirmation prompt when hook returns "ask" decision even in YOLO mode';
// 1. Setup hook script that returns 'ask' decision
const hookOutput = {
@@ -2280,6 +2280,9 @@ console.log(JSON.stringify({
tools: {
approval: 'yolo',
},
general: {
enableAutoUpdateNotification: false,
},
hooksConfig: {
enabled: true,
},
@@ -2300,16 +2303,31 @@ console.log(JSON.stringify({
},
});
// Bypass terminal setup prompt and other startup banners
const stateDir = join(rig.homeDir!, '.gemini');
if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });
writeFileSync(
join(stateDir, 'state.json'),
JSON.stringify({
terminalSetupPromptShown: true,
hasSeenScreenReaderNudge: true,
tipsShown: 100,
}),
);
// 3. Run interactive and verify prompt appears despite YOLO mode
const run = await rig.runInteractive();
// Wait for prompt to appear
await run.expectText('Type your message', 30000);
// Send prompt that will trigger write_file
await run.type('Create a file called ask-test.txt with content "test"');
await run.type('\r');
// Wait for the FORCED confirmation prompt to appear
// It should contain the system message from the hook
await run.expectText('Confirmation forced by security hook', 15000);
await run.expectText('Confirmation forced by security hook', 30000);
await run.expectText('Allow', 5000);
// 4. Approve the permission
@@ -2317,7 +2335,7 @@ console.log(JSON.stringify({
await run.type('\r');
// Wait for command to execute
await run.expectText('approved.txt', 15000);
await run.expectText('approved.txt', 30000);
// Should find the tool call
const foundWriteFile = await rig.waitForToolCall('write_file');
@@ -2329,80 +2347,106 @@ console.log(JSON.stringify({
},
);
it('should allow cancelling when hook forces "ask" decision', async () => {
const testName =
'should allow cancelling when hook forces "ask" decision';
const hookOutput = {
decision: 'ask',
systemMessage: 'Confirmation forced for cancellation test',
hookSpecificOutput: {
hookEventName: 'BeforeTool',
},
};
const hookScript = `console.log(JSON.stringify(${JSON.stringify(
hookOutput,
)}));`;
const scriptPath = join(
os.tmpdir(),
'gemini-cli-tests-ask-cancel-hook.js',
);
writeFileSync(scriptPath, hookScript);
rig.setup(testName, {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.allow-tool.responses',
),
settings: {
debugMode: true,
tools: {
approval: 'yolo',
it(
'should allow cancelling when hook forces "ask" decision',
{ timeout: 60000 },
async () => {
const testName =
'should allow cancelling when hook forces "ask" decision';
const hookOutput = {
decision: 'ask',
systemMessage: 'Confirmation forced for cancellation test',
hookSpecificOutput: {
hookEventName: 'BeforeTool',
},
hooksConfig: {
enabled: true,
};
const hookScript = `console.log(JSON.stringify(${JSON.stringify(
hookOutput,
)}));`;
const scriptPath = join(
os.tmpdir(),
'gemini-cli-tests-ask-cancel-hook.js',
);
writeFileSync(scriptPath, hookScript);
rig.setup(testName, {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.allow-tool.responses',
),
settings: {
debugMode: true,
tools: {
approval: 'yolo',
},
general: {
enableAutoUpdateNotification: false,
},
hooksConfig: {
enabled: true,
},
hooks: {
BeforeTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
command: `node "${scriptPath}"`,
timeout: 5000,
},
],
},
],
},
},
hooks: {
BeforeTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
command: `node "${scriptPath}"`,
timeout: 5000,
},
],
},
],
},
},
});
});
const run = await rig.runInteractive();
// Bypass terminal setup prompt and other startup banners
const stateDir = join(rig.homeDir!, '.gemini');
if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });
writeFileSync(
join(stateDir, 'state.json'),
JSON.stringify({
terminalSetupPromptShown: true,
hasSeenScreenReaderNudge: true,
tipsShown: 100,
}),
);
await run.type(
'Create a file called cancel-test.txt with content "test"',
);
await run.type('\r');
const run = await rig.runInteractive();
await run.expectText('Confirmation forced for cancellation test', 15000);
// Wait for prompt to appear
await run.expectText('Type your message', 30000);
// 4. Deny the permission using option 4
await run.type('4');
await run.type('\r');
await run.type(
'Create a file called cancel-test.txt with content "test"',
);
await run.type('\r');
// Wait for cancellation message
await run.expectText('Cancelled', 10000);
await run.expectText(
'Confirmation forced for cancellation test',
30000,
);
// Tool should NOT be called successfully
const toolLogs = rig.readToolLogs();
const writeFileCalls = toolLogs.filter(
(t) =>
t.toolRequest.name === 'write_file' && t.toolRequest.success === true,
);
expect(writeFileCalls).toHaveLength(0);
});
// 4. Deny the permission using option 4
await run.type('4');
await run.type('\r');
// Wait for cancellation message
await run.expectText('Cancelled', 15000);
// Tool should NOT be called successfully
const toolLogs = rig.readToolLogs();
const writeFileCalls = toolLogs.filter(
(t) =>
t.toolRequest.name === 'write_file' &&
t.toolRequest.success === true,
);
expect(writeFileCalls).toHaveLength(0);
},
);
});
});