pi-llama-cpp 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/tests/mocks.ts ADDED
@@ -0,0 +1,97 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { vi } from "vitest";
3
+ import { Mode } from "../src/enums/mode";
4
+ import { Status } from "../src/enums/status";
5
+ import { BaseModel } from "../src/models/baseModel";
6
+ import { Server } from "../src/server";
7
+
8
+ /** Shared mock RPC — each test configures it */
9
+ export const mockRpc = vi.fn();
10
+
11
+ /** Default mock server that assumes everything works */
12
+ export const createMockServer = (
13
+ overrides: Partial<Server & { apiKey?: string }> = {},
14
+ ): Server => {
15
+ const models: BaseModel[] = [];
16
+ const server: Partial<Server> = {
17
+ baseUrl: "http://127.0.0.1:8080",
18
+ models,
19
+ getApiKey: () => Promise.resolve(overrides.apiKey ?? ""),
20
+ fetchModels: () => mockRpc("/v1/models"),
21
+ fetchModelProps: (modelId: string) =>
22
+ mockRpc(`/props?model=${modelId}&autoload=false`),
23
+ fetchServerHealth: () => mockRpc("/health"),
24
+ fetchServerProps: () => mockRpc("/props?autoload=false"),
25
+ postRequest: (resource: "load" | "unload", model: string) =>
26
+ mockRpc(`/models/${resource}`, { model }),
27
+ isReady: async () => {
28
+ try {
29
+ const r = await mockRpc("/health");
30
+ return r.status === "ok";
31
+ } catch {
32
+ return false;
33
+ }
34
+ },
35
+ initialize: async () => {
36
+ const { data } = (await mockRpc("/v1/models")) as {
37
+ data: BaseModel[];
38
+ };
39
+ models.length = 0;
40
+ models.push(...(data ?? []));
41
+ },
42
+ ...overrides,
43
+ };
44
+ return server as Server;
45
+ };
46
+
47
+ /** Helper to create a mock BaseModel */
48
+ export const createMockModel = (
49
+ name: string,
50
+ overrides: Partial<BaseModel> = {},
51
+ ): BaseModel =>
52
+ ({
53
+ name,
54
+ id: name,
55
+ mode: Mode.ROUTER,
56
+ serverUrl: "http://127.0.0.1:8080",
57
+ capabilities: ["text"] as ["text"],
58
+ getStatus: vi.fn().mockResolvedValue(Status.LOADED),
59
+ getContextSize: vi.fn().mockResolvedValue(4096),
60
+ getInfo: vi.fn().mockResolvedValue(`Model: ${name}\nID: ${name}`),
61
+ load: vi.fn().mockResolvedValue(undefined),
62
+ unload: vi.fn().mockResolvedValue(undefined),
63
+ toProviderConfig: vi.fn().mockResolvedValue({}),
64
+ getLabel: vi.fn().mockResolvedValue(name),
65
+ ...overrides,
66
+ }) as unknown as BaseModel;
67
+
68
+ /** Create a mock extension context */
69
+ export const createMockCtx = (
70
+ selectFn: (prompt: string, options: string[]) => string | null,
71
+ ) => ({
72
+ cwd: "/tmp/test",
73
+ ui: {
74
+ select: vi.fn(selectFn),
75
+ notify: vi.fn(),
76
+ theme: {
77
+ fg: (color: string, text: string) => text,
78
+ },
79
+ },
80
+ modelRegistry: {
81
+ find: vi.fn().mockReturnValue({ id: "test-model-id" }),
82
+ },
83
+ });
84
+
85
+ /** Create a mock Pi instance */
86
+ export const createMockPi = () => ({
87
+ setModel: vi.fn(),
88
+ registerProvider: vi.fn(),
89
+ });
90
+
91
+ /** Create a mock Pi context for EventManager */
92
+ export const createMockPiContext = (notifyFn: ReturnType<typeof vi.fn>) =>
93
+ ({
94
+ ui: {
95
+ notify: notifyFn,
96
+ },
97
+ }) as any as ExtensionContext;
@@ -2,156 +2,215 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import {
3
3
  API_KEY_PLACEHOLDER,
4
4
  DEFAULT_LLAMA_SERVER_URL,
5
- PROVIDER_ID,
6
5
  } from "../src/constants";
7
6
 
