brosh 0.2.2
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/LICENSE +21 -0
- package/README.md +181 -0
- package/brosh_brandmark.svg +3 -0
- package/brosh_logo.svg +27 -0
- package/cli_icon.svg +52 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +138 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +618 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +25 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +28 -0
- package/dist/lib.js.map +1 -0
- package/dist/mode-selector.d.ts +7 -0
- package/dist/mode-selector.d.ts.map +1 -0
- package/dist/mode-selector.js +138 -0
- package/dist/mode-selector.js.map +1 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +79 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/recording/index.d.ts +4 -0
- package/dist/recording/index.d.ts.map +1 -0
- package/dist/recording/index.js +3 -0
- package/dist/recording/index.js.map +1 -0
- package/dist/recording/manager.d.ts +62 -0
- package/dist/recording/manager.d.ts.map +1 -0
- package/dist/recording/manager.js +123 -0
- package/dist/recording/manager.js.map +1 -0
- package/dist/recording/recorder.d.ts +95 -0
- package/dist/recording/recorder.d.ts.map +1 -0
- package/dist/recording/recorder.js +330 -0
- package/dist/recording/recorder.js.map +1 -0
- package/dist/recording/types.d.ts +65 -0
- package/dist/recording/types.d.ts.map +1 -0
- package/dist/recording/types.js +2 -0
- package/dist/recording/types.js.map +1 -0
- package/dist/sandbox/ModeSelector.d.ts +2 -0
- package/dist/sandbox/ModeSelector.d.ts.map +1 -0
- package/dist/sandbox/ModeSelector.js +2 -0
- package/dist/sandbox/ModeSelector.js.map +1 -0
- package/dist/sandbox/config.d.ts +46 -0
- package/dist/sandbox/config.d.ts.map +1 -0
- package/dist/sandbox/config.js +144 -0
- package/dist/sandbox/config.js.map +1 -0
- package/dist/sandbox/controller.d.ts +72 -0
- package/dist/sandbox/controller.d.ts.map +1 -0
- package/dist/sandbox/controller.js +208 -0
- package/dist/sandbox/controller.js.map +1 -0
- package/dist/sandbox/index.d.ts +6 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +4 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/mode-prompt.d.ts +10 -0
- package/dist/sandbox/mode-prompt.d.ts.map +1 -0
- package/dist/sandbox/mode-prompt.js +130 -0
- package/dist/sandbox/mode-prompt.js.map +1 -0
- package/dist/sandbox/prompt.d.ts +10 -0
- package/dist/sandbox/prompt.d.ts.map +1 -0
- package/dist/sandbox/prompt.js +434 -0
- package/dist/sandbox/prompt.js.map +1 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +59 -0
- package/dist/server.js.map +1 -0
- package/dist/terminal/index.d.ts +5 -0
- package/dist/terminal/index.d.ts.map +1 -0
- package/dist/terminal/index.js +3 -0
- package/dist/terminal/index.js.map +1 -0
- package/dist/terminal/manager.d.ts +153 -0
- package/dist/terminal/manager.d.ts.map +1 -0
- package/dist/terminal/manager.js +276 -0
- package/dist/terminal/manager.js.map +1 -0
- package/dist/terminal/session.d.ts +137 -0
- package/dist/terminal/session.d.ts.map +1 -0
- package/dist/terminal/session.js +752 -0
- package/dist/terminal/session.js.map +1 -0
- package/dist/tools/definitions.d.ts +18 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +114 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/getContent.d.ts +32 -0
- package/dist/tools/getContent.d.ts.map +1 -0
- package/dist/tools/getContent.js +38 -0
- package/dist/tools/getContent.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +49 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/screenshot.d.ts +20 -0
- package/dist/tools/screenshot.d.ts.map +1 -0
- package/dist/tools/screenshot.js +28 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/sendKey.d.ts +31 -0
- package/dist/tools/sendKey.d.ts.map +1 -0
- package/dist/tools/sendKey.js +38 -0
- package/dist/tools/sendKey.js.map +1 -0
- package/dist/tools/startRecording.d.ts +68 -0
- package/dist/tools/startRecording.d.ts.map +1 -0
- package/dist/tools/startRecording.js +111 -0
- package/dist/tools/startRecording.js.map +1 -0
- package/dist/tools/stopRecording.d.ts +31 -0
- package/dist/tools/stopRecording.d.ts.map +1 -0
- package/dist/tools/stopRecording.js +76 -0
- package/dist/tools/stopRecording.js.map +1 -0
- package/dist/tools/type.d.ts +31 -0
- package/dist/tools/type.d.ts.map +1 -0
- package/dist/tools/type.js +31 -0
- package/dist/tools/type.js.map +1 -0
- package/dist/transport/gui-protocol.d.ts +163 -0
- package/dist/transport/gui-protocol.d.ts.map +1 -0
- package/dist/transport/gui-protocol.js +68 -0
- package/dist/transport/gui-protocol.js.map +1 -0
- package/dist/transport/gui-stream.d.ts +139 -0
- package/dist/transport/gui-stream.d.ts.map +1 -0
- package/dist/transport/gui-stream.js +440 -0
- package/dist/transport/gui-stream.js.map +1 -0
- package/dist/transport/index.d.ts +6 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +6 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/socket.d.ts +46 -0
- package/dist/transport/socket.d.ts.map +1 -0
- package/dist/transport/socket.js +310 -0
- package/dist/transport/socket.js.map +1 -0
- package/dist/types/mcp-client-info.d.ts +226 -0
- package/dist/types/mcp-client-info.d.ts.map +1 -0
- package/dist/types/mcp-client-info.js +62 -0
- package/dist/types/mcp-client-info.js.map +1 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +84 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/utils/env.d.ts +17 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +35 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/keys.d.ts +16 -0
- package/dist/utils/keys.d.ts.map +1 -0
- package/dist/utils/keys.js +155 -0
- package/dist/utils/keys.js.map +1 -0
- package/dist/utils/platform.d.ts +16 -0
- package/dist/utils/platform.d.ts.map +1 -0
- package/dist/utils/platform.js +41 -0
- package/dist/utils/platform.js.map +1 -0
- package/dist/utils/session-logger.d.ts +31 -0
- package/dist/utils/session-logger.d.ts.map +1 -0
- package/dist/utils/session-logger.js +125 -0
- package/dist/utils/session-logger.js.map +1 -0
- package/dist/utils/stats.d.ts +46 -0
- package/dist/utils/stats.d.ts.map +1 -0
- package/dist/utils/stats.js +89 -0
- package/dist/utils/stats.js.map +1 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +9 -0
- package/dist/utils/version.js.map +1 -0
- package/logo.png +0 -0
- package/package.json +61 -0
- package/packages/desktop-electron/THIRD-PARTY-NOTICES +56 -0
- package/packages/desktop-electron/build/afterPack.cjs +147 -0
- package/packages/desktop-electron/package-lock.json +10071 -0
- package/packages/desktop-electron/package.json +170 -0
- package/packages/desktop-electron/resources/icons/mac/icon.icns +0 -0
- package/packages/desktop-electron/resources/icons/png/1024x1024.png +0 -0
- package/packages/desktop-electron/resources/icons/png/128x128.png +0 -0
- package/packages/desktop-electron/resources/icons/png/16x16.png +0 -0
- package/packages/desktop-electron/resources/icons/png/24x24.png +0 -0
- package/packages/desktop-electron/resources/icons/png/256x256.png +0 -0
- package/packages/desktop-electron/resources/icons/png/32x32.png +0 -0
- package/packages/desktop-electron/resources/icons/png/48x48.png +0 -0
- package/packages/desktop-electron/resources/icons/png/512x512.png +0 -0
- package/packages/desktop-electron/resources/icons/png/64x64.png +0 -0
- package/packages/desktop-electron/resources/icons/win/icon.ico +0 -0
- package/packages/desktop-electron/scripts/download-models.js +97 -0
- package/packages/desktop-electron/scripts/prepare-sandbox-bins.js +186 -0
- package/packages/desktop-electron/tests/main/ai-detection/additionalFunctions.test.ts +224 -0
- package/packages/desktop-electron/tests/main/ai-detection/checkOverridePrefix.test.ts +162 -0
- package/packages/desktop-electron/tests/main/ai-detection/classifyInput.test.ts +132 -0
- package/packages/desktop-electron/tests/main/ai-detection/detectTypos.test.ts +342 -0
- package/packages/desktop-electron/tests/main/ai-detection/fixtures/commands.ts +134 -0
- package/packages/desktop-electron/tests/main/ai-detection/fixtures/natural-language.ts +133 -0
- package/packages/desktop-electron/tests/main/ai-detection/fixtures/typos.ts +123 -0
- package/packages/desktop-electron/tests/main/ai-detection/hasValidSubcommand.test.ts +218 -0
- package/packages/desktop-electron/tests/main/ai-detection/isCommandNotFound.test.ts +117 -0
- package/packages/desktop-electron/tests/main/error-triage/buildTriagePrompt.test.ts +133 -0
- package/packages/desktop-electron/tests/main/error-triage/parseTriageResponse.test.ts +123 -0
- package/packages/desktop-electron/tests/main/terminal-bridge/battery-optimization.test.ts +243 -0
- package/packages/desktop-electron/tests/main/terminal-bridge/command-fast-track.test.ts +292 -0
- package/packages/desktop-electron/tests/main/terminal-bridge/default-cwd.test.ts +70 -0
- package/packages/desktop-electron/tests/setup.ts +274 -0
- package/packages/desktop-electron/tsconfig.json +18 -0
- package/packages/desktop-electron/tsconfig.main.json +20 -0
- package/packages/desktop-electron/vite.config.ts +19 -0
- package/packages/desktop-electron/vitest.config.ts +18 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
initializeDetection,
|
|
4
|
+
isKnownCommand,
|
|
5
|
+
findTypoSuggestion,
|
|
6
|
+
} from '../../../src/main/ai-detection.js';
|
|
7
|
+
|
|
8
|
+
describe('initializeDetection', () => {
|
|
9
|
+
it('should initialize without error', async () => {
|
|
10
|
+
await expect(initializeDetection()).resolves.not.toThrow();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should be idempotent (can be called multiple times)', async () => {
|
|
14
|
+
await initializeDetection();
|
|
15
|
+
await initializeDetection();
|
|
16
|
+
// Should not throw
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('isKnownCommand', () => {
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
await initializeDetection();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('should return true for known commands', () => {
|
|
26
|
+
const knownCommands = [
|
|
27
|
+
'ls', 'cd', 'pwd', 'echo', 'cat', 'grep', 'find',
|
|
28
|
+
'git', 'npm', 'docker', 'node', 'python',
|
|
29
|
+
'vim', 'nano', 'ssh', 'curl', 'wget',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
for (const cmd of knownCommands) {
|
|
33
|
+
it(`should return true for "${cmd}"`, () => {
|
|
34
|
+
expect(isKnownCommand(cmd)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('should discover commands via PATH fallback', () => {
|
|
40
|
+
it('should find commands that exist in PATH even if not in initial cache', () => {
|
|
41
|
+
// 'newcmd' and 'latestool' are mocked to exist in PATH
|
|
42
|
+
// but weren't in the initial compgen output
|
|
43
|
+
// The PATH fallback should find them
|
|
44
|
+
expect(isKnownCommand('newcmd')).toBe(true);
|
|
45
|
+
expect(isKnownCommand('latestool')).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should cache discovered commands for future lookups', () => {
|
|
49
|
+
// First call discovers via PATH, second call uses cache
|
|
50
|
+
expect(isKnownCommand('newcmd')).toBe(true);
|
|
51
|
+
expect(isKnownCommand('newcmd')).toBe(true); // Should be fast (cached)
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('should return true for shell builtins', () => {
|
|
56
|
+
const builtins = [
|
|
57
|
+
'cd', 'echo', 'exit', 'export', 'alias', 'source', 'pwd',
|
|
58
|
+
'pushd', 'popd', 'set', 'unset', 'readonly', 'declare',
|
|
59
|
+
'local', 'return', 'break', 'continue', 'eval', 'exec',
|
|
60
|
+
'trap', 'wait', 'kill', 'jobs', 'fg', 'bg', 'test',
|
|
61
|
+
'[', '[[', 'true', 'false', 'read', 'printf',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
for (const cmd of builtins) {
|
|
65
|
+
it(`should return true for builtin "${cmd}"`, () => {
|
|
66
|
+
expect(isKnownCommand(cmd)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('should return true for path-like commands', () => {
|
|
72
|
+
it('should return true for relative paths', () => {
|
|
73
|
+
expect(isKnownCommand('./script.sh')).toBe(true);
|
|
74
|
+
expect(isKnownCommand('./bin/tool')).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return true for absolute paths', () => {
|
|
78
|
+
expect(isKnownCommand('/usr/bin/python')).toBe(true);
|
|
79
|
+
expect(isKnownCommand('/bin/bash')).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return true for home-relative paths', () => {
|
|
83
|
+
expect(isKnownCommand('~/bin/tool')).toBe(true);
|
|
84
|
+
expect(isKnownCommand('~/.local/bin/cmd')).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('should return false for unknown commands', () => {
|
|
89
|
+
it('should return false for random strings', () => {
|
|
90
|
+
expect(isKnownCommand('foobar')).toBe(false);
|
|
91
|
+
expect(isKnownCommand('asdfgh')).toBe(false);
|
|
92
|
+
expect(isKnownCommand('xyz123')).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should return false for natural language words', () => {
|
|
96
|
+
expect(isKnownCommand('hello')).toBe(false);
|
|
97
|
+
expect(isKnownCommand('please')).toBe(false);
|
|
98
|
+
expect(isKnownCommand('how')).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('should return true for shell functions', () => {
|
|
103
|
+
// These are mock functions defined in tests/setup.ts
|
|
104
|
+
const shellFunctions = [
|
|
105
|
+
'p10k', // Powerlevel10k
|
|
106
|
+
'nvm', // Node version manager
|
|
107
|
+
'pyenv', // Python version manager
|
|
108
|
+
'rbenv', // Ruby version manager
|
|
109
|
+
'direnv', // Directory-specific env vars
|
|
110
|
+
'z', // Zoxide
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const func of shellFunctions) {
|
|
114
|
+
it(`should return true for shell function "${func}"`, () => {
|
|
115
|
+
expect(isKnownCommand(func)).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('should filter out internal/private functions', () => {
|
|
121
|
+
// Functions starting with _ or - should be filtered out during initialization
|
|
122
|
+
const privateFunctions = [
|
|
123
|
+
'_p10k_worker_start',
|
|
124
|
+
'_nvm_auto',
|
|
125
|
+
'-my-hidden-func',
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
for (const func of privateFunctions) {
|
|
129
|
+
it(`should return false for internal function "${func}"`, () => {
|
|
130
|
+
expect(isKnownCommand(func)).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('findTypoSuggestion', () => {
|
|
137
|
+
describe('should find close matches', () => {
|
|
138
|
+
it('should find match within maxDistance', () => {
|
|
139
|
+
const candidates = ['git', 'npm', 'docker', 'kubectl'];
|
|
140
|
+
expect(findTypoSuggestion('gti', candidates, 2)).toBe('git');
|
|
141
|
+
expect(findTypoSuggestion('dcoker', candidates, 2)).toBe('docker');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should work with Set input', () => {
|
|
145
|
+
const candidates = new Set(['git', 'npm', 'docker']);
|
|
146
|
+
expect(findTypoSuggestion('gti', candidates, 2)).toBe('git');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should return closest match', () => {
|
|
150
|
+
const candidates = ['cat', 'bat', 'hat', 'mat'];
|
|
151
|
+
// 'cet' is distance 1 from 'cat', distance 2 from others
|
|
152
|
+
expect(findTypoSuggestion('cet', candidates, 2)).toBe('cat');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should prefer transpositions over substitutions', () => {
|
|
156
|
+
// 'nmp' is distance 1 from 'cmp' (substitution n→c)
|
|
157
|
+
// 'nmp' is distance 2 from 'npm' (transposition)
|
|
158
|
+
// But 'nmp' has same letters as 'npm', so should prefer npm
|
|
159
|
+
const candidates = ['cmp', 'npm', 'nap', 'amp'];
|
|
160
|
+
expect(findTypoSuggestion('nmp', candidates, 2)).toBe('npm');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should prefer same-first-letter when distances are equal', () => {
|
|
164
|
+
// Both 'git' and 'hit' are distance 1 from 'gat' (single substitution)
|
|
165
|
+
// But 'git' starts with 'g' like 'gat', so should prefer 'git'
|
|
166
|
+
const candidates = ['hit', 'git', 'bit'];
|
|
167
|
+
expect(findTypoSuggestion('gat', candidates, 2)).toBe('git');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should still find matches even if suggestion is shorter', () => {
|
|
171
|
+
// findTypoSuggestion itself doesn't reject shorter matches
|
|
172
|
+
// That validation happens in detectTypos
|
|
173
|
+
const candidates = ['la', 'ls', 'rm'];
|
|
174
|
+
// 'eza' → 'la' has distance 2, which is within threshold
|
|
175
|
+
expect(findTypoSuggestion('eza', candidates, 2)).toBe('la');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('should return null when no match', () => {
|
|
180
|
+
it('should return null when distance exceeds maxDistance', () => {
|
|
181
|
+
const candidates = ['git', 'npm', 'docker'];
|
|
182
|
+
expect(findTypoSuggestion('xyz', candidates, 2)).toBeNull();
|
|
183
|
+
expect(findTypoSuggestion('foobar', candidates, 2)).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should return null for exact matches (distance 0)', () => {
|
|
187
|
+
const candidates = ['git', 'npm', 'docker'];
|
|
188
|
+
// Exact match has distance 0, which is not > 0
|
|
189
|
+
expect(findTypoSuggestion('git', candidates, 2)).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should skip candidates with very different lengths', () => {
|
|
193
|
+
// The length check in findTypoSuggestion skips if length diff > maxDistance
|
|
194
|
+
// 'abc' (3) vs 'a' (1) = diff of 2, which equals maxDistance so it's NOT skipped
|
|
195
|
+
// 'abc' (3) vs 'superlongcommand' (16) = diff of 13, which is > maxDistance so it IS skipped
|
|
196
|
+
const candidates = ['a', 'superlongcommand'];
|
|
197
|
+
// 'abc' can still match 'a' because length diff is exactly 2 (not > 2)
|
|
198
|
+
// and edit distance from 'abc' to 'a' is 2 (delete b, delete c)
|
|
199
|
+
const result = findTypoSuggestion('abc', candidates, 2);
|
|
200
|
+
expect(result).toBe('a'); // 'a' matches because distance is 2
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('should handle edge cases', () => {
|
|
205
|
+
it('should handle empty candidates array', () => {
|
|
206
|
+
expect(findTypoSuggestion('git', [], 2)).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should handle empty candidates Set', () => {
|
|
210
|
+
expect(findTypoSuggestion('git', new Set(), 2)).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should be case insensitive', () => {
|
|
214
|
+
const candidates = ['Git', 'NPM', 'Docker'];
|
|
215
|
+
expect(findTypoSuggestion('gti', candidates, 2)).toBe('Git');
|
|
216
|
+
expect(findTypoSuggestion('GTI', candidates, 2)).toBe('Git');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should use default maxDistance of 2', () => {
|
|
220
|
+
const candidates = ['git', 'npm'];
|
|
221
|
+
expect(findTypoSuggestion('gti', candidates)).toBe('git');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { checkOverridePrefix } from '../../../src/main/ai-detection.js';
|
|
3
|
+
|
|
4
|
+
describe('checkOverridePrefix', () => {
|
|
5
|
+
describe('COMMAND override (!)', () => {
|
|
6
|
+
it('should detect ! prefix and return COMMAND override', () => {
|
|
7
|
+
const result = checkOverridePrefix('!ls -la');
|
|
8
|
+
expect(result.override).toBe('COMMAND');
|
|
9
|
+
expect(result.cleanedInput).toBe('ls -la');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should strip ! from various commands', () => {
|
|
13
|
+
expect(checkOverridePrefix('!git status')).toEqual({
|
|
14
|
+
override: 'COMMAND',
|
|
15
|
+
cleanedInput: 'git status',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(checkOverridePrefix('!npm install')).toEqual({
|
|
19
|
+
override: 'COMMAND',
|
|
20
|
+
cleanedInput: 'npm install',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(checkOverridePrefix('!echo hello')).toEqual({
|
|
24
|
+
override: 'COMMAND',
|
|
25
|
+
cleanedInput: 'echo hello',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should handle single character after !', () => {
|
|
30
|
+
const result = checkOverridePrefix('!a');
|
|
31
|
+
expect(result.override).toBe('COMMAND');
|
|
32
|
+
expect(result.cleanedInput).toBe('a');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should handle ! at start with spaces after', () => {
|
|
36
|
+
const result = checkOverridePrefix('! ls -la');
|
|
37
|
+
expect(result.override).toBe('COMMAND');
|
|
38
|
+
expect(result.cleanedInput).toBe(' ls -la');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('NATURAL_LANGUAGE override (?)', () => {
|
|
43
|
+
it('should detect ? prefix and return NATURAL_LANGUAGE override', () => {
|
|
44
|
+
const result = checkOverridePrefix('?how do I list files');
|
|
45
|
+
expect(result.override).toBe('NATURAL_LANGUAGE');
|
|
46
|
+
expect(result.cleanedInput).toBe('how do I list files');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should strip ? from various queries', () => {
|
|
50
|
+
expect(checkOverridePrefix('?what is git')).toEqual({
|
|
51
|
+
override: 'NATURAL_LANGUAGE',
|
|
52
|
+
cleanedInput: 'what is git',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(checkOverridePrefix('?help me with docker')).toEqual({
|
|
56
|
+
override: 'NATURAL_LANGUAGE',
|
|
57
|
+
cleanedInput: 'help me with docker',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(checkOverridePrefix('?explain this code')).toEqual({
|
|
61
|
+
override: 'NATURAL_LANGUAGE',
|
|
62
|
+
cleanedInput: 'explain this code',
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should handle single character after ?', () => {
|
|
67
|
+
const result = checkOverridePrefix('?a');
|
|
68
|
+
expect(result.override).toBe('NATURAL_LANGUAGE');
|
|
69
|
+
expect(result.cleanedInput).toBe('a');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('no override (null)', () => {
|
|
74
|
+
it('should return null override for regular commands', () => {
|
|
75
|
+
const result = checkOverridePrefix('ls -la');
|
|
76
|
+
expect(result.override).toBeNull();
|
|
77
|
+
expect(result.cleanedInput).toBe('ls -la');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return null override for regular NL', () => {
|
|
81
|
+
const result = checkOverridePrefix('how do I list files');
|
|
82
|
+
expect(result.override).toBeNull();
|
|
83
|
+
expect(result.cleanedInput).toBe('how do I list files');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should return null for just ! (no content after)', () => {
|
|
87
|
+
const result = checkOverridePrefix('!');
|
|
88
|
+
expect(result.override).toBeNull();
|
|
89
|
+
expect(result.cleanedInput).toBe('!');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return null for just ? (no content after)', () => {
|
|
93
|
+
const result = checkOverridePrefix('?');
|
|
94
|
+
expect(result.override).toBeNull();
|
|
95
|
+
expect(result.cleanedInput).toBe('?');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should not treat ! in middle as override', () => {
|
|
99
|
+
const result = checkOverridePrefix('echo hello!');
|
|
100
|
+
expect(result.override).toBeNull();
|
|
101
|
+
expect(result.cleanedInput).toBe('echo hello!');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should not treat ? at end as override', () => {
|
|
105
|
+
const result = checkOverridePrefix('what is this?');
|
|
106
|
+
expect(result.override).toBeNull();
|
|
107
|
+
expect(result.cleanedInput).toBe('what is this?');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('whitespace handling', () => {
|
|
112
|
+
it('should trim input before checking prefix', () => {
|
|
113
|
+
const result = checkOverridePrefix(' !ls -la ');
|
|
114
|
+
expect(result.override).toBe('COMMAND');
|
|
115
|
+
expect(result.cleanedInput).toBe('ls -la');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should trim input for ? prefix', () => {
|
|
119
|
+
const result = checkOverridePrefix(' ?how do I ');
|
|
120
|
+
expect(result.override).toBe('NATURAL_LANGUAGE');
|
|
121
|
+
expect(result.cleanedInput).toBe('how do I');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle whitespace-only input', () => {
|
|
125
|
+
const result = checkOverridePrefix(' ');
|
|
126
|
+
expect(result.override).toBeNull();
|
|
127
|
+
expect(result.cleanedInput).toBe('');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should handle empty input', () => {
|
|
131
|
+
const result = checkOverridePrefix('');
|
|
132
|
+
expect(result.override).toBeNull();
|
|
133
|
+
expect(result.cleanedInput).toBe('');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('edge cases', () => {
|
|
138
|
+
it('should handle multiple ! at start', () => {
|
|
139
|
+
const result = checkOverridePrefix('!!ls');
|
|
140
|
+
expect(result.override).toBe('COMMAND');
|
|
141
|
+
expect(result.cleanedInput).toBe('!ls');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should handle multiple ? at start', () => {
|
|
145
|
+
const result = checkOverridePrefix('??what');
|
|
146
|
+
expect(result.override).toBe('NATURAL_LANGUAGE');
|
|
147
|
+
expect(result.cleanedInput).toBe('?what');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should handle !? combination', () => {
|
|
151
|
+
const result = checkOverridePrefix('!?test');
|
|
152
|
+
expect(result.override).toBe('COMMAND');
|
|
153
|
+
expect(result.cleanedInput).toBe('?test');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should handle ?! combination', () => {
|
|
157
|
+
const result = checkOverridePrefix('?!test');
|
|
158
|
+
expect(result.override).toBe('NATURAL_LANGUAGE');
|
|
159
|
+
expect(result.cleanedInput).toBe('!test');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
classifyInput,
|
|
4
|
+
initializeDetection,
|
|
5
|
+
type ClassificationResult,
|
|
6
|
+
} from '../../../src/main/ai-detection.js';
|
|
7
|
+
import { VALID_COMMANDS } from './fixtures/commands.js';
|
|
8
|
+
import { NATURAL_LANGUAGE } from './fixtures/natural-language.js';
|
|
9
|
+
|
|
10
|
+
describe('classifyInput', () => {
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
await initializeDetection({ preloadML: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('COMMAND classification', () => {
|
|
16
|
+
it('should classify empty input as COMMAND (shell handles it)', async () => {
|
|
17
|
+
const result = await classifyInput('');
|
|
18
|
+
expect(result.classification).toBe('COMMAND');
|
|
19
|
+
expect(result.confidence).toBe(1.0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should classify single word commands', async () => {
|
|
23
|
+
for (const cmd of VALID_COMMANDS.singleWord) {
|
|
24
|
+
const result = await classifyInput(cmd);
|
|
25
|
+
expect(result.classification).toBe('COMMAND');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should classify shell builtins', async () => {
|
|
30
|
+
// Shell builtins that are commonly used
|
|
31
|
+
const builtins = ['cd', 'echo', 'exit', 'export', 'alias', 'source', 'pwd'];
|
|
32
|
+
for (const cmd of builtins) {
|
|
33
|
+
const result = await classifyInput(cmd);
|
|
34
|
+
expect(result.classification).toBe('COMMAND');
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should classify git commands', async () => {
|
|
39
|
+
for (const cmd of VALID_COMMANDS.git) {
|
|
40
|
+
const result = await classifyInput(cmd);
|
|
41
|
+
expect(result.classification).toBe('COMMAND');
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should classify npm commands', async () => {
|
|
46
|
+
for (const cmd of VALID_COMMANDS.npm) {
|
|
47
|
+
const result = await classifyInput(cmd);
|
|
48
|
+
expect(result.classification).toBe('COMMAND');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should classify docker commands', async () => {
|
|
53
|
+
for (const cmd of VALID_COMMANDS.docker) {
|
|
54
|
+
const result = await classifyInput(cmd);
|
|
55
|
+
expect(result.classification).toBe('COMMAND');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should classify commands with flags', async () => {
|
|
60
|
+
for (const cmd of VALID_COMMANDS.withFlags) {
|
|
61
|
+
const result = await classifyInput(cmd);
|
|
62
|
+
expect(result.classification).toBe('COMMAND');
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should classify commands with paths', async () => {
|
|
67
|
+
for (const cmd of VALID_COMMANDS.withPaths) {
|
|
68
|
+
const result = await classifyInput(cmd);
|
|
69
|
+
expect(result.classification).toBe('COMMAND');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should classify pipelines as commands', async () => {
|
|
74
|
+
for (const cmd of VALID_COMMANDS.pipelines) {
|
|
75
|
+
const result = await classifyInput(cmd);
|
|
76
|
+
expect(result.classification).toBe('COMMAND');
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should classify path-based commands', async () => {
|
|
81
|
+
expect((await classifyInput('./script.sh')).classification).toBe('COMMAND');
|
|
82
|
+
expect((await classifyInput('/usr/bin/foo')).classification).toBe('COMMAND');
|
|
83
|
+
expect((await classifyInput('~/bin/tool')).classification).toBe('COMMAND');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('NATURAL_LANGUAGE classification', () => {
|
|
88
|
+
it('should classify questions', async () => {
|
|
89
|
+
for (const input of NATURAL_LANGUAGE.questions) {
|
|
90
|
+
const result = await classifyInput(input);
|
|
91
|
+
expect(result.classification).toBe('NATURAL_LANGUAGE');
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should classify first-person statements', async () => {
|
|
96
|
+
for (const input of NATURAL_LANGUAGE.firstPerson) {
|
|
97
|
+
const result = await classifyInput(input);
|
|
98
|
+
expect(result.classification).toBe('NATURAL_LANGUAGE');
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('edge cases', () => {
|
|
104
|
+
it('should handle whitespace-only input', async () => {
|
|
105
|
+
const result = await classifyInput(' ');
|
|
106
|
+
expect(result.classification).toBe('COMMAND');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle input with leading/trailing whitespace', async () => {
|
|
110
|
+
const result = await classifyInput(' ls -la ');
|
|
111
|
+
expect(result.classification).toBe('COMMAND');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should classify inputs ending with ? as NL', async () => {
|
|
115
|
+
const result = await classifyInput('how do I list files?');
|
|
116
|
+
expect(result.classification).toBe('NATURAL_LANGUAGE');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('classification result structure', () => {
|
|
121
|
+
it('should return proper ClassificationResult structure', async () => {
|
|
122
|
+
const result = await classifyInput('ls -la');
|
|
123
|
+
expect(result).toHaveProperty('classification');
|
|
124
|
+
expect(result).toHaveProperty('confidence');
|
|
125
|
+
expect(result).toHaveProperty('tier');
|
|
126
|
+
expect(result).toHaveProperty('reason');
|
|
127
|
+
expect(typeof result.confidence).toBe('number');
|
|
128
|
+
expect(result.confidence).toBeGreaterThanOrEqual(0);
|
|
129
|
+
expect(result.confidence).toBeLessThanOrEqual(1);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|