pi-free 1.0.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/.github/workflows/update-benchmarks.yml +67 -0
- package/.pi/skills/pi-extension-dev/SKILL.md +155 -0
- package/CHANGELOG.md +59 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/config.ts +224 -0
- package/constants.ts +110 -0
- package/docs/free-tier-limits.md +213 -0
- package/docs/model-hopping.md +214 -0
- package/docs/plans/file-reorganization.md +172 -0
- package/docs/plans/package-json-fix.md +143 -0
- package/docs/provider-failover-plan.md +279 -0
- package/lib/json-persistence.ts +102 -0
- package/lib/logger.ts +94 -0
- package/lib/model-enhancer.ts +20 -0
- package/lib/types.ts +108 -0
- package/lib/util.ts +256 -0
- package/package.json +52 -0
- package/provider-factory.ts +221 -0
- package/provider-failover/errors.ts +275 -0
- package/provider-failover/hardcoded-benchmarks.ts +9889 -0
- package/provider-failover/index.ts +194 -0
- package/provider-helper.ts +336 -0
- package/providers/cline-auth.ts +473 -0
- package/providers/cline-models.ts +77 -0
- package/providers/cline.ts +257 -0
- package/providers/factory.ts +125 -0
- package/providers/fireworks.ts +49 -0
- package/providers/kilo-auth.ts +172 -0
- package/providers/kilo-models.ts +26 -0
- package/providers/kilo.ts +144 -0
- package/providers/mistral.ts +144 -0
- package/providers/model-fetcher.ts +138 -0
- package/providers/nvidia.ts +97 -0
- package/providers/ollama.ts +113 -0
- package/providers/openrouter.ts +175 -0
- package/providers/zen.ts +416 -0
- package/scripts/update-benchmarks.ts +255 -0
- package/tests/cline.test.ts +149 -0
- package/tests/errors.test.ts +139 -0
- package/tests/failover.test.ts +94 -0
- package/tests/fireworks.test.ts +148 -0
- package/tests/free-tier-limits.test.ts +191 -0
- package/tests/json-persistence.test.ts +105 -0
- package/tests/kilo.test.ts +186 -0
- package/tests/mistral.test.ts +138 -0
- package/tests/nvidia.test.ts +55 -0
- package/tests/ollama.test.ts +261 -0
- package/tests/openrouter.test.ts +192 -0
- package/tests/usage-tracking.test.ts +150 -0
- package/tests/util.test.ts +413 -0
- package/tests/zen.test.ts +180 -0
- package/todo.md +153 -0
- package/tsconfig.json +26 -0
- package/usage/commands.ts +17 -0
- package/usage/cumulative.ts +193 -0
- package/usage/formatters.ts +131 -0
- package/usage/index.ts +46 -0
- package/usage/limits.ts +166 -0
- package/usage/metrics.ts +222 -0
- package/usage/sessions.ts +355 -0
- package/usage/store.ts +99 -0
- package/usage/tracking.ts +329 -0
- package/usage/widget.ts +90 -0
- package/vitest.config.ts +20 -0
- package/widget/data.ts +113 -0
- package/widget/format.ts +26 -0
- package/widget/render.ts +117 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fireworks Provider Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
// Mock dependencies
|
|
9
|
+
vi.mock("../config.ts", () => ({
|
|
10
|
+
FIREWORKS_API_KEY: "test-fireworks-key",
|
|
11
|
+
FIREWORKS_SHOW_PAID: true,
|
|
12
|
+
PROVIDER_FIREWORKS: "fireworks",
|
|
13
|
+
applyHidden: (models: unknown[]) => models,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("../constants.ts", () => ({
|
|
17
|
+
BASE_URL_FIREWORKS: "https://api.fireworks.ai/inference/v1",
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("../provider-helper.ts", () => ({
|
|
21
|
+
createReRegister: vi.fn(() => vi.fn()),
|
|
22
|
+
setupProvider: vi.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("../lib/logger.ts", () => ({
|
|
26
|
+
createLogger: () => ({
|
|
27
|
+
warn: vi.fn(),
|
|
28
|
+
info: vi.fn(),
|
|
29
|
+
debug: vi.fn(),
|
|
30
|
+
error: vi.fn(),
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
import { setupProvider } from "../provider-helper.ts";
|
|
35
|
+
|
|
36
|
+
describe("Fireworks Provider", () => {
|
|
37
|
+
let mockPi: ExtensionAPI;
|
|
38
|
+
let mockRegisterProvider: ReturnType<typeof vi.fn>;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
mockRegisterProvider = vi.fn();
|
|
43
|
+
|
|
44
|
+
mockPi = {
|
|
45
|
+
registerProvider: mockRegisterProvider,
|
|
46
|
+
registerCommand: vi.fn(),
|
|
47
|
+
} as unknown as ExtensionAPI;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("initialization", () => {
|
|
51
|
+
it("should register provider with hardcoded models", async () => {
|
|
52
|
+
const { default: fireworksProvider } = await import(
|
|
53
|
+
"../providers/fireworks.ts"
|
|
54
|
+
);
|
|
55
|
+
await fireworksProvider(mockPi);
|
|
56
|
+
|
|
57
|
+
expect(mockRegisterProvider).toHaveBeenCalledWith(
|
|
58
|
+
"fireworks",
|
|
59
|
+
expect.objectContaining({
|
|
60
|
+
baseUrl: "https://api.fireworks.ai/inference/v1",
|
|
61
|
+
apiKey: "FIREWORKS_API_KEY",
|
|
62
|
+
api: "openai-completions",
|
|
63
|
+
models: expect.any(Array),
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should set API key in environment", async () => {
|
|
69
|
+
delete process.env.FIREWORKS_API_KEY;
|
|
70
|
+
|
|
71
|
+
const { default: fireworksProvider } = await import(
|
|
72
|
+
"../providers/fireworks.ts"
|
|
73
|
+
);
|
|
74
|
+
await fireworksProvider(mockPi);
|
|
75
|
+
|
|
76
|
+
expect(process.env.FIREWORKS_API_KEY).toBe("test-fireworks-key");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should skip registration without API key", async () => {
|
|
80
|
+
// Mock no API key by temporarily clearing the module
|
|
81
|
+
const apiKeySpy = vi
|
|
82
|
+
.spyOn(await import("../config.ts"), "FIREWORKS_API_KEY", "get")
|
|
83
|
+
.mockReturnValue(undefined as any);
|
|
84
|
+
|
|
85
|
+
const { default: fireworksProvider } = await import(
|
|
86
|
+
"../providers/fireworks.ts"
|
|
87
|
+
);
|
|
88
|
+
await fireworksProvider(mockPi);
|
|
89
|
+
|
|
90
|
+
// Should not register provider
|
|
91
|
+
expect(mockRegisterProvider).not.toHaveBeenCalled();
|
|
92
|
+
|
|
93
|
+
// Restore the mock so subsequent tests have the API key
|
|
94
|
+
apiKeySpy.mockRestore();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("model configuration", () => {
|
|
99
|
+
it("should have hardcoded models with correct structure", async () => {
|
|
100
|
+
const { default: fireworksProvider } = await import(
|
|
101
|
+
"../providers/fireworks.ts"
|
|
102
|
+
);
|
|
103
|
+
await fireworksProvider(mockPi);
|
|
104
|
+
|
|
105
|
+
expect(mockRegisterProvider).toHaveBeenCalled();
|
|
106
|
+
const registerCall = mockRegisterProvider.mock.calls[0];
|
|
107
|
+
expect(registerCall).toBeDefined();
|
|
108
|
+
const models = registerCall?.[1]?.models;
|
|
109
|
+
|
|
110
|
+
expect(models).toBeInstanceOf(Array);
|
|
111
|
+
expect(models.length).toBeGreaterThan(0);
|
|
112
|
+
|
|
113
|
+
// Check first model has required properties
|
|
114
|
+
const firstModel = models[0];
|
|
115
|
+
expect(firstModel).toHaveProperty("id");
|
|
116
|
+
expect(firstModel).toHaveProperty("name");
|
|
117
|
+
expect(firstModel).toHaveProperty("reasoning");
|
|
118
|
+
expect(firstModel).toHaveProperty("input");
|
|
119
|
+
expect(firstModel).toHaveProperty("cost");
|
|
120
|
+
expect(firstModel).toHaveProperty("contextWindow");
|
|
121
|
+
expect(firstModel).toHaveProperty("maxTokens");
|
|
122
|
+
|
|
123
|
+
// Verify non-zero costs (paid model, not free)
|
|
124
|
+
expect(firstModel.cost.input).toBeGreaterThan(0);
|
|
125
|
+
expect(firstModel.cost.output).toBeGreaterThan(0);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("setupProvider integration", () => {
|
|
130
|
+
it("should call setupProvider with stored models", async () => {
|
|
131
|
+
const { default: fireworksProvider } = await import(
|
|
132
|
+
"../providers/fireworks.ts"
|
|
133
|
+
);
|
|
134
|
+
await fireworksProvider(mockPi);
|
|
135
|
+
|
|
136
|
+
expect(setupProvider).toHaveBeenCalledWith(
|
|
137
|
+
mockPi,
|
|
138
|
+
expect.objectContaining({
|
|
139
|
+
providerId: "fireworks",
|
|
140
|
+
}),
|
|
141
|
+
expect.objectContaining({
|
|
142
|
+
free: expect.any(Array),
|
|
143
|
+
all: expect.any(Array),
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
FREE_TIER_LIMITS,
|
|
4
|
+
formatFreeTierStatus,
|
|
5
|
+
getFreeTierUsage,
|
|
6
|
+
getLimitWarning,
|
|
7
|
+
getModelUsage,
|
|
8
|
+
getProviderModelUsage,
|
|
9
|
+
getSessionUsage,
|
|
10
|
+
getTopModels,
|
|
11
|
+
incrementModelRequestCount,
|
|
12
|
+
isApproachingLimit,
|
|
13
|
+
resetUsageStats,
|
|
14
|
+
} from "../usage/limits.ts";
|
|
15
|
+
|
|
16
|
+
describe("Free Tier Limits", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
// Reset usage stats before each test
|
|
19
|
+
resetUsageStats();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("FREE_TIER_LIMITS", () => {
|
|
23
|
+
it("should have limits for kilo provider", () => {
|
|
24
|
+
expect(FREE_TIER_LIMITS.kilo).toBeDefined();
|
|
25
|
+
expect(FREE_TIER_LIMITS.kilo.requestsPerHour).toBe(200);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should have limits for openrouter provider", () => {
|
|
29
|
+
expect(FREE_TIER_LIMITS.openrouter).toBeDefined();
|
|
30
|
+
expect(FREE_TIER_LIMITS.openrouter.requestsPerDay).toBe(1000);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should have limits for nvidia provider", () => {
|
|
34
|
+
expect(FREE_TIER_LIMITS.nvidia).toBeDefined();
|
|
35
|
+
expect(FREE_TIER_LIMITS.nvidia.requestsPerMonth).toBe(1000);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should have limits for fireworks provider", () => {
|
|
39
|
+
expect(FREE_TIER_LIMITS.fireworks).toBeDefined();
|
|
40
|
+
expect(FREE_TIER_LIMITS.fireworks.requestsPerMonth).toBe(1000);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("Request Counting", () => {
|
|
45
|
+
it("should increment request count", () => {
|
|
46
|
+
// Just verify it doesn't throw
|
|
47
|
+
expect(() => {
|
|
48
|
+
incrementModelRequestCount("kilo", "gpt-4", 100, 50);
|
|
49
|
+
}).not.toThrow();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should track model usage", () => {
|
|
53
|
+
incrementModelRequestCount("test", "model-1", 100, 50);
|
|
54
|
+
incrementModelRequestCount("test", "model-1", 200, 100);
|
|
55
|
+
|
|
56
|
+
const usage = getModelUsage("test", "model-1");
|
|
57
|
+
expect(usage).toBeDefined();
|
|
58
|
+
expect(usage?.count).toBe(2);
|
|
59
|
+
expect(usage?.tokensIn).toBe(300);
|
|
60
|
+
expect(usage?.tokensOut).toBe(150);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should return undefined for unknown models", () => {
|
|
64
|
+
const usage = getModelUsage("unknown", "unknown-model");
|
|
65
|
+
expect(usage).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should track different models separately", () => {
|
|
69
|
+
incrementModelRequestCount("test", "model-a", 100, 50);
|
|
70
|
+
incrementModelRequestCount("test", "model-b", 200, 100);
|
|
71
|
+
|
|
72
|
+
expect(getModelUsage("test", "model-a")?.count).toBe(1);
|
|
73
|
+
expect(getModelUsage("test", "model-b")?.count).toBe(1);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("Provider Model Usage", () => {
|
|
78
|
+
it("should return all models for a provider", () => {
|
|
79
|
+
incrementModelRequestCount("test-provider", "model-1", 100, 50);
|
|
80
|
+
incrementModelRequestCount("test-provider", "model-2", 200, 100);
|
|
81
|
+
|
|
82
|
+
const models = getProviderModelUsage("test-provider");
|
|
83
|
+
expect(models).toHaveLength(2);
|
|
84
|
+
expect(models.map((m) => m.modelId)).toContain("model-1");
|
|
85
|
+
expect(models.map((m) => m.modelId)).toContain("model-2");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should return empty array for unknown provider", () => {
|
|
89
|
+
const models = getProviderModelUsage("nonexistent");
|
|
90
|
+
expect(models).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("Top Models", () => {
|
|
95
|
+
it("should return top N models by request count", () => {
|
|
96
|
+
// Add many models with varying request counts
|
|
97
|
+
for (let i = 0; i < 5; i++) {
|
|
98
|
+
incrementModelRequestCount("test", "popular-model", 100, 50);
|
|
99
|
+
}
|
|
100
|
+
incrementModelRequestCount("test", "unpopular-model", 100, 50);
|
|
101
|
+
|
|
102
|
+
const top = getTopModels(2);
|
|
103
|
+
expect(top).toHaveLength(2);
|
|
104
|
+
expect(top[0].modelId).toBe("popular-model");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("Free Tier Usage", () => {
|
|
109
|
+
it("should calculate usage for kilo provider", () => {
|
|
110
|
+
// Simulate requests (out of 200/hour limit)
|
|
111
|
+
for (let i = 0; i < 100; i++) {
|
|
112
|
+
incrementModelRequestCount("kilo", "gpt-4", 10, 10);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const usage = getFreeTierUsage("kilo");
|
|
116
|
+
// requestsThisHour is capped at 50 as a rough estimate
|
|
117
|
+
expect(usage.requestsToday).toBeGreaterThanOrEqual(100);
|
|
118
|
+
expect(usage.requestsThisHour).toBeLessThanOrEqual(50);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should calculate usage for providers without hourly limits", () => {
|
|
122
|
+
// OpenRouter has daily limit, not hourly
|
|
123
|
+
const usage = getFreeTierUsage("openrouter");
|
|
124
|
+
expect(usage.requestsThisHour).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("Limit Warnings", () => {
|
|
129
|
+
it("should detect when approaching limit", () => {
|
|
130
|
+
// Use openrouter with daily limit (1000/day)
|
|
131
|
+
// Add 750 requests = 75% which triggers warning
|
|
132
|
+
for (let i = 0; i < 750; i++) {
|
|
133
|
+
incrementModelRequestCount("openrouter", "gpt-4", 10, 10);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
expect(isApproachingLimit("openrouter")).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should not trigger warning when usage is low", () => {
|
|
140
|
+
incrementModelRequestCount("kilo", "gpt-4", 10, 10);
|
|
141
|
+
expect(isApproachingLimit("kilo")).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should return warning message when approaching limit", () => {
|
|
145
|
+
// Use openrouter with daily limit (1000/day)
|
|
146
|
+
// Add 750 requests = 75% which triggers warning
|
|
147
|
+
for (let i = 0; i < 750; i++) {
|
|
148
|
+
incrementModelRequestCount("openrouter", "gpt-4", 10, 10);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const warning = getLimitWarning("openrouter");
|
|
152
|
+
expect(warning).not.toBeNull();
|
|
153
|
+
expect(warning).toContain("%");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should return null when not approaching limit", () => {
|
|
157
|
+
const warning = getLimitWarning("kilo");
|
|
158
|
+
expect(warning).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("Session Usage", () => {
|
|
163
|
+
it("should generate session report", () => {
|
|
164
|
+
incrementModelRequestCount("kilo", "gpt-4", 1000, 500);
|
|
165
|
+
incrementModelRequestCount("openrouter", "mimo", 500, 250);
|
|
166
|
+
|
|
167
|
+
const report = getSessionUsage();
|
|
168
|
+
expect(report.totalRequests).toBe(2);
|
|
169
|
+
expect(report.totalTokensIn).toBe(1500);
|
|
170
|
+
expect(report.totalTokensOut).toBe(750);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should track providers in session report", () => {
|
|
174
|
+
incrementModelRequestCount("kilo", "gpt-4", 100, 50);
|
|
175
|
+
incrementModelRequestCount("openrouter", "mimo", 100, 50);
|
|
176
|
+
|
|
177
|
+
const report = getSessionUsage();
|
|
178
|
+
const providerNames = report.providers.map((p) => p.name);
|
|
179
|
+
expect(providerNames).toContain("kilo");
|
|
180
|
+
expect(providerNames).toContain("openrouter");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("Status Formatting", () => {
|
|
185
|
+
it("should format status for provider", () => {
|
|
186
|
+
const status = formatFreeTierStatus("kilo");
|
|
187
|
+
expect(typeof status).toBe("string");
|
|
188
|
+
expect(status).toContain("kilo");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Persistence Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, rmdirSync, unlinkSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
8
|
+
import { createJSONLStore, createJSONStore } from "../lib/json-persistence.ts";
|
|
9
|
+
|
|
10
|
+
const TEST_DIR = join(
|
|
11
|
+
process.env.HOME || process.env.USERPROFILE || "",
|
|
12
|
+
".pi-test",
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
describe("JSON Persistence", () => {
|
|
16
|
+
describe("createJSONStore", () => {
|
|
17
|
+
const testFile = join(TEST_DIR, "test-store.json");
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Clean up
|
|
21
|
+
try {
|
|
22
|
+
if (existsSync(testFile)) unlinkSync(testFile);
|
|
23
|
+
} catch {}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
try {
|
|
28
|
+
if (existsSync(testFile)) unlinkSync(testFile);
|
|
29
|
+
if (existsSync(TEST_DIR)) rmdirSync(TEST_DIR);
|
|
30
|
+
} catch {}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should return default value when file doesn't exist", () => {
|
|
34
|
+
const store = createJSONStore(testFile, { default: true });
|
|
35
|
+
const data = store.load();
|
|
36
|
+
expect(data).toEqual({ default: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should persist and load data", () => {
|
|
40
|
+
const store = createJSONStore(testFile, { count: 0 });
|
|
41
|
+
store.save({ count: 42 });
|
|
42
|
+
|
|
43
|
+
// Create new store instance to test loading from disk
|
|
44
|
+
const store2 = createJSONStore(testFile, { count: 0 });
|
|
45
|
+
const data = store2.load();
|
|
46
|
+
expect(data.count).toBe(42);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should cache data after first load", () => {
|
|
50
|
+
const store = createJSONStore(testFile, { value: "initial" });
|
|
51
|
+
store.save({ value: "updated" });
|
|
52
|
+
|
|
53
|
+
// First load reads from disk
|
|
54
|
+
const data1 = store.load();
|
|
55
|
+
expect(data1.value).toBe("updated");
|
|
56
|
+
|
|
57
|
+
// Second load should return cached value
|
|
58
|
+
const data2 = store.load();
|
|
59
|
+
expect(data2.value).toBe("updated");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("createJSONLStore", () => {
|
|
64
|
+
const testFile = join(TEST_DIR, "test-log.jsonl");
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
try {
|
|
68
|
+
if (existsSync(testFile)) unlinkSync(testFile);
|
|
69
|
+
} catch {}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
try {
|
|
74
|
+
if (existsSync(testFile)) unlinkSync(testFile);
|
|
75
|
+
if (existsSync(TEST_DIR)) rmdirSync(TEST_DIR);
|
|
76
|
+
} catch {}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should return empty array when file doesn't exist", () => {
|
|
80
|
+
const store = createJSONLStore<{ msg: string }>(testFile);
|
|
81
|
+
const data = store.load();
|
|
82
|
+
expect(data).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should append entries", () => {
|
|
86
|
+
const store = createJSONLStore<{ event: string }>(testFile);
|
|
87
|
+
store.append({ event: "first" });
|
|
88
|
+
store.append({ event: "second" });
|
|
89
|
+
|
|
90
|
+
const data = store.load();
|
|
91
|
+
expect(data).toHaveLength(2);
|
|
92
|
+
expect(data[0].event).toBe("first");
|
|
93
|
+
expect(data[1].event).toBe("second");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should clear entries", () => {
|
|
97
|
+
const store = createJSONLStore<{ event: string }>(testFile);
|
|
98
|
+
store.append({ event: "test" });
|
|
99
|
+
store.clear();
|
|
100
|
+
|
|
101
|
+
const data = store.load();
|
|
102
|
+
expect(data).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kilo Provider Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
// Create mock functions first
|
|
9
|
+
const mockFetchKiloModels = vi.fn();
|
|
10
|
+
const mockSetupProvider = vi.fn();
|
|
11
|
+
const mockLoginKilo = vi.fn();
|
|
12
|
+
|
|
13
|
+
// Mock dependencies before importing the provider
|
|
14
|
+
vi.mock("../kilo-auth.ts", () => ({
|
|
15
|
+
loginKilo: (...args: unknown[]) => mockLoginKilo(...args),
|
|
16
|
+
refreshKiloToken: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("../kilo-models.ts", () => ({
|
|
20
|
+
fetchKiloModels: (...args: unknown[]) => mockFetchKiloModels(...args),
|
|
21
|
+
KILO_GATEWAY_BASE: "https://api.kilo.ai/api/gateway",
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock("../provider-helper.ts", () => ({
|
|
25
|
+
createReRegister: vi.fn(() => vi.fn()),
|
|
26
|
+
setupProvider: (...args: unknown[]) => mockSetupProvider(...args),
|
|
27
|
+
enhanceWithCI: (models: unknown[]) => models,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("../usage-widget.ts", () => ({
|
|
31
|
+
registerUsageWidget: vi.fn(),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("../util.ts", () => ({
|
|
35
|
+
logWarning: vi.fn(),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
import kiloProvider from "../providers/kilo.ts";
|
|
39
|
+
|
|
40
|
+
describe("Kilo Provider", () => {
|
|
41
|
+
let _mockPi: ExtensionAPI;
|
|
42
|
+
let mockRegisterProvider: ReturnType<typeof vi.fn>;
|
|
43
|
+
let mockOn: ReturnType<typeof vi.fn>;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
mockFetchKiloModels.mockReset();
|
|
48
|
+
mockSetupProvider.mockReset();
|
|
49
|
+
mockLoginKilo.mockReset();
|
|
50
|
+
|
|
51
|
+
mockRegisterProvider = vi.fn();
|
|
52
|
+
mockOn = vi.fn();
|
|
53
|
+
|
|
54
|
+
_mockPi = {
|
|
55
|
+
registerProvider: mockRegisterProvider,
|
|
56
|
+
on: mockOn,
|
|
57
|
+
registerCommand: vi.fn(),
|
|
58
|
+
} as unknown as ExtensionAPI;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("Kilo Provider", () => {
|
|
62
|
+
let mockPi: ExtensionAPI;
|
|
63
|
+
let mockRegisterProvider: ReturnType<typeof vi.fn>;
|
|
64
|
+
let mockOn: ReturnType<typeof vi.fn>;
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
vi.clearAllMocks();
|
|
68
|
+
mockRegisterProvider = vi.fn();
|
|
69
|
+
mockOn = vi.fn();
|
|
70
|
+
|
|
71
|
+
mockPi = {
|
|
72
|
+
registerProvider: mockRegisterProvider,
|
|
73
|
+
on: mockOn,
|
|
74
|
+
registerCommand: vi.fn(),
|
|
75
|
+
} as unknown as ExtensionAPI;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("initialization", () => {
|
|
79
|
+
it("should register provider with free models on startup", async () => {
|
|
80
|
+
const mockModels = [
|
|
81
|
+
{
|
|
82
|
+
id: "gpt-4",
|
|
83
|
+
name: "GPT-4",
|
|
84
|
+
reasoning: true,
|
|
85
|
+
input: ["text"],
|
|
86
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
87
|
+
contextWindow: 128000,
|
|
88
|
+
maxTokens: 4096,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
vi.mocked(fetchKiloModels).mockResolvedValue(mockModels);
|
|
92
|
+
|
|
93
|
+
await kiloProvider(mockPi);
|
|
94
|
+
|
|
95
|
+
expect(mockRegisterProvider).toHaveBeenCalledWith(
|
|
96
|
+
"kilo",
|
|
97
|
+
expect.objectContaining({
|
|
98
|
+
baseUrl: "https://api.kilo.ai/api/gateway",
|
|
99
|
+
apiKey: "KILO_API_KEY",
|
|
100
|
+
api: "openai-completions",
|
|
101
|
+
models: mockModels,
|
|
102
|
+
oauth: expect.any(Object),
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should handle model fetch failure gracefully", async () => {
|
|
108
|
+
vi.mocked(fetchKiloModels).mockRejectedValue(new Error("Network error"));
|
|
109
|
+
|
|
110
|
+
await kiloProvider(mockPi);
|
|
111
|
+
|
|
112
|
+
// Should still register with empty models
|
|
113
|
+
expect(mockRegisterProvider).toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("OAuth integration", () => {
|
|
118
|
+
it("should have oauth configuration", async () => {
|
|
119
|
+
vi.mocked(fetchKiloModels).mockResolvedValue([]);
|
|
120
|
+
|
|
121
|
+
await kiloProvider(mockPi);
|
|
122
|
+
|
|
123
|
+
const registerCall = mockRegisterProvider.mock.calls[0];
|
|
124
|
+
expect(registerCall[1]).toHaveProperty("oauth");
|
|
125
|
+
expect(registerCall[1].oauth).toHaveProperty("name", "Kilo");
|
|
126
|
+
expect(registerCall[1].oauth).toHaveProperty("login");
|
|
127
|
+
expect(registerCall[1].oauth).toHaveProperty("refreshToken");
|
|
128
|
+
expect(registerCall[1].oauth).toHaveProperty("getApiKey");
|
|
129
|
+
expect(registerCall[1].oauth).toHaveProperty("modifyModels");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should fetch all models after login", async () => {
|
|
133
|
+
vi.mocked(fetchKiloModels).mockResolvedValue([]);
|
|
134
|
+
|
|
135
|
+
await kiloProvider(mockPi);
|
|
136
|
+
|
|
137
|
+
const oauth = mockRegisterProvider.mock.calls[0][1].oauth;
|
|
138
|
+
const _mockCreds = {
|
|
139
|
+
access: "test-token",
|
|
140
|
+
refresh: "",
|
|
141
|
+
expires: Date.now() + 3600000,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
vi.mocked(fetchKiloModels).mockResolvedValue([
|
|
145
|
+
{ id: "gpt-4", name: "GPT-4" },
|
|
146
|
+
{ id: "claude-3", name: "Claude 3" },
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
await oauth.login({ onProgress: vi.fn() });
|
|
150
|
+
|
|
151
|
+
expect(fetchKiloModels).toHaveBeenCalledWith({ token: "test-token" });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("event handlers", () => {
|
|
156
|
+
it("should register session_start handler", async () => {
|
|
157
|
+
vi.mocked(fetchKiloModels).mockResolvedValue([]);
|
|
158
|
+
|
|
159
|
+
await kiloProvider(mockPi);
|
|
160
|
+
|
|
161
|
+
expect(mockOn).toHaveBeenCalledWith(
|
|
162
|
+
"session_start",
|
|
163
|
+
expect.any(Function),
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("setupProvider integration", () => {
|
|
169
|
+
it("should call setupProvider with correct config", async () => {
|
|
170
|
+
vi.mocked(fetchKiloModels).mockResolvedValue([]);
|
|
171
|
+
|
|
172
|
+
await kiloProvider(mockPi);
|
|
173
|
+
|
|
174
|
+
expect(setupProvider).toHaveBeenCalledWith(
|
|
175
|
+
mockPi,
|
|
176
|
+
expect.objectContaining({
|
|
177
|
+
providerId: "kilo",
|
|
178
|
+
tosUrl: expect.stringContaining("kilo.ai"),
|
|
179
|
+
reRegister: expect.any(Function),
|
|
180
|
+
}),
|
|
181
|
+
expect.objectContaining({ free: [], all: [] }),
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|