pi-llama-cpp 0.10.0 → 0.11.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,59 +1,29 @@
1
- import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { THINKING_BUDGETS } from "../src/constants";
3
3
  import { Status } from "../src/enums/status";
4
- import { createMockModel, createMockServer } from "./mocks";
5
-
6
- // Mock settings — reactToModelSelect and autoloadOnMessage
7
- const mockSettings = {
8
- resolveReactToModelSelect: vi.fn(() => true),
9
- resolveAutoloadOnMessage: vi.fn(() => false),
10
- resolveThinkingLevel: vi.fn(() => "medium"),
11
- resolveThinkingBudgets: vi.fn(() => ({ ...THINKING_BUDGETS })),
12
- };
13
-
14
- // Wire resolveThinkingBudgets to use the SettingsManager mock when set
15
- mockSettings.resolveThinkingBudgets.mockImplementation(() => {
16
- const userBudgets = mockSettingsManager.getThinkingBudgets();
17
- if (userBudgets) {
18
- return { ...THINKING_BUDGETS, ...userBudgets };
19
- }
20
- return { ...THINKING_BUDGETS };
21
- });
22
-
23
- // Create a mutable mock object shared across tests
24
- const mockSettingsManager = {
25
- getDefaultThinkingLevel: vi.fn(() => "medium"),
26
- getThinkingBudgets: vi.fn<() => Record<string, number> | undefined>(),
27
- };
28
-
29
- vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
30
- const actual =
31
- await importOriginal<typeof import("@earendil-works/pi-coding-agent")>();
32
- return {
33
- ...actual,
34
- SettingsManager: {
35
- create: () => mockSettingsManager,
36
- },
37
- };
38
- });
39
-
40
- vi.mock("../src/managers/settings", () => ({
41
- settings: mockSettings,
42
- }));
43
-
44
- let EventManager: typeof import("../src/managers/events").EventManager;
45
-
46
- beforeAll(async () => {
47
- const mod = await vi.importActual("../src/managers/events");
48
- EventManager =
49
- mod.EventManager as typeof import("../src/managers/events").EventManager;
50
- });
4
+ import { EventManager } from "../src/managers/events";
5
+ import { ServerManager } from "../src/managers/server";
6
+ import type { Server } from "../src/server";
7
+ import { createMockModel, createMockServer, makeSettingsStub } from "./mocks";
8
+
9
+ /**
10
+ * Injected settings stub (EventManager and — in the live-list test — the
11
+ * real ServerManager). Recreated in `beforeEach` so per-case overrides
12
+ * (budgets, autoload, …) cannot leak between tests.
13
+ */
14
+ let settingsStub = makeSettingsStub();
15
+
16
+ /**
17
+ * Builds an EventManager on a ServerManager stub exposing `servers`.
18
+ * (The real ServerManager is exercised in the live-list test below.)
19
+ */
20
+ const createEventManager = (...servers: Server[]) =>
21
+ new EventManager({ servers } as unknown as ServerManager, settingsStub);
51
22
 
52
23
  beforeEach(() => {
53
24
  vi.restoreAllMocks();
54
25
  EventManager.resetInflightModel();
55
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("medium");
56
- mockSettingsManager.getThinkingBudgets.mockReturnValue(undefined);
26
+ settingsStub = makeSettingsStub();
57
27
  });
58
28
 