7
+ // Mock getAgentDir before importing resolver
8
+ vi.mock("@earendil-works/pi-coding-agent", () => ({
9
+ getAgentDir: vi.fn().mockReturnValue("/fake/agent/dir"),
10
+ }));
11
+
12
+ vi.mock("node:fs/promises", () => ({
13
+ access: vi.fn(),
14
+ constants: { F_OK: 0 },
15
+ readFile: vi.fn(),
16
+ }));
17
+
18
+ // Import mocked modules
19
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
20
+ import { access, readFile } from "node:fs/promises";
21
+ import { ConfigResolver } from "../src/resolver";
22
+
8
23
  describe("URL resolution fallback chain", () => {
9
- beforeEach(() => {
10
- vi.clearAllMocks();
11
- vi.resetModules();
12
- });
24
+ const mockAccess = vi.mocked(access);
25
+ const mockReadFile = vi.mocked(readFile);
26
+ const mockGetAgentDir = vi.mocked(getAgentDir);
13
27
 
14
28
  afterEach(() => {
15
29
  delete process.env.LLAMA_SERVER_URL;
30
+ vi.resetModules();
16
31
  });
17
32
 
18
- it("should return default URL when no config is found", async () => {
19
- vi.doMock("node:fs/promises", () => ({
20
- access: vi.fn().mockRejectedValue(new Error("ENOENT")),
21
- constants: { F_OK: 0 },
22
- readFile: vi.fn().mockResolvedValue(""),
23
- }));
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ mockGetAgentDir.mockReturnValue("/fake/agent/dir");
36
+ // Default: no files exist
37
+ mockAccess.mockRejectedValue(new Error("ENOENT"));
38
+ mockReadFile.mockResolvedValue("");
39
+ });
24
40
 
25
- const { resolveUrl } = await import("../src/tools/resolver");
26
- const result = await resolveUrl("/tmp/test-project");
41
+ it("should return default URL when no config is found", async () => {
42
+ const resolver = new ConfigResolver();
43
+ const result = await resolver.resolveUrls("/tmp/test-project");
27
44
 
28
- expect(result).toBe(DEFAULT_LLAMA_SERVER_URL);
45
+ expect(result).toEqual([DEFAULT_LLAMA_SERVER_URL]);
29
46
  });
30
47
 
31
48
  it("should prioritize project config over env variable", async () => {
32
- vi.doMock("node:fs/promises", () => ({
33
- access: vi.fn().mockImplementation(async (path: string) => {
34
- if (path.includes("llama-server.json")) return undefined;
35
- throw new Error("ENOENT");
36
- }),
37
- constants: { F_OK: 0 },
38
- readFile: vi
39
- .fn()
40
- .mockResolvedValue(JSON.stringify({ url: "http://localhost:9999" })),
41
- }));
49
+ mockAccess.mockImplementation(async (_path: unknown) => {
50
+ if (typeof _path === "string" && _path.includes("llama-server.json"))
51
+ return undefined;
52
+ throw new Error("ENOENT");
53
+ });
54
+ mockReadFile.mockResolvedValue(
55
+ JSON.stringify({ url: "http://localhost:9999" }),
56
+ );
42
57
 
43
58
  process.env.LLAMA_SERVER_URL = "http://env-url:8080";
44
59
 
45
- const { resolveUrl } = await import("../src/tools/resolver");
46
- const result = await resolveUrl("/tmp/test-project");
60
+ const resolver = new ConfigResolver();
61
+ const result = await resolver.resolveUrls("/tmp/test-project");
47
62
 
48
- expect(result).toBe("http://localhost:9999");
63
+ expect(result).toEqual(["http://localhost:9999"]);
49
64
  });
50
65
 
51
66
  it("should use env variable when no project config exists", async () => {
52
- vi.doMock("node:fs/promises", () => ({
53
- access: vi.fn().mockRejectedValue(new Error("ENOENT")),
54
- constants: { F_OK: 0 },
55
- readFile: vi.fn().mockResolvedValue(""),
56
- }));
57
-
58
67
  process.env.LLAMA_SERVER_URL = "http://env-url:8080";
59
68
 
60
- const { resolveUrl } = await import("../src/tools/resolver");
61
- const result = await resolveUrl("/tmp/test-project");
69
+ const resolver = new ConfigResolver();
70
+ const result = await resolver.resolveUrls("/tmp/test-project");
62
71
 
63
- expect(result).toBe("http://env-url:8080");
72
+ expect(result).toEqual(["http://env-url:8080"]);
64
73
  });
65
74
 
66
75
  it("should use global settings when no project config or env exists", async () => {
67
- vi.doMock("node:fs/promises", () => ({
68
- access: vi.fn().mockImplementation(async (path: string) => {
69
- if (path.includes("settings.json")) return undefined;
70
- throw new Error("ENOENT");
71
- }),
72
- constants: { F_OK: 0 },
73
- readFile: vi
74
- .fn()
75
- .mockResolvedValue(
76
- JSON.stringify({ llamaServerUrl: "http://global:8080" }),
77
- ),
78
- }));
79
-
80
- const { resolveUrl } = await import("../src/tools/resolver");
81
- const result = await resolveUrl("/tmp/test-project");
82
-
83
- expect(result).toBe("http://global:8080");
76
+ mockAccess.mockImplementation(async (_path: unknown) => {
77
+ if (typeof _path === "string" && _path.includes("settings.json"))
78
+ return undefined;
79
+ throw new Error("ENOENT");
80
+ });
81
+ mockReadFile.mockResolvedValue(
82
+ JSON.stringify({ llamaServerUrl: "http://global:8080" }),
83
+ );
84
+
85
+ const resolver = new ConfigResolver();
86
+ const result = await resolver.resolveUrls("/tmp/test-project");
87
+
88
+ expect(result).toEqual(["http://global:8080"]);
84
89
  });
85
90
 
86
91
  it("should strip trailing slashes from resolved URL", async () => {
87
- vi.doMock("node:fs/promises", () => ({
88
- access: vi.fn().mockImplementation(async (path: string) => {
89
- if (path.includes("llama-server.json")) return undefined;
90
- throw new Error("ENOENT");
91
- }),
92
- constants: { F_OK: 0 },
93
- readFile: vi
94
- .fn()
95
- .mockResolvedValue(JSON.stringify({ url: "http://localhost:8080/" })),
96
- }));
92
+ mockAccess.mockImplementation(async (_path: unknown) => {
93
+ if (typeof _path === "string" && _path.includes("llama-server.json"))
94
+ return undefined;
95
+ throw new Error("ENOENT");
96
+ });
97
+ mockReadFile.mockResolvedValue(
98
+ JSON.stringify({ url: "http://localhost:8080/" }),
99
+ );
100
+
101
+ const resolver = new ConfigResolver();
102
+ const result = await resolver.resolveUrls("/tmp/test-project");
103
+
104
+ expect(result).toEqual(["http://localhost:8080"]);
105
+ });
97
106
 
98
- const { resolveUrl } = await import("../src/tools/resolver");
99
- const result = await resolveUrl("/tmp/test-project");
107
+ it("should cache the resolved URL on subsequent calls", async () => {
108
+ mockAccess.mockImplementation(async (_path: unknown) => {
109
+ if (typeof _path === "string" && _path.includes("llama-server.json"))
110
+ return undefined;
111
+ throw new Error("ENOENT");
112
+ });
113
+ mockReadFile.mockResolvedValue(
114
+ JSON.stringify({ url: "http://first:8080" }),
115
+ );
116
+
117
+ const resolver = new ConfigResolver();
118
+ const result1 = await resolver.resolveUrls("/tmp/project1");
119
+ const result2 = await resolver.resolveUrls("/tmp/project2");
120
+
121
+ expect(result1).toEqual(["http://first:8080"]);
122
+ expect(result2).toEqual(["http://first:8080"]);
123
+ });
124
+
125
+ it("should handle multiple URLs separated by semicolons", async () => {
126
+ mockAccess.mockImplementation(async (_path: unknown) => {
127
+ if (typeof _path === "string" && _path.includes("llama-server.json"))
128
+ return undefined;
129
+ throw new Error("ENOENT");
130
+ });
131
+ mockReadFile.mockResolvedValue(
132
+ JSON.stringify({ url: "http://first:8080;http://second:9090/" }),
133
+ );
134
+
135
+ const resolver = new ConfigResolver();
136
+ const result = await resolver.resolveUrls("/tmp/test-project");
100
137
 
101
- expect(result).toBe("http://localhost:8080");
138
+ expect(result).toEqual(["http://first:8080", "http://second:9090"]);
102
139
  });
103
140
  });
