Files
release-factory/scripts/test-openclaw-native-typescript.mjs
okxlin d62a561380 fix(openclaw): omit native TypeScript compiler from runtime (#98)
Remove the development-only native compiler retained through optional peers before the final runtime copy. Keep TypeScript 5, fail closed on runtime dependencies or unsafe paths, and retain the existing image security gates.
2026-10-09 17:11:41 +08:00

86 lines
4.5 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {test} from 'node:test';
import {pruneNativeTypeScript} from '../openclaw-builder/image/prune-native-typescript.mjs';
function fixture(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openclaw-native-typescript-'));
t.after(() => fs.rmSync(root, {recursive: true, force: true}));
const write = (name, data) => {
fs.mkdirSync(path.dirname(path.join(root, name)), {recursive: true});
fs.writeFileSync(path.join(root, name), data);
};
const pkg = (name, version) => {
const entry = `node_modules/.pnpm/${name.replace('/', '+')}@${version}`;
const directory = `${entry}/node_modules/${name}`;
write(`${directory}/package.json`, JSON.stringify({name, version}));
write(`${directory}/lib/tsc`, 'compiler');
return {entry, directory};
};
const link = (name, target) => {
fs.mkdirSync(path.dirname(path.join(root, name)), {recursive: true});
fs.symlinkSync(path.relative(path.dirname(path.join(root, name)), path.join(root, target)), path.join(root, name));
};
write('package.json', JSON.stringify({devDependencies: {typescript: '7.1.0-dev.20260920.1'}}));
const native = pkg('typescript', '7.1.0-dev.20260920.1');
const x64 = pkg('@typescript/typescript-linux-x64', '7.1.0-dev.20260920.1');
const arm64 = pkg('@typescript/typescript-linux-arm64', '7.1.0-dev.20260920.1');
const legacy = pkg('typescript', '5.9.3');
const peer = pkg('cosmiconfig', '9.0.2');
const unrelated = pkg('@typescript-eslint/parser', '7.0.0');
link('node_modules/typescript', native.directory);
link('node_modules/.pnpm/node_modules/typescript', native.directory);
link('node_modules/@typescript/typescript-linux-x64', x64.directory);
link('node_modules/.pnpm/node_modules/@typescript/typescript-linux-arm64', arm64.directory);
link(`${peer.entry}/node_modules/typescript`, native.directory);
link(`${unrelated.entry}/node_modules/typescript`, legacy.directory);
write('node_modules/.bin/tsc', 'native compiler launcher');
write(`${unrelated.entry}/node_modules/.bin/tsc`, 'legacy compiler launcher');
return {root, write, native, x64, arm64, legacy, peer, unrelated};
}
test('prunes native compiler payloads, peers and launcher while retaining TypeScript 5 and unrelated packages', t => {
const f = fixture(t);
assert.equal(pruneNativeTypeScript(f.root), 3);
for (const name of [f.native.entry, f.x64.entry, f.arm64.entry, 'node_modules/typescript',
'node_modules/@typescript/typescript-linux-x64', 'node_modules/.pnpm/node_modules/@typescript/typescript-linux-arm64',
'node_modules/.pnpm/node_modules/typescript', `${f.peer.entry}/node_modules/typescript`, 'node_modules/.bin/tsc']) {
assert.equal(fs.existsSync(path.join(f.root, name)), false, name);
assert.throws(() => fs.lstatSync(path.join(f.root, name)), {code: 'ENOENT'});
}
for (const name of [f.legacy.directory, f.peer.directory, f.unrelated.directory,
`${f.unrelated.entry}/node_modules/typescript`, `${f.unrelated.entry}/node_modules/.bin/tsc`]) {
assert.ok(fs.existsSync(path.join(f.root, name)), name);
}
assert.equal(pruneNativeTypeScript(f.root), 0, 'cleanup must be idempotent');
});
test('refuses direct runtime TypeScript dependencies before removing anything', t => {
const f = fixture(t);
for (const section of ['dependencies', 'optionalDependencies']) {
f.write('package.json', JSON.stringify({[section]: {typescript: '7.1.0-dev.20260920.1'}}));
assert.throws(() => pruneNativeTypeScript(f.root), /direct runtime dependency/);
assert.ok(fs.existsSync(path.join(f.root, f.x64.directory, 'lib/tsc')));
}
});
test('rejects a compiler directory symlink before changing packages', t => {
const f = fixture(t);
const entry = path.join(f.root, f.x64.entry);
fs.renameSync(entry, path.join(f.root, 'outside-store'));
fs.symlinkSync(path.join(f.root, 'outside-store'), entry);
assert.throws(() => pruneNativeTypeScript(f.root), /store entry must not be a symlink/);
assert.ok(fs.existsSync(path.join(f.root, f.native.directory, 'lib/tsc')));
assert.ok(fs.existsSync(path.join(f.root, f.x64.directory, 'lib/tsc')));
});
test('rejects a pnpm store symlink', t => {
const f = fixture(t);
const store = path.join(f.root, 'node_modules/.pnpm');
fs.renameSync(store, path.join(f.root, 'outside-store'));
fs.symlinkSync(path.join(f.root, 'outside-store'), store);
assert.throws(() => pruneNativeTypeScript(f.root), /pnpm store must not be a symlink/);
});