Skip to content

Commit b825aa8

Browse files
committed
fix: restore app vitest coverage and last-used model wiring
Put desktop, pythinker-web, and vscode projects back in the root vitest matrix. Port last-used model events onto the live llm-adapter ModelService so provider detail routes stop throwing during initialize. Keep exact modelCatalog assertions, fix web load test deps, and align vscode bridge/telemetry/migration fixtures with the reconciled APIs.
1 parent fb015cc commit b825aa8

12 files changed

Lines changed: 85 additions & 42 deletions

File tree

‎apps/pythinker-web/test/composer-toolbar.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ describe('Composer toolbar overflow valves', () => {
404404

405405
expect(wrapper.get('.composer-card').classes()).not.toContain('labels-collapsed');
406406
expect(wrapper.get('.model-pill').classes()).not.toContain('icon-only');
407-
expect(wrapper.get('.perm-pill-label').text()).toBe('Manual');
407+
expect(wrapper.get('.perm-pill-label').text()).toBe('Always Ask');
408408
});
409409

410410
it('opens thinking effort separately and closes it with Escape', async () => {

‎apps/pythinker-web/test/workspace-state.test.ts‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,20 @@ function createDeps(): UseWorkspaceStateDeps {
138138
return {
139139
taskPoller: {},
140140
sideChat: { clearSideChatForSession: vi.fn() },
141-
modelProvider: { resolveThinkingForPrompt: async () => undefined },
141+
modelProvider: {
142+
resolveThinkingForPrompt: async () => undefined,
143+
loadModels: vi.fn().mockResolvedValue(undefined),
144+
},
142145
pushOperationFailure: vi.fn(),
143146
activity: computed(() => 'running'),
144147
sessionsKnownEmpty: new Set(),
148+
bootStage: ref('idle'),
149+
bootRetries: ref(0),
150+
selectedDiffPath: ref(null),
151+
fileDiffLines: ref([]),
152+
fileDiffLoading: ref(false),
153+
initialized: ref(true),
154+
connectIssue: ref(null),
145155
setSessions: vi.fn(),
146156
updateSession: vi.fn(),
147157
upsertSessionFront: vi.fn(),
@@ -174,10 +184,6 @@ function createDeps(): UseWorkspaceStateDeps {
174184
resetFastMoon: vi.fn(),
175185
getExpertTalkArmId: () => undefined,
176186
onExpertTalkPromptAccepted: vi.fn(),
177-
initialized: ref(true),
178-
selectedDiffPath: ref(null),
179-
fileDiffLines: ref([]),
180-
fileDiffLoading: ref(false),
181187
} as unknown as UseWorkspaceStateDeps;
182188
}
183189

@@ -1368,9 +1374,9 @@ describe('useWorkspaceState — first-load auth gate', () => {
13681374
): UseWorkspaceStateDeps {
13691375
return {
13701376
...createDeps(),
1371-
modelProvider: { loadModels: vi.fn().mockResolvedValue(undefined) },
13721377
initialized,
13731378
connectIssue,
1379+
bootStage: ref('auth'),
13741380
} as unknown as UseWorkspaceStateDeps;
13751381
}
13761382

‎apps/vscode/src/migration/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { LegacyMigrationManager } from './legacy-migration.manager';

‎apps/vscode/test/bridge-handler.test.ts‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ describe("Webview RPC boundary (validates requests before host dispatch)", () =>
319319
] as never);
320320

321321
const result = await bridge.handle(
322-
{ id: "rpc-1", method: Methods.GetPythinkerSessions },
322+
{ id: "rpc-1", method: Methods.GetSessions },
323323
"view-1",
324324
);
325325

@@ -349,7 +349,7 @@ describe("Webview RPC boundary (validates requests before host dispatch)", () =>
349349
const result = await bridge.handle(
350350
{
351351
id: "rpc-1",
352-
method: Methods.ForkPythinkerSession,
352+
method: Methods.ForkSession,
353353
params: { sessionId: "session-1", turnIndex: 0 },
354354
},
355355
"view-1",
@@ -382,7 +382,7 @@ describe("Webview RPC boundary (validates requests before host dispatch)", () =>
382382
const result = await bridge.handle(
383383
{
384384
id: "rpc-1",
385-
method: Methods.ForkPythinkerSession,
385+
method: Methods.ForkSession,
386386
params: { sessionId: "session-1", turnIndex: 0 },
387387
},
388388
"view-1",
@@ -407,7 +407,7 @@ describe("Webview RPC boundary (validates requests before host dispatch)", () =>
407407
const result = await bridge.handle(
408408
{
409409
id: "rpc-1",
410-
method: Methods.ForkPythinkerSession,
410+
method: Methods.ForkSession,
411411
params: { sessionId: "session-1", turnIndex: 0 },
412412
},
413413
"view-1",
@@ -441,8 +441,8 @@ describe("Webview RPC boundary (validates requests before host dispatch)", () =>
441441
const result = await bridge.handle(
442442
{
443443
id: "rpc-1",
444-
method: Methods.LoadPythinkerSessionHistory,
445-
params: { pythinkerSessionId: "session-1" },
444+
method: Methods.LoadSessionHistory,
445+
params: { sessionId: "session-1" },
446446
},
447447
"view-1",
448448
);
@@ -467,8 +467,8 @@ describe("Webview RPC boundary (validates requests before host dispatch)", () =>
467467
const failed = await bridge.handle(
468468
{
469469
id: "rpc-1",
470-
method: Methods.LoadPythinkerSessionHistory,
471-
params: { pythinkerSessionId: "session-1" },
470+
method: Methods.LoadSessionHistory,
471+
params: { sessionId: "session-1" },
472472
},
473473
"view-1",
474474
);

‎apps/vscode/test/pythinker-harness.integration.test.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -364,15 +364,16 @@ async function runSlash(
364364
}
365365

366366
describe("VS Code Pythinker harness integration (shares one in-process SDK home)", () => {
367-
it("only intercepts released slash commands and user-invoked skills", () => {
368-
expect(parseHostSlashCommand("/plan on")).toEqual({ name: "plan", args: "on", raw: "/plan on" });
369-
expect(parseHostSlashCommand(" /skill:review carefully ")).toEqual({
367+
it("only intercepts released slash commands and user-invoked skills", async () => {
368+
expect(await parseHostSlashCommand("/plan on")).toEqual({ name: "plan", args: "on", raw: "/plan on" });
369+
expect(await parseHostSlashCommand(" /skill:review carefully ")).toEqual({
370370
name: "skill:review",
371371
args: "carefully",
372372
raw: "/skill:review carefully",
373+
skillName: "review",
373374
});
374-
expect(parseHostSlashCommand("/not-a-host-command")).toBeUndefined();
375-
expect(parseHostSlashCommand([{ type: "text", text: "/clear" }])).toBeUndefined();
375+
expect(await parseHostSlashCommand("/not-a-host-command")).toBeUndefined();
376+
expect(await parseHostSlashCommand([{ type: "text", text: "/clear" }])).toBeUndefined();
376377
});
377378

378379
it("combines the released slash commands with user-activatable workspace skills", async () => {

‎apps/vscode/test/settings-store.test.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
14
/**
25
* Scenario: Webview state crosses the VS Code bridge during settings changes, MCP edits, and chat failures.
36
* Responsibilities: model metadata and selections remain provider-aware; MCP edits stay lossless; chat errors recover visibly.
@@ -202,7 +205,10 @@ describe("Webview MCP update bridge", () => {
202205
const posted: unknown[] = [];
203206
let receiveMessage: ((event: { data: unknown }) => void) | undefined;
204207
vi.stubGlobal("document", {
205-
body: { getAttribute: () => "mcp-test-view" },
208+
body: {
209+
dataset: { webviewid: "mcp-test-view" },
210+
getAttribute: () => "mcp-test-view",
211+
},
206212
});
207213
vi.stubGlobal("window", {
208214
addEventListener: (_type: string, listener: (event: { data: unknown }) => void) => {

‎apps/vscode/test/telemetry.test.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,6 @@ describe("extension telemetry composition root", () => {
123123
model: "kimi-k2",
124124
});
125125
expect(options["endpoint"]).toBeTypeOf("function");
126-
expect(options["getAccessToken"]).toBeTypeOf("function");
127-
await expect((options["getAccessToken"] as () => Promise<string | null>)()).resolves.toBe("token-1");
128126
expect(host.shouldEnableTelemetry).toHaveBeenCalledWith({ enabled: true });
129127
});
130128

@@ -144,9 +142,8 @@ describe("extension telemetry composition root", () => {
144142
expect(endpoint()).toBe("https://telemetry-logs.example.net/v1/event");
145143
expect(host.resolvePythinkerRegion).toHaveBeenCalledWith(
146144
expect.objectContaining({
147-
configuredOAuthHost: "https://auth.kimi.ai",
148-
configuredOAuthKey: "oauth/pythinker-code-global",
149145
homeDir: "/pythinker-home",
146+
readMarker: true,
150147
}),
151148
);
152149
});

‎packages/agent-core-v2/src/app/kosongConfig/kosongConfigService.ts‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,10 @@ export class KosongConfigService extends Disposable implements IKosongConfigServ
102102
);
103103
break;
104104
case MODELS_SECTION:
105-
const models = this.models as {
106-
loadAll: (section: ModelsSection, defaultModel: string | undefined, lastUsed?: string | undefined) => void;
107-
getDefaultModel: () => string | undefined;
108-
getLastUsedModel?: () => string | undefined;
109-
};
110-
models.loadAll(
105+
this.models.loadAll(
111106
(e.value as ModelsSection | undefined) ?? {},
112-
models.getDefaultModel(),
113-
models.getLastUsedModel?.(),
107+
this.models.getDefaultModel(),
108+
this.models.getLastUsedModel(),
114109
);
115110
break;
116111
case DEFAULT_PROVIDER_SECTION:

‎packages/agent-core-v2/src/llm-adapter/model/model-service.ts‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export class ModelService extends Disposable implements IModelService {
2020

2121
private models: Readonly<Record<string, ModelRecord>> = {};
2222
private defaultModel: string | undefined;
23+
private lastUsedModel: string | undefined;
2324
private hydrated = false;
2425
private resolveReady!: () => void;
2526
readonly ready: Promise<void> = new Promise<void>((resolve) => {
@@ -36,6 +37,11 @@ export class ModelService extends Disposable implements IModelService {
3637
);
3738
readonly onDidChangeDefaultModel: Event<DefaultModelChangedEvent & IWaitUntil> =
3839
this._onDidChangeDefaultModel.event;
40+
private readonly _onDidChangeLastUsedModel = this._register(
41+
new AsyncEmitter<DefaultModelChangedEvent & IWaitUntil>(),
42+
);
43+
readonly onDidChangeLastUsedModel: Event<DefaultModelChangedEvent & IWaitUntil> =
44+
this._onDidChangeLastUsedModel.event;
3945

4046
get(id: string): ModelRecord | undefined {
4147
return this.models[id];
@@ -49,9 +55,18 @@ export class ModelService extends Disposable implements IModelService {
4955
return this.defaultModel;
5056
}
5157

52-
loadAll(models: ModelsSection, defaultModel: string | undefined): void {
58+
getLastUsedModel(): string | undefined {
59+
return this.lastUsedModel;
60+
}
61+
62+
loadAll(
63+
models: ModelsSection,
64+
defaultModel: string | undefined,
65+
lastUsedModel?: string | undefined,
66+
): void {
5367
void this.applyRecords(models);
5468
void this.applyDefaultModel(defaultModel);
69+
void this.applyLastUsedModel(lastUsedModel);
5570
if (!this.hydrated) {
5671
this.hydrated = true;
5772
this.resolveReady();
@@ -81,6 +96,11 @@ export class ModelService extends Disposable implements IModelService {
8196
await this.applyDefaultModel(id);
8297
}
8398

99+
async setLastUsedModel(id: string | undefined): Promise<void> {
100+
await this.ready;
101+
await this.applyLastUsedModel(id);
102+
}
103+
84104
private async applyRecords(next: Readonly<Record<string, ModelRecord>>): Promise<void> {
85105
const diff = diffRecords(this.models, next);
86106
if (isEmptyDiff(diff)) return;
@@ -93,6 +113,12 @@ export class ModelService extends Disposable implements IModelService {
93113
this.defaultModel = id;
94114
await this._onDidChangeDefaultModel.fireAsync({ id }, NO_ABORT);
95115
}
116+
117+
private async applyLastUsedModel(id: string | undefined): Promise<void> {
118+
if (this.lastUsedModel === id) return;
119+
this.lastUsedModel = id;
120+
await this._onDidChangeLastUsedModel.fireAsync({ id }, NO_ABORT);
121+
}
96122
}
97123

98124
registerScopedService(LifecycleScope.App, IModelService, ModelService, ScopeActivation.OnScopeCreated, 'model');

‎packages/agent-core-v2/src/llm-adapter/model/model.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,21 @@ export interface IModelService {
6666
readonly ready: Promise<void>;
6767
readonly onDidChangeModels: Event<ModelsChangedEvent & IWaitUntil>;
6868
readonly onDidChangeDefaultModel: Event<DefaultModelChangedEvent & IWaitUntil>;
69+
readonly onDidChangeLastUsedModel: Event<DefaultModelChangedEvent & IWaitUntil>;
6970
get(id: string): ModelRecord | undefined;
7071
list(): Readonly<Record<string, ModelRecord>>;
7172
getDefaultModel(): string | undefined;
73+
getLastUsedModel(): string | undefined;
7274
set(id: string, model: ModelRecord): Promise<void>;
7375
delete(id: string): Promise<void>;
74-
loadAll(models: ModelsSection, defaultModel: string | undefined): void;
76+
loadAll(
77+
models: ModelsSection,
78+
defaultModel: string | undefined,
79+
lastUsedModel?: string | undefined,
80+
): void;
7581
replaceAll(models: ModelsSection): Promise<void>;
7682
setDefaultModel(id: string | undefined): Promise<void>;
83+
setLastUsedModel(id: string | undefined): Promise<void>;
7784
}
7885

7986
export const IModelService: ServiceIdentifier<IModelService> =

0 commit comments

Comments
 (0)