opencode-morphllm 0.0.7 → 0.0.9

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.
@@ -1,310 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
2
- import { homedir } from 'node:os';
3
- import { join, resolve } from 'node:path';
4
- import { getOpenCodeConfigDir, getOpenCodeConfigPaths, isDevBuild, detectExistingConfigDir, TAURI_APP_IDENTIFIER, TAURI_APP_IDENTIFIER_DEV, } from './opencode-config-dir';
5
- describe('opencode-config-dir', () => {
6
- let originalPlatform;
7
- let originalEnv;
8
- beforeEach(() => {
9
- originalPlatform = process.platform;
10
- originalEnv = {
11
- APPDATA: process.env.APPDATA,
12
- XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
13
- XDG_DATA_HOME: process.env.XDG_DATA_HOME,
14
- OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
15
- };
16
- });
17
- afterEach(() => {
18
- Object.defineProperty(process, 'platform', { value: originalPlatform });
19
- for (const [key, value] of Object.entries(originalEnv)) {
20
- if (value !== undefined) {
21
- process.env[key] = value;
22
- }
23
- else {
24
- delete process.env[key];
25
- }
26
- }
27
- });
28
- describe('OPENCODE_CONFIG_DIR environment variable', () => {
29
- test('returns OPENCODE_CONFIG_DIR when env var is set', () => {
30
- // #given OPENCODE_CONFIG_DIR is set to a custom path
31
- process.env.OPENCODE_CONFIG_DIR = '/custom/opencode/path';
32
- Object.defineProperty(process, 'platform', { value: 'linux' });
33
- // #when getOpenCodeConfigDir is called with binary="opencode"
34
- const result = getOpenCodeConfigDir({
35
- binary: 'opencode',
36
- version: '1.0.200',
37
- });
38
- // #then returns the custom path
39
- expect(result).toBe('/custom/opencode/path');
40
- });
41
- test('falls back to default when env var is not set', () => {
42
- // #given OPENCODE_CONFIG_DIR is not set, platform is Linux
43
- delete process.env.OPENCODE_CONFIG_DIR;
44
- delete process.env.XDG_CONFIG_HOME;
45
- Object.defineProperty(process, 'platform', { value: 'linux' });
46
- // #when getOpenCodeConfigDir is called with binary="opencode"
47
- const result = getOpenCodeConfigDir({
48
- binary: 'opencode',
49
- version: '1.0.200',
50
- });
51
- // #then returns default ~/.config/opencode
52
- expect(result).toBe(join(homedir(), '.config', 'opencode'));
53
- });
54
- test('falls back to default when env var is empty string', () => {
55
- // #given OPENCODE_CONFIG_DIR is set to empty string
56
- process.env.OPENCODE_CONFIG_DIR = '';
57
- delete process.env.XDG_CONFIG_HOME;
58
- Object.defineProperty(process, 'platform', { value: 'linux' });
59
- // #when getOpenCodeConfigDir is called with binary="opencode"
60
- const result = getOpenCodeConfigDir({
61
- binary: 'opencode',
62
- version: '1.0.200',
63
- });
64
- // #then returns default ~/.config/opencode
65
- expect(result).toBe(join(homedir(), '.config', 'opencode'));
66
- });
67
- test('falls back to default when env var is whitespace only', () => {
68
- // #given OPENCODE_CONFIG_DIR is set to whitespace only
69
- process.env.OPENCODE_CONFIG_DIR = ' ';
70
- delete process.env.XDG_CONFIG_HOME;
71
- Object.defineProperty(process, 'platform', { value: 'linux' });
72
- // #when getOpenCodeConfigDir is called with binary="opencode"
73
- const result = getOpenCodeConfigDir({
74
- binary: 'opencode',
75
- version: '1.0.200',
76
- });
77
- // #then returns default ~/.config/opencode
78
- expect(result).toBe(join(homedir(), '.config', 'opencode'));
79
- });
80
- test('resolves relative path to absolute path', () => {
81
- // #given OPENCODE_CONFIG_DIR is set to a relative path
82
- process.env.OPENCODE_CONFIG_DIR = './my-opencode-config';
83
- Object.defineProperty(process, 'platform', { value: 'linux' });
84
- // #when getOpenCodeConfigDir is called with binary="opencode"
85
- const result = getOpenCodeConfigDir({
86
- binary: 'opencode',
87
- version: '1.0.200',
88
- });
89
- // #then returns resolved absolute path
90
- expect(result).toBe(resolve('./my-opencode-config'));
91
- });
92
- test('OPENCODE_CONFIG_DIR takes priority over XDG_CONFIG_HOME', () => {
93
- // #given both OPENCODE_CONFIG_DIR and XDG_CONFIG_HOME are set
94
- process.env.OPENCODE_CONFIG_DIR = '/custom/opencode/path';
95
- process.env.XDG_CONFIG_HOME = '/xdg/config';
96
- Object.defineProperty(process, 'platform', { value: 'linux' });
97
- // #when getOpenCodeConfigDir is called with binary="opencode"
98
- const result = getOpenCodeConfigDir({
99
- binary: 'opencode',
100
- version: '1.0.200',
101
- });
102
- // #then OPENCODE_CONFIG_DIR takes priority
103
- expect(result).toBe('/custom/opencode/path');
104
- });
105
- });
106
- describe('isDevBuild', () => {
107
- test('returns false for null version', () => {
108
- expect(isDevBuild(null)).toBe(false);
109
- });
110
- test('returns false for undefined version', () => {
111
- expect(isDevBuild(undefined)).toBe(false);
112
- });
113
- test('returns false for production version', () => {
114
- expect(isDevBuild('1.0.200')).toBe(false);
115
- expect(isDevBuild('2.1.0')).toBe(false);
116
- });
117
- test('returns true for version containing -dev', () => {
118
- expect(isDevBuild('1.0.0-dev')).toBe(true);
119
- expect(isDevBuild('1.0.0-dev.123')).toBe(true);
120
- });
121
- test('returns true for version containing .dev', () => {
122
- expect(isDevBuild('1.0.0.dev')).toBe(true);
123
- expect(isDevBuild('1.0.0.dev.456')).toBe(true);
124
- });
125
- });
126
- describe('getOpenCodeConfigDir', () => {
127
- describe('for opencode CLI binary', () => {
128
- test('returns ~/.config/opencode on Linux', () => {
129
- // #given opencode CLI binary detected, platform is Linux
130
- Object.defineProperty(process, 'platform', { value: 'linux' });
131
- delete process.env.XDG_CONFIG_HOME;
132
- delete process.env.OPENCODE_CONFIG_DIR;
133
- // #when getOpenCodeConfigDir is called with binary="opencode"
134
- const result = getOpenCodeConfigDir({
135
- binary: 'opencode',
136
- version: '1.0.200',
137
- });
138
- // #then returns ~/.config/opencode
139
- expect(result).toBe(join(homedir(), '.config', 'opencode'));
140
- });
141
- test('returns $XDG_CONFIG_HOME/opencode on Linux when XDG_CONFIG_HOME is set', () => {
142
- // #given opencode CLI binary detected, platform is Linux with XDG_CONFIG_HOME set
143
- Object.defineProperty(process, 'platform', { value: 'linux' });
144
- process.env.XDG_CONFIG_HOME = '/custom/config';
145
- delete process.env.OPENCODE_CONFIG_DIR;
146
- // #when getOpenCodeConfigDir is called with binary="opencode"
147
- const result = getOpenCodeConfigDir({
148
- binary: 'opencode',
149
- version: '1.0.200',
150
- });
151
- // #then returns $XDG_CONFIG_HOME/opencode
152
- expect(result).toBe('/custom/config/opencode');
153
- });
154
- test('returns ~/.config/opencode on macOS', () => {
155
- // #given opencode CLI binary detected, platform is macOS
156
- Object.defineProperty(process, 'platform', { value: 'darwin' });
157
- delete process.env.XDG_CONFIG_HOME;
158
- delete process.env.OPENCODE_CONFIG_DIR;
159
- // #when getOpenCodeConfigDir is called with binary="opencode"
160
- const result = getOpenCodeConfigDir({
161
- binary: 'opencode',
162
- version: '1.0.200',
163
- });
164
- // #then returns ~/.config/opencode
165
- expect(result).toBe(join(homedir(), '.config', 'opencode'));
166
- });
167
- test('returns ~/.config/opencode on Windows by default', () => {
168
- // #given opencode CLI binary detected, platform is Windows
169
- Object.defineProperty(process, 'platform', { value: 'win32' });
170
- delete process.env.APPDATA;
171
- delete process.env.OPENCODE_CONFIG_DIR;
172
- // #when getOpenCodeConfigDir is called with binary="opencode"
173
- const result = getOpenCodeConfigDir({
174
- binary: 'opencode',
175
- version: '1.0.200',
176
- checkExisting: false,
177
- });
178
- // #then returns ~/.config/opencode (cross-platform default)
179
- expect(result).toBe(join(homedir(), '.config', 'opencode'));
180
- });
181
- });
182
- describe('for opencode-desktop Tauri binary', () => {
183
- test('returns ~/.config/ai.opencode.desktop on Linux', () => {
184
- // #given opencode-desktop binary detected, platform is Linux
185
- Object.defineProperty(process, 'platform', { value: 'linux' });
186
- delete process.env.XDG_CONFIG_HOME;
187
- // #when getOpenCodeConfigDir is called with binary="opencode-desktop"
188
- const result = getOpenCodeConfigDir({
189
- binary: 'opencode-desktop',
190
- version: '1.0.200',
191
- checkExisting: false,
192
- });
193
- // #then returns ~/.config/ai.opencode.desktop
194
- expect(result).toBe(join(homedir(), '.config', TAURI_APP_IDENTIFIER));
195
- });
196
- test('returns ~/Library/Application Support/ai.opencode.desktop on macOS', () => {
197
- // #given opencode-desktop binary detected, platform is macOS
198
- Object.defineProperty(process, 'platform', { value: 'darwin' });
199
- // #when getOpenCodeConfigDir is called with binary="opencode-desktop"
200
- const result = getOpenCodeConfigDir({
201
- binary: 'opencode-desktop',
202
- version: '1.0.200',
203
- checkExisting: false,
204
- });
205
- // #then returns ~/Library/Application Support/ai.opencode.desktop
206
- expect(result).toBe(join(homedir(), 'Library', 'Application Support', TAURI_APP_IDENTIFIER));
207
- });
208
- test('returns %APPDATA%/ai.opencode.desktop on Windows', () => {
209
- // #given opencode-desktop binary detected, platform is Windows
210
- Object.defineProperty(process, 'platform', { value: 'win32' });
211
- process.env.APPDATA = 'C:\\Users\\TestUser\\AppData\\Roaming';
212
- // #when getOpenCodeConfigDir is called with binary="opencode-desktop"
213
- const result = getOpenCodeConfigDir({
214
- binary: 'opencode-desktop',
215
- version: '1.0.200',
216
- checkExisting: false,
217
- });
218
- // #then returns %APPDATA%/ai.opencode.desktop
219
- expect(result).toBe(join('C:\\Users\\TestUser\\AppData\\Roaming', TAURI_APP_IDENTIFIER));
220
- });
221
- });
222
- describe('dev build detection', () => {
223
- test('returns ai.opencode.desktop.dev path when dev version detected', () => {
224
- // #given opencode-desktop dev version
225
- Object.defineProperty(process, 'platform', { value: 'linux' });
226
- delete process.env.XDG_CONFIG_HOME;
227
- // #when getOpenCodeConfigDir is called with dev version
228
- const result = getOpenCodeConfigDir({
229
- binary: 'opencode-desktop',
230
- version: '1.0.0-dev.123',
231
- checkExisting: false,
232
- });
233
- // #then returns path with ai.opencode.desktop.dev
234
- expect(result).toBe(join(homedir(), '.config', TAURI_APP_IDENTIFIER_DEV));
235
- });
236
- test('returns ai.opencode.desktop.dev on macOS for dev build', () => {
237
- // #given opencode-desktop dev version on macOS
238
- Object.defineProperty(process, 'platform', { value: 'darwin' });
239
- // #when getOpenCodeConfigDir is called with dev version
240
- const result = getOpenCodeConfigDir({
241
- binary: 'opencode-desktop',
242
- version: '1.0.0-dev',
243
- checkExisting: false,
244
- });
245
- // #then returns path with ai.opencode.desktop.dev
246
- expect(result).toBe(join(homedir(), 'Library', 'Application Support', TAURI_APP_IDENTIFIER_DEV));
247
- });
248
- });
249
- });
250
- describe('getOpenCodeConfigPaths', () => {
251
- test('returns all config paths for CLI binary', () => {
252
- // #given opencode CLI binary on Linux
253
- Object.defineProperty(process, 'platform', { value: 'linux' });
254
- delete process.env.XDG_CONFIG_HOME;
255
- delete process.env.OPENCODE_CONFIG_DIR;
256
- // #when getOpenCodeConfigPaths is called
257
- const paths = getOpenCodeConfigPaths({
258
- binary: 'opencode',
259
- version: '1.0.200',
260
- });
261
- // #then returns all expected paths
262
- const expectedDir = join(homedir(), '.config', 'opencode');
263
- expect(paths.configDir).toBe(expectedDir);
264
- expect(paths.configJson).toBe(join(expectedDir, 'opencode.json'));
265
- expect(paths.configJsonc).toBe(join(expectedDir, 'opencode.jsonc'));
266
- expect(paths.packageJson).toBe(join(expectedDir, 'package.json'));
267
- expect(paths.omoConfig).toBe(join(expectedDir, 'oh-my-opencode.json'));
268
- });
269
- test('returns all config paths for desktop binary', () => {
270
- // #given opencode-desktop binary on macOS
271
- Object.defineProperty(process, 'platform', { value: 'darwin' });
272
- // #when getOpenCodeConfigPaths is called
273
- const paths = getOpenCodeConfigPaths({
274
- binary: 'opencode-desktop',
275
- version: '1.0.200',
276
- checkExisting: false,
277
- });
278
- // #then returns all expected paths
279
- const expectedDir = join(homedir(), 'Library', 'Application Support', TAURI_APP_IDENTIFIER);
280
- expect(paths.configDir).toBe(expectedDir);
281
- expect(paths.configJson).toBe(join(expectedDir, 'opencode.json'));
282
- expect(paths.configJsonc).toBe(join(expectedDir, 'opencode.jsonc'));
283
- expect(paths.packageJson).toBe(join(expectedDir, 'package.json'));
284
- expect(paths.omoConfig).toBe(join(expectedDir, 'oh-my-opencode.json'));
285
- });
286
- });
287
- describe('detectExistingConfigDir', () => {
288
- test('returns null when no config exists', () => {
289
- // #given no config files exist
290
- Object.defineProperty(process, 'platform', { value: 'linux' });
291
- delete process.env.XDG_CONFIG_HOME;
292
- delete process.env.OPENCODE_CONFIG_DIR;
293
- // #when detectExistingConfigDir is called
294
- const result = detectExistingConfigDir('opencode', '1.0.200');
295
- // #then result is either null or a valid string path
296
- expect(result === null || typeof result === 'string').toBe(true);
297
- });
298
- test('includes OPENCODE_CONFIG_DIR in search locations when set', () => {
299
- // #given OPENCODE_CONFIG_DIR is set to a custom path
300
- process.env.OPENCODE_CONFIG_DIR = '/custom/opencode/path';
301
- Object.defineProperty(process, 'platform', { value: 'linux' });
302
- delete process.env.XDG_CONFIG_HOME;
303
- // #when detectExistingConfigDir is called
304
- const result = detectExistingConfigDir('opencode', '1.0.200');
305
- // #then result is either null (no config file exists) or a valid string path
306
- // The important thing is that the function doesn't throw
307
- expect(result === null || typeof result === 'string').toBe(true);
308
- });
309
- });
310
- });
package/src/index.ts DELETED
@@ -1,22 +0,0 @@
1
- import { Plugin } from '@opencode-ai/plugin';
2
- import type { McpLocalConfig } from '@opencode-ai/sdk';
3
-
4
- import { createBuiltinMcps } from './morph/mcps';
5
- import { createModelRouterHook } from './morph/router';
6
-
7
- const MorphOpenCodePlugin: Plugin = async () => {
8
- const builtinMcps: Record<string, McpLocalConfig> = createBuiltinMcps();
9
- const routerHook = createModelRouterHook();
10
-
11
- return {
12
- config: async (currentConfig: any) => {
13
- currentConfig.mcp = {
14
- ...currentConfig.mcp,
15
- ...builtinMcps,
16
- };
17
- },
18
- ...routerHook,
19
- };
20
- };
21
-
22
- export default MorphOpenCodePlugin;
@@ -1,51 +0,0 @@
1
- import { describe, it, expect, vi } from 'bun:test';
2
-
3
- vi.mock('../shared/config', () => ({
4
- API_KEY: 'test-api-key-123',
5
- }));
6
-
7
- import { createBuiltinMcps } from './mcps';
8
-
9
- describe('mcps.ts', () => {
10
- describe('createBuiltinMcps', () => {
11
- it('should create morph_mcp configuration', () => {
12
- const mcps = createBuiltinMcps();
13
-
14
- expect(mcps).toHaveProperty('morph_mcp');
15
- expect(mcps.morph_mcp.type).toBe('local');
16
- expect(mcps.morph_mcp.enabled).toBe(true);
17
- });
18
-
19
- it('should set correct command for morph_mcp', () => {
20
- const mcps = createBuiltinMcps();
21
-
22
- expect(mcps.morph_mcp.command).toEqual([
23
- 'npx',
24
- '-y',
25
- '@morphllm/morphmcp',
26
- ]);
27
- });
28
-
29
- it('should set MORPH_API_KEY in environment', () => {
30
- const mcps = createBuiltinMcps();
31
- const env = mcps.morph_mcp.environment;
32
-
33
- expect(env).toBeDefined();
34
- expect(env?.MORPH_API_KEY).toBe('test-api-key-123');
35
- });
36
-
37
- it('should set ENABLED_TOOLS environment variable', () => {
38
- const mcps = createBuiltinMcps();
39
- const env = mcps.morph_mcp.environment;
40
-
41
- expect(env).toBeDefined();
42
- expect(env?.ENABLED_TOOLS).toBe('edit_file,warpgrep_codebase_search');
43
- });
44
-
45
- it('should only create morph_mcp key', () => {
46
- const mcps = createBuiltinMcps();
47
-
48
- expect(Object.keys(mcps)).toEqual(['morph_mcp']);
49
- });
50
- });
51
- });
package/src/morph/mcps.ts DELETED
@@ -1,18 +0,0 @@
1
- import type { McpLocalConfig } from '@opencode-ai/sdk';
2
- import { API_KEY } from '../shared/config';
3
-
4
- export function createBuiltinMcps(): Record<string, McpLocalConfig> {
5
- return {
6
- morph_mcp: {
7
- type: 'local',
8
- command: ['npx', '-y', '@morphllm/morphmcp'],
9
- environment: {
10
- MORPH_API_KEY: API_KEY,
11
- ENABLED_TOOLS: 'edit_file,warpgrep_codebase_search',
12
- },
13
- enabled: true,
14
- },
15
- };
16
- }
17
-
18
- export default createBuiltinMcps;
@@ -1,267 +0,0 @@
1
- import { describe, it, expect, vi } from 'bun:test';
2
- import type { Part } from '@opencode-ai/sdk';
3
-
4
- vi.mock('@morphllm/morphsdk', () => ({
5
- MorphClient: vi.fn().mockImplementation(() => ({
6
- routers: {
7
- raw: {
8
- classify: vi.fn(),
9
- },
10
- },
11
- })),
12
- }));
13
-
14
- vi.mock('../shared/config', () => ({
15
- API_KEY: 'sk-test-api-key-123',
16
- MORPH_MODEL_EASY: 'easy/easy',
17
- MORPH_MODEL_MEDIUM: 'medium/medium',
18
- MORPH_MODEL_HARD: 'hard/hard',
19
- MORPH_MODEL_DEFAULT: 'default/default',
20
- MORPH_ROUTER_PROMPT_CACHING_AWARE: false,
21
- MORPH_ROUTER_ENABLED: true,
22
- }));
23
-
24
- import { createModelRouterHook, extractPromptText } from './router';
25
-
26
- describe('router.ts', () => {
27
- describe('extractPromptText', () => {
28
- it('should extract text from parts', () => {
29
- const parts: Part[] = [
30
- { type: 'text', text: 'Hello' },
31
- { type: 'text', text: 'World' },
32
- ] as any;
33
- expect(extractPromptText(parts)).toBe('Hello World');
34
- });
35
-
36
- it('should handle empty parts', () => {
37
- const parts: Part[] = [];
38
- expect(extractPromptText(parts)).toBe('');
39
- });
40
-
41
- it('should filter out non-text parts', () => {
42
- const parts: Part[] = [
43
- { type: 'text', text: 'Hello' },
44
- { type: 'other', text: 'Ignore' },
45
- { type: 'text', text: 'World' },
46
- ] as any;
47
- expect(extractPromptText(parts)).toBe('Hello World');
48
- });
49
-
50
- it('should handle parts with no text', () => {
51
- const parts: Part[] = [
52
- { type: 'text', text: 'Hello' },
53
- { type: 'text' },
54
- { type: 'text', text: 'World' },
55
- ] as any;
56
- expect(extractPromptText(parts)).toBe('Hello World');
57
- });
58
-
59
- it('should handle all non-text parts', () => {
60
- const parts: Part[] = [
61
- { type: 'image', data: 'base64...' },
62
- { type: 'tool_use', name: 'bash' },
63
- ] as any;
64
- expect(extractPromptText(parts)).toBe('');
65
- });
66
- });
67
-
68
- describe('createModelRouterHook', () => {
69
- it('should return a chat.message hook', () => {
70
- const hook = createModelRouterHook();
71
- expect('chat.message' in hook).toBe(true);
72
- expect(typeof hook['chat.message']).toBe('function');
73
- });
74
-
75
- it('should call the classifier with the correct input', async () => {
76
- const hook = createModelRouterHook();
77
- const classify = vi.fn().mockResolvedValue({ difficulty: 'easy' });
78
- const input = {
79
- sessionID: '123',
80
- classify,
81
- };
82
- const output = {
83
- message: {} as any,
84
- parts: [{ type: 'text', text: 'test prompt' }],
85
- } as any;
86
- await hook['chat.message'](input as any, output);
87
- expect(classify).toHaveBeenCalledWith({ input: 'test prompt' });
88
- });
89
-
90
- it('should assign the correct model based on difficulty', async () => {
91
- const hook = createModelRouterHook();
92
- const classify = vi.fn().mockResolvedValue({ difficulty: 'hard' });
93
- const input = {
94
- sessionID: '123',
95
- classify,
96
- };
97
- const output = {
98
- message: {} as any,
99
- parts: [{ type: 'text', text: 'test prompt' }],
100
- } as any;
101
- await hook['chat.message'](input as any, output);
102
- expect(input).toHaveProperty('model');
103
- expect((input as any).model.providerID).toBe('hard');
104
- expect((input as any).model.modelID).toBe('hard');
105
- });
106
-
107
- it('should use the default model if no difficulty is returned', async () => {
108
- const hook = createModelRouterHook();
109
- const classify = vi.fn().mockResolvedValue({});
110
- const input = {
111
- sessionID: '123',
112
- classify,
113
- };
114
- const output = {
115
- message: {} as any,
116
- parts: [{ type: 'text', text: 'test prompt' }],
117
- } as any;
118
- await hook['chat.message'](input as any, output);
119
- expect(input).toHaveProperty('model');
120
- expect((input as any).model.providerID).toBe('default');
121
- expect((input as any).model.modelID).toBe('default');
122
- });
123
-
124
- it('should use default model when router returns nothing', async () => {
125
- const hook = createModelRouterHook();
126
- const classify = vi.fn().mockResolvedValue({});
127
- const input = {
128
- sessionID: '123',
129
- classify,
130
- };
131
- const output = {
132
- message: {} as any,
133
- parts: [{ type: 'text', text: 'test prompt' }],
134
- } as any;
135
- await hook['chat.message'](input as any, output);
136
- expect(input).toHaveProperty('model');
137
- expect((input as any).model.providerID).toBe('default');
138
- expect((input as any).model.modelID).toBe('default');
139
- });
140
-
141
- it('should handle classifier throwing an error gracefully', async () => {
142
- const hook = createModelRouterHook();
143
- const classify = vi.fn().mockRejectedValue(new Error('API Error'));
144
- const input = {
145
- sessionID: '123',
146
- classify,
147
- };
148
- const output = {
149
- message: {} as any,
150
- parts: [{ type: 'text', text: 'test prompt' }],
151
- } as any;
152
-
153
- await expect(hook['chat.message'](input as any, output)).rejects.toThrow(
154
- 'API Error'
155
- );
156
- });
157
-
158
- it('should handle classifier returning null', async () => {
159
- const hook = createModelRouterHook();
160
- const classify = vi.fn().mockResolvedValue(null);
161
- const input = {
162
- sessionID: '123',
163
- classify,
164
- };
165
- const output = {
166
- message: {} as any,
167
- parts: [{ type: 'text', text: 'test prompt' }],
168
- } as any;
169
- await hook['chat.message'](input as any, output);
170
- expect((input as any).model.providerID).toBe('default');
171
- expect((input as any).model.modelID).toBe('default');
172
- });
173
-
174
- it('should handle classifier returning undefined difficulty', async () => {
175
- const hook = createModelRouterHook();
176
- const classify = vi.fn().mockResolvedValue({ difficulty: undefined });
177
- const input = {
178
- sessionID: '123',
179
- classify,
180
- };
181
- const output = {
182
- message: {} as any,
183
- parts: [{ type: 'text', text: 'test prompt' }],
184
- } as any;
185
- await hook['chat.message'](input as any, output);
186
- expect((input as any).model.providerID).toBe('default');
187
- expect((input as any).model.modelID).toBe('default');
188
- });
189
-
190
- it('should default to default model for invalid difficulty values', async () => {
191
- const hook = createModelRouterHook();
192
- const classify = vi.fn().mockResolvedValue({ difficulty: 'ultra-hard' });
193
- const input = {
194
- sessionID: '123',
195
- classify,
196
- };
197
- const output = {
198
- message: {} as any,
199
- parts: [{ type: 'text', text: 'test prompt' }],
200
- } as any;
201
- await hook['chat.message'](input as any, output);
202
- expect((input as any).model.providerID).toBe('default');
203
- expect((input as any).model.modelID).toBe('default');
204
- });
205
-
206
- it('should default to default model for empty string difficulty', async () => {
207
- const hook = createModelRouterHook();
208
- const classify = vi.fn().mockResolvedValue({ difficulty: '' });
209
- const input = {
210
- sessionID: '123',
211
- classify,
212
- };
213
- const output = {
214
- message: {} as any,
215
- parts: [{ type: 'text', text: 'test prompt' }],
216
- } as any;
217
- await hook['chat.message'](input as any, output);
218
- expect((input as any).model.providerID).toBe('default');
219
- expect((input as any).model.modelID).toBe('default');
220
- });
221
-
222
- it('should handle case-insensitive difficulty matching', async () => {
223
- const hook = createModelRouterHook();
224
- const classify = vi.fn().mockResolvedValue({ difficulty: 'HARD' });
225
- const input = {
226
- sessionID: '123',
227
- classify,
228
- };
229
- const output = {
230
- message: {} as any,
231
- parts: [{ type: 'text', text: 'test prompt' }],
232
- } as any;
233
- await hook['chat.message'](input as any, output);
234
- expect((input as any).model.providerID).toBe('hard');
235
- expect((input as any).model.modelID).toBe('hard');
236
- });
237
-
238
- it('should route all messages when MORPH_ROUTER_PROMPT_CACHING_AWARE is disabled', async () => {
239
- const hook = createModelRouterHook();
240
- const classify = vi
241
- .fn()
242
- .mockResolvedValueOnce({ difficulty: 'hard' })
243
- .mockResolvedValueOnce({ difficulty: 'easy' });
244
-
245
- const sessionID = 'session-456';
246
- const input1 = { sessionID, classify };
247
- const output1 = {
248
- message: {} as any,
249
- parts: [{ type: 'text', text: 'first message' }],
250
- } as any;
251
-
252
- await hook['chat.message'](input1 as any, output1);
253
- expect(classify).toHaveBeenCalledTimes(1);
254
- expect((input1 as any).model.providerID).toBe('hard');
255
-
256
- const input2 = { sessionID, classify };
257
- const output2 = {
258
- message: {} as any,
259
- parts: [{ type: 'text', text: 'second message' }],
260
- } as any;
261
-
262
- await hook['chat.message'](input2 as any, output2);
263
- expect(classify).toHaveBeenCalledTimes(2);
264
- expect((input2 as any).model.providerID).toBe('easy');
265
- });
266
- });
267
- });