104
141
 
105
142
  describe("API key resolution", () => {
143
+ const mockAccess = vi.mocked(access);
144
+ const mockReadFile = vi.mocked(readFile);
145
+ const mockGetAgentDir = vi.mocked(getAgentDir);
146
+
147
+ afterEach(() => {
148
+ vi.resetModules();
149
+ });
150
+
106
151
  beforeEach(() => {
107
152
  vi.clearAllMocks();
108
- vi.resetModules();
153
+ mockGetAgentDir.mockReturnValue("/fake/agent/dir");
154
+ mockAccess.mockRejectedValue(new Error("ENOENT"));
155
+ mockReadFile.mockResolvedValue("");
109
156
  });
110
157
 
111
158
  it("should return placeholder when auth file does not exist", async () => {
112
- vi.doMock("node:fs/promises", () => ({
113
- access: vi.fn().mockRejectedValue(new Error("ENOENT")),
114
- constants: { F_OK: 0 },
115
- readFile: vi.fn().mockResolvedValue(""),
116
- }));
159
+ const resolver = new ConfigResolver();
160
+ const result = await resolver.resolveApiKey(
161
+ "llama-server=http://127.0.0.1:8080",
162
+ );
117
163
 
118
- const { resolveApiKey } = await import("../src/tools/resolver");
119
- const result = await resolveApiKey();
120
-
121
- expect(result).toBe(API_KEY_PLACEHOLDER);
164
+ expect(result).toEqual(API_KEY_PLACEHOLDER);
122
165
  });
123
166
 
124
167
  it("should return placeholder when provider key is missing", async () => {
125
- vi.doMock("node:fs/promises", () => ({
126
- access: vi.fn().mockResolvedValue(undefined),
127
- constants: { F_OK: 0 },
128
- readFile: vi
129
- .fn()
130
- .mockResolvedValue(
131
- JSON.stringify({ "other-provider": { key: "other-key" } }),
132
- ),
133
- }));
134
-
135
- const { resolveApiKey } = await import("../src/tools/resolver");
136
- const result = await resolveApiKey();
137
-
138
- expect(result).toBe(API_KEY_PLACEHOLDER);
168
+ mockAccess.mockResolvedValue(undefined);
169
+ mockReadFile.mockResolvedValue(
170
+ JSON.stringify({ "other-provider": { key: "other-key" } }),
171
+ );
172
+
173
+ const resolver = new ConfigResolver();
174
+ const result = await resolver.resolveApiKey(
175
+ "llama-server=http://127.0.0.1:8080",
176
+ );
177
+
178
+ expect(result).toEqual(API_KEY_PLACEHOLDER);
139
179
  });
140
180
 
141
181
  it("should return the provider key when present", async () => {
142
- vi.doMock("node:fs/promises", () => ({
143
- access: vi.fn().mockResolvedValue(undefined),
144
- constants: { F_OK: 0 },
145
- readFile: vi
146
- .fn()
147
- .mockResolvedValue(
148
- JSON.stringify({ [PROVIDER_ID]: { key: "test-api-key" } }),
149
- ),
150
- }));
151
-
152
- const { resolveApiKey } = await import("../src/tools/resolver");
153
- const result = await resolveApiKey();
154
-
155
- expect(result).toBe("test-api-key");
182
+ mockAccess.mockResolvedValue(undefined);
183
+ mockReadFile.mockResolvedValue(
184
+ JSON.stringify({
185
+ "llama-server=http://127.0.0.1:8080": { key: "test-api-key" },
186
+ }),
187
+ );
188
+
189
+ const resolver = new ConfigResolver();
190
+ const result = await resolver.resolveApiKey(
191
+ "llama-server=http://127.0.0.1:8080",
192
+ );
193
+
194
+ expect(result).toEqual("test-api-key");
195
+ });
196
+
197
+ it("should cache the auth file and reuse the key", async () => {
198
+ mockAccess.mockResolvedValue(undefined);
199
+ mockReadFile.mockResolvedValue(
200
+ JSON.stringify({
201
+ "llama-server=http://127.0.0.1:8080": { key: "cached-key" },
202
+ }),
203
+ );
204
+
205
+ const resolver = new ConfigResolver();
206
+ const result1 = await resolver.resolveApiKey(
207
+ "llama-server=http://127.0.0.1:8080",
208
+ );
209
+ const result2 = await resolver.resolveApiKey(
210
+ "llama-server=http://127.0.0.1:8080",
211
+ );
212
+
213
+ expect(result1).toBe("cached-key");
214
+ expect(result2).toBe("cached-key");
156
215
  });
157
216
  });
