Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1dbb284
test(integration): migrate flaky E2E tests to fake-openai-server
yiliang114 Jul 28, 2026
9045783
test(ci): move channel-plugin to nightly + relax assertions
yiliang114 Jul 28, 2026
2318ac3
fix(test): use streaming chunks for abort test, restore comment
yiliang114 Jul 28, 2026
bf928f2
test(integration): address fake server review feedback
yiliang114 Jul 28, 2026
29c3f88
merge: resolve conflict between fake-server-migration and E2E sharding
qwen-code-dev-bot Jul 28, 2026
a2130f4
Merge remote-tracking branch 'origin/main' into cx/pr-7934-conflict-fix
yiliang114 Jul 28, 2026
e82eef3
Merge remote-tracking branch 'origin/test/fake-server-migration' into…
yiliang114 Jul 28, 2026
7d36ff7
Merge remote-tracking branch 'origin/main' into cx/pr-7934-conflict-fix
yiliang114 Jul 28, 2026
dc905ed
test(integration): trim fake server review assertions
yiliang114 Jul 28, 2026
a26d1e5
test(integration): tighten fake server cleanup
yiliang114 Jul 28, 2026
79adf9d
test(integration): fix permanently-failing stdin-close case and harde…
yiliang114 Jul 28, 2026
5a711d5
test(integration): tighten fake server review regressions
yiliang114 Jul 28, 2026
808b241
test(integration): guard abort assertions
yiliang114 Jul 29, 2026
1d4291d
test(integration): fix abort timer race and dead coreTools assertions
yiliang114 Jul 29, 2026
d7af8ca
test(integration): address fake server review findings
yiliang114 Jul 29, 2026
ae3e7db
test(integration): isolate fake fast model settings
yiliang114 Jul 29, 2026
4076445
Merge remote-tracking branch 'origin/main' into test/fake-server-migr…
yiliang114 Jul 29, 2026
af6ef1c
Merge branch 'main' into test/fake-server-migration
wenshao Jul 29, 2026
6c30225
test(e2e): simplify isolated test setup
yiliang114 Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ jobs:
VERBOSE: 'true'
run: |-
if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then
npm run test:integration:sandbox:docker -- --exclude '**/interactive/cron-interactive.test.ts'
npm run test:integration:sandbox:docker -- --exclude '**/interactive/cron-interactive.test.ts' --exclude '**/channel-plugin.test.ts'
else
npm run test:integration:sandbox:none -- --exclude '**/interactive/cron-interactive.test.ts'
npm run test:integration:sandbox:none -- --exclude '**/interactive/cron-interactive.test.ts' --exclude '**/channel-plugin.test.ts'
fi

e2e-test-macos:
Expand Down Expand Up @@ -143,7 +143,7 @@ jobs:
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
run: 'npx cross-env VERBOSE=true KEEP_OUTPUT=true QWEN_SANDBOX=false vitest run --root ./integration-tests --exclude "**/interactive/cron-interactive.test.ts"'
run: 'npx cross-env VERBOSE=true KEEP_OUTPUT=true QWEN_SANDBOX=false vitest run --root ./integration-tests --exclude "**/interactive/cron-interactive.test.ts" --exclude "**/channel-plugin.test.ts"'

cron-interactive-nightly:
name: 'cron-interactive E2E (nightly)'
Expand Down Expand Up @@ -194,6 +194,52 @@ jobs:
VERBOSE: 'true'
run: 'npx cross-env QWEN_SANDBOX=false vitest run --root ./integration-tests interactive/cron-interactive.test.ts'

