qwen-code/packages/vscode-ide-companion/src/services/qwenAgentManager.test.ts
易良 fb91acdf25
fix(vscode): force fresh ACP session on new-session action (#2874)
* fix(vscode-ide-companion/session): force fresh sessions for new chats

Ensure explicit new-session actions bypass active ACP session reuse so the VS Code sidebar clears context correctly.

Add regression coverage for the agent manager and webview new-session entry points.

* fix(vscode): remove core runtime imports from webview bundle

Replace the runtime import of `isSupportedImageMimeType` from
`@qwen-code/qwen-code-core` with a local `SUPPORTED_PASTED_IMAGE_MIME_TYPES`
set in the vscode-ide-companion package. The webview is bundled for a
browser environment where Node.js-only core modules are unavailable,
so keeping the MIME list local avoids esbuild failures during development.

Added tests to verify the local list stays aligned with core and that
the webview bundle does not contain core runtime imports.

* fix(vscode): reset context usage display on new session (#2847)

The webview context-usage bar did not clear when the user started a new
session because the old code always fell back to DEFAULT_TOKEN_LIMIT,
producing a stale percentage even after usageStats and modelInfo were
both cleared.

Key changes:
- Extract `knownTokenLimit()` in core/tokenLimits.ts that returns
  `undefined` for unrecognized models instead of a default, keeping
  `tokenLimit()` behavior unchanged.
- In acpModelInfo.ts, derive `_meta.contextLimit` from the known-model
  table when the ACP payload omits a numeric limit.
- Extract `computeContextUsage()` into its own module, which returns
  `null` when no trusted numeric limit is available — the UI then
  correctly hides the context bar.
- Remove the `@qwen-code/qwen-code-core` runtime import from App.tsx
  so the webview bundle stays free of Node-only dependencies.

Closes #2847

* fix(vscode-ide-companion/webview): reset state on new session

* test(vscode-ide-companion/webview): cover stale conversation reset

* fix(vscode): remove webview token limit runtime import

* fix(vscode): fully reset state for explicit new session

* fix(vscode-ide-companion/webview): clear residual state on new session

---------

Co-authored-by: tanzhenxin <tanzhenxing1987@gmail.com>
2026-04-11 10:16:16 +08:00

176 lines
5.1 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it, vi } from 'vitest';
import {
extractSessionListItems,
QwenAgentManager,
} from './qwenAgentManager.js';
import type { ModelInfo } from '@agentclientprotocol/sdk';
vi.mock('vscode', () => ({
window: {
showInformationMessage: vi.fn(),
showWarningMessage: vi.fn(),
showErrorMessage: vi.fn(),
},
}));
describe('extractSessionListItems', () => {
it('returns sessions array from the "sessions" field', () => {
const items = extractSessionListItems({
sessions: [{ sessionId: 'session-1' }],
});
expect(items).toEqual([{ sessionId: 'session-1' }]);
});
it('returns items array from the legacy "items" field', () => {
const items = extractSessionListItems({
items: [{ sessionId: 'session-2' }],
});
expect(items).toEqual([{ sessionId: 'session-2' }]);
});
it('prefers "sessions" over "items" when both are present', () => {
const items = extractSessionListItems({
sessions: [{ sessionId: 'from-sessions' }],
items: [{ sessionId: 'from-items' }],
});
expect(items).toEqual([{ sessionId: 'from-sessions' }]);
});
it('returns empty array for null/undefined input', () => {
expect(extractSessionListItems(null)).toEqual([]);
expect(extractSessionListItems(undefined)).toEqual([]);
});
it('returns empty array for non-object input', () => {
expect(extractSessionListItems('string')).toEqual([]);
expect(extractSessionListItems(42)).toEqual([]);
});
it('returns empty array when neither field is an array', () => {
expect(extractSessionListItems({ sessions: 'not-array' })).toEqual([]);
expect(extractSessionListItems({ items: 123 })).toEqual([]);
expect(extractSessionListItems({})).toEqual([]);
});
});
describe('QwenAgentManager.setModelFromUi', () => {
it('emits the selected model metadata from the available models list', async () => {
const manager = new QwenAgentManager();
const onModelChanged = vi.fn();
manager.onModelChanged(onModelChanged);
const selectedModel: ModelInfo = {
modelId: 'qwen3-coder-plus',
name: 'Qwen3 Coder Plus',
_meta: {
contextLimit: 262144,
},
};
(
manager as unknown as {
baselineAvailableModels: ModelInfo[];
}
).baselineAvailableModels = [
{
modelId: 'qwen3-coder-base',
name: 'Qwen3 Coder Base',
_meta: {
contextLimit: 131072,
},
},
selectedModel,
];
(
manager as unknown as {
connection: {
setModel: (modelId: string) => Promise<{ modelId: string }>;
};
}
).connection = {
setModel: vi.fn().mockResolvedValue({ modelId: selectedModel.modelId }),
};
await manager.setModelFromUi(selectedModel.modelId);
expect(onModelChanged).toHaveBeenCalledWith(selectedModel);
});
});
describe('QwenAgentManager.createNewSession', () => {
it('creates a fresh ACP session when explicitly requested even if one is already active', async () => {
const manager = new QwenAgentManager();
const connection = {
currentSessionId: 'session-1',
newSession: vi.fn().mockImplementation(async () => {
connection.currentSessionId = 'session-2';
return { sessionId: 'session-2' };
}),
authenticate: vi.fn(),
};
(
manager as unknown as {
connection: typeof connection;
}
).connection = connection;
const newSessionId = await manager.createNewSession('/workspace', {
forceNew: true,
} as never);
expect(connection.newSession).toHaveBeenCalledWith('/workspace');
expect(newSessionId).toBe('session-2');
});
it('creates a distinct fresh session after an in-flight bootstrap when forceNew is requested', async () => {
const manager = new QwenAgentManager();
const connection = {
currentSessionId: null as string | null,
newSession: vi.fn().mockImplementation(async () => {
connection.currentSessionId = 'session-2';
return { sessionId: 'session-2' };
}),
authenticate: vi.fn(),
};
let resolveBootstrap: ((value: string | null) => void) | undefined;
const bootstrapSession = new Promise<string | null>((resolve) => {
resolveBootstrap = (value) => {
connection.currentSessionId = value;
resolve(value);
};
});
(
manager as unknown as {
connection: typeof connection;
sessionCreateInFlight: Promise<string | null> | null;
}
).connection = connection;
(
manager as unknown as {
sessionCreateInFlight: Promise<string | null> | null;
}
).sessionCreateInFlight = bootstrapSession;
const newSessionPromise = manager.createNewSession('/workspace', {
forceNew: true,
} as never);
expect(connection.newSession).not.toHaveBeenCalled();
resolveBootstrap?.('session-1');
await expect(newSessionPromise).resolves.toBe('session-2');
expect(connection.newSession).toHaveBeenCalledTimes(1);
expect(connection.newSession).toHaveBeenCalledWith('/workspace');
});
});