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 ts from 'typescript'; 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 windowsTauriConfig = JSON.parse( fs.readFileSync( new URL('../src-tauri/tauri.windows.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 appInvokeSources = readSourceFiles( new URL('../src/', import.meta.url), new Set(['.ts', '.tsx']), ); const appEntrypointSource = fs.readFileSync( new URL('../src/main.tsx', import.meta.url), 'utf8', ); const tauriHandlerSource = fs.readFileSync( new URL('../src-tauri/src/main.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 = [ 'append_direct_project_conversation_message', // TODO: Remove the retired binding command after the legacy runtime path is removed. 'bind_components', 'chat_with_game_creator_agent', 'check_ui_editor_font_glyph_coverage', 'create_ui_design_resource', 'open_game_creator_launcher_window', 'open_game_creator_workspace_window', 'read_direct_project_conversation', 'reset_design_agent_session', 'stop_local_game_preview_if_matches', 'start_game_creator_external_mcp', 'stop_game_creator_external_mcp', ]; 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 readSourceFiles(path, extensions) { const stat = fs.statSync(path); if (stat.isDirectory()) { return fs .readdirSync(path, { withFileTypes: true }) .sort((left, right) => left.name.localeCompare(right.name)) .flatMap((entry) => readSourceFiles( new URL(`${entry.name}${entry.isDirectory() ? '/' : ''}`, path), extensions, ), ); } if (!extensions.has(pathnameExtension(path.pathname))) return []; return [{ fileName: path.pathname, source: 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')}`, ); } } const APP_INVOKE_FILE_MAX_COUNT = 4 * 1024; const APP_INVOKE_TOTAL_SOURCE_MAX_LENGTH = 16 * 1024 * 1024; const APP_INVOKE_SOURCE_MAX_LENGTH = 2 * 1024 * 1024; const APP_INVOKE_COMMAND_MAX_LENGTH = 128; const APP_INVOKE_CALL_MAX_COUNT = 4 * 1024; const APP_INVOKE_BARE_CALL_NAMES = new Set([ 'invoke', 'directInvoke', 'invokeInput', 'invokeAuthenticatedInput', 'invokeDiagnostic', ]); function parseAppInvokeCommandNames(source, fileName = 'fixture.tsx') { const sourceByteLength = Buffer.byteLength(source, 'utf8'); if (sourceByteLength > APP_INVOKE_SOURCE_MAX_LENGTH) { throw new Error( `AI game creator shell App invoke source exceeds ${APP_INVOKE_SOURCE_MAX_LENGTH} bytes: ${fileName}`, ); } const sourceFile = ts.createSourceFile( fileName, source, ts.ScriptTarget.Latest, true, fileName.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS, ); const parseDiagnostic = sourceFile.parseDiagnostics[0]; if (parseDiagnostic !== undefined) { throw new Error( `AI game creator shell App invoke source cannot be parsed: ${fileName} (TS${parseDiagnostic.code})`, ); } const commands = []; const visit = (node) => { if (ts.isCallExpression(node)) { const expression = node.expression; const isBareCall = ts.isIdentifier(expression) && APP_INVOKE_BARE_CALL_NAMES.has(expression.text); const isObjectInvoke = ts.isPropertyAccessExpression(expression) && expression.name.text === 'invoke'; const commandArgument = ts.isIdentifier(expression) && expression.text === 'invokeDiagnostic' ? node.arguments[1] : node.arguments[0]; if ( (isBareCall || isObjectInvoke) && commandArgument !== undefined && ts.isStringLiteral(commandArgument) && commandArgument.text.length <= APP_INVOKE_COMMAND_MAX_LENGTH && /^[a-z0-9_]+$/u.test(commandArgument.text) ) { commands.push(commandArgument.text); if (commands.length > APP_INVOKE_CALL_MAX_COUNT) { throw new Error( `AI game creator shell App invoke calls exceed ${APP_INVOKE_CALL_MAX_COUNT}: ${fileName}`, ); } } } ts.forEachChild(node, visit); }; visit(sourceFile); return commands; } function parseAppInvokeSourceFiles(files) { if (files.length > APP_INVOKE_FILE_MAX_COUNT) { throw new Error( `AI game creator shell App invoke files exceed ${APP_INVOKE_FILE_MAX_COUNT}`, ); } const totalLength = files.reduce( (length, file) => length + Buffer.byteLength(file.source, 'utf8'), 0, ); if (totalLength > APP_INVOKE_TOTAL_SOURCE_MAX_LENGTH) { throw new Error( `AI game creator shell App invoke sources exceed ${APP_INVOKE_TOTAL_SOURCE_MAX_LENGTH} bytes`, ); } const commands = files.flatMap(({ fileName, source }) => parseAppInvokeCommandNames(source, fileName), ); if (commands.length > APP_INVOKE_CALL_MAX_COUNT) { throw new Error( `AI game creator shell App invoke calls exceed ${APP_INVOKE_CALL_MAX_COUNT}`, ); } return commands; } 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 assertCommandNamesDisjoint(label, leftNames, rightNames) { const right = new Set(rightNames); const overlapping = Array.from(new Set(leftNames)) .filter((name) => right.has(name)) .sort((left, rightName) => left.localeCompare(rightName)); if (overlapping.length > 0) { throw new Error(`${label} overlapping commands: ${overlapping.join(', ')}`); } } function runAppInvokeParserRegressionChecks() { assert.deepEqual( parseAppInvokeCommandNames(` invoke('direct_command', {}); directInvoke('generic_direct_command', {}); invokeInput < Result > ('input_wrapper_command', {}); invokeAuthenticatedInput( 'authenticated_input_wrapper_command', {}, ); input.invoke('object_field_command', {}); `), [ 'direct_command', 'generic_direct_command', 'input_wrapper_command', 'authenticated_input_wrapper_command', 'object_field_command', ], ); assert.deepEqual( parseAppInvokeCommandNames(` // invoke('line_comment_decoy') /* invokeInput('block_comment_decoy') */ const quoted = "directInvoke('string_decoy')"; const template = \`input.invoke('template_decoy')\`; const expression = /invokeAuthenticatedInput\\('regex_decoy'\\)/u; invokeCommand('unrelated_name'); myinvoke('unrelated_suffix'); invoke(dynamicCommand, {}); invoke('UPPERCASE_COMMAND', {}); `), [], ); assert.throws( () => parseAppInvokeCommandNames("invoke('malformed_generic', {"), /source cannot be parsed/u, ); assert.deepEqual( parseAppInvokeCommandNames( `invoke('${'a'.repeat(APP_INVOKE_COMMAND_MAX_LENGTH + 1)}', {})`, ), [], ); assert.throws( () => parseAppInvokeCommandNames(' '.repeat(APP_INVOKE_SOURCE_MAX_LENGTH + 1)), /source exceeds/u, ); const wrapperInvocations = parseAppInvokeCommandNames( "invokeInput('wrapper_reachability_command', {})", ); assert.doesNotThrow(() => assertCommandNamesSubset( 'App invoke parser reachability fixture', ['wrapper_reachability_command'], wrapperInvocations, ), ); assert.throws( () => assertCommandNamesDisjoint( 'App invoke parser false allowlist fixture', wrapperInvocations, ['wrapper_reachability_command'], ), /overlapping commands: wrapper_reachability_command/u, ); assert.throws( () => assertCommandNamesSubset( 'App invoke parser removed wrapper fixture', ['wrapper_reachability_command'], parseAppInvokeCommandNames('const wrapperWasRemoved = true;'), ), /missing commands: wrapper_reachability_command/u, ); } 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', }, }; const localConfig = { llm: { apiKey: 'fixture-old-overlay-key', model: 'old-overlay-model', stream: true, }, agentLlm: { planner: { model: 'planner-overlay-model' }, }, }; 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( 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', }, ); assert.equal(updatedPrimary.agentMode, 'provider'); await writeGameCreatorWizardConfig(overlayState, updatedPrimary); const reloadedState = await readGameCreatorWizardConfigState(overlayConfigDir); assert.equal(reloadedState.effectiveConfig.llm.apiKey, 'fixture-new-key'); assert.equal(reloadedState.effectiveConfig.agentMode, 'provider'); 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); 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('../src-tauri/tauri.windows.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), new URL('../src-tauri/tauri.windows.conf.json', import.meta.url), ]); assertNoNativeBrowserConfirm([new URL('../src/', import.meta.url)]); assertNoBlockingNativeFilePicker(tauriRustSource); runAppInvokeParserRegressionChecks(); const appInvokeCommandNames = parseAppInvokeSourceFiles(appInvokeSources); 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', appInvokeCommandNames, 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), [...appInvokeCommandNames, ...allowedUncalledTauriCommands], ); assertCommandNamesSubset( 'AI game creator shell explicit native-only allowlist', allowedUncalledTauriCommands, parseTauriHandlerCommandNames(tauriHandlerSource), ); assertCommandNamesDisjoint( 'AI game creator shell App invoke and explicit native-only allowlist', appInvokeCommandNames, allowedUncalledTauriCommands, ); const tauriHandlerCommandNames = parseTauriHandlerCommandNames(tauriHandlerSource); if (!tauriHandlerCommandNames.includes('create_automatic_local_game_project')) { throw new Error( 'AI game creator shell home creation must expose automatic project creation', ); } if ( tauriHandlerCommandNames.includes('chat_with_game_creator_home_direct_codex') ) { throw new Error( 'AI game creator shell home surface must not expose a projectless Codex conversation', ); } 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', ); } 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'); } const expectedBundledDesignAgentResources = { 'design-agent': 'design-agent', }; const expectedBundledCodexResources = { 'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe', 'resources/codex/win-x64/bin/codex-code-mode-host.exe': 'codex/win-x64/bin/codex-code-mode-host.exe', 'resources/codex/win-x64/codex-path/rg.exe': 'codex/win-x64/codex-path/rg.exe', 'resources/codex/win-x64/codex-resources/codex-command-runner.exe': 'codex/win-x64/codex-resources/codex-command-runner.exe', 'resources/codex/win-x64/codex-resources/codex-windows-sandbox-setup.exe': 'codex/win-x64/codex-resources/codex-windows-sandbox-setup.exe', 'resources/codex/win-x64/codex-package.json': 'codex/win-x64/codex-package.json', 'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md', 'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json', }; assert.deepEqual( tauriConfig.bundle?.resources, expectedBundledDesignAgentResources, 'AI game creator shell base Tauri config must bundle the design-agent resource pack', ); for (const key of Object.keys(tauriConfig.bundle?.resources ?? {})) { if (String(key).includes('codex')) { throw new Error( 'AI game creator shell base Tauri config must not require Windows-only Codex resources', ); } } assert.deepEqual( windowsTauriConfig.bundle?.resources, expectedBundledCodexResources, 'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set', ); if ( Object.prototype.hasOwnProperty.call( windowsTauriConfig.bundle?.resources ?? {}, 'design-agent', ) ) { throw new Error( 'design-agent resource pack must not be mixed into the Windows Codex sidecar bundle', ); } if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) { throw new Error( 'AI game creator shell Windows Tauri config must cache bundling tools in the project target directory', ); } 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]?.title !== '陶泥儿' || 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'); } if (defaultAppConfig.agentMode !== 'codex_app_server') { throw new Error( 'AI game creator shell default agentMode must be codex_app_server', ); } const allowedLlmReasoningEfforts = new Set([ 'default', 'low', 'medium', 'high', 'max', ]); if (defaultAppConfig.llm?.reasoningEffort !== 'max') { throw new Error( 'AI game creator shell default llm.reasoningEffort must stay max', ); } 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 !== undefined) { throw new Error( 'AI game creator shell ordinary default config must not contain editorApi', ); } 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 !== 720 ) { throw new Error( 'AI game creator shell client window must default to 1280x800 and stay at least 1280x720', ); } 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 ( !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', ); } const releaseVersions = [ packageConfig.version, tauriConfig.version, cargoPackageVersion, ]; if ( releaseVersions.some((version) => !/^\d+\.\d+\.\d+$/u.test(version ?? '')) || new Set(releaseVersions).size !== 1 ) { throw new Error( 'AI game creator release versions must be valid three-part semver and synchronized', ); } 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('陶泥儿')", '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', "'-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 (!/runChildCapture\(\s*['"]powershell\.exe['"]/u.test(configWizardSource)) { throw new Error( 'AI game creator config wizard guardrail drifted: runChildCapture(powershell.exe)', ); } 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(', 'game_creator_runtime_config_dir()', '.unwrap_or_else(|| 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', 'GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH', 'validate_game_creator_bundled_codex_cli', '内置 Codex CLI 完整性校验失败', ]) { 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('setup_log.fail(') || !runtimeConfigSetupSource.includes( 'startup.appdata.configure.failed details={details}', ) || !tauriHandlerSource.includes('impl StartupLogSlot {') || !tauriHandlerSource.includes('append_bounded_diagnostic_line(&path, line)') || !tauriHandlerSource.includes( 'self.append(line);\n show_startup_error_dialog(self.path().as_deref());', ) || !tauriHandlerSource.includes('early_startup_log_path(') ) { 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', '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}`, ); } } if (tauriHandlerSource.includes('open_developer_window(app.handle())?')) { throw new Error( 'AI game creator normal startup must not automatically open the developer window', ); } 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="运行时配置"', '陶泥儿智能创作(固定)', '官方账号服务(固定)', '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}`, ); } }