diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/hookUtils.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/hookUtils.test.ts new file mode 100644 index 00000000000..299a622ad4c --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/hookUtils.test.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { findHookCommandSelection } from '../../../browser/promptSyntax/hookUtils.js'; + +suite('hookUtils', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('findHookCommandSelection', () => { + + test('finds command field in first hook entry', () => { + const content = '{\n\t"sessionStart": [\n\t\t{\n\t\t\t"command": "echo hello"\n\t\t}\n\t]\n}'; + const result = findHookCommandSelection(content, 'sessionStart', 0, 'command'); + assert.deepStrictEqual(result, { + startLineNumber: 4, + startColumn: 16, + endLineNumber: 4, + endColumn: 26 + }); + }); + + test('finds command field in second hook entry', () => { + const content = '{\n\t"sessionStart": [\n\t\t{\n\t\t\t"command": "first"\n\t\t},\n\t\t{\n\t\t\t"command": "second"\n\t\t}\n\t]\n}'; + const result = findHookCommandSelection(content, 'sessionStart', 1, 'command'); + assert.deepStrictEqual(result, { + startLineNumber: 7, + startColumn: 16, + endLineNumber: 7, + endColumn: 22 + }); + }); + + test('finds bash field for platform-specific hook', () => { + const content = '{\n\t"preToolUse": [\n\t\t{\n\t\t\t"bash": "bash command",\n\t\t\t"powershell": "powershell command"\n\t\t}\n\t]\n}'; + const result = findHookCommandSelection(content, 'preToolUse', 0, 'bash'); + assert.deepStrictEqual(result, { + startLineNumber: 4, + startColumn: 12, + endLineNumber: 4, + endColumn: 24 + }); + }); + + test('finds powershell field for platform-specific hook', () => { + const content = '{\n\t"preToolUse": [\n\t\t{\n\t\t\t"bash": "bash command",\n\t\t\t"powershell": "powershell command"\n\t\t}\n\t]\n}'; + const result = findHookCommandSelection(content, 'preToolUse', 0, 'powershell'); + assert.deepStrictEqual(result, { + startLineNumber: 5, + startColumn: 18, + endLineNumber: 5, + endColumn: 36 + }); + }); + + test('returns undefined for non-existent hook type', () => { + const content = '{\n\t"sessionStart": [\n\t\t{\n\t\t\t"command": "echo hello"\n\t\t}\n\t]\n}'; + const result = findHookCommandSelection(content, 'nonExistent', 0, 'command'); + assert.strictEqual(result, undefined); + }); + + test('returns undefined for out-of-bounds index', () => { + const content = '{\n\t"sessionStart": [\n\t\t{\n\t\t\t"command": "echo hello"\n\t\t}\n\t]\n}'; + const result = findHookCommandSelection(content, 'sessionStart', 5, 'command'); + assert.strictEqual(result, undefined); + }); + + test('returns undefined for non-existent field', () => { + const content = '{\n\t"sessionStart": [\n\t\t{\n\t\t\t"command": "echo hello"\n\t\t}\n\t]\n}'; + const result = findHookCommandSelection(content, 'sessionStart', 0, 'bash'); + assert.strictEqual(result, undefined); + }); + + test('returns undefined for invalid JSON', () => { + const content = '{ invalid json }'; + const result = findHookCommandSelection(content, 'sessionStart', 0, 'command'); + assert.strictEqual(result, undefined); + }); + + test('returns undefined for empty content', () => { + const result = findHookCommandSelection('', 'sessionStart', 0, 'command'); + assert.strictEqual(result, undefined); + }); + + test('handles command with special characters', () => { + const content = '{\n\t"sessionStart": [\n\t\t{\n\t\t\t"command": "echo \\"quoted\\""\n\t\t}\n\t]\n}'; + const result = findHookCommandSelection(content, 'sessionStart', 0, 'command'); + assert.deepStrictEqual(result, { + startLineNumber: 4, + startColumn: 16, + endLineNumber: 4, + endColumn: 32 + }); + }); + + test('works with different hook types', () => { + const content = '{\n\t"userPromptSubmitted": [\n\t\t{\n\t\t\t"command": "validate"\n\t\t}\n\t],\n\t"postToolUse": [\n\t\t{\n\t\t\t"command": "cleanup"\n\t\t}\n\t]\n}'; + const result1 = findHookCommandSelection(content, 'userPromptSubmitted', 0, 'command'); + assert.deepStrictEqual(result1, { + startLineNumber: 4, + startColumn: 16, + endLineNumber: 4, + endColumn: 24 + }); + + const result2 = findHookCommandSelection(content, 'postToolUse', 0, 'command'); + assert.deepStrictEqual(result2, { + startLineNumber: 9, + startColumn: 16, + endLineNumber: 9, + endColumn: 23 + }); + }); + + test('handles hooks with additional properties', () => { + const content = '{\n\t"sessionStart": [\n\t\t{\n\t\t\t"command": "my-command",\n\t\t\t"cwd": "/some/path",\n\t\t\t"timeoutSec": 30\n\t\t}\n\t]\n}'; + const result = findHookCommandSelection(content, 'sessionStart', 0, 'command'); + assert.deepStrictEqual(result, { + startLineNumber: 4, + startColumn: 16, + endLineNumber: 4, + endColumn: 26 + }); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts index 7cff10af00f..f9557563e11 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts @@ -99,24 +99,27 @@ suite('HookSchema', () => { }); }); - test('empty command returns undefined', () => { + test('empty command returns object without command', () => { const result = resolveHookCommand({ type: 'command', command: '' }, workspaceRoot, userHome); - assert.strictEqual(result, undefined); + assert.deepStrictEqual(result, { + type: 'command', + cwd: workspaceRoot + }); }); }); suite('bash shorthand', () => { - test('resolves bash to command', () => { + test('preserves bash property', () => { const result = resolveHookCommand({ type: 'command', bash: 'echo "hello world"' }, workspaceRoot, userHome); assert.deepStrictEqual(result, { type: 'command', - command: 'bash -c "echo \\"hello world\\""', + bash: 'echo "hello world"', cwd: workspaceRoot }); }); @@ -130,30 +133,33 @@ suite('HookSchema', () => { }, workspaceRoot, userHome); assert.deepStrictEqual(result, { type: 'command', - command: 'bash -c "./test.sh"', + bash: './test.sh', cwd: URI.file('/workspace/scripts'), env: { DEBUG: '1' } }); }); - test('empty bash returns undefined', () => { + test('empty bash returns object without bash', () => { const result = resolveHookCommand({ type: 'command', bash: '' }, workspaceRoot, userHome); - assert.strictEqual(result, undefined); + assert.deepStrictEqual(result, { + type: 'command', + cwd: workspaceRoot + }); }); }); suite('powershell shorthand', () => { - test('resolves powershell to command', () => { + test('preserves powershell property', () => { const result = resolveHookCommand({ type: 'command', powershell: 'Write-Host "hello"' }, workspaceRoot, userHome); assert.deepStrictEqual(result, { type: 'command', - command: 'powershell -Command "Write-Host \\"hello\\""', + powershell: 'Write-Host "hello"', cwd: workspaceRoot }); }); @@ -166,23 +172,26 @@ suite('HookSchema', () => { }, workspaceRoot, userHome); assert.deepStrictEqual(result, { type: 'command', - command: 'powershell -Command "Get-Process"', + powershell: 'Get-Process', cwd: workspaceRoot, timeoutSec: 30 }); }); - test('empty powershell returns undefined', () => { + test('empty powershell returns object without powershell', () => { const result = resolveHookCommand({ type: 'command', powershell: '' }, workspaceRoot, userHome); - assert.strictEqual(result, undefined); + assert.deepStrictEqual(result, { + type: 'command', + cwd: workspaceRoot + }); }); }); - suite('priority when multiple specified', () => { - test('command takes precedence over bash', () => { + suite('multiple properties specified', () => { + test('preserves both command and bash', () => { const result = resolveHookCommand({ type: 'command', command: 'direct-command', @@ -191,11 +200,12 @@ suite('HookSchema', () => { assert.deepStrictEqual(result, { type: 'command', command: 'direct-command', + bash: 'bash-script.sh', cwd: workspaceRoot }); }); - test('command takes precedence over powershell', () => { + test('preserves both command and powershell', () => { const result = resolveHookCommand({ type: 'command', command: 'direct-command', @@ -204,11 +214,12 @@ suite('HookSchema', () => { assert.deepStrictEqual(result, { type: 'command', command: 'direct-command', + powershell: 'ps-script.ps1', cwd: workspaceRoot }); }); - test('bash takes precedence over powershell when no command', () => { + test('preserves both bash and powershell when no command', () => { const result = resolveHookCommand({ type: 'command', bash: 'bash-script.sh', @@ -216,7 +227,8 @@ suite('HookSchema', () => { }, workspaceRoot, userHome); assert.deepStrictEqual(result, { type: 'command', - command: 'bash -c "bash-script.sh"', + bash: 'bash-script.sh', + powershell: 'ps-script.ps1', cwd: workspaceRoot }); }); @@ -265,12 +277,15 @@ suite('HookSchema', () => { assert.strictEqual(result, undefined); }); - test('no command/bash/powershell returns undefined', () => { + test('no command/bash/powershell returns object with just type and cwd', () => { const result = resolveHookCommand({ type: 'command', cwd: '/workspace' }, workspaceRoot, userHome); - assert.strictEqual(result, undefined); + assert.deepStrictEqual(result, { + type: 'command', + cwd: URI.file('/workspace') + }); }); test('ignores non-string cwd', () => {