pi-llama-cpp 0.11.0 → 0.12.0
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/README.md +174 -58
- package/package.json +4 -4
- package/src/interfaces/server.ts +7 -0
- package/src/interfaces/settings.ts +65 -0
- package/src/managers/command.ts +108 -31
- package/src/managers/events.ts +2 -2
- package/src/managers/server.ts +12 -9
- package/src/managers/settings.ts +120 -45
- package/src/models/baseModel.ts +37 -7
- package/src/models/routerModel.ts +2 -1
- package/src/server.ts +38 -4
- package/src/sse/manager.ts +9 -8
- package/src/ui/dialog.ts +287 -0
- package/src/ui/overrideEntryEditor.ts +119 -0
- package/src/ui/overrideSettingsList.ts +682 -0
- package/src/ui/serverListEditor.ts +0 -449
- package/src/ui/serverSettingsList.ts +466 -0
- package/src/ui/strings.ts +127 -0
- package/src/utils/settingsStore.ts +2 -6
- package/tests/commandManager.test.ts +106 -16
- package/tests/dialog.test.ts +186 -0
- package/tests/events.test.ts +12 -9
- package/tests/legacyModel.test.ts +4 -19
- package/tests/mocks.ts +12 -8
- package/tests/overrides.test.ts +352 -0
- package/tests/server.test.ts +12 -12
- package/tests/serverManager.test.ts +24 -58
- package/tests/settings.test.ts +489 -43
- package/tests/settingsStore.test.ts +0 -19
- package/tests/singleModel.test.ts +32 -0
- package/tests/sseManager.test.ts +4 -4
- package/tests/serverListEditor.test.ts +0 -637
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { initTheme } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import type { KeybindingsManager, TUI } from "@earendil-works/pi-tui";
|
|
3
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
5
|
import { Action } from "../src/enums/action";
|
|
@@ -10,8 +11,7 @@ import {
|
|
|
10
11
|
} from "../src/managers/command";
|
|
11
12
|
import { ServerManager } from "../src/managers/server";
|
|
12
13
|
import type { LlamaSettingsManager } from "../src/managers/settings";
|
|
13
|
-
import
|
|
14
|
-
import { ServerListEditor } from "../src/ui/serverListEditor";
|
|
14
|
+
import { ServerSettingsList } from "../src/ui/serverSettingsList";
|
|
15
15
|
import {
|
|
16
16
|
createMockCtx,
|
|
17
17
|
createMockModel,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "./mocks";
|
|
23
23
|
|
|
24
24
|
beforeEach(() => {
|
|
25
|
+
initTheme();
|
|
25
26
|
vi.clearAllMocks();
|
|
26
27
|
mockRpc.mockResolvedValue({ data: [] });
|
|
27
28
|
});
|
|
@@ -42,12 +43,13 @@ describe("CommandManager", () => {
|
|
|
42
43
|
describe("getArgumentCompletions", () => {
|
|
43
44
|
it("should provide completions for /models", () => {
|
|
44
45
|
const completions = commandManager.getArgumentCompletions("");
|
|
45
|
-
expect(completions).toHaveLength(
|
|
46
|
+
expect(completions).toHaveLength(5);
|
|
46
47
|
expect(completions?.map((c) => c.value)).toEqual([
|
|
47
48
|
"info",
|
|
48
49
|
"unload",
|
|
49
|
-
"servers",
|
|
50
50
|
"settings",
|
|
51
|
+
"servers",
|
|
52
|
+
"overrides",
|
|
51
53
|
]);
|
|
52
54
|
});
|
|
53
55
|
|
|
@@ -64,7 +66,7 @@ describe("CommandManager", () => {
|
|
|
64
66
|
|
|
65
67
|
it("should provide the server/settings completions by prefix", () => {
|
|
66
68
|
const completions = commandManager.getArgumentCompletions("s");
|
|
67
|
-
expect(completions?.map((c) => c.value)).toEqual(["
|
|
69
|
+
expect(completions?.map((c) => c.value)).toEqual(["settings", "servers"]);
|
|
68
70
|
});
|
|
69
71
|
});
|
|
70
72
|
|
|
@@ -77,7 +79,7 @@ describe("CommandManager", () => {
|
|
|
77
79
|
models: [model1, model2],
|
|
78
80
|
});
|
|
79
81
|
const unloadSettings = makeSettingsStub({
|
|
80
|
-
resolveServers: vi.fn(()
|
|
82
|
+
resolveServers: vi.fn(async () => [server]),
|
|
81
83
|
});
|
|
82
84
|
serverManager = new ServerManager(unloadSettings);
|
|
83
85
|
commandManager = new CommandManager(serverManager, unloadSettings);
|
|
@@ -107,7 +109,7 @@ describe("CommandManager", () => {
|
|
|
107
109
|
models: [model1, model2],
|
|
108
110
|
});
|
|
109
111
|
const infoSettings = makeSettingsStub({
|
|
110
|
-
resolveServers: vi.fn(()
|
|
112
|
+
resolveServers: vi.fn(async () => [server]),
|
|
111
113
|
});
|
|
112
114
|
serverManager = new ServerManager(infoSettings);
|
|
113
115
|
commandManager = new CommandManager(serverManager, infoSettings);
|
|
@@ -133,8 +135,8 @@ describe("CommandManager", () => {
|
|
|
133
135
|
expect(formatMs(1500)).toBe("1500ms");
|
|
134
136
|
});
|
|
135
137
|
|
|
136
|
-
it("should build one item per editable scalar field", () => {
|
|
137
|
-
const items = buildSettingsItems(settingsStub);
|
|
138
|
+
it("should build one item per editable scalar field", async () => {
|
|
139
|
+
const items = await buildSettingsItems(settingsStub);
|
|
138
140
|
expect(items.map((i) => i.id)).toEqual([
|
|
139
141
|
"reactToModelSelect",
|
|
140
142
|
"autoloadOnMessage",
|
|
@@ -220,7 +222,9 @@ describe("CommandManager", () => {
|
|
|
220
222
|
matches: vi.fn(
|
|
221
223
|
(data: string, name: string) =>
|
|
222
224
|
(data === ENTER && name === "tui.select.confirm") ||
|
|
223
|
-
(data === ESC && name === "tui.select.cancel")
|
|
225
|
+
(data === ESC && name === "tui.select.cancel") ||
|
|
226
|
+
(data === "\x1b[A" && name === "tui.select.up") ||
|
|
227
|
+
(data === "\x1b[B" && name === "tui.select.down"),
|
|
224
228
|
),
|
|
225
229
|
}) as unknown as KeybindingsManager;
|
|
226
230
|
|
|
@@ -256,7 +260,7 @@ describe("CommandManager", () => {
|
|
|
256
260
|
|
|
257
261
|
it("should wire the editor to the merged servers and the write path", async () => {
|
|
258
262
|
const editorSettings = makeSettingsStub({
|
|
259
|
-
|
|
263
|
+
getLlamaServers: vi.fn(async () => [{ url: "http://seed:1" }]),
|
|
260
264
|
});
|
|
261
265
|
commandManager = new CommandManager(serverManager, editorSettings);
|
|
262
266
|
const ctx = createMockCtx(() => null);
|
|
@@ -269,7 +273,7 @@ describe("CommandManager", () => {
|
|
|
269
273
|
theme: Theme,
|
|
270
274
|
kb: KeybindingsManager,
|
|
271
275
|
done: (result: undefined) => void,
|
|
272
|
-
) =>
|
|
276
|
+
) => ServerSettingsList;
|
|
273
277
|
const done = vi.fn();
|
|
274
278
|
const editor = factory(
|
|
275
279
|
{ requestRender: vi.fn() } as unknown as TUI,
|
|
@@ -284,14 +288,20 @@ describe("CommandManager", () => {
|
|
|
284
288
|
done,
|
|
285
289
|
);
|
|
286
290
|
|
|
287
|
-
expect(editor).toBeInstanceOf(
|
|
291
|
+
expect(editor).toBeInstanceOf(ServerSettingsList);
|
|
288
292
|
// Seeded with the merged snapshot
|
|
289
293
|
expect(editor.render(80).join("\n")).toContain("http://seed:1");
|
|
290
294
|
|
|
291
|
-
// Add a server through the
|
|
295
|
+
// Add a server through the wizard (URL → ID → name) →
|
|
296
|
+
// setLlamaSetting("servers", …) persists only after the final step
|
|
292
297
|
editor.handleInput("a");
|
|
298
|
+
expect(editor.render(80).join("\n")).toContain("Add server · 1/3");
|
|
293
299
|
for (const ch of "http://new:2") editor.handleInput(ch);
|
|
294
300
|
editor.handleInput(ENTER);
|
|
301
|
+
expect(editor.render(80).join("\n")).toContain("Add server · 2/3");
|
|
302
|
+
editor.handleInput(ENTER); // skip optional ID
|
|
303
|
+
expect(editor.render(80).join("\n")).toContain("Add server · 3/3");
|
|
304
|
+
editor.handleInput(ENTER); // skip optional name → persist
|
|
295
305
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
296
306
|
expect(editorSettings.setLlamaSetting).toHaveBeenCalledWith("servers", [
|
|
297
307
|
{ url: "http://seed:1" },
|
|
@@ -302,6 +312,86 @@ describe("CommandManager", () => {
|
|
|
302
312
|
editor.handleInput(ESC);
|
|
303
313
|
expect(done).toHaveBeenCalledTimes(1);
|
|
304
314
|
});
|
|
315
|
+
|
|
316
|
+
it("should persist nothing when the add wizard is cancelled", async () => {
|
|
317
|
+
const editorSettings = makeSettingsStub({
|
|
318
|
+
getLlamaServers: vi.fn(async () => [{ url: "http://seed:1" }]),
|
|
319
|
+
});
|
|
320
|
+
commandManager = new CommandManager(serverManager, editorSettings);
|
|
321
|
+
const ctx = createMockCtx(() => null);
|
|
322
|
+
|
|
323
|
+
await commandManager.handleCommand("servers", ctx as any, mockPi as any);
|
|
324
|
+
|
|
325
|
+
const factory = vi.mocked(ctx.ui.custom).mock.calls[0][0] as (
|
|
326
|
+
tui: TUI,
|
|
327
|
+
theme: Theme,
|
|
328
|
+
kb: KeybindingsManager,
|
|
329
|
+
done: (result: undefined) => void,
|
|
330
|
+
) => ServerSettingsList;
|
|
331
|
+
const editor = factory(
|
|
332
|
+
{ requestRender: vi.fn() } as unknown as TUI,
|
|
333
|
+
createMockTheme(),
|
|
334
|
+
createMockKeybindings(),
|
|
335
|
+
vi.fn(),
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// Walk to wizard step 2, then abort — nothing must be persisted
|
|
339
|
+
editor.handleInput("a");
|
|
340
|
+
for (const ch of "http://new:2") editor.handleInput(ch);
|
|
341
|
+
editor.handleInput(ENTER);
|
|
342
|
+
editor.handleInput(ESC);
|
|
343
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
344
|
+
|
|
345
|
+
expect(editorSettings.setLlamaSetting).not.toHaveBeenCalled();
|
|
346
|
+
// Back to the list view
|
|
347
|
+
expect(editor.render(80).join("\n")).toContain("http://seed:1");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("should delete the selected server after confirming", async () => {
|
|
351
|
+
const editorSettings = makeSettingsStub({
|
|
352
|
+
getLlamaServers: vi.fn(async () => [
|
|
353
|
+
{ url: "http://seed:1" },
|
|
354
|
+
{ url: "http://doomed:2" },
|
|
355
|
+
]),
|
|
356
|
+
});
|
|
357
|
+
commandManager = new CommandManager(serverManager, editorSettings);
|
|
358
|
+
const ctx = createMockCtx(() => null);
|
|
359
|
+
|
|
360
|
+
await commandManager.handleCommand("servers", ctx as any, mockPi as any);
|
|
361
|
+
|
|
362
|
+
const factory = vi.mocked(ctx.ui.custom).mock.calls[0][0] as (
|
|
363
|
+
tui: TUI,
|
|
364
|
+
theme: Theme,
|
|
365
|
+
kb: KeybindingsManager,
|
|
366
|
+
done: (result: undefined) => void,
|
|
367
|
+
) => ServerSettingsList;
|
|
368
|
+
const editor = factory(
|
|
369
|
+
{ requestRender: vi.fn() } as unknown as TUI,
|
|
370
|
+
createMockTheme(),
|
|
371
|
+
createMockKeybindings(),
|
|
372
|
+
vi.fn(),
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// Move to the second row and open the confirm dialog
|
|
376
|
+
editor.handleInput("\x1b[B");
|
|
377
|
+
editor.handleInput("d");
|
|
378
|
+
expect(editor.render(80).join("\n")).toContain(
|
|
379
|
+
'Delete "http://doomed:2"?',
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Esc keeps the server
|
|
383
|
+
editor.handleInput(ESC);
|
|
384
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
385
|
+
expect(editorSettings.setLlamaSetting).not.toHaveBeenCalled();
|
|
386
|
+
|
|
387
|
+
// Re-open and confirm the default "Delete" selection
|
|
388
|
+
editor.handleInput("d");
|
|
389
|
+
editor.handleInput(ENTER);
|
|
390
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
391
|
+
expect(editorSettings.setLlamaSetting).toHaveBeenCalledWith("servers", [
|
|
392
|
+
{ url: "http://seed:1" },
|
|
393
|
+
]);
|
|
394
|
+
});
|
|
305
395
|
});
|
|
306
396
|
|
|
307
397
|
describe("/models interactive menu", () => {
|
|
@@ -321,7 +411,7 @@ describe("CommandManager", () => {
|
|
|
321
411
|
}),
|
|
322
412
|
);
|
|
323
413
|
const settingsStub = makeSettingsStub({
|
|
324
|
-
resolveServers: vi.fn(()
|
|
414
|
+
resolveServers: vi.fn(async () => servers),
|
|
325
415
|
});
|
|
326
416
|
const serverManager = new ServerManager(settingsStub);
|
|
327
417
|
return {
|
|
@@ -432,7 +522,7 @@ describe("CommandManager", () => {
|
|
|
432
522
|
baseUrl: "http://127.0.0.1:8081",
|
|
433
523
|
models: [modelB],
|
|
434
524
|
});
|
|
435
|
-
vi.mocked(settingsStub.resolveServers).
|
|
525
|
+
vi.mocked(settingsStub.resolveServers).mockResolvedValue([
|
|
436
526
|
...servers,
|
|
437
527
|
serverB,
|
|
438
528
|
]);
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { initTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
getKeybindings,
|
|
4
|
+
type KeybindingsManager,
|
|
5
|
+
type TUI,
|
|
6
|
+
} from "@earendil-works/pi-tui";
|
|
7
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { ConfirmDialog, InputDialog } from "../src/ui/dialog";
|
|
9
|
+
import { ServerSettingsList } from "../src/ui/serverSettingsList";
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
initTheme();
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const ESC = "\x1b";
|
|
17
|
+
const ENTER = "\r";
|
|
18
|
+
|
|
19
|
+
const createMockTheme = (): Theme =>
|
|
20
|
+
({
|
|
21
|
+
fg: (_color: string, text: string) => text,
|
|
22
|
+
bold: (text: string) => text,
|
|
23
|
+
}) as unknown as Theme;
|
|
24
|
+
|
|
25
|
+
const createMockTui = (): TUI => ({ requestRender: vi.fn() }) as unknown as TUI;
|
|
26
|
+
|
|
27
|
+
const createKeybindings = (): KeybindingsManager => getKeybindings();
|
|
28
|
+
|
|
29
|
+
describe("InputDialog", () => {
|
|
30
|
+
const build = (
|
|
31
|
+
overrides: Partial<ConstructorParameters<typeof InputDialog>[0]> = {},
|
|
32
|
+
) => {
|
|
33
|
+
const onSubmit = vi.fn();
|
|
34
|
+
const onCancel = vi.fn();
|
|
35
|
+
const dialog = new InputDialog({
|
|
36
|
+
theme: createMockTheme(),
|
|
37
|
+
tui: createMockTui(),
|
|
38
|
+
title: "Dialog title",
|
|
39
|
+
message: "Server URL",
|
|
40
|
+
placeholder: "http://127.0.0.1:8080",
|
|
41
|
+
validate: (raw) => (raw.startsWith("http") ? raw : null),
|
|
42
|
+
onSubmit,
|
|
43
|
+
onCancel,
|
|
44
|
+
...overrides,
|
|
45
|
+
});
|
|
46
|
+
return { dialog, onSubmit, onCancel };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
it("renders the framed title, message, placeholder and key hints", () => {
|
|
50
|
+
const { dialog } = build();
|
|
51
|
+
const rendered = dialog.render(60).join("\n");
|
|
52
|
+
expect(rendered).toContain("Dialog title");
|
|
53
|
+
expect(rendered).toContain("Server URL");
|
|
54
|
+
expect(rendered).toContain("e.g., http://127.0.0.1:8080");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("submits the validated value on Enter", () => {
|
|
58
|
+
const { dialog, onSubmit, onCancel } = build({
|
|
59
|
+
initialValue: "http://x:1",
|
|
60
|
+
});
|
|
61
|
+
dialog.handleInput(ENTER);
|
|
62
|
+
expect(onSubmit).toHaveBeenCalledWith("http://x:1");
|
|
63
|
+
expect(onCancel).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("keeps the dialog open with an error on invalid input", () => {
|
|
67
|
+
const { dialog, onSubmit, onCancel } = build();
|
|
68
|
+
for (const ch of "no-scheme") dialog.handleInput(ch);
|
|
69
|
+
dialog.handleInput(ENTER);
|
|
70
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
71
|
+
expect(dialog.render(60).join("\n")).toContain('Invalid value "no-scheme"');
|
|
72
|
+
|
|
73
|
+
// Esc still cancels after an error
|
|
74
|
+
dialog.handleInput(ESC);
|
|
75
|
+
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("cancels on Esc without submitting", () => {
|
|
79
|
+
const { dialog, onSubmit, onCancel } = build({
|
|
80
|
+
initialValue: "http://x:1",
|
|
81
|
+
});
|
|
82
|
+
dialog.handleInput(ESC);
|
|
83
|
+
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("ConfirmDialog", () => {
|
|
89
|
+
const build = () => {
|
|
90
|
+
const onConfirm = vi.fn();
|
|
91
|
+
const onCancel = vi.fn();
|
|
92
|
+
const dialog = new ConfirmDialog({
|
|
93
|
+
theme: createMockTheme(),
|
|
94
|
+
tui: createMockTui(),
|
|
95
|
+
title: "Delete server",
|
|
96
|
+
message: 'Delete "http://x:1"?',
|
|
97
|
+
onConfirm,
|
|
98
|
+
onCancel,
|
|
99
|
+
});
|
|
100
|
+
return { dialog, onConfirm, onCancel };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
it("renders the framed message and both options", () => {
|
|
104
|
+
const { dialog } = build();
|
|
105
|
+
const rendered = dialog.render(60).join("\n");
|
|
106
|
+
expect(rendered).toContain("Delete server");
|
|
107
|
+
expect(rendered).toContain('Delete "http://x:1"?');
|
|
108
|
+
expect(rendered).toContain("Delete");
|
|
109
|
+
expect(rendered).toContain("Cancel");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("confirms with Enter on the default selection", () => {
|
|
113
|
+
const { dialog, onConfirm, onCancel } = build();
|
|
114
|
+
dialog.handleInput(ENTER);
|
|
115
|
+
expect(onConfirm).toHaveBeenCalledTimes(1);
|
|
116
|
+
expect(onCancel).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("cancels on Esc", () => {
|
|
120
|
+
const { dialog, onConfirm, onCancel } = build();
|
|
121
|
+
dialog.handleInput(ESC);
|
|
122
|
+
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
123
|
+
expect(onConfirm).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("ServerSettingsList add wizard", () => {
|
|
128
|
+
const buildEditor = (servers: { url: string; id?: string }[] = []) => {
|
|
129
|
+
const persist = vi.fn().mockResolvedValue(undefined);
|
|
130
|
+
const done = vi.fn();
|
|
131
|
+
const onError = vi.fn();
|
|
132
|
+
const editor = new ServerSettingsList({
|
|
133
|
+
tui: createMockTui(),
|
|
134
|
+
theme: createMockTheme(),
|
|
135
|
+
keybindings: createKeybindings(),
|
|
136
|
+
servers: servers as never[],
|
|
137
|
+
persist,
|
|
138
|
+
done,
|
|
139
|
+
onError,
|
|
140
|
+
});
|
|
141
|
+
return { editor, persist, done, onError };
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
it("persists only the URL when the optional steps are skipped", async () => {
|
|
145
|
+
const { editor, persist } = buildEditor();
|
|
146
|
+
|
|
147
|
+
editor.handleInput("a");
|
|
148
|
+
for (const ch of "http://x:1") editor.handleInput(ch);
|
|
149
|
+
editor.handleInput(ENTER); // URL → ID
|
|
150
|
+
editor.handleInput(ENTER); // skip ID → name
|
|
151
|
+
editor.handleInput(ENTER); // skip name → persist
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
153
|
+
|
|
154
|
+
expect(persist).toHaveBeenCalledWith([{ url: "http://x:1" }]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("persists the URL and ID when the ID step is filled", async () => {
|
|
158
|
+
const { editor, persist } = buildEditor();
|
|
159
|
+
|
|
160
|
+
editor.handleInput("a");
|
|
161
|
+
for (const ch of "http://x:1") editor.handleInput(ch);
|
|
162
|
+
editor.handleInput(ENTER);
|
|
163
|
+
for (const ch of "my-id") editor.handleInput(ch);
|
|
164
|
+
editor.handleInput(ENTER); // ID → name
|
|
165
|
+
editor.handleInput(ENTER); // skip name → persist
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
167
|
+
|
|
168
|
+
expect(persist).toHaveBeenCalledWith([{ url: "http://x:1", id: "my-id" }]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("rejects an invalid URL at step 1 and aborts cleanly on Esc", async () => {
|
|
172
|
+
const { editor, persist } = buildEditor();
|
|
173
|
+
|
|
174
|
+
editor.handleInput("a");
|
|
175
|
+
for (const ch of "not-a-url") editor.handleInput(ch);
|
|
176
|
+
editor.handleInput(ENTER);
|
|
177
|
+
expect(persist).not.toHaveBeenCalled();
|
|
178
|
+
expect(editor.render(80).join("\n")).toContain('Invalid value "not-a-url"');
|
|
179
|
+
|
|
180
|
+
editor.handleInput(ESC);
|
|
181
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
182
|
+
expect(persist).not.toHaveBeenCalled();
|
|
183
|
+
// Back at the (empty) list view
|
|
184
|
+
expect(editor.render(80).join("\n")).toContain("(a) add server");
|
|
185
|
+
});
|
|
186
|
+
});
|
package/tests/events.test.ts
CHANGED
|
@@ -299,7 +299,7 @@ describe("EventManager.onBeforeProviderRequest", () => {
|
|
|
299
299
|
|
|
300
300
|
describe("EventManager.onModelSelect", () => {
|
|
301
301
|
beforeEach(() => {
|
|
302
|
-
vi.mocked(settingsStub.resolveReactToModelSelect).
|
|
302
|
+
vi.mocked(settingsStub.resolveReactToModelSelect).mockResolvedValue(true);
|
|
303
303
|
});
|
|
304
304
|
|
|
305
305
|
it("should load the model when reactToModelSelect is true", async () => {
|
|
@@ -319,7 +319,7 @@ describe("EventManager.onModelSelect", () => {
|
|
|
319
319
|
});
|
|
320
320
|
|
|
321
321
|
it("should return early when reactToModelSelect is false", async () => {
|
|
322
|
-
vi.mocked(settingsStub.resolveReactToModelSelect).
|
|
322
|
+
vi.mocked(settingsStub.resolveReactToModelSelect).mockResolvedValue(false);
|
|
323
323
|
|
|
324
324
|
const server = createMockServer({
|
|
325
325
|
models: ["model-a"].map((id) => createMockModel(id)),
|
|
@@ -339,7 +339,7 @@ describe("EventManager.onModelSelect", () => {
|
|
|
339
339
|
|
|
340
340
|
describe("EventManager.autoLoadIfNeeded", () => {
|
|
341
341
|
it("should load the model when autoloadOnMessage is true and model is UNLOADED", async () => {
|
|
342
|
-
vi.mocked(settingsStub.resolveAutoloadOnMessage).
|
|
342
|
+
vi.mocked(settingsStub.resolveAutoloadOnMessage).mockResolvedValue(true);
|
|
343
343
|
|
|
344
344
|
const server = createMockServer({
|
|
345
345
|
models: [
|
|
@@ -358,7 +358,7 @@ describe("EventManager.autoLoadIfNeeded", () => {
|
|
|
358
358
|
});
|
|
359
359
|
|
|
360
360
|
it("should not load the model when autoloadOnMessage is false", async () => {
|
|
361
|
-
vi.mocked(settingsStub.resolveAutoloadOnMessage).
|
|
361
|
+
vi.mocked(settingsStub.resolveAutoloadOnMessage).mockResolvedValue(false);
|
|
362
362
|
|
|
363
363
|
const server = createMockServer({
|
|
364
364
|
models: [
|
|
@@ -377,7 +377,7 @@ describe("EventManager.autoLoadIfNeeded", () => {
|
|
|
377
377
|
});
|
|
378
378
|
|
|
379
379
|
it("should not load the model when model is already LOADED", async () => {
|
|
380
|
-
vi.mocked(settingsStub.resolveAutoloadOnMessage).
|
|
380
|
+
vi.mocked(settingsStub.resolveAutoloadOnMessage).mockResolvedValue(true);
|
|
381
381
|
|
|
382
382
|
const server = createMockServer({
|
|
383
383
|
models: [
|
|
@@ -396,7 +396,7 @@ describe("EventManager.autoLoadIfNeeded", () => {
|
|
|
396
396
|
});
|
|
397
397
|
|
|
398
398
|
it("should not load the model when model is SLEEPING", async () => {
|
|
399
|
-
vi.mocked(settingsStub.resolveAutoloadOnMessage).
|
|
399
|
+
vi.mocked(settingsStub.resolveAutoloadOnMessage).mockResolvedValue(true);
|
|
400
400
|
|
|
401
401
|
const server = createMockServer({
|
|
402
402
|
models: [
|
|
@@ -427,7 +427,7 @@ describe("EventManager with a live ServerManager", () => {
|
|
|
427
427
|
],
|
|
428
428
|
});
|
|
429
429
|
|
|
430
|
-
vi.mocked(settingsStub.resolveServers).
|
|
430
|
+
vi.mocked(settingsStub.resolveServers).mockResolvedValue([serverA]);
|
|
431
431
|
const serverManager = new ServerManager(settingsStub);
|
|
432
432
|
const mockPi = { registerProvider: vi.fn(), unregisterProvider: vi.fn() };
|
|
433
433
|
await serverManager.update(mockPi as any);
|
|
@@ -435,7 +435,10 @@ describe("EventManager with a live ServerManager", () => {
|
|
|
435
435
|
const eventManager = new EventManager(serverManager, settingsStub);
|
|
436
436
|
|
|
437
437
|
// Second scan adds serverB — no manager re-construction
|
|
438
|
-
vi.mocked(settingsStub.resolveServers).
|
|
438
|
+
vi.mocked(settingsStub.resolveServers).mockResolvedValue([
|
|
439
|
+
serverA,
|
|
440
|
+
serverB,
|
|
441
|
+
]);
|
|
439
442
|
await serverManager.update(mockPi as any);
|
|
440
443
|
|
|
441
444
|
// onBeforeProviderRequest sees the new server's models
|
|
@@ -447,7 +450,7 @@ describe("EventManager with a live ServerManager", () => {
|
|
|
447
450
|
expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.medium);
|
|
448
451
|
|
|
449
452
|
// onModelSelect sees the new server's models too
|
|
450
|
-
vi.mocked(settingsStub.resolveReactToModelSelect).
|
|
453
|
+
vi.mocked(settingsStub.resolveReactToModelSelect).mockResolvedValue(true);
|
|
451
454
|
const selectCtx = createMockCtx();
|
|
452
455
|
await eventManager.onModelSelect(
|
|
453
456
|
{ model: { provider: serverB.providerId, id: "model-b" } } as any,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { Mode } from "../src/enums/mode";
|
|
3
|
-
import { Status } from "../src/enums/status";
|
|
4
3
|
import { DataProperty } from "../src/interfaces/endpoints/models";
|
|
5
4
|
import { LegacyModel } from "../src/models/legacyModel";
|
|
6
5
|
import { createMockServer, mockRpc } from "./mocks";
|
|
@@ -47,28 +46,14 @@ describe("LegacyModel capabilities", () => {
|
|
|
47
46
|
|
|
48
47
|
expect(capabilities).toEqual(["text"]);
|
|
49
48
|
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("LegacyModel getStatus", () => {
|
|
53
|
-
it("should return LOADED when not sleeping", async () => {
|
|
54
|
-
mockRpc.mockResolvedValueOnce({ is_sleeping: false });
|
|
55
|
-
|
|
56
|
-
const model = createModel();
|
|
57
|
-
const status = await model.getStatus();
|
|
58
49
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`/props?model=${model.id}&autoload=false`,
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("should return SLEEPING when is_sleeping is true", async () => {
|
|
66
|
-
mockRpc.mockResolvedValueOnce({ is_sleeping: true });
|
|
50
|
+
it("should fall back to text-only when auth fails", async () => {
|
|
51
|
+
mockRpc.mockRejectedValue(new Error("401 Unauthorized"));
|
|
67
52
|
|
|
68
53
|
const model = createModel();
|
|
69
|
-
const
|
|
54
|
+
const capabilities = await model.getCapabilities();
|
|
70
55
|
|
|
71
|
-
expect(
|
|
56
|
+
expect(capabilities).toEqual(["text"]);
|
|
72
57
|
});
|
|
73
58
|
});
|
|
74
59
|
|
package/tests/mocks.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "../src/constants";
|
|
13
13
|
import { Mode } from "../src/enums/mode";
|
|
14
14
|
import { Status } from "../src/enums/status";
|
|
15
|
-
import type {
|
|
15
|
+
import type { ModelOverride } from "../src/interfaces/settings";
|
|
16
16
|
import type { LlamaSettingsManager } from "../src/managers/settings";
|
|
17
17
|
import { BaseModel } from "../src/models/baseModel";
|
|
18
18
|
import { Server } from "../src/server";
|
|
@@ -32,18 +32,19 @@ export const makeSettingsStub = (
|
|
|
32
32
|
overrides: Partial<LlamaSettingsManager> = {},
|
|
33
33
|
): LlamaSettingsManager =>
|
|
34
34
|
({
|
|
35
|
-
|
|
35
|
+
getLlamaSettings: vi.fn(async () => ({})),
|
|
36
|
+
getLlamaServers: vi.fn(async () => []),
|
|
37
|
+
resolveTimeouts: vi.fn(async () => ({
|
|
36
38
|
pollingTimeout: POLLING_TIMEOUT,
|
|
37
39
|
serverTimeout: SERVER_TIMEOUT,
|
|
38
40
|
})),
|
|
39
|
-
resolveServers: vi.fn(()
|
|
40
|
-
resolveSortBy: vi.fn(() => SORT_BY),
|
|
41
|
+
resolveServers: vi.fn(async () => []),
|
|
42
|
+
resolveSortBy: vi.fn(async () => SORT_BY),
|
|
41
43
|
resolveApiKey: vi.fn(() => API_KEY_PLACEHOLDER),
|
|
42
|
-
resolveReactToModelSelect: vi.fn(() => REACT_TO_MODEL_SELECT),
|
|
43
|
-
resolveAutoloadOnMessage: vi.fn(() => AUTOLOAD_ON_MESSAGE),
|
|
44
|
+
resolveReactToModelSelect: vi.fn(async () => REACT_TO_MODEL_SELECT),
|
|
45
|
+
resolveAutoloadOnMessage: vi.fn(async () => AUTOLOAD_ON_MESSAGE),
|
|
44
46
|
resolveThinkingLevel: vi.fn(() => undefined),
|
|
45
47
|
resolveThinkingBudgets: vi.fn(() => ({ ...THINKING_BUDGETS })),
|
|
46
|
-
llamaServers: [] as LlamaServer[],
|
|
47
48
|
takeWarnings: vi.fn((): string[] => []),
|
|
48
49
|
setLlamaSetting: vi.fn(() => Promise.resolve()),
|
|
49
50
|
...overrides,
|
|
@@ -102,6 +103,7 @@ export type MockServerOverrides = Partial<
|
|
|
102
103
|
models?: BaseModel[];
|
|
103
104
|
pollingTimeout?: number;
|
|
104
105
|
serverTimeout?: number;
|
|
106
|
+
overrides?: Record<string, ModelOverride>;
|
|
105
107
|
};
|
|
106
108
|
|
|
107
109
|
/**
|
|
@@ -124,6 +126,7 @@ export const createMockServer = (
|
|
|
124
126
|
models,
|
|
125
127
|
pollingTimeout,
|
|
126
128
|
serverTimeout,
|
|
129
|
+
overrides: modelOverrides,
|
|
127
130
|
initialize,
|
|
128
131
|
...members
|
|
129
132
|
} = overrides;
|
|
@@ -131,7 +134,7 @@ export const createMockServer = (
|
|
|
131
134
|
const settings = makeSettingsStub({
|
|
132
135
|
...(apiKey !== undefined && { resolveApiKey: vi.fn(() => apiKey) }),
|
|
133
136
|
...((pollingTimeout !== undefined || serverTimeout !== undefined) && {
|
|
134
|
-
resolveTimeouts: vi.fn(() => ({
|
|
137
|
+
resolveTimeouts: vi.fn(async () => ({
|
|
135
138
|
pollingTimeout: pollingTimeout ?? POLLING_TIMEOUT,
|
|
136
139
|
serverTimeout: serverTimeout ?? SERVER_TIMEOUT,
|
|
137
140
|
})),
|
|
@@ -145,6 +148,7 @@ export const createMockServer = (
|
|
|
145
148
|
baseUrl: baseUrl ?? "http://127.0.0.1:8080",
|
|
146
149
|
customId,
|
|
147
150
|
customName,
|
|
151
|
+
overrides: modelOverrides,
|
|
148
152
|
},
|
|
149
153
|
{
|
|
150
154
|
createApiClient: () => apiClient,
|