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.
@@ -2,14 +2,37 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { ServerManager } from "../src/managers/server";
3
3
  import { BaseModel } from "../src/models/baseModel";
4
4
  import { Server } from "../src/server";
5
- import { createMockServer, mockRpc } from "./mocks";
5
+ import {
6
+ createMockModel,
7
+ createMockServer,
8
+ makeSettingsStub,
9
+ mockRpc,
10
+ } from "./mocks";
11
+
12
+ // Injected settings stub — the single instance passed to ServerManager and
13
+ // to every real `new Server(...)` construction below (Server's eager
14
+ // getApiKey() resolves the stub's placeholder, never consumed). Tests
15
+ // re-stub members per case instead of module-mocking the singleton.
16
+ const settingsStub = makeSettingsStub();
6
17
 
7
18
  const mockPi = {
8
19
  registerProvider: vi.fn(),
20
+ unregisterProvider: vi.fn(),
9
21
  registerCommand: vi.fn(),
10
22
  setModel: vi.fn(),
11
23
  };
12
24
 
25
+ /**
26
+ * Creates a ServerManager whose list is driven by the mocked
27
+ * `settings.resolveServers()` — the same path a real scan takes.
28
+ */
29
+ const createManager = async (...servers: Server[]): Promise<ServerManager> => {
30
+ vi.mocked(settingsStub.resolveServers).mockReturnValue(servers);
31
+ const manager = new ServerManager(settingsStub);
32
+ await manager.update(mockPi as any);
33
+ return manager;
34
+ };
35
+
13
36
  beforeEach(() => {
14
37
  vi.clearAllMocks();
15
38
  mockRpc.mockImplementation((endpoint: string, fallback?: unknown) => {
@@ -24,22 +47,34 @@ beforeEach(() => {
24
47
 
25
48
  describe("Server", () => {
26
49
  it("should generate provider IDs from URLs", () => {
27
- const server1 = new Server("http://127.0.0.1:8080");
50
+ const server1 = new Server(settingsStub, {
51
+ baseUrl: "http://127.0.0.1:8080",
52
+ });
28
53
  expect(server1.providerId).toBe("llama-server=http://127.0.0.1:8080");
29
- const server2 = new Server("http://10.0.0.5:8080");
54
+ const server2 = new Server(settingsStub, {
55
+ baseUrl: "http://10.0.0.5:8080",
56
+ });
30
57
  expect(server2.providerId).toBe("llama-server=http://10.0.0.5:8080");
31
- const server3 = new Server("http://127.0.0.1");
58
+ const server3 = new Server(settingsStub, { baseUrl: "http://127.0.0.1" });
32
59
  expect(server3.providerId).toBe("llama-server=http://127.0.0.1");
33
- const server4 = new Server("http://127.0.0.1:80");
60
+ const server4 = new Server(settingsStub, {
61
+ baseUrl: "http://127.0.0.1:80",
62
+ });
34
63
  expect(server4.providerId).toBe("llama-server=http://127.0.0.1:80");
35
- const server5 = new Server("https://127.0.0.1:443");
64
+ const server5 = new Server(settingsStub, {
65
+ baseUrl: "https://127.0.0.1:443",
66
+ });
36
67
  expect(server5.providerId).toBe("llama-server=https://127.0.0.1:443");
37
68
  });
38
69
 
39
70
  it("should generate provider names from URLs", () => {
40
- const server1 = new Server("http://127.0.0.1:8080");
71
+ const server1 = new Server(settingsStub, {
72
+ baseUrl: "http://127.0.0.1:8080",
73
+ });
41
74
  expect(server1.providerName).toBe("Llama.cpp (http://127.0.0.1:8080)");
42
- const server2 = new Server("http://10.0.0.5:8080");
75
+ const server2 = new Server(settingsStub, {
76
+ baseUrl: "http://10.0.0.5:8080",
77
+ });
43
78
  expect(server2.providerName).toBe("Llama.cpp (http://10.0.0.5:8080)");
44
79
  });
45
80
  });
@@ -51,32 +86,17 @@ describe("ServerManager", () => {
51
86
  id: "test-model",
52
87
  toProviderConfig: vi.fn().mockResolvedValue({ id: "test-model" }),
53
88
  } as unknown as BaseModel;
54
- mockRpc.mockImplementation((endpoint: string, fallback?: unknown) => {
55
- if (endpoint === "/v1/models") {
56
- return Promise.resolve({ data: [mockModel], object: "list" });
57
- }
58
- const defaults: Record<string, unknown> = {
59
- "/health": { status: "ok" },
60
- "/props?autoload=false": { role: "router" },
61
- };
62
- return Promise.resolve(defaults[endpoint] ?? fallback ?? {});
63
- });
64
-
65
89
  const server1 = createMockServer({
66
90
  baseUrl: "http://127.0.0.1:8080",
67
91
  apiKey: "key-1",
68
- providerId: "llama-server=http://127.0.0.1:8080",
69
- providerName: "Llama.cpp (http://127.0.0.1:8080)",
92
+ models: [mockModel],
70
93
  });
71
94
  const server2 = createMockServer({
72
95
  baseUrl: "http://127.0.0.1:8081",
73
96
  apiKey: "key-2",
74
- providerId: "llama-server=http://127.0.0.1:8081",
75
- providerName: "Llama.cpp (http://127.0.0.1:8081)",
97
+ models: [mockModel],
76
98
  });
77
- const manager = new ServerManager([server1, server2] as any);
78
-
79
- await manager.initialize(mockPi as any);
99
+ const manager = await createManager(server1, server2);
80
100
 
81
101
  expect(mockPi.registerProvider).toHaveBeenCalledTimes(2);
82
102
  expect(mockPi.registerProvider).toHaveBeenCalledWith(
@@ -101,25 +121,18 @@ describe("ServerManager", () => {
101
121
  );
102
122
  });
103
123
 
104
- it("should return all models from all servers", () => {
105
- const mockModel1 = {
106
- name: "model-1",
107
- id: "model-1",
108
- } as unknown as BaseModel;
109
- const mockModel2 = {
110
- name: "model-2",
111
- id: "model-2",
112
- } as unknown as BaseModel;
124
+ it("should return all models from all servers", async () => {
125
+ const mockModel1 = createMockModel("model-1");
126
+ const mockModel2 = createMockModel("model-2");
113
127
  const server1 = createMockServer({
114
128
  baseUrl: "http://127.0.0.1:8080",
129
+ models: [mockModel1],
115
130
  });
116
131
  const server2 = createMockServer({
117
132
  baseUrl: "http://127.0.0.1:8081",
133
+ models: [mockModel2],
118
134
  });
119
- const manager = new ServerManager([
120
- { ...server1, models: [mockModel1] } as any,
121
- { ...server2, models: [mockModel2] } as any,
122
- ] as any);
135
+ const manager = await createManager(server1, server2);
123
136
 
124
137
  const allModels = manager.getAllModels();
125
138
 
@@ -127,4 +140,234 @@ describe("ServerManager", () => {
127
140
  expect(allModels[0]).toBe(mockModel1);
128
141
  expect(allModels[1]).toBe(mockModel2);
129
142
  });
143
+
144
+ describe("live server list (re-derived from settings)", () => {
145
+ /** Mock server; providerId/name derive from the URL as in production. */
146
+ const makeServer = (url: string, modelName?: string): Server =>
147
+ createMockServer({
148
+ baseUrl: url,
149
+ ...(modelName
150
+ ? { models: [createMockModel(modelName, { serverUrl: url })] }
151
+ : {}),
152
+ });
153
+
154
+ it("should register servers added after the first scan", async () => {
155
+ const server1 = makeServer("http://127.0.0.1:8080", "model-1");
156
+ vi.mocked(settingsStub.resolveServers).mockReturnValue([server1]);
157
+ const manager = new ServerManager(settingsStub);
158
+ await manager.update(mockPi as any);
159
+ expect(manager.servers).toHaveLength(1);
160
+
161
+ const server2 = makeServer("http://127.0.0.1:8081", "model-2");
162
+ vi.mocked(settingsStub.resolveServers).mockReturnValue([
163
+ server1,
164
+ server2,
165
+ ]);
166
+ await manager.update(mockPi as any);
167
+
168
+ expect(mockPi.registerProvider).toHaveBeenCalledWith(
169
+ "llama-server=http://127.0.0.1:8081",
170
+ expect.objectContaining({ baseUrl: "http://127.0.0.1:8081" }),
171
+ );
172
+ expect(manager.servers).toHaveLength(2);
173
+ expect(manager.getAllModels().map((m) => m.id)).toEqual([
174
+ "model-1",
175
+ "model-2",
176
+ ]);
177
+ });
178
+
179
+ it("should unregister and disconnect removed servers on the next scan", async () => {
180
+ const disconnectKept = vi.fn();
181
+ const disconnectRemoved = vi.fn();
182
+ const server1 = makeServer("http://127.0.0.1:8080", "model-1");
183
+ const server2 = makeServer("http://127.0.0.1:8081", "model-2");
184
+ // createMockServer stubs initialize out (models are pre-seeded), so the
185
+ // SSE managers never get wired; seed the slots ServerManager reaches
186
+ // disconnect through, mirroring an initialized server.
187
+ (server1 as any).sse = { disconnect: disconnectKept };
188
+ (server2 as any).sse = { disconnect: disconnectRemoved };
189
+
190
+ const manager = await createManager(server1, server2);
191
+ expect(manager.servers).toHaveLength(2);
192
+
193
+ vi.mocked(settingsStub.resolveServers).mockReturnValue([server1]);
194
+ await manager.update(mockPi as any);
195
+
196
+ expect(mockPi.unregisterProvider).toHaveBeenCalledTimes(1);
197
+ expect(mockPi.unregisterProvider).toHaveBeenCalledWith(
198
+ "llama-server=http://127.0.0.1:8081",
199
+ );
200
+ expect(disconnectRemoved).toHaveBeenCalledTimes(1);
201
+ // Surviving providers keep their SSE manager connected (today's behavior)
202
+ expect(disconnectKept).not.toHaveBeenCalled();
203
+ expect(manager.getAllModels().map((m) => m.id)).toEqual(["model-1"]);
204
+ });
205
+
206
+ it("should unregister the old provider when a server URL changes", async () => {
207
+ const manager = await createManager(
208
+ makeServer("http://127.0.0.1:8080", "model-1"),
209
+ );
210
+
211
+ vi.mocked(settingsStub.resolveServers).mockReturnValue([
212
+ makeServer("http://127.0.0.1:9090", "model-1"),
213
+ ]);
214
+ await manager.update(mockPi as any);
215
+
216
+ expect(mockPi.unregisterProvider).toHaveBeenCalledWith(
217
+ "llama-server=http://127.0.0.1:8080",
218
+ );
219
+ expect(mockPi.registerProvider).toHaveBeenCalledWith(
220
+ "llama-server=http://127.0.0.1:9090",
221
+ expect.objectContaining({ baseUrl: "http://127.0.0.1:9090" }),
222
+ );
223
+ expect(manager.servers[0]?.providerId).toBe(
224
+ "llama-server=http://127.0.0.1:9090",
225
+ );
226
+ });
227
+
228
+ it("should re-register same-id servers without unregistering (name edit)", async () => {
229
+ const original = createMockServer({
230
+ baseUrl: "http://127.0.0.1:8080",
231
+ customId: "my-custom-id",
232
+ customName: "A",
233
+ });
234
+ const manager = await createManager(original);
235
+
236
+ const renamed = createMockServer({
237
+ baseUrl: "http://127.0.0.1:8080",
238
+ customId: "my-custom-id",
239
+ customName: "B",
240
+ });
241
+ vi.mocked(settingsStub.resolveServers).mockReturnValue([renamed]);
242
+ await manager.update(mockPi as any);
243
+
244
+ expect(mockPi.unregisterProvider).not.toHaveBeenCalled();
245
+ expect(vi.mocked(mockPi.registerProvider).mock.calls.at(-1)).toEqual([
246
+ "my-custom-id",
247
+ expect.objectContaining({ name: "Llama.cpp (B)" }),
248
+ ]);
249
+ expect(manager.servers[0]?.providerId).toBe("my-custom-id");
250
+ });
251
+
252
+ it("should collapse duplicate URLs into a single server", async () => {
253
+ const first = makeServer("http://127.0.0.1:8080", "model-1");
254
+ const duplicate = makeServer("http://127.0.0.1:8080", "model-1");
255
+ const manager = await createManager(first, duplicate);
256
+
257
+ expect(manager.servers).toHaveLength(1);
258
+ expect(mockPi.registerProvider).toHaveBeenCalledTimes(1);
259
+ expect(manager.getAllModels()).toHaveLength(1);
260
+ });
261
+
262
+ it("should return undefined from getServer for a removed server's model", async () => {
263
+ const manager = await createManager(
264
+ makeServer("http://127.0.0.1:8080", "model-1"),
265
+ );
266
+
267
+ const live = manager.getAllModels()[0];
268
+ expect(manager.getServer(live)).toBe(manager.servers[0]);
269
+
270
+ const orphan = { serverUrl: "http://gone:1" } as unknown as BaseModel;
271
+ expect(manager.getServer(orphan)).toBeUndefined();
272
+ });
273
+ });
274
+
275
+ describe("sortBy", () => {
276
+ const sortBy = () => vi.mocked(settingsStub.resolveSortBy);
277
+
278
+ it("should return models in API order when sortBy is 'api'", async () => {
279
+ sortBy().mockReturnValue("api");
280
+
281
+ const mockModelA = createMockModel("model-a");
282
+ const mockModelZ = createMockModel("model-z");
283
+ const manager = await createManager(
284
+ createMockServer({
285
+ baseUrl: "http://127.0.0.1:8080",
286
+ models: [mockModelA, mockModelZ],
287
+ }),
288
+ );
289
+
290
+ const allModels = manager.getAllModels();
291
+
292
+ expect(allModels).toHaveLength(2);
293
+ expect(allModels[0]).toBe(mockModelA);
294
+ expect(allModels[1]).toBe(mockModelZ);
295
+ });
296
+
297
+ it("should sort models by ID ascending when sortBy is 'asc'", async () => {
298
+ sortBy().mockReturnValue("asc");
299
+
300
+ const mockModelZ = createMockModel("model-z");
301
+ const mockModelA = createMockModel("model-a");
302
+ const manager = await createManager(
303
+ createMockServer({
304
+ baseUrl: "http://127.0.0.1:8080",
305
+ models: [mockModelZ, mockModelA],
306
+ }),
307
+ );
308
+
309
+ const allModels = manager.getAllModels();
310
+
311
+ expect(allModels).toHaveLength(2);
312
+ expect(allModels[0]).toBe(mockModelA);
313
+ expect(allModels[1]).toBe(mockModelZ);
314
+ });
315
+
316
+ it("should sort models by ID descending when sortBy is 'desc'", async () => {
317
+ sortBy().mockReturnValue("desc");
318
+
319
+ const mockModelA = createMockModel("model-a");
320
+ const mockModelZ = createMockModel("model-z");
321
+ const manager = await createManager(
322
+ createMockServer({
323
+ baseUrl: "http://127.0.0.1:8080",
324
+ models: [mockModelA, mockModelZ],
325
+ }),
326
+ );
327
+
328
+ const allModels = manager.getAllModels();
329
+
330
+ expect(allModels).toHaveLength(2);
331
+ expect(allModels[0]).toBe(mockModelZ);
332
+ expect(allModels[1]).toBe(mockModelA);
333
+ });
334
+
335
+ it("should sort models by name ascending when sortBy is 'asc-name'", async () => {
336
+ sortBy().mockReturnValue("asc-name");
337
+
338
+ const mockModelB = createMockModel("zebra", { id: "model-b" });
339
+ const mockModelA = createMockModel("alpha", { id: "model-a" });
340
+ const manager = await createManager(
341
+ createMockServer({
342
+ baseUrl: "http://127.0.0.1:8080",
343
+ models: [mockModelB, mockModelA],
344
+ }),
345
+ );
346
+
347
+ const allModels = manager.getAllModels();
348
+
349
+ expect(allModels).toHaveLength(2);
350
+ expect(allModels[0]).toBe(mockModelA);
351
+ expect(allModels[1]).toBe(mockModelB);
352
+ });
353
+
354
+ it("should sort models by name descending when sortBy is 'desc-name'", async () => {
355
+ sortBy().mockReturnValue("desc-name");
356
+
357
+ const mockModelA = createMockModel("alpha", { id: "model-a" });
358
+ const mockModelB = createMockModel("zebra", { id: "model-b" });
359
+ const manager = await createManager(
360
+ createMockServer({
361
+ baseUrl: "http://127.0.0.1:8080",
362
+ models: [mockModelA, mockModelB],
363
+ }),
364
+ );
365
+
366
+ const allModels = manager.getAllModels();
367
+
368
+ expect(allModels).toHaveLength(2);
369
+ expect(allModels[0]).toBe(mockModelB);
370
+ expect(allModels[1]).toBe(mockModelA);
371
+ });
372
+ });
130
373
  });
@@ -1,3 +1,4 @@
1
+ import { readFile, rename, writeFile } from "node:fs/promises";
1
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
3
  import {
3
4
  API_KEY_PLACEHOLDER,
@@ -17,6 +18,7 @@ const mockSettingsManager = vi.hoisted(() => ({
17
18
  getGlobalSettings: vi.fn(),
18
19
  getDefaultThinkingLevel: vi.fn(),
19
20
  getThinkingBudgets: vi.fn(),
21
+ reload: vi.fn(),
20
22
  }));
21
23
 
22
24
  // Mock getAgentDir, readStoredCredential, and SettingsManager before importing resolver
@@ -31,6 +33,8 @@ vi.mock("@earendil-works/pi-coding-agent", () => ({
31
33
 
32
34
  vi.mock("node:fs/promises", () => ({
33
35
  readFile: vi.fn(),
36
+ writeFile: vi.fn(),
37
+ rename: vi.fn(),
34
38
  }));
35
39
 
36
40
  // Import mocked modules
@@ -131,6 +135,34 @@ describe("URL resolution fallback chain", () => {
131
135
 
132
136
  expect(result).toEqual(["http://first:8080", "http://second:9090"]);
133
137
  });
138
+
139
+ it("should drop env URLs without an http(s) scheme, warn, and fall through", async () => {
140
+ process.env.LLAMA_SERVER_URL = "127.0.0.1:8080";
141
+
142
+ const result = settings.resolveUrls();
143
+
144
+ expect(result).toEqual([LLAMA_SERVER_URL]);
145
+ expect(settings.takeWarnings()).toEqual([
146
+ "Ignoring invalid server URL '127.0.0.1:8080' (needs http(s)://)",
147
+ ]);
148
+ expect(settings.takeWarnings()).toEqual([]); // drained
149
+ });
150
+
151
+ it("should drop server entries without an http(s) scheme and warn", async () => {
152
+ mockGetProjectSettings.mockReturnValue({
153
+ llamaSettings: {
154
+ servers: [{ url: "127.0.0.1:8080" }, { url: "http://good:8080/" }],
155
+ },
156
+ });
157
+
158
+ const result = settings.resolveUrls();
159
+
160
+ expect(result).toEqual(["http://good:8080"]);
161
+ expect(settings.takeWarnings()).toEqual([
162
+ "Ignoring invalid server URL '127.0.0.1:8080' (needs http(s)://)",
163
+ ]);
164
+ expect(settings.takeWarnings()).toEqual([]); // drained
165
+ });
134
166
  });
135
167
 
136
168
  describe("llamaSettings.servers resolution", () => {
@@ -315,13 +347,16 @@ describe("API key resolution", () => {
315
347
 
316
348
  describe("Server with custom id", () => {
317
349
  it("should use custom id as providerId when provided", () => {
318
- const server = new Server("http://127.0.0.1:8080", "my-custom-id");
350
+ const server = new Server(settings, {
351
+ baseUrl: "http://127.0.0.1:8080",
352
+ customId: "my-custom-id",
353
+ });
319
354
 
320
355
  expect(server.providerId).toEqual("my-custom-id");
321
356
  });
322
357
 
323
358
  it("should fall back to URL-based providerId when no custom id", () => {
324
- const server = new Server("http://127.0.0.1:8080");
359
+ const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
325
360
 
326
361
  expect(server.providerId).toEqual(
327
362
  `${PROVIDER_PREFIX}=http://127.0.0.1:8080`,
@@ -329,14 +364,19 @@ describe("Server with custom id", () => {
329
364
  });
330
365
 
331
366
  it("should try custom id first in getApiKey(), then fall back to URL-based", () => {
367
+ const server = new Server(settings, {
368
+ baseUrl: "http://127.0.0.1:8080",
369
+ customId: "my-custom-id",
370
+ });
371
+
372
+ // Server construction resolves the key eagerly (ApiClient built there);
373
+ // clear so the assertions below observe only the explicit getApiKey() call
332
374
  vi.clearAllMocks();
333
375
  // Mock: custom id returns placeholder (no key found)
334
376
  mockReadStoredCredential
335
377
  .mockReturnValueOnce(API_KEY_PLACEHOLDER)
336
378
  .mockReturnValueOnce({ key: "fallback-key" });
337
379
 
338
- const server = new Server("http://127.0.0.1:8080", "my-custom-id");
339
-
340
380
  const result = server.getApiKey();
341
381
 
342
382
  expect(result).toEqual("fallback-key");
@@ -350,7 +390,10 @@ describe("Server with custom id", () => {
350
390
  it("should return custom id key directly when found", () => {
351
391
  mockReadStoredCredential.mockReturnValue({ key: "custom-key" });
352
392
 
353
- const server = new Server("http://127.0.0.1:8080", "my-custom-id");
393
+ const server = new Server(settings, {
394
+ baseUrl: "http://127.0.0.1:8080",
395
+ customId: "my-custom-id",
396
+ });
354
397
 
355
398
  const result = server.getApiKey();
356
399
 
@@ -361,27 +404,26 @@ describe("Server with custom id", () => {
361
404
 
362
405
  describe("Server with custom name", () => {
363
406
  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
- );
407
+ const server = new Server(settings, {
408
+ baseUrl: "http://127.0.0.1:8080",
409
+ customName: "Remote Server",
410
+ });
369
411
 
370
412
  expect(server.providerName).toEqual(`Llama.cpp (Remote Server)`);
371
413
  });
372
414
 
373
415
  it("should fall back to URL-based name when no custom name", () => {
374
- const server = new Server("http://127.0.0.1:8080");
416
+ const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
375
417
 
376
418
  expect(server.providerName).toEqual(`Llama.cpp (http://127.0.0.1:8080)`);
377
419
  });
378
420
 
379
421
  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
- );
422
+ const server = new Server(settings, {
423
+ baseUrl: "http://127.0.0.1:8080",
424
+ customId: "my-custom-id",
425
+ customName: "Remote Server",
426
+ });
385
427
 
386
428
  expect(server.providerId).toEqual("my-custom-id");
387
429
  expect(server.providerName).toEqual(`Llama.cpp (Remote Server)`);
@@ -409,6 +451,14 @@ describe("reactToModelSelect and autoloadOnMessage fallbacks", () => {
409
451
  expect(result).toBe(false);
410
452
  });
411
453
 
454
+ it("should return 'asc' when sortBy is not set", async () => {
455
+ const { settings } = await import("../src/managers/settings");
456
+
457
+ const result = settings.resolveSortBy();
458
+
459
+ expect(result).toBe("asc");
460
+ });
461
+
412
462
  it("should return user values when set", async () => {
413
463
  mockSettingsManager.getProjectSettings.mockReturnValue({
414
464
  llamaSettings: {
@@ -634,3 +684,110 @@ describe("Thinking config resolution", () => {
634
684
  );
635
685
  });
636
686
  });
687
+
688
+ describe("setLlamaSetting", () => {
689
+ const mockGetAgentDir = vi.mocked(getAgentDir);
690
+ const mockGetProjectSettings = vi.mocked(
691
+ mockSettingsManager.getProjectSettings,
692
+ );
693
+ const mockGetGlobalSettings = vi.mocked(
694
+ mockSettingsManager.getGlobalSettings,
695
+ );
696
+ const mockReload = vi.mocked(mockSettingsManager.reload);
697
+ const mockReadFile = vi.mocked(readFile);
698
+ const mockWriteFile = vi.mocked(writeFile);
699
+ const mockRename = vi.mocked(rename);
700
+
701
+ const SETTINGS_PATH = "/fake/agent/dir/settings.json";
702
+
703
+ beforeEach(() => {
704
+ vi.clearAllMocks();
705
+ mockGetAgentDir.mockReturnValue("/fake/agent/dir");
706
+ mockGetProjectSettings.mockReturnValue({});
707
+ mockGetGlobalSettings.mockReturnValue({});
708
+ mockReload.mockResolvedValue(undefined);
709
+ mockReadFile.mockResolvedValue("{}");
710
+ mockWriteFile.mockResolvedValue(undefined);
711
+ mockRename.mockResolvedValue(undefined);
712
+ });
713
+
714
+ it("should write the merged llamaSettings key atomically and reload", async () => {
715
+ mockReadFile.mockResolvedValue(
716
+ JSON.stringify(
717
+ { unrelated: true, llamaSettings: { reactToModelSelect: true } },
718
+ null,
719
+ 2,
720
+ ),
721
+ );
722
+
723
+ await settings.setLlamaSetting("sortBy", "desc");
724
+
725
+ expect(mockWriteFile).toHaveBeenCalledTimes(1);
726
+ const [tmpPath, written, encoding] = mockWriteFile.mock.calls[0];
727
+ expect(tmpPath).toBe(`${SETTINGS_PATH}.tmp`);
728
+ expect(encoding).toBe("utf-8");
729
+ const parsed = JSON.parse(written as string);
730
+ expect(parsed).toEqual({
731
+ unrelated: true,
732
+ llamaSettings: { reactToModelSelect: true, sortBy: "desc" },
733
+ });
734
+ expect(mockRename).toHaveBeenCalledWith(
735
+ `${SETTINGS_PATH}.tmp`,
736
+ SETTINGS_PATH,
737
+ );
738
+ expect(mockReload).toHaveBeenCalledTimes(1);
739
+ });
740
+
741
+ it("should reflect the new value in resolvers immediately after the write", async () => {
742
+ mockSettingsManager.reload.mockImplementation(async () => {
743
+ mockGetGlobalSettings.mockReturnValue({
744
+ llamaSettings: { sortBy: "desc" },
745
+ });
746
+ });
747
+
748
+ await settings.setLlamaSetting("sortBy", "desc");
749
+
750
+ expect(settings.resolveSortBy()).toBe("desc");
751
+ });
752
+
753
+ it("should reject and skip reload when the write fails", async () => {
754
+ mockWriteFile.mockRejectedValue(new Error("ENOSPC: simulated"));
755
+
756
+ await expect(settings.setLlamaSetting("sortBy", "desc")).rejects.toThrow(
757
+ "ENOSPC",
758
+ );
759
+ expect(mockReload).not.toHaveBeenCalled();
760
+ });
761
+
762
+ it("should reject and leave the file untouched when the JSON is invalid", async () => {
763
+ mockReadFile.mockResolvedValue("{ broken");
764
+
765
+ await expect(settings.setLlamaSetting("sortBy", "desc")).rejects.toThrow(
766
+ /Cannot parse/,
767
+ );
768
+ expect(mockWriteFile).not.toHaveBeenCalled();
769
+ expect(mockReload).not.toHaveBeenCalled();
770
+ });
771
+
772
+ it("should persist booleans and numbers with type fidelity", async () => {
773
+ await settings.setLlamaSetting("reactToModelSelect", false);
774
+
775
+ const [, firstWrite] = mockWriteFile.mock.calls[0];
776
+ expect(JSON.parse(firstWrite as string)).toEqual({
777
+ llamaSettings: { reactToModelSelect: false },
778
+ });
779
+
780
+ await settings.setLlamaSetting("pollingTimeout", 120000);
781
+
782
+ const [, secondWrite] = mockWriteFile.mock.calls[1];
783
+ expect(JSON.parse(secondWrite as string)).toEqual({
784
+ llamaSettings: { pollingTimeout: 120000 },
785
+ });
786
+ });
787
+
788
+ it("should still construct the manager without arguments", async () => {
789
+ const { LlamaSettingsManager } = await import("../src/managers/settings");
790
+
791
+ expect(() => new LlamaSettingsManager()).not.toThrow();
792
+ });
793
+ });