mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
Sync upstream Gemini-CLI v0.8.2 (#838)
This commit is contained in:
@@ -10,9 +10,135 @@ import { describe, it, expect, vi } from 'vitest';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { useSlashCompletion } from './useSlashCompletion.js';
|
||||
import type { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import { CommandKind } from '../commands/types.js';
|
||||
import { useState } from 'react';
|
||||
import type { Suggestion } from '../components/SuggestionsDisplay.js';
|
||||
|
||||
// Test utility type and helper function for creating test SlashCommands
|
||||
type TestSlashCommand = Omit<SlashCommand, 'kind'> &
|
||||
Partial<Pick<SlashCommand, 'kind'>>;
|
||||
|
||||
function createTestCommand(command: TestSlashCommand): SlashCommand {
|
||||
return {
|
||||
kind: CommandKind.BUILT_IN, // default for tests
|
||||
...command,
|
||||
};
|
||||
}
|
||||
|
||||
// Track AsyncFzf constructor calls for cache testing
|
||||
let asyncFzfConstructorCalls = 0;
|
||||
const resetConstructorCallCount = () => {
|
||||
asyncFzfConstructorCalls = 0;
|
||||
};
|
||||
const getConstructorCallCount = () => asyncFzfConstructorCalls;
|
||||
|
||||
// Centralized fuzzy matching simulation logic
|
||||
// Note: This is a simplified reimplementation that may diverge from real fzf behavior.
|
||||
// Integration tests in useSlashCompletion.integration.test.ts use the real fzf library
|
||||
// to catch any behavioral differences and serve as our "canary in a coal mine."
|
||||
function simulateFuzzyMatching(items: readonly string[], query: string) {
|
||||
const results = [];
|
||||
if (query) {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
for (const item of items) {
|
||||
const lowerItem = item.toLowerCase();
|
||||
|
||||
// Exact match gets highest score
|
||||
if (lowerItem === lowerQuery) {
|
||||
results.push({
|
||||
item,
|
||||
positions: [],
|
||||
score: 100,
|
||||
start: 0,
|
||||
end: item.length,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prefix match gets high score
|
||||
if (lowerItem.startsWith(lowerQuery)) {
|
||||
results.push({
|
||||
item,
|
||||
positions: [],
|
||||
score: 80,
|
||||
start: 0,
|
||||
end: query.length,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fuzzy matching: check if query chars appear in order
|
||||
let queryIndex = 0;
|
||||
let score = 0;
|
||||
for (
|
||||
let i = 0;
|
||||
i < lowerItem.length && queryIndex < lowerQuery.length;
|
||||
i++
|
||||
) {
|
||||
if (lowerItem[i] === lowerQuery[queryIndex]) {
|
||||
queryIndex++;
|
||||
score += 10 - i; // Earlier matches get higher scores
|
||||
}
|
||||
}
|
||||
|
||||
// If all query characters were found in order, include this item
|
||||
if (queryIndex === lowerQuery.length) {
|
||||
results.push({
|
||||
item,
|
||||
positions: [],
|
||||
score,
|
||||
start: 0,
|
||||
end: query.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score descending (better matches first)
|
||||
results.sort((a, b) => b.score - a.score);
|
||||
return Promise.resolve(results);
|
||||
}
|
||||
|
||||
// Mock the fzf module to provide a working fuzzy search implementation for tests
|
||||
vi.mock('fzf', async () => {
|
||||
const actual = await vi.importActual<typeof import('fzf')>('fzf');
|
||||
return {
|
||||
...actual,
|
||||
AsyncFzf: vi.fn().mockImplementation((items, _options) => {
|
||||
asyncFzfConstructorCalls++;
|
||||
return {
|
||||
find: vi
|
||||
.fn()
|
||||
.mockImplementation((query: string) =>
|
||||
simulateFuzzyMatching(items, query),
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// Default mock behavior helper - now uses centralized logic
|
||||
const createDefaultAsyncFzfMock =
|
||||
() => (items: readonly string[], _options: unknown) => {
|
||||
asyncFzfConstructorCalls++;
|
||||
return {
|
||||
find: vi
|
||||
.fn()
|
||||
.mockImplementation((query: string) =>
|
||||
simulateFuzzyMatching(items, query),
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
};
|
||||
|
||||
// Export test utilities
|
||||
export {
|
||||
resetConstructorCallCount,
|
||||
getConstructorCallCount,
|
||||
createDefaultAsyncFzfMock,
|
||||
};
|
||||
|
||||
// Test harness to capture the state from the hook's callbacks.
|
||||
function useTestHarnessForSlashCompletion(
|
||||
enabled: boolean,
|
||||
@@ -50,20 +176,26 @@ describe('useSlashCompletion', () => {
|
||||
describe('Top-Level Commands', () => {
|
||||
it('should suggest all top-level commands for the root slash', async () => {
|
||||
const slashCommands = [
|
||||
{ name: 'help', altNames: ['?'], description: 'Show help' },
|
||||
{
|
||||
createTestCommand({
|
||||
name: 'help',
|
||||
altNames: ['?'],
|
||||
description: 'Show help',
|
||||
}),
|
||||
createTestCommand({
|
||||
name: 'stats',
|
||||
altNames: ['usage'],
|
||||
description: 'check session stats. Usage: /stats [model|tools]',
|
||||
},
|
||||
{ name: 'clear', description: 'Clear the screen' },
|
||||
{
|
||||
}),
|
||||
createTestCommand({ name: 'clear', description: 'Clear the screen' }),
|
||||
createTestCommand({
|
||||
name: 'memory',
|
||||
description: 'Manage memory',
|
||||
subCommands: [{ name: 'show', description: 'Show memory' }],
|
||||
},
|
||||
{ name: 'chat', description: 'Manage chat history' },
|
||||
] as unknown as SlashCommand[];
|
||||
subCommands: [
|
||||
createTestCommand({ name: 'show', description: 'Show memory' }),
|
||||
],
|
||||
}),
|
||||
createTestCommand({ name: 'chat', description: 'Manage chat history' }),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
@@ -81,8 +213,8 @@ describe('useSlashCompletion', () => {
|
||||
|
||||
it('should filter commands based on partial input', async () => {
|
||||
const slashCommands = [
|
||||
{ name: 'memory', description: 'Manage memory' },
|
||||
] as unknown as SlashCommand[];
|
||||
createTestCommand({ name: 'memory', description: 'Manage memory' }),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
@@ -92,19 +224,26 @@ describe('useSlashCompletion', () => {
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{ label: 'memory', value: 'memory', description: 'Manage memory' },
|
||||
]);
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{
|
||||
label: 'memory',
|
||||
value: 'memory',
|
||||
description: 'Manage memory',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should suggest commands based on partial altNames', async () => {
|
||||
const slashCommands = [
|
||||
{
|
||||
createTestCommand({
|
||||
name: 'stats',
|
||||
altNames: ['usage'],
|
||||
description: 'check session stats. Usage: /stats [model|tools]',
|
||||
},
|
||||
] as unknown as SlashCommand[];
|
||||
}),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
@@ -114,19 +253,26 @@ describe('useSlashCompletion', () => {
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{
|
||||
label: 'stats',
|
||||
value: 'stats',
|
||||
description: 'check session stats. Usage: /stats [model|tools]',
|
||||
},
|
||||
]);
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{
|
||||
label: 'stats',
|
||||
value: 'stats',
|
||||
description: 'check session stats. Usage: /stats [model|tools]',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT provide suggestions for a perfectly typed command that is a leaf node', async () => {
|
||||
const slashCommands = [
|
||||
{ name: 'clear', description: 'Clear the screen', action: vi.fn() },
|
||||
] as unknown as SlashCommand[];
|
||||
createTestCommand({
|
||||
name: 'clear',
|
||||
description: 'Clear the screen',
|
||||
action: vi.fn(),
|
||||
}),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
@@ -143,19 +289,19 @@ describe('useSlashCompletion', () => {
|
||||
'should not suggest commands when altNames is fully typed',
|
||||
async (query) => {
|
||||
const mockSlashCommands = [
|
||||
{
|
||||
createTestCommand({
|
||||
name: 'help',
|
||||
altNames: ['?'],
|
||||
description: 'Show help',
|
||||
action: vi.fn(),
|
||||
},
|
||||
{
|
||||
}),
|
||||
createTestCommand({
|
||||
name: 'stats',
|
||||
altNames: ['usage'],
|
||||
description: 'check session stats. Usage: /stats [model|tools]',
|
||||
action: vi.fn(),
|
||||
},
|
||||
] as unknown as SlashCommand[];
|
||||
}),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
@@ -172,8 +318,8 @@ describe('useSlashCompletion', () => {
|
||||
|
||||
it('should not provide suggestions for a fully typed command that has no sub-commands or argument completion', async () => {
|
||||
const slashCommands = [
|
||||
{ name: 'clear', description: 'Clear the screen' },
|
||||
] as unknown as SlashCommand[];
|
||||
createTestCommand({ name: 'clear', description: 'Clear the screen' }),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
@@ -188,8 +334,8 @@ describe('useSlashCompletion', () => {
|
||||
|
||||
it('should not provide suggestions for an unknown command', async () => {
|
||||
const slashCommands = [
|
||||
{ name: 'help', description: 'Show help' },
|
||||
] as unknown as SlashCommand[];
|
||||
createTestCommand({ name: 'help', description: 'Show help' }),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
@@ -201,20 +347,45 @@ describe('useSlashCompletion', () => {
|
||||
|
||||
expect(result.current.suggestions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not suggest hidden commands', async () => {
|
||||
const slashCommands = [
|
||||
createTestCommand({
|
||||
name: 'visible',
|
||||
description: 'A visible command',
|
||||
}),
|
||||
createTestCommand({
|
||||
name: 'hidden',
|
||||
description: 'A hidden command',
|
||||
hidden: true,
|
||||
}),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
expect(result.current.suggestions[0].label).toBe('visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sub-Commands', () => {
|
||||
it('should suggest sub-commands for a parent command', async () => {
|
||||
const slashCommands = [
|
||||
{
|
||||
createTestCommand({
|
||||
name: 'memory',
|
||||
description: 'Manage memory',
|
||||
subCommands: [
|
||||
{ name: 'show', description: 'Show memory' },
|
||||
{ name: 'add', description: 'Add to memory' },
|
||||
createTestCommand({ name: 'show', description: 'Show memory' }),
|
||||
createTestCommand({ name: 'add', description: 'Add to memory' }),
|
||||
],
|
||||
},
|
||||
] as unknown as SlashCommand[];
|
||||
}),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
@@ -228,23 +399,33 @@ describe('useSlashCompletion', () => {
|
||||
expect(result.current.suggestions).toHaveLength(2);
|
||||
expect(result.current.suggestions).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ label: 'show', value: 'show', description: 'Show memory' },
|
||||
{ label: 'add', value: 'add', description: 'Add to memory' },
|
||||
{
|
||||
label: 'show',
|
||||
value: 'show',
|
||||
description: 'Show memory',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
{
|
||||
label: 'add',
|
||||
value: 'add',
|
||||
description: 'Add to memory',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should suggest all sub-commands when the query ends with the parent command and a space', async () => {
|
||||
const slashCommands = [
|
||||
{
|
||||
createTestCommand({
|
||||
name: 'memory',
|
||||
description: 'Manage memory',
|
||||
subCommands: [
|
||||
{ name: 'show', description: 'Show memory' },
|
||||
{ name: 'add', description: 'Add to memory' },
|
||||
createTestCommand({ name: 'show', description: 'Show memory' }),
|
||||
createTestCommand({ name: 'add', description: 'Add to memory' }),
|
||||
],
|
||||
},
|
||||
] as unknown as SlashCommand[];
|
||||
}),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
@@ -257,23 +438,33 @@ describe('useSlashCompletion', () => {
|
||||
expect(result.current.suggestions).toHaveLength(2);
|
||||
expect(result.current.suggestions).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ label: 'show', value: 'show', description: 'Show memory' },
|
||||
{ label: 'add', value: 'add', description: 'Add to memory' },
|
||||
{
|
||||
label: 'show',
|
||||
value: 'show',
|
||||
description: 'Show memory',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
{
|
||||
label: 'add',
|
||||
value: 'add',
|
||||
description: 'Add to memory',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter sub-commands by prefix', async () => {
|
||||
const slashCommands = [
|
||||
{
|
||||
createTestCommand({
|
||||
name: 'memory',
|
||||
description: 'Manage memory',
|
||||
subCommands: [
|
||||
{ name: 'show', description: 'Show memory' },
|
||||
{ name: 'add', description: 'Add to memory' },
|
||||
createTestCommand({ name: 'show', description: 'Show memory' }),
|
||||
createTestCommand({ name: 'add', description: 'Add to memory' }),
|
||||
],
|
||||
},
|
||||
] as unknown as SlashCommand[];
|
||||
}),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
@@ -283,22 +474,29 @@ describe('useSlashCompletion', () => {
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{ label: 'add', value: 'add', description: 'Add to memory' },
|
||||
]);
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{
|
||||
label: 'add',
|
||||
value: 'add',
|
||||
description: 'Add to memory',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide no suggestions for an invalid sub-command', async () => {
|
||||
const slashCommands = [
|
||||
{
|
||||
createTestCommand({
|
||||
name: 'memory',
|
||||
description: 'Manage memory',
|
||||
subCommands: [
|
||||
{ name: 'show', description: 'Show memory' },
|
||||
{ name: 'add', description: 'Add to memory' },
|
||||
createTestCommand({ name: 'show', description: 'Show memory' }),
|
||||
createTestCommand({ name: 'add', description: 'Add to memory' }),
|
||||
],
|
||||
},
|
||||
] as unknown as SlashCommand[];
|
||||
}),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
@@ -327,18 +525,18 @@ describe('useSlashCompletion', () => {
|
||||
);
|
||||
|
||||
const slashCommands = [
|
||||
{
|
||||
createTestCommand({
|
||||
name: 'chat',
|
||||
description: 'Manage chat history',
|
||||
subCommands: [
|
||||
{
|
||||
createTestCommand({
|
||||
name: 'resume',
|
||||
description: 'Resume a saved chat',
|
||||
completion: mockCompletionFn,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
] as unknown as SlashCommand[];
|
||||
}),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
@@ -351,7 +549,13 @@ describe('useSlashCompletion', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCompletionFn).toHaveBeenCalledWith(
|
||||
mockCommandContext,
|
||||
expect.objectContaining({
|
||||
invocation: {
|
||||
raw: '/chat resume my-ch',
|
||||
name: 'resume',
|
||||
args: 'my-ch',
|
||||
},
|
||||
}),
|
||||
'my-ch',
|
||||
);
|
||||
});
|
||||
@@ -370,18 +574,18 @@ describe('useSlashCompletion', () => {
|
||||
.mockResolvedValue(['my-chat-tag-1', 'my-chat-tag-2', 'my-channel']);
|
||||
|
||||
const slashCommands = [
|
||||
{
|
||||
createTestCommand({
|
||||
name: 'chat',
|
||||
description: 'Manage chat history',
|
||||
subCommands: [
|
||||
{
|
||||
createTestCommand({
|
||||
name: 'resume',
|
||||
description: 'Resume a saved chat',
|
||||
completion: mockCompletionFn,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
] as unknown as SlashCommand[];
|
||||
}),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
@@ -393,7 +597,16 @@ describe('useSlashCompletion', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCompletionFn).toHaveBeenCalledWith(mockCommandContext, '');
|
||||
expect(mockCompletionFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
invocation: {
|
||||
raw: '/chat resume',
|
||||
name: 'resume',
|
||||
args: '',
|
||||
},
|
||||
}),
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -404,18 +617,18 @@ describe('useSlashCompletion', () => {
|
||||
it('should handle completion function that returns null', async () => {
|
||||
const completionFn = vi.fn().mockResolvedValue(null);
|
||||
const slashCommands = [
|
||||
{
|
||||
createTestCommand({
|
||||
name: 'chat',
|
||||
description: 'Manage chat history',
|
||||
subCommands: [
|
||||
{
|
||||
createTestCommand({
|
||||
name: 'resume',
|
||||
description: 'Resume a saved chat',
|
||||
completion: completionFn,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
] as unknown as SlashCommand[];
|
||||
}),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
@@ -431,4 +644,210 @@ describe('useSlashCompletion', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Command Kind Information', () => {
|
||||
it('should include commandKind for MCP commands in suggestions', async () => {
|
||||
const slashCommands = [
|
||||
{
|
||||
name: 'summarize',
|
||||
description: 'Summarize content',
|
||||
kind: CommandKind.MCP_PROMPT,
|
||||
action: vi.fn(),
|
||||
},
|
||||
{
|
||||
name: 'help',
|
||||
description: 'Show help',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: vi.fn(),
|
||||
},
|
||||
] as SlashCommand[];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.suggestions).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
label: 'summarize',
|
||||
value: 'summarize',
|
||||
description: 'Summarize content',
|
||||
commandKind: CommandKind.MCP_PROMPT,
|
||||
},
|
||||
{
|
||||
label: 'help',
|
||||
value: 'help',
|
||||
description: 'Show help',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include commandKind when filtering MCP commands by prefix', async () => {
|
||||
const slashCommands = [
|
||||
{
|
||||
name: 'summarize',
|
||||
description: 'Summarize content',
|
||||
kind: CommandKind.MCP_PROMPT,
|
||||
action: vi.fn(),
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
description: 'Open settings',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: vi.fn(),
|
||||
},
|
||||
] as SlashCommand[];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/summ',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{
|
||||
label: 'summarize',
|
||||
value: 'summarize',
|
||||
description: 'Summarize content',
|
||||
commandKind: CommandKind.MCP_PROMPT,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include commandKind for sub-commands', async () => {
|
||||
const slashCommands = [
|
||||
{
|
||||
name: 'memory',
|
||||
description: 'Manage memory',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [
|
||||
{
|
||||
name: 'show',
|
||||
description: 'Show memory',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: vi.fn(),
|
||||
},
|
||||
{
|
||||
name: 'add',
|
||||
description: 'Add to memory',
|
||||
kind: CommandKind.MCP_PROMPT,
|
||||
action: vi.fn(),
|
||||
},
|
||||
],
|
||||
},
|
||||
] as SlashCommand[];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/memory',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.suggestions).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
label: 'show',
|
||||
value: 'show',
|
||||
description: 'Show memory',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
{
|
||||
label: 'add',
|
||||
value: 'add',
|
||||
description: 'Add to memory',
|
||||
commandKind: CommandKind.MCP_PROMPT,
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include commandKind for file commands', async () => {
|
||||
const slashCommands = [
|
||||
{
|
||||
name: 'custom-script',
|
||||
description: 'Run custom script',
|
||||
kind: CommandKind.FILE,
|
||||
action: vi.fn(),
|
||||
},
|
||||
] as SlashCommand[];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/custom',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{
|
||||
label: 'custom-script',
|
||||
value: 'custom-script',
|
||||
description: 'Run custom script',
|
||||
commandKind: CommandKind.FILE,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call shared callbacks when disabled', () => {
|
||||
const mockSetSuggestions = vi.fn();
|
||||
const mockSetIsLoadingSuggestions = vi.fn();
|
||||
const mockSetIsPerfectMatch = vi.fn();
|
||||
|
||||
const slashCommands = [
|
||||
createTestCommand({
|
||||
name: 'help',
|
||||
description: 'Show help',
|
||||
}),
|
||||
];
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ enabled, query }) =>
|
||||
useSlashCompletion({
|
||||
enabled,
|
||||
query,
|
||||
slashCommands,
|
||||
commandContext: mockCommandContext,
|
||||
setSuggestions: mockSetSuggestions,
|
||||
setIsLoadingSuggestions: mockSetIsLoadingSuggestions,
|
||||
setIsPerfectMatch: mockSetIsPerfectMatch,
|
||||
}),
|
||||
{
|
||||
initialProps: { enabled: false, query: '@src/file' },
|
||||
},
|
||||
);
|
||||
|
||||
// Clear any initial calls
|
||||
mockSetSuggestions.mockClear();
|
||||
mockSetIsLoadingSuggestions.mockClear();
|
||||
mockSetIsPerfectMatch.mockClear();
|
||||
|
||||
// Change query while disabled (simulating @ completion typing)
|
||||
rerender({ enabled: false, query: '@src/file.ts' });
|
||||
rerender({ enabled: false, query: '@src/file.tsx' });
|
||||
|
||||
// Should not have called shared callbacks during @ completion typing
|
||||
expect(mockSetSuggestions).not.toHaveBeenCalled();
|
||||
expect(mockSetIsLoadingSuggestions).not.toHaveBeenCalled();
|
||||
expect(mockSetIsPerfectMatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user