59
29
  const createPayload = (modelId: string) => ({
@@ -97,12 +67,14 @@ describe("EventManager.onBeforeProviderRequest", () => {
97
67
  ])(
98
68
  'level "$level" should return $expected',
99
69
  async ({ level, expected }) => {
100
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue(level);
70
+ vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue(
71
+ level as any,
72
+ );
101
73
 
102
74
  const server = createMockServer({
103
75
  models: ["model-a"].map((id) => createMockModel(id)),
104
76
  });
105
- const eventManager = new EventManager([server]);
77
+ const eventManager = createEventManager(server);
106
78
  const event = { payload: createPayload("model-a") };
107
79
 
108
80
  const ctx = createMockCtx(level as any);
@@ -117,12 +89,12 @@ describe("EventManager.onBeforeProviderRequest", () => {
117
89
  );
118
90
 
119
91
  it("should preserve original payload fields alongside new ones", async () => {
120
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("low");
92
+ vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("low");
121
93
 
122
94
  const server = createMockServer({
123
95
  models: ["model-b"].map((id) => createMockModel(id)),
124
96
  });
125
- const eventManager = new EventManager([server]);
97
+ const eventManager = createEventManager(server);
126
98
  const event = {
127
99
  payload: {
128
100
  model: "model-b",
@@ -148,7 +120,7 @@ describe("EventManager.onBeforeProviderRequest", () => {
148
120
  const server = createMockServer({
149
121
  models: ["model-a"].map((id) => createMockModel(id)),
150
122
  });
151
- const eventManager = new EventManager([server]);
123
+ const eventManager = createEventManager(server);
152
124
  const event = { payload: createNonLlamaPayload() };
153
125
 
154
126
  const ctx = createMockCtx();
@@ -166,7 +138,7 @@ describe("EventManager.onBeforeProviderRequest", () => {
166
138
  const server = createMockServer({
167
139
  models: ["model-a"].map((id) => createMockModel(id)),
168
140
  });
169
- const eventManager = new EventManager([server]);
141
+ const eventManager = createEventManager(server);
170
142
  const event = { payload: { messages: [] } };
171
143
 
172
144
  const ctx = createMockCtx();
@@ -181,13 +153,16 @@ describe("EventManager.onBeforeProviderRequest", () => {
181
153
 
182
154
  describe("user-defined budget overrides", () => {
183
155
  it("should use user-defined budgets instead of defaults", async () => {
184
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("low");
185
- mockSettingsManager.getThinkingBudgets.mockReturnValue({ low: 4096 });
156
+ vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("low");
157
+ vi.mocked(settingsStub.resolveThinkingBudgets).mockReturnValue({
158
+ ...THINKING_BUDGETS,
159
+ low: 4096,
160
+ });
186
161
 
187
162
  const server = createMockServer({
188
163
  models: ["model-a"].map((id) => createMockModel(id)),
189
164
  });
190
- const eventManager = new EventManager([server]);
165
+ const eventManager = createEventManager(server);
191
166
  const event = { payload: createPayload("model-a") };
192
167
 
193
168
  const ctx = createMockCtx("low");
@@ -200,13 +175,16 @@ describe("EventManager.onBeforeProviderRequest", () => {
200
175
  });
201
176
 
202
177
  it("should merge user budgets with defaults (partial override)", async () => {
203
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("medium");
204
- mockSettingsManager.getThinkingBudgets.mockReturnValue({ low: 4096 });
178
+ vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("medium");
179
+ vi.mocked(settingsStub.resolveThinkingBudgets).mockReturnValue({
180
+ ...THINKING_BUDGETS,
181
+ low: 4096,
182
+ });
205
183
 
206
184
  const server = createMockServer({
207
185
  models: ["model-a"].map((id) => createMockModel(id)),
208
186
  });
209
- const eventManager = new EventManager([server]);
187
+ const eventManager = createEventManager(server);
210
188
  const event = { payload: createPayload("model-a") };
211
189
 
212
190
  const ctx = createMockCtx("medium");
@@ -224,8 +202,9 @@ describe("EventManager.onBeforeProviderRequest", () => {
224
202
 
225
203
  describe("edge cases", () => {
226
204
  it("should ignore invalid keys in user budgets (they are silently dropped)", async () => {
227
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("medium");
228
- mockSettingsManager.getThinkingBudgets.mockReturnValue({
205
+ vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("medium");
206
+ vi.mocked(settingsStub.resolveThinkingBudgets).mockReturnValue({
207
+ ...THINKING_BUDGETS,
229
208
  foo: 999,
230
209
  bar: 123,
231
210
  } as any);
@@ -233,7 +212,7 @@ describe("EventManager.onBeforeProviderRequest", () => {
233
212
  const server = createMockServer({
234
213
  models: ["model-a"].map((id) => createMockModel(id)),
235
214
  });
236
- const eventManager = new EventManager([server]);
215
+ const eventManager = createEventManager(server);
237
216
  const event = { payload: createPayload("model-a") };
238
217
 
239
218
  const ctx = createMockCtx("medium");
@@ -247,15 +226,16 @@ describe("EventManager.onBeforeProviderRequest", () => {
247
226
  });
248
227
 
249
228
  it("should not allow overriding 'off' — thinking stays disabled", async () => {
250
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("off");
251
- mockSettingsManager.getThinkingBudgets.mockReturnValue({
229
+ vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("off");
230
+ vi.mocked(settingsStub.resolveThinkingBudgets).mockReturnValue({
231
+ ...THINKING_BUDGETS,
252
232
  off: 99999,
253
- } as any);
233
+ });
254
234
 
255
235
  const server = createMockServer({
256
236
  models: ["model-a"].map((id) => createMockModel(id)),
257
237
  });
258
- const eventManager = new EventManager([server]);
238
+ const eventManager = createEventManager(server);
259
239
  const event = { payload: createPayload("model-a") };
260
240
 
261
241
  const ctx = createMockCtx("off");
@@ -271,15 +251,16 @@ describe("EventManager.onBeforeProviderRequest", () => {
271
251
  });
272
252
 
273
253
  it("should not inject budget for 'max' — unlimited reasoning", async () => {
274
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("max");
275
- mockSettingsManager.getThinkingBudgets.mockReturnValue({
254
+ vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("max");
255
+ vi.mocked(settingsStub.resolveThinkingBudgets).mockReturnValue({
256
+ ...THINKING_BUDGETS,
276
257
  max: 1,
277
- } as any);
258
+ });
278
259
 
279
260
  const server = createMockServer({
280
261
  models: ["model-a"].map((id) => createMockModel(id)),
281
262
  });
282
- const eventManager = new EventManager([server]);
263
+ const eventManager = createEventManager(server);
283
264
  const event = { payload: createPayload("model-a") };
284
265
 
285
266
  const ctx = createMockCtx("max");
@@ -293,13 +274,16 @@ describe("EventManager.onBeforeProviderRequest", () => {
293
274
  });
294
275
 
295
276
  it("should handle empty user budgets gracefully", async () => {
296
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("high");
297
- mockSettingsManager.getThinkingBudgets.mockReturnValue({});
277
+ vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("high");
278
+ // No user overrides — the resolver returns the default budget table
279
+ vi.mocked(settingsStub.resolveThinkingBudgets).mockReturnValue({
280
+ ...THINKING_BUDGETS,
281
+ });
298
282
 
299
283
  const server = createMockServer({
300
284
  models: ["model-a"].map((id) => createMockModel(id)),
301
285
  });
302
- const eventManager = new EventManager([server]);
286
+ const eventManager = createEventManager(server);
303
287
  const event = { payload: createPayload("model-a") };
304
288
 
305
289
  const ctx = createMockCtx("high");
@@ -315,14 +299,14 @@ describe("EventManager.onBeforeProviderRequest", () => {
315
299
 
316
300
  describe("EventManager.onModelSelect", () => {
317
301
  beforeEach(() => {
318
- mockSettings.resolveReactToModelSelect.mockReturnValue(true);
302
+ vi.mocked(settingsStub.resolveReactToModelSelect).mockReturnValue(true);
319
303
  });
320
304
 
321
305
  it("should load the model when reactToModelSelect is true", async () => {
322
306
  const server = createMockServer({
323
307
  models: ["model-a"].map((id) => createMockModel(id)),
324
308
  });
325
- const eventManager = new EventManager([server]);
309
+ const eventManager = createEventManager(server);
326
310
  const ctx = createMockCtx();
327
311
 
328
312
  const event = {
@@ -335,12 +319,12 @@ describe("EventManager.onModelSelect", () => {
335
319
  });
336
320
 
337
321
  it("should return early when reactToModelSelect is false", async () => {
338
- mockSettings.resolveReactToModelSelect.mockReturnValue(false);
322
+ vi.mocked(settingsStub.resolveReactToModelSelect).mockReturnValue(false);
339
323
 
340
324
  const server = createMockServer({
341
325
  models: ["model-a"].map((id) => createMockModel(id)),
342
326
  });
343
- const eventManager = new EventManager([server]);
327
+ const eventManager = createEventManager(server);
344
328
  const ctx = createMockCtx();
345
329
 
346
330
  const event = {
@@ -355,7 +339,7 @@ describe("EventManager.onModelSelect", () => {
355
339
 
356
340
  describe("EventManager.autoLoadIfNeeded", () => {
357
341
  it("should load the model when autoloadOnMessage is true and model is UNLOADED", async () => {
358
- mockSettings.resolveAutoloadOnMessage.mockReturnValue(true);
342
+ vi.mocked(settingsStub.resolveAutoloadOnMessage).mockReturnValue(true);
359
343
 
360
344
  const server = createMockServer({
361
345
  models: [
@@ -365,7 +349,7 @@ describe("EventManager.autoLoadIfNeeded", () => {
365
349
  }),
366
350
  ],
367
351
  });
368
- const eventManager = new EventManager([server]);
352
+ const eventManager = createEventManager(server);
369
353
  const model = server.models[0];
370
354
 
371
355
  await (eventManager as any).autoLoadIfNeeded(model);
@@ -374,7 +358,7 @@ describe("EventManager.autoLoadIfNeeded", () => {
374
358
  });
375
359
 
376
360
  it("should not load the model when autoloadOnMessage is false", async () => {
377
- mockSettings.resolveAutoloadOnMessage.mockReturnValue(false);
361
+ vi.mocked(settingsStub.resolveAutoloadOnMessage).mockReturnValue(false);
378
362
 
379
363
  const server = createMockServer({
380
364
  models: [
@@ -384,7 +368,7 @@ describe("EventManager.autoLoadIfNeeded", () => {
384
368
  }),
385
369
  ],
386
370
  });
387
- const eventManager = new EventManager([server]);
371
+ const eventManager = createEventManager(server);
388
372
  const model = server.models[0];
389
373
 
390
374
  await (eventManager as any).autoLoadIfNeeded(model);
@@ -393,7 +377,7 @@ describe("EventManager.autoLoadIfNeeded", () => {
393
377
  });
394
378
 
395
379
  it("should not load the model when model is already LOADED", async () => {
396
- mockSettings.resolveAutoloadOnMessage.mockReturnValue(true);
380
+ vi.mocked(settingsStub.resolveAutoloadOnMessage).mockReturnValue(true);
397
381
 
398
382
  const server = createMockServer({
399
383
  models: [
@@ -403,7 +387,7 @@ describe("EventManager.autoLoadIfNeeded", () => {
403
387
  }),
404
388
  ],
405
389
  });
406
- const eventManager = new EventManager([server]);
390
+ const eventManager = createEventManager(server);
407
391
  const model = server.models[0];
408
392
 
409
393
  await (eventManager as any).autoLoadIfNeeded(model);
@@ -412,7 +396,7 @@ describe("EventManager.autoLoadIfNeeded", () => {
412
396
  });
413
397
 
414
398
  it("should not load the model when model is SLEEPING", async () => {
415
- mockSettings.resolveAutoloadOnMessage.mockReturnValue(true);
399
+ vi.mocked(settingsStub.resolveAutoloadOnMessage).mockReturnValue(true);
416
400
 
417
401
  const server = createMockServer({
418
402
  models: [
@@ -422,7 +406,7 @@ describe("EventManager.autoLoadIfNeeded", () => {
422
406
  }),
423
407
  ],
424
408
  });
425
- const eventManager = new EventManager([server]);
409
+ const eventManager = createEventManager(server);
426
410
  const model = server.models[0];
427
411
 
428
412
  await (eventManager as any).autoLoadIfNeeded(model);
@@ -430,3 +414,48 @@ describe("EventManager.autoLoadIfNeeded", () => {
430
414
  expect(model.load).not.toHaveBeenCalled();
431
415
  });
432
416
  });
417
+
418
+ describe("EventManager with a live ServerManager", () => {
419
+ it("should observe servers added after construction", async () => {
420
+ const serverA = createMockServer({
421
+ models: [createMockModel("model-a")],
422
+ });
423
+ const serverB = createMockServer({
424
+ baseUrl: "http://127.0.0.1:8081",
425
+ models: [
426
+ createMockModel("model-b", { serverUrl: "http://127.0.0.1:8081" }),
427
+ ],
428
+ });
429
+
430
+ vi.mocked(settingsStub.resolveServers).mockReturnValue([serverA]);
431
+ const serverManager = new ServerManager(settingsStub);
432
+ const mockPi = { registerProvider: vi.fn(), unregisterProvider: vi.fn() };
433
+ await serverManager.update(mockPi as any);
434
+
435
+ const eventManager = new EventManager(serverManager, settingsStub);
436
+
437
+ // Second scan adds serverB — no manager re-construction
438
+ vi.mocked(settingsStub.resolveServers).mockReturnValue([serverA, serverB]);
439
+ await serverManager.update(mockPi as any);
440
+
441
+ // onBeforeProviderRequest sees the new server's models
442
+ const ctx = createMockCtx("medium");
443
+ const result = (await eventManager.onBeforeProviderRequest(
444
+ { payload: createPayload("model-b") } as any,
445
+ ctx,
446
+ )) as Record<string, unknown>;
447
+ expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.medium);
448
+
449
+ // onModelSelect sees the new server's models too
450
+ vi.mocked(settingsStub.resolveReactToModelSelect).mockReturnValue(true);
451
+ const selectCtx = createMockCtx();
452
+ await eventManager.onModelSelect(
453
+ { model: { provider: serverB.providerId, id: "model-b" } } as any,
454
+ selectCtx,
455
+ );
456
+ expect(selectCtx.ui.notify).toHaveBeenCalledWith(
457
+ "Loading model-b...",
458
+ "info",
459
+ );
460
+ });
461
+ });
package/tests/mocks.ts CHANGED
@@ -1,50 +1,160 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { vi } from "vitest";
3
+ import type { ApiClient } from "../src/api/client";
4
+ import {
5
+ API_KEY_PLACEHOLDER,
6
+ AUTOLOAD_ON_MESSAGE,
7
+ POLLING_TIMEOUT,
8
+ REACT_TO_MODEL_SELECT,
9
+ SERVER_TIMEOUT,
10
+ SORT_BY,
11
+ THINKING_BUDGETS,
12
+ } from "../src/constants";
3
13
  import { Mode } from "../src/enums/mode";
4
- import { ServerStatus } from "../src/enums/serverStatus";
5
14
  import { Status } from "../src/enums/status";
15
+ import type { LlamaServer } from "../src/interfaces/settings";
16
+ import type { LlamaSettingsManager } from "../src/managers/settings";
6
17
  import { BaseModel } from "../src/models/baseModel";
7
18
  import { Server } from "../src/server";
19
+ import type { SSEManager } from "../src/sse/manager";
8
20
 
9
21
  /** Shared mock RPC — each test configures it */
10
22
  export const mockRpc = vi.fn();
11
23
 
12
- /** Default mock server that assumes everything works */
24
+ /**
25
+ * Hand-made stub of `LlamaSettingsManager` for constructor injection —
26
+ * replaces whole-module `vi.mock` blocks and per-case `vi.spyOn` on the
27
+ * real singleton. Defaults give every consumer-used member a benign value;
28
+ * a case that cares passes just that member as an override. The single
29
+ * `as unknown as` cast lives here so test bodies stay cast-free.
30
+ */
31
+ export const makeSettingsStub = (
32
+ overrides: Partial<LlamaSettingsManager> = {},
33
+ ): LlamaSettingsManager =>
34
+ ({
35
+ resolveTimeouts: vi.fn(() => ({
36
+ pollingTimeout: POLLING_TIMEOUT,
37
+ serverTimeout: SERVER_TIMEOUT,
38
+ })),
39
+ resolveServers: vi.fn((): Server[] => []),
40
+ resolveSortBy: vi.fn(() => SORT_BY),
41
+ resolveApiKey: vi.fn(() => API_KEY_PLACEHOLDER),
42
+ resolveReactToModelSelect: vi.fn(() => REACT_TO_MODEL_SELECT),
43
+ resolveAutoloadOnMessage: vi.fn(() => AUTOLOAD_ON_MESSAGE),
44
+ resolveThinkingLevel: vi.fn(() => undefined),
45
+ resolveThinkingBudgets: vi.fn(() => ({ ...THINKING_BUDGETS })),
46
+ llamaServers: [] as LlamaServer[],
47
+ takeWarnings: vi.fn((): string[] => []),
48
+ setLlamaSetting: vi.fn(() => Promise.resolve()),
49
+ ...overrides,
50
+ }) as unknown as LlamaSettingsManager;
51
+
52
+ /**
53
+ * Fake `ApiClient`/`SSEManager` pair for injection via `ServerDeps`. The
54
+ * ApiClient delegates every request to the shared `mockRpc`, keyed by the
55
+ * endpoint path (and body for POSTs) — the same contract the old hand-rolled
56
+ * mock server used, so tests keep configuring responses on `mockRpc`. The
57
+ * SSEManager is inert: `disconnect` is a spy and `probeSSE` resolves false,
58
+ * steering load flows onto the HTTP polling path.
59
+ */
60
+ export const createFakeClients = (): {
61
+ apiClient: ApiClient;
62
+ sseManager: SSEManager;
63
+ } => {
64
+ const apiClient = {
65
+ get: (endpoint: string) => mockRpc(endpoint),
66
+ post: (endpoint: string, body?: Record<string, unknown>) =>
67
+ mockRpc(endpoint, body),
68
+ clearCache: vi.fn(),
69
+ } as unknown as ApiClient;
70
+ const sseManager = {
71
+ disconnect: vi.fn(),
72
+ probeSSE: vi.fn(async () => false),
73
+ subscribeToStatus: vi.fn(),
74
+ subscribeToProgress: vi.fn(() => () => {}),
75
+ } as unknown as SSEManager;
76
+ return { apiClient, sseManager };
77
+ };
78
+
79
+ /**
80
+ * Overrides for {@link createMockServer}. Identity (`baseUrl`/`customId`/
81
+ * `customName`), `apiKey` and the timeout getters are constructor wiring
82
+ * (ServerOptions + a synthesized settings stub) — a real Server has no
83
+ * assignable properties by those names. Everything else is shadowed onto the
84
+ * instance as-is (e.g. a custom `initialize`).
85
+ */
86
+ export type MockServerOverrides = Partial<
87
+ Omit<
88
+ Server,
89
+ | "baseUrl"
90
+ | "providerId"
91
+ | "providerName"
92
+ | "pollingTimeout"
93
+ | "serverTimeout"
94
+ | "sseManager"
95
+ | "models"
96
+ >
97
+ > & {
98
+ baseUrl?: string;
99
+ customId?: string;
100
+ customName?: string;
101
+ apiKey?: string;
102
+ models?: BaseModel[];
103
+ pollingTimeout?: number;
104
+ serverTimeout?: number;
105
+ };
106
+
107
+ /**
108
+ * Builds a real `Server` wired with fake collaborators (`createFakeClients`),
109
+ * replacing the old hand-rolled `Partial<Server>` mock whose `isReady` /
110
+ * `initialize` re-implementations drifted from the real ones.
111
+ *
112
+ * Seeded `models` land directly in `server.models`; `initialize` is stubbed
113
+ * to a no-op so a `ServerManager.update()` scan cannot wipe the seeds with
114
+ * fetched data — pass an `initialize` override to restore the real flow.
115
+ */
13
116
  export const createMockServer = (
14
- overrides: Partial<Server & { apiKey?: string }> = {},
117
+ overrides: MockServerOverrides = {},
15
118
  ): Server => {
16
- const models: BaseModel[] = [];
17
- const server: Partial<Server> = {
18
- baseUrl: "http://127.0.0.1:8080",
119
+ const {
120
+ baseUrl,
121
+ customId,
122
+ customName,
123
+ apiKey,
19
124
  models,
20
- getApiKey: () => overrides.apiKey ?? "",
21
- fetchModels: () => mockRpc("/v1/models"),
22
- fetchModelProps: (modelId: string) =>
23
- mockRpc(`/props?model=${modelId}&autoload=false`),
24
- fetchServerHealth: () => mockRpc("/health"),
25
- fetchServerProps: () => mockRpc("/props?autoload=false"),
26
- postRequest: (resource: "load" | "unload", model: string) =>
27
- mockRpc(`/models/${resource}`, { model }),
28
- isReady: async (timeout: number) => {
29
- try {
30
- const r = await mockRpc("/health");
31
- return r.status === "ok"
32
- ? ServerStatus.READY
33
- : ServerStatus.UNREACHABLE;
34
- } catch {
35
- return ServerStatus.UNREACHABLE;
36
- }
125
+ pollingTimeout,
126
+ serverTimeout,
127
+ initialize,
128
+ ...members
129
+ } = overrides;
130
+
131
+ const settings = makeSettingsStub({
132
+ ...(apiKey !== undefined && { resolveApiKey: vi.fn(() => apiKey) }),
133
+ ...((pollingTimeout !== undefined || serverTimeout !== undefined) && {
134
+ resolveTimeouts: vi.fn(() => ({
135
+ pollingTimeout: pollingTimeout ?? POLLING_TIMEOUT,
136
+ serverTimeout: serverTimeout ?? SERVER_TIMEOUT,
137
+ })),
138
+ }),
139
+ });
140
+
141
+ const { apiClient, sseManager } = createFakeClients();
142
+ const server = new Server(
143
+ settings,
144
+ {
145
+ baseUrl: baseUrl ?? "http://127.0.0.1:8080",
146
+ customId,
147
+ customName,
37
148
  },
38
- initialize: async () => {
39
- const { data } = (await mockRpc("/v1/models")) as {
40
- data: BaseModel[];
41
- };
42
- models.length = 0;
43
- models.push(...(data ?? []));
149
+ {
150
+ createApiClient: () => apiClient,
151
+ createSSEManager: () => sseManager,
44
152
  },
45
- ...overrides,
46
- };
47
- return server as Server;
153
+ );
154
+ if (models) server.models.push(...models);
155
+ return Object.assign(server, members, {
156
+ initialize: initialize ?? (async () => {}),
157
+ });
48
158
  };
49
159
 
50
160
  /** Helper to create a mock BaseModel */
@@ -72,10 +182,12 @@ export const createMockModel = (
72
182
  export const createMockCtx = (
73
183
  selectFn: (prompt: string, options: string[]) => string | null,
74
184
  ) => ({
185
+ mode: "tui",
75
186
  cwd: "/tmp/test",
76
187
  ui: {
77
188
  select: vi.fn(selectFn),
78
189
  notify: vi.fn(),
190
+ custom: vi.fn(),
79
191
  theme: {
80
192
  fg: (color: string, text: string) => text,
81
193
  },
@@ -89,6 +201,7 @@ export const createMockCtx = (
89
201
  export const createMockPi = () => ({
90
202
  setModel: vi.fn(),
91
203
  registerProvider: vi.fn(),
204
+ unregisterProvider: vi.fn(),
92
205
  });
93
206
 
94
207
  /** Create a mock Pi context for EventManager */