pi-llama-cpp 0.9.2 → 0.10.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.
@@ -0,0 +1,636 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ API_KEY_PLACEHOLDER,
4
+ LLAMA_SERVER_URL,
5
+ POLLING_TIMEOUT,
6
+ PROVIDER_PREFIX,
7
+ SERVER_TIMEOUT,
8
+ } from "../src/constants";
9
+ import { settings } from "../src/managers/settings";
10
+ import { Server } from "../src/server";
11
+
12
+ // Hoisted mock instances — survives vi.resetModules()
13
+ const mockReadStoredCredential = vi.hoisted(() => vi.fn());
14
+
15
+ const mockSettingsManager = vi.hoisted(() => ({
16
+ getProjectSettings: vi.fn(),
17
+ getGlobalSettings: vi.fn(),
18
+ getDefaultThinkingLevel: vi.fn(),
19
+ getThinkingBudgets: vi.fn(),
20
+ }));
21
+
22
+ // Mock getAgentDir, readStoredCredential, and SettingsManager before importing resolver
23
+ vi.mock("@earendil-works/pi-coding-agent", () => ({
24
+ getAgentDir: vi.fn().mockReturnValue("/fake/agent/dir"),
25
+ readStoredCredential: (...args: unknown[]) =>
26
+ mockReadStoredCredential(...args),
27
+ SettingsManager: {
28
+ create: vi.fn().mockReturnValue(mockSettingsManager),
29
+ },
30
+ }));
31
+
32
+ vi.mock("node:fs/promises", () => ({
33
+ readFile: vi.fn(),
34
+ }));
35
+
36
+ // Import mocked modules
37
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
38
+
39
+ describe("URL resolution fallback chain", () => {
40
+ const mockGetAgentDir = vi.mocked(getAgentDir);
41
+ const mockGetProjectSettings = vi.mocked(
42
+ mockSettingsManager.getProjectSettings,
43
+ );
44
+ const mockGetGlobalSettings = vi.mocked(
45
+ mockSettingsManager.getGlobalSettings,
46
+ );
47
+
48
+ afterEach(() => {
49
+ delete process.env.LLAMA_SERVER_URL;
50
+ vi.resetModules();
51
+ });
52
+
53
+ beforeEach(() => {
54
+ vi.clearAllMocks();
55
+ mockGetAgentDir.mockReturnValue("/fake/agent/dir");
56
+ // Default: no settings found
57
+ mockGetProjectSettings.mockReturnValue({});
58
+ mockGetGlobalSettings.mockReturnValue({});
59
+ });
60
+
61
+ it("should return default URL when no config is found", async () => {
62
+ // Ensure env var is not set (and not inherited from environment)
63
+ delete process.env.LLAMA_SERVER_URL;
64
+
65
+ const result = settings.resolveUrls();
66
+
67
+ expect(result).toEqual([LLAMA_SERVER_URL]);
68
+ });
69
+
70
+ it("should prioritize env variable over project config", async () => {
71
+ mockGetProjectSettings.mockReturnValue({
72
+ llamaServerUrl: "http://localhost:9999",
73
+ });
74
+ process.env.LLAMA_SERVER_URL = "http://env-url:8080";
75
+
76
+ const result = settings.resolveUrls();
77
+
78
+ expect(result).toEqual(["http://env-url:8080"]);
79
+ });
80
+
81
+ it("should use env variable when no other config exists", async () => {
82
+ process.env.LLAMA_SERVER_URL = "http://env-url:8080";
83
+
84
+ const result = settings.resolveUrls();
85
+
86
+ expect(result).toEqual(["http://env-url:8080"]);
87
+ });
88
+
89
+ it("should use legacy llamaServerUrl when env is not set", async () => {
90
+ mockGetProjectSettings.mockReturnValue({
91
+ llamaServerUrl: "http://project:9999",
92
+ });
93
+
94
+ const result = settings.resolveUrls();
95
+
96
+ expect(result).toEqual(["http://project:9999"]);
97
+ });
98
+
99
+ it("should use global settings when no project config or env exists", async () => {
100
+ mockGetGlobalSettings.mockReturnValue({
101
+ llamaServerUrl: "http://global:8080",
102
+ });
103
+
104
+ const result = settings.resolveUrls();
105
+
106
+ expect(result).toEqual(["http://global:8080"]);
107
+ });
108
+
109
+ it("should strip trailing slashes from resolved URL", async () => {
110
+ process.env.LLAMA_SERVER_URL = "http://localhost:8080/";
111
+
112
+ const result = settings.resolveUrls();
113
+
114
+ expect(result).toEqual(["http://localhost:8080"]);
115
+ });
116
+
117
+ it("should cache the resolved URL on subsequent calls", async () => {
118
+ process.env.LLAMA_SERVER_URL = "http://first:8080";
119
+
120
+ const result1 = settings.resolveUrls();
121
+ const result2 = settings.resolveUrls();
122
+
123
+ expect(result1).toEqual(["http://first:8080"]);
124
+ expect(result2).toEqual(["http://first:8080"]);
125
+ });
126
+
127
+ it("should handle multiple URLs separated by semicolons", async () => {
128
+ process.env.LLAMA_SERVER_URL = "http://first:8080;http://second:9090/";
129
+
130
+ const result = settings.resolveUrls();
131
+
132
+ expect(result).toEqual(["http://first:8080", "http://second:9090"]);
133
+ });
134
+ });
135
+
136
+ describe("llamaSettings.servers resolution", () => {
137
+ const mockGetAgentDir = vi.mocked(getAgentDir);
138
+ const mockGetProjectSettings = vi.mocked(
139
+ mockSettingsManager.getProjectSettings,
140
+ );
141
+ const mockGetGlobalSettings = vi.mocked(
142
+ mockSettingsManager.getGlobalSettings,
143
+ );
144
+
145
+ afterEach(() => {
146
+ delete process.env.LLAMA_SERVER_URL;
147
+ vi.resetModules();
148
+ });
149
+
150
+ beforeEach(() => {
151
+ vi.clearAllMocks();
152
+ mockGetAgentDir.mockReturnValue("/fake/agent/dir");
153
+ mockGetProjectSettings.mockReturnValue({});
154
+ mockGetGlobalSettings.mockReturnValue({});
155
+ });
156
+
157
+ it("should resolve URLs from llamaSettings.servers in project config", async () => {
158
+ mockGetProjectSettings.mockReturnValue({
159
+ llamaSettings: {
160
+ servers: [
161
+ { url: "http://project-server:8080" },
162
+ { url: "http://project-server2:9090" },
163
+ ],
164
+ },
165
+ });
166
+
167
+ const result = settings.resolveUrls();
168
+
169
+ expect(result).toEqual([
170
+ "http://project-server:8080",
171
+ "http://project-server2:9090",
172
+ ]);
173
+ });
174
+
175
+ it("should prioritize project servers over global servers", async () => {
176
+ mockGetProjectSettings.mockReturnValue({
177
+ llamaSettings: {
178
+ servers: [{ url: "http://project:8080" }],
179
+ },
180
+ });
181
+ mockGetGlobalSettings.mockReturnValue({
182
+ llamaSettings: {
183
+ servers: [{ url: "http://global:9090" }],
184
+ },
185
+ });
186
+
187
+ const result = settings.resolveUrls();
188
+
189
+ expect(result).toEqual(["http://project:8080"]);
190
+ });
191
+
192
+ it("should use global servers when no project servers exist", async () => {
193
+ mockGetGlobalSettings.mockReturnValue({
194
+ llamaSettings: {
195
+ servers: [{ url: "http://global:8080" }],
196
+ },
197
+ });
198
+
199
+ const result = settings.resolveUrls();
200
+
201
+ expect(result).toEqual(["http://global:8080"]);
202
+ });
203
+
204
+ it("should prioritize env variable over servers", async () => {
205
+ process.env.LLAMA_SERVER_URL = "http://env:8080";
206
+ mockGetProjectSettings.mockReturnValue({
207
+ llamaSettings: {
208
+ servers: [{ url: "http://server:9090" }],
209
+ },
210
+ });
211
+
212
+ const result = settings.resolveUrls();
213
+
214
+ expect(result).toEqual(["http://env:8080"]);
215
+ });
216
+
217
+ it("should use servers when env is not set", async () => {
218
+ mockGetProjectSettings.mockReturnValue({
219
+ llamaSettings: {
220
+ servers: [{ url: "http://server:9090" }],
221
+ },
222
+ });
223
+
224
+ const result = settings.resolveUrls();
225
+
226
+ expect(result).toEqual(["http://server:9090"]);
227
+ });
228
+
229
+ it("should prioritize servers over legacy llamaServerUrl", async () => {
230
+ mockGetProjectSettings.mockReturnValue({
231
+ llamaServerUrl: "http://legacy:8080",
232
+ llamaSettings: {
233
+ servers: [{ url: "http://server:9090" }],
234
+ },
235
+ });
236
+
237
+ const result = settings.resolveUrls();
238
+
239
+ expect(result).toEqual(["http://server:9090"]);
240
+ });
241
+
242
+ it("should use legacy llamaServerUrl when servers is empty", async () => {
243
+ mockGetProjectSettings.mockReturnValue({
244
+ llamaServerUrl: "http://legacy:8080",
245
+ llamaSettings: {
246
+ servers: [],
247
+ },
248
+ });
249
+
250
+ const result = settings.resolveUrls();
251
+
252
+ expect(result).toEqual(["http://legacy:8080"]);
253
+ });
254
+
255
+ it("should strip trailing slashes from server URLs", async () => {
256
+ mockGetProjectSettings.mockReturnValue({
257
+ llamaSettings: {
258
+ servers: [{ url: "http://localhost:8080/" }],
259
+ },
260
+ });
261
+
262
+ const result = settings.resolveUrls();
263
+
264
+ expect(result).toEqual(["http://localhost:8080"]);
265
+ });
266
+ });
267
+
268
+ describe("API key resolution", () => {
269
+ const mockGetAgentDir = vi.mocked(getAgentDir);
270
+
271
+ afterEach(() => {
272
+ vi.resetModules();
273
+ });
274
+
275
+ beforeEach(() => {
276
+ vi.clearAllMocks();
277
+ mockGetAgentDir.mockReturnValue("/fake/agent/dir");
278
+ mockReadStoredCredential.mockReturnValue(undefined);
279
+ });
280
+
281
+ it("should return placeholder when credential is not found", () => {
282
+ mockReadStoredCredential.mockReturnValue(undefined);
283
+
284
+ const result = settings.resolveApiKey("llama-server=http://127.0.0.1:8080");
285
+
286
+ expect(result).toEqual(API_KEY_PLACEHOLDER);
287
+ });
288
+
289
+ it("should return placeholder when apiKey is missing from credential", () => {
290
+ mockReadStoredCredential.mockReturnValue({});
291
+
292
+ const result = settings.resolveApiKey("llama-server=http://127.0.0.1:8080");
293
+
294
+ expect(result).toEqual(API_KEY_PLACEHOLDER);
295
+ });
296
+
297
+ it("should return the key when present in credential", () => {
298
+ mockReadStoredCredential.mockReturnValue({ key: "test-api-key" });
299
+
300
+ const result = settings.resolveApiKey("llama-server=http://127.0.0.1:8080");
301
+
302
+ expect(result).toEqual("test-api-key");
303
+ });
304
+
305
+ it("should call readStoredCredential with the provider ID", () => {
306
+ mockReadStoredCredential.mockReturnValue({ key: "test-key" });
307
+
308
+ settings.resolveApiKey("llama-server=http://127.0.0.1:8080");
309
+
310
+ expect(mockReadStoredCredential).toHaveBeenCalledWith(
311
+ "llama-server=http://127.0.0.1:8080",
312
+ );
313
+ });
314
+ });
315
+
316
+ describe("Server with custom id", () => {
317
+ it("should use custom id as providerId when provided", () => {
318
+ const server = new Server("http://127.0.0.1:8080", "my-custom-id");
319
+
320
+ expect(server.providerId).toEqual("my-custom-id");
321
+ });
322
+
323
+ it("should fall back to URL-based providerId when no custom id", () => {
324
+ const server = new Server("http://127.0.0.1:8080");
325
+
326
+ expect(server.providerId).toEqual(
327
+ `${PROVIDER_PREFIX}=http://127.0.0.1:8080`,
328
+ );
329
+ });
330
+
331
+ it("should try custom id first in getApiKey(), then fall back to URL-based", () => {
332
+ vi.clearAllMocks();
333
+ // Mock: custom id returns placeholder (no key found)
334
+ mockReadStoredCredential
335
+ .mockReturnValueOnce(API_KEY_PLACEHOLDER)
336
+ .mockReturnValueOnce({ key: "fallback-key" });
337
+
338
+ const server = new Server("http://127.0.0.1:8080", "my-custom-id");
339
+
340
+ const result = server.getApiKey();
341
+
342
+ expect(result).toEqual("fallback-key");
343
+ expect(mockReadStoredCredential).toHaveBeenNthCalledWith(1, "my-custom-id");
344
+ expect(mockReadStoredCredential).toHaveBeenNthCalledWith(
345
+ 2,
346
+ `${PROVIDER_PREFIX}=http://127.0.0.1:8080`,
347
+ );
348
+ });
349
+
350
+ it("should return custom id key directly when found", () => {
351
+ mockReadStoredCredential.mockReturnValue({ key: "custom-key" });
352
+
353
+ const server = new Server("http://127.0.0.1:8080", "my-custom-id");
354
+
355
+ const result = server.getApiKey();
356
+
357
+ expect(result).toEqual("custom-key");
358
+ expect(mockReadStoredCredential).toHaveBeenCalledWith("my-custom-id");
359
+ });
360
+ });
361
+
362
+ describe("Server with custom name", () => {
363
+ it("should use custom name as suffix in providerName", () => {
364
+ const server = new Server(
365
+ "http://127.0.0.1:8080",
366
+ undefined,
367
+ "Remote Server",
368
+ );
369
+
370
+ expect(server.providerName).toEqual(`Llama.cpp (Remote Server)`);
371
+ });
372
+
373
+ it("should fall back to URL-based name when no custom name", () => {
374
+ const server = new Server("http://127.0.0.1:8080");
375
+
376
+ expect(server.providerName).toEqual(`Llama.cpp (http://127.0.0.1:8080)`);
377
+ });
378
+
379
+ it("should use custom name even with custom id", () => {
380
+ const server = new Server(
381
+ "http://127.0.0.1:8080",
382
+ "my-custom-id",
383
+ "Remote Server",
384
+ );
385
+
386
+ expect(server.providerId).toEqual("my-custom-id");
387
+ expect(server.providerName).toEqual(`Llama.cpp (Remote Server)`);
388
+ });
389
+ });
390
+
391
+ describe("reactToModelSelect and autoloadOnMessage fallbacks", () => {
392
+ afterEach(() => {
393
+ vi.resetModules();
394
+ });
395
+
396
+ it("should return true when reactToModelSelect is not set", async () => {
397
+ const { settings } = await import("../src/managers/settings");
398
+
399
+ const result = settings.resolveReactToModelSelect();
400
+
401
+ expect(result).toBe(true);
402
+ });
403
+
404
+ it("should return false when autoloadOnMessage is not set", async () => {
405
+ const { settings } = await import("../src/managers/settings");
406
+
407
+ const result = settings.resolveAutoloadOnMessage();
408
+
409
+ expect(result).toBe(false);
410
+ });
411
+
412
+ it("should return user values when set", async () => {
413
+ mockSettingsManager.getProjectSettings.mockReturnValue({
414
+ llamaSettings: {
415
+ reactToModelSelect: false,
416
+ autoloadOnMessage: true,
417
+ },
418
+ });
419
+
420
+ const { settings } = await import("../src/managers/settings");
421
+
422
+ expect(settings.resolveReactToModelSelect()).toBe(false);
423
+ expect(settings.resolveAutoloadOnMessage()).toBe(true);
424
+ });
425
+ });
426
+
427
+ describe("resolveServers", () => {
428
+ const mockGetAgentDir = vi.mocked(getAgentDir);
429
+ const mockGetProjectSettings = vi.mocked(
430
+ mockSettingsManager.getProjectSettings,
431
+ );
432
+ const mockGetGlobalSettings = vi.mocked(
433
+ mockSettingsManager.getGlobalSettings,
434
+ );
435
+
436
+ afterEach(() => {
437
+ delete process.env.LLAMA_SERVER_URL;
438
+ vi.resetModules();
439
+ });
440
+
441
+ beforeEach(() => {
442
+ vi.clearAllMocks();
443
+ mockGetAgentDir.mockReturnValue("/fake/agent/dir");
444
+ mockGetProjectSettings.mockReturnValue({});
445
+ mockGetGlobalSettings.mockReturnValue({});
446
+ });
447
+
448
+ it("should use llamaSettings.servers when configured", () => {
449
+ mockGetProjectSettings.mockReturnValue({
450
+ llamaSettings: {
451
+ servers: [
452
+ { url: "http://custom:8080", id: "my-server", name: "Custom" },
453
+ ],
454
+ },
455
+ });
456
+
457
+ const result = settings.resolveServers();
458
+
459
+ expect(result).toHaveLength(1);
460
+ expect(result[0].baseUrl).toBe("http://custom:8080");
461
+ expect(result[0].providerId).toBe("my-server");
462
+ });
463
+
464
+ it("should fall back to resolveUrls when servers is empty", async () => {
465
+ process.env.LLAMA_SERVER_URL = "http://env-server:9090";
466
+
467
+ const result = settings.resolveServers();
468
+
469
+ expect(result).toHaveLength(1);
470
+ expect(result[0].baseUrl).toBe("http://env-server:9090");
471
+ });
472
+
473
+ it("should fall back to default URL when no config exists", () => {
474
+ const result = settings.resolveServers();
475
+
476
+ expect(result).toHaveLength(1);
477
+ expect(result[0].baseUrl).toBe(LLAMA_SERVER_URL);
478
+ });
479
+
480
+ it("should apply id/name from llamaSettings.servers as overrides", () => {
481
+ mockGetProjectSettings.mockReturnValue({
482
+ llamaSettings: {
483
+ servers: [
484
+ { url: "http://127.0.0.1:8080", id: "custom-id", name: "Custom" },
485
+ ],
486
+ },
487
+ });
488
+
489
+ const result = settings.resolveServers();
490
+
491
+ expect(result).toHaveLength(1);
492
+ expect(result[0].baseUrl).toBe("http://127.0.0.1:8080");
493
+ expect(result[0].providerId).toBe("custom-id");
494
+ expect(result[0].providerName).toBe(`Llama.cpp (Custom)`);
495
+ });
496
+
497
+ it("should handle multiple URLs with partial id/name overrides", () => {
498
+ mockGetProjectSettings.mockReturnValue({
499
+ llamaSettings: {
500
+ servers: [{ url: "http://first:8080", id: "first-server" }],
501
+ },
502
+ });
503
+ process.env.LLAMA_SERVER_URL = "http://first:8080;http://second:9090";
504
+
505
+ const result = settings.resolveServers();
506
+
507
+ expect(result).toHaveLength(2);
508
+ expect(result[0].baseUrl).toBe("http://first:8080");
509
+ expect(result[0].providerId).toBe("first-server");
510
+ expect(result[1].baseUrl).toBe("http://second:9090");
511
+ expect(result[1].providerId).toBe(`${PROVIDER_PREFIX}=http://second:9090`);
512
+ });
513
+
514
+ it("should prioritize env variable over global servers", async () => {
515
+ process.env.LLAMA_SERVER_URL = "http://env:8080";
516
+ mockGetGlobalSettings.mockReturnValue({
517
+ llamaSettings: {
518
+ servers: [{ url: "http://global:9090" }],
519
+ },
520
+ });
521
+
522
+ const result = settings.resolveServers();
523
+
524
+ // env variable takes precedence via resolveUrls
525
+ expect(result).toHaveLength(1);
526
+ expect(result[0].baseUrl).toBe("http://env:8080");
527
+ });
528
+ });
529
+
530
+ describe("resolveTimeouts", () => {
531
+ afterEach(() => {
532
+ vi.resetModules();
533
+ });
534
+
535
+ it("should return default timeouts when not configured", async () => {
536
+ const { settings } = await import("../src/managers/settings");
537
+
538
+ const result = settings.resolveTimeouts();
539
+
540
+ expect(result).toEqual({
541
+ pollingTimeout: POLLING_TIMEOUT,
542
+ serverTimeout: SERVER_TIMEOUT,
543
+ });
544
+ });
545
+
546
+ it("should use user-defined pollingTimeout", async () => {
547
+ mockSettingsManager.getProjectSettings.mockReturnValue({
548
+ llamaSettings: {
549
+ pollingTimeout: 120000,
550
+ },
551
+ });
552
+
553
+ const { settings } = await import("../src/managers/settings");
554
+
555
+ const result = settings.resolveTimeouts();
556
+
557
+ expect(result.pollingTimeout).toBe(120000);
558
+ expect(result.serverTimeout).toBe(SERVER_TIMEOUT);
559
+ });
560
+
561
+ it("should use user-defined serverTimeout", async () => {
562
+ mockSettingsManager.getProjectSettings.mockReturnValue({
563
+ llamaSettings: {
564
+ serverTimeout: 3000,
565
+ },
566
+ });
567
+
568
+ const { settings } = await import("../src/managers/settings");
569
+
570
+ const result = settings.resolveTimeouts();
571
+
572
+ expect(result.pollingTimeout).toBe(POLLING_TIMEOUT);
573
+ expect(result.serverTimeout).toBe(3000);
574
+ });
575
+
576
+ it("should use both user-defined timeouts", async () => {
577
+ mockSettingsManager.getProjectSettings.mockReturnValue({
578
+ llamaSettings: {
579
+ pollingTimeout: 90000,
580
+ serverTimeout: 2000,
581
+ },
582
+ });
583
+
584
+ const { settings } = await import("../src/managers/settings");
585
+
586
+ const result = settings.resolveTimeouts();
587
+
588
+ expect(result).toEqual({
589
+ pollingTimeout: 90000,
590
+ serverTimeout: 2000,
591
+ });
592
+ });
593
+ });
594
+
595
+ describe("Thinking config resolution", () => {
596
+ const mockGetAgentDir = vi.mocked(getAgentDir);
597
+
598
+ afterEach(() => {
599
+ vi.resetModules();
600
+ });
601
+
602
+ beforeEach(() => {
603
+ vi.clearAllMocks();
604
+ mockGetAgentDir.mockReturnValue("/fake/agent/dir");
605
+ mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("medium");
606
+ mockSettingsManager.getThinkingBudgets.mockReturnValue({});
607
+ });
608
+
609
+ it("should return the default thinking level from SettingsManager", () => {
610
+ mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("low");
611
+
612
+ const result = settings.resolveThinkingLevel();
613
+
614
+ expect(result).toEqual("low");
615
+ });
616
+
617
+ it("should return thinking budgets merged with defaults", () => {
618
+ mockSettingsManager.getThinkingBudgets.mockReturnValue({
619
+ low: 4096,
620
+ });
621
+
622
+ const result = settings.resolveThinkingBudgets();
623
+
624
+ expect(result).toEqual(
625
+ expect.objectContaining({
626
+ off: 0,
627
+ minimal: 1024,
628
+ low: 4096,
629
+ medium: 8192,
630
+ high: 16384,
631
+ xhigh: 32768,
632
+ max: -1,
633
+ }),
634
+ );
635
+ });
636
+ });
@@ -0,0 +1,21 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { SERVER_TIMEOUT } from "../src/constants";
3
+ import { SSEManager } from "../src/sse/manager";
4
+
5
+ beforeEach(() => {
6
+ vi.clearAllMocks();
7
+ });
8
+
9
+ describe("SSEManager serverTimeout", () => {
10
+ it("should use default timeout when not provided", () => {
11
+ const manager = new SSEManager("http://127.0.0.1:8080", "");
12
+
13
+ expect((manager as any).serverTimeout).toBe(SERVER_TIMEOUT);
14
+ });
15
+
16
+ it("should accept custom serverTimeout", () => {
17
+ const manager = new SSEManager("http://127.0.0.1:8080", "", 3000);
18
+
19
+ expect((manager as any).serverTimeout).toBe(3000);
20
+ });
21
+ });