diff --git a/integration-tests/channel-plugin.test.ts b/integration-tests/channel-plugin.test.ts index c5ea92c06e1..9566af326ca 100644 --- a/integration-tests/channel-plugin.test.ts +++ b/integration-tests/channel-plugin.test.ts @@ -31,16 +31,15 @@ import { fileURLToPath } from 'node:url'; import { mkdirSync } from 'node:fs'; // Import from the monorepo channel packages -import { - AcpBridge, - SessionRouter, -} from '../packages/channels/base/dist/index.js'; -import type { ChannelConfig } from '../packages/channels/base/dist/index.js'; +import { AcpBridge, SessionRouter } from '@qwen-code/channel-base'; import { MockPluginChannel, createMockServer, } from '../packages/channels/plugin-example/src/index.js'; -import type { MockServerHandle } from '../packages/channels/plugin-example/src/index.js'; +import type { + MockServerHandle, + MockPluginConfig, +} from '../packages/channels/plugin-example/src/index.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const CLI_PATH = join(__dirname, '..', 'dist', 'cli.js'); @@ -74,7 +73,9 @@ describe('Channel Plugin (Mock WebSocket E2E)', () => { await bridge.start(); // 3. Create and connect MockPluginChannel via WebSocket - const config: ChannelConfig & Record = { + // MockPluginConfig, not ChannelConfig: the constructor below requires + // `serverWsUrl`, and typing the literal as the base interface erased it. + const config: MockPluginConfig & Record = { type: 'plugin-example', token: '', senderPolicy: 'open', @@ -82,6 +83,7 @@ describe('Channel Plugin (Mock WebSocket E2E)', () => { sessionScope: 'user', cwd: testDir, groupPolicy: 'disabled', + dmPolicy: 'open', groups: {}, serverWsUrl: server.wsUrl, }; diff --git a/integration-tests/cli/file-system.test.ts b/integration-tests/cli/file-system.test.ts index bfba679ea59..790a33ded85 100644 --- a/integration-tests/cli/file-system.test.ts +++ b/integration-tests/cli/file-system.test.ts @@ -206,7 +206,7 @@ describe('file-system', () => { const readAttempt = toolLogs.find( (log) => log.toolRequest.name === 'read_file' && - log.toolRequest.args.includes(fileName), + log.toolRequest.args?.includes(fileName), ); const editAttempt = toolLogs.find( (log) => log.toolRequest.name === 'edit_file', diff --git a/integration-tests/cli/notebook-edit.test.ts b/integration-tests/cli/notebook-edit.test.ts index 746380c5ca6..d919e99c4ca 100644 --- a/integration-tests/cli/notebook-edit.test.ts +++ b/integration-tests/cli/notebook-edit.test.ts @@ -83,9 +83,9 @@ const expectNoSuccessfulRawNotebookWrites = ( .readToolLogs() .filter( (log) => - ['edit', 'write_file'].includes(log.toolRequest.name) && + ['edit', 'write_file'].includes(log.toolRequest.name ?? '') && log.toolRequest.success && - log.toolRequest.args.includes(notebookFileName), + log.toolRequest.args?.includes(notebookFileName), ); expect(rawNotebookWrites).toEqual([]); diff --git a/integration-tests/cli/qwen-serve-streaming.test.ts b/integration-tests/cli/qwen-serve-streaming.test.ts index 6e0c68a9972..131fc03ed15 100644 --- a/integration-tests/cli/qwen-serve-streaming.test.ts +++ b/integration-tests/cli/qwen-serve-streaming.test.ts @@ -46,6 +46,10 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { isPathWithinRoot } from '@qwen-code/qwen-code-core'; import { DaemonClient, parseSseStream } from '@qwen-code/sdk'; import type { DaemonEvent, DaemonSessionSummary } from '@qwen-code/sdk'; +import { + isNonBlockingAccepted, + type NonBlockingPromptAccepted, +} from '@qwen-code/sdk/daemon'; import { fakeToolCall, startFakeOpenAIServer, @@ -135,6 +139,12 @@ function findExternalReadBase(): string | undefined { const externalReadBase = findExternalReadBase(); +function asAccepted( + result: Awaited>, +): NonBlockingPromptAccepted | undefined { + return isNonBlockingAccepted(result) ? result : undefined; +} + let daemon: ChildProcess; let port = 0; let base = ''; @@ -619,11 +629,13 @@ describePOSIX('qwen serve — same-host external text reads', () => { const requestStart = fakeServer.requests.length; try { await new Promise((resolve) => setTimeout(resolve, 200)); - const accepted = await client.promptNonBlocking(session.sessionId, { - prompt: [{ type: 'text', text: marker }], - }); - expect('promptId' in accepted).toBe(true); - if (!('promptId' in accepted)) return; + const accepted = asAccepted( + await client.promptNonBlocking(session.sessionId, { + prompt: [{ type: 'text', text: marker }], + }), + ); + expect(accepted).toBeDefined(); + if (!accepted) return; promptId = accepted.promptId; await expect.poll(findReadPermission, { timeout: 30_000 }).toBeDefined(); @@ -766,11 +778,13 @@ describePOSIX('qwen serve — daemon Todo Stop Guard replay', () => { }); const requestStart = fakeServer.requests.length; const guardMarker = `todo-guard-e2e-${requestStart}`; - const accepted = await client.promptNonBlocking(session.sessionId, { - prompt: [{ type: 'text', text: guardMarker }], - }); - expect('promptId' in accepted).toBe(true); - if (!('promptId' in accepted)) return; + const accepted = asAccepted( + await client.promptNonBlocking(session.sessionId, { + prompt: [{ type: 'text', text: guardMarker }], + }), + ); + expect(accepted).toBeDefined(); + if (!accepted) return; await expect .poll( diff --git a/integration-tests/cli/qwen-serve-webui-live-journal-recovery.test.ts b/integration-tests/cli/qwen-serve-webui-live-journal-recovery.test.ts index 1d8c3bb1c08..1e59d75d855 100644 --- a/integration-tests/cli/qwen-serve-webui-live-journal-recovery.test.ts +++ b/integration-tests/cli/qwen-serve-webui-live-journal-recovery.test.ts @@ -157,16 +157,19 @@ describe('qwen serve WebUI live journal recovery', () => { root = createRoot(container); await act(async () => { root?.render( - createElement( - DaemonSessionProvider, - { - autoConnect: true, - baseUrl: activeDaemon!.base, - token: activeDaemon!.token, - sessionId: created.sessionId, - }, - createElement(Harness), - ), + // `children` is the one required prop on DaemonSessionProviderProps, + // and a trailing createElement argument does not satisfy it — the + // call only type checks with children in the props object. The lint + // rule guards JSX readability, which does not apply in this .ts file + // where createElement is already being called by hand. + // eslint-disable-next-line react/no-children-prop + createElement(DaemonSessionProvider, { + autoConnect: true, + baseUrl: activeDaemon!.base, + token: activeDaemon!.token, + sessionId: created.sessionId, + children: createElement(Harness), + }), ); }); diff --git a/integration-tests/cli/sleep-interception.test.ts b/integration-tests/cli/sleep-interception.test.ts index cf2feb404b8..ffcce1f3b31 100644 --- a/integration-tests/cli/sleep-interception.test.ts +++ b/integration-tests/cli/sleep-interception.test.ts @@ -20,9 +20,12 @@ describe('sleep-interception', () => { } }); + // Mirrors the optionality of the parsed telemetry these come from: a + // malformed record yields `undefined` rather than a crash. The predicates + // below only match an explicit `success` boolean. type ShellCall = { - args: string; - success: boolean; + args?: string; + success?: boolean; error?: string; }; @@ -67,7 +70,7 @@ describe('sleep-interception', () => { ); const foundBlockedCall = await waitForShellCall( - (call) => call.args.includes('sleep 5') && !call.success, + (call) => !!call.args?.includes('sleep 5') && call.success === false, ); if (!foundBlockedCall) { @@ -85,7 +88,7 @@ describe('sleep-interception', () => { // error attribute is only available from file-based telemetry; the // podman stdout fallback leaves it undefined. const blockedCall = shellCalls().find( - (call) => call.args.includes('sleep 5') && !call.success, + (call) => !!call.args?.includes('sleep 5') && call.success === false, ); if (blockedCall?.error !== undefined) { expect(blockedCall.error).toContain('Monitor'); @@ -107,7 +110,7 @@ describe('sleep-interception', () => { ); const foundSuccessfulCall = await waitForShellCall( - (call) => call.args.includes('sleep 1') && call.success, + (call) => !!call.args?.includes('sleep 1') && call.success === true, ); if (!foundSuccessfulCall) { @@ -140,7 +143,8 @@ describe('sleep-interception', () => { // The escape hatch worked iff a call carrying the intentional-sleep // comment completed successfully. const foundIntentionalCall = await waitForShellCall( - (call) => call.args.includes('intentional-sleep') && call.success, + (call) => + !!call.args?.includes('intentional-sleep') && call.success === true, ); if (!foundIntentionalCall) { @@ -175,7 +179,7 @@ describe('sleep-interception', () => { ); const foundBlockedCall = await waitForShellCall( - (call) => call.args.includes('sleep 5') && !call.success, + (call) => !!call.args?.includes('sleep 5') && call.success === false, ); if (!foundBlockedCall) { diff --git a/integration-tests/cli/stdin-context.test.ts b/integration-tests/cli/stdin-context.test.ts index 2dd4aca7459..81f8de2ae64 100644 --- a/integration-tests/cli/stdin-context.test.ts +++ b/integration-tests/cli/stdin-context.test.ts @@ -26,7 +26,12 @@ describe.skip('stdin context', () => { const lastRequest = rig.readLastApiRequest(); expect(lastRequest).not.toBeNull(); - const historyString = lastRequest.attributes.request_text; + // `expect(...).not.toBeNull()` is a runtime check; it does not narrow the + // type. Assert the shape explicitly so the `indexOf` calls below are not + // reaching into `unknown`. + const historyString = String( + lastRequest?.attributes?.['request_text'] ?? '', + ); // TODO: This test currently fails in sandbox mode (Docker/Podman) because // stdin content is not properly forwarded to the container when used diff --git a/integration-tests/cli/todo_write.test.ts b/integration-tests/cli/todo_write.test.ts index 5bc28125f53..bae4d4649fc 100644 --- a/integration-tests/cli/todo_write.test.ts +++ b/integration-tests/cli/todo_write.test.ts @@ -49,7 +49,7 @@ Use the todo_write tool to create this list.`; expect(todoWriteCalls.length).toBeGreaterThan(0); // Parse the arguments to verify they contain our tasks - const todoArgs = JSON.parse(todoWriteCalls[0].toolRequest.args); + const todoArgs = JSON.parse(todoWriteCalls[0].toolRequest.args ?? '{}'); expect(todoArgs.todos).toBeDefined(); expect(Array.isArray(todoArgs.todos)).toBe(true); diff --git a/integration-tests/cli/write_file.test.ts b/integration-tests/cli/write_file.test.ts index 2440c593139..6d6e7aea5ce 100644 --- a/integration-tests/cli/write_file.test.ts +++ b/integration-tests/cli/write_file.test.ts @@ -28,13 +28,17 @@ describe('write_file', () => { } const allTools = rig.readToolLogs(); - expect(foundToolCall, 'Expected to find a write_file tool call').toBeTruthy( + // The detailed message belongs on `expect`, not on `toBeTruthy` — the + // latter takes no arguments, so this diagnostic was being built and + // discarded on every failure, leaving only the bare literal. + expect( + foundToolCall, createToolCallErrorMessage( 'write_file', allTools.map((t) => t.toolRequest.name), result, ), - ); + ).toBeTruthy(); // Validate model output - will throw if no output, warn if missing expected content validateModelOutput(result, 'dad.txt', 'Write file test'); diff --git a/integration-tests/hook-integration/hooks.test.ts b/integration-tests/hook-integration/hooks.test.ts index 1be3c3139c2..4247fd84cb9 100644 --- a/integration-tests/hook-integration/hooks.test.ts +++ b/integration-tests/hook-integration/hooks.test.ts @@ -1427,11 +1427,8 @@ describe('Hooks System Integration', () => { }); // When Stop hooks block, agent continues execution normally (with max turns to prevent infinite loop) - const _result = await rig.run( - 'Say all block', - '--max-session-turns', - '3', - ); + // The run is the subject of the assertions below; its output is not. + await rig.run('Say all block', '--max-session-turns', '3'); // Verify Stop hook was invoked multiple times (indicating multiple rounds) const hookInvokeCount = rig diff --git a/integration-tests/terminal-bench/terminal-bench.test.ts b/integration-tests/terminal-bench/terminal-bench.test.ts index ed5348a9c7a..ed26d225f43 100644 --- a/integration-tests/terminal-bench/terminal-bench.test.ts +++ b/integration-tests/terminal-bench/terminal-bench.test.ts @@ -94,7 +94,9 @@ describe('terminal-bench integration', () => { .map((s) => s.trim()) .filter(Boolean); - const available = new Set(baseTestTasks.map((t) => t)); + // Set, not Set: the whole point is to test + // arbitrary env-supplied ids for membership. + const available = new Set(baseTestTasks); const unknown = selected.filter((s) => !available.has(s)); if (unknown.length > 0) { throw new Error( diff --git a/integration-tests/terminal-capture/scenarios/bugfix-2833.ts b/integration-tests/terminal-capture/scenarios/bugfix-2833.ts index dffa2567fd0..17e715aa141 100644 --- a/integration-tests/terminal-capture/scenarios/bugfix-2833.ts +++ b/integration-tests/terminal-capture/scenarios/bugfix-2833.ts @@ -9,6 +9,10 @@ export default { name: 'streaming-bugfix-2833', spawn: ['node', 'dist/cli.js', '--yolo'], terminal: { title: 'qwen-code', cwd: '../../..' }, + // Generate an animated GIF. This is a scenario-level switch (see + // ScenarioConfig); it used to sit inside `streaming` below, where the runner + // never read it. + gif: true, flow: [ { type: '/qc:bugfix https://github.com/QwenLM/qwen-code/issues/2833', @@ -17,7 +21,6 @@ export default { delayMs: 10000, // Wait 10s for initial prompt processing intervalMs: 30000, // Capture every 30 seconds count: 50, // Up to 25 minutes of capture (50 * 30s) - gif: true, // Generate animated GIF }, }, ], diff --git a/integration-tests/terminal-capture/scenarios/pr-2371-review.ts b/integration-tests/terminal-capture/scenarios/pr-2371-review.ts index 0752f0a207b..7054e690c28 100644 --- a/integration-tests/terminal-capture/scenarios/pr-2371-review.ts +++ b/integration-tests/terminal-capture/scenarios/pr-2371-review.ts @@ -4,6 +4,9 @@ export default { name: 'pr-2371-review', spawn: ['node', 'dist/cli.js', '--yolo'], terminal: { title: 'qwen-code', cwd: '../../..' }, + // `gif` is a scenario-level switch (see ScenarioConfig). It used to sit + // inside `streaming` below, where the runner never read it. + gif: true, flow: [ { type: '/review https://github.com/QwenLM/qwen-code/pull/2371', @@ -11,7 +14,6 @@ export default { delayMs: 5000, intervalMs: 10000, // Every 10s count: 60, // 10 minutes total (60 * 10s) - gif: true, }, }, ], diff --git a/integration-tests/terminal-capture/table-pending-height-scroll-lock-regression.ts b/integration-tests/terminal-capture/table-pending-height-scroll-lock-regression.ts index a26562c4ac8..3d62f1bfea8 100644 --- a/integration-tests/terminal-capture/table-pending-height-scroll-lock-regression.ts +++ b/integration-tests/terminal-capture/table-pending-height-scroll-lock-regression.ts @@ -47,7 +47,9 @@ * QWEN_TUI_E2E_OUT output dir (default under os.tmpdir()) * QWEN_TUI_E2E_REPO repo root whose dist/cli.js is launched */ -import { createServer, type AddressInfo } from 'node:http'; +import { createServer } from 'node:http'; +// AddressInfo is declared by node:net, not node:http. +import type { AddressInfo } from 'node:net'; import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { basename, dirname, join, resolve } from 'node:path'; diff --git a/integration-tests/test-helper.ts b/integration-tests/test-helper.ts index 5dfa9dda1a2..b35ab0303b8 100644 --- a/integration-tests/test-helper.ts +++ b/integration-tests/test-helper.ts @@ -27,7 +27,11 @@ function sanitizeTestName(name: string) { // Helper to create detailed error messages export function createToolCallErrorMessage( expectedTools: string | string[], - foundTools: string[], + // Callers build this by mapping `toolRequest.name` over the parsed + // telemetry, where the name is optional. This is a failure message, so a + // missing entry should print as `undefined` rather than force every call + // site to filter first. + foundTools: Array, result: string, ) { const expectedStr = Array.isArray(expectedTools) @@ -170,6 +174,10 @@ interface ParsedLog { duration_ms?: number; status?: string; 'error.message'?: string; + // Telemetry carries far more attributes than the tool-call subset named + // above; callers reach them by key (`attributes['request_text']`). Every + // value is `unknown` because nothing validates the payload shape. + [key: string]: unknown; }; scopeMetrics?: { metrics: { @@ -811,12 +819,17 @@ export class TestRig { } const parsedLogs = this._readAndParseTelemetryLog(); + // Every field is optional because it is copied straight out of the + // telemetry attributes, which nothing validates. The stdout fallback above + // reconstructs the same fields from a regex and can promise them; this + // branch cannot, and claiming otherwise just moved the `undefined` past + // the type checker into the assertions. const logs: { toolRequest: { - name: string; - args: string; - success: boolean; - duration_ms: number; + name?: string; + args?: string; + success?: boolean; + duration_ms?: number; status?: string; error?: string; }; @@ -845,7 +858,9 @@ export class TestRig { return logs; } - readLastApiRequest(): Record | null { + // Returns the parsed log, not a bare record: callers want `.attributes`, + // and `Record` hid that the value already has a shape. + readLastApiRequest(): ParsedLog | null { const logs = this._readAndParseTelemetryLog(); const apiRequests = logs.filter( (logData) => diff --git a/integration-tests/tsconfig.json b/integration-tests/tsconfig.json index 8e94434c0c3..101ca5a9f62 100644 --- a/integration-tests/tsconfig.json +++ b/integration-tests/tsconfig.json @@ -3,12 +3,159 @@ "compilerOptions": { "noEmit": true, "allowJs": true, + // Nothing references this project and it emits nothing, but the root + // config turns `composite` on for the packages that do. Composite demands + // that every file in the program appear in `include`, and these tests + // import package sources across the repo by relative path, so inheriting + // it produced 300+ TS6307 "not listed within the file list" errors. + "composite": false, + // The root turns `noPropertyAccessFromIndexSignature` on, which forced + // bracket-access rewrites in production SDK sources just to satisfy this + // test program (packages/desktop already sets it false). Relax it here so + // packages keep their own compiler regime and the tests keep dot access. + "noPropertyAccessFromIndexSignature": false, + // Matches packages/cli. The suite drives browser-side code in + // `terminal-capture/` and pulls SDK sources that reference `WebSocket` / + // `HeadersInit`, none of which exist in the root's ES2023-only lib. + "lib": ["DOM", "DOM.Iterable", "ES2023"], "baseUrl": ".", + // Resolve workspace packages from source so `tsc -p` here does not depend + // on whether someone had built them recently — a missing dist used to fail + // resolution outright and a stale one silently typechecked against old + // declarations. nodenext does no extension or index probing on + // substituted paths, so every subpath the program imports needs an + // explicit entry naming its source file; a bare wildcard falls through to + // the package exports map, i.e. back to dist. Keep these in sync with the + // packages' exports maps. The runtime vitest aliases + // (`integration-tests/vitest.config.ts`) still point at the built SDK + // bundle to exercise the published-bundle shape; these entries only affect + // type resolution. + // + // Keep notes like this OUT of `paths` itself: every value there must be an + // array, so a `"//"` string key makes tsc abort with TS5063 before it type + // checks a single file — which is how this project silently went unchecked. "paths": { - "//": "Resolve types from SDK source rather than dist so `tsc -p` here does not require a fresh `npm run build` of the SDK package before checking integration tests. The runtime vitest alias (`integration-tests/vitest.config.ts`) still points at the built `dist/index.mjs` to exercise the published-bundle shape; this paths entry only affects type resolution.", - "@qwen-code/sdk": ["../packages/sdk-typescript/src/index.ts"] + // These tests import package sources by relative path + // (`../../packages/cli/src/...`), so those files get checked here too + // and must resolve their own imports. + "@qwen-code/qwen-code-core": ["../packages/core/src/index.ts"], + "@qwen-code/qwen-code-core/transcriptRecords": [ + "../packages/core/src/utils/transcript-records.ts" + ], + "@qwen-code/qwen-code-core/goalWire": [ + "../packages/core/src/goals/goal-wire.ts" + ], + "@qwen-code/qwen-code-core/memoryScopes": [ + "../packages/core/src/memory/scopes.ts" + ], + "@qwen-code/qwen-code-core/userPromptSubmitContext": [ + "../packages/core/src/hooks/user-prompt-submit-context.ts" + ], + "@qwen-code/sdk": ["../packages/sdk-typescript/src/index.ts"], + "@qwen-code/sdk/daemon": [ + "../packages/sdk-typescript/src/daemon/index.ts" + ], + "@qwen-code/sdk/daemon/transcript": [ + "../packages/sdk-typescript/src/daemon/transcript.ts" + ], + "@qwen-code/sdk/daemon/transports": [ + "../packages/sdk-typescript/src/daemon/transports.ts" + ], + "@qwen-code/sdk/daemon/types": [ + "../packages/sdk-typescript/src/daemon/types.ts" + ], + "@qwen-code/sdk/daemon/ui/transcript": [ + "../packages/sdk-typescript/src/daemon/ui/transcript.ts" + ], + "@qwen-code/acp-bridge": ["../packages/acp-bridge/src/index.ts"], + "@qwen-code/acp-bridge/bridge": ["../packages/acp-bridge/src/bridge.ts"], + "@qwen-code/acp-bridge/bridgeClient": [ + "../packages/acp-bridge/src/bridgeClient.ts" + ], + "@qwen-code/acp-bridge/bridgeErrors": [ + "../packages/acp-bridge/src/bridgeErrors.ts" + ], + "@qwen-code/acp-bridge/bridgeFileSystem": [ + "../packages/acp-bridge/src/bridgeFileSystem.ts" + ], + "@qwen-code/acp-bridge/bridgeOptions": [ + "../packages/acp-bridge/src/bridgeOptions.ts" + ], + "@qwen-code/acp-bridge/bridgeTypes": [ + "../packages/acp-bridge/src/bridgeTypes.ts" + ], + "@qwen-code/acp-bridge/channelControlTimeouts": [ + "../packages/acp-bridge/src/channel-control-timeouts.ts" + ], + "@qwen-code/acp-bridge/childHeapPolicy": [ + "../packages/acp-bridge/src/child-heap-policy.ts" + ], + "@qwen-code/acp-bridge/daemonEventTypes": [ + "../packages/acp-bridge/src/daemonEventTypes.ts" + ], + "@qwen-code/acp-bridge/daemonMemoryBudget": [ + "../packages/acp-bridge/src/daemon-memory-budget.ts" + ], + "@qwen-code/acp-bridge/eventBus": [ + "../packages/acp-bridge/src/eventBus.ts" + ], + "@qwen-code/acp-bridge/externalToolGuard": [ + "../packages/acp-bridge/src/externalToolGuard.ts" + ], + "@qwen-code/acp-bridge/logRedaction": [ + "../packages/acp-bridge/src/logRedaction.ts" + ], + "@qwen-code/acp-bridge/mcpTimeouts": [ + "../packages/acp-bridge/src/mcpTimeouts.ts" + ], + "@qwen-code/acp-bridge/sessionArtifacts": [ + "../packages/acp-bridge/src/sessionArtifacts.ts" + ], + "@qwen-code/acp-bridge/spawnChannel": [ + "../packages/acp-bridge/src/spawnChannel.ts" + ], + "@qwen-code/acp-bridge/status": ["../packages/acp-bridge/src/status.ts"], + "@qwen-code/acp-bridge/transcriptReplay": [ + "../packages/acp-bridge/src/transcript-replay.ts" + ], + "@qwen-code/acp-bridge/workspacePaths": [ + "../packages/acp-bridge/src/workspacePaths.ts" + ], + // qwen-serve-webui-live-journal-recovery.test.ts imports this subpath; + // without an entry it resolves through the exports map to dist. + "@qwen-code/webui/daemon-react-sdk": [ + "../packages/webui/src/daemon-react-sdk.ts" + ], + // channel-plugin.test.ts and the plugin-example sources it imports + // both import `@qwen-code/channel-base`. Map it to source so the + // typecheck does not depend on channel-base's dist and both import + // sites share one declaration — mixing src and dist declarations + // produces duplicate-private-class errors. + "@qwen-code/channel-base": ["../packages/channels/base/src/index.ts"], + // cli's channel-registry.ts imports the eight builtin channel + // adapters and html.ts imports web-templates; without entries they + // resolve through their exports maps to dist, leaving the typecheck + // dependent on those packages being built. + "@qwen-code/channel-telegram": [ + "../packages/channels/telegram/src/index.ts" + ], + "@qwen-code/channel-weixin": ["../packages/channels/weixin/src/index.ts"], + "@qwen-code/channel-dingtalk": [ + "../packages/channels/dingtalk/src/index.ts" + ], + "@qwen-code/channel-wecom": ["../packages/channels/wecom/src/index.ts"], + "@qwen-code/channel-feishu": ["../packages/channels/feishu/src/index.ts"], + "@qwen-code/channel-qqbot": ["../packages/channels/qqbot/src/index.ts"], + "@qwen-code/channel-github": ["../packages/channels/github/src/index.ts"], + "@qwen-code/channel-gitlab": ["../packages/channels/gitlab/src/index.ts"], + "@qwen-code/web-templates": ["../packages/web-templates/src/index.ts"], + // node-pty declares `types` at the top level but its `exports` map is a + // bare string with no `types` condition, so nodenext resolution never + // reaches the declarations and every pty handle degrades to `any` — + // which is what silently untyped the `data` / `exitCode` callbacks in + // test-helper.ts. Point at the shipped .d.ts directly. + "@lydell/node-pty": ["../node_modules/@lydell/node-pty/node-pty.d.ts"] } }, - "include": ["**/*.ts"], - "references": [{ "path": "../packages/core" }] + "include": ["**/*.ts", "**/*.tsx"] } diff --git a/integrations/external-context/tsconfig.json b/integrations/external-context/tsconfig.json index 07d93d16c90..053e5ad66a5 100644 --- a/integrations/external-context/tsconfig.json +++ b/integrations/external-context/tsconfig.json @@ -2,6 +2,15 @@ "extends": "../../tsconfig.json", "compilerOptions": { "composite": true, + // Override the root's `vitest/globals` entry: vitest's types import the + // optional `jsdom` peer types, and once `@types/jsdom` is installed that + // drags `/// ` into this program. The DOM lib + // flips @types/node's conditional fetch globals to their DOM variants, + // whose ReadableStream is not async-iterable (needs lib.dom.asynciterable), + // breaking the `for await` over `response.body` in http-client.ts. The + // sources compiled here use no vitest globals (tests are excluded), so + // `node` alone is enough. + "types": ["node"], "outDir": "dist", "rootDir": "src" }, diff --git a/package-lock.json b/package-lock.json index 27889eea2c9..cb2da4bfd3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ }, "devDependencies": { "@types/chrome": "^0.1.32", + "@types/jsdom": "^28.0.3", "@types/marked": "^5.0.2", "@types/mime-types": "^3.0.1", "@types/minimatch": "^5.1.2", @@ -58,6 +59,7 @@ "glob": "^10.5.0", "globals": "^16.0.0", "husky": "^9.1.7", + "jsdom": "^26.1.0", "json": "^11.0.0", "lint-staged": "^16.1.6", "memfs": "^4.42.0", @@ -91,7 +93,7 @@ }, "integrations/external-context": { "name": "@qwen-code/external-context", - "version": "0.21.7", + "version": "0.20.1", "dependencies": { "@modelcontextprotocol/sdk": "^1.30.0", "undici": "^7.28.0", @@ -8445,6 +8447,48 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsdom": { + "version": "28.0.3", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-28.0.3.tgz", + "integrity": "sha512-/HQ2uFoetFTXuye8vzIcHw2z6Fwi7Hi/qcgC+RoS9NCyewiqxhVGqlG+ViGB6lkax481R6dmhf1I7lIGlzJStQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^8.0.0", + "undici-types": "^7.21.0" + } + }, + "node_modules/@types/jsdom/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@types/jsdom/node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/@types/jsdom/node_modules/undici-types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.29.0.tgz", + "integrity": "sha512-vamA8dGlzMwhpyYpQp9d8vka3o4D/yn5I7ez7Or+msDA4bZ8Uh+Zy91WvWf3I73gDAkFha9JcYRqm2li0Npfgg==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", diff --git a/package.json b/package.json index da9dbffc545..e277a495b42 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ ], "devDependencies": { "@types/chrome": "^0.1.32", + "@types/jsdom": "^28.0.3", "@types/marked": "^5.0.2", "@types/mime-types": "^3.0.1", "@types/minimatch": "^5.1.2", @@ -142,6 +143,7 @@ "glob": "^10.5.0", "globals": "^16.0.0", "husky": "^9.1.7", + "jsdom": "^26.1.0", "json": "^11.0.0", "lint-staged": "^16.1.6", "memfs": "^4.42.0",