@@ -1,16 +1,8 @@
1
- import { describe, expect, it, vi } from "vitest";
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
2
  import { Mode } from "../src/enums/mode";
3
3
  import { DataProperty } from "../src/interfaces/endpoints/models";
4
4
  import { RouterModel } from "../src/models/routerModel";
5
-
6
- // Mock the retriever module before importing anything that depends on it
7
- const mockRpc = vi.fn();
8
-
9
- vi.mock("../src/tools/retriever", () => ({
10
- rpc: (...args: unknown[]) => mockRpc(...args),
11
- isServerReady: vi.fn(),
12
- listModels: vi.fn(),
13
- }));
5
+ import { createMockServer, mockRpc } from "./mocks";
14
6
 
15
7
  // Helper to create a mock DataProperty
16
8
  const createModel = (overrides: Partial<DataProperty> = {}): DataProperty => ({
@@ -24,6 +16,10 @@ const createModel = (overrides: Partial<DataProperty> = {}): DataProperty => ({
24
16
  ...overrides,
25
17
  });
26
18
 
19
+ beforeEach(() => {
20
+ mockRpc.mockClear();
21
+ });
22
+
27
23
  describe("RouterModel context size extraction", () => {
28
24
  it("should extract --ctx-size value", () => {
29
25
  const model = new RouterModel(
@@ -41,6 +37,7 @@ describe("RouterModel context size extraction", () => {
41
37
  preset: "default",
42
38
  },
43
39
  }),
40
+ createMockServer(),
44
41
  );
45
42
 
46
43
  // Access the private method via any
@@ -57,6 +54,7 @@ describe("RouterModel context size extraction", () => {
57
54
  preset: "default",
58
55
  },
59
56
  }),
57
+ createMockServer(),
60
58
  );
61
59
 
62
60
  const extractFrom = (model as any).extractFrom.bind(model);
@@ -72,6 +70,7 @@ describe("RouterModel context size extraction", () => {
72
70
  preset: "default",
73
71
  },
74
72
  }),
73
+ createMockServer(),
75
74
  );
76
75
 
77
76
  const extractFrom = (model as any).extractFrom.bind(model);
@@ -88,6 +87,7 @@ describe("RouterModel context size extraction", () => {
88
87
  preset: "default",
89
88
  },
90
89
  }),