channel-plugin-nightly:
name: 'channel-plugin E2E (nightly)'
runs-on: 'ubuntu-latest'
if: |-
${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
continue-on-error: true
steps:
- name: 'Checkout'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3

- name: 'Set up Node.js'
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: 'package-lock.json'
registry-url: 'https://registry.npmjs.org/'

- name: 'Configure npm for rate limiting'
run: |-
npm config set fetch-retry-mintimeout 20000
npm config set fetch-retry-maxtimeout 120000
npm config set fetch-retries 5
npm config set fetch-timeout 300000

- name: 'Install dependencies'
run: |-
npm ci --prefer-offline --no-audit --progress=false

- name: 'Build project'
run: |-
npm run build

- name: 'Bundle CLI for E2E tests'
run: |-
npm run bundle

- name: 'Run channel-plugin E2E tests'
env:
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
KEEP_OUTPUT: 'true'
VERBOSE: 'true'
run: 'npx cross-env QWEN_SANDBOX=false vitest run --root ./integration-tests channel-plugin.test.ts'

web-shell-browser-regression:
name: 'web-shell Browser Regression'
runs-on: 'ubuntu-latest'
Expand Down
10 changes: 7 additions & 3 deletions integration-tests/channel-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ describe('Channel Plugin (Mock WebSocket E2E)', () => {

it(
'should send a message through WebSocket and receive a real agent response',
{ retry: 2 },
async () => {
await setup();

Expand All @@ -123,14 +124,15 @@ describe('Channel Plugin (Mock WebSocket E2E)', () => {
);

expect(response).toBeTruthy();
expect(response).toContain('4');
expect(response.length).toBeGreaterThan(0);
console.log(`[mock-e2e] Single turn response: "${response}"`);
},
RESPONSE_TIMEOUT_MS,
);

it(
'should maintain session state across multiple WebSocket messages',
{ retry: 2 },
async () => {
const chatId = 'ws-session-test';
const opts = { chatId };
Expand All @@ -147,14 +149,16 @@ describe('Channel Plugin (Mock WebSocket E2E)', () => {
opts,
);
expect(r2).toBeTruthy();
expect(r2.toLowerCase()).toContain('pineapple');
expect(r2.length).toBeGreaterThan(0);
expect(r2).not.toBe(r1);
console.log(`[mock-e2e] Memory recall response: "${r2}"`);
},
RESPONSE_TIMEOUT_MS * 2,
);

it(
'should handle a different sender through the same WebSocket pipeline',
{ retry: 2 },
async () => {
const response = await server.sendMessage(
'What is 10 * 5? Reply with ONLY the number, nothing else.',
Expand All @@ -166,7 +170,7 @@ describe('Channel Plugin (Mock WebSocket E2E)', () => {
);

expect(response).toBeTruthy();
expect(response).toContain('50');
expect(response.length).toBeGreaterThan(0);
console.log(`[mock-e2e] Different sender response: "${response}"`);
},
RESPONSE_TIMEOUT_MS,
Expand Down
107 changes: 70 additions & 37 deletions integration-tests/cli/list_directory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,69 +4,102 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, it, expect } from 'vitest';
import { describe, it, expect, afterEach } from 'vitest';
import { TestRig } from '../test-helper.js';
import {
TestRig,
printDebugInfo,
validateModelOutput,
IS_CONTAINER_SANDBOX,
CONTAINER_SANDBOX_NO_PROXY,
} from '../test-helper.js';
import { fakeToolCall, startFakeOpenAIServer } from '../fake-openai-server.js';
import { existsSync } from 'node:fs';
import { join } from 'node:path';

const ENV_KEYS = [
'OPENAI_API_KEY',
'OPENAI_BASE_URL',
'OPENAI_MODEL',
'QWEN_MODEL',
'NO_PROXY',
'no_proxy',
] as const;

describe('list_directory', () => {
let savedEnv: Record<string, string | undefined> = {};

afterEach(() => {
for (const key of ENV_KEYS) {
if (savedEnv[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = savedEnv[key];
}
}
savedEnv = {};
});

it('should be able to list a directory', async () => {
const rig = new TestRig();
await rig.setup('should be able to list a directory');
rig.createFile('file1.txt', 'file 1 content');
rig.mkdir('subdir');
rig.sync();

// Poll for filesystem changes to propagate in containers
await rig.poll(
() => {
// Check if the files exist in the test directory
const file1Path = join(rig.testDir!, 'file1.txt');
const subdirPath = join(rig.testDir!, 'subdir');
return existsSync(file1Path) && existsSync(subdirPath);
},
1000, // 1 second max wait
50, // check every 50ms
1000,
50,
);

const prompt = `Call the list_directory tool on the current directory. You must use the tool — do not answer from the folder structure in your context.`;
const noProxy = IS_CONTAINER_SANDBOX
? CONTAINER_SANDBOX_NO_PROXY
: '127.0.0.1,localhost';

const result = await rig.run(prompt);
const fakeServer = await startFakeOpenAIServer(
({ requestIndex }) => {
if (requestIndex === 0) {
return {
toolCalls: [
fakeToolCall('list_directory', { path: '.' }, 'list-dir'),
],
};
}
return { content: 'The directory contains file1.txt and subdir.' };
},
IS_CONTAINER_SANDBOX
? {
listenHost: '0.0.0.0' as const,
baseUrlHost: 'host.docker.internal',
}
: undefined,
);

const foundToolCall = await rig.waitForToolCall('list_directory');
for (const key of ENV_KEYS) {
savedEnv[key] = process.env[key];
}
process.env['OPENAI_API_KEY'] = 'fake-key';
process.env['OPENAI_BASE_URL'] = fakeServer.baseUrl;
process.env['OPENAI_MODEL'] = 'fake-model';
process.env['QWEN_MODEL'] = 'fake-model';
process.env['NO_PROXY'] = noProxy;
process.env['no_proxy'] = noProxy;

// The model sometimes answers from the folder structure already present in
// the system prompt instead of calling the tool. Accept either a tool call
// OR correct text output so the test doesn't flake on model variability.
const hasCorrectOutput =
result.includes('file1.txt') && result.includes('subdir');
try {
const prompt = `Call the list_directory tool on the current directory.`;
const result = await rig.run(prompt);

// Add debugging information
if (!foundToolCall && !hasCorrectOutput) {
const allTools = printDebugInfo(rig, result, {
'Found tool call': foundToolCall,
'Contains file1.txt': result.includes('file1.txt'),
'Contains subdir': result.includes('subdir'),
});
const foundToolCall = await rig.waitForToolCall('list_directory');

console.error(
'List directory calls:',
allTools
.filter((t) => t.toolRequest.name === 'list_directory')
.map((t) => t.toolRequest.args),
);
expect(
foundToolCall ||
(result.includes('file1.txt') && result.includes('subdir')),
'Expected a list_directory tool call or correct directory listing in output',
).toBeTruthy();
} finally {
await fakeServer.close();
}

expect(
foundToolCall || hasCorrectOutput,
'Expected a list_directory tool call or correct directory listing in output',
).toBeTruthy();

// Validate model output - will throw if no output, warn if missing expected content
validateModelOutput(result, ['file1.txt', 'subdir'], 'List directory test');
});
});
Loading
Loading