Files
Genarrative/apps/ai-game-creator-shell/scripts/check-config.mjs
T
kdletters 83f07fc58d
Project CI / Repository checks (push) Failing after 1m2s
Project CI / Frontend tests (push) Successful in 3m19s
Project CI / Backend tests (push) Successful in 4m18s
Project CI / Native shell tests (push) Failing after 11m47s
统一 AGC 开发端口分配
将 AGC Vite 纳入 Linux 用户端口段第六槽位
同步 Tauri devUrl、Vite 监听和配套后端端口预留
兼容迁移旧五端口注册记录并阻止重复分配
补齐动态配置顺序、跨平台和进程生命周期回归测试
更新开发运维文档、端口 skill 与项目共享记忆
2026-08-08 16:18:45 +08:00

1773 lines
53 KiB
JavaScript

import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { EventEmitter } from 'node:events';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
appIdentifier,
defaultRealSwarmTestTask,
} from './agent-swarm-test-chat.mjs';
import {
askHidden,
assertSafeGameCreatorConfigDestination,
buildGameCreatorWizardConfig,
readGameCreatorWizardConfigState,
writeGameCreatorConfigAtomically,
writeGameCreatorWizardConfig,
} from './game-creator-config-wizard.mjs';
const packageConfig = JSON.parse(
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
);
const tauriConfig = JSON.parse(
fs.readFileSync(
new URL('../src-tauri/tauri.conf.json', import.meta.url),
'utf8',
),
);
const gameChatReleaseTauriConfig = JSON.parse(
fs.readFileSync(
new URL('../src-tauri/tauri.game-chat-release.conf.json', import.meta.url),
'utf8',
),
);
const cargoManifestSource = fs.readFileSync(
new URL('../src-tauri/Cargo.toml', import.meta.url),
'utf8',
);
const cargoPackageVersion = cargoManifestSource
.split(/\r?\n(?=\[)/u)
.find((section) => section.startsWith('[package]'))
?.match(/^version\s*=\s*"([^"]+)"\s*$/mu)?.[1];
const eventCapabilityPath = new URL(
'../src-tauri/capabilities/events.json',
import.meta.url,
);
const defaultAppConfig = JSON.parse(
fs.readFileSync(
new URL('../game-creator.config.json', import.meta.url),
'utf8',
),
);
const rootPackageConfig = JSON.parse(
fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'),
);
const swarmTestChatSource = fs.readFileSync(
new URL('../scripts/agent-swarm-test-chat.mjs', import.meta.url),
'utf8',
);
const viteConfigSource = fs.readFileSync(
new URL('../vite.config.ts', import.meta.url),
'utf8',
);
const devPortSource = fs.readFileSync(
new URL('../scripts/dev-port.mjs', import.meta.url),
'utf8',
);
const startTauriDevSource = fs.readFileSync(
new URL('../scripts/start-tauri-dev.mjs', import.meta.url),
'utf8',
);
const appSource = [
readSourceTree(new URL('../src/', import.meta.url), '.ts'),
readSourceTree(new URL('../src/', import.meta.url), '.tsx'),
].join('\n');
const appInvokeSource = appSource;
const appEntrypointSource = fs.readFileSync(
new URL('../src/main.tsx', import.meta.url),
'utf8',
);
const appModuleSource = fs.readFileSync(
new URL('../src/App.tsx', import.meta.url),
'utf8',
);
const gameChatReleaseBuildSource = fs.readFileSync(
new URL('../scripts/build-game-chat-release.mjs', import.meta.url),
'utf8',
);
const tauriHandlerSource = fs.readFileSync(
new URL('../src-tauri/src/main.rs', import.meta.url),
'utf8',
);
const tauriWindowSource = fs.readFileSync(
new URL('../src-tauri/src/windows.rs', import.meta.url),
'utf8',
);
const tauriRustSource = readSourceTree(
new URL('../src-tauri/src/', import.meta.url),
'.rs',
);
const sharedContractSource = fs.readFileSync(
new URL(
'../../../packages/shared/src/contracts/gameCreationApp.ts',
import.meta.url,
),
'utf8',
);
const rustSharedContractSource = fs.readFileSync(
new URL(
'../../../server-rs/crates/shared-contracts/src/game_creation_app.rs',
import.meta.url,
),
'utf8',
);
const allowedUncalledTauriCommands = [
'chat_with_game_creator_agent',
'open_game_creator_launcher_window',
'open_game_creator_workspace_window',
];
const sourceExtensions = new Set([
'.json',
'.md',
'.mjs',
'.rs',
'.toml',
'.ts',
'.tsx',
]);
function collectFiles(path) {
const stat = fs.statSync(path);
if (stat.isDirectory()) {
return fs.readdirSync(path, { withFileTypes: true }).flatMap((entry) => {
if (entry.name === 'node_modules' || entry.name === 'target') {
return [];
}
return collectFiles(
new URL(`${entry.name}${entry.isDirectory() ? '/' : ''}`, path),
);
});
}
if (sourceExtensions.has(pathnameExtension(path.pathname))) {
return [path];
}
return [];
}
function readSourceTree(path, extension) {
const stat = fs.statSync(path);
if (stat.isDirectory()) {
return fs
.readdirSync(path, { withFileTypes: true })
.sort((left, right) => left.name.localeCompare(right.name))
.map((entry) =>
readSourceTree(
new URL(`${entry.name}${entry.isDirectory() ? '/' : ''}`, path),
extension,
),
)
.join('\n');
}
if (pathnameExtension(path.pathname) !== extension) {
return '';
}
return fs.readFileSync(path, 'utf8');
}
function pathnameExtension(pathname) {
const index = pathname.lastIndexOf('.');
return index === -1 ? '' : pathname.slice(index);
}
function assertNoOpenAiApiKeys(paths) {
const secretPattern = /sk-[A-Za-z0-9_-]{20,}/;
for (const path of paths.flatMap((entry) => collectFiles(entry))) {
const source = fs.readFileSync(path, 'utf8');
if (secretPattern.test(source)) {
throw new Error(
`AI game creator shell source must not contain API keys: ${path.pathname}`,
);
}
}
}
function assertNoEnvironmentConfigFallbacks(paths) {
const allowedDevCheck = 'import.meta.env.DEV';
for (const path of paths.flatMap((entry) => collectFiles(entry))) {
const source = fs
.readFileSync(path, 'utf8')
.replaceAll(allowedDevCheck, '')
.replaceAll('game-creator.config.local.json', '');
if (/\bprocess\.env\b|\bdotenv\b/.test(source)) {
throw new Error(
`AI game creator shell must use runtime config, not environment config: ${path.pathname}`,
);
}
}
}
function assertNoNativeBrowserConfirm(paths) {
for (const path of paths.flatMap((entry) => collectFiles(entry))) {
const source = fs.readFileSync(path, 'utf8');
if (/\bwindow\.confirm\b/.test(source)) {
throw new Error(
`AI game creator shell confirmations must use in-app UI: ${path.pathname}`,
);
}
}
}
function assertNoBlockingNativeFilePicker(source) {
if (/\.blocking_pick_(?:file|files|folder|folders)\s*\(/.test(source)) {
throw new Error(
'AI game creator shell native file pickers must not block the Tauri event loop',
);
}
}
function extractConstArrayBlock(source, name) {
const start = source.indexOf(`const ${name}`);
if (start === -1) {
throw new Error(`Missing contract array: ${name}`);
}
const end = source.indexOf('];', start);
if (end === -1) {
throw new Error(`Missing contract array end: ${name}`);
}
return source.slice(start, end + 2);
}
function parseTsCommands(source) {
const block = extractConstArrayBlock(source, 'GAME_CREATION_APP_COMMANDS');
return Array.from(
block.matchAll(/\{\s*id:\s*'([^']+)',\s*permission:\s*'([^']+)'\s*\}/g),
([, id, permission]) => ({ id, permission }),
);
}
function parseRustCommands(source) {
const block = extractConstArrayBlock(source, 'GAME_CREATION_APP_COMMANDS');
const permissionNames = {
Auto: 'auto',
Confirm: 'confirm',
Deny: 'deny',
};
return Array.from(
block.matchAll(
/command\(\s*"([^"]+)",\s*GameCreationAppPermission::(Auto|Confirm|Deny)\s*\)/g,
),
([, id, permission]) => ({ id, permission: permissionNames[permission] }),
);
}
function parseTsCapabilities(source) {
const block = extractConstArrayBlock(
source,
'GAME_CREATION_AGENT_CAPABILITIES',
);
return Array.from(
block.matchAll(
/\{\s*id:\s*'([^']+)',\s*area:\s*'([^']+)',\s*title:\s*'([^']+)',?\s*\}/g,
),
([, id, area, title]) => ({ id, area, title }),
);
}
function parseRustCapabilities(source) {
const block = extractConstArrayBlock(
source,
'GAME_CREATION_AGENT_CAPABILITIES',
);
return Array.from(
block.matchAll(
/capability\(\s*"([^"]+)",\s*"([^"]+)",\s*"([^"]+)",?\s*\)/g,
),
([, id, area, title]) => ({ id, area, title }),
);
}
function assertContractRecordsMatch(label, leftRecords, rightRecords) {
const normalize = (records) =>
records
.map((record) => JSON.stringify(record))
.sort((left, right) => left.localeCompare(right));
const left = normalize(leftRecords);
const right = normalize(rightRecords);
if (left.length === 0 || right.length === 0) {
throw new Error(`${label} parser returned no records`);
}
if (JSON.stringify(left) !== JSON.stringify(right)) {
throw new Error(
`${label} drifted between TypeScript and Rust contracts\nTS=${left.join(
'\n',
)}\nRust=${right.join('\n')}`,
);
}
}
function parseAppInvokeCommandNames(source) {
return Array.from(
source.matchAll(/invoke(?:<[^>]*>)?\(\s*['"]([a-z0-9_]+)['"]/g),
([, command]) => command,
);
}
function parseTauriHandlerCommandNames(source) {
const match = source.match(/tauri::generate_handler!\[([\s\S]*?)\]/);
if (!match) {
throw new Error('AI game creator shell Tauri handler list is missing');
}
return Array.from(
match[1].matchAll(/\b([a-z][a-z0-9_]+)\b/g),
([, command]) => command,
);
}
function parseRustFunctionNames(source) {
return Array.from(
source.matchAll(/\b(?:async\s+)?fn\s+([a-z][a-z0-9_]*)\s*\(/g),
([, name]) => name,
);
}
function assertCommandNamesSubset(label, leftNames, rightNames) {
const right = new Set(rightNames);
const missing = Array.from(new Set(leftNames))
.filter((name) => !right.has(name))
.sort((left, rightName) => left.localeCompare(rightName));
if (missing.length > 0) {
throw new Error(`${label} missing commands: ${missing.join(', ')}`);
}
}
function gitCheckResult({ code = 0, signal = null, stdout = '', stderr = '' }) {
return { code, signal, stdout, stderr };
}
class HiddenInputFixture extends EventEmitter {
constructor({ failRawRestore = false } = {}) {
super();
this.isTTY = true;
this.isRaw = false;
this.paused = true;
this.failRawRestore = failRawRestore;
this.rawModeChanges = [];
}
isPaused() {
return this.paused;
}
setRawMode(enabled) {
this.rawModeChanges.push(enabled);
this.isRaw = enabled;
if (!enabled && this.failRawRestore) {
throw new Error('fixture raw restore failure');
}
return this;
}
resume() {
this.paused = false;
return this;
}
pause() {
this.paused = true;
return this;
}
}
function hiddenOutputFixture() {
const writes = [];
return {
writes,
write(value) {
writes.push(value);
return true;
},
};
}
function assertHiddenInputRestored(input) {
assert.equal(input.listenerCount('data'), 0);
assert.equal(input.listenerCount('end'), 0);
assert.equal(input.listenerCount('error'), 0);
assert.equal(input.paused, true);
}
async function runHiddenInputRegressionChecks() {
const endedInput = new HiddenInputFixture();
const endedPromise = askHidden('fixture', {
input: endedInput,
output: hiddenOutputFixture(),
});
endedInput.emit('end');
await assert.rejects(endedPromise, /隐藏输入在完成前已结束/u);
assert.deepEqual(endedInput.rawModeChanges, [true, false]);
assertHiddenInputRestored(endedInput);
const erroredInput = new HiddenInputFixture();
const erroredPromise = askHidden('fixture', {
input: erroredInput,
output: hiddenOutputFixture(),
});
erroredInput.emit('error', new Error('fixture stdin error'));
await assert.rejects(erroredPromise, /fixture stdin error/u);
assert.deepEqual(erroredInput.rawModeChanges, [true, false]);
assertHiddenInputRestored(erroredInput);
const restoreFailedInput = new HiddenInputFixture({ failRawRestore: true });
const restoreFailedPromise = askHidden('fixture', {
input: restoreFailedInput,
output: hiddenOutputFixture(),
});
restoreFailedInput.emit('data', Buffer.from('secret\n'));
await assert.rejects(restoreFailedPromise, /恢复终端 raw mode 失败/u);
assert.deepEqual(restoreFailedInput.rawModeChanges, [true, false]);
assertHiddenInputRestored(restoreFailedInput);
const signaledInput = new HiddenInputFixture();
const signalSource = new EventEmitter();
const forwardedSignals = [];
const signaledPromise = askHidden('fixture', {
input: signaledInput,
output: hiddenOutputFixture(),
signalSource,
terminateForSignal: (signal) => forwardedSignals.push(signal),
platform: 'win32',
});
signalSource.emit('SIGBREAK');
await assert.rejects(signaledPromise, /SIGBREAK/u);
assert.deepEqual(signaledInput.rawModeChanges, [true, false]);
assertHiddenInputRestored(signaledInput);
assert.deepEqual(forwardedSignals, ['SIGBREAK']);
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK']) {
assert.equal(signalSource.listenerCount(signal), 0);
}
}
async function runConfigWizardRegressionChecks() {
const testRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'genarrative-agc-config-check-'),
);
try {
const canonicalTestRoot = fs.realpathSync.native(testRoot);
const realConfigAncestor = path.join(testRoot, 'real-config-ancestor');
const linkedConfigAncestor = path.join(testRoot, 'linked-config-ancestor');
fs.mkdirSync(realConfigAncestor);
fs.symlinkSync(
realConfigAncestor,
linkedConfigAncestor,
process.platform === 'win32' ? 'junction' : 'dir',
);
const missingLinkedConfigDir = path.join(
linkedConfigAncestor,
'missing-appdata',
);
assert.equal(fs.existsSync(missingLinkedConfigDir), false);
assert.equal(
await assertSafeGameCreatorConfigDestination(missingLinkedConfigDir),
path.join(fs.realpathSync.native(realConfigAncestor), 'missing-appdata'),
);
const gitRoot = path.join(testRoot, 'tracked-repository');
const trackedConfigDir = path.join(gitRoot, 'runtime-config');
fs.mkdirSync(trackedConfigDir, { recursive: true });
fs.writeFileSync(
path.join(trackedConfigDir, 'game-creator.config.json'),
'{}\n',
{ mode: 0o600 },
);
execFileSync('git', ['init', '--quiet', gitRoot], { stdio: 'ignore' });
execFileSync(
'git',
['-C', gitRoot, 'add', 'runtime-config/game-creator.config.json'],
{ stdio: 'ignore' },
);
await assert.rejects(
assertSafeGameCreatorConfigDestination(trackedConfigDir),
/Git 已跟踪/u,
);
const untrackedConfigDir = path.join(gitRoot, 'untracked-runtime-config');
await assert.rejects(
assertSafeGameCreatorConfigDestination(untrackedConfigDir),
/Git 仓库之外/u,
);
const outsideConfigDir = path.join(testRoot, 'outside-appdata');
assert.equal(
await assertSafeGameCreatorConfigDestination(outsideConfigDir),
path.join(canonicalTestRoot, 'outside-appdata'),
);
await assert.rejects(
assertSafeGameCreatorConfigDestination(outsideConfigDir, {
requireDedicatedLeaf: true,
}),
new RegExp(appIdentifier.replaceAll('.', '\\.')),
);
const dedicatedConfigDir = path.join(testRoot, appIdentifier);
assert.equal(
await assertSafeGameCreatorConfigDestination(dedicatedConfigDir, {
requireDedicatedLeaf: true,
}),
path.join(canonicalTestRoot, appIdentifier),
);
const injectedNonGitConfigDir = path.join(testRoot, 'injected-non-git');
assert.equal(
await assertSafeGameCreatorConfigDestination(injectedNonGitConfigDir, {
runGit: async () =>
gitCheckResult({
code: 128,
stderr:
'fatal: not a git repository (or any of the parent directories): .git\n',
}),
}),
path.join(canonicalTestRoot, 'injected-non-git'),
);
await assert.rejects(
assertSafeGameCreatorConfigDestination(
path.join(testRoot, 'missing-git-command'),
{
runGit: async () => {
const error = new Error('spawn git ENOENT');
error.code = 'ENOENT';
throw error;
},
},
),
/Git 命令不可用或启动失败/u,
);
await assert.rejects(
assertSafeGameCreatorConfigDestination(
path.join(testRoot, 'abnormal-rev-parse'),
{
runGit: async () =>
gitCheckResult({
code: 129,
stderr: 'fixture rev-parse failure',
}),
},
),
/Git 检查异常.*fixture rev-parse failure/u,
);
const abnormalTrackedGitRoot = path.join(
testRoot,
'abnormal-tracked-repository',
);
const abnormalTrackedConfigDir = path.join(
abnormalTrackedGitRoot,
'runtime-config',
);
fs.mkdirSync(abnormalTrackedConfigDir, { recursive: true });
let gitCheckCount = 0;
await assert.rejects(
assertSafeGameCreatorConfigDestination(abnormalTrackedConfigDir, {
runGit: async () => {
gitCheckCount += 1;
if (gitCheckCount === 1) {
return gitCheckResult({ stdout: `${abnormalTrackedGitRoot}\n` });
}
return gitCheckResult({
code: 2,
stderr: 'fixture ls-files failure',
});
},
}),
/配置文件跟踪状态.*fixture ls-files failure/u,
);
assert.equal(gitCheckCount, 2);
const overlayConfigDir = path.join(testRoot, 'overlay-appdata');
fs.mkdirSync(overlayConfigDir, { recursive: true, mode: 0o700 });
const primaryConfigPath = path.join(
overlayConfigDir,
'game-creator.config.json',
);
const localConfigPath = path.join(
overlayConfigDir,
'game-creator.config.local.json',
);
const primaryConfig = {
llm: {
apiKey: 'fixture-primary-key',
baseUrl: 'https://primary.example.test/v1',
model: 'primary-model',
apiKind: 'openai_chat',
stream: false,
requestTimeoutMs: 12345,
},
editorApi: {
apiKey: 'fixture-editor-key',
baseUrl: 'http://127.0.0.1:8082',
},
mcpServers: {
primary: { command: 'fixture-primary-command' },
},
};
const localConfig = {
llm: {
apiKey: 'fixture-old-overlay-key',
model: 'old-overlay-model',
stream: true,
},
agentLlm: {
planner: { model: 'planner-overlay-model' },
},
mcpServers: {
local: { command: 'fixture-local-command' },
},
};
fs.writeFileSync(
primaryConfigPath,
`${JSON.stringify(primaryConfig, null, 2)}\n`,
{ mode: 0o644 },
);
fs.writeFileSync(
localConfigPath,
`${JSON.stringify(localConfig, null, 2)}\n`,
{ mode: 0o644 },
);
const overlayState =
await readGameCreatorWizardConfigState(overlayConfigDir);
if (process.platform !== 'win32') {
assert.equal(fs.statSync(primaryConfigPath).mode & 0o777, 0o600);
assert.equal(fs.statSync(localConfigPath).mode & 0o777, 0o600);
}
assert.equal(overlayState.configPath, primaryConfigPath);
assert.equal(overlayState.localConfigPath, localConfigPath);
assert.equal(overlayState.effectiveConfig.llm.model, 'old-overlay-model');
assert.equal(overlayState.effectiveConfig.llm.stream, true);
assert.equal(overlayState.effectiveConfig.llm.requestTimeoutMs, 12345);
assert.deepEqual(Object.keys(overlayState.effectiveConfig.mcpServers), [
'primary',
'local',
]);
assert.deepEqual(
overlayState.writeConfig.editorApi,
primaryConfig.editorApi,
);
const updatedPrimary = buildGameCreatorWizardConfig(
overlayState.writeConfig,
{
apiKey: 'fixture-new-key',
baseUrl: 'https://new.example.test/v1/',
model: 'new-model',
apiKind: 'openai_responses',
},
);
await writeGameCreatorWizardConfig(overlayState, updatedPrimary);
const reloadedState =
await readGameCreatorWizardConfigState(overlayConfigDir);
assert.equal(reloadedState.effectiveConfig.llm.apiKey, 'fixture-new-key');
assert.equal(reloadedState.effectiveConfig.llm.model, 'new-model');
assert.equal(
reloadedState.effectiveConfig.llm.baseUrl,
'https://new.example.test/v1',
);
assert.equal(reloadedState.effectiveConfig.llm.requestTimeoutMs, 12345);
assert.equal(reloadedState.effectiveConfig.llm.stream, true);
assert.equal(
JSON.parse(fs.readFileSync(primaryConfigPath, 'utf8')).llm.apiKey,
'fixture-new-key',
);
const sanitizedLocalConfig = JSON.parse(
fs.readFileSync(localConfigPath, 'utf8'),
);
assert.equal(sanitizedLocalConfig.llm, undefined);
assert.deepEqual(sanitizedLocalConfig.agentLlm, localConfig.agentLlm);
assert.deepEqual(sanitizedLocalConfig.mcpServers, localConfig.mcpServers);
if (process.platform !== 'win32') {
assert.equal(fs.statSync(localConfigPath).mode & 0o777, 0o600);
}
const unchangedLocalConfigDir = path.join(
testRoot,
'unchanged-local-appdata',
);
fs.mkdirSync(unchangedLocalConfigDir, { recursive: true, mode: 0o700 });
const unchangedPrimaryConfigPath = path.join(
unchangedLocalConfigDir,
'game-creator.config.json',
);
const unchangedLocalConfigPath = path.join(
unchangedLocalConfigDir,
'game-creator.config.local.json',
);
const unchangedLocalSource =
'{\n "editorApi": { "baseUrl": "http://127.0.0.1:8082" }\n}\n';
fs.writeFileSync(unchangedPrimaryConfigPath, '{}\n', { mode: 0o600 });
fs.writeFileSync(unchangedLocalConfigPath, unchangedLocalSource, {
mode: 0o644,
});
const unchangedLocalState = await readGameCreatorWizardConfigState(
unchangedLocalConfigDir,
);
await writeGameCreatorWizardConfig(
unchangedLocalState,
buildGameCreatorWizardConfig(unchangedLocalState.writeConfig, {
apiKey: 'fixture-private-local-key',
baseUrl: 'https://private.example.test/v1',
model: 'private-model',
apiKind: 'openai_responses',
}),
);
assert.equal(
fs.readFileSync(unchangedLocalConfigPath, 'utf8'),
unchangedLocalSource,
);
if (process.platform !== 'win32') {
assert.equal(fs.statSync(unchangedLocalConfigPath).mode & 0o777, 0o600);
}
const invalidLocalConfigDir = path.join(testRoot, 'invalid-local-appdata');
fs.mkdirSync(invalidLocalConfigDir, { recursive: true, mode: 0o700 });
const invalidPrimaryConfigPath = path.join(
invalidLocalConfigDir,
'game-creator.config.json',
);
const invalidLocalConfigPath = path.join(
invalidLocalConfigDir,
'game-creator.config.local.json',
);
fs.writeFileSync(invalidPrimaryConfigPath, '{}\n', { mode: 0o600 });
fs.writeFileSync(invalidLocalConfigPath, '{ invalid json\n', {
mode: 0o644,
});
await assert.rejects(
writeGameCreatorWizardConfig(
{
configPath: invalidPrimaryConfigPath,
localConfigPath: invalidLocalConfigPath,
},
{ llm: { apiKey: 'fixture-invalid-local-key' } },
),
/读取客户端配置失败/u,
);
if (process.platform !== 'win32') {
assert.equal(fs.statSync(invalidLocalConfigPath).mode & 0o777, 0o600);
}
const windowsLocalConfigDir = path.join(testRoot, 'windows-local-appdata');
fs.mkdirSync(windowsLocalConfigDir, { recursive: true });
const windowsLocalPrimaryPath = path.join(
windowsLocalConfigDir,
'game-creator.config.json',
);
const windowsLocalConfigPath = path.join(
windowsLocalConfigDir,
'game-creator.config.local.json',
);
fs.writeFileSync(windowsLocalPrimaryPath, '{}\n');
fs.writeFileSync(windowsLocalConfigPath, unchangedLocalSource);
const windowsReadAclPaths = [];
const windowsLocalState = await readGameCreatorWizardConfigState(
windowsLocalConfigDir,
{
platform: 'win32',
secureWindowsPath: async (targetPath, { isDirectory }) => {
windowsReadAclPaths.push({ targetPath, isDirectory });
},
},
);
assert.deepEqual(windowsReadAclPaths, [
{ targetPath: windowsLocalConfigDir, isDirectory: true },
{ targetPath: windowsLocalPrimaryPath, isDirectory: false },
{ targetPath: windowsLocalConfigPath, isDirectory: false },
]);
const windowsLocalAclPaths = [];
await writeGameCreatorWizardConfig(
windowsLocalState,
buildGameCreatorWizardConfig(windowsLocalState.writeConfig, {
apiKey: 'fixture-windows-local-key',
baseUrl: 'https://windows.example.test/v1',
model: 'windows-model',
apiKind: 'openai_responses',
}),
{
platform: 'win32',
secureWindowsPath: async (targetPath, { isDirectory }) => {
if (!isDirectory) windowsLocalAclPaths.push(targetPath);
},
},
);
assert.equal(
fs.readFileSync(windowsLocalConfigPath, 'utf8'),
unchangedLocalSource,
);
assert.equal(windowsLocalAclPaths.includes(windowsLocalConfigPath), true);
const guiSavedConfig = JSON.parse(
fs.readFileSync(primaryConfigPath, 'utf8'),
);
guiSavedConfig.llm = {
...guiSavedConfig.llm,
apiKey: 'fixture-gui-key',
model: 'gui-model',
};
await writeGameCreatorConfigAtomically(primaryConfigPath, guiSavedConfig);
const afterGuiSave =
await readGameCreatorWizardConfigState(overlayConfigDir);
assert.equal(afterGuiSave.effectiveConfig.llm.apiKey, 'fixture-gui-key');
assert.equal(afterGuiSave.effectiveConfig.llm.model, 'gui-model');
const windowsConfigDir = path.join(testRoot, 'windows-appdata');
const windowsConfigPath = path.join(
windowsConfigDir,
'game-creator.config.json',
);
const aclEvents = [];
await writeGameCreatorConfigAtomically(
windowsConfigPath,
{ llm: { apiKey: 'fixture-windows-key' } },
{
platform: 'win32',
secureWindowsPath: async (targetPath, { isDirectory }) => {
const metadata = fs.lstatSync(targetPath);
const temporaryFile =
!isDirectory && path.basename(targetPath).startsWith('.');
if (temporaryFile) {
assert.equal(metadata.size, 0);
assert.equal(fs.readFileSync(targetPath, 'utf8'), '');
}
aclEvents.push({ isDirectory, temporaryFile });
},
},
);
assert.deepEqual(aclEvents, [
{ isDirectory: true, temporaryFile: false },
{ isDirectory: false, temporaryFile: true },
{ isDirectory: false, temporaryFile: false },
]);
assert.equal(
JSON.parse(fs.readFileSync(windowsConfigPath, 'utf8')).llm.apiKey,
'fixture-windows-key',
);
const failedWindowsConfigDir = path.join(
testRoot,
'failed-windows-appdata',
);
const failedWindowsConfigPath = path.join(
failedWindowsConfigDir,
'game-creator.config.json',
);
await assert.rejects(
writeGameCreatorConfigAtomically(
failedWindowsConfigPath,
{ llm: { apiKey: 'fixture-must-not-reach-disk' } },
{
platform: 'win32',
secureWindowsPath: async (_targetPath, { isDirectory }) => {
if (!isDirectory) throw new Error('fixture DACL failure');
},
},
),
/fixture DACL failure/u,
);
assert.equal(fs.existsSync(failedWindowsConfigPath), false);
assert.deepEqual(fs.readdirSync(failedWindowsConfigDir), []);
} finally {
fs.rmSync(testRoot, { recursive: true, force: true });
}
}
assertNoOpenAiApiKeys([
new URL('../src/', import.meta.url),
new URL('../scripts/', import.meta.url),
new URL('../game-creator.config.json', import.meta.url),
new URL('../src-tauri/src/', import.meta.url),
new URL('../package.json', import.meta.url),
new URL('../vite.config.ts', import.meta.url),
new URL('../src-tauri/Cargo.toml', import.meta.url),
new URL('../src-tauri/tauri.conf.json', import.meta.url),
new URL(
'../../../packages/shared/src/contracts/gameCreationApp.ts',
import.meta.url,
),
new URL(
'../../../packages/shared/src/contracts/gameCreationApp.test.ts',
import.meta.url,
),
new URL(
'../../../server-rs/crates/platform-agent/src/game_creation.rs',
import.meta.url,
),
new URL(
'../../../server-rs/crates/shared-contracts/src/game_creation_app.rs',
import.meta.url,
),
new URL(
'../../../docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md',
import.meta.url,
),
]);
assertNoEnvironmentConfigFallbacks([
new URL('../src/', import.meta.url),
new URL('../scripts/run-cli-with-config.mjs', import.meta.url),
new URL('../scripts/smoke-agent-run-local-provider.mjs', import.meta.url),
new URL('../scripts/start-dev-server.mjs', import.meta.url),
new URL('../src-tauri/src/', import.meta.url),
new URL('../package.json', import.meta.url),
new URL('../vite.config.ts', import.meta.url),
new URL('../src-tauri/Cargo.toml', import.meta.url),
new URL('../src-tauri/tauri.conf.json', import.meta.url),
]);
assertNoNativeBrowserConfirm([new URL('../src/', import.meta.url)]);
assertNoBlockingNativeFilePicker(tauriRustSource);
assertContractRecordsMatch(
'AI game creator shell command contract',
parseTsCommands(sharedContractSource),
parseRustCommands(rustSharedContractSource),
);
assertContractRecordsMatch(
'AI game creator shell capability contract',
parseTsCapabilities(sharedContractSource),
parseRustCapabilities(rustSharedContractSource),
);
assertCommandNamesSubset(
'AI game creator shell Tauri handler',
parseAppInvokeCommandNames(appInvokeSource),
parseTauriHandlerCommandNames(tauriHandlerSource),
);
assertCommandNamesSubset(
'AI game creator shell Tauri command implementation',
parseTauriHandlerCommandNames(tauriHandlerSource),
parseRustFunctionNames(tauriRustSource),
);
assertCommandNamesSubset(
'AI game creator shell App invoke or explicit native-only allowlist',
parseTauriHandlerCommandNames(tauriHandlerSource),
[
...parseAppInvokeCommandNames(appInvokeSource),
...allowedUncalledTauriCommands,
],
);
assertCommandNamesSubset(
'AI game creator shell explicit native-only allowlist',
allowedUncalledTauriCommands,
parseTauriHandlerCommandNames(tauriHandlerSource),
);
if (packageConfig.name !== '@genarrative/ai-game-creator-shell') {
throw new Error('AI game creator shell package name drifted');
}
if (
packageConfig.scripts?.['llm-status'] !==
'node scripts/run-cli-with-config.mjs --llm-status'
) {
throw new Error(
'AI game creator shell llm-status must use client config before checking LLM config',
);
}
const gameChatReleaseAppIndex = appModuleSource.indexOf(
'export function GameChatReleaseApp(',
);
const gameChatReleaseBranchIndex = appEntrypointSource.indexOf(
'{gameChatReleaseMode ? (',
);
const authenticatedClientIndex = appEntrypointSource.indexOf(
'<AuthenticatedClient>',
gameChatReleaseBranchIndex,
);
if (
gameChatReleaseAppIndex === -1 ||
gameChatReleaseBranchIndex === -1 ||
!appEntrypointSource
.slice(gameChatReleaseBranchIndex, authenticatedClientIndex)
.includes('gameChatApp') ||
authenticatedClientIndex < gameChatReleaseBranchIndex
) {
throw new Error(
'AI game creator game-chat release must render the local chat App before the platform authentication boundary',
);
}
if (
packageConfig.scripts?.['agent-run'] !==
'node scripts/run-cli-with-config.mjs --agent-run'
) {
throw new Error(
'AI game creator shell agent-run must use client config before running the provider path',
);
}
if (
packageConfig.scripts?.['agent-task'] !==
'node scripts/run-cli-with-config.mjs --agent-task'
) {
throw new Error(
'AI game creator shell agent-task must use client config before starting the single Agent runtime',
);
}
if (
packageConfig.scripts?.swarm !==
'node scripts/run-cli-with-config.mjs --swarm-chat'
) {
throw new Error(
'AI game creator shell swarm must use client config before starting the interactive Agent runtime',
);
}
if (
packageConfig.scripts?.config !==
'node scripts/game-creator-config-wizard.mjs'
) {
throw new Error(
'AI game creator shell config must use the AppData configuration wizard',
);
}
if (
packageConfig.scripts?.['test:chat'] !==
`node scripts/agent-swarm-test-chat.mjs --task ${JSON.stringify(defaultRealSwarmTestTask)} --no-open`
) {
throw new Error(
'AI game creator shell test:chat must use the one-click Swarm test entry',
);
}
if (
packageConfig.scripts?.['test:chat:manual'] !==
'node scripts/agent-swarm-test-chat.mjs'
) {
throw new Error(
'AI game creator shell test:chat:manual must keep the interactive Swarm test entry',
);
}
if (
rootPackageConfig.scripts?.['agc:test'] !==
'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --'
) {
throw new Error('agc:test must delegate to the deterministic playable E2E');
}
if (
rootPackageConfig.scripts?.['agc:config'] !==
'npm --prefix apps/ai-game-creator-shell run config --'
) {
throw new Error(
'agc:config must delegate to the AppData configuration wizard',
);
}
if (
rootPackageConfig.scripts?.['agc:test:chat'] !==
'npm --prefix apps/ai-game-creator-shell run test:chat --'
) {
throw new Error(
'agc:test:chat must delegate to the one-click Swarm test entry',
);
}
if (
rootPackageConfig.scripts?.['agc:test:chat:manual'] !==
'npm --prefix apps/ai-game-creator-shell run test:chat:manual --'
) {
throw new Error(
'agc:test:chat:manual must delegate to the interactive Swarm test entry',
);
}
for (const requiredSource of [
"export const appIdentifier = 'world.genarrative.ai-game-creator'",
"'--swarm-chat'",
"'--autonomous-game-build'",
"'--preview-serve'",
'cleanupSwarmTestProject(project)',
]) {
if (!swarmTestChatSource.includes(requiredSource)) {
throw new Error(`Swarm test entry contract drifted: ${requiredSource}`);
}
}
if (tauriConfig.productName !== 'Genarrative AI Game Creator') {
throw new Error('AI game creator shell productName drifted');
}
if (tauriConfig.identifier !== 'world.genarrative.ai-game-creator') {
throw new Error('AI game creator shell identifier drifted');
}
if (tauriConfig.app?.withGlobalTauri !== true) {
throw new Error(
'AI game creator shell must expose window.__TAURI__ for local commands',
);
}
if (!fs.existsSync(eventCapabilityPath)) {
throw new Error(
'AI game creator shell must declare a Tauri event-listener capability',
);
}
const eventCapability = JSON.parse(
fs.readFileSync(eventCapabilityPath, 'utf8'),
);
const eventCapabilityWindows = new Set(eventCapability.windows ?? []);
const eventCapabilityPermissions = new Set(eventCapability.permissions ?? []);
for (const windowLabel of [
'client',
'developer',
'main',
'launcher',
'supervisor-chat',
]) {
if (!eventCapabilityWindows.has(windowLabel)) {
throw new Error(
`AI game creator shell event capability missing window: ${windowLabel}`,
);
}
}
for (const permission of [
'core:event:allow-listen',
'core:event:allow-unlisten',
]) {
if (!eventCapabilityPermissions.has(permission)) {
throw new Error(
`AI game creator shell event capability missing permission: ${permission}`,
);
}
}
if (
eventCapabilityPermissions.has('core:event:allow-emit') ||
eventCapabilityPermissions.has('core:event:allow-emit-to') ||
eventCapabilityPermissions.has('core:event:default')
) {
throw new Error(
'AI game creator shell frontend event capability must stay listen-only',
);
}
if (
!Array.isArray(tauriConfig.app?.windows) ||
tauriConfig.app.windows.length !== 1 ||
tauriConfig.app.windows[0]?.label !== 'client' ||
tauriConfig.app.windows[0]?.url !== 'index.html'
) {
throw new Error(
'AI game creator shell must start with only the client window',
);
}
if (defaultAppConfig.llm?.apiKey !== '') {
throw new Error('AI game creator shell default llm.apiKey must stay empty');
}
const allowedLlmReasoningEfforts = new Set([
'default',
'low',
'medium',
'high',
]);
if (defaultAppConfig.llm?.reasoningEffort !== 'high') {
throw new Error(
'AI game creator shell default llm.reasoningEffort must stay high',
);
}
for (const [agentId, agentConfig] of Object.entries(
defaultAppConfig.agentLlm ?? {},
)) {
if (
agentConfig?.reasoningEffort !== undefined &&
!allowedLlmReasoningEfforts.has(agentConfig.reasoningEffort)
) {
throw new Error(
`AI game creator shell agentLlm.${agentId}.reasoningEffort is invalid`,
);
}
}
if (defaultAppConfig.editorApi?.apiKey !== '') {
throw new Error(
'AI game creator shell default editorApi.apiKey must stay empty',
);
}
if (
defaultAppConfig.llm?.requestTimeoutMs < 1000 ||
defaultAppConfig.llm?.maxRetries < 0 ||
defaultAppConfig.llm?.retryBackoffMs < 1
) {
throw new Error('AI game creator shell default LLM timing config is invalid');
}
const windows = tauriConfig.app?.windows ?? [];
if (
windows.length !== 1 ||
windows[0]?.label !== 'client' ||
windows[0]?.url !== 'index.html'
) {
throw new Error(
'AI game creator shell release config must expose only the client window',
);
}
const clientWindow = windows[0];
if (
clientWindow.width !== 1280 ||
clientWindow.height !== 800 ||
clientWindow.minWidth !== 1280 ||
clientWindow.minHeight !== 800
) {
throw new Error(
'AI game creator shell client window must keep the landscape workbench size',
);
}
if (tauriConfig.build?.devUrl !== 'http://127.0.0.1:3080/') {
throw new Error(
'AI game creator shell Tauri config must retain the non-launcher fallback devUrl',
);
}
if (!viteConfigSource.includes("host: '127.0.0.1'")) {
throw new Error(
'AI game creator shell Vite dev server must bind to localhost',
);
}
if (
!viteConfigSource.includes('port: 3080') ||
!viteConfigSource.includes('port: server.config.server.port')
) {
throw new Error(
'AI game creator shell Vite config must retain its fallback and report the actual CLI-selected port',
);
}
for (const snippet of [
'mapDevPortsToPortRange',
'agcVitePort',
'resolveAgcDevEndpoint',
'GENARRATIVE_AGC_VITE_PORT',
]) {
if (!devPortSource.includes(snippet)) {
throw new Error(
`AI game creator shell dev port resolver drifted: ${snippet}`,
);
}
}
for (const snippet of [
'resolveAgcDevEndpoint',
'withAgcDevEndpointEnv',
"'--config'",
'configOverride',
]) {
if (!startTauriDevSource.includes(snippet)) {
throw new Error(
`AI game creator shell Tauri dev port injection drifted: ${snippet}`,
);
}
}
if (!viteConfigSource.includes('strictPort: true')) {
throw new Error(
'AI game creator shell Vite dev server must not drift away from Tauri devUrl',
);
}
if (!viteConfigSource.includes('allow: [repoRoot]')) {
throw new Error(
'AI game creator shell Vite dev server must allow shared repository sources',
);
}
if (
!(
tauriConfig.build?.beforeDevCommand?.includes(
'run ai-game-creator-shell:dev-server',
) || tauriConfig.build?.beforeDevCommand?.includes('run agc:serve')
)
) {
throw new Error(
'AI game creator shell beforeDevCommand must start the selected Vite dev server',
);
}
if (packageConfig.scripts?.dev !== 'node scripts/start-tauri-dev.mjs') {
throw new Error(
'AI game creator shell dev must run through the managed Tauri dev launcher',
);
}
if (
packageConfig.scripts?.['game-chat'] !==
'node scripts/start-tauri-dev.mjs --game-chat'
) {
throw new Error(
'AI game creator shell game-chat must run through the managed Tauri dev launcher',
);
}
const gameChatInitialUrlApply =
'apply_game_chat_initial_window_url(tauri_context.config_mut(), options)';
const gameChatInitialUrlApplyIndexes = Array.from(
tauriHandlerSource.matchAll(
/apply_game_chat_initial_window_url\(tauri_context\.config_mut\(\), options\)/gu,
),
(match) => match.index,
);
const tauriContextIndex = tauriHandlerSource.indexOf(
'let mut tauri_context = tauri::generate_context!()',
);
const tauriBuilderIndex = tauriHandlerSource.indexOf(
'tauri::Builder::default()',
);
if (
gameChatInitialUrlApplyIndexes.length !== 1 ||
tauriContextIndex === -1 ||
tauriBuilderIndex === -1 ||
gameChatInitialUrlApplyIndexes[0] < tauriContextIndex ||
gameChatInitialUrlApplyIndexes[0] > tauriBuilderIndex
) {
throw new Error(
`AI game creator game-chat URL must be applied exactly once between Context creation and Tauri Builder creation: ${gameChatInitialUrlApply}`,
);
}
const tauriSetupStartIndex = tauriHandlerSource.indexOf('.setup(move |app| {');
const tauriSetupEndIndex = tauriHandlerSource.indexOf(
'.invoke_handler(',
tauriSetupStartIndex,
);
if (tauriSetupStartIndex === -1 || tauriSetupEndIndex === -1) {
throw new Error('AI game creator Tauri setup block is missing');
}
const tauriSetupSource = tauriHandlerSource.slice(
tauriSetupStartIndex,
tauriSetupEndIndex,
);
for (const forbiddenSetupSnippet of [
'game_chat_launch.as_ref()',
'navigate_client_to_game_chat',
'.navigate(',
]) {
if (tauriSetupSource.includes(forbiddenSetupSnippet)) {
throw new Error(
`AI game creator setup must not perform game-chat runtime navigation: ${forbiddenSetupSnippet}`,
);
}
}
for (const forbiddenSnippet of [
'navigate_client_to_game_chat',
'client.url()',
'client.navigate(',
]) {
if (
`${tauriHandlerSource}\n${tauriWindowSource}`.includes(forbiddenSnippet)
) {
throw new Error(
`AI game creator game-chat startup must not navigate an initialized client WebView: ${forbiddenSnippet}`,
);
}
}
if (
/get_webview_window\s*\(\s*['"]client['"]\s*\)/u.test(
`${tauriHandlerSource}\n${tauriWindowSource}`,
)
) {
throw new Error(
'AI game creator game-chat startup must not look up the runtime client WebView',
);
}
for (const requiredSnippet of [
'fn apply_game_chat_initial_window_url(',
'.find(|window| window.label == "client")',
'client.url = game_chat_window_url(',
]) {
if (
!`${tauriHandlerSource}\n${tauriWindowSource}`.includes(requiredSnippet)
) {
throw new Error(
`AI game creator game-chat initial WindowConfig guardrail drifted: ${requiredSnippet}`,
);
}
}
if (
!tauriHandlerSource.includes('.build(tauri_context)') ||
!tauriHandlerSource.includes('handle_game_creator_gui_run_event(&event)')
) {
throw new Error(
'AI game creator Tauri runtime must build from the prepared Context and preserve the generic GUI exit hook',
);
}
if (
!tauriConfig.build?.beforeBuildCommand?.includes('--config vite.config.ts')
) {
throw new Error(
'AI game creator shell beforeBuildCommand must resolve vite config from app root',
);
}
if (
!appEntrypointSource.includes(
"import.meta.env.VITE_AGC_GAME_CHAT_ONLY === 'true'",
) ||
!appEntrypointSource.includes(
"import.meta.env.DEV && initialSearchParams.has('game-chat')",
)
) {
throw new Error(
'AI game creator game-chat release must be compile-time fixed while preserving the dev query entry',
);
}
if (
packageConfig.scripts?.['build:game-chat-release'] !==
'npm --prefix ../.. exec tauri -- build --config src-tauri/tauri.game-chat-release.conf.json --bundles nsis --features game-chat-release' ||
rootPackageConfig.scripts?.['agc:build:game-chat-release'] !==
'npm --prefix apps/ai-game-creator-shell run build:game-chat-release --'
) {
throw new Error(
'AI game creator game-chat release build commands must stay wired through the dedicated Tauri config',
);
}
if (
gameChatReleaseTauriConfig.productName !== 'Genarrative Game Chat' ||
gameChatReleaseTauriConfig.version !== '0.1.1' ||
gameChatReleaseTauriConfig.identifier === tauriConfig.identifier ||
gameChatReleaseTauriConfig.build?.beforeBuildCommand !==
'node scripts/build-game-chat-release.mjs' ||
!gameChatReleaseTauriConfig.bundle?.targets?.includes('nsis')
) {
throw new Error(
'AI game creator game-chat release must keep version 0.1.1, its independent identity, frontend build, and NSIS target',
);
}
if (
tauriConfig.version !== '0.1.0' ||
packageConfig.version !== '0.1.0' ||
cargoPackageVersion !== '0.1.0'
) {
throw new Error(
'AI game creator standard release must remain version 0.1.0 while game-chat uses its dedicated version',
);
}
for (const requiredSnippet of [
"'ai-game-creator-shell:typecheck'",
"VITE_AGC_GAME_CHAT_ONLY: 'true'",
"'--config'",
"'vite.config.ts'",
]) {
if (!gameChatReleaseBuildSource.includes(requiredSnippet)) {
throw new Error(
`AI game creator game-chat release build guardrail drifted: ${requiredSnippet}`,
);
}
}
const devServerSource = fs.readFileSync(
new URL('../scripts/start-dev-server.mjs', import.meta.url),
'utf8',
);
const runCliWithConfigSource = fs.readFileSync(
new URL('../scripts/run-cli-with-config.mjs', import.meta.url),
'utf8',
);
if (!runCliWithConfigSource.includes('resolveGameCreatorAppConfigDir')) {
throw new Error(
'AI game creator shell CLI wrapper must resolve the GUI AppData directory',
);
}
if (
!runCliWithConfigSource.includes(
"'--config-dir', resolveGameCreatorAppConfigDir()",
)
) {
throw new Error(
'AI game creator shell CLI wrapper must pass the GUI AppData directory to native commands',
);
}
for (const snippet of [
'resolveAgcDevEndpoint',
'withAgcDevEndpointEnv',
"response.body.includes('<title>AI 游戏创作</title>')",
'function isPortListening()',
'cannot be safely reused',
'non-HTTP or unrecognized server',
"'--config'",
"'vite.config.ts'",
"'--port'",
]) {
if (!devServerSource.includes(snippet)) {
throw new Error(
`AI game creator shell dev server wrapper drifted: ${snippet}`,
);
}
}
const configWizardSource = fs.readFileSync(
new URL('./game-creator-config-wizard.mjs', import.meta.url),
'utf8',
);
for (const snippet of [
'assertSafeGameCreatorConfigDestination',
'readGameCreatorWizardConfigState',
'$security.SetAccessRuleProtection($true, $false)',
'$targetItem = Get-Item -LiteralPath $target -Force',
'$targetItem.SetAccessControl($security)',
'$verified = $targetItem.GetAccessControl()',
'$rules.Count -ne 1',
'[System.Security.AccessControl.FileSystemRights]::FullControl',
"runChildCapture('powershell.exe'",
"'-NoProfile'",
"'-Command'",
'windowsPrivateAclScript',
'await secureWindowsPath(temporaryPath, { isDirectory: false })',
'await temporaryFile.writeFile',
]) {
if (!configWizardSource.includes(snippet)) {
throw new Error(
`AI game creator config wizard guardrail drifted: ${snippet}`,
);
}
}
if (/\bGet-Acl\b/u.test(configWizardSource)) {
throw new Error(
'AI game creator config wizard must not rely on Get-Acl module auto-loading',
);
}
await runConfigWizardRegressionChecks();
await runHiddenInputRegressionChecks();
for (const snippet of [
"new URL('..', import.meta.url)",
"'--manifest-path'",
"'src-tauri/Cargo.toml'",
]) {
if (!runCliWithConfigSource.includes(snippet)) {
throw new Error(
`AI game creator shell config CLI wrapper drifted: ${snippet}`,
);
}
}
for (const snippet of [
'const GAME_CREATOR_CONFIG_FILE_NAME: &str = "game-creator.config.json"',
'const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.json"',
'const DEFAULT_GAME_CREATOR_APP_CONFIG_JSON: &str = include_str!("../../game-creator.config.json")',
'fn configure_game_creator_runtime_config_dir(',
'app.path().app_config_dir()?',
'fn load_game_creator_app_config()',
'fn read_game_creator_app_config()',
'fn write_game_creator_app_config(',
'fn writable_game_creator_config_path()',
'fn normalize_game_creator_app_config(',
'fn merge_game_creator_config_file(',
'.join("apps")',
'.join("ai-game-creator-shell")',
'read_game_creator_app_config,',
'write_game_creator_app_config,',
'resolve_game_creator_llm_config_for_agent(app_config, "planner")',
'resolve_game_creator_llm_config_for_agent(app_config, "generator")',
'build_game_creator_llm_client_from_llm_config(&planner_llm, "agentLlm.planner")?',
'build_game_creator_llm_client_from_llm_config(&generator_llm, "agentLlm.generator")?',
'agentLlm.{agent_id}',
'let app_config = match load_game_creator_app_config()',
'fn append_local_permission_log_at(',
'"command.auto"',
'GameCreationAppPermission::Auto',
]) {
if (!tauriRustSource.includes(snippet)) {
throw new Error(
`AI game creator shell developer window guardrail drifted: ${snippet}`,
);
}
}
const runtimeConfigSetupStart = tauriHandlerSource.indexOf(
'configure_game_creator_runtime_config_dir(app.handle()).inspect_err(|error| {',
);
const runtimeConfigSetupEnd = tauriHandlerSource.indexOf(
'})?;',
runtimeConfigSetupStart,
);
const runtimeConfigSetupSource = tauriHandlerSource.slice(
runtimeConfigSetupStart,
runtimeConfigSetupEnd,
);
if (
runtimeConfigSetupStart === -1 ||
runtimeConfigSetupEnd === -1 ||
!runtimeConfigSetupSource.includes('sanitize_diagnostic_message(') ||
!runtimeConfigSetupSource.includes('append_bounded_diagnostic_line(') ||
!runtimeConfigSetupSource.includes(
'startup.appdata.configure.failed details={details}',
)
) {
throw new Error(
'AI game creator setup must configure the runtime AppData directory and log sanitized setup failures',
);
}
for (const snippet of [
'import.meta.env.DEV',
'#[cfg(all(debug_assertions, not(test)))]',
'open_developer_window(app.handle())?',
'tauri::WebviewWindowBuilder::new(app, "developer", developer_window_url())',
'index.html?agent-chat',
'supervisorChatMode',
'supervisorChatOnly',
'open_project_supervisor_chat_window',
'index.html?supervisor-chat&projectPath=',
]) {
if (!`${appEntrypointSource}\n${tauriRustSource}`.includes(snippet)) {
throw new Error(
`AI game creator shell developer window guardrail drifted: ${snippet}`,
);
}
}
const smokeAgentRunSource = fs.readFileSync(
new URL('./smoke-agent-run-local-provider.mjs', import.meta.url),
'utf8',
);
for (const snippet of [
'agentLlm',
'planner-smoke-model',
'generator-smoke-model',
'global-smoke-model-unused',
]) {
if (!smokeAgentRunSource.includes(snippet)) {
throw new Error(
`AI game creator local-provider smoke lost per-agent LLM coverage: ${snippet}`,
);
}
}
for (const snippet of [
"'game.run_local'",
"'read_game_creator_app_config'",
"'write_game_creator_app_config'",
'aria-label="运行时配置"',
'LLM API Key',
'画板 API Key',
'runtime_config.save',
"'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'",
"'activate_local_game_preview'",
'已切换到客户端运行视图',
'async function executeRunLocal',
'function needsInitializedChatProject',
'function resolvePendingCommandProjectPath',
'resolveChatProjectPath(localProject) ?? draftProjectPath',
'`permission.cancel ${command.id} missing-project`',
"'/remember [short|long|blackboard] 内容:追加短期、长期或黑板记忆'",
"'/memory-set [short|long|blackboard] 内容:覆盖保存对应记忆'",
'function parseRememberInput',
"'/trace 或 /loop:查看最近一次 Agent loop trace'",
'async function executeAgentTraceChat',
"relativePath: '.agent/logs/command.log'",
"'permission.pending'",
"'permission.confirm'",
"'permission.cancel'",
"'command.auto'",
"'agent.run_status'",
'function summarizeAgentRunTrace',
'工具调用:${agentRunTrace.toolCallCount}/${agentRunTrace.maxToolCalls}',
'agentRunTrace.error ?',
'className="trace-error"',
'agentRunTrace.taskGraph.repairRoutes.map',
"in: ${step.inputPaths.join(', ') || 'none'}",
"out: ${step.outputPaths.join(', ') || 'none'}",
]) {
if (!appSource.includes(snippet)) {
throw new Error(
`AI game creator shell trace panel guardrail drifted: ${snippet}`,
);
}
}
for (const script of [
'ai-game-creator-shell:dev',
'ai-game-creator-shell:dev-server',
'ai-game-creator-shell:build',
'ai-game-creator-shell:agent-task',
'agc:swarm',
'ai-game-creator-shell:typecheck',
'ai-game-creator-shell:agent-run:smoke',
'ai-game-creator-shell:check',
]) {
if (!rootPackageConfig.scripts?.[script]) {
throw new Error(`root package missing ${script}`);
}
}
if (
rootPackageConfig.scripts?.['agc:swarm'] !==
'npm --prefix apps/ai-game-creator-shell run swarm --'
) {
throw new Error('root agc:swarm script must forward CLI args');
}
if (
rootPackageConfig.scripts?.['ai-game-creator-shell:build'] !==
'npm --prefix apps/ai-game-creator-shell run build --'
) {
throw new Error(
'root ai-game-creator-shell:build script must forward build args',
);
}
const agentRunSmokeSource = fs.readFileSync(
new URL('../scripts/smoke-agent-run-local-provider.mjs', import.meta.url),
'utf8',
);
for (const snippet of [
"const smokeAssetMarker = 'SMOKE_LOCAL_ASSET:chef'",
"const smokeAudioAssetPath = 'assets/uploads/smoke-bounce.mp3'",
"mediaType: 'audio/mpeg'",
'document.body.dataset.smokeFrame = String(frameCount)',
"ctx.fillStyle = '#ff00ff'",
'ctx.fillRect(4, 4, 8, 24)',
'const sample = ctx.getImageData(4, 4, 24, 24).data',
'chef.complete && chef.naturalWidth > 0',
'document.body.dataset.smokeCanvasPixels = String(litPixels)',
'document.body.dataset.smokeCanvasColors = String(colors.size)',
'readBrowserDom(previewUrl)',
"extractDomNumber(previewDom, 'smoke-canvas-pixels')",
'--dump-dom',
'readHttpHead(new URL(smokeAssetPath, previewUrl).toString())',
'readHttpHead(new URL(smokeAudioAssetPath, previewUrl).toString())',
'function writeStreamingChatCompletion',
'requestJson?.stream === true',
`requestBodies.every((body) => body.includes('"stream":true'))`,
"const localConfigPath = path.join(appRoot, 'game-creator.config.local.json')",
"apiKind: 'openai_chat'",
'stream: true',
'await restoreOptionalFile(localConfigPath, previousLocalConfig)',
"method: 'HEAD'",
'previewAssetHead.contentLength === String(smokeAssetBytes.length)',
"previewAudioHead.contentType === 'audio/mpeg'",
"previewAssetHead.body === ''",
'trace missing professional group ${group}',
]) {
if (!agentRunSmokeSource.includes(snippet)) {
throw new Error(
`AI game creator shell agent-run smoke drifted: ${snippet}`,
);
}
}