Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions .github/scripts/web-shell-visuals-publish.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,10 @@ function runHostingBlock(
runAttempt = '1',
} = {},
) {
const dir = mkdtempSync(join(tmpdir(), 'visuals-hosting-'));
const scopeRoot = mkdtempSync(join(tmpdir(), 'visuals-hosting-scope-'));
const dir = join(scopeRoot, 'fixture');
writeFileSync(join(scopeRoot, 'package.json'), '{"type":"commonjs"}\n');
mkdirSync(dir);
try {
return runHostingBlockIn(dir, hasImages, {
publicBaseUrl,
Expand All @@ -213,7 +216,7 @@ function runHostingBlock(
} finally {
// The fixture used to leak a mkdtemp dir per call; capture everything
// the assertions need inside, then tear it down.
rmSync(dir, { recursive: true, force: true });
rmSync(scopeRoot, { recursive: true, force: true });
}
}

Expand Down
15 changes: 13 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,14 @@ jobs:
# unit tests.
if: "${{ !cancelled() && github.event_name != 'schedule' }}"
runs-on: '${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'') }}'
timeout-minutes: 60
# Shared ECS hosts can stretch the normally 25-minute lane beyond an hour
# while tests are still making progress. Keep a finite ceiling without
# turning host contention into a false cancellation. The extension is
# scoped to the pool that shows the contention: fork PRs from untrusted
# authors and MAINTAINER_ECS_RUNNER_DISABLED=true fall back to
# GitHub-hosted runners (see classify_pr), which keep the pre-contention
# ceiling so a genuine hang there does not burn the extra 30 minutes.
timeout-minutes: '${{ fromJSON(contains(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'', ''ecs-qwen'') && ''90'' || ''60'') }}'
outputs:
ci_profile: '${{ steps.ci_profile.outputs.ci_profile }}'
permissions:
Expand Down Expand Up @@ -1305,7 +1312,11 @@ jobs:
# serialize this job behind the hour-long unit run.
if: "${{ !cancelled() && needs.classify_pr.outputs.skip_ci != 'true' && (github.event_name == 'pull_request' || github.event_name == 'merge_group') }}"
runs-on: '${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'') }}'
timeout-minutes: 30
# Dependency installation alone can exceed 30 minutes on a contended shared
# ECS host, before the separately bounded 20-minute integration step starts.
# Same ECS-only scoping as the `test` job: hosted fallback routing keeps
# the pre-contention ceiling.
timeout-minutes: '${{ fromJSON(contains(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'', ''ecs-qwen'') && ''60'' || ''30'') }}'
permissions:
contents: 'read'
steps:
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/sdk-java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,20 @@ jobs:
tar -xzf "${archive}" -C "${RUNNER_TEMP}"
echo "${RUNNER_TEMP}/apache-maven-${MAVEN_VERSION}/bin" >> "${GITHUB_PATH}"

- name: 'Run Java SDK tests'
- name: 'Run Java SDK tests (self-hosted)'
if: "${{ runner.environment == 'self-hosted' }}"
working-directory: 'packages/sdk-java/qwencode'
run: |-
mkdir -p "${HOME}/.cache/qwen-code-ci"
exec 9>"${HOME}/.cache/qwen-code-ci/sdk-java-tests.lock"
if ! flock --wait 1200 9; then
echo "::error::sdk-java host lock not acquired within 20 minutes"
exit 1
fi
mvn --batch-mode --no-transfer-progress clean test

- name: 'Run Java SDK tests (hosted)'
if: "${{ runner.environment == 'github-hosted' }}"
working-directory: 'packages/sdk-java/qwencode'
run: 'mvn --batch-mode --no-transfer-progress clean test'

Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/serve-ab.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ jobs:
# other fork PRs stay on ephemeral hosted runners. Keep in sync with
# ci.yml's classify_pr routing. Kill-switch: MAINTAINER_ECS_RUNNER_DISABLED.
runs-on: '${{ (github.repository == ''QwenLM/qwen-code'' && vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true'' && (github.event.pull_request.head.repo.full_name == github.repository || contains(fromJSON(''["OWNER","MEMBER","COLLABORATOR"]''), github.event.pull_request.author_association))) && fromJSON(''["self-hosted", "linux", "x64", "ecs-qwen"]'') || fromJSON(''["ubuntu-latest"]'') }}'
# Two full checkouts, each npm-ci + build + drive: a healthy run lands
# near twenty minutes, and a slow runner pushed a run past the old
# 30-minute bound, cancelling it.
timeout-minutes: 45
# Two full checkouts, each npm-ci + build + drive. A contended shared ECS
# runner completed the PR-head cycle but hit the 45-minute ceiling near
# the end of the merge-base cycle, so keep a finite 60-minute budget.
timeout-minutes: 60
steps:
- name: 'Restore workspace ownership'
if: "${{ runner.environment == 'self-hosted' }}"
Expand Down
8 changes: 8 additions & 0 deletions packages/acp-bridge/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ export default defineConfig({
},
},
test: {
// Shared ECS hosts can pause an otherwise healthy test past Vitest's 5s
// default when several CI runners on the same machine are busy.
testTimeout: process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? 60_000
: undefined,
hookTimeout: process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? 60_000
: undefined,
reporters: ['default'],
silent: true,
coverage: {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/acp-integration/acpAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2372,7 +2372,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
['non-positive', () => 0],
['non-integer', () => Date.now() + 0.5],
['non-safe', () => Number.MAX_SAFE_INTEGER + 1],
['beyond the timer range', () => Date.now() + 2_147_483_648],
['beyond the timer range', () => Number.MAX_SAFE_INTEGER],
])(
'rejects a %s trusted session initialization deadline before creating state',
async (_label, deadline) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/agent-view/pty-host-process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,10 @@ describe('Agent View PTY host process server', () => {
const connected = await connectAgentViewPtyHostProcess(
createLaunch('session-control-byte-logs'),
socketPath,
undefined,
process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? { requestTimeoutMs: 60_000 }
: {},
);

await expect(connected.getOutput?.()).resolves.toBe(output);
Expand Down
13 changes: 12 additions & 1 deletion packages/cli/src/agent-view/pty-host-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ export async function connectAgentViewPtyHostProcess(
socketPath,
launch,
authToken,
...(options.requestTimeoutMs !== undefined
? { requestTimeoutMs: options.requestTimeoutMs }
: {}),
pid: status.pid,
workerPid: status.workerPid,
});
Expand All @@ -199,6 +202,7 @@ function createRemotePtyHostHandle({
socketPath,
launch,
authToken,
requestTimeoutMs,
pid,
workerPid,
child,
Expand All @@ -207,6 +211,7 @@ function createRemotePtyHostHandle({
socketPath: string;
launch: AgentViewLaunchFile;
authToken?: string;
requestTimeoutMs?: number;
pid: number;
workerPid: number;
child?: ChildProcess;
Expand All @@ -227,7 +232,13 @@ function createRemotePtyHostHandle({
output,
exited: exitTracker.exited,
async getOutput(): Promise<string> {
const result = await callAgentViewPtyHost(socketPath, authToken, 'logs');
const result = await callAgentViewPtyHost(
socketPath,
authToken,
'logs',
undefined,
requestTimeoutMs,
);
if (isRecord(result) && typeof result['output'] === 'string') {
return result['output'];
}
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/commands/serve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,10 @@ describe('maybeOpenWebShellBrowser', () => {
});

describe('serve startup import boundary', () => {
const ecs = process.env['RUNNER_NAME']?.startsWith('ecs-qwen-');
const startupMs = ecs ? 60_000 : 30_000;
const testMs = ecs ? 70_000 : 40_000;

it('reaches listening through the dev entrypoint without loading interactive Ink internals first', async () => {
const workspace = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'qws-import-boundary-')),
Expand Down Expand Up @@ -1163,7 +1167,7 @@ describe('serve startup import boundary', () => {
`serve did not reach listening\nstdout:\n${stdout}\nstderr:\n${stderr}`,
),
);
}, 30_000);
}, startupMs);

child.stdout.on('data', (chunk: Buffer) => {
stdout += chunk.toString('utf8');
Expand Down Expand Up @@ -1209,5 +1213,5 @@ describe('serve startup import boundary', () => {
await removeTempDir(workspace);
await removeTempDir(qwenHome);
}
}, 40_000);
}, testMs);
});
73 changes: 44 additions & 29 deletions packages/cli/src/serve/acp-http/transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7919,35 +7919,50 @@ describe('ACP Streamable HTTP transport (over the wire)', () => {
// own spawnOrAttach would block on bridge.gate and never grant ownership.
bridge.gate = new Promise<void>((r) => (releaseLoad = r));
const connStream = await openStream(connId);
const got = takeFrames(connStream, 3); // session/new + close reject + load success
await new Promise((r) => setTimeout(r, 50));
// Load goes in-flight (awaits bridge.gate); pre-await closingSessions empty.
void post(connId, {
jsonrpc: '2.0',
id: 340,
method: 'session/load',
params: { sessionId: 'sess-1' },
});
await new Promise((r) => setTimeout(r, 20));
// Close starts DURING the load → marks sess-1 closing (awaits closeGate).
void post(connId, {
jsonrpc: '2.0',
id: 341,
method: 'session/close',
params: { sessionId: 'sess-1' },
});
await new Promise((r) => setTimeout(r, 20));
releaseLoad(); // loadSession resolves after close has been rejected.
const frames = (await got) as Array<{
id: number;
result?: { replayed?: boolean };
error?: { code: number; message: string; data?: { errorKind?: string } };
}>;
const closeReply = frames.find((f) => f.id === 341);
expect(closeReply?.error?.code).toBe(-32603);
expect(closeReply?.error?.data?.errorKind).toBe('session_archiving');
const loadReply = frames.find((f) => f.id === 340);
expect(loadReply?.result?.replayed).toBe(true);
const reader = frameReader(connStream);
const frameTimeoutMs = process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? 60_000
: 2_000;
let loadReply: { id: number; result?: { replayed?: boolean } } | undefined;
try {
// Load goes in-flight (awaits bridge.gate); pre-await closingSessions empty.
void post(connId, {
jsonrpc: '2.0',
id: 340,
method: 'session/load',
params: { sessionId: 'sess-1' },
});
await waitUntil(
() =>
bridge.loadRequests.some((request) => request.sessionId === 'sess-1'),
frameTimeoutMs,
);
// Close starts DURING the load and is rejected by the archive gate.
void post(connId, {
jsonrpc: '2.0',
id: 341,
method: 'session/close',
params: { sessionId: 'sess-1' },
});
const closeReply = (await reader.next(frameTimeoutMs)) as {
id: number;
error?: {
code: number;
message: string;
data?: { errorKind?: string };
};
};
expect(closeReply).toMatchObject({
id: 341,
error: { code: -32603, data: { errorKind: 'session_archiving' } },
});
releaseLoad();
loadReply = (await reader.next(frameTimeoutMs)) as typeof loadReply;
} finally {
releaseLoad();
reader.close();
}
expect(loadReply).toMatchObject({ id: 340, result: { replayed: true } });
expect(bridge.detached.some((d) => d.sessionId === 'sess-1')).toBe(false);
expect(bridge.killed).not.toContain('sess-1');

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/serve/server-default-bridge-wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('createServeApp default bridge wiring', () => {
).toEqual({
kind: 'not_found',
});
}, 15_000);
});

it('keeps the same-host write route disabled for an injected filesystem factory', async () => {
let bridgeOptions: BridgeOptions | undefined;
Expand Down Expand Up @@ -182,7 +182,7 @@ describe('createServeApp default bridge wiring', () => {
}),
).rejects.toBe(boundaryError);
expect(writeSameHostToolText).not.toHaveBeenCalled();
}, 15_000);
});

it('wires total admission into the internally-created bridge', async () => {
let freshSessionAdmission: BridgeFreshSessionAdmission | undefined;
Expand Down
10 changes: 8 additions & 2 deletions packages/cli/src/startup/worktreeStartup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ describe('setupStartupWorktree', () => {
// Real git operations + fetch through a local bare remote can take
// 10–15s on slower runners; bump the per-test ceiling so the PR-ref
// happy-path test doesn't flake.
vi.setConfig({ testTimeout: 30000, hookTimeout: 30000 });
const timeoutMs = process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? 60_000
: 30_000;
vi.setConfig({ testTimeout: timeoutMs, hookTimeout: timeoutMs });

let prevCwd: string;
let tempRepo: string | null = null;
Expand Down Expand Up @@ -365,7 +368,10 @@ describe('setupStartupWorktree', () => {
});

describe('persistStartupWorktreeSidecar', () => {
vi.setConfig({ testTimeout: 30000, hookTimeout: 30000 });
const timeoutMs = process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? 60_000
: 30_000;
vi.setConfig({ testTimeout: timeoutMs, hookTimeout: timeoutMs });

let prevCwd: string;
let tempRepo: string | null = null;
Expand Down
13 changes: 10 additions & 3 deletions packages/cli/src/ui/components/SettingsDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1340,9 +1340,16 @@ describe('SettingsDialog', () => {

// Press Escape to exit
stdin.write('\u001B');
await wait();

expect(onSelect).toHaveBeenCalledWith(undefined, 'User');
await waitFor(
() => {
expect(onSelect).toHaveBeenCalledWith(undefined, 'User');
},
{
timeout: process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? 10_000
: 1_000,
},
);

unmount();
});
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/ui/utils/clipboardUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,10 @@ const originalPlatform = process.platform;
// The beforeEach below resets the module registry and re-imports the module
// graph for every test; under heavy parallel CI load that can exceed the
// default hook timeout without any real hang.
vi.setConfig({ testTimeout: 30000, hookTimeout: 30000 });
const timeoutMs = process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? 60_000
: 30_000;
vi.setConfig({ testTimeout: timeoutMs, hookTimeout: timeoutMs });

describe('clipboardUtils', () => {
let clipboardHasImage: () => Promise<boolean>;
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,12 @@ export default defineConfig({
// See packages/core/vitest.config.ts: raise the per-test ceiling above
// vitest's 5s default so I/O-bound tests (e.g. the workspace registration
// store's tempdir round-trip) don't blow it purely under CI contention.
testTimeout: 15000,
testTimeout: process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? 60_000
: 15_000,
hookTimeout: process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? 60_000
: undefined,
// ECS hosts run several jobs at once; leave capacity for neighboring jobs.
maxWorkers: process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? '25%'
Expand Down
7 changes: 6 additions & 1 deletion packages/core/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ export default defineConfig({
// or WASM-load-bound tests (e.g. the web-tree-sitter lazy runtime, tar
// extraction) blow 5s purely under contention, not from any logic fault.
// Assertions still fail instantly; only the timeout ceiling grows.
testTimeout: 15000,
testTimeout: process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? 60_000
: 15_000,
hookTimeout: process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? 60_000
: undefined,
// ECS hosts run several jobs at once; leave capacity for neighboring jobs.
maxWorkers: process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
? '25%'
Expand Down
Loading
Loading