mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-31 02:06:21 +00:00
* fix(integration-tests): make the project typecheckable and fix what that found `tsc -p integration-tests/tsconfig.json` could not run at all. The config carried a `"//"` documentation key inside `compilerOptions.paths`, and every value there must be an array, so tsc aborted with TS5063 before checking a single file. Nothing in CI runs it either, so the directory has been unchecked for its whole life -- which is how PR #8620 shipped an `integration-tests/cli/qwen-serve-streaming.test.ts` that referenced an undeclared `REPO_ROOT`, swallowed the ReferenceError in a bare catch, and reported a green skip for a security regression test. Moving that note out of `paths` exposed 404 errors. Three more config defects accounted for 353 of them: - `composite: true` is inherited from the root config for the packages that are actually referenced. Composite requires every file in the program to appear in `include`, and these tests import package sources by relative path, so it produced 324 TS6307. Nothing references this project and it emits nothing, so it is now `composite: false`. - The root `lib` is ES2023 only. The suite drives browser-side code in `terminal-capture/` and pulls SDK sources that name `WebSocket` and `HeadersInit`, so 21 identifiers resolved to nothing. Now DOM + DOM.Iterable + ES2023, matching packages/cli. - Workspace packages resolved through `packages/core/dist` via a project reference, so with core unbuilt the checker reported a dozen members as missing from `Storage` that are right there in the source. They now resolve from source through `paths`, mirroring packages/cli, and the reference is gone. node-pty declares `types` at the top level but its `exports` map is a bare string with no `types` condition, so nodenext never reached the declarations and every pty handle degraded to `any` -- which is what silently untyped the `data` and `exitCode` callbacks in test-helper.ts. It now resolves through `paths` as well. `@types/jsdom` is added for the one file that uses it; DefinitelyTyped has no release matching jsdom 26 (it jumps 21 -> 27), so this pins the current 28.x. Two real defects fell out of the remaining 51: - write_file.test.ts built a detailed tool-call failure message and passed it to `toBeTruthy()`, which takes no arguments. It was discarded on every failure, leaving only a bare literal. - Two terminal-capture scenarios set `gif: true` inside `streaming`, where the runner never reads it. It is a scenario-level switch. The rest was making an existing `undefined` visible. `readToolLogs()` promised `name: string` for fields copied straight out of telemetry attributes that nothing validates; the stdout fallback can promise them, the telemetry branch cannot, and claiming otherwise just moved the `undefined` past the type checker into the assertions. This is type resolution only. `integration-tests/vitest.config.ts` keeps its own hardcoded aliases onto the built SDK bundle, so the suite still exercises the published-bundle shape at runtime. Not wired into CI here, but not for cost reasons: a cold run of `tsc -p integration-tests/tsconfig.json` takes about 106s on an idle developer box. The program is 2679 files, of which 103 are integration tests and roughly 1100 are package sources their own projects already check, so there is duplicated work available to reclaim by resolving the packages from their built declarations -- but at ~106s it is already cheap enough to gate on as-is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(integration-tests): isolate jsdom types and complete source-resolution paths Address review round 1: - external-context: override `types` to ["node"]. The root @types/jsdom entered its program through vitest's optional jsdom types and injected lib dom, flipping @types/node's fetch globals to DOM variants whose ReadableStream is not async-iterable (TS2504 in http-client.ts), which failed every CI job during the npm ci prepare build. - integration-tests tsconfig: explicit nodenext paths entries for every workspace subpath the program imports (sdk/daemon, 19 acp-bridge subpaths, core goalWire/memoryScopes/userPromptSubmitContext, webui daemon-react-sdk, channel-base); drop the dead `*` wildcards; include **/*.tsx. Typechecks green with the source packages' dists removed. - Relax noPropertyAccessFromIndexSignature in integration-tests and revert the six bracket-access rewrites it forced in SDK sources. - channel-plugin: import channels/base from src and map @qwen-code/channel-base to source so both declarations agree. - qwen-serve-streaming: asAccepted delegates to the SDK's exported isNonBlockingAccepted type predicate instead of a drifted copy. - sleep-interception: tighten blocked predicates to success === false and fix the comment describing them. - Declare jsdom at the root next to @types/jsdom. * fix(integration-tests): complete source-resolution paths and restore single channel-base instance Address review round 2: - Map the eight builtin channel adapters and web-templates to source. channel-registry.ts and html.ts still resolved them through their exports maps to dist, so the typecheck's build-independence was incomplete: on a tree without built dists it failed with the exact 9 x TS2307 the maintainer verification measured. - channel-plugin.test.ts: import @qwen-code/channel-base by bare specifier instead of a relative src path. At runtime the test and plugin-example now resolve the same dist/index.js through the exports map, restoring the single ChannelBase / SessionRouter instance the relative src import silently split; type resolution still maps to source through paths, and vitest.config.ts keeps pointing e2e runs at the built bundles. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
239 lines
6.7 KiB
TypeScript
239 lines
6.7 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import path from 'node:path';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
import {
|
|
TestRig,
|
|
createToolCallErrorMessage,
|
|
printDebugInfo,
|
|
validateModelOutput,
|
|
} from '../test-helper.js';
|
|
|
|
type NotebookCell = {
|
|
id?: string;
|
|
cell_type: 'code' | 'markdown' | 'raw';
|
|
metadata: Record<string, unknown>;
|
|
source: string | string[];
|
|
execution_count?: number | null;
|
|
outputs?: unknown[];
|
|
};
|
|
|
|
type NotebookContent = {
|
|
cells: NotebookCell[];
|
|
metadata: Record<string, unknown>;
|
|
nbformat: number;
|
|
nbformat_minor: number;
|
|
};
|
|
|
|
const sourceText = (source: string | string[]) =>
|
|
Array.isArray(source) ? source.join('') : source;
|
|
|
|
const promptPath = (filePath: string) => filePath.split(path.sep).join('/');
|
|
|
|
const readNotebook = (rig: TestRig, fileName: string): NotebookContent =>
|
|
JSON.parse(rig.readFile(fileName)) as NotebookContent;
|
|
|
|
const baseNotebook = (cells: NotebookCell[]): NotebookContent => ({
|
|
cells,
|
|
metadata: {
|
|
kernelspec: {
|
|
display_name: 'Python 3',
|
|
language: 'python',
|
|
name: 'python3',
|
|
},
|
|
language_info: {
|
|
name: 'python',
|
|
},
|
|
},
|
|
nbformat: 4,
|
|
nbformat_minor: 5,
|
|
});
|
|
|
|
const expectReadThenNotebookEdit = (rig: TestRig, result: string) => {
|
|
const logs = rig.readToolLogs();
|
|
const foundTools = logs.map((t) => t.toolRequest.name);
|
|
const readIndex = foundTools.findIndex((name) => name === 'read_file');
|
|
const notebookEditIndex = foundTools.findIndex(
|
|
(name) => name === 'notebook_edit',
|
|
);
|
|
|
|
if (readIndex === -1 || notebookEditIndex === -1) {
|
|
printDebugInfo(rig, result, { foundTools });
|
|
}
|
|
|
|
expect(
|
|
readIndex,
|
|
createToolCallErrorMessage('read_file', foundTools, result),
|
|
).toBeGreaterThanOrEqual(0);
|
|
expect(
|
|
notebookEditIndex,
|
|
createToolCallErrorMessage('notebook_edit', foundTools, result),
|
|
).toBeGreaterThan(readIndex);
|
|
};
|
|
|
|
const expectNoSuccessfulRawNotebookWrites = (
|
|
rig: TestRig,
|
|
notebookFileName: string,
|
|
) => {
|
|
const rawNotebookWrites = rig
|
|
.readToolLogs()
|
|
.filter(
|
|
(log) =>
|
|
['edit', 'write_file'].includes(log.toolRequest.name ?? '') &&
|
|
log.toolRequest.success &&
|
|
log.toolRequest.args?.includes(notebookFileName),
|
|
);
|
|
|
|
expect(rawNotebookWrites).toEqual([]);
|
|
};
|
|
|
|
describe('notebook_edit integration', () => {
|
|
let rig: TestRig;
|
|
|
|
afterEach(async () => {
|
|
await rig?.cleanup();
|
|
});
|
|
|
|
it('replaces a code cell after reading the notebook and clears stale outputs', async () => {
|
|
rig = new TestRig();
|
|
await rig.setup('notebook edit replace code cell clears outputs');
|
|
|
|
const fileName = 'analysis.ipynb';
|
|
const notebookPath = rig.createFile(
|
|
fileName,
|
|
JSON.stringify(
|
|
baseNotebook([
|
|
{
|
|
id: 'intro',
|
|
cell_type: 'markdown',
|
|
metadata: {},
|
|
source: ['# Analysis\n'],
|
|
},
|
|
{
|
|
id: 'load-data',
|
|
cell_type: 'code',
|
|
metadata: {},
|
|
source: ['old_value = 1\n', 'print(old_value)\n'],
|
|
execution_count: 7,
|
|
outputs: [
|
|
{
|
|
output_type: 'stream',
|
|
name: 'stdout',
|
|
text: ['1\n'],
|
|
},
|
|
],
|
|
},
|
|
]),
|
|
null,
|
|
1,
|
|
),
|
|
);
|
|
|
|
const prompt = `Read the notebook at ${promptPath(notebookPath)} with read_file first.
|
|
Then use notebook_edit, not edit or write_file, to replace the code cell whose id is load-data.
|
|
Set the new source exactly to:
|
|
|
|
result = 41 + 1
|
|
print(result)
|
|
|
|
Do not change any other cell.`;
|
|
|
|
const result = await rig.run(prompt);
|
|
|
|
expectReadThenNotebookEdit(rig, result);
|
|
expectNoSuccessfulRawNotebookWrites(rig, fileName);
|
|
validateModelOutput(result, null, 'Notebook replace');
|
|
|
|
const notebook = readNotebook(rig, fileName);
|
|
const target = notebook.cells.find((cell) => cell.id === 'load-data');
|
|
|
|
expect(target).toBeDefined();
|
|
expect(target?.cell_type).toBe('code');
|
|
expect(sourceText(target!.source).trimEnd()).toBe(
|
|
'result = 41 + 1\nprint(result)',
|
|
);
|
|
expect(target?.execution_count).toBeNull();
|
|
expect(target?.outputs).toEqual([]);
|
|
expect(sourceText(notebook.cells[0]!.source)).toBe('# Analysis\n');
|
|
});
|
|
|
|
it('inserts a markdown cell and deletes a target cell using notebook_edit', async () => {
|
|
rig = new TestRig();
|
|
await rig.setup('notebook edit insert and delete cells');
|
|
|
|
const fileName = 'workflow.ipynb';
|
|
const notebookPath = rig.createFile(
|
|
fileName,
|
|
JSON.stringify(
|
|
baseNotebook([
|
|
{
|
|
id: 'intro',
|
|
cell_type: 'markdown',
|
|
metadata: {},
|
|
source: ['# Workflow\n'],
|
|
},
|
|
{
|
|
id: 'remove-me',
|
|
cell_type: 'markdown',
|
|
metadata: {},
|
|
source: ['This temporary cell should be deleted.\n'],
|
|
},
|
|
{
|
|
id: 'calculate',
|
|
cell_type: 'code',
|
|
metadata: {},
|
|
source: ['value = 10\n'],
|
|
execution_count: null,
|
|
outputs: [],
|
|
},
|
|
]),
|
|
null,
|
|
1,
|
|
),
|
|
);
|
|
|
|
const insertedMarkdown =
|
|
'## Inserted Note\nThis cell was inserted by NotebookEdit.';
|
|
const prompt = `Read the notebook at ${promptPath(notebookPath)} with read_file first.
|
|
Then use notebook_edit, not edit or write_file, for both changes:
|
|
1. Insert a markdown cell after the cell whose id is intro. Its source must be exactly:
|
|
|
|
${insertedMarkdown}
|
|
|
|
2. Delete the cell whose id is remove-me.
|
|
Do not change the calculate code cell.`;
|
|
|
|
const result = await rig.run(prompt);
|
|
|
|
expectReadThenNotebookEdit(rig, result);
|
|
expectNoSuccessfulRawNotebookWrites(rig, fileName);
|
|
validateModelOutput(result, null, 'Notebook insert/delete');
|
|
|
|
const successfulNotebookEdits = rig
|
|
.readToolLogs()
|
|
.filter(
|
|
(log) =>
|
|
log.toolRequest.name === 'notebook_edit' && log.toolRequest.success,
|
|
);
|
|
expect(successfulNotebookEdits.length).toBeGreaterThanOrEqual(2);
|
|
|
|
const notebook = readNotebook(rig, fileName);
|
|
const cellIds = notebook.cells.map((cell) => cell.id);
|
|
const insertedCell = notebook.cells.find((cell) =>
|
|
sourceText(cell.source).includes('Inserted Note'),
|
|
);
|
|
|
|
expect(cellIds).toHaveLength(3);
|
|
expect(cellIds).not.toContain('remove-me');
|
|
expect(notebook.cells[0]?.id).toBe('intro');
|
|
expect(insertedCell).toBeDefined();
|
|
expect(insertedCell?.cell_type).toBe('markdown');
|
|
expect(sourceText(insertedCell!.source).trimEnd()).toBe(insertedMarkdown);
|
|
expect(notebook.cells.at(-1)?.id).toBe('calculate');
|
|
expect(sourceText(notebook.cells.at(-1)!.source)).toBe('value = 10\n');
|
|
});
|
|
});
|