@google/gemini-cli 0.3.0-preview.2 → 0.4.0-nightly.20250904.e133acd2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/google-gemini-cli-0.2.2.tgz +0 -0
- package/dist/package.json +5 -4
- package/dist/src/commands/extensions/enable.js +1 -1
- package/dist/src/commands/extensions/enable.js.map +1 -1
- package/dist/src/commands/extensions/examples/context/GEMINI.md +8 -0
- package/dist/src/commands/extensions/examples/context/gemini-extension.json +5 -0
- package/dist/src/commands/extensions/examples/custom-commands/commands/fs/grep-code.toml +6 -0
- package/dist/src/commands/extensions/examples/custom-commands/gemini-extension.json +4 -0
- package/dist/src/commands/extensions/examples/exclude-tools/gemini-extension.json +5 -0
- package/dist/src/commands/extensions/examples/mcp-server/example.d.ts +6 -0
- package/dist/src/commands/extensions/examples/mcp-server/example.js +46 -0
- package/dist/src/commands/extensions/examples/mcp-server/example.js.map +1 -0
- package/dist/src/commands/extensions/examples/mcp-server/example.ts +60 -0
- package/dist/src/commands/extensions/examples/mcp-server/gemini-extension.json +10 -0
- package/dist/src/commands/extensions/install.js +35 -7
- package/dist/src/commands/extensions/install.js.map +1 -1
- package/dist/src/commands/extensions/install.test.js +20 -2
- package/dist/src/commands/extensions/install.test.js.map +1 -1
- package/dist/src/commands/extensions/link.d.ts +12 -0
- package/dist/src/commands/extensions/link.js +37 -0
- package/dist/src/commands/extensions/link.js.map +1 -0
- package/dist/src/commands/extensions/new.d.ts +7 -0
- package/dist/src/commands/extensions/new.js +70 -0
- package/dist/src/commands/extensions/new.js.map +1 -0
- package/dist/src/commands/extensions/new.test.d.ts +6 -0
- package/dist/src/commands/extensions/new.test.js +50 -0
- package/dist/src/commands/extensions/new.test.js.map +1 -0
- package/dist/src/commands/extensions/update.d.ts +2 -1
- package/dist/src/commands/extensions/update.js +36 -15
- package/dist/src/commands/extensions/update.js.map +1 -1
- package/dist/src/commands/extensions.js +4 -0
- package/dist/src/commands/extensions.js.map +1 -1
- package/dist/src/commands/mcp/add.js +1 -1
- package/dist/src/commands/mcp/add.js.map +1 -1
- package/dist/src/commands/mcp/list.js +2 -2
- package/dist/src/commands/mcp/list.js.map +1 -1
- package/dist/src/commands/mcp/remove.js +1 -1
- package/dist/src/commands/mcp/remove.js.map +1 -1
- package/dist/src/config/auth.d.ts +1 -1
- package/dist/src/config/auth.js +4 -4
- package/dist/src/config/auth.js.map +1 -1
- package/dist/src/config/auth.test.js +15 -7
- package/dist/src/config/auth.test.js.map +1 -1
- package/dist/src/config/config.d.ts +4 -1
- package/dist/src/config/config.js +35 -20
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/extension.d.ts +7 -4
- package/dist/src/config/extension.js +76 -24
- package/dist/src/config/extension.js.map +1 -1
- package/dist/src/config/keyBindings.d.ts +1 -0
- package/dist/src/config/keyBindings.js +6 -25
- package/dist/src/config/keyBindings.js.map +1 -1
- package/dist/src/config/settings.d.ts +3 -4
- package/dist/src/config/settings.js +28 -84
- package/dist/src/config/settings.js.map +1 -1
- package/dist/src/config/settingsSchema.d.ts +102 -14
- package/dist/src/config/settingsSchema.js +97 -14
- package/dist/src/config/settingsSchema.js.map +1 -1
- package/dist/src/config/trustedFolders.d.ts +12 -2
- package/dist/src/config/trustedFolders.js +59 -40
- package/dist/src/config/trustedFolders.js.map +1 -1
- package/dist/src/config/trustedFolders.test.js +81 -2
- package/dist/src/config/trustedFolders.test.js.map +1 -1
- package/dist/src/gemini.d.ts +1 -1
- package/dist/src/gemini.js +53 -23
- package/dist/src/gemini.js.map +1 -1
- package/dist/src/gemini.test.js +2 -30
- package/dist/src/gemini.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/generated/git-commit.js.map +1 -1
- package/dist/src/services/FileCommandLoader.d.ts +2 -0
- package/dist/src/services/FileCommandLoader.js +7 -0
- package/dist/src/services/FileCommandLoader.js.map +1 -1
- package/dist/src/ui/App.js +100 -45
- package/dist/src/ui/App.js.map +1 -1
- package/dist/src/ui/commands/directoryCommand.js +1 -1
- package/dist/src/ui/commands/directoryCommand.js.map +1 -1
- package/dist/src/ui/commands/memoryCommand.js +1 -1
- package/dist/src/ui/commands/memoryCommand.js.map +1 -1
- package/dist/src/ui/components/AuthDialog.js +8 -2
- package/dist/src/ui/components/AuthDialog.js.map +1 -1
- package/dist/src/ui/components/AuthDialog.test.js +41 -10
- package/dist/src/ui/components/AuthDialog.test.js.map +1 -1
- package/dist/src/ui/components/FolderTrustDialog.js +3 -1
- package/dist/src/ui/components/FolderTrustDialog.js.map +1 -1
- package/dist/src/ui/components/FolderTrustDialog.test.js +19 -4
- package/dist/src/ui/components/FolderTrustDialog.test.js.map +1 -1
- package/dist/src/ui/components/Footer.d.ts +3 -0
- package/dist/src/ui/components/Footer.js +4 -3
- package/dist/src/ui/components/Footer.js.map +1 -1
- package/dist/src/ui/components/GeminiRespondingSpinner.js +5 -3
- package/dist/src/ui/components/GeminiRespondingSpinner.js.map +1 -1
- package/dist/src/ui/components/InputPrompt.js +35 -25
- package/dist/src/ui/components/InputPrompt.js.map +1 -1
- package/dist/src/ui/components/MemoryUsageDisplay.js +1 -1
- package/dist/src/ui/components/MemoryUsageDisplay.js.map +1 -1
- package/dist/src/ui/components/ProQuotaDialog.d.ts +13 -0
- package/dist/src/ui/components/ProQuotaDialog.js +21 -0
- package/dist/src/ui/components/ProQuotaDialog.js.map +1 -0
- package/dist/src/ui/components/ProQuotaDialog.test.d.ts +6 -0
- package/dist/src/ui/components/ProQuotaDialog.test.js +56 -0
- package/dist/src/ui/components/ProQuotaDialog.test.js.map +1 -0
- package/dist/src/ui/components/SettingsDialog.test.js +2 -2
- package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
- package/dist/src/ui/components/messages/CompressionMessage.js +1 -1
- package/dist/src/ui/components/messages/CompressionMessage.js.map +1 -1
- package/dist/src/ui/components/messages/DiffRenderer.js +5 -1
- package/dist/src/ui/components/messages/DiffRenderer.js.map +1 -1
- package/dist/src/ui/components/messages/GeminiMessage.js +1 -1
- package/dist/src/ui/components/messages/GeminiMessage.js.map +1 -1
- package/dist/src/ui/components/messages/ToolConfirmationMessage.js +1 -1
- package/dist/src/ui/components/messages/ToolConfirmationMessage.js.map +1 -1
- package/dist/src/ui/components/messages/ToolConfirmationMessage.test.js +0 -8
- package/dist/src/ui/components/messages/ToolConfirmationMessage.test.js.map +1 -1
- package/dist/src/ui/components/messages/ToolMessage.js +1 -1
- package/dist/src/ui/components/messages/ToolMessage.js.map +1 -1
- package/dist/src/ui/components/messages/UserMessage.js +1 -1
- package/dist/src/ui/components/messages/UserMessage.js.map +1 -1
- package/dist/src/ui/components/shared/MaxSizedBox.js.map +1 -1
- package/dist/src/ui/components/shared/RadioButtonSelect.js +1 -1
- package/dist/src/ui/components/shared/RadioButtonSelect.js.map +1 -1
- package/dist/src/ui/components/shared/text-buffer.js +35 -51
- package/dist/src/ui/components/shared/text-buffer.js.map +1 -1
- package/dist/src/ui/constants.d.ts +0 -2
- package/dist/src/ui/constants.js +0 -2
- package/dist/src/ui/constants.js.map +1 -1
- package/dist/src/ui/contexts/KeypressContext.js +255 -42
- package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
- package/dist/src/ui/contexts/KeypressContext.test.js +115 -1
- package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
- package/dist/src/ui/hooks/slashCommandProcessor.js +2 -2
- package/dist/src/ui/hooks/slashCommandProcessor.js.map +1 -1
- package/dist/src/ui/hooks/useFolderTrust.js +6 -4
- package/dist/src/ui/hooks/useFolderTrust.js.map +1 -1
- package/dist/src/ui/hooks/useGeminiStream.d.ts +3 -2
- package/dist/src/ui/hooks/useGeminiStream.js +28 -3
- package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
- package/dist/src/ui/hooks/useIdeTrustListener.d.ts +15 -0
- package/dist/src/ui/hooks/useIdeTrustListener.js +34 -0
- package/dist/src/ui/hooks/useIdeTrustListener.js.map +1 -0
- package/dist/src/ui/hooks/useInputHistoryStore.d.ts +19 -0
- package/dist/src/ui/hooks/useInputHistoryStore.js +81 -0
- package/dist/src/ui/hooks/useInputHistoryStore.js.map +1 -0
- package/dist/src/ui/hooks/useInputHistoryStore.test.d.ts +6 -0
- package/dist/src/ui/hooks/useInputHistoryStore.test.js +234 -0
- package/dist/src/ui/hooks/useInputHistoryStore.test.js.map +1 -0
- package/dist/src/ui/hooks/useLoadingIndicator.d.ts +1 -1
- package/dist/src/ui/hooks/useLoadingIndicator.js +2 -2
- package/dist/src/ui/hooks/useLoadingIndicator.js.map +1 -1
- package/dist/src/ui/hooks/useLoadingIndicator.test.js +2 -2
- package/dist/src/ui/hooks/useLoadingIndicator.test.js.map +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.d.ts +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.js +11 -8
- package/dist/src/ui/hooks/usePhraseCycler.js.map +1 -1
- package/dist/src/ui/hooks/usePrivacySettings.d.ts +1 -1
- package/dist/src/ui/hooks/usePrivacySettings.js +8 -13
- package/dist/src/ui/hooks/usePrivacySettings.js.map +1 -1
- package/dist/src/ui/hooks/usePrivacySettings.test.js +33 -97
- package/dist/src/ui/hooks/usePrivacySettings.test.js.map +1 -1
- package/dist/src/ui/hooks/useSlashCompletion.js +263 -67
- package/dist/src/ui/hooks/useSlashCompletion.js.map +1 -1
- package/dist/src/ui/hooks/useSlashCompletion.test.d.ts +4 -1
- package/dist/src/ui/hooks/useSlashCompletion.test.js +452 -59
- package/dist/src/ui/hooks/useSlashCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useToolScheduler.test.js +57 -59
- package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
- package/dist/src/ui/keyMatchers.test.js +9 -0
- package/dist/src/ui/keyMatchers.test.js.map +1 -1
- package/dist/src/ui/textConstants.d.ts +9 -0
- package/dist/src/ui/textConstants.js +10 -0
- package/dist/src/ui/textConstants.js.map +1 -0
- package/dist/src/ui/utils/MarkdownDisplay.test.js +2 -2
- package/dist/src/ui/utils/MarkdownDisplay.test.js.map +1 -1
- package/dist/src/ui/utils/highlight.d.ts +10 -0
- package/dist/src/ui/utils/highlight.js +41 -0
- package/dist/src/ui/utils/highlight.js.map +1 -0
- package/dist/src/ui/utils/highlight.test.d.ts +6 -0
- package/dist/src/ui/utils/highlight.test.js +93 -0
- package/dist/src/ui/utils/highlight.test.js.map +1 -0
- package/dist/src/ui/utils/platformConstants.d.ts +24 -1
- package/dist/src/ui/utils/platformConstants.js +26 -1
- package/dist/src/ui/utils/platformConstants.js.map +1 -1
- package/dist/src/utils/envVarResolver.d.ts +39 -0
- package/dist/src/utils/envVarResolver.js +97 -0
- package/dist/src/utils/envVarResolver.js.map +1 -0
- package/dist/src/utils/envVarResolver.test.d.ts +6 -0
- package/dist/src/utils/envVarResolver.test.js +221 -0
- package/dist/src/utils/envVarResolver.test.js.map +1 -0
- package/dist/src/utils/userStartupWarnings.d.ts +1 -1
- package/dist/src/utils/userStartupWarnings.js +1 -1
- package/dist/src/utils/userStartupWarnings.js.map +1 -1
- package/dist/src/validateNonInterActiveAuth.d.ts +2 -1
- package/dist/src/validateNonInterActiveAuth.js +11 -2
- package/dist/src/validateNonInterActiveAuth.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/dist/google-gemini-cli-0.3.0-preview.1.tgz +0 -0
|
@@ -7,7 +7,105 @@
|
|
|
7
7
|
import { describe, it, expect, vi } from 'vitest';
|
|
8
8
|
import { renderHook, waitFor } from '@testing-library/react';
|
|
9
9
|
import { useSlashCompletion } from './useSlashCompletion.js';
|
|
10
|
+
import { CommandKind } from '../commands/types.js';
|
|
10
11
|
import { useState } from 'react';
|
|
12
|
+
function createTestCommand(command) {
|
|
13
|
+
return {
|
|
14
|
+
kind: CommandKind.BUILT_IN, // default for tests
|
|
15
|
+
...command,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// Track AsyncFzf constructor calls for cache testing
|
|
19
|
+
let asyncFzfConstructorCalls = 0;
|
|
20
|
+
const resetConstructorCallCount = () => {
|
|
21
|
+
asyncFzfConstructorCalls = 0;
|
|
22
|
+
};
|
|
23
|
+
const getConstructorCallCount = () => asyncFzfConstructorCalls;
|
|
24
|
+
// Centralized fuzzy matching simulation logic
|
|
25
|
+
// Note: This is a simplified reimplementation that may diverge from real fzf behavior.
|
|
26
|
+
// Integration tests in useSlashCompletion.integration.test.ts use the real fzf library
|
|
27
|
+
// to catch any behavioral differences and serve as our "canary in a coal mine."
|
|
28
|
+
function simulateFuzzyMatching(items, query) {
|
|
29
|
+
const results = [];
|
|
30
|
+
if (query) {
|
|
31
|
+
const lowerQuery = query.toLowerCase();
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
const lowerItem = item.toLowerCase();
|
|
34
|
+
// Exact match gets highest score
|
|
35
|
+
if (lowerItem === lowerQuery) {
|
|
36
|
+
results.push({
|
|
37
|
+
item,
|
|
38
|
+
positions: [],
|
|
39
|
+
score: 100,
|
|
40
|
+
start: 0,
|
|
41
|
+
end: item.length,
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// Prefix match gets high score
|
|
46
|
+
if (lowerItem.startsWith(lowerQuery)) {
|
|
47
|
+
results.push({
|
|
48
|
+
item,
|
|
49
|
+
positions: [],
|
|
50
|
+
score: 80,
|
|
51
|
+
start: 0,
|
|
52
|
+
end: query.length,
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// Fuzzy matching: check if query chars appear in order
|
|
57
|
+
let queryIndex = 0;
|
|
58
|
+
let score = 0;
|
|
59
|
+
for (let i = 0; i < lowerItem.length && queryIndex < lowerQuery.length; i++) {
|
|
60
|
+
if (lowerItem[i] === lowerQuery[queryIndex]) {
|
|
61
|
+
queryIndex++;
|
|
62
|
+
score += 10 - i; // Earlier matches get higher scores
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// If all query characters were found in order, include this item
|
|
66
|
+
if (queryIndex === lowerQuery.length) {
|
|
67
|
+
results.push({
|
|
68
|
+
item,
|
|
69
|
+
positions: [],
|
|
70
|
+
score,
|
|
71
|
+
start: 0,
|
|
72
|
+
end: query.length,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Sort by score descending (better matches first)
|
|
78
|
+
results.sort((a, b) => b.score - a.score);
|
|
79
|
+
return Promise.resolve(results);
|
|
80
|
+
}
|
|
81
|
+
// Mock the fzf module to provide a working fuzzy search implementation for tests
|
|
82
|
+
vi.mock('fzf', async () => {
|
|
83
|
+
const actual = await vi.importActual('fzf');
|
|
84
|
+
return {
|
|
85
|
+
...actual,
|
|
86
|
+
AsyncFzf: vi.fn().mockImplementation((items, _options) => {
|
|
87
|
+
asyncFzfConstructorCalls++;
|
|
88
|
+
return {
|
|
89
|
+
find: vi
|
|
90
|
+
.fn()
|
|
91
|
+
.mockImplementation((query) => simulateFuzzyMatching(items, query)),
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
};
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
// Default mock behavior helper - now uses centralized logic
|
|
98
|
+
const createDefaultAsyncFzfMock = () => (items, _options) => {
|
|
99
|
+
asyncFzfConstructorCalls++;
|
|
100
|
+
return {
|
|
101
|
+
find: vi
|
|
102
|
+
.fn()
|
|
103
|
+
.mockImplementation((query) => simulateFuzzyMatching(items, query)),
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
// Export test utilities
|
|
108
|
+
export { resetConstructorCallCount, getConstructorCallCount, createDefaultAsyncFzfMock, };
|
|
11
109
|
// Test harness to capture the state from the hook's callbacks.
|
|
12
110
|
function useTestHarnessForSlashCompletion(enabled, query, slashCommands, commandContext) {
|
|
13
111
|
const [suggestions, setSuggestions] = useState([]);
|
|
@@ -36,19 +134,25 @@ describe('useSlashCompletion', () => {
|
|
|
36
134
|
describe('Top-Level Commands', () => {
|
|
37
135
|
it('should suggest all top-level commands for the root slash', async () => {
|
|
38
136
|
const slashCommands = [
|
|
39
|
-
{
|
|
40
|
-
|
|
137
|
+
createTestCommand({
|
|
138
|
+
name: 'help',
|
|
139
|
+
altNames: ['?'],
|
|
140
|
+
description: 'Show help',
|
|
141
|
+
}),
|
|
142
|
+
createTestCommand({
|
|
41
143
|
name: 'stats',
|
|
42
144
|
altNames: ['usage'],
|
|
43
145
|
description: 'check session stats. Usage: /stats [model|tools]',
|
|
44
|
-
},
|
|
45
|
-
{ name: 'clear', description: 'Clear the screen' },
|
|
46
|
-
{
|
|
146
|
+
}),
|
|
147
|
+
createTestCommand({ name: 'clear', description: 'Clear the screen' }),
|
|
148
|
+
createTestCommand({
|
|
47
149
|
name: 'memory',
|
|
48
150
|
description: 'Manage memory',
|
|
49
|
-
subCommands: [
|
|
50
|
-
|
|
51
|
-
|
|
151
|
+
subCommands: [
|
|
152
|
+
createTestCommand({ name: 'show', description: 'Show memory' }),
|
|
153
|
+
],
|
|
154
|
+
}),
|
|
155
|
+
createTestCommand({ name: 'chat', description: 'Manage chat history' }),
|
|
52
156
|
];
|
|
53
157
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/', slashCommands, mockCommandContext));
|
|
54
158
|
expect(result.current.suggestions.length).toBe(slashCommands.length);
|
|
@@ -56,65 +160,73 @@ describe('useSlashCompletion', () => {
|
|
|
56
160
|
});
|
|
57
161
|
it('should filter commands based on partial input', async () => {
|
|
58
162
|
const slashCommands = [
|
|
59
|
-
{ name: 'memory', description: 'Manage memory' },
|
|
163
|
+
createTestCommand({ name: 'memory', description: 'Manage memory' }),
|
|
60
164
|
];
|
|
61
165
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/mem', slashCommands, mockCommandContext));
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
166
|
+
await waitFor(() => {
|
|
167
|
+
expect(result.current.suggestions).toEqual([
|
|
168
|
+
{ label: 'memory', value: 'memory', description: 'Manage memory' },
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
65
171
|
});
|
|
66
172
|
it('should suggest commands based on partial altNames', async () => {
|
|
67
173
|
const slashCommands = [
|
|
68
|
-
{
|
|
174
|
+
createTestCommand({
|
|
69
175
|
name: 'stats',
|
|
70
176
|
altNames: ['usage'],
|
|
71
177
|
description: 'check session stats. Usage: /stats [model|tools]',
|
|
72
|
-
},
|
|
178
|
+
}),
|
|
73
179
|
];
|
|
74
180
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/usag', slashCommands, mockCommandContext));
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
181
|
+
await waitFor(() => {
|
|
182
|
+
expect(result.current.suggestions).toEqual([
|
|
183
|
+
{
|
|
184
|
+
label: 'stats',
|
|
185
|
+
value: 'stats',
|
|
186
|
+
description: 'check session stats. Usage: /stats [model|tools]',
|
|
187
|
+
},
|
|
188
|
+
]);
|
|
189
|
+
});
|
|
82
190
|
});
|
|
83
191
|
it('should NOT provide suggestions for a perfectly typed command that is a leaf node', async () => {
|
|
84
192
|
const slashCommands = [
|
|
85
|
-
{
|
|
193
|
+
createTestCommand({
|
|
194
|
+
name: 'clear',
|
|
195
|
+
description: 'Clear the screen',
|
|
196
|
+
action: vi.fn(),
|
|
197
|
+
}),
|
|
86
198
|
];
|
|
87
199
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/clear', slashCommands, mockCommandContext));
|
|
88
200
|
expect(result.current.suggestions).toHaveLength(0);
|
|
89
201
|
});
|
|
90
202
|
it.each([['/?'], ['/usage']])('should not suggest commands when altNames is fully typed', async (query) => {
|
|
91
203
|
const mockSlashCommands = [
|
|
92
|
-
{
|
|
204
|
+
createTestCommand({
|
|
93
205
|
name: 'help',
|
|
94
206
|
altNames: ['?'],
|
|
95
207
|
description: 'Show help',
|
|
96
208
|
action: vi.fn(),
|
|
97
|
-
},
|
|
98
|
-
{
|
|
209
|
+
}),
|
|
210
|
+
createTestCommand({
|
|
99
211
|
name: 'stats',
|
|
100
212
|
altNames: ['usage'],
|
|
101
213
|
description: 'check session stats. Usage: /stats [model|tools]',
|
|
102
214
|
action: vi.fn(),
|
|
103
|
-
},
|
|
215
|
+
}),
|
|
104
216
|
];
|
|
105
217
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, query, mockSlashCommands, mockCommandContext));
|
|
106
218
|
expect(result.current.suggestions).toHaveLength(0);
|
|
107
219
|
});
|
|
108
220
|
it('should not provide suggestions for a fully typed command that has no sub-commands or argument completion', async () => {
|
|
109
221
|
const slashCommands = [
|
|
110
|
-
{ name: 'clear', description: 'Clear the screen' },
|
|
222
|
+
createTestCommand({ name: 'clear', description: 'Clear the screen' }),
|
|
111
223
|
];
|
|
112
224
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/clear ', slashCommands, mockCommandContext));
|
|
113
225
|
expect(result.current.suggestions).toHaveLength(0);
|
|
114
226
|
});
|
|
115
227
|
it('should not provide suggestions for an unknown command', async () => {
|
|
116
228
|
const slashCommands = [
|
|
117
|
-
{ name: 'help', description: 'Show help' },
|
|
229
|
+
createTestCommand({ name: 'help', description: 'Show help' }),
|
|
118
230
|
];
|
|
119
231
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/unknown-command', slashCommands, mockCommandContext));
|
|
120
232
|
expect(result.current.suggestions).toHaveLength(0);
|
|
@@ -123,14 +235,14 @@ describe('useSlashCompletion', () => {
|
|
|
123
235
|
describe('Sub-Commands', () => {
|
|
124
236
|
it('should suggest sub-commands for a parent command', async () => {
|
|
125
237
|
const slashCommands = [
|
|
126
|
-
{
|
|
238
|
+
createTestCommand({
|
|
127
239
|
name: 'memory',
|
|
128
240
|
description: 'Manage memory',
|
|
129
241
|
subCommands: [
|
|
130
|
-
{ name: 'show', description: 'Show memory' },
|
|
131
|
-
{ name: 'add', description: 'Add to memory' },
|
|
242
|
+
createTestCommand({ name: 'show', description: 'Show memory' }),
|
|
243
|
+
createTestCommand({ name: 'add', description: 'Add to memory' }),
|
|
132
244
|
],
|
|
133
|
-
},
|
|
245
|
+
}),
|
|
134
246
|
];
|
|
135
247
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/memory', slashCommands, mockCommandContext));
|
|
136
248
|
expect(result.current.suggestions).toHaveLength(2);
|
|
@@ -141,14 +253,14 @@ describe('useSlashCompletion', () => {
|
|
|
141
253
|
});
|
|
142
254
|
it('should suggest all sub-commands when the query ends with the parent command and a space', async () => {
|
|
143
255
|
const slashCommands = [
|
|
144
|
-
{
|
|
256
|
+
createTestCommand({
|
|
145
257
|
name: 'memory',
|
|
146
258
|
description: 'Manage memory',
|
|
147
259
|
subCommands: [
|
|
148
|
-
{ name: 'show', description: 'Show memory' },
|
|
149
|
-
{ name: 'add', description: 'Add to memory' },
|
|
260
|
+
createTestCommand({ name: 'show', description: 'Show memory' }),
|
|
261
|
+
createTestCommand({ name: 'add', description: 'Add to memory' }),
|
|
150
262
|
],
|
|
151
|
-
},
|
|
263
|
+
}),
|
|
152
264
|
];
|
|
153
265
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/memory ', slashCommands, mockCommandContext));
|
|
154
266
|
expect(result.current.suggestions).toHaveLength(2);
|
|
@@ -159,30 +271,32 @@ describe('useSlashCompletion', () => {
|
|
|
159
271
|
});
|
|
160
272
|
it('should filter sub-commands by prefix', async () => {
|
|
161
273
|
const slashCommands = [
|
|
162
|
-
{
|
|
274
|
+
createTestCommand({
|
|
163
275
|
name: 'memory',
|
|
164
276
|
description: 'Manage memory',
|
|
165
277
|
subCommands: [
|
|
166
|
-
{ name: 'show', description: 'Show memory' },
|
|
167
|
-
{ name: 'add', description: 'Add to memory' },
|
|
278
|
+
createTestCommand({ name: 'show', description: 'Show memory' }),
|
|
279
|
+
createTestCommand({ name: 'add', description: 'Add to memory' }),
|
|
168
280
|
],
|
|
169
|
-
},
|
|
281
|
+
}),
|
|
170
282
|
];
|
|
171
283
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/memory a', slashCommands, mockCommandContext));
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
284
|
+
await waitFor(() => {
|
|
285
|
+
expect(result.current.suggestions).toEqual([
|
|
286
|
+
{ label: 'add', value: 'add', description: 'Add to memory' },
|
|
287
|
+
]);
|
|
288
|
+
});
|
|
175
289
|
});
|
|
176
290
|
it('should provide no suggestions for an invalid sub-command', async () => {
|
|
177
291
|
const slashCommands = [
|
|
178
|
-
{
|
|
292
|
+
createTestCommand({
|
|
179
293
|
name: 'memory',
|
|
180
294
|
description: 'Manage memory',
|
|
181
295
|
subCommands: [
|
|
182
|
-
{ name: 'show', description: 'Show memory' },
|
|
183
|
-
{ name: 'add', description: 'Add to memory' },
|
|
296
|
+
createTestCommand({ name: 'show', description: 'Show memory' }),
|
|
297
|
+
createTestCommand({ name: 'add', description: 'Add to memory' }),
|
|
184
298
|
],
|
|
185
|
-
},
|
|
299
|
+
}),
|
|
186
300
|
];
|
|
187
301
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/memory dothisnow', slashCommands, mockCommandContext));
|
|
188
302
|
expect(result.current.suggestions).toHaveLength(0);
|
|
@@ -199,17 +313,17 @@ describe('useSlashCompletion', () => {
|
|
|
199
313
|
.fn()
|
|
200
314
|
.mockImplementation(async (_context, partialArg) => availableTags.filter((tag) => tag.startsWith(partialArg)));
|
|
201
315
|
const slashCommands = [
|
|
202
|
-
{
|
|
316
|
+
createTestCommand({
|
|
203
317
|
name: 'chat',
|
|
204
318
|
description: 'Manage chat history',
|
|
205
319
|
subCommands: [
|
|
206
|
-
{
|
|
320
|
+
createTestCommand({
|
|
207
321
|
name: 'resume',
|
|
208
322
|
description: 'Resume a saved chat',
|
|
209
323
|
completion: mockCompletionFn,
|
|
210
|
-
},
|
|
324
|
+
}),
|
|
211
325
|
],
|
|
212
|
-
},
|
|
326
|
+
}),
|
|
213
327
|
];
|
|
214
328
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/chat resume my-ch', slashCommands, mockCommandContext));
|
|
215
329
|
await waitFor(() => {
|
|
@@ -227,17 +341,17 @@ describe('useSlashCompletion', () => {
|
|
|
227
341
|
.fn()
|
|
228
342
|
.mockResolvedValue(['my-chat-tag-1', 'my-chat-tag-2', 'my-channel']);
|
|
229
343
|
const slashCommands = [
|
|
230
|
-
{
|
|
344
|
+
createTestCommand({
|
|
231
345
|
name: 'chat',
|
|
232
346
|
description: 'Manage chat history',
|
|
233
347
|
subCommands: [
|
|
234
|
-
{
|
|
348
|
+
createTestCommand({
|
|
235
349
|
name: 'resume',
|
|
236
350
|
description: 'Resume a saved chat',
|
|
237
351
|
completion: mockCompletionFn,
|
|
238
|
-
},
|
|
352
|
+
}),
|
|
239
353
|
],
|
|
240
|
-
},
|
|
354
|
+
}),
|
|
241
355
|
];
|
|
242
356
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/chat resume ', slashCommands, mockCommandContext));
|
|
243
357
|
await waitFor(() => {
|
|
@@ -250,17 +364,17 @@ describe('useSlashCompletion', () => {
|
|
|
250
364
|
it('should handle completion function that returns null', async () => {
|
|
251
365
|
const completionFn = vi.fn().mockResolvedValue(null);
|
|
252
366
|
const slashCommands = [
|
|
253
|
-
{
|
|
367
|
+
createTestCommand({
|
|
254
368
|
name: 'chat',
|
|
255
369
|
description: 'Manage chat history',
|
|
256
370
|
subCommands: [
|
|
257
|
-
{
|
|
371
|
+
createTestCommand({
|
|
258
372
|
name: 'resume',
|
|
259
373
|
description: 'Resume a saved chat',
|
|
260
374
|
completion: completionFn,
|
|
261
|
-
},
|
|
375
|
+
}),
|
|
262
376
|
],
|
|
263
|
-
},
|
|
377
|
+
}),
|
|
264
378
|
];
|
|
265
379
|
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/chat resume ', slashCommands, mockCommandContext));
|
|
266
380
|
await waitFor(() => {
|
|
@@ -268,5 +382,284 @@ describe('useSlashCompletion', () => {
|
|
|
268
382
|
});
|
|
269
383
|
});
|
|
270
384
|
});
|
|
385
|
+
describe('Fuzzy Matching', () => {
|
|
386
|
+
const fuzzyTestCommands = [
|
|
387
|
+
createTestCommand({
|
|
388
|
+
name: 'help',
|
|
389
|
+
altNames: ['?'],
|
|
390
|
+
description: 'Show help',
|
|
391
|
+
}),
|
|
392
|
+
createTestCommand({
|
|
393
|
+
name: 'history',
|
|
394
|
+
description: 'Show command history',
|
|
395
|
+
}),
|
|
396
|
+
createTestCommand({ name: 'hello', description: 'Hello world command' }),
|
|
397
|
+
createTestCommand({
|
|
398
|
+
name: 'config',
|
|
399
|
+
altNames: ['configure'],
|
|
400
|
+
description: 'Configure settings',
|
|
401
|
+
}),
|
|
402
|
+
createTestCommand({ name: 'clear', description: 'Clear the screen' }),
|
|
403
|
+
];
|
|
404
|
+
it('should match commands with fuzzy search for partial queries', async () => {
|
|
405
|
+
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/he', fuzzyTestCommands, mockCommandContext));
|
|
406
|
+
await waitFor(() => {
|
|
407
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
408
|
+
});
|
|
409
|
+
const labels = result.current.suggestions.map((s) => s.label);
|
|
410
|
+
expect(labels).toEqual(expect.arrayContaining(['help', 'hello']));
|
|
411
|
+
});
|
|
412
|
+
it('should handle case-insensitive fuzzy matching', async () => {
|
|
413
|
+
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/HeLp', fuzzyTestCommands, mockCommandContext));
|
|
414
|
+
await waitFor(() => {
|
|
415
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
416
|
+
});
|
|
417
|
+
const labels = result.current.suggestions.map((s) => s.label);
|
|
418
|
+
expect(labels).toContain('help');
|
|
419
|
+
});
|
|
420
|
+
it('should provide typo-tolerant matching', async () => {
|
|
421
|
+
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/hlp', fuzzyTestCommands, mockCommandContext));
|
|
422
|
+
await waitFor(() => {
|
|
423
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
424
|
+
});
|
|
425
|
+
const labels = result.current.suggestions.map((s) => s.label);
|
|
426
|
+
expect(labels).toContain('help');
|
|
427
|
+
});
|
|
428
|
+
it('should match against alternative names with fuzzy search', async () => {
|
|
429
|
+
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/conf', fuzzyTestCommands, mockCommandContext));
|
|
430
|
+
await waitFor(() => {
|
|
431
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
432
|
+
});
|
|
433
|
+
const labels = result.current.suggestions.map((s) => s.label);
|
|
434
|
+
expect(labels).toContain('config');
|
|
435
|
+
});
|
|
436
|
+
it('should fallback to prefix matching when AsyncFzf find fails', async () => {
|
|
437
|
+
// Mock console.error to avoid noise in test output
|
|
438
|
+
const consoleErrorSpy = vi
|
|
439
|
+
.spyOn(console, 'error')
|
|
440
|
+
.mockImplementation(() => { });
|
|
441
|
+
// Import the mocked AsyncFzf
|
|
442
|
+
const { AsyncFzf } = await import('fzf');
|
|
443
|
+
// Create a failing find method for this specific test
|
|
444
|
+
const mockFind = vi
|
|
445
|
+
.fn()
|
|
446
|
+
.mockRejectedValue(new Error('AsyncFzf find failed'));
|
|
447
|
+
// Mock AsyncFzf to return an instance with failing find
|
|
448
|
+
vi.mocked(AsyncFzf).mockImplementation((_items, _options) => ({
|
|
449
|
+
finder: vi.fn(),
|
|
450
|
+
find: mockFind,
|
|
451
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
452
|
+
}));
|
|
453
|
+
const testCommands = [
|
|
454
|
+
createTestCommand({ name: 'clear', description: 'Clear the screen' }),
|
|
455
|
+
createTestCommand({
|
|
456
|
+
name: 'config',
|
|
457
|
+
description: 'Configure settings',
|
|
458
|
+
}),
|
|
459
|
+
createTestCommand({ name: 'chat', description: 'Start chat' }),
|
|
460
|
+
];
|
|
461
|
+
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/cle', testCommands, mockCommandContext));
|
|
462
|
+
await waitFor(() => {
|
|
463
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
464
|
+
});
|
|
465
|
+
// Should still get suggestions via prefix matching fallback
|
|
466
|
+
const labels = result.current.suggestions.map((s) => s.label);
|
|
467
|
+
expect(labels).toContain('clear');
|
|
468
|
+
expect(labels).not.toContain('config'); // Doesn't start with 'cle'
|
|
469
|
+
expect(labels).not.toContain('chat'); // Doesn't start with 'cle'
|
|
470
|
+
// Verify the error was logged
|
|
471
|
+
await waitFor(() => {
|
|
472
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('[Fuzzy search - falling back to prefix matching]', expect.any(Error));
|
|
473
|
+
});
|
|
474
|
+
consoleErrorSpy.mockRestore();
|
|
475
|
+
// Reset AsyncFzf mock to default behavior for other tests
|
|
476
|
+
vi.mocked(AsyncFzf).mockImplementation(createDefaultAsyncFzfMock());
|
|
477
|
+
});
|
|
478
|
+
it('should show all commands for empty partial query', async () => {
|
|
479
|
+
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/', fuzzyTestCommands, mockCommandContext));
|
|
480
|
+
expect(result.current.suggestions.length).toBe(fuzzyTestCommands.length);
|
|
481
|
+
});
|
|
482
|
+
it('should handle AsyncFzf errors gracefully and fallback to prefix matching', async () => {
|
|
483
|
+
// Mock console.error to avoid noise in test output
|
|
484
|
+
const consoleErrorSpy = vi
|
|
485
|
+
.spyOn(console, 'error')
|
|
486
|
+
.mockImplementation(() => { });
|
|
487
|
+
// Import the mocked AsyncFzf
|
|
488
|
+
const { AsyncFzf } = await import('fzf');
|
|
489
|
+
// Create a failing find method for this specific test
|
|
490
|
+
const mockFind = vi
|
|
491
|
+
.fn()
|
|
492
|
+
.mockRejectedValue(new Error('AsyncFzf error in find'));
|
|
493
|
+
// Mock AsyncFzf to return an instance with failing find
|
|
494
|
+
vi.mocked(AsyncFzf).mockImplementation((_items, _options) => ({
|
|
495
|
+
finder: vi.fn(),
|
|
496
|
+
find: mockFind,
|
|
497
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
498
|
+
}));
|
|
499
|
+
const testCommands = [
|
|
500
|
+
{ name: 'test', description: 'Test command' },
|
|
501
|
+
{ name: 'temp', description: 'Temporary command' },
|
|
502
|
+
];
|
|
503
|
+
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/te', testCommands, mockCommandContext));
|
|
504
|
+
await waitFor(() => {
|
|
505
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
506
|
+
});
|
|
507
|
+
// Should get suggestions via prefix matching fallback
|
|
508
|
+
const labels = result.current.suggestions.map((s) => s.label);
|
|
509
|
+
expect(labels).toEqual(expect.arrayContaining(['test', 'temp']));
|
|
510
|
+
// Verify the error was logged
|
|
511
|
+
await waitFor(() => {
|
|
512
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('[Fuzzy search - falling back to prefix matching]', expect.any(Error));
|
|
513
|
+
});
|
|
514
|
+
consoleErrorSpy.mockRestore();
|
|
515
|
+
// Reset AsyncFzf mock to default behavior for other tests
|
|
516
|
+
vi.mocked(AsyncFzf).mockImplementation(createDefaultAsyncFzfMock());
|
|
517
|
+
});
|
|
518
|
+
it('should cache AsyncFzf instances for performance', async () => {
|
|
519
|
+
// Reset constructor call count and ensure mock is set up correctly
|
|
520
|
+
resetConstructorCallCount();
|
|
521
|
+
// Import the mocked AsyncFzf
|
|
522
|
+
const { AsyncFzf } = await import('fzf');
|
|
523
|
+
vi.mocked(AsyncFzf).mockImplementation(createDefaultAsyncFzfMock());
|
|
524
|
+
const { result, rerender } = renderHook(({ query }) => useTestHarnessForSlashCompletion(true, query, fuzzyTestCommands, mockCommandContext), { initialProps: { query: '/he' } });
|
|
525
|
+
await waitFor(() => {
|
|
526
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
527
|
+
});
|
|
528
|
+
const firstResults = result.current.suggestions.map((s) => s.label);
|
|
529
|
+
const callCountAfterFirst = getConstructorCallCount();
|
|
530
|
+
expect(callCountAfterFirst).toBeGreaterThan(0);
|
|
531
|
+
// Rerender with same query - should use cached instance
|
|
532
|
+
rerender({ query: '/he' });
|
|
533
|
+
await waitFor(() => {
|
|
534
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
535
|
+
});
|
|
536
|
+
const secondResults = result.current.suggestions.map((s) => s.label);
|
|
537
|
+
const callCountAfterSecond = getConstructorCallCount();
|
|
538
|
+
// Should have same number of constructor calls (reused cached instance)
|
|
539
|
+
expect(callCountAfterSecond).toBe(callCountAfterFirst);
|
|
540
|
+
expect(secondResults).toEqual(firstResults);
|
|
541
|
+
// Different query should still use same cached instance for same command set
|
|
542
|
+
rerender({ query: '/hel' });
|
|
543
|
+
await waitFor(() => {
|
|
544
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
545
|
+
});
|
|
546
|
+
const thirdCallCount = getConstructorCallCount();
|
|
547
|
+
expect(thirdCallCount).toBe(callCountAfterFirst); // Same constructor call count
|
|
548
|
+
});
|
|
549
|
+
it('should not return duplicate suggestions when query matches both name and altNames', async () => {
|
|
550
|
+
const commandsWithAltNames = [
|
|
551
|
+
createTestCommand({
|
|
552
|
+
name: 'config',
|
|
553
|
+
altNames: ['configure', 'conf'],
|
|
554
|
+
description: 'Configure settings',
|
|
555
|
+
}),
|
|
556
|
+
createTestCommand({
|
|
557
|
+
name: 'help',
|
|
558
|
+
altNames: ['?'],
|
|
559
|
+
description: 'Show help',
|
|
560
|
+
}),
|
|
561
|
+
];
|
|
562
|
+
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/con', commandsWithAltNames, mockCommandContext));
|
|
563
|
+
await waitFor(() => {
|
|
564
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
565
|
+
});
|
|
566
|
+
const labels = result.current.suggestions.map((s) => s.label);
|
|
567
|
+
const uniqueLabels = new Set(labels);
|
|
568
|
+
// Should not have duplicates
|
|
569
|
+
expect(labels.length).toBe(uniqueLabels.size);
|
|
570
|
+
expect(labels).toContain('config');
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
describe('Race Condition Handling', () => {
|
|
574
|
+
it('should handle rapid input changes without race conditions', async () => {
|
|
575
|
+
const mockDelayedCompletion = vi
|
|
576
|
+
.fn()
|
|
577
|
+
.mockImplementation(async (_context, partialArg) => {
|
|
578
|
+
// Simulate network delay with different delays for different inputs
|
|
579
|
+
const delay = partialArg.includes('slow') ? 200 : 50;
|
|
580
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
581
|
+
return [`suggestion-for-${partialArg}`];
|
|
582
|
+
});
|
|
583
|
+
const slashCommands = [
|
|
584
|
+
createTestCommand({
|
|
585
|
+
name: 'test',
|
|
586
|
+
description: 'Test command',
|
|
587
|
+
completion: mockDelayedCompletion,
|
|
588
|
+
}),
|
|
589
|
+
];
|
|
590
|
+
const { result, rerender } = renderHook(({ query }) => useTestHarnessForSlashCompletion(true, query, slashCommands, mockCommandContext), { initialProps: { query: '/test slowquery' } });
|
|
591
|
+
// Quickly change to a faster query
|
|
592
|
+
rerender({ query: '/test fastquery' });
|
|
593
|
+
await waitFor(() => {
|
|
594
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
595
|
+
});
|
|
596
|
+
// Should show suggestions for the latest query only
|
|
597
|
+
const labels = result.current.suggestions.map((s) => s.label);
|
|
598
|
+
expect(labels).toContain('suggestion-for-fastquery');
|
|
599
|
+
expect(labels).not.toContain('suggestion-for-slowquery');
|
|
600
|
+
});
|
|
601
|
+
it('should not update suggestions if component unmounts during async operation', async () => {
|
|
602
|
+
let resolveCompletion;
|
|
603
|
+
const mockCompletion = vi.fn().mockImplementation(async () => new Promise((resolve) => {
|
|
604
|
+
resolveCompletion = resolve;
|
|
605
|
+
}));
|
|
606
|
+
const slashCommands = [
|
|
607
|
+
createTestCommand({
|
|
608
|
+
name: 'test',
|
|
609
|
+
description: 'Test command',
|
|
610
|
+
completion: mockCompletion,
|
|
611
|
+
}),
|
|
612
|
+
];
|
|
613
|
+
const { unmount } = renderHook(() => useTestHarnessForSlashCompletion(true, '/test query', slashCommands, mockCommandContext));
|
|
614
|
+
// Start the async operation
|
|
615
|
+
await waitFor(() => {
|
|
616
|
+
expect(mockCompletion).toHaveBeenCalled();
|
|
617
|
+
});
|
|
618
|
+
// Unmount before completion resolves
|
|
619
|
+
unmount();
|
|
620
|
+
// Now resolve the completion
|
|
621
|
+
resolveCompletion(['late-suggestion']);
|
|
622
|
+
// Wait a bit to ensure any pending updates would have been processed
|
|
623
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
624
|
+
// Since the component is unmounted, suggestions should remain empty
|
|
625
|
+
// and no state update errors should occur
|
|
626
|
+
expect(true).toBe(true); // Test passes if no errors are thrown
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
describe('Error Logging', () => {
|
|
630
|
+
it('should log errors to the console', async () => {
|
|
631
|
+
// Mock console.error to capture log calls
|
|
632
|
+
const consoleErrorSpy = vi
|
|
633
|
+
.spyOn(console, 'error')
|
|
634
|
+
.mockImplementation(() => { });
|
|
635
|
+
// Import the mocked AsyncFzf
|
|
636
|
+
const { AsyncFzf } = await import('fzf');
|
|
637
|
+
// Create a failing find method with error containing sensitive-looking data
|
|
638
|
+
const sensitiveError = new Error('Database connection failed: user=admin, pass=secret123');
|
|
639
|
+
const mockFind = vi.fn().mockRejectedValue(sensitiveError);
|
|
640
|
+
// Mock AsyncFzf to return an instance with failing find
|
|
641
|
+
vi.mocked(AsyncFzf).mockImplementation((_items, _options) => ({
|
|
642
|
+
find: mockFind,
|
|
643
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
644
|
+
}));
|
|
645
|
+
const testCommands = [
|
|
646
|
+
createTestCommand({ name: 'test', description: 'Test command' }),
|
|
647
|
+
];
|
|
648
|
+
const { result } = renderHook(() => useTestHarnessForSlashCompletion(true, '/test', testCommands, mockCommandContext));
|
|
649
|
+
await waitFor(() => {
|
|
650
|
+
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
|
651
|
+
});
|
|
652
|
+
// Should get fallback suggestions
|
|
653
|
+
const labels = result.current.suggestions.map((s) => s.label);
|
|
654
|
+
expect(labels).toContain('test');
|
|
655
|
+
// Verify error logging occurred
|
|
656
|
+
await waitFor(() => {
|
|
657
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('[Fuzzy search - falling back to prefix matching]', sensitiveError);
|
|
658
|
+
});
|
|
659
|
+
consoleErrorSpy.mockRestore();
|
|
660
|
+
// Reset AsyncFzf mock to default behavior
|
|
661
|
+
vi.mocked(AsyncFzf).mockImplementation(createDefaultAsyncFzfMock());
|
|
662
|
+
});
|
|
663
|
+
});
|
|
271
664
|
});
|
|
272
665
|
//# sourceMappingURL=useSlashCompletion.test.js.map
|