8a8d94c14a
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 16s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 16s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 12s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 16s
Project CI / Backend tests (pull_request) Failing after 21s
Project CI / Native shell tests (pull_request) Failing after 20s
Project CI / Frontend tests (pull_request) Failing after 8s
Project CI / AI game creator shell web tests (pull_request) Failing after 17s
Project CI / Repository checks (pull_request) Failing after 17s
- 自动环境预检接入首页与 Direct 后端:新建 Web 游戏在生成前完成 Node/npm、真实构建与双端浏览器检查 - 新增宿主权威执行账本与交付合同:冻结核验项、绑定当前输入证据、封口后拒绝新副作用并产出宿主报告 - 原生 shell、内置浏览器/托管命令与第三方 MCP 共用执行许可和累计执行时间预算,按执行批次与墙钟分别约束 - 新增固定种子 runner 物理基线、真实双端输入与公平窗口负例,可复用且不冒充完整关卡 - 新增请求与工具分段计时、有界并行批读和首轮上下文预取,未知耗时不补零 - 移除 SDK 全局串行的 apply_patch/update_plan 注册,改由宿主 agc_apply_patch/agc_update_plan 提供等价能力,独立工具可并发 - 付费提交与本地写入绑定原回合租约:封口、取消或耗尽后零新增提交,已受理操作保留 GET 对账 - 捆绑 Codex 由 0.147.0 升级到 0.155.1,固定版本收敛到 build_support/codex_bundle.rs,vendor 解析源码按 rust-v0.155.1 重取并更新 UPSTREAM 证据 - 适配 0.155 统一 exec(exec_command/write_stdin),生产夹具新增会话用例,宿主 Job 与期限约束保持有效 - 同步更新 AGC 主规范、Skill 与提示词、共享记忆和发行载荷校验
276 lines
9.0 KiB
JavaScript
276 lines
9.0 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-')),
|
|
);
|
|
// 侧车清单版本必须等于锁定的 @openai/codex 版本,避免两处固定版本漂移。
|
|
const appPackage = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(
|
|
path.dirname(new URL(import.meta.url).pathname),
|
|
'../package.json',
|
|
),
|
|
'utf8',
|
|
),
|
|
);
|
|
const pinnedCodexVersion =
|
|
appPackage.dependencies?.['@openai/codex'] ??
|
|
appPackage.devDependencies?.['@openai/codex'] ??
|
|
appPackage.optionalDependencies?.['@openai/codex'];
|
|
assert.match(
|
|
pinnedCodexVersion,
|
|
/^\d+\.\d+\.\d+$/u,
|
|
'package.json 必须锁定精确的 @openai/codex 版本',
|
|
);
|
|
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 ${pinnedCodexVersion}`);
|
|
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 nodeRoot = path.join(resources, 'game-runtime/node');
|
|
const nodeManifest = JSON.parse(
|
|
fs.readFileSync(path.join(nodeRoot, 'manifest.json'), 'utf8'),
|
|
);
|
|
assert.equal(nodeManifest.schemaVersion, 'agc-node-runtime.v1');
|
|
assert.equal(nodeManifest.platform, 'darwin');
|
|
assert.equal(nodeManifest.arch, process.arch);
|
|
const runtimeFiles = fs
|
|
.readdirSync(nodeRoot, { recursive: true })
|
|
.filter(
|
|
(file) =>
|
|
fs.statSync(path.join(nodeRoot, file)).isFile() &&
|
|
file !== 'manifest.json',
|
|
);
|
|
assert.deepEqual(runtimeFiles.sort(), Object.keys(nodeManifest.files).sort());
|
|
for (const [file, digest] of Object.entries(nodeManifest.files)) {
|
|
assert.equal(await hashFile(path.join(nodeRoot, file)), digest, file);
|
|
}
|
|
assert.ok(nodeManifest.files['NODE-LICENSE']);
|
|
assert.ok(nodeManifest.files['node_modules/npm/LICENSE']);
|
|
assert.equal(
|
|
run(path.join(nodeRoot, 'node'), ['--version']).stdout.trim(),
|
|
nodeManifest.nodeVersion,
|
|
);
|
|
assert.equal(
|
|
run(path.join(nodeRoot, 'node'), [
|
|
path.join(nodeRoot, 'node_modules/npm/bin/npm-cli.js'),
|
|
'--version',
|
|
]).stdout.trim(),
|
|
nodeManifest.npmVersion,
|
|
);
|
|
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|target|\.git)(\/|$)|\.(exe|dll)$/.test(
|
|
file,
|
|
) ||
|
|
(/(^|\/)node_modules(\/|$)/.test(file) &&
|
|
!file.startsWith('game-runtime/node/node_modules/npm')),
|
|
),
|
|
);
|
|
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 });
|
|
}
|