pi-llama-cpp 0.15.0 → 0.16.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.
@@ -48,309 +48,111 @@ vi.mock("node:fs/promises", () => ({
48
48
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
49
49
  import { access } from "node:fs/promises";
50
50
 
51
- describe("URL resolution fallback chain", () => {
52
- const mockGetAgentDir = vi.mocked(getAgentDir);
53
- const mockGetProjectSettings = vi.mocked(
54
- mockSettingsManager.getProjectSettings,
55
- );
56
- const mockGetGlobalSettings = vi.mocked(
57
- mockSettingsManager.getGlobalSettings,
58
- );
59
-
60
- afterEach(() => {
61
- delete process.env.LLAMA_SERVER_URL;
62
- });
63
-
64
- beforeEach(() => {
65
- vi.clearAllMocks();
66
- mockGetAgentDir.mockReturnValue("/fake/agent/dir");
67
- // Default: no settings found
68
- mockGetProjectSettings.mockReturnValue({});
69
- mockGetGlobalSettings.mockReturnValue({});
70
- manager = createSettingsManager();
71
- });
72
-
73
- it("should return default URL when no config is found", async () => {
74
- // Ensure env var is not set (and not inherited from environment)
75
- delete process.env.LLAMA_SERVER_URL;
76
-
77
- const result = await manager.resolveUrls();
78
-
79
- expect(result).toEqual([LLAMA_SERVER_URL]);
80
- });
81
-
82
- it("should prioritize env variable over project config", async () => {
83
- mockGetProjectSettings.mockReturnValue({
84
- llamaServerUrl: "http://localhost:9999",
85
- });
86
- process.env.LLAMA_SERVER_URL = "http://env-url:8080";
87
-
88
- const result = await manager.resolveUrls();
89
-
90
- expect(result).toEqual(["http://env-url:8080"]);
91
- });
92
-
93
- it("should use env variable when no other config exists", async () => {
94
- process.env.LLAMA_SERVER_URL = "http://env-url:8080";
95
-
96
- const result = await manager.resolveUrls();
97
-
98
- expect(result).toEqual(["http://env-url:8080"]);
99
- });
100
-
101
- it("should use legacy llamaServerUrl when env is not set", async () => {
102
- mockGetProjectSettings.mockReturnValue({
103
- llamaServerUrl: "http://project:9999",
104
- });
105
-
106
- const result = await manager.resolveUrls();
107
-
108
- expect(result).toEqual(["http://project:9999"]);
109
- });
110
-
111
- it("should use global settings when no project config or env exists", async () => {
112
- mockGetGlobalSettings.mockReturnValue({
113
- llamaServerUrl: "http://global:8080",
114
- });
115
-
116
- const result = await manager.resolveUrls();
117
-
118
- expect(result).toEqual(["http://global:8080"]);
119
- });
120
-
121
- it("should strip trailing slashes from resolved URL", async () => {
122
- process.env.LLAMA_SERVER_URL = "http://localhost:8080/";
123
-
124
- const result = await manager.resolveUrls();
125
-
126
- expect(result).toEqual(["http://localhost:8080"]);
127
- });
128
-
129
- it("should cache the resolved URL on subsequent calls", async () => {
130
- process.env.LLAMA_SERVER_URL = "http://first:8080";
131
-
132
- const result1 = await manager.resolveUrls();
133
- const result2 = await manager.resolveUrls();
134
-
135
- expect(result1).toEqual(["http://first:8080"]);
136
- expect(result2).toEqual(["http://first:8080"]);
137
- });
138
-
139
- it("should handle multiple URLs separated by semicolons", async () => {
140
- process.env.LLAMA_SERVER_URL = "http://first:8080;http://second:9090/";
141
-
142
- const result = await manager.resolveUrls();
143
-
144
- expect(result).toEqual(["http://first:8080", "http://second:9090"]);
145
- });
146
-
147
- it("should drop env URLs without an http(s) scheme, warn, and fall through", async () => {
148
- process.env.LLAMA_SERVER_URL = "127.0.0.1:8080";
149
-
150
- const manager = createSettingsManager();
151
- const result = await manager.resolveUrls();
152
-
153
- expect(result).toEqual([LLAMA_SERVER_URL]);
154
- expect(manager.takeWarnings()).toEqual([
155
- "Ignoring invalid server URL '127.0.0.1:8080' (needs http(s)://)",
156
- ]);
157
- expect(manager.takeWarnings()).toEqual([]); // drained
158
- });
159
-
160
- it("should drop server entries without an http(s) scheme and warn", async () => {
161
- mockGetProjectSettings.mockReturnValue({
162
- llamaSettings: {
163
- servers: [{ url: "127.0.0.1:8080" }, { url: "http://good:8080/" }],
164
- },
165
- });
166
-
167
- const manager = createSettingsManager();
168
- const result = await manager.resolveUrls();
169
-
170
- expect(result).toEqual(["http://good:8080"]);
171
- expect(manager.takeWarnings()).toEqual([
172
- "Ignoring invalid server URL '127.0.0.1:8080' (needs http(s)://)",
173
- ]);
174
- expect(manager.takeWarnings()).toEqual([]); // drained
175
- });
176
- });
177
-
178
- describe("llamaSettings.servers resolution", () => {
51
+ describe("API key resolution", () => {
179
52
  const mockGetAgentDir = vi.mocked(getAgentDir);
180
- const mockGetProjectSettings = vi.mocked(
181
- mockSettingsManager.getProjectSettings,
182
- );
183
- const mockGetGlobalSettings = vi.mocked(
184
- mockSettingsManager.getGlobalSettings,
185
- );
186
53
 
187
- afterEach(() => {
188
- delete process.env.LLAMA_SERVER_URL;
189
- });
54
+ afterEach(() => {});
190
55
 
191
56
  beforeEach(() => {
192
57
  vi.clearAllMocks();
193
58
  mockGetAgentDir.mockReturnValue("/fake/agent/dir");
194
- mockGetProjectSettings.mockReturnValue({});
195
- mockGetGlobalSettings.mockReturnValue({});
59
+ mockReadStoredCredential.mockReturnValue(undefined);
196
60
  manager = createSettingsManager();
197
61
  });
198
62
 
199
- it("should resolve URLs from llamaSettings.servers in project config", async () => {
200
- mockGetProjectSettings.mockReturnValue({
201
- llamaSettings: {
202
- servers: [
203
- { url: "http://project-server:8080" },
204
- { url: "http://project-server2:9090" },
205
- ],
206
- },
207
- });
208
-
209
- const result = await manager.resolveUrls();
210
-
211
- expect(result).toEqual([
212
- "http://project-server:8080",
213
- "http://project-server2:9090",
214
- ]);
215
- });
216
-
217
- it("should prioritize project servers over global servers", async () => {
218
- mockGetProjectSettings.mockReturnValue({
219
- llamaSettings: {
220
- servers: [{ url: "http://project:8080" }],
221
- },
222
- });
223
- mockGetGlobalSettings.mockReturnValue({
224
- llamaSettings: {
225
- servers: [{ url: "http://global:9090" }],
226
- },
227
- });
63
+ it("should return placeholder when credential is not found", () => {
64
+ mockReadStoredCredential.mockReturnValue(undefined);
228
65
 
229
- const result = await manager.resolveUrls();
66
+ const result = manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
230
67
 
231
- expect(result).toEqual(["http://project:8080"]);
68
+ expect(result).toEqual(API_KEY_PLACEHOLDER);
232
69
  });
233
70
 
234
- it("should use global servers when no project servers exist", async () => {
235
- mockGetGlobalSettings.mockReturnValue({
236
- llamaSettings: {
237
- servers: [{ url: "http://global:8080" }],
238
- },
239
- });
71
+ it("should return placeholder when apiKey is missing from credential", () => {
72
+ mockReadStoredCredential.mockReturnValue({});
240
73
 
241
- const result = await manager.resolveUrls();
74
+ const result = manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
242
75
 
243
- expect(result).toEqual(["http://global:8080"]);
76
+ expect(result).toEqual(API_KEY_PLACEHOLDER);
244
77
  });
245
78
 
246
- it("should prioritize env variable over servers", async () => {
247
- process.env.LLAMA_SERVER_URL = "http://env:8080";
248
- mockGetProjectSettings.mockReturnValue({
249
- llamaSettings: {
250
- servers: [{ url: "http://server:9090" }],
251
- },
252
- });
79
+ it("should return the key when present in credential", () => {
80
+ mockReadStoredCredential.mockReturnValue({ key: "test-api-key" });
253
81
 
254
- const result = await manager.resolveUrls();
82
+ const result = manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
255
83
 
256
- expect(result).toEqual(["http://env:8080"]);
84
+ expect(result).toEqual("test-api-key");
257
85
  });
258
86
 
259
- it("should use servers when env is not set", async () => {
260
- mockGetProjectSettings.mockReturnValue({
261
- llamaSettings: {
262
- servers: [{ url: "http://server:9090" }],
263
- },
264
- });
87
+ it("should call readStoredCredential with the provider ID", () => {
88
+ mockReadStoredCredential.mockReturnValue({ key: "test-key" });
265
89
 
266
- const result = await manager.resolveUrls();
90
+ manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
267
91
 
268
- expect(result).toEqual(["http://server:9090"]);
92
+ expect(mockReadStoredCredential).toHaveBeenCalledWith(
93
+ "llama-server=http://127.0.0.1:8080",
94
+ );
269
95
  });
270
96
 
271
- it("should prioritize servers over legacy llamaServerUrl", async () => {
272
- mockGetProjectSettings.mockReturnValue({
273
- llamaServerUrl: "http://legacy:8080",
274
- llamaSettings: {
275
- servers: [{ url: "http://server:9090" }],
276
- },
277
- });
97
+ it("should resolve $VAR env reference from process.env", () => {
98
+ mockReadStoredCredential.mockReturnValue({ key: "$MY_API_KEY" });
99
+ process.env.MY_API_KEY = "env-resolved-key";
278
100
 
279
- const result = await manager.resolveUrls();
101
+ const result = manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
280
102
 
281
- expect(result).toEqual(["http://server:9090"]);
103
+ expect(result).toBe("env-resolved-key");
104
+ delete process.env.MY_API_KEY;
282
105
  });
283
106
 
284
- it("should use legacy llamaServerUrl when servers is empty", async () => {
285
- mockGetProjectSettings.mockReturnValue({
286
- llamaServerUrl: "http://legacy:8080",
287
- llamaSettings: {
288
- servers: [],
289
- },
290
- });
107
+ it("should resolve ${VAR} braced env reference from process.env", () => {
108
+ mockReadStoredCredential.mockReturnValue({ key: "${MY_API_KEY}" });
109
+ process.env.MY_API_KEY = "braced-resolved-key";
291
110
 
292
- const result = await manager.resolveUrls();
111
+ const result = manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
293
112
 
294
- expect(result).toEqual(["http://legacy:8080"]);
113
+ expect(result).toBe("braced-resolved-key");
114
+ delete process.env.MY_API_KEY;
295
115
  });
296
116
 
297
- it("should strip trailing slashes from server URLs", async () => {
298
- mockGetProjectSettings.mockReturnValue({
299
- llamaSettings: {
300
- servers: [{ url: "http://localhost:8080/" }],
301
- },
117
+ it("should prefer credential.env over process.env", () => {
118
+ mockReadStoredCredential.mockReturnValue({
119
+ key: "$MY_KEY",
120
+ env: { MY_KEY: "credential-env-value" },
302
121
  });
303
-
304
- const result = await manager.resolveUrls();
305
-
306
- expect(result).toEqual(["http://localhost:8080"]);
307
- });
308
- });
309
-
310
- describe("API key resolution", () => {
311
- const mockGetAgentDir = vi.mocked(getAgentDir);
312
-
313
- afterEach(() => {});
314
-
315
- beforeEach(() => {
316
- vi.clearAllMocks();
317
- mockGetAgentDir.mockReturnValue("/fake/agent/dir");
318
- mockReadStoredCredential.mockReturnValue(undefined);
319
- manager = createSettingsManager();
320
- });
321
-
322
- it("should return placeholder when credential is not found", () => {
323
- mockReadStoredCredential.mockReturnValue(undefined);
122
+ process.env.MY_KEY = "process-env-value";
324
123
 
325
124
  const result = manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
326
125
 
327
- expect(result).toEqual(API_KEY_PLACEHOLDER);
126
+ expect(result).toBe("credential-env-value");
127
+ delete process.env.MY_KEY;
328
128
  });
329
129
 
330
- it("should return placeholder when apiKey is missing from credential", () => {
331
- mockReadStoredCredential.mockReturnValue({});
130
+ it("should return placeholder when env var is not set", () => {
131
+ mockReadStoredCredential.mockReturnValue({ key: "$UNSET_VAR" });
132
+ delete process.env.UNSET_VAR;
332
133
 
333
134
  const result = manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
334
135
 
335
- expect(result).toEqual(API_KEY_PLACEHOLDER);
136
+ expect(result).toBe(API_KEY_PLACEHOLDER);
336
137
  });
337
138
 
338
- it("should return the key when present in credential", () => {
339
- mockReadStoredCredential.mockReturnValue({ key: "test-api-key" });
139
+ // Shell command tests are covered by Pi core's resolve-config-value tests.
140
+ // The local implementation delegates to execSync which is tested there.
141
+
142
+ it("should escape $$ as literal $", () => {
143
+ mockReadStoredCredential.mockReturnValue({ key: "$$LITERAL" });
340
144
 
341
145
  const result = manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
342
146
 
343
- expect(result).toEqual("test-api-key");
147
+ expect(result).toBe("$LITERAL");
344
148
  });
345
149
 
346
- it("should call readStoredCredential with the provider ID", () => {
347
- mockReadStoredCredential.mockReturnValue({ key: "test-key" });
150
+ it("should escape $! as literal !", () => {
151
+ mockReadStoredCredential.mockReturnValue({ key: "$!LITERAL" });
348
152
 
349
- manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
153
+ const result = manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
350
154
 
351
- expect(mockReadStoredCredential).toHaveBeenCalledWith(
352
- "llama-server=http://127.0.0.1:8080",
353
- );
155
+ expect(result).toBe("!LITERAL");
354
156
  });
355
157
  });
356
158
 
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { CredentialResolver } from "../../src/utils/credentialResolver";
3
+
4
+ describe("CredentialResolver", () => {
5
+ const resolver = new CredentialResolver();
6
+
7
+ describe("resolve", () => {
8
+ it("returns placeholder for empty key", () => {
9
+ expect(resolver.resolve("")).toBe("sk-placeholder");
10
+ expect(resolver.resolve(undefined as any)).toBe("sk-placeholder");
11
+ });
12
+
13
+ it("returns literal key as-is", () => {
14
+ expect(resolver.resolve("sk-abc123")).toBe("sk-abc123");
15
+ expect(resolver.resolve("plain-key-without-dollars")).toBe(
16
+ "plain-key-without-dollars",
17
+ );
18
+ });
19
+
20
+ it("resolves escape sequences $$", () => {
21
+ expect(resolver.resolve("$$literal")).toBe("$literal");
22
+ });
23
+
24
+ it("resolves escape sequences $!", () => {
25
+ expect(resolver.resolve("$!bang")).toBe("!bang");
26
+ });
27
+
28
+ it("resolves $VAR from credential.env", () => {
29
+ expect(resolver.resolve("$MY_KEY", { MY_KEY: "from-env" })).toBe(
30
+ "from-env",
31
+ );
32
+ });
33
+
34
+ it("resolves ${VAR} from credential.env", () => {
35
+ expect(resolver.resolve("${MY_KEY}", { MY_KEY: "from-env" })).toBe(
36
+ "from-env",
37
+ );
38
+ });
39
+
40
+ it("resolves $VAR from process.env as fallback", () => {
41
+ process.env.TEST_PROCESS_VAR = "process-value";
42
+ expect(resolver.resolve("$TEST_PROCESS_VAR")).toBe("process-value");
43
+ delete process.env.TEST_PROCESS_VAR;
44
+ });
45
+
46
+ it("prioritizes credential.env over process.env", () => {
47
+ process.env.PREF_KEY = "process-value";
48
+ expect(
49
+ resolver.resolve("$PREF_KEY", { PREF_KEY: "credential-value" }),
50
+ ).toBe("credential-value");
51
+ delete process.env.PREF_KEY;
52
+ });
53
+
54
+ it("returns placeholder when env var is missing", () => {
55
+ expect(resolver.resolve("$UNSET_VAR")).toBe("sk-placeholder");
56
+ expect(resolver.resolve("${UNSET_VAR}", { OTHER_KEY: "value" })).toBe(
57
+ "sk-placeholder",
58
+ );
59
+ });
60
+
61
+ it("returns placeholder for invalid env var syntax", () => {
62
+ expect(resolver.resolve("$123invalid")).toBe("sk-placeholder");
63
+ expect(resolver.resolve("$has-dash")).toBe("sk-placeholder");
64
+ });
65
+ });
66
+ });
@@ -0,0 +1,116 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { UrlResolver } from "../../src/utils/urlResolver";
3
+
4
+ function createResolver(
5
+ getLlamaSettings: () => Promise<Record<string, unknown>>,
6
+ getMergedSettings: () => Promise<Record<string, unknown>>,
7
+ ) {
8
+ return new UrlResolver({ getLlamaSettings, getMergedSettings });
9
+ }
10
+
11
+ describe("UrlResolver", () => {
12
+ beforeEach(() => {
13
+ delete process.env.LLAMA_SERVER_URL;
14
+ });
15
+
16
+ describe("resolveUrls", () => {
17
+ it("falls back to default when nothing is configured", async () => {
18
+ const resolver = createResolver(
19
+ () => Promise.resolve({}),
20
+ () => Promise.resolve({}),
21
+ );
22
+
23
+ const result = await resolver.resolveUrls();
24
+ expect(result).toEqual(["http://127.0.0.1:8080"]);
25
+ });
26
+
27
+ it("prioritizes env variable over all config sources", async () => {
28
+ process.env.LLAMA_SERVER_URL = "http://env:9090";
29
+ const resolver = createResolver(
30
+ () =>
31
+ Promise.resolve({
32
+ servers: [{ url: "http://servers:8080" }],
33
+ }),
34
+ () => Promise.resolve({ llamaServerUrl: "http://legacy:7070" }),
35
+ );
36
+
37
+ const result = await resolver.resolveUrls();
38
+ expect(result).toEqual(["http://env:9090"]);
39
+ });
40
+
41
+ it("prioritizes llamaSettings.servers over legacy llamaServerUrl", async () => {
42
+ const resolver = createResolver(
43
+ () =>
44
+ Promise.resolve({
45
+ servers: [{ url: "http://servers:8080" }],
46
+ }),
47
+ () => Promise.resolve({ llamaServerUrl: "http://legacy:7070" }),
48
+ );
49
+
50
+ const result = await resolver.resolveUrls();
51
+ expect(result).toEqual(["http://servers:8080"]);
52
+ });
53
+
54
+ it("falls back to legacy when servers is empty", async () => {
55
+ const resolver = createResolver(
56
+ () => Promise.resolve({ servers: [] }),
57
+ () => Promise.resolve({ llamaServerUrl: "http://legacy:7070" }),
58
+ );
59
+
60
+ const result = await resolver.resolveUrls();
61
+ expect(result).toEqual(["http://legacy:7070"]);
62
+ });
63
+
64
+ it("returns empty arrays from settings when values are falsy", async () => {
65
+ const resolver = createResolver(
66
+ () => Promise.resolve({ servers: undefined }),
67
+ () => Promise.resolve({ llamaServerUrl: undefined }),
68
+ );
69
+
70
+ const result = await resolver.resolveUrls();
71
+ expect(result).toEqual(["http://127.0.0.1:8080"]);
72
+ });
73
+ });
74
+
75
+ describe("warnings", () => {
76
+ it("collects warnings for invalid env URLs", async () => {
77
+ process.env.LLAMA_SERVER_URL = "invalid-url;http://good:8080";
78
+ const resolver = createResolver(
79
+ () => Promise.resolve({}),
80
+ () => Promise.resolve({}),
81
+ );
82
+
83
+ await resolver.resolveUrls();
84
+ expect(resolver.takeWarnings()).toEqual([
85
+ "Ignoring invalid server URL 'invalid-url' (needs http(s)://)",
86
+ ]);
87
+ });
88
+
89
+ it("collects warnings for invalid server URLs", async () => {
90
+ const resolver = createResolver(
91
+ () =>
92
+ Promise.resolve({
93
+ servers: [{ url: "bad-url" }, { url: "http://good:8080" }],
94
+ }),
95
+ () => Promise.resolve({}),
96
+ );
97
+
98
+ await resolver.resolveUrls();
99
+ expect(resolver.takeWarnings()).toEqual([
100
+ "Ignoring invalid server URL 'bad-url' (needs http(s)://)",
101
+ ]);
102
+ });
103
+
104
+ it("clears warnings after takeWarnings()", async () => {
105
+ process.env.LLAMA_SERVER_URL = "bad;http://good:8080";
106
+ const resolver = createResolver(
107
+ () => Promise.resolve({}),
108
+ () => Promise.resolve({}),
109
+ );
110
+
111
+ await resolver.resolveUrls();
112
+ expect(resolver.takeWarnings()).toHaveLength(1);
113
+ expect(resolver.takeWarnings()).toEqual([]);
114
+ });
115
+ });
116
+ });