pi-llama-cpp 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,5 @@
1
1
  import { beforeEach, describe, expect, it } from "vitest";
2
2
  import { Mode } from "../src/enums/mode";
3
- import { Status } from "../src/enums/status";
4
3
  import { DataProperty } from "../src/interfaces/endpoints/models";
5
4
  import { LegacyModel } from "../src/models/legacyModel";
6
5
  import { createMockServer, mockRpc } from "./mocks";
@@ -47,28 +46,14 @@ describe("LegacyModel capabilities", () => {
47
46
 
48
47
  expect(capabilities).toEqual(["text"]);
49
48
  });
50
- });
51
-
52
- describe("LegacyModel getStatus", () => {
53
- it("should return LOADED when not sleeping", async () => {
54
- mockRpc.mockResolvedValueOnce({ is_sleeping: false });
55
-
56
- const model = createModel();
57
- const status = await model.getStatus();
58
49
 
59
- expect(status).toBe(Status.LOADED);
60
- expect(mockRpc).toHaveBeenCalledWith(
61
- `/props?model=${model.id}&autoload=false`,
62
- );
63
- });
64
-
65
- it("should return SLEEPING when is_sleeping is true", async () => {
66
- mockRpc.mockResolvedValueOnce({ is_sleeping: true });
50
+ it("should fall back to text-only when auth fails", async () => {
51
+ mockRpc.mockRejectedValue(new Error("401 Unauthorized"));
67
52
 
68
53
  const model = createModel();
69
- const status = await model.getStatus();
54
+ const capabilities = await model.getCapabilities();
70
55
 
71
- expect(status).toBe(Status.SLEEPING);
56
+ expect(capabilities).toEqual(["text"]);
72
57
  });
73
58
  });
74
59
 
package/tests/mocks.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  } from "../src/constants";
13
13
  import { Mode } from "../src/enums/mode";
14
14
  import { Status } from "../src/enums/status";
15
- import type { LlamaServer } from "../src/interfaces/settings";
15
+ import type { ModelOverride } from "../src/interfaces/settings";
16
16
  import type { LlamaSettingsManager } from "../src/managers/settings";
17
17
  import { BaseModel } from "../src/models/baseModel";
18
18
  import { Server } from "../src/server";
@@ -32,18 +32,19 @@ export const makeSettingsStub = (
32
32
  overrides: Partial<LlamaSettingsManager> = {},
33
33
  ): LlamaSettingsManager =>
34
34
  ({
35
- resolveTimeouts: vi.fn(() => ({
35
+ getLlamaSettings: vi.fn(async () => ({})),
36
+ getLlamaServers: vi.fn(async () => []),
37
+ resolveTimeouts: vi.fn(async () => ({
36
38
  pollingTimeout: POLLING_TIMEOUT,
37
39
  serverTimeout: SERVER_TIMEOUT,
38
40
  })),
39
- resolveServers: vi.fn((): Server[] => []),
40
- resolveSortBy: vi.fn(() => SORT_BY),
41
+ resolveServers: vi.fn(async () => []),
42
+ resolveSortBy: vi.fn(async () => SORT_BY),
41
43
  resolveApiKey: vi.fn(() => API_KEY_PLACEHOLDER),
42
- resolveReactToModelSelect: vi.fn(() => REACT_TO_MODEL_SELECT),
43
- resolveAutoloadOnMessage: vi.fn(() => AUTOLOAD_ON_MESSAGE),
44
+ resolveReactToModelSelect: vi.fn(async () => REACT_TO_MODEL_SELECT),
45
+ resolveAutoloadOnMessage: vi.fn(async () => AUTOLOAD_ON_MESSAGE),
44
46
  resolveThinkingLevel: vi.fn(() => undefined),
45
47
  resolveThinkingBudgets: vi.fn(() => ({ ...THINKING_BUDGETS })),
46
- llamaServers: [] as LlamaServer[],
47
48
  takeWarnings: vi.fn((): string[] => []),
48
49
  setLlamaSetting: vi.fn(() => Promise.resolve()),
49
50
  ...overrides,
@@ -102,6 +103,7 @@ export type MockServerOverrides = Partial<
102
103
  models?: BaseModel[];
103
104
  pollingTimeout?: number;
104
105
  serverTimeout?: number;
106
+ overrides?: Record<string, ModelOverride>;
105
107
  };
106
108
 
107
109
  /**
@@ -124,6 +126,7 @@ export const createMockServer = (
124
126
  models,
125
127
  pollingTimeout,
126
128
  serverTimeout,
129
+ overrides: modelOverrides,
127
130
  initialize,
128
131
  ...members
129
132
  } = overrides;
@@ -131,7 +134,7 @@ export const createMockServer = (
131
134
  const settings = makeSettingsStub({
132
135
  ...(apiKey !== undefined && { resolveApiKey: vi.fn(() => apiKey) }),
133
136
  ...((pollingTimeout !== undefined || serverTimeout !== undefined) && {
134
- resolveTimeouts: vi.fn(() => ({
137
+ resolveTimeouts: vi.fn(async () => ({
135
138
  pollingTimeout: pollingTimeout ?? POLLING_TIMEOUT,
136
139
  serverTimeout: serverTimeout ?? SERVER_TIMEOUT,
137
140
  })),
@@ -145,6 +148,7 @@ export const createMockServer = (
145
148
  baseUrl: baseUrl ?? "http://127.0.0.1:8080",
146
149
  customId,
147
150
  customName,
151
+ overrides: modelOverrides,
148
152
  },
149
153
  {
150
154
  createApiClient: () => apiClient,
@@ -0,0 +1,384 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { FALLBACK_CTX } from "../src/constants";
3
+ import type { LlamaServer, ModelOverride } from "../src/interfaces/settings";
4
+ import { SingleModel } from "../src/models/singleModel";
5
+ import { Server } from "../src/server";
6
+ import {
7
+ addOverrideEntry,
8
+ applyCostFieldValue,
9
+ formatOverrideSummary,
10
+ parseCostValue,
11
+ removeOverrideEntry,
12
+ updateOverrideEntry,
13
+ } from "../src/ui/overrideEntryEditor";
14
+ import { overrideFieldValue } from "../src/ui/overrideSettingsList";
15
+ import { createMockServer, mockRpc } from "./mocks";
16
+
17
+ beforeEach(() => {
18
+ mockRpc.mockReset();
19
+ });
20
+
21
+ const createModel = (overrides?: Record<string, ModelOverride>): SingleModel =>
22
+ new SingleModel(
23
+ {
24
+ id: "test",
25
+ tags: [],
26
+ object: "model",
27
+ owned_by: "test",
28
+ created: Date.now(),
29
+ },
30
+ createMockServer({ overrides }),
31
+ );
32
+
33
+ /** Standard server responses: /props for capabilities, /v1/models for n_ctx */
34
+ const mockDetection = (nCtx: number | undefined) =>
35
+ mockRpc.mockImplementation((endpoint: string) => {
36
+ if (endpoint.startsWith("/props")) {
37
+ return Promise.resolve({ modalities: { vision: false } });
38
+ }
39
+ return Promise.resolve({
40
+ data: [{ id: "test", meta: nCtx === undefined ? {} : { n_ctx: nCtx } }],
41
+ });
42
+ });
43
+
44
+ // ─── Pattern matching (Server.findOverrideForModel) ───────────────────────
45
+
46
+ describe("Server.findOverrideForModel", () => {
47
+ const makeServer = (overrides: Record<string, ModelOverride>): Server =>
48
+ createMockServer({ overrides });
49
+
50
+ it("should match a model by id prefix", () => {
51
+ const server = makeServer({ lla: { reasoning: false } });
52
+ expect(server.findOverrideForModel("llama-3")).toEqual({
53
+ reasoning: false,
54
+ });
55
+ });
56
+
57
+ it("should return undefined when no pattern matches", () => {
58
+ const server = makeServer({ qwen: { reasoning: false } });
59
+ expect(server.findOverrideForModel("llama-3")).toBeUndefined();
60
+ });
61
+
62
+ it("should prefer the longest (most specific) matching pattern", () => {
63
+ const server = makeServer({
64
+ llama: { reasoning: false },
65
+ "llama-3-8b": { reasoning: true, contextSize: 128 },
66
+ });
67
+ expect(server.findOverrideForModel("llama-3-8b")).toEqual({
68
+ reasoning: true,
69
+ contextSize: 128,
70
+ });
71
+ expect(server.findOverrideForModel("llama-3-70b")).toEqual({
72
+ reasoning: false,
73
+ });
74
+ });
75
+
76
+ it("should ignore empty patterns", () => {
77
+ const server = makeServer({ "": { reasoning: false } });
78
+ expect(server.findOverrideForModel("llama-3")).toBeUndefined();
79
+ });
80
+ });
81
+
82
+ // ─── reasoning override ────────────────────────────────────────────────────
83
+
84
+ describe("BaseModel reasoning override", () => {
85
+ it("should default to true when no override matches", () => {
86
+ expect(createModel().reasoning).toBe(true);
87
+ });
88
+
89
+ it("should let the override win over the default", () => {
90
+ const model = createModel({ test: { reasoning: false } });
91
+ expect(model.reasoning).toBe(false);
92
+ });
93
+ });
94
+
95
+ // ─── capabilities override ────────────────────────────────────────────────
96
+
97
+ describe("BaseModel capabilities override", () => {
98
+ it("should fully replace detection without contacting the server", async () => {
99
+ mockRpc.mockImplementation(() => {
100
+ throw new Error("override must not trigger a fetch");
101
+ });
102
+
103
+ const model = createModel({ test: { capabilities: ["text", "image"] } });
104
+ const capabilities = await model.getCapabilities();
105
+
106
+ expect(capabilities).toEqual(["text", "image"]);
107
+ expect(mockRpc).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it("should not intercept detection when no override matches", async () => {
111
+ mockDetection(8192);
112
+
113
+ const model = createModel({ other: { capabilities: ["text"] } });
114
+ const capabilities = await model.getCapabilities();
115
+
116
+ expect(capabilities).toEqual(["text"]);
117
+ expect(mockRpc).toHaveBeenCalled();
118
+ });
119
+ });
120
+
121
+ // ─── contextSize override ─────────────────────────────────────────────────
122
+
123
+ describe("BaseModel.getContextSize with contextSize override", () => {
124
+ it("should return the overridden context size instead of the detected one", async () => {
125
+ mockDetection(8192);
126
+
127
+ const model = createModel({ test: { contextSize: 32768 } });
128
+ const ctxSize = await model.getContextSize();
129
+
130
+ expect(ctxSize).toBe(32768);
131
+ });
132
+
133
+ it("should treat a stored 0 as absent and autodetect", async () => {
134
+ mockDetection(8192);
135
+
136
+ const model = createModel({ test: { contextSize: 0 } });
137
+ const ctxSize = await model.getContextSize();
138
+
139
+ expect(ctxSize).toBe(8192);
140
+ });
141
+
142
+ it("should autodetect when no override matches", async () => {
143
+ mockDetection(8192);
144
+
145
+ const model = createModel({ other: { contextSize: 32768 } });
146
+ const ctxSize = await model.getContextSize();
147
+
148
+ expect(ctxSize).toBe(8192);
149
+ });
150
+ });
151
+
152
+ // ─── toProviderConfig (cost, maxTokens, compat, precedence) ───────────────
153
+
154
+ describe("toProviderConfig overrides", () => {
155
+ it("should merge a partial cost override with zero defaults", async () => {
156
+ mockDetection(8192);
157
+
158
+ const model = createModel({ test: { cost: { input: 0.2 } } });
159
+ const config = await model.toProviderConfig();
160
+
161
+ expect(config.cost).toEqual({
162
+ input: 0.2,
163
+ output: 0,
164
+ cacheRead: 0,
165
+ cacheWrite: 0,
166
+ });
167
+ });
168
+
169
+ it("should default every cost field to zero without an override", async () => {
170
+ mockDetection(8192);
171
+
172
+ const model = createModel();
173
+ const config = await model.toProviderConfig();
174
+
175
+ expect(config.cost).toEqual({
176
+ input: 0,
177
+ output: 0,
178
+ cacheRead: 0,
179
+ cacheWrite: 0,
180
+ });
181
+ });
182
+
183
+ it("should pass a compat override through to the provider config", async () => {
184
+ mockDetection(8192);
185
+
186
+ const compat = {
187
+ supportsStore: false,
188
+ maxTokensField: "max_tokens",
189
+ } as const;
190
+ const model = createModel({ test: { compat } });
191
+ const config = await model.toProviderConfig();
192
+
193
+ expect(config.compat).toEqual(compat);
194
+ });
195
+
196
+ it("should leave compat undefined without an override", async () => {
197
+ mockDetection(8192);
198
+
199
+ const model = createModel();
200
+ const config = await model.toProviderConfig();
201
+
202
+ expect(config.compat).toBeUndefined();
203
+ });
204
+
205
+ it("should use the overridden context size for contextWindow and as the maxTokens fallback", async () => {
206
+ mockDetection(8192);
207
+
208
+ const model = createModel({ test: { contextSize: 32768 } });
209
+ const config = await model.toProviderConfig();
210
+
211
+ expect(config.contextWindow).toBe(32768);
212
+ expect(config.maxTokens).toBe(32768);
213
+ });
214
+
215
+ it("should prefer an explicit maxTokens override over the contextSize override", async () => {
216
+ mockDetection(8192);
217
+
218
+ const model = createModel({
219
+ test: { contextSize: 32768, maxTokens: 4096 },
220
+ });
221
+ const config = await model.toProviderConfig();
222
+
223
+ expect(config.contextWindow).toBe(32768);
224
+ expect(config.maxTokens).toBe(4096);
225
+ });
226
+
227
+ it("should use the detected context size when no override matches", async () => {
228
+ mockDetection(8192);
229
+
230
+ const model = createModel();
231
+ const config = await model.toProviderConfig();
232
+
233
+ expect(config.contextWindow).toBe(8192);
234
+ expect(config.maxTokens).toBe(8192);
235
+ });
236
+
237
+ it("should fall back to FALLBACK_CTX when the model is missing from detection", async () => {
238
+ mockRpc.mockImplementation((endpoint: string) => {
239
+ if (endpoint.startsWith("/props")) {
240
+ return Promise.resolve({ modalities: { vision: false } });
241
+ }
242
+ // /v1/models: no entry for this model → getContextSize throws → fallback
243
+ return Promise.resolve({ data: [] });
244
+ });
245
+
246
+ const model = createModel();
247
+ const config = await model.toProviderConfig();
248
+
249
+ expect(config.contextWindow).toBe(FALLBACK_CTX);
250
+ expect(config.maxTokens).toBe(FALLBACK_CTX);
251
+ });
252
+ });
253
+
254
+ // ─── formatOverrideSummary ────────────────────────────────────────────────
255
+
256
+ describe("formatOverrideSummary", () => {
257
+ it("should render every configured field", () => {
258
+ const summary = formatOverrideSummary({
259
+ cost: { input: 0.2, output: 0.6, cacheRead: 0.01 },
260
+ capabilities: ["text", "image"],
261
+ reasoning: false,
262
+ contextSize: 32768,
263
+ maxTokens: 4096,
264
+ });
265
+
266
+ expect(summary).toContain("input: $0.2");
267
+ expect(summary).toContain("output: $0.6");
268
+ expect(summary).toContain("cache read: $0.01");
269
+ expect(summary).toContain("capabilities: text,image");
270
+ expect(summary).toContain("reasoning: false");
271
+ expect(summary).toContain("contextSize: 32768");
272
+ expect(summary).toContain("maxTokens: 4096");
273
+ });
274
+
275
+ it("should omit zero, undefined and empty fields", () => {
276
+ const summary = formatOverrideSummary({
277
+ cost: { input: 0, output: 0.6 },
278
+ reasoning: undefined,
279
+ });
280
+
281
+ expect(summary).not.toContain("input:");
282
+ expect(summary).toContain("output: $0.6");
283
+ expect(summary).not.toContain("capabilities");
284
+ expect(summary).not.toContain("reasoning");
285
+ expect(summary).not.toContain("contextSize");
286
+ expect(summary).not.toContain("maxTokens");
287
+ });
288
+
289
+ it("should render an em dash for an empty override", () => {
290
+ expect(formatOverrideSummary({})).toBe("—");
291
+ });
292
+ });
293
+
294
+ // ─── override entry list helpers (overrideEntryEditor) ────────────────────
295
+
296
+ describe("override entry list helpers", () => {
297
+ const servers = [
298
+ { url: "http://a", overrides: { "a-1": { reasoning: false } } },
299
+ { url: "http://b" },
300
+ ] as LlamaServer[];
301
+
302
+ it("addOverrideEntry should append a pattern → override entry", () => {
303
+ const next = addOverrideEntry(servers, 0, "a-2", { contextSize: 4096 });
304
+ expect(next[0].overrides).toEqual({
305
+ "a-1": { reasoning: false },
306
+ "a-2": { contextSize: 4096 },
307
+ });
308
+ });
309
+
310
+ it("updateOverrideEntry should replace in place without reordering", () => {
311
+ const next = updateOverrideEntry(servers, 0, 0, "a-renamed", {
312
+ reasoning: true,
313
+ });
314
+ expect(Object.keys(next[0].overrides!)).toEqual(["a-renamed"]);
315
+ expect(next[0].overrides!["a-renamed"]).toEqual({ reasoning: true });
316
+ expect(next[1]).toBe(servers[1]);
317
+ });
318
+
319
+ it("removeOverrideEntry should drop only the targeted entry", () => {
320
+ const withTwo = addOverrideEntry(servers, 0, "a-2", {});
321
+ const next = removeOverrideEntry(withTwo, 0, 0);
322
+ expect(Object.keys(next[0].overrides!)).toEqual(["a-2"]);
323
+ });
324
+
325
+ it("parseCostValue should accept blank, non-negative numbers; reject the rest", () => {
326
+ expect(parseCostValue("")).toBe(0);
327
+ expect(parseCostValue(" 0.5 ")).toBe(0.5);
328
+ expect(parseCostValue("-1")).toBeNull();
329
+ expect(parseCostValue("abc")).toBeNull();
330
+ expect(parseCostValue("Infinity")).toBeNull();
331
+ });
332
+
333
+ it("applyCostFieldValue should set a positive value, keeping other fields", () => {
334
+ const next = applyCostFieldValue({ input: 0.2 }, "output", 0.6);
335
+ expect(next).toEqual({ input: 0.2, output: 0.6 });
336
+ });
337
+
338
+ it("applyCostFieldValue should remove the field on zero", () => {
339
+ const next = applyCostFieldValue({ input: 0.2, output: 0.6 }, "input", 0);
340
+ expect(next).toEqual({ output: 0.6 });
341
+ });
342
+
343
+ it("applyCostFieldValue should return undefined when the last field is zeroed", () => {
344
+ expect(applyCostFieldValue({ input: 0.2 }, "input", 0)).toBeUndefined();
345
+ expect(applyCostFieldValue(undefined, "input", 0)).toBeUndefined();
346
+ });
347
+
348
+ it("applyCostFieldValue should treat an empty override as unset cost", () => {
349
+ expect(applyCostFieldValue(undefined, "input", 0.5)).toEqual({
350
+ input: 0.5,
351
+ });
352
+ });
353
+ });
354
+
355
+ describe("overrideFieldValue", () => {
356
+ it("formats cost fields, defaulting to 0", () => {
357
+ expect(overrideFieldValue("cost.input", {})).toBe("0");
358
+ expect(overrideFieldValue("cost.input", { cost: { input: 0.2 } })).toBe(
359
+ "0.2",
360
+ );
361
+ expect(overrideFieldValue("cost.cacheWrite", {})).toBe("0");
362
+ });
363
+
364
+ it("formats capabilities and reasoning labels", () => {
365
+ expect(overrideFieldValue("capabilities", {})).toBe("text");
366
+ expect(
367
+ overrideFieldValue("capabilities", { capabilities: ["text", "image"] }),
368
+ ).toBe("text | image");
369
+ expect(overrideFieldValue("reasoning", {})).toBe("true");
370
+ expect(overrideFieldValue("reasoning", { reasoning: false })).toBe("false");
371
+ });
372
+
373
+ it("formats maxTokens/contextSize, defaulting to 0", () => {
374
+ expect(overrideFieldValue("maxTokens", {})).toBe("0");
375
+ expect(overrideFieldValue("maxTokens", { maxTokens: 4096 })).toBe("4096");
376
+ expect(overrideFieldValue("contextSize", { contextSize: 8192 })).toBe(
377
+ "8192",
378
+ );
379
+ });
380
+
381
+ it("returns an empty label for unknown fields", () => {
382
+ expect(overrideFieldValue("other", {})).toBe("");
383
+ });
384
+ });
@@ -1,4 +1,4 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { POLLING_TIMEOUT, SERVER_TIMEOUT } from "../src/constants";
3
3
  import { ServerStatus } from "../src/enums/serverStatus";
4
4
  import type { LlamaSettingsManager } from "../src/managers/settings";
@@ -35,6 +35,26 @@ describe("Server providerName", () => {
35
35
  });
36
36
  });
37
37
 
38
+ describe("Server apiBaseUrl", () => {
39
+ it("should append the ENDPOINT_PREFIX to baseUrl", () => {
40
+ const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
41
+ expect(server.apiBaseUrl).toBe("http://127.0.0.1:8080/v1");
42
+ });
43
+
44
+ it("should not double the prefix when baseUrl already ends with it", () => {
45
+ const server = new Server(settings, {
46
+ baseUrl: "http://127.0.0.1:8080/v1",
47
+ });
48
+ expect(server.apiBaseUrl).toBe("http://127.0.0.1:8080/v1");
49
+ });
50
+
51
+ it("should keep baseUrl untouched for provider identity", () => {
52
+ const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
53
+ expect(server.baseUrl).toBe("http://127.0.0.1:8080");
54
+ expect(server.providerId).toBe("llama-server=http://127.0.0.1:8080");
55
+ });
56
+ });
57
+
38
58
  describe("Server fetchModels", () => {
39
59
  it("should call the /models endpoint", async () => {
40
60
  mockRpc.mockResolvedValueOnce({
@@ -154,16 +174,16 @@ describe("Server postRequest", () => {
154
174
  });
155
175
  });
156
176
 
157
- describe("Server timeouts", () => {
158
- it("should resolve timeouts from the injected settings", () => {
177
+ describe("Server timeouts", async () => {
178
+ it("should resolve timeouts from the injected settings", async () => {
159
179
  const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
160
180
 
161
- expect(server.serverTimeout).toBe(SERVER_TIMEOUT);
162
- expect(server.pollingTimeout).toBe(POLLING_TIMEOUT);
181
+ expect(await server.getServerTimeout()).toBe(SERVER_TIMEOUT);
182
+ expect(await server.getPollingTimeout()).toBe(POLLING_TIMEOUT);
163
183
  });
164
184
 
165
- it("should return resolved custom values", () => {
166
- vi.mocked(settings.resolveTimeouts).mockReturnValue({
185
+ it("should return resolved custom values", async () => {
186
+ vi.mocked(settings.resolveTimeouts).mockResolvedValue({
167
187
  pollingTimeout: 90000,
168
188
  serverTimeout: 2000,
169
189
  });
@@ -174,29 +194,39 @@ describe("Server timeouts", () => {
174
194
  customName: "My Server",
175
195
  });
176
196
 
177
- expect(server.pollingTimeout).toBe(90000);
197
+ expect(await server.getPollingTimeout()).toBe(90000);
178
198
  });
179
199
 
180
- it("should read timeouts live from settings", () => {
200
+ it("should read timeouts live from settings", async () => {
181
201
  const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
182
202
 
183
- vi.mocked(settings.resolveTimeouts).mockReturnValue({
203
+ vi.mocked(settings.resolveTimeouts).mockResolvedValue({
184
204
  pollingTimeout: 120000,
185
205
  serverTimeout: 3000,
186
206
  });
187
- expect(server.pollingTimeout).toBe(120000);
207
+ expect(await server.getPollingTimeout()).toBe(120000);
188
208
 
189
- vi.mocked(settings.resolveTimeouts).mockReturnValue({
209
+ vi.mocked(settings.resolveTimeouts).mockResolvedValue({
190
210
  pollingTimeout: 90000,
191
211
  serverTimeout: 2000,
192
212
  });
193
- expect(server.pollingTimeout).toBe(90000);
213
+ expect(await server.getPollingTimeout()).toBe(90000);
194
214
  });
195
215
  });
196
216
 
197
217
  describe("Server isReady", () => {
218
+ // `isReady` delegates to the shared health probe (`utils/health`), which
219
+ // uses plain `fetch` instead of the ApiClient — stub it per test.
220
+ const stubFetch = (impl: () => Promise<unknown>) => {
221
+ vi.stubGlobal("fetch", vi.fn(impl));
222
+ };
223
+
224
+ afterEach(() => {
225
+ vi.unstubAllGlobals();
226
+ });
227
+
198
228
  it("should return READY when health status is ok", async () => {
199
- mockRpc.mockResolvedValueOnce({ status: "ok" });
229
+ stubFetch(async () => ({ json: async () => ({ status: "ok" }) }));
200
230
 
201
231
  const server = createMockServer();
202
232
  const status = await server.isReady(1000);
@@ -205,7 +235,9 @@ describe("Server isReady", () => {
205
235
  });
206
236
 
207
237
  it("should return UNREACHABLE when health check fails", async () => {
208
- mockRpc.mockRejectedValueOnce(new Error("connection refused"));
238
+ stubFetch(async () => {
239
+ throw new Error("connection refused");
240
+ });
209
241
 
210
242
  const server = createMockServer();
211
243
  const status = await server.isReady(1000);
@@ -214,11 +246,22 @@ describe("Server isReady", () => {
214
246
  });
215
247
 
216
248
  it("should return UNREACHABLE when health status is not ok", async () => {
217
- mockRpc.mockResolvedValueOnce({ status: "error" });
249
+ stubFetch(async () => ({ json: async () => ({ status: "error" }) }));
218
250
 
219
251
  const server = createMockServer();
220
252
  const status = await server.isReady(1000);
221
253
 
222
254
  expect(status).toBe(ServerStatus.UNREACHABLE);
223
255
  });
256
+
257
+ it("should return TIMEOUT when the health check aborts", async () => {
258
+ stubFetch(async () => {
259
+ throw new DOMException("The operation timed out.", "TimeoutError");
260
+ });
261
+
262
+ const server = createMockServer();
263
+ const status = await server.isReady(1000);
264
+
265
+ expect(status).toBe(ServerStatus.TIMEOUT);
266
+ });
224
267
  });