pi-llama-cpp 0.9.2 → 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,37 +1,29 @@
1
- import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { DEFAULT_THINKING_BUDGETS } from "../src/constants";
3
- import { createMockModel, createMockServer } from "./mocks";
4
-
5
- // Create a mutable mock object shared across tests
6
- const mockSettingsManager = {
7
- getDefaultThinkingLevel: vi.fn(() => "medium"),
8
- getThinkingBudgets: vi.fn<() => Record<string, number> | undefined>(),
9
- };
10
-
11
- vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
12
- const actual =
13
- await importOriginal<typeof import("@earendil-works/pi-coding-agent")>();
14
- return {
15
- ...actual,
16
- SettingsManager: {
17
- create: () => mockSettingsManager,
18
- },
19
- };
20
- });
21
-
22
- let EventManager: typeof import("../src/managers/events").EventManager;
23
-
24
- beforeAll(async () => {
25
- const mod = await vi.importActual("../src/managers/events");
26
- EventManager =
27
- mod.EventManager as typeof import("../src/managers/events").EventManager;
28
- });
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { THINKING_BUDGETS } from "../src/constants";
3
+ import { Status } from "../src/enums/status";
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);
29
22
 
30
23
  beforeEach(() => {
31
24
  vi.restoreAllMocks();
32
25
  EventManager.resetInflightModel();
33
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("medium");
34
- mockSettingsManager.getThinkingBudgets.mockReturnValue(undefined);
26
+ settingsStub = makeSettingsStub();
35
27
  });
36
28
 
