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.
@@ -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 type { Server } from "../src/server";
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(4);
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(["servers", "settings"]);
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((): Server[] => [server]),
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((): Server[] => [server]),
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
- llamaServers: [{ url: "http://seed:1" }],
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
- ) => ServerListEditor;
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(ServerListEditor);
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 editor → setLlamaSetting("servers", …)
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((): Server[] => servers),
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).mockReturnValue([
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
+ });
@@ -299,7 +299,7 @@ describe("EventManager.onBeforeProviderRequest", () => {
299
299
 
300
300
  describe("EventManager.onModelSelect", () => {
301
301
  beforeEach(() => {
302
- vi.mocked(settingsStub.resolveReactToModelSelect).mockReturnValue(true);
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).mockReturnValue(false);
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).mockReturnValue(true);
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).mockReturnValue(false);
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).mockReturnValue(true);
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).mockReturnValue(true);
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).mockReturnValue([serverA]);
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).mockReturnValue([serverA, serverB]);
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).mockReturnValue(true);
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
- expect(status).toBe(Status.LOADED);
60
- expect(mockRpc).toHaveBeenCalledWith(
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 status = await model.getStatus();
54
+ const capabilities = await model.getCapabilities();
70
55
 
71
- expect(status).toBe(Status.SLEEPING);
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 { LlamaServer } from "../src/interfaces/settings";
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
- resolveTimeouts: vi.fn(() => ({
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((): Server[] => []),
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,