Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions packages/core/src/hooks/hook-runner.process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,80 @@ const isRunning = (pid: number): boolean => {
return !result.stdout.trim().startsWith('Z');
};

describe.runIf(process.platform === 'win32')(
'HookRunner Windows project directory expansion',
() => {
it.each(['QWEN_PROJECT_DIR', 'GEMINI_PROJECT_DIR', 'CLAUDE_PROJECT_DIR'])(
'expands $%s for cmd.exe',
async (variable) => {
const tempDir = await mkdtemp(
join(tmpdir(), "qwen hook (project) & 'quoted' "),
);

try {
const runner = new HookRunner();
const input: HookInput = {
session_id: 'project-dir-test',
transcript_path: join(tempDir, 'transcript.jsonl'),
cwd: tempDir,
hook_event_name: HookEventName.PreToolUse,
timestamp: new Date().toISOString(),
};

const result = await runner.executeHook(
{
type: HookType.Command,
command: `if exist $${variable} (echo FOUND) else (echo MISSING)`,
source: HooksConfigSource.Project,
},
HookEventName.PreToolUse,
input,
);

expect(result.success).toBe(true);
expect(result.stdout?.trim()).toBe('FOUND');
} finally {
await rm(tempDir, { recursive: true, force: true });
}
},
);

it('expands QWEN_PROJECT_DIR for PowerShell', async () => {
const tempDir = await mkdtemp(
join(tmpdir(), "qwen hook (project) & 'quoted' "),
);

try {
const runner = new HookRunner();
const input: HookInput = {
session_id: 'project-dir-test',
transcript_path: join(tempDir, 'transcript.jsonl'),
cwd: tempDir,
hook_event_name: HookEventName.PreToolUse,
timestamp: new Date().toISOString(),
};

const result = await runner.executeHook(
{
type: HookType.Command,
command:
"if (Test-Path $QWEN_PROJECT_DIR) { Write-Output 'FOUND' } else { Write-Output 'MISSING' }",
source: HooksConfigSource.Project,
shell: 'powershell',
},
HookEventName.PreToolUse,
input,
);

expect(result.success).toBe(true);
expect(result.stdout?.trim()).toBe('FOUND');
} finally {
await rm(tempDir, { recursive: true, force: true });
}
});
},
);

describe.skipIf(process.platform === 'win32')(
'HookRunner process tree cancellation',
() => {
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/hooks/hookRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,25 @@ describe('HookRunner', () => {
});

describe('expandCommand', () => {
it('should expand QWEN_PROJECT_DIR placeholder', async () => {
const mockProcess = createMockProcess(0, 'result');
mockSpawn.mockImplementation(() => mockProcess);

const hookConfig: HookConfig = {
type: HookType.Command,
command: 'echo $QWEN_PROJECT_DIR',
source: HooksConfigSource.Project,
};
const input = createMockInput({ cwd: '/test/project' });

await hookRunner.executeHook(hookConfig, HookEventName.PreToolUse, input);

const spawnCall = mockSpawn.mock.calls[0];
const command = spawnCall[1][spawnCall[1].length - 1];
expect(command).toContain('/test/project');
expect(command).not.toContain('$QWEN_PROJECT_DIR');
});

it('should expand GEMINI_PROJECT_DIR placeholder', async () => {
const mockProcess = createMockProcess(0, 'result');
mockSpawn.mockImplementation(() => mockProcess);
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/hooks/hookRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,8 @@ export class HookRunner {
cwd: input.cwd,
stdio: ['pipe', 'pipe', 'pipe'],
shell: false,
windowsVerbatimArguments:
process.platform === 'win32' && shellConfig.shell === 'cmd',
// Own a process group so cancellation can signal the entire tree.
detached: process.platform !== 'win32',
},
Expand Down Expand Up @@ -1056,6 +1058,7 @@ export class HookRunner {
debugLogger.debug(`Expanding hook command: ${command} (cwd: ${input.cwd})`);
const escapedCwd = escapeShellArg(input.cwd, shellType);
return command
.replace(/\$QWEN_PROJECT_DIR/g, () => escapedCwd)
.replace(/\$GEMINI_PROJECT_DIR/g, () => escapedCwd)
.replace(/\$CLAUDE_PROJECT_DIR/g, () => escapedCwd); // For compatibility
}
Expand Down
Loading