|
3 | 3 | * Licensed under the MIT License. See License.txt in the project root for license information. |
4 | 4 | *--------------------------------------------------------------------------------------------*/ |
5 | 5 |
|
| 6 | +import { spawn } from 'child_process'; |
6 | 7 | import { EventEmitter } from 'events'; |
7 | 8 | import { beforeEach, describe, expect, test, vi } from 'vitest'; |
8 | 9 | import type { ChatHookCommand } from 'vscode'; |
@@ -36,6 +37,11 @@ function createMockChild(): MockChildProcess { |
36 | 37 | return child; |
37 | 38 | } |
38 | 39 |
|
| 40 | +function getSpawnCommand(): string { |
| 41 | + const calls = vi.mocked(spawn).mock.calls; |
| 42 | + return String(calls.at(-1)?.[0]); |
| 43 | +} |
| 44 | + |
39 | 45 | /** |
40 | 46 | * Simulates a child process completing with the given stdout, stderr, and exit code. |
41 | 47 | */ |
@@ -169,6 +175,54 @@ describe('NodeHookExecutor', () => { |
169 | 175 | expect(result.kind).toBe(HookCommandResultKind.Success); |
170 | 176 | }); |
171 | 177 |
|
| 178 | + test('sanitizes file URIs in hook commands before spawn', async () => { |
| 179 | + const promise = executor.executeCommand( |
| 180 | + cmd('cat file:///tmp/example.txt'), |
| 181 | + undefined, |
| 182 | + CancellationToken.None |
| 183 | + ); |
| 184 | + completeChild(child, { stdout: 'ok', exitCode: 0 }); |
| 185 | + await promise; |
| 186 | + |
| 187 | + expect(getSpawnCommand()).toBe('cat /tmp/example.txt'); |
| 188 | + }); |
| 189 | + |
| 190 | + test('quotes sanitized file URIs with spaces in hook commands', async () => { |
| 191 | + const promise = executor.executeCommand( |
| 192 | + cmd('cat file:///tmp/my%20file.txt'), |
| 193 | + undefined, |
| 194 | + CancellationToken.None |
| 195 | + ); |
| 196 | + completeChild(child, { stdout: 'ok', exitCode: 0 }); |
| 197 | + await promise; |
| 198 | + |
| 199 | + expect(getSpawnCommand()).toBe('cat "/tmp/my file.txt"'); |
| 200 | + }); |
| 201 | + |
| 202 | + test('preserves existing quotes around sanitized file URIs', async () => { |
| 203 | + const promise = executor.executeCommand( |
| 204 | + cmd('cat "file:///tmp/my%20file.txt"'), |
| 205 | + undefined, |
| 206 | + CancellationToken.None |
| 207 | + ); |
| 208 | + completeChild(child, { stdout: 'ok', exitCode: 0 }); |
| 209 | + await promise; |
| 210 | + |
| 211 | + expect(getSpawnCommand()).toBe('cat "/tmp/my file.txt"'); |
| 212 | + }); |
| 213 | + |
| 214 | + test('does not sanitize non-file URIs in hook commands', async () => { |
| 215 | + const promise = executor.executeCommand( |
| 216 | + cmd('cat vscode-remote://ssh-remote+test/workspace/file.txt'), |
| 217 | + undefined, |
| 218 | + CancellationToken.None |
| 219 | + ); |
| 220 | + completeChild(child, { stdout: 'ok', exitCode: 0 }); |
| 221 | + await promise; |
| 222 | + |
| 223 | + expect(getSpawnCommand()).toBe('cat vscode-remote://ssh-remote+test/workspace/file.txt'); |
| 224 | + }); |
| 225 | + |
172 | 226 | test('handles spawn error as non-blocking error', async () => { |
173 | 227 | const promise = executor.executeCommand(cmd('badcmd'), undefined, CancellationToken.None); |
174 | 228 | child.emit('error', new Error('spawn ENOENT')); |
|
0 commit comments