Files
qwen-code/scripts/tests/runtime-archive-cache.test.js
e9b09e1c82 fix(test): budget the standalone runtime suite for the release pool (#15102)
* fix(test): budget the standalone runtime suite for the release pool (#15091)

The new Node/Bun regression suite runs two sequential workers that each spawn hundreds of probes. Release run 39621619809 lost both attempts at the 300 s per-worker and 600 s total spawn caps on the release pool, even though the same suite completes in about five minutes on a developer machine.

Raise the per-worker budget to 600 s and the total budget to 1500 s so release validation can finish on the shared runner. The job's own 30-minute timeout remains the upper bound.

* fix(test): fit the runtime budget inside the release job (#15091)

The old caps were never both hit: in release run 39621619809 the node worker passed at 294 s while the bun worker was SIGTERM'd by the 300 s per-worker cap at 301 s, and the child exited before the 600 s outer cap. Correct that attribution in the budget comment.

Derive the outer cap from the two sequential workers plus setup, and pick a 540 s worker cap so the derived 1140 s spawn still fits the 30-minute quality_scripts job beside the measured ~295 s setup and ~288 s scripts suite. A release-workflow meta-test pins the derivation, the worker/outer ordering, and the job-budget fit.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(test): classify the OpenTUI parity suites for the no-AK gate (#15091)

Main's OpenTUI migration added three interactive TUI parity suites, but the
no-AK membership guard requires every runnable integration test to be either
selected or explicitly exempted. The merged tree therefore failed the Test job
at no-ak-integration-ci.test.js:378.

The three suites are heavyweight interactive tests already covered by the
three-leg tui-parity workflow, so exempt them as "Runtime budget" alongside
the existing tui-migration and interactive-session-viewport entries.

* fix(ci): preserve enrolled parity suites in runtime budget repair

* test(scripts): pin standalone runtime timeout wiring and budget envelope

Cover the outer-margin floor, both spawnSync timeout bindings, and the package.json hop that places the spawn inside quality_scripts, so the release budget ratchet fails when any of them detaches.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(test): close the standalone runtime budget over setup phases (#15091)

The budget ratchet counted only the child spawn plus a hand-picked reserve, leaving the parent's four downloads and two tar extractions and the child's five pre-worker probes outside the model. Bound each process's setup with a shared SETUP_TIMEOUT_MS, derive the outer cap from it, and assert the closed sum against quality_scripts' 30-minute budget.

Also pin the per-host failure diagnostic the margin protects and the package.json hop that reaches --ci, so removing either fails the ratchet.

Co-authored-by: Qwen-Coder <[email protected]>

* test(scripts): make the runtime cache fetch pin shape-insensitive (#15091)

Co-authored-by: Qwen-Coder <[email protected]>

* test(scripts): enforce the setup budget through one factory (#15091)

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen Autofix <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: 易良 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-10-25 15:47:18 +08:00

353 lines
12 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { arePermissionBitsEnforced } from '../../packages/cli/src/test-utils/permission-bits.js';
import {
fetchVerifiedArchive,
sweepRuntimeCache,
} from '../lib/runtime-archive-cache.js';
import { RUNTIME_ARCHIVE_DIGESTS } from '../lib/standalone-runtime-pins.js';
import { getWorkflowJob, getWorkflowStep } from './workflow-helpers.js';
const ARCHIVE = Buffer.from('pinned archive bytes');
const WRONG = Buffer.from('stale archive bytes');
const URL = 'https://example.invalid/runtime.tgz';
const FILENAME = 'runtime.tgz';
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
const PIN = { digest: sha256, expected: sha256(ARCHIVE) };
const DAY_MS = 24 * 60 * 60 * 1000;
const ROOT = path.resolve(import.meta.dirname, '../..');
describe('fetchVerifiedArchive', () => {
let root;
let fetchImpl;
let sleep;
let log;
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'runtime-archive-cache-'));
fetchImpl = vi.fn(async () => new Response(ARCHIVE));
sleep = vi.fn(async () => undefined);
log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
});
afterEach(() => {
log.mockRestore();
fs.rmSync(root, { recursive: true, force: true });
});
const fetchWith = (options) =>
fetchVerifiedArchive(URL, FILENAME, PIN, { fetchImpl, sleep, ...options });
const age = (file, ms) => {
const time = new Date(Date.now() - ms);
fs.utimesSync(file, time, time);
};
it('caches a verified download and reuses it without fetching', async () => {
const cacheDir = path.join(root, 'cache');
const first = await fetchWith({ cacheDir });
const second = await fetchWith({ cacheDir });
expect(first.equals(ARCHIVE)).toBe(true);
expect(second.equals(ARCHIVE)).toBe(true);
expect(fetchImpl).toHaveBeenCalledTimes(1);
expect(fetchImpl).toHaveBeenCalledWith(URL, {
signal: expect.any(AbortSignal),
});
expect(fs.readdirSync(cacheDir)).toEqual([FILENAME]);
expect(log.mock.calls).toEqual([
[`runtime archive cached: ${path.join(cacheDir, FILENAME)}`],
[`runtime archive reused from cache: ${path.join(cacheDir, FILENAME)}`],
]);
});
it('replaces a cached copy that fails verification', async () => {
const cacheDir = path.join(root, 'cache');
fs.mkdirSync(cacheDir);
fs.writeFileSync(path.join(cacheDir, FILENAME), 'tampered');
const bytes = await fetchWith({ cacheDir });
expect(bytes.equals(ARCHIVE)).toBe(true);
expect(fetchImpl).toHaveBeenCalledTimes(1);
expect(fs.readFileSync(path.join(cacheDir, FILENAME)).equals(ARCHIVE)).toBe(
true,
);
expect(log).toHaveBeenCalledWith(
`runtime archive in cache failed verification: ${path.join(cacheDir, FILENAME)}`,
);
});
it('downloads without touching the disk when no cache is configured', async () => {
const bytes = await fetchWith({});
expect(bytes.equals(ARCHIVE)).toBe(true);
expect(fetchImpl).toHaveBeenCalledTimes(1);
expect(fs.readdirSync(root)).toEqual([]);
expect(log).not.toHaveBeenCalled();
});
it('retries failed requests and logs why each one failed', async () => {
fetchImpl
.mockResolvedValueOnce(new Response('bad gateway', { status: 502 }))
.mockRejectedValueOnce(
Object.assign(new TypeError('fetch failed'), {
cause: new Error('socket hang up'),
}),
);
const bytes = await fetchWith({});
expect(bytes.equals(ARCHIVE)).toBe(true);
expect(fetchImpl).toHaveBeenCalledTimes(3);
expect(sleep.mock.calls).toEqual([[5_000], [10_000]]);
expect(log.mock.calls).toEqual([
[
expect.stringMatching(
/^runtime download attempt 1 failed: .*returned 502/,
),
],
[
'runtime download attempt 2 failed: TypeError: fetch failed (cause: socket hang up)',
],
]);
});
it('retries a download that fails verification', async () => {
const cacheDir = path.join(root, 'cache');
fetchImpl.mockResolvedValueOnce(new Response(WRONG));
const bytes = await fetchWith({ cacheDir });
expect(bytes.equals(ARCHIVE)).toBe(true);
expect(fetchImpl).toHaveBeenCalledTimes(2);
expect(fs.readFileSync(path.join(cacheDir, FILENAME)).equals(ARCHIVE)).toBe(
true,
);
});
it('gives up after three downloads, naming both digests', async () => {
const cacheDir = path.join(root, 'cache');
fetchImpl.mockImplementation(async () => new Response(WRONG));
await expect(fetchWith({ cacheDir })).rejects.toThrow(
`runtime archive digest mismatch: ${URL} is ${sha256(WRONG)}, pinned ${sha256(ARCHIVE)}`,
);
expect(fetchImpl).toHaveBeenCalledTimes(3);
expect(fs.existsSync(cacheDir)).toBe(false);
});
it('removes cache entries no run has used for a week', async () => {
const cacheDir = path.join(root, 'cache');
fs.mkdirSync(cacheDir);
const seed = (entry, ms) => {
fs.writeFileSync(path.join(cacheDir, entry), 'x');
age(path.join(cacheDir, entry), ms);
};
// A previous pin and a partial file from a killed run.
seed('runtime-old.tgz', 8 * DAY_MS);
seed(`${FILENAME}.999999.tmp`, 8 * DAY_MS);
// Another revision's pin and a concurrent job's partial file.
seed('runtime-other.tgz', DAY_MS);
seed(`${FILENAME}.123.tmp`, 0);
// Not a cache entry: left alone, and the sweep goes on past it.
fs.mkdirSync(path.join(cacheDir, 'a-directory'));
age(path.join(cacheDir, 'a-directory'), 8 * DAY_MS);
await fetchWith({ cacheDir });
sweepRuntimeCache(cacheDir, [FILENAME]);
expect(fs.readdirSync(cacheDir).sort()).toEqual([
'a-directory',
'runtime-other.tgz',
FILENAME,
`${FILENAME}.123.tmp`,
]);
});
it('keeps every archive a run uses when it sweeps after the last fetch', async () => {
const cacheDir = path.join(root, 'cache');
fs.mkdirSync(cacheDir);
const second = Buffer.from('second pinned archive bytes');
// Both pins are cached but unused for a week, as after a quiet week.
for (const [entry, bytes] of [
[FILENAME, ARCHIVE],
['second.tgz', second],
['runtime-old.tgz', WRONG],
]) {
fs.writeFileSync(path.join(cacheDir, entry), bytes);
age(path.join(cacheDir, entry), 8 * DAY_MS);
}
await fetchWith({ cacheDir });
await fetchVerifiedArchive(
'https://example.invalid/second.tgz',
'second.tgz',
{ digest: sha256, expected: sha256(second) },
{ cacheDir, fetchImpl, sleep },
);
sweepRuntimeCache(cacheDir, [FILENAME, 'second.tgz']);
expect(fetchImpl).not.toHaveBeenCalled();
expect(fs.readdirSync(cacheDir).sort()).toEqual([FILENAME, 'second.tgz']);
});
it('marks a reused archive as used', async () => {
const cacheDir = path.join(root, 'cache');
fs.mkdirSync(cacheDir);
const cached = path.join(cacheDir, FILENAME);
fs.writeFileSync(cached, ARCHIVE);
age(cached, 8 * DAY_MS);
fs.writeFileSync(path.join(cacheDir, 'runtime-old.tgz'), 'x');
age(path.join(cacheDir, 'runtime-old.tgz'), 8 * DAY_MS);
await fetchWith({ cacheDir });
// Even without naming it, a later sweep keeps what a run just used.
sweepRuntimeCache(cacheDir, []);
expect(fetchImpl).not.toHaveBeenCalled();
expect(fs.statSync(cached).mtimeMs).toBeGreaterThan(Date.now() - DAY_MS);
expect(fs.readdirSync(cacheDir)).toEqual([FILENAME]);
});
it('caches through a partial file, so a failed write leaves no cached copy', async () => {
const cacheDir = path.join(root, 'cache');
const writeFileSync = fs.writeFileSync.bind(fs);
const write = vi
.spyOn(fs, 'writeFileSync')
.mockImplementationOnce((file, data) => {
writeFileSync(file, data.subarray(0, 4));
throw new Error('disk full');
});
try {
const bytes = await fetchWith({ cacheDir });
expect(bytes.equals(ARCHIVE)).toBe(true);
expect(write).toHaveBeenCalledWith(
`${path.join(cacheDir, FILENAME)}.${process.pid}.tmp`,
ARCHIVE,
);
} finally {
write.mockRestore();
}
expect(fs.readdirSync(cacheDir)).toEqual([]);
expect(log).toHaveBeenCalledWith(
'runtime archive not cached: Error: disk full',
);
});
it('keeps a verified download when the cache path runs through a file', async () => {
const blocker = path.join(root, 'not-a-directory');
fs.writeFileSync(blocker, '');
const bytes = await fetchWith({ cacheDir: path.join(blocker, 'cache') });
expect(bytes.equals(ARCHIVE)).toBe(true);
expect(log).toHaveBeenCalledWith(
expect.stringMatching(/^runtime archive not cached: /),
);
});
it.skipIf(!arePermissionBitsEnforced())(
'keeps a verified download when the cache directory cannot be searched',
async () => {
const cacheDir = path.join(root, 'cache');
fs.mkdirSync(cacheDir);
fs.chmodSync(cacheDir, 0o000);
try {
const bytes = await fetchWith({ cacheDir });
expect(bytes.equals(ARCHIVE)).toBe(true);
expect(log).toHaveBeenCalledWith(
expect.stringMatching(/^runtime archive not cached: .*EACCES/),
);
} finally {
fs.chmodSync(cacheDir, 0o700);
}
expect(fs.readdirSync(cacheDir)).toEqual([]);
},
);
});
describe('standalone runtime cache wiring', () => {
const CACHE_STEP =
'Configure persistent standalone runtime cache (self-hosted)';
const read = (file) => fs.readFileSync(path.join(ROOT, file), 'utf8');
const expectCacheExportedBefore = (job, consumer) => {
const step = getWorkflowStep(job, CACHE_STEP);
expect(step).toMatch(/^\s+if: .*runner\.environment == 'self-hosted'/m);
expect(step).toContain(
'cache_dir="${HOME}/.cache/qwen-code/standalone-runtimes"',
);
expect(step).toContain(
'echo "QWEN_STANDALONE_RUNTIME_CACHE=${cache_dir}" >> "${GITHUB_ENV}"',
);
expect(step).toContain('du -sh "${cache_dir}"');
// $GITHUB_ENV reaches only the steps after the one that writes it.
getWorkflowStep(job, consumer);
expect(job.indexOf(`- name: '${CACHE_STEP}'`)).toBeLessThan(
job.indexOf(`- name: '${consumer}'`),
);
};
it('exports the cache directory before the CI tests run', () => {
expectCacheExportedBefore(
getWorkflowJob(read('.github/workflows/ci.yml'), 'test'),
'Run tests and generate reports',
);
});
it('exports the cache directory before the release script tests run', () => {
expectCacheExportedBefore(
getWorkflowJob(read('.github/workflows/release.yml'), 'quality_scripts'),
'Run Script Tests',
);
});
it('hands the exported directory to every fetch and sweeps after the last', () => {
const script = read('scripts/test-standalone-runtimes.mjs');
expect(script).toContain(
'const cacheDir = process.env.QWEN_STANDALONE_RUNTIME_CACHE;',
);
expect(script).toMatch(
/return fetchVerifiedArchive\(url, filename, pin, \{\s*cacheDir,\s*fetchImpl: setup\.fetch,?\s*\}\)/,
);
expect(script.indexOf('sweepRuntimeCache(cacheDir, used)')).toBeGreaterThan(
script.lastIndexOf('await fetchArchive('),
);
});
});
describe('pinned standalone runtime archives', () => {
it('pins a well-formed, distinct digest of each runtime for every POSIX platform', () => {
expect(Object.keys(RUNTIME_ARCHIVE_DIGESTS).sort()).toEqual([
'darwin-arm64',
'darwin-x64',
'linux-arm64',
'linux-x64',
]);
const digests = Object.values(RUNTIME_ARCHIVE_DIGESTS).flatMap(
({ node, bun }) => {
expect(node).toMatch(/^[a-f0-9]{64}$/);
expect(bun).toMatch(/^sha512-[A-Za-z0-9+/]{86}==$/);
expect(Buffer.from(bun.slice('sha512-'.length), 'base64')).toHaveLength(
64,
);
return [node, bun];
},
);
// A row pasted over another platform's.
expect(new Set(digests).size).toBe(digests.length);
});
});