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.
- package/README.md +163 -27
- package/package.json +4 -3
- package/src/api/client.ts +76 -32
- package/src/constants.ts +28 -3
- package/src/index.ts +4 -12
- package/src/interfaces/events.ts +14 -3
- package/src/interfaces/server.ts +20 -0
- package/src/interfaces/settings.ts +69 -0
- package/src/managers/command.ts +285 -43
- package/src/managers/events.ts +50 -12
- package/src/managers/server.ts +90 -17
- package/src/managers/settings.ts +268 -0
- package/src/models/baseModel.ts +32 -17
- package/src/models/legacyModel.ts +2 -2
- package/src/models/routerModel.ts +3 -3
- package/src/server.ts +86 -21
- package/src/sse/client.ts +28 -16
- package/src/sse/manager.ts +23 -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 +229 -64
- package/tests/mocks.ts +145 -32
- package/tests/server.test.ts +54 -6
- package/tests/serverListEditor.test.ts +637 -0
- package/tests/serverManager.test.ts +282 -39
- package/tests/settings.test.ts +793 -0
- package/tests/settingsStore.test.ts +209 -0
- package/tests/sseManager.test.ts +98 -0
- package/src/interfaces/auth.ts +0 -6
- package/src/resolver.ts +0 -149
- package/src/utils/cache.ts +0 -39
- package/src/utils/mutex.ts +0 -24
- package/tests/resolver.test.ts +0 -184
package/tests/events.test.ts
CHANGED
|
@@ -1,37 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
163
|
-
|
|
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 =
|
|
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
|
-
|
|
182
|
-
|
|
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 =
|
|
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
|
-
|
|
208
|
-
|
|
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 =
|
|
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
|
-
|
|
233
|
-
|
|
229
|
+
vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("off");
|
|
230
|
+
vi.mocked(settingsStub.resolveThinkingBudgets).mockReturnValue({
|
|
231
|
+
...THINKING_BUDGETS,
|
|
234
232
|
off: 99999,
|
|
235
|
-
}
|
|
233
|
+
});
|
|
236
234
|
|
|
237
235
|
const server = createMockServer({
|
|
238
236
|
models: ["model-a"].map((id) => createMockModel(id)),
|
|
239
237
|
});
|
|
240
|
-
const eventManager =
|
|
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
|
-
|
|
257
|
-
|
|
254
|
+
vi.mocked(settingsStub.resolveThinkingLevel).mockReturnValue("max");
|
|
255
|
+
vi.mocked(settingsStub.resolveThinkingBudgets).mockReturnValue({
|
|
256
|
+
...THINKING_BUDGETS,
|
|
258
257
|
max: 1,
|
|
259
|
-
}
|
|
258
|
+
});
|
|
260
259
|
|
|
261
260
|
const server = createMockServer({
|
|
262
261
|
models: ["model-a"].map((id) => createMockModel(id)),
|
|
263
262
|
});
|
|
264
|
-
const eventManager =
|
|
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
|
-
|
|
279
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
/**
|
|
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 */
|