qwen-code/integration-tests/cli/file-system.test.ts
jinye 3037744602
fix(integration-tests): make the project typecheckable and fix what that found (#8693)
* 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>
2026-08-08 16:32:31 +00:00

255 lines
7.6 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import {
TestRig,
printDebugInfo,
validateModelOutput,
} from '../test-helper.js';
describe('file-system', () => {
it('should be able to read a file', async () => {
const rig = new TestRig();
await rig.setup('should be able to read a file');
rig.createFile('test.txt', 'hello world');
const result = await rig.run(
`read the file test.txt and show me its contents`,
);
const foundToolCall = await rig.waitForToolCall('read_file');
// Add debugging information
if (!foundToolCall || !result.includes('hello world')) {
printDebugInfo(rig, result, {
'Found tool call': foundToolCall,
'Contains hello world': result.includes('hello world'),
});
}
expect(
foundToolCall,
'Expected to find a read_file tool call',
).toBeTruthy();
// Validate model output - will throw if no output, warn if missing expected content
validateModelOutput(result, 'hello world', 'File read test');
});
it('should be able to write a file', async () => {
const rig = new TestRig();
await rig.setup('should be able to write a file');
rig.createFile('test.txt', '');
const result = await rig.run(`edit test.txt to have a hello world message`);
// Accept multiple valid tools for editing files
const foundToolCall = await rig.waitForAnyToolCall(['write_file', 'edit']);
// Add debugging information
if (!foundToolCall) {
printDebugInfo(rig, result);
}
expect(
foundToolCall,
'Expected to find a write_file or edit tool call',
).toBeTruthy();
// Validate model output - will throw if no output
validateModelOutput(result, null, 'File write test');
const fileContent = rig.readFile('test.txt');
// Add debugging for file content
if (!fileContent.toLowerCase().includes('hello')) {
const writeCalls = rig
.readToolLogs()
.filter((t) => t.toolRequest.name === 'write_file')
.map((t) => t.toolRequest.args);
printDebugInfo(rig, result, {
'File content mismatch': true,
'Expected to contain': 'hello',
'Actual content': fileContent,
'Write tool calls': JSON.stringify(writeCalls),
});
}
expect(
fileContent.toLowerCase().includes('hello'),
'Expected file to contain hello',
).toBeTruthy();
// Log success info if verbose
if (process.env['VERBOSE'] === 'true') {
console.log('File written successfully with hello message.');
}
});
it('should correctly handle file paths with spaces', async () => {
const rig = new TestRig();
await rig.setup('should correctly handle file paths with spaces');
const fileName = 'my test file.txt';
const result = await rig.run(
`Use write_file to write exactly "hello" to "${fileName}".`,
);
const foundToolCall = await rig.waitForToolCall('write_file');
if (!foundToolCall) {
printDebugInfo(rig, result);
}
expect(
foundToolCall,
'Expected to find a write_file tool call',
).toBeTruthy();
const newFileContent = rig.readFile(fileName);
expect(newFileContent.trimEnd()).toBe('hello');
});
it('should perform a read-then-write sequence', async () => {
const rig = new TestRig();
await rig.setup('should perform a read-then-write sequence');
const fileName = 'version.txt';
rig.createFile(fileName, '1.0.0');
const prompt = `Read the version from ${fileName} and write the next version 1.0.1 back to the file.`;
const result = await rig.run(prompt);
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const readCall = toolLogs.find(
(log) => log.toolRequest.name === 'read_file',
);
const writeCall = toolLogs.find(
(log) =>
log.toolRequest.name === 'write_file' ||
log.toolRequest.name === 'replace',
);
if (!readCall || !writeCall) {
printDebugInfo(rig, result, { readCall, writeCall });
}
expect(readCall, 'Expected to find a read_file tool call').toBeDefined();
expect(
writeCall,
'Expected to find a write_file or replace tool call',
).toBeDefined();
const newFileContent = rig.readFile(fileName);
expect(newFileContent).toContain('1.0.1');
});
it.skip('should replace multiple instances of a string', async () => {
const rig = new TestRig();
await rig.setup('should replace multiple instances of a string');
const fileName = 'ambiguous.txt';
const fileContent = 'Hey there, \ntest line\ntest line';
const expectedContent = 'Hey there, \nnew line\nnew line';
rig.createFile(fileName, fileContent);
const result = await rig.run(
`replace "test line" with "new line" in ${fileName}`,
);
const foundToolCall = await rig.waitForAnyToolCall([
'replace',
'write_file',
]);
if (!foundToolCall) {
printDebugInfo(rig, result);
}
expect(
foundToolCall,
'Expected to find a replace or write_file tool call',
).toBeTruthy();
const toolLogs = rig.readToolLogs();
const successfulEdit = toolLogs.some(
(log) =>
(log.toolRequest.name === 'replace' ||
log.toolRequest.name === 'write_file') &&
log.toolRequest.success,
);
if (!successfulEdit) {
console.error(
'Expected a successful edit tool call, but none was found.',
);
printDebugInfo(rig, result);
}
expect(successfulEdit, 'Expected a successful edit tool call').toBeTruthy();
const newFileContent = rig.readFile(fileName);
expect(newFileContent).toBe(expectedContent);
});
it('should fail safely when trying to edit a non-existent file', async () => {
const rig = new TestRig();
await rig.setup(
'should fail safely when trying to edit a non-existent file',
);
const fileName = 'non_existent.txt';
const result = await rig.run(`In ${fileName}, replace "a" with "b"`);
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const readAttempt = toolLogs.find(
(log) =>
log.toolRequest.name === 'read_file' &&
log.toolRequest.args?.includes(fileName),
);
const editAttempt = toolLogs.find(
(log) => log.toolRequest.name === 'edit_file',
);
const successfulReplace = toolLogs.find(
(log) => log.toolRequest.name === 'replace' && log.toolRequest.success,
);
// The model can either investigate (and fail) or do nothing.
// If it chose to investigate by reading, that read must have failed.
if (readAttempt && readAttempt.toolRequest.success) {
console.error(
'A read_file attempt succeeded for a non-existent file when it should have failed.',
);
printDebugInfo(rig, result);
}
if (readAttempt) {
expect(
readAttempt.toolRequest.success,
'If model tries to read the file, that attempt must fail',
).toBe(false);
}
// CRITICAL: Verify that no matter what the model did, it never successfully
// wrote or replaced anything.
if (editAttempt) {
console.error(
'A edit_file attempt was made when no file should be written.',
);
printDebugInfo(rig, result);
}
expect(
editAttempt,
'edit_file should not have been called',
).toBeUndefined();
if (successfulReplace) {
console.error('A successful replace occurred when it should not have.');
printDebugInfo(rig, result);
}
expect(
successfulReplace,
'A successful replace should not have occurred',
).toBeUndefined();
});
});