Files
qwen-code/scripts/tests/browser-use-assets.test.js
顾盼 a791e47a54 feat(cua): streamline computer use and port native reliability fixes (#14846)
* feat(cua): preload the JavaScript computer use runtime

* refactor(cua): remove the native Wayland backend

* feat(cua): move app computer use into the driver

* feat(cua): preserve background delivery and port platform fixes

* feat(cua): add explicit desktop input and browser runtime facade

* feat(cua): package the unified 0.26.0 computer use runtime

* fix(cua): finalize cross-platform native reliability

* fix(cua): isolate runtime releases from CLI versioning

* fix(cua): keep Electron out of root workspace installs

* fix(cua): synchronize consumers and async cleanup tests

* fix(browser): migrate smoke to preloaded cua runtime

* docs(cua): document MCP setup for Qwen Code Claude Code and Codex
2026-10-21 14:03:31 +08:00

253 lines
7.8 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { parse } from 'yaml';
import { copyBrowserUseAssets } from '../copy-browser-use-assets.js';
import { copyBundleAssets } from '../copy_bundle_assets.js';
import { copyFiles } from '../copy_files.js';
import { getWorkflowJob } from './workflow-helpers.js';
const skillPath = 'packages/core/src/skills/bundled/browser-use';
const runtimeFiles = [
'index.js',
'native-host.js',
'scripts/native-host-setup.js',
];
const playwrightPath = 'packages/browser-use/node_modules/playwright-core';
describe('isolated browser runtime fixtures', () => {
let root;
beforeEach(() => {
root = fs.mkdtempSync(path.join(tmpdir(), 'qwen-browser-use-assets-'));
vi.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(console, 'warn').mockImplementation(() => {});
write(`${skillPath}/SKILL.md`, '# Browser use\n');
write(
'packages/browser-use/package.json',
JSON.stringify({
name: '@qwen-code/browser-use',
dependencies: { 'playwright-core': '1.2.3' },
}),
);
write('packages/browser-use/NOTICE', 'Browser use notice\n');
for (const file of runtimeFiles) {
write(`packages/browser-use/dist/${file}`, `// ${file}\n`);
}
write('packages/browser-use/dist/index.d.ts', 'export {};\n');
write(
'packages/browser-use/dist/scripts/smoke.js',
'// development only\n',
);
write(
`${playwrightPath}/package.json`,
JSON.stringify({
name: 'playwright-core',
version: '1.2.3',
}),
);
write(`${playwrightPath}/lib/server/injected.js`, '// runtime asset\n');
write(`${playwrightPath}/LICENSE`, 'Apache-2.0\n');
write(`${playwrightPath}/NOTICE`, 'Playwright notice\n');
write(`${playwrightPath}/types/types.d.ts`, 'export {};\n');
write(`${playwrightPath}/bin/install.sh`, '#!/bin/sh\n');
});
afterEach(() => {
vi.restoreAllMocks();
fs.rmSync(root, { force: true, recursive: true });
});
it('copies an isolated runtime fixture and removes stale generated files', () => {
const target = 'isolated-fixture';
write(`${target}/runtime/stale.json`, '{}\n');
copyBrowserUseAssets(root, path.join(root, target));
const runtimeDir = path.join(root, target, 'runtime');
expect(fs.readdirSync(runtimeDir).sort()).toEqual([
'NOTICE',
'index.js',
'native-host.js',
'node_modules',
'scripts',
]);
expect(fs.readdirSync(path.join(runtimeDir, 'scripts'))).toEqual([
'native-host-setup.js',
]);
for (const file of runtimeFiles) {
expect(fs.readFileSync(path.join(runtimeDir, file), 'utf8')).toBe(
`// ${file}\n`,
);
}
for (const file of [
'package.json',
'lib/server/injected.js',
'LICENSE',
'NOTICE',
'types/types.d.ts',
'bin/install.sh',
]) {
expect(
fs.readFileSync(
path.join(runtimeDir, 'node_modules/playwright-core', file),
),
).toEqual(fs.readFileSync(path.join(root, playwrightPath, file)));
}
expect(fs.readFileSync(path.join(runtimeDir, 'NOTICE'), 'utf8')).toBe(
'Browser use notice\n',
);
});
it.each(['core', 'bundle'])(
'excludes retired skills from %s even with stale source and output assets',
(mode) => {
const target =
mode === 'core'
? 'packages/core/dist/src/skills/bundled'
: 'dist/bundled';
for (const name of ['browser-use', 'computer-use']) {
write(
`packages/core/src/skills/bundled/${name}/SKILL.md`,
'# Retired\n',
);
write(
`packages/core/src/skills/bundled/${name}/runtime/stale.json`,
'{}\n',
);
write(`${target}/${name}/SKILL.md`, '# Stale output\n');
}
write('packages/core/src/skills/bundled/current/SKILL.md', '# Current\n');
fs.rmSync(path.join(root, 'packages/browser-use/dist'), {
recursive: true,
});
if (mode === 'core')
copyFiles({ root: path.join(root, 'packages/core') });
else copyBundleAssets({ root });
for (const name of ['browser-use', 'computer-use']) {
expect(fs.existsSync(path.join(root, target, name))).toBe(false);
}
expect(
fs.readFileSync(path.join(root, target, 'current/SKILL.md'), 'utf8'),
).toBe('# Current\n');
},
);
it.each(runtimeFiles)(
'fails with build advice when %s is missing',
(file) => {
fs.rmSync(path.join(root, 'packages/browser-use/dist', file));
expect(() =>
copyBrowserUseAssets(root, path.join(root, skillPath)),
).toThrow('npm run build --workspace=@qwen-code/browser-use');
},
);
it('uses the SDK-local pinned Playwright instead of the hoisted version', () => {
write(
'node_modules/playwright-core/package.json',
JSON.stringify({
name: 'playwright-core',
version: '9.9.9',
}),
);
copyBrowserUseAssets(root, path.join(root, skillPath));
const copied = JSON.parse(
fs.readFileSync(
path.join(
root,
skillPath,
'runtime/node_modules/playwright-core/package.json',
),
'utf8',
),
);
expect(copied.version).toBe('1.2.3');
});
it('rejects a resolved Playwright version different from the SDK pin', () => {
write(
`${playwrightPath}/package.json`,
JSON.stringify({
name: 'playwright-core',
version: '9.9.9',
}),
);
expect(() =>
copyBrowserUseAssets(root, path.join(root, skillPath)),
).toThrow('Browser-use requires playwright-core 1.2.3, but resolved 9.9.9');
});
it('declares the browser-use build dependency for recursive scheduling', () => {
const core = JSON.parse(
fs.readFileSync(
new URL('../../packages/core/package.json', import.meta.url),
'utf8',
),
);
expect(
core.devDependencies?.['@qwen-code/browser-use'] ??
core.dependencies?.['@qwen-code/browser-use'],
).toBe('file:../browser-use');
});
it('builds browser-use before core when publishing Live Host', () => {
const publish = getWorkflowJob(
fs.readFileSync(
new URL(
'../../.github/workflows/live-host-release.yml',
import.meta.url,
),
'utf8',
),
'publish',
);
const browserUseIndex = publish.indexOf(
'npm run build --workspace @qwen-code/browser-use',
);
expect(browserUseIndex).toBeGreaterThanOrEqual(0);
expect(browserUseIndex).toBeLessThan(
publish.indexOf('npm run build --workspace @qwen-code/qwen-code-core'),
);
});
it('keeps the pinned Playwright installable as a workspace dependency', () => {
const manifest = JSON.parse(
fs.readFileSync(
new URL('../../packages/browser-use/package.json', import.meta.url),
'utf8',
),
);
const lock = parse(
fs.readFileSync(new URL('../../pnpm-lock.yaml', import.meta.url), 'utf8'),
);
const playwright =
lock.importers['packages/browser-use'].dependencies['playwright-core'];
expect(manifest.dependencies['playwright-core']).toMatch(/^\d+\.\d+\.\d+$/);
expect(manifest.bundledDependencies).toBeUndefined();
expect(manifest.bundleDependencies).toBeUndefined();
expect(playwright.specifier).toBe(manifest.dependencies['playwright-core']);
expect(playwright.version).toBe(manifest.dependencies['playwright-core']);
expect(
lock.packages[`playwright-core@${playwright.version}`],
).toBeDefined();
});
function write(relativePath, contents) {
const file = path.join(root, relativePath);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, contents);
}
});