37
29
  const createPayload = (modelId: string) => ({
@@ -75,12 +67,14 @@ describe("EventManager.onBeforeProviderRequest", () => {
75
67
  ])(
76
68
  'level "$level" should return $expected',
77
69
  async ({ level, expected }) => {
78
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue(level);
70
+ vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue(
71
+ level as any,
72
+ );
79
73
 
80
74
  const server = createMockServer({
81
75
  models: ["model-a"].map((id) => createMockModel(id)),
82
76
  });
83
- const eventManager = new EventManager([server]);
77
+ const eventManager = createEventManager(server);
84
78
  const event = { payload: createPayload("model-a") };
85
79
 
86
80
  const ctx = createMockCtx(level as any);
@@ -95,12 +89,12 @@ describe("EventManager.onBeforeProviderRequest", () => {
95
89
  );
96
90
 
97
91
  it("should preserve original payload fields alongside new ones", async () => {
98
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("low");
92
+ vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("low");
99
93
 
100
94
  const server = createMockServer({
101
95
  models: ["model-b"].map((id) => createMockModel(id)),
102
96
  });
103
- const eventManager = new EventManager([server]);
97
+ const eventManager = createEventManager(server);
104
98
  const event = {
105
99
  payload: {
106
100
  model: "model-b",
@@ -117,7 +111,7 @@ describe("EventManager.onBeforeProviderRequest", () => {
117
111
 
118
112
  expect(result.messages).toEqual([{ role: "user", content: "test" }]);
119
113
  expect(result.temperature).toBe(0.7);
120
- expect(result.thinking_budget_tokens).toBe(DEFAULT_THINKING_BUDGETS.low);
114
+ expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.low);
121
115
  });
122
116
  });
123
117
 
@@ -126,7 +120,7 @@ describe("EventManager.onBeforeProviderRequest", () => {
126
120
  const server = createMockServer({
127
121
  models: ["model-a"].map((id) => createMockModel(id)),
128
122
  });
129
- const eventManager = new EventManager([server]);
123
+ const eventManager = createEventManager(server);
130
124
  const event = { payload: createNonLlamaPayload() };
131
125
 
132
126
  const ctx = createMockCtx();
@@ -144,7 +138,7 @@ describe("EventManager.onBeforeProviderRequest", () => {
144
138
  const server = createMockServer({
145
139
  models: ["model-a"].map((id) => createMockModel(id)),
146
140
  });
147
- const eventManager = new EventManager([server]);
141
+ const eventManager = createEventManager(server);
148
142
  const event = { payload: { messages: [] } };
149
143
 
150
144
  const ctx = createMockCtx();
@@ -159,13 +153,16 @@ describe("EventManager.onBeforeProviderRequest", () => {
159
153
 
160
154
  describe("user-defined budget overrides", () => {
161
155
  it("should use user-defined budgets instead of defaults", async () => {
162
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("low");
163
- 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
+ });
164
161
 
165
162
  const server = createMockServer({
166
163
  models: ["model-a"].map((id) => createMockModel(id)),
167
164
  });
168
- const eventManager = new EventManager([server]);
165
+ const eventManager = createEventManager(server);
169
166
  const event = { payload: createPayload("model-a") };
170
167
 
171
168
  const ctx = createMockCtx("low");
@@ -178,13 +175,16 @@ describe("EventManager.onBeforeProviderRequest", () => {
178
175
  });
179
176
 
180
177
  it("should merge user budgets with defaults (partial override)", async () => {
181
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("medium");
182
- 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
+ });
183
183
 
184
184
  const server = createMockServer({
185
185
  models: ["model-a"].map((id) => createMockModel(id)),
186
186
  });
187
- const eventManager = new EventManager([server]);
187
+ const eventManager = createEventManager(server);
188
188
  const event = { payload: createPayload("model-a") };
189
189
 
190
190
  const ctx = createMockCtx("medium");
@@ -194,9 +194,7 @@ describe("EventManager.onBeforeProviderRequest", () => {
194
194
  )) as Record<string, unknown>;
195
195
 
196
196
  // medium uses default since user only overrode low
197
- expect(result.thinking_budget_tokens).toBe(
198
- DEFAULT_THINKING_BUDGETS.medium,
199
- );
197
+ expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.medium);
200
198
  });
201
199
  });
202
200
 
@@ -204,8 +202,9 @@ describe("EventManager.onBeforeProviderRequest", () => {
204
202
 
205
203
  describe("edge cases", () => {
206
204
  it("should ignore invalid keys in user budgets (they are silently dropped)", async () => {
207
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("medium");
208
- mockSettingsManager.getThinkingBudgets.mockReturnValue({
205
+ vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("medium");
206
+ vi.mocked(settingsStub.resolveThinkingBudgets).mockReturnValue({
207
+ ...THINKING_BUDGETS,
209
208
  foo: 999,
210
209
  bar: 123,
211
210
  } as any);
@@ -213,7 +212,7 @@ describe("EventManager.onBeforeProviderRequest", () => {
213
212
  const server = createMockServer({
214
213
  models: ["model-a"].map((id) => createMockModel(id)),
215
214
  });
216
- const eventManager = new EventManager([server]);
215
+ const eventManager = createEventManager(server);
217
216
  const event = { payload: createPayload("model-a") };
218
217
 
219
218
  const ctx = createMockCtx("medium");
@@ -223,21 +222,20 @@ describe("EventManager.onBeforeProviderRequest", () => {
223
222
  )) as Record<string, unknown>;
224
223
 
225
224
  // Should fall back to default since "medium" is not in user budgets
226
- expect(result.thinking_budget_tokens).toBe(
227
- DEFAULT_THINKING_BUDGETS.medium,
228
- );
225
+ expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.medium);
229
226
  });
230
227
 
231
228
  it("should not allow overriding 'off' — thinking stays disabled", async () => {
232
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("off");
233
- mockSettingsManager.getThinkingBudgets.mockReturnValue({
229
+ vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("off");
230
+ vi.mocked(settingsStub.resolveThinkingBudgets).mockReturnValue({
231
+ ...THINKING_BUDGETS,
234
232
  off: 99999,
235
- } as any);
233
+ });
236
234
 
237
235
  const server = createMockServer({
238
236
  models: ["model-a"].map((id) => createMockModel(id)),
239
237
  });
240
- const eventManager = new EventManager([server]);
238
+ const eventManager = createEventManager(server);
241
239
  const event = { payload: createPayload("model-a") };
242
240
 
243
241
  const ctx = createMockCtx("off");
@@ -253,15 +251,16 @@ describe("EventManager.onBeforeProviderRequest", () => {
253
251
  });
254
252
 
255
253
  it("should not inject budget for 'max' — unlimited reasoning", async () => {
256
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("max");
257
- mockSettingsManager.getThinkingBudgets.mockReturnValue({
254
+ vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("max");
255
+ vi.mocked(settingsStub.resolveThinkingBudgets).mockReturnValue({
256
+ ...THINKING_BUDGETS,
258
257
  max: 1,
259
- } as any);
258
+ });
260
259
 
261
260
  const server = createMockServer({
262
261
  models: ["model-a"].map((id) => createMockModel(id)),
263
262
  });
264
- const eventManager = new EventManager([server]);
263
+ const eventManager = createEventManager(server);
265
264
  const event = { payload: createPayload("model-a") };
266
265
 
267
266
  const ctx = createMockCtx("max");
@@ -275,13 +274,16 @@ describe("EventManager.onBeforeProviderRequest", () => {
275
274
  });
276
275
 
277
276
  it("should handle empty user budgets gracefully", async () => {
278
- mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("high");
279
- 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
+ });
280
282
 
281
283
  const server = createMockServer({
282
284
  models: ["model-a"].map((id) => createMockModel(id)),
283
285
  });
284
- const eventManager = new EventManager([server]);
286
+ const eventManager = createEventManager(server);
285
287
  const event = { payload: createPayload("model-a") };
286
288
 
287
289
  const ctx = createMockCtx("high");
@@ -290,7 +292,170 @@ describe("EventManager.onBeforeProviderRequest", () => {
290
292
  ctx,
291
293
  )) as Record<string, unknown>;
292
294
 
293
- expect(result.thinking_budget_tokens).toBe(DEFAULT_THINKING_BUDGETS.high);
295
+ expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.high);
294
296
  });
295
297
  });
296
298
  });
299
+
300
+ describe("EventManager.onModelSelect", () => {
301
+ beforeEach(() => {
302
+ vi.mocked(settingsStub.resolveReactToModelSelect).mockReturnValue(true);
303
+ });
304
+
305
+ it("should load the model when reactToModelSelect is true", async () => {
306
+ const server = createMockServer({
307
+ models: ["model-a"].map((id) => createMockModel(id)),
308
+ });
309
+ const eventManager = createEventManager(server);
310
+ const ctx = createMockCtx();
311
+
312
+ const event = {
313
+ model: { provider: server.providerId, id: "model-a" },
314
+ } as any;
315
+
316
+ await eventManager.onModelSelect(event, ctx);
317
+
318
+ expect(ctx.ui.notify).toHaveBeenCalledWith("Loading model-a...", "info");
319
+ });
320
+
321
+ it("should return early when reactToModelSelect is false", async () => {
322
+ vi.mocked(settingsStub.resolveReactToModelSelect).mockReturnValue(false);
323
+
324
+ const server = createMockServer({
325
+ models: ["model-a"].map((id) => createMockModel(id)),
326
+ });
327
+ const eventManager = createEventManager(server);
328
+ const ctx = createMockCtx();
329
+
330
+ const event = {
331
+ model: { provider: server.providerId, id: "model-a" },
332
+ } as any;
333
+
334
+ await eventManager.onModelSelect(event, ctx);
335
+
336
+ expect(ctx.ui.notify).not.toHaveBeenCalled();
337
+ });
338
+ });
339
+
340
+ describe("EventManager.autoLoadIfNeeded", () => {
341
+ it("should load the model when autoloadOnMessage is true and model is UNLOADED", async () => {
342
+ vi.mocked(settingsStub.resolveAutoloadOnMessage).mockReturnValue(true);
343
+
344
+ const server = createMockServer({
345
+ models: [
346
+ createMockModel("model-a", {
347
+ getStatus: vi.fn().mockResolvedValue(Status.UNLOADED),
348
+ load: vi.fn().mockResolvedValue(undefined),
349
+ }),
350
+ ],
351
+ });
352
+ const eventManager = createEventManager(server);
353
+ const model = server.models[0];
354
+
355
+ await (eventManager as any).autoLoadIfNeeded(model);
356
+
357
+ expect(model.load).toHaveBeenCalled();
358
+ });
359
+
360
+ it("should not load the model when autoloadOnMessage is false", async () => {
361
+ vi.mocked(settingsStub.resolveAutoloadOnMessage).mockReturnValue(false);
362
+
363
+ const server = createMockServer({
364
+ models: [
365
+ createMockModel("model-a", {
366
+ getStatus: vi.fn().mockResolvedValue(Status.UNLOADED),
367
+ load: vi.fn().mockResolvedValue(undefined),
368
+ }),
369
+ ],
370
+ });
371
+ const eventManager = createEventManager(server);
372
+ const model = server.models[0];
373
+
374
+ await (eventManager as any).autoLoadIfNeeded(model);
375
+
376
+ expect(model.load).not.toHaveBeenCalled();
377
+ });
378
+
379
+ it("should not load the model when model is already LOADED", async () => {
380
+ vi.mocked(settingsStub.resolveAutoloadOnMessage).mockReturnValue(true);
381
+
382
+ const server = createMockServer({
383
+ models: [
384
+ createMockModel("model-a", {
385
+ getStatus: vi.fn().mockResolvedValue(Status.LOADED),
386
+ load: vi.fn().mockResolvedValue(undefined),
387
+ }),
388
+ ],
389
+ });
390
+ const eventManager = createEventManager(server);
391
+ const model = server.models[0];
392
+
393
+ await (eventManager as any).autoLoadIfNeeded(model);
394
+
395
+ expect(model.load).not.toHaveBeenCalled();
396
+ });
397
+
398
+ it("should not load the model when model is SLEEPING", async () => {
399
+ vi.mocked(settingsStub.resolveAutoloadOnMessage).mockReturnValue(true);
400
+
401
+ const server = createMockServer({
402
+ models: [
403
+ createMockModel("model-a", {
404
+ getStatus: vi.fn().mockResolvedValue(Status.SLEEPING),
405
+ load: vi.fn().mockResolvedValue(undefined),
406
+ }),
407
+ ],
408
+ });
409
+ const eventManager = createEventManager(server);
410
+ const model = server.models[0];
411
+
412
+ await (eventManager as any).autoLoadIfNeeded(model);
413
+
414
+ expect(model.load).not.toHaveBeenCalled();
415
+ });
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: () => Promise.resolve(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 */