@selesai/code 0.13.29 → 0.13.30
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/CHANGELOG.md +10 -0
- package/README.md +8 -2
- package/dist/core/model-registry.d.ts +13 -1
- package/dist/core/model-registry.js +16 -0
- package/dist/defaults/settings.json +8 -13
- package/dist/extensions/capability-gateway/catalog.ts +2 -2
- package/dist/extensions/capability-gateway/index.ts +103 -18
- package/dist/extensions/capability-gateway/integration.test.ts +394 -8
- package/dist/extensions/capability-gateway/routing.test.ts +415 -0
- package/dist/extensions/capability-gateway/routing.ts +221 -0
- package/dist/extensions/grep-app/index.ts +10 -0
- package/dist/extensions/jev/decisions.test.ts +316 -0
- package/dist/extensions/jev/decisions.ts +527 -0
- package/dist/extensions/jev/test-support.ts +233 -0
- package/dist/extensions/jev-advisory-lifecycle.test.ts +206 -0
- package/dist/extensions/jev-advisory-memory.test.ts +191 -0
- package/dist/extensions/jev-advisory-recommendations.test.ts +240 -0
- package/dist/extensions/jev-advisory-routing.ts +539 -0
- package/dist/extensions/package.json +2 -2
- package/dist/extensions/pi-hermes-memory/src/memory-search-bridge.ts +40 -0
- package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +57 -46
- package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +17 -0
- package/dist/extensions/pi-hermes-memory/src/tools/session-search-tool.ts +10 -0
- package/dist/extensions/pi-hermes-memory/src/tools/skill-tool.ts +5 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +25 -0
- package/dist/extensions/pi-intercom/index.ts +10 -0
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +5 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +5 -0
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +10 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +10 -0
- package/dist/extensions/pi-web-agent/src/extension.ts +5 -0
- package/dist/extensions/question/index.ts +5 -0
- package/dist/extensions/tokenin-onboarding.ts +185 -0
- package/dist/skills/code-review-and-quality/SKILL.md +396 -0
- package/dist/skills/code-simplification/SKILL.md +331 -0
- package/dist/skills/incremental-implementation/SKILL.md +249 -0
- package/dist/skills/planning-and-task-breakdown/SKILL.md +257 -0
- package/dist/skills/references/agent-skills-LICENSE +21 -0
- package/dist/skills/references/definition-of-done.md +67 -0
- package/dist/skills/references/performance-checklist.md +236 -0
- package/dist/skills/references/security-checklist.md +248 -0
- package/docs/settings.md +68 -31
- package/package.json +3 -3
- package/dist/extensions/auto-model.test.ts +0 -438
- package/dist/extensions/auto-model.ts +0 -357
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@selesai/code";
|
|
6
|
-
|
|
7
|
-
const state = vi.hoisted(() => ({ settingsPath: "" }));
|
|
8
|
-
|
|
9
|
-
vi.mock("@selesai/code", () => ({
|
|
10
|
-
getSettingsPath: () => state.settingsPath,
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
vi.mock("@earendil-works/pi-ai/compat", async (importOriginal) => {
|
|
14
|
-
const actual = await importOriginal<typeof import("@earendil-works/pi-ai/compat")>();
|
|
15
|
-
return { ...actual, complete: vi.fn() };
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
import { complete } from "@earendil-works/pi-ai/compat";
|
|
19
|
-
import autoModelExtension, {
|
|
20
|
-
buildConversation,
|
|
21
|
-
buildJevPayload,
|
|
22
|
-
classifierModel,
|
|
23
|
-
classifyTier,
|
|
24
|
-
DEFAULT_AUTO_MODEL_CONFIG,
|
|
25
|
-
parseModelRef,
|
|
26
|
-
readAutoModelConfig,
|
|
27
|
-
TIERS,
|
|
28
|
-
tierFromJevResponse,
|
|
29
|
-
type AutoModelConfig,
|
|
30
|
-
} from "./auto-model.ts";
|
|
31
|
-
|
|
32
|
-
const completeMock = vi.mocked(complete);
|
|
33
|
-
|
|
34
|
-
type Handler = (event: unknown, ctx: ExtensionContext) => unknown;
|
|
35
|
-
|
|
36
|
-
function templateModel(provider = "tokenin", id = "celestial-pro") {
|
|
37
|
-
return {
|
|
38
|
-
provider,
|
|
39
|
-
id,
|
|
40
|
-
name: id,
|
|
41
|
-
api: "openai-completions",
|
|
42
|
-
baseUrl: "https://lite.andlet.me/v1",
|
|
43
|
-
reasoning: true,
|
|
44
|
-
input: ["text"],
|
|
45
|
-
cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
|
|
46
|
-
contextWindow: 393216,
|
|
47
|
-
maxTokens: 64000,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function userEntry(text: string, id = "u1", content?: unknown): SessionEntry {
|
|
52
|
-
return {
|
|
53
|
-
id,
|
|
54
|
-
type: "message",
|
|
55
|
-
message: { role: "user", content: content ?? [{ type: "text", text }], timestamp: 1 },
|
|
56
|
-
parentId: "root",
|
|
57
|
-
timestamp: "2025-01-01T00:00:00.000Z",
|
|
58
|
-
} as unknown as SessionEntry;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function assistantEntry(text: string, id = "a1"): SessionEntry {
|
|
62
|
-
return {
|
|
63
|
-
id,
|
|
64
|
-
type: "message",
|
|
65
|
-
message: { role: "assistant", content: [{ type: "text", text }], timestamp: 1 },
|
|
66
|
-
parentId: "root",
|
|
67
|
-
timestamp: "2025-01-01T00:00:00.000Z",
|
|
68
|
-
} as unknown as SessionEntry;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function createHarness(branch: SessionEntry[] = [], scopedModels: unknown[] = []) {
|
|
72
|
-
const handlers = new Map<string, Handler>();
|
|
73
|
-
const setModel = vi.fn().mockResolvedValue(true);
|
|
74
|
-
const setThinkingLevel = vi.fn();
|
|
75
|
-
const pi = {
|
|
76
|
-
on: vi.fn((event: string, handler: Handler) => handlers.set(event, handler)),
|
|
77
|
-
setModel,
|
|
78
|
-
setThinkingLevel,
|
|
79
|
-
} as unknown as ExtensionAPI;
|
|
80
|
-
const target = templateModel();
|
|
81
|
-
const known = new Set(["celestial-pro", "celestial-max", "celestial-ultra", "deepseek-v4.1-flash"]);
|
|
82
|
-
const ctx = {
|
|
83
|
-
modelRegistry: {
|
|
84
|
-
getAll: () => [templateModel()],
|
|
85
|
-
find: vi.fn((provider: string, id: string) =>
|
|
86
|
-
provider === "tokenin" && known.has(id) ? { ...target, id } : undefined,
|
|
87
|
-
),
|
|
88
|
-
getApiKeyAndHeaders: vi.fn().mockResolvedValue({ ok: true, apiKey: "key", headers: {} }),
|
|
89
|
-
},
|
|
90
|
-
sessionManager: { getBranch: () => branch },
|
|
91
|
-
scopedModels,
|
|
92
|
-
getSystemPrompt: () => "system prompt",
|
|
93
|
-
} as unknown as ExtensionContext;
|
|
94
|
-
autoModelExtension(pi);
|
|
95
|
-
return { handlers, ctx, setModel, setThinkingLevel };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function writeSettings(value: unknown): void {
|
|
99
|
-
writeFileSync(state.settingsPath, typeof value === "string" ? value : JSON.stringify(value), "utf-8");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function jevAnswer(choice: unknown, confidence?: unknown): string {
|
|
103
|
-
return JSON.stringify({ answers: { complexity: { choice, confidence } } });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function textResponse(text: string) {
|
|
107
|
-
return { content: [{ type: "text", text }], stopReason: "stop" } as never;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
beforeEach(() => {
|
|
111
|
-
vi.clearAllMocks();
|
|
112
|
-
state.settingsPath = join(mkdtempSync(join(tmpdir(), "auto-model-")), "settings.json");
|
|
113
|
-
writeSettings({ autoModel: { enabled: true } });
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
afterEach(() => {
|
|
117
|
-
vi.restoreAllMocks();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe("readAutoModelConfig", () => {
|
|
121
|
-
it("falls back to the disabled defaults on a missing or malformed file", () => {
|
|
122
|
-
expect(readAutoModelConfig(join(tmpdir(), "auto-model-does-not-exist", "settings.json"))).toEqual(
|
|
123
|
-
DEFAULT_AUTO_MODEL_CONFIG,
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
writeSettings("{ not json");
|
|
127
|
-
expect(readAutoModelConfig()).toEqual(DEFAULT_AUTO_MODEL_CONFIG);
|
|
128
|
-
|
|
129
|
-
writeSettings([1, 2, 3]);
|
|
130
|
-
expect(readAutoModelConfig()).toEqual(DEFAULT_AUTO_MODEL_CONFIG);
|
|
131
|
-
|
|
132
|
-
writeSettings({ autoModel: "nope" });
|
|
133
|
-
expect(readAutoModelConfig()).toEqual(DEFAULT_AUTO_MODEL_CONFIG);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("merges partial config over the defaults and honors a valid fallback tier", () => {
|
|
137
|
-
writeSettings({
|
|
138
|
-
autoModel: {
|
|
139
|
-
enabled: true,
|
|
140
|
-
classifier: {
|
|
141
|
-
model: "jev-9",
|
|
142
|
-
baseUrl: "https://custom/v1",
|
|
143
|
-
timeoutMs: 1,
|
|
144
|
-
minConfidence: 0.2,
|
|
145
|
-
contextTurns: 2,
|
|
146
|
-
contextChars: "bad",
|
|
147
|
-
},
|
|
148
|
-
tiers: { simple: "tokenin/x" },
|
|
149
|
-
fallbackTier: "reasoning",
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
const config = readAutoModelConfig();
|
|
153
|
-
expect(config.enabled).toBe(true);
|
|
154
|
-
expect(config.classifier).toEqual({
|
|
155
|
-
provider: "tokenin",
|
|
156
|
-
model: "jev-9",
|
|
157
|
-
baseUrl: "https://custom/v1",
|
|
158
|
-
timeoutMs: 1,
|
|
159
|
-
minConfidence: 0.2,
|
|
160
|
-
contextTurns: 2,
|
|
161
|
-
contextChars: DEFAULT_AUTO_MODEL_CONFIG.classifier.contextChars,
|
|
162
|
-
});
|
|
163
|
-
expect(config.tiers).toEqual({ ...DEFAULT_AUTO_MODEL_CONFIG.tiers, simple: "tokenin/x" });
|
|
164
|
-
expect(config.fallbackTier).toBe("reasoning");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("rejects bad field types and an unknown fallback tier", () => {
|
|
168
|
-
writeSettings({
|
|
169
|
-
autoModel: {
|
|
170
|
-
enabled: "yes",
|
|
171
|
-
classifier: { provider: 1, model: "", baseUrl: "", timeoutMs: -1, minConfidence: "x", contextTurns: 0, contextChars: 0 },
|
|
172
|
-
tiers: { nonTier: "ignored" },
|
|
173
|
-
fallbackTier: "nonsense",
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
const config = readAutoModelConfig();
|
|
177
|
-
expect(config.enabled).toBe(false);
|
|
178
|
-
expect(config.classifier.provider).toBe("tokenin");
|
|
179
|
-
expect(config.classifier.baseUrl).toBeUndefined();
|
|
180
|
-
expect(config.fallbackTier).toBe("medium");
|
|
181
|
-
expect(config.tiers).toEqual(DEFAULT_AUTO_MODEL_CONFIG.tiers);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
describe("buildJevPayload", () => {
|
|
186
|
-
it("puts the turns in state and the four tiers in the choice question", () => {
|
|
187
|
-
const payload = buildJevPayload([{ role: "user", text: "hi" }], " be terse ", 100) as any;
|
|
188
|
-
expect(payload.state).toEqual({ conversation: [{ role: "user", text: "hi" }], system_prompt: "be terse" });
|
|
189
|
-
expect(Object.keys(payload.questions.complexity.criteria)).toEqual(["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"]);
|
|
190
|
-
expect(payload.questions.complexity.type).toBe("choice");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("omits an empty or missing system prompt and caps a long one", () => {
|
|
194
|
-
expect((buildJevPayload([], undefined, 10) as any).state.system_prompt).toBeUndefined();
|
|
195
|
-
expect((buildJevPayload([], " ", 10) as any).state.system_prompt).toBeUndefined();
|
|
196
|
-
expect((buildJevPayload([], "x".repeat(50), 10) as any).state.system_prompt).toHaveLength(10);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe("tierFromJevResponse", () => {
|
|
201
|
-
it("reads the chosen tier and rejects unusable or unsure answers", () => {
|
|
202
|
-
expect(tierFromJevResponse(jevAnswer("COMPLEX", 0.9), 0.5)).toBe("complex");
|
|
203
|
-
expect(tierFromJevResponse(jevAnswer("simple"), 0.5)).toBe("simple");
|
|
204
|
-
expect(tierFromJevResponse(jevAnswer("REASONING", 0.5), 0.5)).toBe("reasoning");
|
|
205
|
-
expect(tierFromJevResponse(jevAnswer("MEDIUM", 0.4), 0.5)).toBeUndefined();
|
|
206
|
-
expect(tierFromJevResponse(jevAnswer("MEDIUM", "high"), 0.5)).toBe("medium");
|
|
207
|
-
expect(tierFromJevResponse(jevAnswer("UNKNOWN", 0.9), 0.5)).toBeUndefined();
|
|
208
|
-
expect(tierFromJevResponse(jevAnswer(7, 0.9), 0.5)).toBeUndefined();
|
|
209
|
-
expect(tierFromJevResponse("{ bad", 0.5)).toBeUndefined();
|
|
210
|
-
expect(tierFromJevResponse(JSON.stringify({ answers: null }), 0.5)).toBeUndefined();
|
|
211
|
-
expect(tierFromJevResponse(JSON.stringify({ answers: { complexity: 1 } }), 0.5)).toBeUndefined();
|
|
212
|
-
expect(tierFromJevResponse("42", 0.5)).toBeUndefined();
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
describe("parseModelRef", () => {
|
|
217
|
-
it("splits provider, id, and an optional thinking level", () => {
|
|
218
|
-
expect(parseModelRef("tokenin/celestial-max")).toEqual({ provider: "tokenin", id: "celestial-max" });
|
|
219
|
-
expect(parseModelRef("tokenin/celestial-max:max")).toEqual({
|
|
220
|
-
provider: "tokenin",
|
|
221
|
-
id: "celestial-max",
|
|
222
|
-
thinking: "max",
|
|
223
|
-
});
|
|
224
|
-
expect(parseModelRef("no-slash")).toBeUndefined();
|
|
225
|
-
expect(parseModelRef("/leading")).toBeUndefined();
|
|
226
|
-
expect(parseModelRef("trailing/")).toBeUndefined();
|
|
227
|
-
expect(parseModelRef("tokenin/:max")).toEqual({ provider: "tokenin", id: ":max" });
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
describe("buildConversation", () => {
|
|
232
|
-
it("keeps the current ask last and pulls prior user turns under the budget", () => {
|
|
233
|
-
const branch = [
|
|
234
|
-
userEntry("oldest", "u1"),
|
|
235
|
-
assistantEntry("ignored narration"),
|
|
236
|
-
{ id: "x", type: "model_change" } as unknown as SessionEntry,
|
|
237
|
-
userEntry("previous", "u2"),
|
|
238
|
-
];
|
|
239
|
-
const turns = buildConversation("current", branch, { contextTurns: 4, contextChars: 1000 });
|
|
240
|
-
expect(turns.map((t) => t.text)).toEqual(["oldest", "previous", "current"]);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it("drops empty turns, array-content text, and enforces turn and char caps", () => {
|
|
244
|
-
const branch = [
|
|
245
|
-
userEntry("", "e1"),
|
|
246
|
-
userEntry("", "e2", [{ type: "image", data: "x" }]),
|
|
247
|
-
userEntry("", "e3", 42),
|
|
248
|
-
userEntry("", "e4", "plain string"),
|
|
249
|
-
userEntry("kept", "u2"),
|
|
250
|
-
];
|
|
251
|
-
expect(buildConversation(" ", branch, { contextTurns: 4, contextChars: 100 })).toEqual([
|
|
252
|
-
{ role: "user", text: "plain string" },
|
|
253
|
-
{ role: "user", text: "kept" },
|
|
254
|
-
]);
|
|
255
|
-
expect(buildConversation("current", branch, { contextTurns: 2, contextChars: 100 }).map((t) => t.text)).toEqual([
|
|
256
|
-
"kept",
|
|
257
|
-
"current",
|
|
258
|
-
]);
|
|
259
|
-
const truncated = buildConversation("abcdef", [], { contextTurns: 4, contextChars: 3 });
|
|
260
|
-
expect(truncated).toEqual([{ role: "user", text: "def" }]);
|
|
261
|
-
const exhausted = buildConversation("abc", [userEntry("later", "u1")], { contextTurns: 4, contextChars: 3 });
|
|
262
|
-
expect(exhausted).toEqual([{ role: "user", text: "abc" }]);
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
describe("classifierModel", () => {
|
|
267
|
-
it("inherits the provider template, or uses an explicit baseUrl", () => {
|
|
268
|
-
const registry = { getAll: () => [templateModel()] } as unknown as ExtensionContext["modelRegistry"];
|
|
269
|
-
const model = classifierModel(registry, DEFAULT_AUTO_MODEL_CONFIG.classifier);
|
|
270
|
-
expect(model).toMatchObject({
|
|
271
|
-
id: "jev-1.13",
|
|
272
|
-
provider: "tokenin",
|
|
273
|
-
baseUrl: "https://lite.andlet.me/v1",
|
|
274
|
-
contextWindow: 393216,
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
const bare = { getAll: () => [] } as unknown as ExtensionContext["modelRegistry"];
|
|
278
|
-
expect(classifierModel(bare, DEFAULT_AUTO_MODEL_CONFIG.classifier)).toBeUndefined();
|
|
279
|
-
const override = classifierModel(bare, { ...DEFAULT_AUTO_MODEL_CONFIG.classifier, baseUrl: "https://x/v1" });
|
|
280
|
-
expect(override).toMatchObject({ baseUrl: "https://x/v1", reasoning: false, input: ["text"] });
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
describe("classifyTier", () => {
|
|
285
|
-
const config: AutoModelConfig = { ...DEFAULT_AUTO_MODEL_CONFIG, enabled: true };
|
|
286
|
-
|
|
287
|
-
function ctxWith(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
|
|
288
|
-
return {
|
|
289
|
-
modelRegistry: {
|
|
290
|
-
getAll: () => [templateModel()],
|
|
291
|
-
getApiKeyAndHeaders: vi.fn().mockResolvedValue({ ok: true, apiKey: "key", headers: {} }),
|
|
292
|
-
},
|
|
293
|
-
sessionManager: { getBranch: () => [] },
|
|
294
|
-
getSystemPrompt: () => "sys",
|
|
295
|
-
...overrides,
|
|
296
|
-
} as unknown as ExtensionContext;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
it("returns the tier Jev answers", async () => {
|
|
300
|
-
completeMock.mockResolvedValue(textResponse(jevAnswer("COMPLEX", 0.9)));
|
|
301
|
-
await expect(classifyTier(ctxWith(), config, "fix the parser")).resolves.toBe("complex");
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it("returns undefined when the classifier is unusable", async () => {
|
|
305
|
-
const noTemplate = { modelRegistry: { getAll: () => [] } } as unknown as ExtensionContext;
|
|
306
|
-
await expect(classifyTier(noTemplate, config, "hi")).resolves.toBeUndefined();
|
|
307
|
-
|
|
308
|
-
const noAuth = ctxWith({
|
|
309
|
-
modelRegistry: {
|
|
310
|
-
getAll: () => [templateModel()],
|
|
311
|
-
getApiKeyAndHeaders: vi.fn().mockResolvedValue({ ok: false, error: "no key" }),
|
|
312
|
-
} as unknown as ExtensionContext["modelRegistry"],
|
|
313
|
-
});
|
|
314
|
-
await expect(classifyTier(noAuth, config, "hi")).resolves.toBeUndefined();
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
describe("auto-model extension", () => {
|
|
319
|
-
it("registers the session, model, and input handlers", () => {
|
|
320
|
-
const { handlers } = createHarness();
|
|
321
|
-
expect([...handlers.keys()]).toEqual(["session_start", "model_select", "input"]);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it("routes an idle prompt to the classified tier model", async () => {
|
|
325
|
-
completeMock.mockResolvedValue(textResponse(jevAnswer("COMPLEX", 0.9)));
|
|
326
|
-
const { handlers, ctx, setModel, setThinkingLevel } = createHarness();
|
|
327
|
-
await handlers.get("input")!({ type: "input", text: "fix the parser", source: "interactive" }, ctx);
|
|
328
|
-
expect(setModel).toHaveBeenCalledWith(expect.objectContaining({ provider: "tokenin" }));
|
|
329
|
-
expect(setThinkingLevel).not.toHaveBeenCalled();
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it("applies a per-tier thinking level when the mapping carries one", async () => {
|
|
333
|
-
writeSettings({ autoModel: { enabled: true, tiers: { complex: "tokenin/celestial-pro:max" } } });
|
|
334
|
-
completeMock.mockResolvedValue(textResponse(jevAnswer("COMPLEX", 0.9)));
|
|
335
|
-
const { handlers, ctx, setThinkingLevel } = createHarness();
|
|
336
|
-
await handlers.get("input")!({ type: "input", text: "fix", source: "interactive" }, ctx);
|
|
337
|
-
expect(setThinkingLevel).toHaveBeenCalledWith("max");
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it("skips non-eligible input and disabled routing", async () => {
|
|
341
|
-
const { handlers, ctx, setModel } = createHarness();
|
|
342
|
-
const input = handlers.get("input")!;
|
|
343
|
-
await input({ type: "input", text: "hi", source: "extension" }, ctx);
|
|
344
|
-
await input({ type: "input", text: "hi", source: "interactive", streamingBehavior: "steer" }, ctx);
|
|
345
|
-
await input({ type: "input", text: " ", source: "interactive" }, ctx);
|
|
346
|
-
await input({ type: "input", text: "/model", source: "interactive" }, ctx);
|
|
347
|
-
writeSettings({ autoModel: { enabled: false } });
|
|
348
|
-
await input({ type: "input", text: "hi", source: "interactive" }, ctx);
|
|
349
|
-
expect(setModel).not.toHaveBeenCalled();
|
|
350
|
-
expect(completeMock).not.toHaveBeenCalled();
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it("suspends after a manual model change and resumes on the next session", async () => {
|
|
354
|
-
completeMock.mockResolvedValue(textResponse(jevAnswer("SIMPLE", 0.9)));
|
|
355
|
-
const { handlers, ctx, setModel } = createHarness();
|
|
356
|
-
await handlers.get("model_select")!({ type: "model_select", model: templateModel(), source: "cycle" }, ctx);
|
|
357
|
-
await handlers.get("input")!({ type: "input", text: "hi", source: "interactive" }, ctx);
|
|
358
|
-
expect(setModel).not.toHaveBeenCalled();
|
|
359
|
-
|
|
360
|
-
handlers.get("session_start")!({ type: "session_start" }, ctx);
|
|
361
|
-
await handlers.get("input")!({ type: "input", text: "hi", source: "interactive" }, ctx);
|
|
362
|
-
expect(setModel).toHaveBeenCalledTimes(1);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it("ignores its own routing switch and session restore", async () => {
|
|
366
|
-
completeMock.mockResolvedValue(textResponse(jevAnswer("SIMPLE", 0.9)));
|
|
367
|
-
const { handlers, ctx, setModel } = createHarness();
|
|
368
|
-
const modelSelect = handlers.get("model_select")!;
|
|
369
|
-
modelSelect({ type: "model_select", model: templateModel(), source: "restore" }, ctx);
|
|
370
|
-
// The model_select emitted by our own setModel must not suspend the next prompt.
|
|
371
|
-
setModel.mockImplementation(async () => {
|
|
372
|
-
modelSelect({ type: "model_select", model: templateModel(), source: "set" }, ctx);
|
|
373
|
-
return true;
|
|
374
|
-
});
|
|
375
|
-
await handlers.get("input")!({ type: "input", text: "hi", source: "interactive" }, ctx);
|
|
376
|
-
await handlers.get("input")!({ type: "input", text: "again", source: "interactive" }, ctx);
|
|
377
|
-
expect(setModel).toHaveBeenCalledTimes(2);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it("keeps the current model when no mapping resolves or the target is out of scope", async () => {
|
|
381
|
-
completeMock.mockResolvedValue(textResponse(jevAnswer("COMPLEX", 0.9)));
|
|
382
|
-
const unparsable = createHarness();
|
|
383
|
-
writeSettings({ autoModel: { enabled: true, tiers: { complex: "no-slash-here" } } });
|
|
384
|
-
await unparsable.handlers.get("input")!({ type: "input", text: "go", source: "interactive" }, unparsable.ctx);
|
|
385
|
-
expect(unparsable.setModel).not.toHaveBeenCalled();
|
|
386
|
-
|
|
387
|
-
writeSettings({ autoModel: { enabled: true, tiers: { complex: "tokenin/missing" } } });
|
|
388
|
-
const missing = createHarness();
|
|
389
|
-
await missing.handlers.get("input")!({ type: "input", text: "go", source: "interactive" }, missing.ctx);
|
|
390
|
-
expect(missing.setModel).not.toHaveBeenCalled();
|
|
391
|
-
|
|
392
|
-
const scoped = createHarness([], [{ model: templateModel("other", "x") }]);
|
|
393
|
-
writeSettings({ autoModel: { enabled: true } });
|
|
394
|
-
await scoped.handlers.get("input")!({ type: "input", text: "go", source: "interactive" }, scoped.ctx);
|
|
395
|
-
expect(scoped.setModel).not.toHaveBeenCalled();
|
|
396
|
-
|
|
397
|
-
const sameProvider = createHarness([], [{ model: templateModel("tokenin", "other-id") }]);
|
|
398
|
-
await sameProvider.handlers.get("input")!({ type: "input", text: "go", source: "interactive" }, sameProvider.ctx);
|
|
399
|
-
expect(sameProvider.setModel).not.toHaveBeenCalled();
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("falls back to the fallback tier and survives classifier and switch failures", async () => {
|
|
403
|
-
writeSettings({ autoModel: { enabled: true, fallbackTier: "medium", tiers: { medium: "tokenin/celestial-pro" } } });
|
|
404
|
-
completeMock.mockRejectedValue(new Error("network down"));
|
|
405
|
-
const failed = createHarness();
|
|
406
|
-
await failed.handlers.get("input")!({ type: "input", text: "go", source: "interactive" }, failed.ctx);
|
|
407
|
-
expect(failed.setModel).toHaveBeenCalledWith(expect.objectContaining({ id: "celestial-pro" }));
|
|
408
|
-
|
|
409
|
-
writeSettings({ autoModel: { enabled: true, tiers: { medium: "tokenin/celestial-pro" } } });
|
|
410
|
-
completeMock.mockResolvedValue(textResponse(jevAnswer("COMPLEX", 0.9)));
|
|
411
|
-
const refused = createHarness();
|
|
412
|
-
refused.setModel.mockResolvedValue(false);
|
|
413
|
-
await refused.handlers.get("input")!({ type: "input", text: "go", source: "interactive" }, refused.ctx);
|
|
414
|
-
expect(refused.setThinkingLevel).not.toHaveBeenCalled();
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it("serializes concurrent prompts so only one classification runs", async () => {
|
|
418
|
-
let release: (() => void) | undefined;
|
|
419
|
-
completeMock.mockImplementation(
|
|
420
|
-
() =>
|
|
421
|
-
new Promise((resolve) => {
|
|
422
|
-
release = () => resolve(textResponse(jevAnswer("SIMPLE", 0.9)));
|
|
423
|
-
}) as never,
|
|
424
|
-
);
|
|
425
|
-
const { handlers, ctx, setModel } = createHarness();
|
|
426
|
-
const input = handlers.get("input")!;
|
|
427
|
-
const first = input({ type: "input", text: "one", source: "interactive" }, ctx);
|
|
428
|
-
const second = input({ type: "input", text: "two", source: "interactive" }, ctx);
|
|
429
|
-
await vi.waitFor(() => expect(completeMock).toHaveBeenCalledTimes(1));
|
|
430
|
-
release?.();
|
|
431
|
-
await Promise.all([first, second]);
|
|
432
|
-
expect(setModel).toHaveBeenCalledTimes(1);
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it("exposes the tier list used by the payload", () => {
|
|
436
|
-
expect(TIERS).toEqual(["simple", "medium", "complex", "reasoning"]);
|
|
437
|
-
});
|
|
438
|
-
});
|