Path: blob/main/src/vs/workbench/api/test/node/extHostHooks.test.ts
5240 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { URI } from '../../../../base/common/uri.js';7import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';8import { NullLogService } from '../../../../platform/log/common/log.js';9import { NodeExtHostHooks } from '../../node/extHostHooksNode.js';10import { IHookCommandDto, MainThreadHooksShape } from '../../common/extHost.protocol.js';11import { HookCommandResultKind } from '../../../contrib/chat/common/hooks/hooksCommandTypes.js';12import { IHookResult } from '../../../contrib/chat/common/hooks/hooksTypes.js';13import { IExtHostRpcService } from '../../common/extHostRpcService.js';14import { CancellationToken } from '../../../../base/common/cancellation.js';1516function createHookCommandDto(command: string, options?: Partial<Omit<IHookCommandDto, 'type' | 'command'>>): IHookCommandDto {17return {18type: 'command',19command,20...options,21};22}2324function createMockExtHostRpcService(mainThreadProxy: MainThreadHooksShape): IExtHostRpcService {25return {26_serviceBrand: undefined,27getProxy<T>(): T {28return mainThreadProxy as unknown as T;29},30set<T, R extends T>(_identifier: unknown, instance: R): R {31return instance;32},33dispose(): void { },34assertRegistered(): void { },35drain(): Promise<void> { return Promise.resolve(); },36} as IExtHostRpcService;37}3839suite.skip('ExtHostHooks', () => {40ensureNoDisposablesAreLeakedInTestSuite();4142let hooksService: NodeExtHostHooks;4344setup(() => {45const mockMainThreadProxy: MainThreadHooksShape = {46$executeHook: async (): Promise<IHookResult[]> => {47return [];48},49dispose: () => { }50};5152const mockRpcService = createMockExtHostRpcService(mockMainThreadProxy);53hooksService = new NodeExtHostHooks(mockRpcService, new NullLogService());54});5556test('$runHookCommand runs command and returns success result', async () => {57const hookCommand = createHookCommandDto('echo "hello world"');58const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);5960assert.strictEqual(result.kind, HookCommandResultKind.Success);61assert.strictEqual((result.result as string).trim(), 'hello world');62});6364test('$runHookCommand parses JSON output', async () => {65const hookCommand = createHookCommandDto('echo \'{"key": "value"}\'');66const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);6768assert.strictEqual(result.kind, HookCommandResultKind.Success);69assert.deepStrictEqual(result.result, { key: 'value' });70});7172test('$runHookCommand returns non-blocking error for exit code 1', async () => {73const hookCommand = createHookCommandDto('exit 1');74const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);7576assert.strictEqual(result.kind, HookCommandResultKind.NonBlockingError);77});7879test('$runHookCommand returns blocking error for exit code 2', async () => {80const hookCommand = createHookCommandDto('exit 2');81const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);8283assert.strictEqual(result.kind, HookCommandResultKind.Error);84});8586test('$runHookCommand captures stderr on non-blocking error', async () => {87const hookCommand = createHookCommandDto('echo "error message" >&2 && exit 1');88const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);8990assert.strictEqual(result.kind, HookCommandResultKind.NonBlockingError);91assert.strictEqual((result.result as string).trim(), 'error message');92});9394test('$runHookCommand captures stderr on blocking error', async () => {95const hookCommand = createHookCommandDto('echo "blocking error" >&2 && exit 2');96const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);9798assert.strictEqual(result.kind, HookCommandResultKind.Error);99assert.strictEqual((result.result as string).trim(), 'blocking error');100});101102test('$runHookCommand passes input to stdin as JSON', async () => {103const hookCommand = createHookCommandDto('cat');104const input = { tool: 'bash', args: { command: 'ls' } };105const result = await hooksService.$runHookCommand(hookCommand, input, CancellationToken.None);106107assert.strictEqual(result.kind, HookCommandResultKind.Success);108assert.deepStrictEqual(result.result, input);109});110111test('$runHookCommand returns non-blocking error for invalid command', async () => {112const hookCommand = createHookCommandDto('/nonexistent/command/that/does/not/exist');113const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);114115// Invalid commands typically return non-zero exit codes (127 for command not found)116// which are treated as non-blocking errors unless it's exit code 2117assert.strictEqual(result.kind, HookCommandResultKind.NonBlockingError);118});119120test('$runHookCommand uses custom environment variables', async () => {121const hookCommand = createHookCommandDto('echo $MY_VAR', { env: { MY_VAR: 'custom_value' } });122const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);123124assert.strictEqual(result.kind, HookCommandResultKind.Success);125assert.strictEqual((result.result as string).trim(), 'custom_value');126});127128test('$runHookCommand uses custom cwd', async () => {129const hookCommand = createHookCommandDto('pwd', { cwd: URI.file('/tmp') });130const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);131132assert.strictEqual(result.kind, HookCommandResultKind.Success);133// The result should contain /tmp or /private/tmp (macOS symlink)134assert.ok((result.result as string).includes('tmp'));135});136});137138139