90
+ createMockServer(),
91
91
  );
92
92
 
93
93
  const extractFrom = (model as any).extractFrom.bind(model);
@@ -103,6 +103,7 @@ describe("RouterModel context size extraction", () => {
103
103
  preset: "default",
104
104
  },
105
105
  }),
106
+ createMockServer(),
106
107
  );
107
108
 
108
109
  const extractFrom = (model as any).extractFrom.bind(model);
@@ -110,27 +111,9 @@ describe("RouterModel context size extraction", () => {
110
111
  });
111
112
 
112
113
  it("should prefer --ctx-size over --fit-ctx when loaded", async () => {
113
- // First call: getStatus() -> /models
114
- mockRpc.mockResolvedValueOnce({
115
- data: [
116
- {
117
- id: "test-model",
118
- status: {
119
- value: "loaded",
120
- args: [
121
- "--model",
122
- "gguf",
123
- "--ctx-size",
124
- "4096",
125
- "--fit-ctx",
126
- "8192",
127
- ],
128
- preset: "default",
129
- },
130
- },
131
- ],
132
- });
133
- // Second call: super.getContextSize() -> /models with meta.n_ctx
114
+ // First call: getStatus() -> fetchModelProps
115
+ mockRpc.mockResolvedValueOnce({ is_sleeping: false });
116
+ // Second call: super.getContextSize() -> fetchModels with meta.n_ctx
134
117
  mockRpc.mockResolvedValueOnce({
135
118
  data: [
136
119
  {
@@ -148,6 +131,7 @@ describe("RouterModel context size extraction", () => {
148
131
  preset: "default",
149
132
  },
150
133
  }),
134
+ createMockServer(),
151
135
  );
152
136
 
153
137
  const ctxSize = await model.getContextSize();
@@ -155,20 +139,9 @@ describe("RouterModel context size extraction", () => {
155
139
  });
156
140
 
157
141
  it("should return n_ctx from meta when loaded without context size args", async () => {
158
- // First call: getStatus() -> /models
159
- mockRpc.mockResolvedValueOnce({
160
- data: [
161
- {
162
- id: "test-model",
163
- status: {
164
- value: "loaded",
165
- args: ["--model", "gguf"],
166
- preset: "default",
167
- },
168
- },
169
- ],
170
- });
171
- // Second call: super.getContextSize() -> /models with meta.n_ctx
142
+ // First call: getStatus() -> fetchModelProps
143
+ mockRpc.mockResolvedValueOnce({ is_sleeping: false });
144
+ // Second call: super.getContextSize() -> fetchModels with meta.n_ctx
172
145
  mockRpc.mockResolvedValueOnce({
173
146
  data: [
174
147
  {
@@ -186,6 +159,7 @@ describe("RouterModel context size extraction", () => {
186
159
  preset: "default",
187
160
  },
188
161
  }),
162
+ createMockServer(),
189
163
  );
190
164
 
191
165
  const ctxSize = await model.getContextSize();
@@ -194,33 +168,34 @@ describe("RouterModel context size extraction", () => {
194
168
  });
195
169
 
196
170
  describe("RouterModel capabilities detection", () => {
197
- it("should detect image capability from architecture.input_modalities", async () => {
198
- mockRpc.mockResolvedValueOnce({
199
- data: [
200
- {
201
- id: "test-model",
202
- status: {
203
- value: "loaded",
204
- args: [],
205
- preset: "default",
206
- failed: false,
207
- },
208
- architecture: {
209
- input_modalities: ["text", "image"],
210
- output_modalities: ["text"],
211
- },
212
- },
213
- ],
214
- });
171
+ it("should detect image capability when modalities.vision is true", async () => {
172
+ mockRpc.mockResolvedValueOnce({ modalities: { vision: true } });
215
173
 
216
- const model = new RouterModel(createModel());
174
+ const model = new RouterModel(createModel(), createMockServer());
217
175
  const capabilities = await model.getCapabilities();
218
176
 
219
177
  expect(capabilities).toEqual(["text", "image"]);
220
- expect(mockRpc).toHaveBeenCalledWith("/models");
178
+ expect(mockRpc).toHaveBeenCalledWith(
179
+ "/props?model=test-model&autoload=false",
180
+ );
181
+ });
182
+
183
+ it("should return text-only when fetchModelProps fails", async () => {
184
+ // First call (fetchModelProps) throws to trigger fallback
185
+ mockRpc.mockRejectedValueOnce(new Error("props not available"));
186
+ // Second call (fetchModels) returns empty data so model is not found
187
+ mockRpc.mockResolvedValueOnce({ data: [] });
188
+
189
+ const model = new RouterModel(createModel(), createMockServer());
190
+ const capabilities = await model.getCapabilities();
191
+
192
+ expect(capabilities).toEqual(["text"]);
221
193
  });
222
194
 
223
195
  it("should detect text-only capability when only text in input_modalities", async () => {
196
+ // First call (fetchModelProps) throws to trigger fallback
197
+ mockRpc.mockRejectedValueOnce(new Error("props not available"));
198
+ // Second call (fetchModels) returns the data
224
199
  mockRpc.mockResolvedValueOnce({
225
200
  data: [
226
201
  {
@@ -239,13 +214,16 @@ describe("RouterModel capabilities detection", () => {
239
214
  ],
240
215
  });
241
216
 
242
- const model = new RouterModel(createModel());
217
+ const model = new RouterModel(createModel(), createMockServer());
243
218
  const capabilities = await model.getCapabilities();
244
219
 
245
220
  expect(capabilities).toEqual(["text"]);
246
221
  });
247
222
 
248
223
  it("should return text when model not found in /models response", async () => {
224
+ // First call (fetchModelProps) throws to trigger fallback
225
+ mockRpc.mockRejectedValueOnce(new Error("props not available"));
226
+ // Second call (fetchModels) returns data without matching model
249
227
  mockRpc.mockResolvedValueOnce({
250
228
  data: [
251
229
  {
@@ -260,7 +238,7 @@ describe("RouterModel capabilities detection", () => {
260
238
  ],
261
239
  });
262
240
 
263
- const model = new RouterModel(createModel());
241
+ const model = new RouterModel(createModel(), createMockServer());
264
242
  const capabilities = await model.getCapabilities();
265
243
 
266
244
  expect(capabilities).toEqual(["text"]);
@@ -269,7 +247,7 @@ describe("RouterModel capabilities detection", () => {
269
247
 
270
248
  describe("RouterModel mode", () => {
271
249
  it("should always return ROUTER mode", () => {
272
- const model = new RouterModel(createModel());
250
+ const model = new RouterModel(createModel(), createMockServer());
273
251
  expect(model.mode).toBe(Mode.ROUTER);
274
252
  });
275
253
  });