pi-llama-cpp 0.9.1 → 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.
@@ -1,5 +1,5 @@
1
1
  import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
2
- import { DEFAULT_CTX, POLLING_INTERVAL, POLLING_TIMEOUT } from "../constants";
2
+ import { FALLBACK_CTX, POLLING_INTERVAL } from "../constants";
3
3
  import { Mode } from "../enums/mode";
4
4
  import { Status } from "../enums/status";
5
5
  import { DataProperty } from "../interfaces/endpoints/models";
@@ -130,9 +130,9 @@ export abstract class BaseModel {
130
130
  const { data } = await this.server.fetchModels();
131
131
  const { n_ctx } = data.find((m) => m.id === this.id)?.meta!;
132
132
 
133
- return n_ctx ?? DEFAULT_CTX;
133
+ return n_ctx ?? FALLBACK_CTX;
134
134
  } catch {
135
- return DEFAULT_CTX;
135
+ return FALLBACK_CTX;
136
136
  }
137
137
  }
138
138
 
@@ -202,11 +202,31 @@ export abstract class BaseModel {
202
202
  await this.server.postRequest("load", this.id);
203
203
 
204
204
  if (await this.server.sseManager.probeSSE()) {
205
- const { status, exit_code } =
206
- await this.server.sseManager.subscribeToStatus(this.id);
205
+ try {
206
+ const { status, exit_code } =
207
+ await this.server.sseManager.subscribeToStatus(this.id);
207
208
 
208
- if (status === "failed" || (status === "unloaded" && exit_code !== 0)) {
209
- throw new Error(`Model loading failed: ${this.id}`);
209
+ if (status === "failed" || (status === "unloaded" && exit_code !== 0)) {
210
+ throw new Error(`Model loading failed: ${this.id}`);
211
+ }
212
+ } catch (err) {
213
+ // A real failed status from the server stays fatal.
214
+ if (
215
+ err instanceof Error &&
216
+ err.message.startsWith("Model loading failed")
217
+ ) {
218
+ throw err;
219
+ }
220
+ // An SSE timeout or connection failure is not proof the load failed:
221
+ // subscribeToStatus timers are only cleared by a terminal event on
222
+ // the same SSE connection, so a timer orphaned by a server restart
223
+ // or reconnect can fire during a later, healthy load. Check the
224
+ // model's real status over HTTP before surfacing an error.
225
+ await this.pollStatus();
226
+ const finalStatus = await this.getStatus();
227
+ if (finalStatus !== Status.LOADED && finalStatus !== Status.SLEEPING) {
228
+ throw new Error(`Model loading failed: ${this.id}`);
229
+ }
210
230
  }
211
231
  } else {
212
232
  await this.pollStatus();
@@ -224,14 +244,17 @@ export abstract class BaseModel {
224
244
  * Polls llama-server to check when the model is loaded
225
245
  *
226
246
  * @param startTime The initial polling timestamp
227
- * @param timeout The maximum amount of ms before timeout. Defaults to POLLING_TIMEOUT
247
+ * @param timeout The maximum amount of ms before timeout. Defaults to server's pollingTimeout
228
248
  * @param interval The polling interval. Defaults to POLLING_INTERVAL
229
249
  */
230
250
  async pollStatus(
231
251
  startTime: number = Date.now(),
232
- timeout: number = POLLING_TIMEOUT,
252
+ timeout?: number,
233
253
  interval: number = POLLING_INTERVAL,
234
254
  ): Promise<void> {
255
+ if (timeout === undefined) {
256
+ timeout = this.server.pollingTimeout;
257
+ }
235
258
  while ((await this.getStatus()) === Status.LOADING) {
236
259
  // Force a timeout if we wasted too much time polling
237
260
  if (Date.now() - startTime > timeout) {
@@ -1,4 +1,4 @@
1
- import { DEFAULT_CTX } from "../constants";
1
+ import { FALLBACK_CTX } from "../constants";
2
2
  import { Mode } from "../enums/mode";
3
3
  import { SingleModel } from "./singleModel";
4
4
 
@@ -25,7 +25,7 @@ export class LegacyModel extends SingleModel {
25
25
  const [{ max_model_len }] = data;
26
26
  const contextSize = max_model_len === 0 ? n_ctx : max_model_len;
27
27
 
28
- return contextSize ?? DEFAULT_CTX;
28
+ return contextSize ?? FALLBACK_CTX;
29
29
  }
30
30
 
31
31
  /**
@@ -1,4 +1,4 @@
1
- import { DEFAULT_CTX, POLLING_INTERVAL, POLLING_TIMEOUT } from "../constants";
1
+ import { FALLBACK_CTX, POLLING_INTERVAL } from "../constants";
2
2
  import { Mode } from "../enums/mode";
3
3
  import { Status } from "../enums/status";
4
4
  import { BaseModel } from "./baseModel";
@@ -41,7 +41,7 @@ export class RouterModel extends BaseModel {
41
41
  }
42
42
  }
43
43
 
44
- const timeout = POLLING_TIMEOUT - elapsed;
44
+ const timeout = this.server.pollingTimeout - elapsed;
45
45
  return await super.pollStatus(startTime, timeout);
46
46
  }
47
47
 
@@ -60,7 +60,7 @@ export class RouterModel extends BaseModel {
60
60
  const response =
61
61
  this.extractFrom("--ctx-size") ??
62
62
  this.extractFrom("--fit-ctx") ??
63
- DEFAULT_CTX;
63
+ FALLBACK_CTX;
64
64
 
65
65
  return response;
66
66
  }
package/src/server.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import { ApiClient } from "./api/client";
2
- import { PROVIDER_NAME, PROVIDER_PREFIX } from "./constants";
2
+ import {
3
+ API_KEY_PLACEHOLDER,
4
+ POLLING_TIMEOUT,
5
+ PROVIDER_NAME,
6
+ PROVIDER_PREFIX,
7
+ SERVER_TIMEOUT,
8
+ } from "./constants";
3
9
  import { Mode } from "./enums/mode";
4
10
  import { ServerStatus } from "./enums/serverStatus";
5
11
  import { HealthEndpoint } from "./interfaces/endpoints/health";
@@ -8,20 +14,25 @@ import {
8
14
  PropsEndpoint,
9
15
  PropsModelEndpoint,
10
16
  } from "./interfaces/endpoints/props";
17
+ import { settings } from "./managers/settings";
11
18
  import { BaseModel } from "./models/baseModel";
12
19
  import { LegacyModel } from "./models/legacyModel";
13
20
  import { RouterModel } from "./models/routerModel";
14
21
  import { SingleModel } from "./models/singleModel";
15
- import { ConfigResolver } from "./resolver";
16
22
  import { SSEManager } from "./sse/manager";
17
23
 
18
24
  export class Server {
19
25
  public readonly models: BaseModel[] = [];
20
- private configResolver = new ConfigResolver();
21
26
  private apiClient!: ApiClient;
22
27
  private sse!: SSEManager;
23
28
 
24
- constructor(readonly baseUrl: string) {}
29
+ constructor(
30
+ readonly baseUrl: string,
31
+ private readonly customId?: string,
32
+ private readonly customName?: string,
33
+ readonly serverTimeout: number = SERVER_TIMEOUT,
34
+ readonly pollingTimeout: number = POLLING_TIMEOUT,
35
+ ) {}
25
36
 
26
37
  /**
27
38
  * Provides access to the SSE manager for direct subscriptions.
@@ -32,24 +43,37 @@ export class Server {
32
43
 
33
44
  /**
34
45
  * Generates a unique provider ID from a server URL.
46
+ * Uses custom ID if provided, otherwise falls back to URL-based ID.
35
47
  */
36
48
  get providerId(): string {
37
- return `${PROVIDER_PREFIX}=${this.baseUrl}`;
49
+ return this.customId ?? `${PROVIDER_PREFIX}=${this.baseUrl}`;
38
50
  }
39
51
 
40
52
  /**
41
53
  * Generates a human-readable provider name from a server URL.
54
+ * Uses custom name as suffix if provided.
42
55
  */
43
56
  get providerName(): string {
57
+ if (this.customName) {
58
+ return `${PROVIDER_NAME} (${this.customName})`;
59
+ }
44
60
  return `${PROVIDER_NAME} (${this.baseUrl})`;
45
61
  }
46
62
 
47
63
  /**
48
- * Retrieves the API key from the resolver
64
+ * Retrieves the API key from the config resolver.
65
+ * Tries custom ID first, then falls back to URL-based ID.
66
+ *
49
67
  * @returns The API key
50
68
  */
51
- async getApiKey(): Promise<string> {
52
- return await this.configResolver.resolveApiKey(this.providerId);
69
+ getApiKey(): string {
70
+ // Try custom ID first
71
+ if (this.customId) {
72
+ const key = settings.resolveApiKey(this.customId);
73
+ if (key !== API_KEY_PLACEHOLDER) return key;
74
+ }
75
+ // Fall back to URL-based ID
76
+ return settings.resolveApiKey(`${PROVIDER_PREFIX}=${this.baseUrl}`);
53
77
  }
54
78
 
55
79
  /**
@@ -59,7 +83,7 @@ export class Server {
59
83
  async initialize() {
60
84
  const apiKey = await this.getApiKey();
61
85
  this.apiClient = new ApiClient(this.baseUrl, apiKey);
62
- this.sse = new SSEManager(this.baseUrl, apiKey);
86
+ this.sse = new SSEManager(this.baseUrl, apiKey, this.serverTimeout);
63
87
  const { data } = await this.fetchModels();
64
88
  const mode = await this.detectServerMode();
65
89
 
@@ -27,6 +27,7 @@ export class SSEManager {
27
27
  constructor(
28
28
  private readonly baseUrl: string,
29
29
  private readonly apiKey: string,
30
+ readonly serverTimeout: number = SERVER_TIMEOUT,
30
31
  ) {}
31
32
 
32
33
  /**
@@ -52,7 +53,7 @@ export class SSEManager {
52
53
  }
53
54
  const response = await fetch(url, {
54
55
  method: "GET",
55
- signal: AbortSignal.timeout(SERVER_TIMEOUT),
56
+ signal: AbortSignal.timeout(this.serverTimeout),
56
57
  });
57
58
  this.sseSupported =
58
59
  response.ok &&
@@ -1,7 +1,25 @@
1
1
  import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { DEFAULT_THINKING_BUDGETS } from "../src/constants";
2
+ import { THINKING_BUDGETS } from "../src/constants";
3
+ import { Status } from "../src/enums/status";
3
4
  import { createMockModel, createMockServer } from "./mocks";
4
5
 
6
+ // Mock settings — reactToModelSelect and autoloadOnMessage
7
+ const mockSettings = {
8
+ resolveReactToModelSelect: vi.fn(() => true),
9
+ resolveAutoloadOnMessage: vi.fn(() => false),
10
+ resolveThinkingLevel: vi.fn(() => "medium"),
11
+ resolveThinkingBudgets: vi.fn(() => ({ ...THINKING_BUDGETS })),
12
+ };
13
+
14
+ // Wire resolveThinkingBudgets to use the SettingsManager mock when set
15
+ mockSettings.resolveThinkingBudgets.mockImplementation(() => {
16
+ const userBudgets = mockSettingsManager.getThinkingBudgets();
17
+ if (userBudgets) {
18
+ return { ...THINKING_BUDGETS, ...userBudgets };
19
+ }
20
+ return { ...THINKING_BUDGETS };
21
+ });
22
+
5
23
  // Create a mutable mock object shared across tests
6
24
  const mockSettingsManager = {
7
25
  getDefaultThinkingLevel: vi.fn(() => "medium"),
@@ -19,6 +37,10 @@ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
19
37
  };
20
38
  });
21
39
 
40
+ vi.mock("../src/managers/settings", () => ({
41
+ settings: mockSettings,
42
+ }));
43
+
22
44
  let EventManager: typeof import("../src/managers/events").EventManager;
23
45
 
24
46
  beforeAll(async () => {
@@ -44,6 +66,21 @@ const createNonLlamaPayload = () => ({
44
66
  messages: [{ role: "user", content: "hello" }],
45
67
  });
46
68
 
69
+ const createMockCtx = (
70
+ thinkingLevel?:
71
+ | "off"
72
+ | "minimal"
73
+ | "low"
74
+ | "medium"
75
+ | "high"
76
+ | "xhigh"
77
+ | "max",
78
+ ) =>
79
+ ({
80
+ thinkingLevel,
81
+ ui: { notify: vi.fn() },
82
+ }) as any;
83
+
47
84
  describe("EventManager.onBeforeProviderRequest", () => {
48
85
  describe("normal usage — each thinking level", () => {
49
86
  it.each([
@@ -68,8 +105,10 @@ describe("EventManager.onBeforeProviderRequest", () => {
68
105
  const eventManager = new EventManager([server]);
69
106
  const event = { payload: createPayload("model-a") };
70
107
 
108
+ const ctx = createMockCtx(level as any);
71
109
  const result = (await eventManager.onBeforeProviderRequest(
72
110
  event as any,
111
+ ctx,
73
112
  )) as Record<string, unknown>;
74
113
 
75
114
  expect(result.model).toBe("model-a");
@@ -92,13 +131,15 @@ describe("EventManager.onBeforeProviderRequest", () => {
92
131
  },
93
132
  };
94
133
 
134
+ const ctx = createMockCtx("low");
95
135
  const result = (await eventManager.onBeforeProviderRequest(
96
136
  event as any,
137
+ ctx,
97
138
  )) as Record<string, unknown>;
98
139
 
99
140
  expect(result.messages).toEqual([{ role: "user", content: "test" }]);
100
141
  expect(result.temperature).toBe(0.7);
101
- expect(result.thinking_budget_tokens).toBe(DEFAULT_THINKING_BUDGETS.low);
142
+ expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.low);
102
143
  });
103
144
  });
104
145
 
@@ -110,7 +151,11 @@ describe("EventManager.onBeforeProviderRequest", () => {
110
151
  const eventManager = new EventManager([server]);
111
152
  const event = { payload: createNonLlamaPayload() };
112
153
 
113
- const result = await eventManager.onBeforeProviderRequest(event as any);
154
+ const ctx = createMockCtx();
155
+ const result = await eventManager.onBeforeProviderRequest(
156
+ event as any,
157
+ ctx,
158
+ );
114
159
 
115
160
  expect(result).toEqual(createNonLlamaPayload());
116
161
  });
@@ -124,7 +169,11 @@ describe("EventManager.onBeforeProviderRequest", () => {
124
169
  const eventManager = new EventManager([server]);
125
170
  const event = { payload: { messages: [] } };
126
171
 
127
- const result = await eventManager.onBeforeProviderRequest(event as any);
172
+ const ctx = createMockCtx();
173
+ const result = await eventManager.onBeforeProviderRequest(
174
+ event as any,
175
+ ctx,
176
+ );
128
177
 
129
178
  expect(result).toEqual({ messages: [] });
130
179
  });
@@ -141,8 +190,10 @@ describe("EventManager.onBeforeProviderRequest", () => {
141
190
  const eventManager = new EventManager([server]);
142
191
  const event = { payload: createPayload("model-a") };
143
192
 
193
+ const ctx = createMockCtx("low");
144
194
  const result = (await eventManager.onBeforeProviderRequest(
145
195
  event as any,
196
+ ctx,
146
197
  )) as Record<string, unknown>;
147
198
 
148
199
  expect(result.thinking_budget_tokens).toBe(4096);
@@ -158,14 +209,14 @@ describe("EventManager.onBeforeProviderRequest", () => {
158
209
  const eventManager = new EventManager([server]);
159
210
  const event = { payload: createPayload("model-a") };
160
211
 
212
+ const ctx = createMockCtx("medium");
161
213
  const result = (await eventManager.onBeforeProviderRequest(
162
214
  event as any,
215
+ ctx,
163
216
  )) as Record<string, unknown>;
164
217
 
165
218
  // medium uses default since user only overrode low
166
- expect(result.thinking_budget_tokens).toBe(
167
- DEFAULT_THINKING_BUDGETS.medium,
168
- );
219
+ expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.medium);
169
220
  });
170
221
  });
171
222
 
@@ -185,14 +236,14 @@ describe("EventManager.onBeforeProviderRequest", () => {
185
236
  const eventManager = new EventManager([server]);
186
237
  const event = { payload: createPayload("model-a") };
187
238
 
239
+ const ctx = createMockCtx("medium");
188
240
  const result = (await eventManager.onBeforeProviderRequest(
189
241
  event as any,
242
+ ctx,
190
243
  )) as Record<string, unknown>;
191
244
 
192
245
  // Should fall back to default since "medium" is not in user budgets
193
- expect(result.thinking_budget_tokens).toBe(
194
- DEFAULT_THINKING_BUDGETS.medium,
195
- );
246
+ expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.medium);
196
247
  });
197
248
 
198
249
  it("should not allow overriding 'off' — thinking stays disabled", async () => {
@@ -207,8 +258,10 @@ describe("EventManager.onBeforeProviderRequest", () => {
207
258
  const eventManager = new EventManager([server]);
208
259
  const event = { payload: createPayload("model-a") };
209
260
 
261
+ const ctx = createMockCtx("off");
210
262
  const result = (await eventManager.onBeforeProviderRequest(
211
263
  event as any,
264
+ ctx,
212
265
  )) as Record<string, unknown>;
213
266
 
214
267
  expect(result).toMatchObject({
@@ -229,8 +282,10 @@ describe("EventManager.onBeforeProviderRequest", () => {
229
282
  const eventManager = new EventManager([server]);
230
283
  const event = { payload: createPayload("model-a") };
231
284
 
285
+ const ctx = createMockCtx("max");
232
286
  const result = (await eventManager.onBeforeProviderRequest(
233
287
  event as any,
288
+ ctx,
234
289
  )) as Record<string, unknown>;
235
290
 
236
291
  expect(result).toEqual(createPayload("model-a"));
@@ -247,11 +302,131 @@ describe("EventManager.onBeforeProviderRequest", () => {
247
302
  const eventManager = new EventManager([server]);
248
303
  const event = { payload: createPayload("model-a") };
249
304
 
305
+ const ctx = createMockCtx("high");
250
306
  const result = (await eventManager.onBeforeProviderRequest(
251
307
  event as any,
308
+ ctx,
252
309
  )) as Record<string, unknown>;
253
310
 
254
- expect(result.thinking_budget_tokens).toBe(DEFAULT_THINKING_BUDGETS.high);
311
+ expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.high);
312
+ });
313
+ });
314
+ });
315
+
316
+ describe("EventManager.onModelSelect", () => {
317
+ beforeEach(() => {
318
+ mockSettings.resolveReactToModelSelect.mockReturnValue(true);
319
+ });
320
+
321
+ it("should load the model when reactToModelSelect is true", async () => {
322
+ const server = createMockServer({
323
+ models: ["model-a"].map((id) => createMockModel(id)),
324
+ });
325
+ const eventManager = new EventManager([server]);
326
+ const ctx = createMockCtx();
327
+
328
+ const event = {
329
+ model: { provider: server.providerId, id: "model-a" },
330
+ } as any;
331
+
332
+ await eventManager.onModelSelect(event, ctx);
333
+
334
+ expect(ctx.ui.notify).toHaveBeenCalledWith("Loading model-a...", "info");
335
+ });
336
+
337
+ it("should return early when reactToModelSelect is false", async () => {
338
+ mockSettings.resolveReactToModelSelect.mockReturnValue(false);
339
+
340
+ const server = createMockServer({
341
+ models: ["model-a"].map((id) => createMockModel(id)),
342
+ });
343
+ const eventManager = new EventManager([server]);
344
+ const ctx = createMockCtx();
345
+
346
+ const event = {
347
+ model: { provider: server.providerId, id: "model-a" },
348
+ } as any;
349
+
350
+ await eventManager.onModelSelect(event, ctx);
351
+
352
+ expect(ctx.ui.notify).not.toHaveBeenCalled();
353
+ });
354
+ });
355
+
356
+ describe("EventManager.autoLoadIfNeeded", () => {
357
+ it("should load the model when autoloadOnMessage is true and model is UNLOADED", async () => {
358
+ mockSettings.resolveAutoloadOnMessage.mockReturnValue(true);
359
+
360
+ const server = createMockServer({
361
+ models: [
362
+ createMockModel("model-a", {
363
+ getStatus: vi.fn().mockResolvedValue(Status.UNLOADED),
364
+ load: vi.fn().mockResolvedValue(undefined),
365
+ }),
366
+ ],
367
+ });
368
+ const eventManager = new EventManager([server]);
369
+ const model = server.models[0];
370
+
371
+ await (eventManager as any).autoLoadIfNeeded(model);
372
+
373
+ expect(model.load).toHaveBeenCalled();
374
+ });
375
+
376
+ it("should not load the model when autoloadOnMessage is false", async () => {
377
+ mockSettings.resolveAutoloadOnMessage.mockReturnValue(false);
378
+
379
+ const server = createMockServer({
380
+ models: [
381
+ createMockModel("model-a", {
382
+ getStatus: vi.fn().mockResolvedValue(Status.UNLOADED),
383
+ load: vi.fn().mockResolvedValue(undefined),
384
+ }),
385
+ ],
386
+ });
387
+ const eventManager = new EventManager([server]);
388
+ const model = server.models[0];
389
+
390
+ await (eventManager as any).autoLoadIfNeeded(model);
391
+
392
+ expect(model.load).not.toHaveBeenCalled();
393
+ });
394
+
395
+ it("should not load the model when model is already LOADED", async () => {
396
+ mockSettings.resolveAutoloadOnMessage.mockReturnValue(true);
397
+
398
+ const server = createMockServer({
399
+ models: [
400
+ createMockModel("model-a", {
401
+ getStatus: vi.fn().mockResolvedValue(Status.LOADED),
402
+ load: vi.fn().mockResolvedValue(undefined),
403
+ }),
404
+ ],
255
405
  });
406
+ const eventManager = new EventManager([server]);
407
+ const model = server.models[0];
408
+
409
+ await (eventManager as any).autoLoadIfNeeded(model);
410
+
411
+ expect(model.load).not.toHaveBeenCalled();
412
+ });
413
+
414
+ it("should not load the model when model is SLEEPING", async () => {
415
+ mockSettings.resolveAutoloadOnMessage.mockReturnValue(true);
416
+
417
+ const server = createMockServer({
418
+ models: [
419
+ createMockModel("model-a", {
420
+ getStatus: vi.fn().mockResolvedValue(Status.SLEEPING),
421
+ load: vi.fn().mockResolvedValue(undefined),
422
+ }),
423
+ ],
424
+ });
425
+ const eventManager = new EventManager([server]);
426
+ const model = server.models[0];
427
+
428
+ await (eventManager as any).autoLoadIfNeeded(model);
429
+
430
+ expect(model.load).not.toHaveBeenCalled();
256
431
  });
257
432
  });
package/tests/mocks.ts CHANGED
@@ -17,7 +17,7 @@ export const createMockServer = (
17
17
  const server: Partial<Server> = {
18
18
  baseUrl: "http://127.0.0.1:8080",
19
19
  models,
20
- getApiKey: () => Promise.resolve(overrides.apiKey ?? ""),
20
+ getApiKey: () => overrides.apiKey ?? "",
21
21
  fetchModels: () => mockRpc("/v1/models"),
22
22
  fetchModelProps: (modelId: string) =>
23
23
  mockRpc(`/props?model=${modelId}&autoload=false`),
@@ -1,4 +1,5 @@
1
1
  import { beforeEach, describe, expect, it } from "vitest";
2
+ import { POLLING_TIMEOUT, SERVER_TIMEOUT } from "../src/constants";
2
3
  import { ServerStatus } from "../src/enums/serverStatus";
3
4
  import { Server } from "../src/server";
4
5
  import { createMockServer, mockRpc } from "./mocks";
@@ -146,6 +147,51 @@ describe("Server postRequest", () => {
146
147
  });
147
148
  });
148
149
 
150
+ describe("Server timeouts", () => {
151
+ it("should use default timeouts when not provided", () => {
152
+ const server = new Server("http://127.0.0.1:8080");
153
+
154
+ expect(server.serverTimeout).toBe(SERVER_TIMEOUT);
155
+ expect(server.pollingTimeout).toBe(POLLING_TIMEOUT);
156
+ });
157
+
158
+ it("should accept custom serverTimeout", () => {
159
+ const server = new Server(
160
+ "http://127.0.0.1:8080",
161
+ undefined,
162
+ undefined,
163
+ 3000,
164
+ );
165
+
166
+ expect(server.serverTimeout).toBe(3000);
167
+ });
168
+
169
+ it("should accept custom pollingTimeout", () => {
170
+ const server = new Server(
171
+ "http://127.0.0.1:8080",
172
+ undefined,
173
+ undefined,
174
+ 1000,
175
+ 120000,
176
+ );
177
+
178
+ expect(server.pollingTimeout).toBe(120000);
179
+ });
180
+
181
+ it("should accept both custom timeouts", () => {
182
+ const server = new Server(
183
+ "http://127.0.0.1:8080",
184
+ "my-id",
185
+ "My Server",
186
+ 2000,
187
+ 90000,
188
+ );
189
+
190
+ expect(server.serverTimeout).toBe(2000);
191
+ expect(server.pollingTimeout).toBe(90000);
192
+ });
193
+ });
194
+
149
195
  describe("Server isReady", () => {
150
196
  it("should return READY when health status is ok", async () => {
151
197
  mockRpc.mockResolvedValueOnce({ status: "ok" });