pi-llama-cpp 0.5.1 → 0.7.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/README.md +96 -30
- package/package.json +6 -5
- package/src/constants.ts +27 -5
- package/src/enums/action.ts +3 -2
- package/src/enums/mode.ts +1 -0
- package/src/enums/serverStatus.ts +6 -0
- package/src/enums/status.ts +1 -0
- package/src/index.ts +53 -31
- package/src/interfaces/auth.ts +1 -5
- package/src/interfaces/endpoints/props.ts +1 -0
- package/src/interfaces/levels.ts +7 -0
- package/src/managers/command.ts +290 -0
- package/src/managers/events.ts +101 -0
- package/src/managers/server.ts +136 -0
- package/src/models/baseModel.ts +75 -20
- package/src/models/legacyModel.ts +45 -0
- package/src/models/routerModel.ts +7 -30
- package/src/models/singleModel.ts +9 -6
- package/src/resolver.ts +152 -0
- package/src/server.ts +187 -0
- package/tests/commandManager.test.ts +182 -133
- package/tests/events.test.ts +256 -0
- package/tests/legacyModel.test.ts +112 -0
- package/tests/mocks.ts +100 -0
- package/tests/resolver.test.ts +143 -106
- package/tests/routerModel.test.ts +46 -68
- package/tests/server.test.ts +176 -0
- package/tests/serverManager.test.ts +130 -0
- package/tests/singleModel.test.ts +21 -29
- package/src/commands/models.ts +0 -228
- package/src/events.ts +0 -26
- package/src/manager.ts +0 -96
- package/src/tools/resolver.ts +0 -136
- package/src/tools/retriever.ts +0 -71
- package/tests/handlers.test.ts +0 -164
- package/tests/modelsCommand.test.ts +0 -270
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { Mode } from "../src/enums/mode";
|
|
3
|
+
import { Status } from "../src/enums/status";
|
|
4
|
+
import { DataProperty } from "../src/interfaces/endpoints/models";
|
|
5
|
+
import { LegacyModel } from "../src/models/legacyModel";
|
|
6
|
+
import { createMockServer, mockRpc } from "./mocks";
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockRpc.mockReset();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const createModel = (extra: Partial<DataProperty> = {}): LegacyModel =>
|
|
13
|
+
new LegacyModel(
|
|
14
|
+
{
|
|
15
|
+
id: "test",
|
|
16
|
+
tags: [],
|
|
17
|
+
object: "model",
|
|
18
|
+
owned_by: "test",
|
|
19
|
+
created: Date.now(),
|
|
20
|
+
...extra,
|
|
21
|
+
},
|
|
22
|
+
createMockServer(),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
describe("LegacyModel mode", () => {
|
|
26
|
+
it("should always return LEGACY mode", () => {
|
|
27
|
+
const model = createModel();
|
|
28
|
+
expect(model.mode).toBe(Mode.LEGACY);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("LegacyModel capabilities", () => {
|
|
33
|
+
it("should detect image capability when multimodal is in capabilities", async () => {
|
|
34
|
+
mockRpc.mockResolvedValueOnce({ modalities: { vision: true } });
|
|
35
|
+
|
|
36
|
+
const model = createModel();
|
|
37
|
+
const capabilities = await model.getCapabilities();
|
|
38
|
+
|
|
39
|
+
expect(capabilities).toEqual(["text", "image"]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should detect text-only capability when multimodal is not in capabilities", async () => {
|
|
43
|
+
mockRpc.mockResolvedValueOnce({ modalities: { vision: false } });
|
|
44
|
+
|
|
45
|
+
const model = createModel();
|
|
46
|
+
const capabilities = await model.getCapabilities();
|
|
47
|
+
|
|
48
|
+
expect(capabilities).toEqual(["text"]);
|
|
49
|
+
});
|
|
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
|
+
|
|
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 });
|
|
67
|
+
|
|
68
|
+
const model = createModel();
|
|
69
|
+
const status = await model.getStatus();
|
|
70
|
+
|
|
71
|
+
expect(status).toBe(Status.SLEEPING);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("LegacyModel getContextSize", () => {
|
|
76
|
+
it("should use max_model_len when it is non-zero", async () => {
|
|
77
|
+
mockRpc.mockResolvedValueOnce({ n_ctx: 4096 });
|
|
78
|
+
mockRpc.mockResolvedValueOnce({
|
|
79
|
+
data: [{ max_model_len: 8192 }],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const model = createModel();
|
|
83
|
+
const ctxSize = await model.getContextSize();
|
|
84
|
+
|
|
85
|
+
expect(ctxSize).toBe(8192);
|
|
86
|
+
expect(mockRpc).toHaveBeenCalledWith("/v1/models");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should fall back to n_ctx when max_model_len is 0", async () => {
|
|
90
|
+
mockRpc.mockResolvedValueOnce({ n_ctx: 4096 });
|
|
91
|
+
mockRpc.mockResolvedValueOnce({
|
|
92
|
+
data: [{ max_model_len: 0 }],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const model = createModel();
|
|
96
|
+
const ctxSize = await model.getContextSize();
|
|
97
|
+
|
|
98
|
+
expect(ctxSize).toBe(4096);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should return DEFAULT_CTX when both values are missing/null", async () => {
|
|
102
|
+
mockRpc.mockResolvedValueOnce({});
|
|
103
|
+
mockRpc.mockResolvedValueOnce({
|
|
104
|
+
data: [{ max_model_len: null }],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const model = createModel();
|
|
108
|
+
const ctxSize = await model.getContextSize();
|
|
109
|
+
|
|
110
|
+
expect(ctxSize).toBe(128000);
|
|
111
|
+
});
|
|
112
|
+
});
|
package/tests/mocks.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { vi } from "vitest";
|
|
3
|
+
import { Mode } from "../src/enums/mode";
|
|
4
|
+
import { ServerStatus } from "../src/enums/serverStatus";
|
|
5
|
+
import { Status } from "../src/enums/status";
|
|
6
|
+
import { BaseModel } from "../src/models/baseModel";
|
|
7
|
+
import { Server } from "../src/server";
|
|
8
|
+
|
|
9
|
+
/** Shared mock RPC — each test configures it */
|
|
10
|
+
export const mockRpc = vi.fn();
|
|
11
|
+
|
|
12
|
+
/** Default mock server that assumes everything works */
|
|
13
|
+
export const createMockServer = (
|
|
14
|
+
overrides: Partial<Server & { apiKey?: string }> = {},
|
|
15
|
+
): Server => {
|
|
16
|
+
const models: BaseModel[] = [];
|
|
17
|
+
const server: Partial<Server> = {
|
|
18
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
19
|
+
models,
|
|
20
|
+
getApiKey: () => Promise.resolve(overrides.apiKey ?? ""),
|
|
21
|
+
fetchModels: () => mockRpc("/v1/models"),
|
|
22
|
+
fetchModelProps: (modelId: string) =>
|
|
23
|
+
mockRpc(`/props?model=${modelId}&autoload=false`),
|
|
24
|
+
fetchServerHealth: () => mockRpc("/health"),
|
|
25
|
+
fetchServerProps: () => mockRpc("/props?autoload=false"),
|
|
26
|
+
postRequest: (resource: "load" | "unload", model: string) =>
|
|
27
|
+
mockRpc(`/models/${resource}`, { model }),
|
|
28
|
+
isReady: async (timeout: number) => {
|
|
29
|
+
try {
|
|
30
|
+
const r = await mockRpc("/health");
|
|
31
|
+
return r.status === "ok"
|
|
32
|
+
? ServerStatus.READY
|
|
33
|
+
: ServerStatus.UNREACHABLE;
|
|
34
|
+
} catch {
|
|
35
|
+
return ServerStatus.UNREACHABLE;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
initialize: async () => {
|
|
39
|
+
const { data } = (await mockRpc("/v1/models")) as {
|
|
40
|
+
data: BaseModel[];
|
|
41
|
+
};
|
|
42
|
+
models.length = 0;
|
|
43
|
+
models.push(...(data ?? []));
|
|
44
|
+
},
|
|
45
|
+
...overrides,
|
|
46
|
+
};
|
|
47
|
+
return server as Server;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Helper to create a mock BaseModel */
|
|
51
|
+
export const createMockModel = (
|
|
52
|
+
name: string,
|
|
53
|
+
overrides: Partial<BaseModel> = {},
|
|
54
|
+
): BaseModel =>
|
|
55
|
+
({
|
|
56
|
+
name,
|
|
57
|
+
id: name,
|
|
58
|
+
mode: Mode.ROUTER,
|
|
59
|
+
serverUrl: "http://127.0.0.1:8080",
|
|
60
|
+
capabilities: ["text"] as ["text"],
|
|
61
|
+
getStatus: vi.fn().mockResolvedValue(Status.LOADED),
|
|
62
|
+
getContextSize: vi.fn().mockResolvedValue(4096),
|
|
63
|
+
getInfo: vi.fn().mockResolvedValue(`Model: ${name}\nID: ${name}`),
|
|
64
|
+
load: vi.fn().mockResolvedValue(undefined),
|
|
65
|
+
unload: vi.fn().mockResolvedValue(undefined),
|
|
66
|
+
toProviderConfig: vi.fn().mockResolvedValue({}),
|
|
67
|
+
getLabel: vi.fn().mockResolvedValue(name),
|
|
68
|
+
...overrides,
|
|
69
|
+
}) as unknown as BaseModel;
|
|
70
|
+
|
|
71
|
+
/** Create a mock extension context */
|
|
72
|
+
export const createMockCtx = (
|
|
73
|
+
selectFn: (prompt: string, options: string[]) => string | null,
|
|
74
|
+
) => ({
|
|
75
|
+
cwd: "/tmp/test",
|
|
76
|
+
ui: {
|
|
77
|
+
select: vi.fn(selectFn),
|
|
78
|
+
notify: vi.fn(),
|
|
79
|
+
theme: {
|
|
80
|
+
fg: (color: string, text: string) => text,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
modelRegistry: {
|
|
84
|
+
find: vi.fn().mockReturnValue({ id: "test-model-id" }),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/** Create a mock Pi instance */
|
|
89
|
+
export const createMockPi = () => ({
|
|
90
|
+
setModel: vi.fn(),
|
|
91
|
+
registerProvider: vi.fn(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/** Create a mock Pi context for EventManager */
|
|
95
|
+
export const createMockPiContext = (notifyFn: ReturnType<typeof vi.fn>) =>
|
|
96
|
+
({
|
|
97
|
+
ui: {
|
|
98
|
+
notify: notifyFn,
|
|
99
|
+
},
|
|
100
|
+
}) as any as ExtensionContext;
|
package/tests/resolver.test.ts
CHANGED
|
@@ -2,156 +2,193 @@ 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
|
+
// Hoisted mock instances — survives vi.resetModules()
|
|
8
|
+
const mockAuthStorage = vi.hoisted(() => ({
|
|
9
|
+
reload: vi.fn(),
|
|
10
|
+
getApiKey: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const mockSettingsManager = vi.hoisted(() => ({
|
|
14
|
+
getProjectSettings: vi.fn(),
|
|
15
|
+
getGlobalSettings: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Mock getAgentDir, AuthStorage, and SettingsManager before importing resolver
|
|
19
|
+
vi.mock("@earendil-works/pi-coding-agent", () => ({
|
|
20
|
+
getAgentDir: vi.fn().mockReturnValue("/fake/agent/dir"),
|
|
21
|
+
AuthStorage: {
|
|
22
|
+
create: vi.fn().mockReturnValue(mockAuthStorage),
|
|
23
|
+
},
|
|
24
|
+
SettingsManager: {
|
|
25
|
+
create: vi.fn().mockReturnValue(mockSettingsManager),
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock("node:fs/promises", () => ({
|
|
30
|
+
readFile: vi.fn(),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// Import mocked modules
|
|
34
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
35
|
+
import { readFile } from "node:fs/promises";
|
|
36
|
+
import { ConfigResolver } from "../src/resolver";
|
|
37
|
+
|
|
8
38
|
describe("URL resolution fallback chain", () => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
39
|
+
const mockReadFile = vi.mocked(readFile);
|
|
40
|
+
const mockGetAgentDir = vi.mocked(getAgentDir);
|
|
41
|
+
const mockGetProjectSettings = vi.mocked(
|
|
42
|
+
mockSettingsManager.getProjectSettings,
|
|
43
|
+
);
|
|
44
|
+
const mockGetGlobalSettings = vi.mocked(
|
|
45
|
+
mockSettingsManager.getGlobalSettings,
|
|
46
|
+
);
|
|
13
47
|
|
|
14
48
|
afterEach(() => {
|
|
15
49
|
delete process.env.LLAMA_SERVER_URL;
|
|
50
|
+
vi.resetModules();
|
|
16
51
|
});
|
|
17
52
|
|
|
18
|
-
|
|
19
|
-
vi.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
})
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
mockGetAgentDir.mockReturnValue("/fake/agent/dir");
|
|
56
|
+
// Default: no settings found
|
|
57
|
+
mockGetProjectSettings.mockReturnValue({});
|
|
58
|
+
mockGetGlobalSettings.mockReturnValue({});
|
|
59
|
+
});
|
|
24
60
|
|
|
25
|
-
|
|
26
|
-
const
|
|
61
|
+
it("should return default URL when no config is found", async () => {
|
|
62
|
+
const resolver = new ConfigResolver();
|
|
63
|
+
const result = await resolver.resolveUrls();
|
|
27
64
|
|
|
28
|
-
expect(result).
|
|
65
|
+
expect(result).toEqual([DEFAULT_LLAMA_SERVER_URL]);
|
|
29
66
|
});
|
|
30
67
|
|
|
31
68
|
it("should prioritize project config over env variable", async () => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
}));
|
|
42
|
-
|
|
69
|
+
mockGetProjectSettings.mockReturnValue({
|
|
70
|
+
llamaServerUrl: "http://localhost:9999",
|
|
71
|
+
});
|
|
43
72
|
process.env.LLAMA_SERVER_URL = "http://env-url:8080";
|
|
44
73
|
|
|
45
|
-
const
|
|
46
|
-
const result = await
|
|
74
|
+
const resolver = new ConfigResolver();
|
|
75
|
+
const result = await resolver.resolveUrls();
|
|
47
76
|
|
|
48
|
-
expect(result).
|
|
77
|
+
expect(result).toEqual(["http://localhost:9999"]);
|
|
49
78
|
});
|
|
50
79
|
|
|
51
80
|
it("should use env variable when no project config exists", async () => {
|
|
52
|
-
|
|
53
|
-
access: vi.fn().mockRejectedValue(new Error("ENOENT")),
|
|
54
|
-
constants: { F_OK: 0 },
|
|
55
|
-
readFile: vi.fn().mockResolvedValue(""),
|
|
56
|
-
}));
|
|
57
|
-
|
|
81
|
+
mockGetProjectSettings.mockReturnValue({});
|
|
58
82
|
process.env.LLAMA_SERVER_URL = "http://env-url:8080";
|
|
59
83
|
|
|
60
|
-
const
|
|
61
|
-
const result = await
|
|
84
|
+
const resolver = new ConfigResolver();
|
|
85
|
+
const result = await resolver.resolveUrls();
|
|
62
86
|
|
|
63
|
-
expect(result).
|
|
87
|
+
expect(result).toEqual(["http://env-url:8080"]);
|
|
64
88
|
});
|
|
65
89
|
|
|
66
90
|
it("should use global settings when no project config or env exists", async () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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");
|
|
91
|
+
mockGetProjectSettings.mockReturnValue({});
|
|
92
|
+
mockGetGlobalSettings.mockReturnValue({
|
|
93
|
+
llamaServerUrl: "http://global:8080",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const resolver = new ConfigResolver();
|
|
97
|
+
const result = await resolver.resolveUrls();
|
|
98
|
+
|
|
99
|
+
expect(result).toEqual(["http://global:8080"]);
|
|
84
100
|
});
|
|
85
101
|
|
|
86
102
|
it("should strip trailing slashes from resolved URL", async () => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
mockGetProjectSettings.mockReturnValue({
|
|
104
|
+
llamaServerUrl: "http://localhost:8080/",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const resolver = new ConfigResolver();
|
|
108
|
+
const result = await resolver.resolveUrls();
|
|
109
|
+
|
|
110
|
+
expect(result).toEqual(["http://localhost:8080"]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should cache the resolved URL on subsequent calls", async () => {
|
|
114
|
+
mockGetProjectSettings.mockReturnValue({
|
|
115
|
+
llamaServerUrl: "http://first:8080",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const resolver = new ConfigResolver();
|
|
119
|
+
const result1 = await resolver.resolveUrls();
|
|
120
|
+
const result2 = await resolver.resolveUrls();
|
|
121
|
+
|
|
122
|
+
expect(result1).toEqual(["http://first:8080"]);
|
|
123
|
+
expect(result2).toEqual(["http://first:8080"]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should handle multiple URLs separated by semicolons", async () => {
|
|
127
|
+
mockGetProjectSettings.mockReturnValue({
|
|
128
|
+
llamaServerUrl: "http://first:8080;http://second:9090/",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const resolver = new ConfigResolver();
|
|
132
|
+
const result = await resolver.resolveUrls();
|
|
133
|
+
|
|
134
|
+
expect(result).toEqual(["http://first:8080", "http://second:9090"]);
|
|
102
135
|
});
|
|
103
136
|
});
|
|
104
137
|
|
|
105
138
|
describe("API key resolution", () => {
|
|
139
|
+
const mockGetAgentDir = vi.mocked(getAgentDir);
|
|
140
|
+
|
|
141
|
+
afterEach(() => {
|
|
142
|
+
vi.resetModules();
|
|
143
|
+
});
|
|
144
|
+
|
|
106
145
|
beforeEach(() => {
|
|
107
146
|
vi.clearAllMocks();
|
|
108
|
-
|
|
147
|
+
mockGetAgentDir.mockReturnValue("/fake/agent/dir");
|
|
148
|
+
mockAuthStorage.reload.mockReturnValue(undefined);
|
|
149
|
+
mockAuthStorage.getApiKey.mockResolvedValue(undefined);
|
|
109
150
|
});
|
|
110
151
|
|
|
111
152
|
it("should return placeholder when auth file does not exist", async () => {
|
|
112
|
-
|
|
113
|
-
access: vi.fn().mockRejectedValue(new Error("ENOENT")),
|
|
114
|
-
constants: { F_OK: 0 },
|
|
115
|
-
readFile: vi.fn().mockResolvedValue(""),
|
|
116
|
-
}));
|
|
153
|
+
mockAuthStorage.getApiKey.mockResolvedValue(undefined);
|
|
117
154
|
|
|
118
|
-
const
|
|
119
|
-
const result = await resolveApiKey(
|
|
155
|
+
const resolver = new ConfigResolver();
|
|
156
|
+
const result = await resolver.resolveApiKey(
|
|
157
|
+
"llama-server=http://127.0.0.1:8080",
|
|
158
|
+
);
|
|
120
159
|
|
|
121
|
-
expect(result).
|
|
160
|
+
expect(result).toEqual(API_KEY_PLACEHOLDER);
|
|
122
161
|
});
|
|
123
162
|
|
|
124
163
|
it("should return placeholder when provider key is missing", async () => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}));
|
|
134
|
-
|
|
135
|
-
const { resolveApiKey } = await import("../src/tools/resolver");
|
|
136
|
-
const result = await resolveApiKey();
|
|
137
|
-
|
|
138
|
-
expect(result).toBe(API_KEY_PLACEHOLDER);
|
|
164
|
+
mockAuthStorage.getApiKey.mockResolvedValue(undefined);
|
|
165
|
+
|
|
166
|
+
const resolver = new ConfigResolver();
|
|
167
|
+
const result = await resolver.resolveApiKey(
|
|
168
|
+
"llama-server=http://127.0.0.1:8080",
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(result).toEqual(API_KEY_PLACEHOLDER);
|
|
139
172
|
});
|
|
140
173
|
|
|
141
174
|
it("should return the provider key when present", async () => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
175
|
+
mockAuthStorage.getApiKey.mockResolvedValue("test-api-key");
|
|
176
|
+
|
|
177
|
+
const resolver = new ConfigResolver();
|
|
178
|
+
const result = await resolver.resolveApiKey(
|
|
179
|
+
"llama-server=http://127.0.0.1:8080",
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(result).toEqual("test-api-key");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should call reload before each getApiKey", async () => {
|
|
186
|
+
mockAuthStorage.getApiKey.mockResolvedValue("cached-key");
|
|
187
|
+
|
|
188
|
+
const resolver = new ConfigResolver();
|
|
189
|
+
await resolver.resolveApiKey("llama-server=http://127.0.0.1:8080");
|
|
190
|
+
await resolver.resolveApiKey("llama-server=http://127.0.0.1:8080");
|
|
191
|
+
|
|
192
|
+
expect(mockAuthStorage.reload).toHaveBeenCalledTimes(2);
|
|
156
193
|
});
|
|
157
194
|
});
|