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.
- package/README.md +66 -5
- package/package.json +3 -3
- package/src/api/client.ts +76 -32
- package/src/constants.ts +15 -0
- package/src/index.ts +3 -5
- package/src/interfaces/events.ts +14 -3
- package/src/interfaces/server.ts +20 -0
- package/src/interfaces/settings.ts +11 -2
- package/src/managers/command.ts +285 -43
- package/src/managers/events.ts +28 -10
- package/src/managers/server.ts +87 -16
- package/src/managers/settings.ts +78 -16
- package/src/models/baseModel.ts +0 -8
- package/src/server.ts +69 -28
- package/src/sse/client.ts +28 -16
- package/src/sse/manager.ts +22 -10
- package/src/ui/serverListEditor.ts +481 -0
- package/src/utils/errors.ts +5 -0
- package/src/utils/settingsStore.ts +60 -0
- package/src/utils/urls.ts +16 -0
- package/tests/commandManager.test.ts +256 -11
- package/tests/events.test.ts +117 -88
- package/tests/mocks.ts +145 -32
- package/tests/server.test.ts +37 -35
- package/tests/serverListEditor.test.ts +637 -0
- package/tests/serverManager.test.ts +282 -39
- package/tests/settings.test.ts +173 -16
- package/tests/settingsStore.test.ts +209 -0
- package/tests/sseManager.test.ts +88 -11
- package/src/interfaces/auth.ts +0 -6
- package/src/utils/cache.ts +0 -39
- package/src/utils/mutex.ts +0 -24
package/tests/events.test.ts
CHANGED
|
@@ -1,59 +1,29 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
185
|
-
|
|
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 =
|
|
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
|
-
|
|
204
|
-
|
|
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 =
|
|
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
|
-
|
|
228
|
-
|
|
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 =
|
|
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
|
-
|
|
251
|
-
|
|
229
|
+
vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("off");
|
|
230
|
+
vi.mocked(settingsStub.resolveThinkingBudgets).mockReturnValue({
|
|
231
|
+
...THINKING_BUDGETS,
|
|
252
232
|
off: 99999,
|
|
253
|
-
}
|
|
233
|
+
});
|
|
254
234
|
|
|
255
235
|
const server = createMockServer({
|
|
256
236
|
models: ["model-a"].map((id) => createMockModel(id)),
|
|
257
237
|
});
|
|
258
|
-
const eventManager =
|
|
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
|
-
|
|
275
|
-
|
|
254
|
+
vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("max");
|
|
255
|
+
vi.mocked(settingsStub.resolveThinkingBudgets).mockReturnValue({
|
|
256
|
+
...THINKING_BUDGETS,
|
|
276
257
|
max: 1,
|
|
277
|
-
}
|
|
258
|
+
});
|
|
278
259
|
|
|
279
260
|
const server = createMockServer({
|
|
280
261
|
models: ["model-a"].map((id) => createMockModel(id)),
|
|
281
262
|
});
|
|
282
|
-
const eventManager =
|
|
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
|
-
|
|
297
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
/**
|
|
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:
|
|
117
|
+
overrides: MockServerOverrides = {},
|
|
15
118
|
): Server => {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
119
|
+
const {
|
|
120
|
+
baseUrl,
|
|
121
|
+
customId,
|
|
122
|
+
customName,
|
|
123
|
+
apiKey,
|
|
19
124
|
models,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
models.length = 0;
|
|
43
|
-
models.push(...(data ?? []));
|
|
149
|
+
{
|
|
150
|
+
createApiClient: () => apiClient,
|
|
151
|
+
createSSEManager: () => sseManager,
|
|
44
152
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return 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 */
|