修复AGC随包资源staging边界
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m34s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 2m3s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m7s
Project CI / Repository checks (pull_request) Failing after 25s
Project CI / Native shell tests (pull_request) Failing after 1m44s
Project CI / Frontend tests (pull_request) Successful in 2m12s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m3s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m30s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m34s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 2m3s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m7s
Project CI / Repository checks (pull_request) Failing after 25s
Project CI / Native shell tests (pull_request) Failing after 1m44s
Project CI / Frontend tests (pull_request) Successful in 2m12s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m3s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m30s
修复 --no-bundle 构建前的资源准备接线。 拒绝插件随包目录中的未声明文件并同步Rust校验。 增加staging替换失败后的旧资源恢复与回归测试。
This commit is contained in:
@@ -505,10 +505,11 @@ export function runTauriBuild(
|
||||
const tauriArguments = buildTauriBuildArguments(args, context.target);
|
||||
const { channel, target } = context;
|
||||
const features = resolveEditorFeatures({ argv: args, target });
|
||||
// --no-bundle 只跳过发行运行时资源;应用自身构建仍需先准备随包资源。
|
||||
if (!args.includes('--no-bundle')) {
|
||||
stageRuntime(target);
|
||||
stageBundled(target, { features });
|
||||
}
|
||||
stageBundled(target, { features });
|
||||
const configPath = writeChannelConfigFile(
|
||||
channel,
|
||||
target,
|
||||
|
||||
@@ -1001,6 +1001,7 @@ test('release stages Node before Tauri and injects its resource mapping only for
|
||||
events.push('bundled');
|
||||
},
|
||||
spawn(_binary, args) {
|
||||
events.push('build');
|
||||
const config = JSON.parse(
|
||||
readFileSync(args[args.lastIndexOf('--config') + 1], 'utf8'),
|
||||
);
|
||||
@@ -1013,6 +1014,7 @@ test('release stages Node before Tauri and injects its resource mapping only for
|
||||
return { status: 0 };
|
||||
},
|
||||
});
|
||||
assert.deepEqual(events, ['bundled', 'build']);
|
||||
assert.throws(
|
||||
() =>
|
||||
runTauriBuild(['--target', windowsTarget], context, {
|
||||
|
||||
@@ -485,6 +485,7 @@ function renderLibraryStaging(entry) {
|
||||
['targets', rustStrings(entry.targets)],
|
||||
['features', rustStrings(entry.features)],
|
||||
['layout', rustString(entry.layout)],
|
||||
['files', rustStrings(entry.files)],
|
||||
],
|
||||
1,
|
||||
);
|
||||
|
||||
@@ -488,8 +488,11 @@ function buildUnitInto(
|
||||
}
|
||||
|
||||
function stageAtomically(unitPath, builder) {
|
||||
const stagingPath = `${unitPath}-staging-${process.pid}-${randomBytes(4).toString('hex')}`;
|
||||
const suffix = `${process.pid}-${randomBytes(4).toString('hex')}`;
|
||||
const stagingPath = `${unitPath}-staging-${suffix}`;
|
||||
const backupPath = `${unitPath}-backup-${suffix}`;
|
||||
rmSync(stagingPath, { recursive: true, force: true });
|
||||
rmSync(backupPath, { recursive: true, force: true });
|
||||
mkdirSync(stagingPath, { recursive: true });
|
||||
try {
|
||||
builder(stagingPath);
|
||||
@@ -497,15 +500,31 @@ function stageAtomically(unitPath, builder) {
|
||||
rmSync(stagingPath, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
rmSync(unitPath, { recursive: true, force: true });
|
||||
|
||||
const hadPrevious = existsSync(unitPath);
|
||||
try {
|
||||
if (hadPrevious) {
|
||||
renameSync(unitPath, backupPath);
|
||||
}
|
||||
renameSync(stagingPath, unitPath);
|
||||
} catch (error) {
|
||||
// 并发调用未做加锁(见技术方案 §4.3):这里只保证失败可读、且不丢已经生成好的 staging。
|
||||
let restored = !hadPrevious;
|
||||
if (hadPrevious && existsSync(backupPath)) {
|
||||
try {
|
||||
if (existsSync(unitPath)) {
|
||||
rmSync(unitPath, { recursive: true, force: true });
|
||||
}
|
||||
renameSync(backupPath, unitPath);
|
||||
restored = true;
|
||||
} catch {
|
||||
restored = false;
|
||||
}
|
||||
}
|
||||
fail(
|
||||
`替换 ${unitPath} 失败(${error.code ?? error.message}):同一资源目录可能正被另一个准备步骤进程写入;staging 已保留在 ${stagingPath},确认没有并发进程后重试`,
|
||||
`替换 ${unitPath} 失败(${error.code ?? error.message}):staging 保留在 ${stagingPath};旧资源${restored ? '已恢复' : '恢复失败,请检查 ' + backupPath},确认没有并发进程后重试`,
|
||||
);
|
||||
}
|
||||
rmSync(backupPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function prepareCodex({
|
||||
@@ -860,6 +879,30 @@ function subdirectoryEnabled(subdirectory, target, features) {
|
||||
return matchesContains && matchesTarget && matchesFeatures;
|
||||
}
|
||||
|
||||
function collectTreeFiles(root, label) {
|
||||
const files = [];
|
||||
const stack = [['', root]];
|
||||
while (stack.length > 0) {
|
||||
const [prefix, directory] = stack.pop();
|
||||
if (!existsSync(directory)) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
||||
const entryPath = path.join(directory, entry.name);
|
||||
if (entry.isSymbolicLink()) {
|
||||
fail(`${label}不允许符号链接:${entryPath}`);
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
stack.push([relative, entryPath]);
|
||||
} else if (entry.isFile()) {
|
||||
files.push(relative);
|
||||
}
|
||||
}
|
||||
}
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
/// 复制规则由各 section 自带(plugins / claudeAgent),同名语义、同序判定。
|
||||
function skipDirectory(rules, name) {
|
||||
return (
|
||||
@@ -958,7 +1001,7 @@ function pluginSourceFingerprint(declaration, plugins, target, features) {
|
||||
for (const staging of declaration.plugins.libraryStaging ?? []) {
|
||||
if (
|
||||
plugin.name !== staging.plugin ||
|
||||
libraryStagingEnabled(staging, target, features)
|
||||
!libraryStagingEnabled(staging, target, features)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -978,7 +1021,7 @@ function pluginSourceFingerprint(declaration, plugins, target, features) {
|
||||
for (const payload of declaration.plugins.nativePayloads ?? []) {
|
||||
if (
|
||||
plugin.name !== payload.plugin ||
|
||||
nativePayloadEnabled(payload, target, features)
|
||||
!nativePayloadEnabled(payload, target, features)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -1007,8 +1050,11 @@ function pluginTreeMatches(
|
||||
if (!existsSync(destination)) {
|
||||
return false;
|
||||
}
|
||||
const allowedFiles = new Set();
|
||||
for (const plugin of plugins) {
|
||||
const pluginDestination = path.join(destination, plugin.name);
|
||||
const pluginPrefix = `${plugin.name}/`;
|
||||
allowedFiles.add(`${pluginPrefix}${declaration.plugins.manifestFileName}`);
|
||||
if (
|
||||
!existsSync(
|
||||
path.join(pluginDestination, declaration.plugins.manifestFileName),
|
||||
@@ -1022,17 +1068,24 @@ function pluginTreeMatches(
|
||||
}
|
||||
const stagedDirectory = path.join(pluginDestination, subdirectory.path);
|
||||
if (!subdirectoryEnabled(subdirectory, target, features)) {
|
||||
// 当前目标/feature 下不该出现的子目录:存在即说明是别的构建留下的,必须重建清理。
|
||||
if (existsSync(stagedDirectory)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const relativePrefix = `${plugin.name}/${subdirectory.path}/`;
|
||||
const sourceRoot = path.join(plugin.root, subdirectory.path);
|
||||
if (subdirectory.origin !== 'source') {
|
||||
if (existsSync(sourceRoot) && !existsSync(stagedDirectory)) {
|
||||
return false;
|
||||
}
|
||||
for (const relative of walkFiles(
|
||||
declaration.plugins,
|
||||
sourceRoot,
|
||||
'插件资源',
|
||||
)) {
|
||||
allowedFiles.add(`${relativePrefix}${relative}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (const relative of walkFiles(
|
||||
@@ -1046,6 +1099,7 @@ function pluginTreeMatches(
|
||||
subdirectory.path,
|
||||
relative,
|
||||
);
|
||||
allowedFiles.add(`${relativePrefix}${relative}`);
|
||||
if (!existsSync(staged)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1057,8 +1111,32 @@ function pluginTreeMatches(
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const staging of declaration.plugins.libraryStaging ?? []) {
|
||||
if (
|
||||
plugin.name === staging.plugin &&
|
||||
libraryStagingEnabled(staging, target, features)
|
||||
) {
|
||||
for (const relative of staging.files) {
|
||||
allowedFiles.add(
|
||||
`${plugin.name}/${staging.sourceSubdirectory}/${relative}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const payload of declaration.plugins.nativePayloads ?? []) {
|
||||
if (
|
||||
plugin.name === payload.plugin &&
|
||||
nativePayloadEnabled(payload, target, features)
|
||||
) {
|
||||
allowedFiles.add(
|
||||
`${plugin.name}/${payload.destinationSubdirectory}/${payload.destinationFileName ?? payload.sourceFileName}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return collectTreeFiles(destination, '插件随包目录').every((relative) =>
|
||||
allowedFiles.has(relative),
|
||||
);
|
||||
}
|
||||
|
||||
function assertOwnedPluginRoot(destination, plugins) {
|
||||
@@ -1504,10 +1582,16 @@ function preparePlugins({
|
||||
});
|
||||
return {
|
||||
summary: `plugins 重新生成(${plugins.length} 个插件,写入 ${declaration.plugins.destinationDirectory})`,
|
||||
record: { fingerprint },
|
||||
record: {
|
||||
fingerprint: pluginSourceFingerprint(
|
||||
declaration,
|
||||
plugins,
|
||||
target,
|
||||
features,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// 准备全部随包资源;返回逐条汇总,供入口日志与测试断言。
|
||||
export function prepareBundledResources({
|
||||
target = resolveHostTarget(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import { syncBuiltinESMExports } from 'node:module';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { test } from 'node:test';
|
||||
@@ -286,6 +287,41 @@ function prepare(fixture, overrides = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 替换失败注入:让「staging → 目标目录」的那一次 rename 抛错。
|
||||
/// 走 node:fs 的 ESM 活绑定(syncBuiltinESMExports 同步),实现里不得为测试开后门;
|
||||
/// body 是同步的,注入窗口内不会有别的调用跑进来。
|
||||
function withRenameFailure(predicate, body) {
|
||||
const original = fs.renameSync;
|
||||
let injected = false;
|
||||
fs.renameSync = (from, to) => {
|
||||
if (
|
||||
!injected &&
|
||||
predicate(path.resolve(String(from)), path.resolve(String(to)))
|
||||
) {
|
||||
injected = true;
|
||||
throw Object.assign(new Error('注入的替换失败'), { code: 'EPERM' });
|
||||
}
|
||||
return original.call(fs, from, to);
|
||||
};
|
||||
syncBuiltinESMExports();
|
||||
try {
|
||||
return body();
|
||||
} finally {
|
||||
fs.renameSync = original;
|
||||
syncBuiltinESMExports();
|
||||
}
|
||||
}
|
||||
|
||||
/// 把「必须失败」的调用收敛成断言:返回错误对象,没抛错即用例失败。
|
||||
function expectThrow(body) {
|
||||
try {
|
||||
body();
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
throw new Error('预期抛错但调用成功了');
|
||||
}
|
||||
|
||||
test('stages declared codex components with manifest and preserved notice', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
@@ -824,3 +860,300 @@ test('delivers editor branch artifacts declared as prepared or library staging',
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
/// 全量 Windows 编辑器 feature:prepared / libraryStaging / nativePayload 三条交付路径全开。
|
||||
const ALL_WINDOWS_FEATURES = new Set([
|
||||
'unity-editor-execute',
|
||||
'godot-editor-execute',
|
||||
'cocos-editor-injection',
|
||||
]);
|
||||
|
||||
function pluginStaged(fixture, relative) {
|
||||
return path.join(
|
||||
fixture.destinationRoot,
|
||||
fixture.declaration.plugins.destinationDirectory,
|
||||
relative,
|
||||
);
|
||||
}
|
||||
|
||||
test('keeps the previous codex resources when the replacement fails', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
prepare(fixture);
|
||||
const declaration = fixture.declaration;
|
||||
const layout = declaration.codex.targets.find(
|
||||
(entry) => entry.target === WINDOWS_TARGET,
|
||||
);
|
||||
const unit = path.join(
|
||||
fixture.destinationRoot,
|
||||
declaration.codex.resourceDirectory,
|
||||
layout.directory,
|
||||
);
|
||||
const before = snapshot(unit);
|
||||
const component = layout.files.find((relative) =>
|
||||
relative.includes('code-mode-host'),
|
||||
);
|
||||
const vendor = path.join(
|
||||
fixture.appRoot,
|
||||
`node_modules/@openai/codex-${layout.platform}/vendor/${WINDOWS_TARGET}`,
|
||||
);
|
||||
// 上游组件变了:这一次必然走「staging → 目标」的替换,注入的失败正好落在替换上。
|
||||
fs.writeFileSync(path.join(vendor, component), 'changed component\n');
|
||||
// 锁定包未变化时 Codex 走缓存;移除记录强制重新读取上游内容。
|
||||
fs.rmSync(fixture.recordPath);
|
||||
|
||||
const error = expectThrow(() =>
|
||||
withRenameFailure(
|
||||
(from, to) => to === path.resolve(unit),
|
||||
() => prepare(fixture),
|
||||
),
|
||||
);
|
||||
assert.match(error.message, /替换 .*win-x64.* 失败/u);
|
||||
assert.match(error.message, /旧资源已恢复/u);
|
||||
assert.deepEqual(
|
||||
snapshot(unit),
|
||||
before,
|
||||
'替换失败必须把旧资源原样恢复:内容、尺寸、时间戳与可执行位都不许变',
|
||||
);
|
||||
assert.equal(
|
||||
fs.readFileSync(
|
||||
path.join(unit, declaration.codex.noticeFileName),
|
||||
'utf8',
|
||||
),
|
||||
'windows codex notice\n',
|
||||
'受版本控制的第三方声明必须还在原位',
|
||||
);
|
||||
const stagingPath = /staging 保留在 ([^;]+);/u.exec(error.message)?.[1];
|
||||
assert.ok(stagingPath, `报错必须给出 staging 位置:${error.message}`);
|
||||
// 目标目录旁边只允许剩「旧资源」和「留给人工检查的新 staging」;backup 必须已经归位。
|
||||
const siblings = fs.readdirSync(path.dirname(unit));
|
||||
assert.ok(siblings.includes(path.basename(unit)));
|
||||
assert.ok(siblings.includes(path.basename(stagingPath)));
|
||||
assert.ok(
|
||||
siblings.every((entry) => !entry.includes('-backup-')),
|
||||
'恢复成功后不得留下 backup 目录',
|
||||
);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(stagingPath, component), 'utf8'),
|
||||
'changed component\n',
|
||||
'没有落盘的新产物必须留在 staging 里供人工检查',
|
||||
);
|
||||
|
||||
// 注入消失后重试必须成功:旧资源完整 → 替换重新做一遍 → 落盘的是上游新内容。
|
||||
const summaries = prepare(fixture);
|
||||
assert.match(summaries[0], /重新生成/u);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(unit, component), 'utf8'),
|
||||
'changed component\n',
|
||||
);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps the previous plugin resources when the replacement fails', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
prepare(fixture);
|
||||
const destination = path.join(
|
||||
fixture.destinationRoot,
|
||||
fixture.declaration.plugins.destinationDirectory,
|
||||
);
|
||||
// 未声明的额外文件让缓存判定必然漂移:这次一定会走完整替换。
|
||||
fs.writeFileSync(
|
||||
path.join(destination, 'agc-demo-editor/src/rogue.mjs'),
|
||||
'rogue\n',
|
||||
);
|
||||
const before = snapshot(destination);
|
||||
|
||||
const error = expectThrow(() =>
|
||||
withRenameFailure(
|
||||
(from, to) => to === path.resolve(destination),
|
||||
() => prepare(fixture),
|
||||
),
|
||||
);
|
||||
assert.match(error.message, /替换 .*plugins.* 失败/u);
|
||||
assert.match(error.message, /旧资源已恢复/u);
|
||||
assert.deepEqual(
|
||||
snapshot(destination),
|
||||
before,
|
||||
'替换失败时旧插件资源(含尚未清理的额外文件)必须逐字节保留',
|
||||
);
|
||||
|
||||
prepare(fixture);
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(destination, 'agc-demo-editor/src/rogue.mjs')),
|
||||
'注入消失后重试必须成功并清掉额外文件',
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(
|
||||
path.join(destination, 'agc-demo-editor/panels/panel.html'),
|
||||
),
|
||||
'重试后声明的随包内容必须齐全',
|
||||
);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('clears undeclared and hidden entries left in the staged plugin tree', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
prepare(fixture);
|
||||
// 三种残留:额外目录、隐藏目录、声明的源码目录里的隐藏文件。
|
||||
for (const relative of [
|
||||
'agc-demo-editor/extra/rogue.txt',
|
||||
'agc-demo-editor/.cache/tmp.bin',
|
||||
'agc-demo-editor/src/.env',
|
||||
]) {
|
||||
const file = pluginStaged(fixture, relative);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, 'residue\n');
|
||||
}
|
||||
|
||||
const summaries = prepare(fixture);
|
||||
assert.match(summaries[1], /plugins 重新生成/u);
|
||||
for (const relative of [
|
||||
'agc-demo-editor/extra',
|
||||
'agc-demo-editor/.cache',
|
||||
'agc-demo-editor/src/.env',
|
||||
]) {
|
||||
assert.ok(
|
||||
!fs.existsSync(pluginStaged(fixture, relative)),
|
||||
`未声明的残留 ${relative} 必须被清掉`,
|
||||
);
|
||||
}
|
||||
for (const relative of [
|
||||
'agc-demo-editor/plugin.json',
|
||||
'agc-demo-editor/src/entry.mjs',
|
||||
'agc-demo-editor/panels/panel.html',
|
||||
]) {
|
||||
assert.ok(
|
||||
fs.existsSync(pluginStaged(fixture, relative)),
|
||||
`声明的随包内容 ${relative} 必须还在`,
|
||||
);
|
||||
}
|
||||
assert.ok(
|
||||
!fs.existsSync(
|
||||
pluginStaged(fixture, 'agc-demo-editor/panels/panel.test.mjs'),
|
||||
),
|
||||
'跳过规则在重建后依然生效',
|
||||
);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('removes staged plugin files whose source has been deleted', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
prepare(fixture);
|
||||
assert.ok(
|
||||
fs.existsSync(pluginStaged(fixture, 'agc-demo-editor/panels/panel.html')),
|
||||
'前置条件:源码里的文件已经随包',
|
||||
);
|
||||
fs.rmSync(
|
||||
path.join(fixture.repoRoot, 'plugins/agc-demo-editor/panels/panel.html'),
|
||||
);
|
||||
|
||||
const summaries = prepare(fixture);
|
||||
assert.match(summaries[1], /plugins 重新生成/u);
|
||||
assert.ok(
|
||||
!fs.existsSync(
|
||||
pluginStaged(fixture, 'agc-demo-editor/panels/panel.html'),
|
||||
),
|
||||
'源码里已删除的文件不得留在随包目录',
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(pluginStaged(fixture, 'agc-demo-editor/src/entry.mjs')),
|
||||
'同一插件的其他声明内容必须还在',
|
||||
);
|
||||
// 重建后的目录必须自洽:紧接着一次准备应命中缓存而不是反复重建。
|
||||
assert.match(prepare(fixture)[1], /命中缓存/u);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('clears staged prepared payloads when their feature is disabled', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
prepare(fixture, { features: ALL_WINDOWS_FEATURES });
|
||||
for (const relative of [
|
||||
'agc-cocos-editor/native/payload/cocos-editor-bridge.dll',
|
||||
'agc-unity-editor/dotnet/publish/win-x64/Agc.Unity.Attach.exe',
|
||||
'agc-godot-editor/native/gdextension/bin/win-x64/agc_godot_editor.dll',
|
||||
'agc-godot-editor/native/gdextension/vendor/provenance.json',
|
||||
]) {
|
||||
assert.ok(
|
||||
fs.existsSync(pluginStaged(fixture, relative)),
|
||||
`前置条件:feature 全开时 ${relative} 必须随包`,
|
||||
);
|
||||
}
|
||||
|
||||
prepare(fixture, { features: new Set() });
|
||||
for (const relative of [
|
||||
'agc-cocos-editor/native/payload',
|
||||
'agc-unity-editor/dotnet',
|
||||
'agc-godot-editor/native',
|
||||
]) {
|
||||
assert.ok(
|
||||
!fs.existsSync(pluginStaged(fixture, relative)),
|
||||
`feature 关闭后 ${relative} 不得作为残留继续随包`,
|
||||
);
|
||||
}
|
||||
for (const plugin of [
|
||||
'agc-cocos-editor',
|
||||
'agc-unity-editor',
|
||||
'agc-godot-editor',
|
||||
]) {
|
||||
assert.ok(
|
||||
fs.existsSync(pluginStaged(fixture, `${plugin}/plugin.json`)),
|
||||
`feature 关闭不得动到 ${plugin} 的声明内容`,
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(pluginStaged(fixture, `${plugin}/src/entry.mjs`)),
|
||||
`feature 关闭不得动到 ${plugin} 的源码内容`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('treats declared prepared and library-staging artifacts as owned on a second run', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
prepare(fixture, { features: ALL_WINDOWS_FEATURES });
|
||||
const destination = path.join(
|
||||
fixture.destinationRoot,
|
||||
fixture.declaration.plugins.destinationDirectory,
|
||||
);
|
||||
const before = snapshot(destination);
|
||||
|
||||
const summaries = prepare(fixture, { features: ALL_WINDOWS_FEATURES });
|
||||
assert.match(
|
||||
summaries[1],
|
||||
/plugins 命中缓存(未写入/u,
|
||||
'prepared 子目录与 libraryStaging 产物属于本工具,不得被误判成外来内容而反复重建',
|
||||
);
|
||||
assert.deepEqual(
|
||||
snapshot(destination),
|
||||
before,
|
||||
'命中缓存时一个字节、一个时间戳都不许动',
|
||||
);
|
||||
for (const relative of [
|
||||
'agc-cocos-editor/native/payload/cocos-editor-bridge.dll',
|
||||
'agc-unity-editor/dotnet/publish/win-x64/Agc.Unity.Attach.exe',
|
||||
'agc-godot-editor/native/gdextension/bin/win-x64/agc_godot_editor.dll',
|
||||
'agc-godot-editor/native/gdextension/vendor/provenance.json',
|
||||
]) {
|
||||
assert.ok(
|
||||
fs.existsSync(pluginStaged(fixture, relative)),
|
||||
`已声明产物 ${relative} 不得因为缓存判定被删掉`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user