6f012d419a
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m37s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m46s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m28s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m43s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m40s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m20s
Project CI / Frontend tests (pull_request) Successful in 7m41s
Project CI / Repository checks (pull_request) Successful in 7m41s
Project CI / Native shell tests (pull_request) Successful in 11m20s
Project CI / Backend tests (pull_request) Successful in 12m11s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m12s
统一Codex平台布局并补齐macOS原生组件、完整性清单和资源加载路径 补齐macOS插件资源并保留Windows专属原生桥接边界 增加隔离安装包验证、平台配置门禁与侧车回归测试 声明macOS 15最低系统版本并同步规范及Windows待验收计划
223 lines
7.1 KiB
JavaScript
223 lines
7.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
// 只操作临时复制品;不启动 GUI、不读取开发机凭据、不访问 Provider。
|
|
assert.equal(process.platform, 'darwin', '此验证必须在 macOS 执行');
|
|
const source = path.resolve(process.argv[2] || '');
|
|
assert.ok(
|
|
source.endsWith('.app') && fs.statSync(source).isDirectory(),
|
|
'请传入 .app 绝对路径',
|
|
);
|
|
const root = fs.realpathSync(
|
|
fs.mkdtempSync(path.join(os.tmpdir(), 'agc-macos-bundle-')),
|
|
);
|
|
const app = path.join(root, '陶泥儿 隔离测试.app');
|
|
const home = path.join(root, 'home');
|
|
const config = path.join(root, 'config');
|
|
const tmp = path.join(root, 'tmp');
|
|
const codexHome = path.join(root, 'codex-home');
|
|
for (const directory of [home, config, tmp, codexHome]) {
|
|
fs.mkdirSync(directory, { mode: 0o700 });
|
|
}
|
|
const env = {
|
|
HOME: home,
|
|
PATH: '/usr/bin:/bin',
|
|
TMPDIR: tmp,
|
|
CODEX_HOME: codexHome,
|
|
};
|
|
|
|
function run(command, args) {
|
|
const result = spawnSync(command, args, {
|
|
cwd: root,
|
|
env,
|
|
encoding: 'utf8',
|
|
timeout: 30_000,
|
|
maxBuffer: 1024 * 1024,
|
|
});
|
|
assert.ifError(result.error);
|
|
return result;
|
|
}
|
|
|
|
async function hashFile(file) {
|
|
const hash = createHash('sha256');
|
|
for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
|
|
return hash.digest('hex');
|
|
}
|
|
|
|
async function handshake(executable) {
|
|
const child = spawn(executable, ['app-server'], {
|
|
cwd: root,
|
|
env,
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
});
|
|
let buffered = '';
|
|
let stderrBytes = 0;
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
const timer = setTimeout(
|
|
() => reject(new Error('app-server 初始化超时')),
|
|
15_000,
|
|
);
|
|
const finish = (error) => {
|
|
clearTimeout(timer);
|
|
if (error) reject(error);
|
|
else resolve();
|
|
};
|
|
child.on('error', finish);
|
|
child.on('exit', (code) =>
|
|
finish(new Error(`app-server 提前退出 ${code}`)),
|
|
);
|
|
child.stderr.on('data', (chunk) => {
|
|
stderrBytes += chunk.length;
|
|
if (stderrBytes > 1024 * 1024)
|
|
finish(new Error('app-server stderr 超限'));
|
|
});
|
|
child.stdout.on('data', (chunk) => {
|
|
buffered += chunk.toString('utf8');
|
|
if (buffered.length > 1024 * 1024)
|
|
return finish(new Error('app-server stdout 超限'));
|
|
let end;
|
|
while ((end = buffered.indexOf('\n')) >= 0) {
|
|
const line = buffered.slice(0, end);
|
|
buffered = buffered.slice(end + 1);
|
|
try {
|
|
const message = JSON.parse(line);
|
|
if (message.id !== 1) continue;
|
|
assert.ok(message.result?.userAgent, '初始化必须返回真实服务身份');
|
|
assert.equal(message.error, undefined);
|
|
child.stdin.write(`${JSON.stringify({ method: 'initialized' })}\n`);
|
|
finish();
|
|
} catch (error) {
|
|
finish(error);
|
|
}
|
|
}
|
|
});
|
|
child.stdin.on('error', finish);
|
|
child.stdin.write(
|
|
`${JSON.stringify({
|
|
id: 1,
|
|
method: 'initialize',
|
|
params: {
|
|
clientInfo: {
|
|
name: 'agc_bundle_smoke',
|
|
title: 'AGC bundle smoke',
|
|
version: '1',
|
|
},
|
|
capabilities: { experimentalApi: true },
|
|
},
|
|
})}\n`,
|
|
);
|
|
});
|
|
} finally {
|
|
if (child.exitCode === null && child.signalCode === null) {
|
|
await new Promise((resolve) => {
|
|
const timer = setTimeout(() => child.kill('SIGKILL'), 3000);
|
|
child.once('exit', () => {
|
|
clearTimeout(timer);
|
|
resolve();
|
|
});
|
|
child.kill('SIGTERM');
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
fs.cpSync(source, app, { recursive: true });
|
|
const resources = path.join(app, 'Contents/Resources');
|
|
const bundle = path.join(resources, 'coding-agent/mac-native');
|
|
const executable = path.join(bundle, 'bin/codex');
|
|
const main = path.join(
|
|
app,
|
|
'Contents/MacOS/genarrative-ai-game-creator-shell',
|
|
);
|
|
const manifest = JSON.parse(
|
|
fs.readFileSync(path.join(bundle, 'manifest.json'), 'utf8'),
|
|
);
|
|
assert.equal(manifest.schemaVersion, 'genarrative-codex-sidecar.v2');
|
|
assert.equal(
|
|
manifest.platform,
|
|
process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64',
|
|
);
|
|
assert.equal(manifest.version, 'codex-cli 0.147.0');
|
|
const components = [
|
|
'bin/codex',
|
|
'bin/codex-code-mode-host',
|
|
'codex-path/rg',
|
|
'codex-resources/zsh/bin/zsh',
|
|
'codex-package.json',
|
|
];
|
|
assert.deepEqual(Object.keys(manifest.files).sort(), [...components].sort());
|
|
for (const component of components) {
|
|
const file = path.join(bundle, component);
|
|
assert.equal(await hashFile(file), manifest.files[component], component);
|
|
if (component !== 'codex-package.json') {
|
|
fs.accessSync(file, fs.constants.X_OK);
|
|
const arch = run('/usr/bin/lipo', ['-archs', file]);
|
|
assert.equal(arch.status, 0, component);
|
|
assert.equal(
|
|
arch.stdout.trim(),
|
|
process.arch === 'arm64' ? 'arm64' : 'x86_64',
|
|
component,
|
|
);
|
|
}
|
|
}
|
|
assert.ok(fs.existsSync(path.join(bundle, 'NOTICE.md')));
|
|
const plugin = path.join(resources, 'plugins/agc-cocos-editor');
|
|
for (const file of [
|
|
'plugin.json',
|
|
'src/entry.mjs',
|
|
'panels/cocos-editor.html',
|
|
]) {
|
|
assert.ok(fs.existsSync(path.join(plugin, file)), file);
|
|
}
|
|
const packageFiles = fs.readdirSync(resources, { recursive: true });
|
|
assert.ok(
|
|
!packageFiles.some((file) =>
|
|
/(^|\/)(\.env[^/]*|auth\.json|node_modules|target|\.git)(\/|$)|\.(exe|dll)$/.test(
|
|
file,
|
|
),
|
|
),
|
|
);
|
|
assert.equal(run(executable, ['--version']).stdout.trim(), manifest.version);
|
|
assert.equal(
|
|
run(path.join(bundle, 'codex-path/rg'), ['--version']).status,
|
|
0,
|
|
);
|
|
assert.equal(
|
|
run(path.join(bundle, 'codex-resources/zsh/bin/zsh'), ['--version']).status,
|
|
0,
|
|
);
|
|
|
|
// 使用正式 AGC 查找/校验入口,而非只证明 sidecar 可以独立执行。
|
|
const status = run(main, ['--config-dir', config, '--llm-status']);
|
|
const statusText = `${status.stdout}\n${status.stderr}`;
|
|
assert.ok(!statusText.includes('Codex CLI 未安装'), statusText);
|
|
assert.ok(
|
|
statusText.includes('authentication-required'),
|
|
'隔离账号应仅被登录门禁拒绝',
|
|
);
|
|
await handshake(executable);
|
|
|
|
// 临时复制品缺少辅助程序时,正式入口必须拒绝内置程序;PATH 无全局 Codex 可兜底。
|
|
fs.renameSync(
|
|
path.join(bundle, 'bin/codex-code-mode-host'),
|
|
path.join(root, 'saved-code-mode-host'),
|
|
);
|
|
const broken = run(main, ['--config-dir', config, '--llm-status']);
|
|
assert.notEqual(broken.status, 0);
|
|
assert.match(`${broken.stdout}\n${broken.stderr}`, /Codex CLI 未安装/);
|
|
console.log(
|
|
'PASS: 隔离安装包资源、架构、摘要、权限、正式 Codex 查找、app-server 握手及缺组件拒绝',
|
|
);
|
|
console.log(
|
|
'未验证:GUI、真实登录/Provider 对话、Cocos macOS 原生桥接;插件 Node 仍为外部前提',
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|