按评估结论落地:门禁交叉校验、CLI 缺值校验、编辑器分支覆盖与文档
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m19s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m51s
Project CI / Native shell tests (pull_request) Failing after 1m10s
Project CI / Frontend tests (pull_request) Failing after 43s
Project CI / Backend tests (pull_request) Successful in 3m26s
Project CI / Repository checks (pull_request) Successful in 1m51s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m29s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m33s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m34s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m19s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m51s
Project CI / Native shell tests (pull_request) Failing after 1m10s
Project CI / Frontend tests (pull_request) Failing after 43s
Project CI / Backend tests (pull_request) Successful in 3m26s
Project CI / Repository checks (pull_request) Successful in 1m51s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m29s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m33s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m34s
- 门禁新增交叉校验:plugin 必须指向真实插件目录、features 必须存在于 shell crate 的 [features]、所有声明路径必须是仓库内相对路径(拒绝绝对路径与 ..) - 准备步骤 CLI:--target/--destination/--features 缺值直接失败(不再静默回退宿主目标),--features 空值也拒绝 - 用例:fixture 补齐 agc-unity-editor/agc-godot-editor/agc-cocos-editor,新增「编辑器分支产物按 prepared 与 library staging 交付」用例;godot_bundle 补 validate 负例单测 - 运维文档:dev 构建下客户端读 target/debug/plugins/**,改资源要重跑准备步骤 - 未做(已按评估结论记录):P2-2 运行时常量锁定需要先把 nativePayloads 也 emit 进 Rust 声明,留作后续 - 验证:准备步骤 14/14、发布入口 39/39、dev 入口 12/12、godot_bundle 19 通过、声明门禁一致、cargo check 通过
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
// 设计约束:Rust 侧不解析 JSON(避免运行期解析与生命周期妥协),只使用本脚本产出的常量;
|
||||
// Node 侧准备步骤直接读同一份 JSON。因此本门禁是“单一声明”的机械保障。
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
@@ -487,6 +487,46 @@ function appLockedCodexVersion() {
|
||||
return declared.replace(/^[\^~]/, '');
|
||||
}
|
||||
|
||||
const REPO_ROOT = path.resolve(APP_ROOT, '..', '..');
|
||||
const PLUGINS_ROOT = path.join(REPO_ROOT, 'plugins');
|
||||
const SHELL_MANIFEST_PATH = path.join(SRC_TAURI, 'Cargo.toml');
|
||||
|
||||
/// 声明里的路径必须是仓库内相对路径:绝对路径或含 `..` 会写到工作区之外。
|
||||
function assertRelativePath(value, at) {
|
||||
if (
|
||||
path.isAbsolute(value) ||
|
||||
value.startsWith('/') ||
|
||||
value.split('/').includes('..')
|
||||
) {
|
||||
fail(`${at} 必须是仓库内相对路径(不允许绝对路径或 ..):${value}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// 声明引用的插件必须是真实存在的插件目录(拼错插件的后果是静默不交付)。
|
||||
function assertKnownPlugin(name, at) {
|
||||
const directory = path.join(PLUGINS_ROOT, name);
|
||||
if (!existsSync(path.join(directory, 'plugin.json'))) {
|
||||
fail(`${at} 指向的插件不存在或缺少 plugin.json:${name}`);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/// shell crate 的 feature 表(拼错 feature 的后果同样是静默不交付)。
|
||||
function shellCrateFeatures() {
|
||||
const manifest = readFileSync(SHELL_MANIFEST_PATH, 'utf8');
|
||||
const section = /\[features\]([\s\S]*?)(?:\n\[|$)/u.exec(manifest);
|
||||
if (!section) {
|
||||
fail('无法从 src-tauri/Cargo.toml 读取 [features] 段');
|
||||
}
|
||||
return new Set(
|
||||
section[1]
|
||||
.split('\n')
|
||||
.map((line) => /^([A-Za-z0-9_-]+)\s*=/u.exec(line.trim())?.[1])
|
||||
.filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
/// 新 section(prepareSteps / nativePayloads / prepared 来源)不参与 Rust 生成物,
|
||||
/// 必须在门禁里单独把关,避免「声明了却没人用」或引用到不存在的准备步骤。
|
||||
function validateExtendedDeclarations() {
|
||||
@@ -566,6 +606,80 @@ function validateExtendedDeclarations() {
|
||||
}
|
||||
}
|
||||
|
||||
const knownFeatures = shellCrateFeatures();
|
||||
const checkFeatures = (values, at) => {
|
||||
for (const name of expectStringArray(values ?? [], at)) {
|
||||
if (!knownFeatures.has(name)) {
|
||||
fail(`${at} 引用了 Cargo.toml 里不存在的 feature:${name}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const [index, raw] of expectArray(
|
||||
plugins.subdirectories ?? [],
|
||||
'plugins.subdirectories',
|
||||
).entries()) {
|
||||
const at = `plugins.subdirectories[${index}]`;
|
||||
const subdirectory = expectObject(raw, at);
|
||||
assertRelativePath(
|
||||
expectString(subdirectory.path, `${at}.path`),
|
||||
`${at}.path`,
|
||||
);
|
||||
if (subdirectory.plugin) {
|
||||
assertKnownPlugin(subdirectory.plugin, `${at}.plugin`);
|
||||
}
|
||||
checkFeatures(subdirectory.features, `${at}.features`);
|
||||
}
|
||||
for (const [index, raw] of expectArray(
|
||||
plugins.libraryStaging ?? [],
|
||||
'plugins.libraryStaging',
|
||||
).entries()) {
|
||||
const at = `plugins.libraryStaging[${index}]`;
|
||||
const staging = expectObject(raw, at);
|
||||
assertKnownPlugin(
|
||||
expectString(staging.plugin, `${at}.plugin`),
|
||||
`${at}.plugin`,
|
||||
);
|
||||
assertRelativePath(
|
||||
expectString(staging.sourceSubdirectory, `${at}.sourceSubdirectory`),
|
||||
`${at}.sourceSubdirectory`,
|
||||
);
|
||||
for (const relative of expectStringArray(
|
||||
staging.files ?? [],
|
||||
`${at}.files`,
|
||||
)) {
|
||||
assertRelativePath(relative, `${at}.files`);
|
||||
}
|
||||
checkFeatures(staging.features, `${at}.features`);
|
||||
}
|
||||
for (const payload of payloads) {
|
||||
assertRelativePath(
|
||||
payload.destinationSubdirectory,
|
||||
`nativePayloads.${payload.plugin}.destinationSubdirectory`,
|
||||
);
|
||||
checkFeatures(
|
||||
payload.features,
|
||||
`nativePayloads.${payload.plugin}.features`,
|
||||
);
|
||||
}
|
||||
for (const [index, raw] of steps.entries()) {
|
||||
const at = `plugins.prepareSteps[${index}]`;
|
||||
const step = expectObject(raw, at);
|
||||
for (const key of ['workingDirectory', 'packageDirectory']) {
|
||||
if (step[key]) {
|
||||
assertRelativePath(
|
||||
expectString(step[key], `${at}.${key}`),
|
||||
`${at}.${key}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const relative of expectStringArray(
|
||||
step.requiredOutputs ?? [],
|
||||
`${at}.requiredOutputs`,
|
||||
)) {
|
||||
assertRelativePath(relative, `${at}.requiredOutputs`);
|
||||
}
|
||||
}
|
||||
|
||||
const referenced = [
|
||||
...expectArray(plugins.subdirectories ?? [], 'plugins.subdirectories')
|
||||
.filter(
|
||||
|
||||
@@ -1312,15 +1312,25 @@ export function prepareBundledResources({
|
||||
return summaries;
|
||||
}
|
||||
|
||||
function requireFlagValue(argv, index, flag) {
|
||||
const value = argv[index + 1];
|
||||
if (value === undefined || String(value).startsWith('--')) {
|
||||
fail(
|
||||
`${flag} 缺少取值(例如 ${flag} <value>);拒绝回退到宿主默认值,以免准备错目标`,
|
||||
);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const args = { target: undefined, destinationRoot: undefined, dryRun: false };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const value = argv[index];
|
||||
if (value === '--target') {
|
||||
args.target = argv[index + 1];
|
||||
args.target = requireFlagValue(argv, index, '--target');
|
||||
index += 1;
|
||||
} else if (value === '--destination') {
|
||||
args.destinationRoot = argv[index + 1];
|
||||
args.destinationRoot = requireFlagValue(argv, index, '--destination');
|
||||
index += 1;
|
||||
} else if (value === '--dry-run') {
|
||||
args.dryRun = true;
|
||||
|
||||
@@ -140,6 +140,21 @@ function buildFixture({ targets = [WINDOWS_TARGET], plugins = true } = {}) {
|
||||
fs.writeFileSync(path.join(pluginRoot, '.git/HEAD'), 'ref\n');
|
||||
fs.writeFileSync(path.join(pluginRoot, '.env'), 'secret\n');
|
||||
|
||||
for (const name of [
|
||||
'agc-cocos-editor',
|
||||
'agc-unity-editor',
|
||||
'agc-godot-editor',
|
||||
]) {
|
||||
const root = path.join(repoRoot, 'plugins', name);
|
||||
fs.mkdirSync(path.join(root, 'src'), {
|
||||
recursive: true,
|
||||
});
|
||||
fs.writeFileSync(path.join(root, 'plugin.json'), `{"name":"${name}"}\n`);
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'src/entry.mjs'),
|
||||
`export const ${name} = 1;\n`,
|
||||
);
|
||||
}
|
||||
const cocosRoot = path.join(repoRoot, 'plugins/agc-cocos-editor');
|
||||
fs.mkdirSync(path.join(cocosRoot, 'src'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
@@ -515,7 +530,7 @@ test('declaration drives source lookup and staging units', () => {
|
||||
source,
|
||||
/codex-win32-x64[\\/]vendor[\\/]x86_64-pc-windows-msvc$/u,
|
||||
);
|
||||
assert.equal(pluginDirectories(declaration, fixture.repoRoot).length, 2);
|
||||
assert.equal(pluginDirectories(declaration, fixture.repoRoot).length, 4);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
@@ -605,3 +620,42 @@ test('fails closed when a prepare step does not produce its declared outputs', (
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('delivers editor branch artifacts declared as prepared or library staging', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
const features = new Set([
|
||||
'unity-editor-execute',
|
||||
'godot-editor-execute',
|
||||
'cocos-editor-injection',
|
||||
]);
|
||||
prepare(fixture, { features });
|
||||
const staged = (relative) =>
|
||||
path.join(fixture.destinationRoot, 'resources/plugins', relative);
|
||||
|
||||
// Unity:prepared 子目录整体复制
|
||||
assert.ok(
|
||||
fs.existsSync(
|
||||
staged('agc-unity-editor/dotnet/publish/win-x64/Agc.Unity.Attach.exe'),
|
||||
),
|
||||
'Unity helper 必须随包',
|
||||
);
|
||||
// Godot:libraryStaging 声明文件逐个复制
|
||||
assert.ok(
|
||||
fs.existsSync(
|
||||
staged(
|
||||
'agc-godot-editor/native/gdextension/bin/win-x64/agc_godot_editor.dll',
|
||||
),
|
||||
),
|
||||
'Godot 扩展必须随包',
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(
|
||||
staged('agc-godot-editor/native/gdextension/vendor/provenance.json'),
|
||||
),
|
||||
'Godot 随包清单文件必须随包',
|
||||
);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -99,4 +99,22 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_incomplete_bundle() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let error = validate(temp.path()).expect_err("空目录必须被拒绝");
|
||||
assert!(!error.is_empty(), "失败原因不能为空");
|
||||
|
||||
// 只有文件名、内容不合法的「像样」目录也必须被拒绝。
|
||||
for relative in BUNDLE_FILES {
|
||||
let file = temp.path().join(relative);
|
||||
fs::create_dir_all(file.parent().expect("parent")).expect("create dir");
|
||||
fs::write(&file, b"not a real artifact").expect("write file");
|
||||
}
|
||||
assert!(
|
||||
validate(temp.path()).is_err(),
|
||||
"内容不合法的随包库必须被拒绝",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user