修复构建测试隔离并对齐 Direct 消息契约
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m25s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 5m24s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 5m45s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m48s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m36s
Project CI / AI game creator shell Rust crates (push) Successful in 2m1s
Project CI / Frontend tests (push) Failing after 3m5s
Project CI / Repository checks (push) Failing after 3m22s
Project CI / Native shell tests (push) Successful in 6m27s
Project CI / AI game creator shell web tests (push) Successful in 2m35s
Project CI / Backend tests (push) Successful in 7m6s

隔离 Git hook 测试子进程环境并验证外层仓库不变
恢复被测试污染的完整 ESLint 与 Prettier 配置并清除测试产物
撤销无依据的聊天参数和资源初始化时序变更
严格验证 canonical userItem 和现行历史切片契约
同步开发运维文档与共享排障约定
This commit is contained in:
2026-09-16 01:40:58 +08:00
parent ca9cde1b91
commit e72a2da733
15 changed files with 708 additions and 160 deletions
+425 -5
View File
@@ -1,12 +1,432 @@
const globals = require('globals');
module.exports = {
root: true,
ignorePatterns: ['ignored/**'],
env: {
es2022: true,
},
parser: '@typescript-eslint/parser',
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
plugins: ['simple-import-sort', 'react-hooks', 'react-refresh'],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
globals: {
...globals.browser,
...globals.node,
},
overrides: [
{
files: ['scripts/**/*.{ts,js,mjs,cjs}'],
rules: {
'no-console': 'off',
},
},
{
files: ['**/scripts/**/*.{ts,js,mjs,cjs}'],
rules: {
'no-console': 'off',
},
},
{
files: ['**/*.test.{ts,tsx,js,mjs,cjs}'],
rules: {
'no-console': 'off',
'unused-imports/no-unused-vars': 'off',
},
},
{
files: ['miniprogram/**/*.js'],
globals: {
App: 'readonly',
},
},
{
files: [
'apps/admin-web/src/pages/*.tsx',
'src/components/platform-entry/PlatformActiveMobileWelcomeDialog.tsx',
'src/components/platform-entry/PlatformMobileHomeWelcomeDialog.tsx',
],
rules: {
'react-refresh/only-export-components': 'off',
},
},
{
files: ['src/components/platform-entry/PlatformEntryFlowShellImpl.tsx'],
rules: {
'react-hooks/exhaustive-deps': 'off',
},
},
{
files: [
'src/active-main.tsx',
'src/ActiveApp.tsx',
'src/AuthenticatedApp.tsx',
'src/hooks/useGameSettings.ts',
'src/routing/activeApp*.ts',
'src/routing/activeApp*.tsx',
'src/components/auth/**/*.{ts,tsx}',
'src/components/common/**/*.{ts,tsx}',
'src/components/creation-home/**/*.{ts,tsx}',
'src/components/image-editor/**/*.{ts,tsx}',
'src/components/project/**/*.{ts,tsx}',
'src/components/platform-entry/PlatformActiveProfileView*.tsx',
'src/components/platform-entry/PlatformActiveMobileWelcomeDialog*.tsx',
'src/components/platform-entry/PlatformEntryActiveFlowShell*.tsx',
'src/components/platform-entry/PlatformEntryFlowShell.tsx',
'src/components/platform-entry/PlatformProfileApiKeysModal.tsx',
'src/components/platform-entry/PlatformProfileModalShell.tsx',
'src/components/platform-entry/PlatformProfilePrimitives.tsx',
'src/components/platform-entry/PlatformProfileRechargeModal.tsx',
'src/components/platform-entry/PlatformProfileReferralModal.tsx',
'src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.tsx',
'src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx',
'src/components/platform-entry/PlatformRechargePaymentStatusDialogs.tsx',
'src/components/platform-entry/platformActiveProfileModel.ts',
'src/components/platform-entry/platformEntryActiveTypes.ts',
'src/components/platform-entry/platformProfileFundsModel.ts',
'src/components/platform-entry/platformProfileHostClipboard.ts',
'src/components/platform-entry/usePlatformProfileCenterController.ts',
'src/services/image-editor/**/*.{ts,tsx}',
'src/services/platform-entry/**/*.{ts,tsx}',
],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: [
'**/components/rpg-entry/**',
'**/components/*-creation/**',
'**/components/*-result/**',
'**/components/*-runtime/**',
'**/components/creation-agent/**',
'**/components/creative-agent/**',
'**/components/custom-world-*/**',
'**/components/unified-creation/**',
'**/services/rpg-entry/**',
'**/services/rpg-runtime/**',
'**/services/*-creation/**',
'**/services/*-runtime/**',
'**/services/*-works/**',
'**/services/creation-agent/**',
'**/services/creative-agent/**',
'**/services/puzzle-*/**',
'**/services/storyEngine/**',
],
message: '现役前端不得重新导入已退役的创作模板模块。',
},
],
},
],
},
},
{
files: ['src/components/game-canvas/**/*.tsx'],
rules: {
'react-refresh/only-export-components': 'off',
},
},
{
files: ['src/components/GameShell.tsx', 'src/hooks/useCombatFlow.ts'],
rules: {
'simple-import-sort/imports': 'off',
'simple-import-sort/exports': 'off',
},
},
],
plugins: [
'@typescript-eslint',
'react-hooks',
'react-refresh',
'simple-import-sort',
'unused-imports',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
ignorePatterns: [
'dist',
'dist_check',
'dist_check_monster_position',
'coverage',
'build',
'output',
'retired/**',
'node_modules',
'server-rs/target',
'server-rs/target-*',
'apps/desktop-shell/src-tauri/target',
'apps/ai-game-creator-shell/src/features/ui-editor/types/**',
'apps/ai-game-creator-shell/src/features/project-workspace/generated/**',
'target',
'src/main.tsx',
'src/App.tsx',
'src/routing/appPageRoutes.ts',
'src/routing/appPageRoutes.test.ts',
'src/routing/appRoutes.tsx',
'src/routing/appRoutes.test.ts',
'src/services/appTitle.ts',
'src/services/appTitle.test.ts',
'src/components/CharacterAnimator.tsx',
'src/components/*.ts',
'src/components/*.tsx',
'!src/components/ResolvedAssetAudio.tsx',
'!src/components/ResolvedAssetImage.tsx',
'!src/components/ResolvedAssetVideo.tsx',
'src/hooks/*.ts',
'src/hooks/*.tsx',
'!src/hooks/useGameSettings.ts',
'!src/hooks/useHostNavigationCanGoBack.ts',
'!src/hooks/useHostNavigationCanGoBack.test.tsx',
'!src/hooks/useResolvedAssetReadUrl.ts',
'!src/hooks/useResolvedAssetReadUrl.test.tsx',
'src/persistence/*.ts',
'src/persistence/*.tsx',
'!src/persistence/gameSettingsStorage.ts',
'!src/persistence/gameSettingsStorage.test.ts',
'!src/persistence/storage.ts',
'!src/persistence/storage.test.ts',
'src/routing/*.ts',
'src/routing/*.tsx',
'!src/routing/activeAppPageRoutes.ts',
'!src/routing/activeAppPageRoutes.test.ts',
'!src/routing/activeAppRoutes.tsx',
'!src/routing/activeAppRoutes.test.ts',
'src/services/*.ts',
'src/services/*.tsx',
'!src/services/activeAppTitle.ts',
'!src/services/activeAppTitle.test.ts',
'!src/services/apiClient.ts',
'!src/services/apiClient.test.ts',
'!src/services/assetReadUrlService.ts',
'!src/services/assetReadUrlService.test.ts',
'!src/services/authService.ts',
'!src/services/authService.test.ts',
'!src/services/clipboard.ts',
'!src/services/clipboard.test.ts',
'!src/services/frontendRuntimeConfigService.ts',
'!src/services/frontendRuntimeConfigService.test.ts',
'!src/services/sseStream.ts',
'!src/services/sseStream.test.ts',
'src/AdventurePanel.tsx',
'src/AdventureEntityModal.tsx',
'src/App.tsx',
'src/App.test.tsx',
'src/CharacterCreation.tsx',
'src/RpgRuntimeApp.tsx',
'src/routing/appPageRoutes.ts',
'src/routing/appPageRoutes.test.ts',
'src/routing/appRoutes.tsx',
'src/routing/appRoutes.test.ts',
'src/*PlaygroundApp.tsx',
'src/ChildMotionDemoApp.tsx',
'src/Match3DPlaygroundApp.tsx',
'src/components/child-motion-demo/**',
'src/components/asset-studio/**',
'src/components/bark-battle-creation/**',
'src/components/big-fish-creation/**',
'src/components/big-fish-result/**',
'src/components/big-fish-runtime/**',
'src/components/creation-agent/**',
'src/components/creative-agent/**',
'src/components/custom-world-agent/**',
'src/components/custom-world-home/**',
'src/components/edutainment-creation/**',
'src/components/edutainment-result/**',
'src/components/edutainment-runtime/**',
'src/components/game-canvas/**',
'src/components/jump-hop-result/**',
'src/components/jump-hop-runtime/**',
'src/components/match3d-result/**',
'src/components/match3d-runtime/**',
'src/components/puzzle-clear-creation/**',
'src/components/puzzle-clear-result/**',
'src/components/puzzle-clear-runtime/**',
'src/components/puzzle-gallery/**',
'src/components/puzzle-result/**',
'src/components/puzzle-runtime/**',
'src/components/rpg-creation-asset-studio/**',
'src/components/rpg-creation-result/**',
'src/components/rpg-runtime-panels/**',
'src/components/square-hole-creation/**',
'src/components/square-hole-result/**',
'src/components/square-hole-runtime/**',
'src/components/unified-creation/**',
'src/components/visual-novel-creation/**',
'src/components/visual-novel-result/**',
'src/components/visual-novel-runtime/**',
'src/components/wooden-fish-result/**',
'src/components/wooden-fish-runtime/**',
'src/components/common/PublishShare*',
'src/components/common/publishShare*',
'src/components/platform-entry/PlatformEntryCreation*',
'src/components/platform-entry/PlatformEntryFlowShellImpl.tsx',
'src/components/platform-entry/platformEntryTypes.ts',
'src/components/platform-entry/PlatformEntryHome*',
'src/components/platform-entry/PlatformEntryWorld*',
'src/components/platform-entry/PlatformMobileHome*',
'src/components/platform-entry/PlatformWork*',
'src/components/platform-entry/PlatformDraft*',
'src/components/platform-entry/PlatformTask*',
'src/components/platform-entry/PlatformError*',
'src/components/platform-entry/barkBattle*',
'src/components/platform-entry/platformCreation*',
'src/components/platform-entry/platformDialog*',
'src/components/platform-entry/platformDraft*',
'src/components/platform-entry/platformEdutainment*',
'src/components/platform-entry/platformEntryCreation*',
'src/components/platform-entry/platformExternal*',
'src/components/platform-entry/platformGeneration*',
'src/components/platform-entry/platformHost*',
'src/components/platform-entry/platformMiniGame*',
'src/components/platform-entry/platformPlayed*',
'src/components/platform-entry/platformPublic*',
'src/components/platform-entry/platformPuzzle*',
'src/components/platform-entry/platformRecommend*',
'src/components/platform-entry/platformRpg*',
'src/components/platform-entry/platformSelection*',
'src/components/platform-entry/puzzleDraft*',
'src/components/platform-entry/usePlatformCreation*',
'src/components/platform-entry/usePlatformEntry*',
'src/components/platform-entry/PlatformEntryFlowShellImpl/**',
'src/components/platform-entry/platformMatch3DRuntimeProfile*',
'src/components/unified-creation/workspaces/JumpHopCreationWorkspace*',
'src/components/unified-creation/workspaces/Match3DCreationWorkspace*',
'src/components/rpg-creation-editor/**',
'src/components/rpg-entry/**',
'src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx',
'src/components/custom-world-home/CustomWorldCreationHub.test.tsx',
'src/components/custom-world-home/CustomWorldCreationHub.testAdapter.tsx',
'src/components/custom-world-home/creationWorkShelf.test.ts',
'src/components/rpg-runtime-shell/**',
'src/hooks/rpg-runtime-story/**',
'src/hooks/rpg-session/**',
'src/hooks/combat/**',
'src/hooks/useCombatFlow.ts',
'src/hooks/useStoryOptions.ts',
'src/prompts/customWorldPrompts.ts',
'src/services/ai.ts',
'src/services/appTitle.ts',
'src/services/appTitle.test.ts',
'src/services/miniGameDraftGenerationProgress.ts',
'src/services/creationEntryConfigService.ts',
'src/services/creationUrlState*',
'src/services/customWorld*',
'src/services/input-devices/**',
'src/services/publicWorkCode.ts',
'src/services/runtimeGuestAuth.ts',
'src/services/runtimeRequest*',
'src/services/runtimeAudioFeedback.ts',
'src/services/useMocapInput*',
'src/services/wechatMiniProgramSubscribe*',
'src/services/bark-battle-creation/**',
'src/services/bark-battle-runtime/**',
'src/services/big-fish-creation/**',
'src/services/big-fish-gallery/**',
'src/services/big-fish-runtime/**',
'src/services/big-fish-works/**',
'src/services/creation-agent/**',
'src/services/creation-audio/**',
'src/services/creative-agent/**',
'src/services/edutainment-baby-drawing/**',
'src/services/edutainment-baby-object/**',
'src/services/child-motion-demo/**',
'src/services/jump-hop/**',
'src/services/match3d-creation/**',
'src/services/match3d-runtime/**',
'src/services/match3d-works/**',
'src/services/match3dGeneratedModelCache*',
'src/services/match3dSpritesheetParser*',
'src/services/puzzle-agent/**',
'src/services/puzzle-gallery/**',
'src/services/puzzle-onboarding/**',
'src/services/puzzle-runtime/**',
'src/services/puzzle-works/**',
'src/services/rpg-creation/**',
'src/services/rpg-entry/**',
'src/services/rpg-runtime/**',
'src/services/square-hole-creation/**',
'src/services/square-hole-runtime/**',
'src/services/square-hole-works/**',
'src/services/storyEngine/**',
'src/services/visual-novel-creation/**',
'src/services/visual-novel-runtime/**',
'src/services/visual-novel-works/**',
'src/services/wooden-fish/**',
'src/types.ts',
'src/types/**',
'src/uiAssets.ts',
'scripts/loadtest/**',
'packages/shared/src/contracts/jumpHop.ts',
'packages/shared/src/contracts/match3dAgent.ts',
'packages/shared/src/contracts/match3dRuntime.ts',
'packages/shared/src/contracts/match3dWorks.ts',
'packages/shared/src/contracts/barkBattle*',
'packages/shared/src/contracts/bigFish*',
'packages/shared/src/contracts/creationAgent*',
'packages/shared/src/contracts/creationAudio*',
'packages/shared/src/contracts/creativeAgent*',
'packages/shared/src/contracts/customWorld*',
'packages/shared/src/contracts/edutainment*',
'packages/shared/src/contracts/playTypes*',
'packages/shared/src/contracts/publicWork*',
'packages/shared/src/contracts/puzzle*',
'packages/shared/src/contracts/rpg*',
'packages/shared/src/contracts/squareHole*',
'packages/shared/src/contracts/story*',
'packages/shared/src/contracts/visualNovel*',
'packages/shared/src/contracts/woodenFish*',
'scripts/export-match3d-resource-pipeline*',
'scripts/generate-child-motion-demo-assets.mjs',
'src/services/puzzle-clear/**',
'src/games/**',
'src/data/**',
'src/prompts/**',
'apps/admin-web/src/pages/AdminCreationEntrySwitchPage*',
'apps/admin-web/src/pages/AdminWorkVisibilityPage*',
'src/services/recommendedRuntimeGuestLaunch.test.ts',
'src/data/sceneEncounterPreviews.ts',
'public/Icons',
'media',
'.codex-logs',
'*.log',
'.preview.*',
'temp-build-goal-check/**',
'tmp_*',
'tmp/**',
'npc-editor-*',
'temp-write-check.txt',
'**/__pycache__/**',
'*.timestamp-*.mjs',
],
rules: {
'@typescript-eslint/no-var-requires': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-refresh/only-export-components': [
'error',
{ allowConstantExport: true },
],
'simple-import-sort/imports': 'error',
'react-hooks/exhaustive-deps': 'warn',
'react-refresh/only-export-components': 'off',
'simple-import-sort/exports': 'error',
'@typescript-eslint/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'no-constant-condition': 'error',
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-useless-escape': 'error',
'prefer-const': 'error',
},
};
+5 -1
View File
@@ -1 +1,5 @@
{"singleQuote":true,"semi":true,"trailingComma":"all"}
{
"singleQuote": true,
"semi": true,
"trailingComma": "all"
}
-2
View File
@@ -6669,7 +6669,6 @@ export function App({
setProjectSupervisorRuntimeError('');
try {
const directTurnInput: {
projectId: string;
projectPath: string;
prompt: string;
clientTurnId: string;
@@ -6677,7 +6676,6 @@ export function App({
attachments?: DirectCodexTurnAttachment[];
userItem: ReturnType<typeof chatComposerDraftToDirectCodexUserItem>;
} = {
projectId: localProject.manifest.projectId,
projectPath: directProjectPath,
prompt,
clientTurnId,
@@ -1,8 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HorizontalFillOrigin } from "./HorizontalFillOrigin";
import type { Radial90Origin } from "./Radial90Origin";
import type { Radial180Origin } from "./Radial180Origin";
import type { Radial360Origin } from "./Radial360Origin";
import type { Radial90Origin } from "./Radial90Origin";
import type { VerticalFillOrigin } from "./VerticalFillOrigin";
export type FillMethod = { "Horizontal": HorizontalFillOrigin } | { "Vertical": VerticalFillOrigin } | { "Radial90": { origin: Radial90Origin, clockwise: boolean, } } | { "Radial180": { origin: Radial180Origin, clockwise: boolean, } } | { "Radial360": { origin: Radial360Origin, clockwise: boolean, } };
@@ -1448,7 +1448,6 @@ export default function ProjectDevelopmentView({
onMakeGame,
}: ProjectDevelopmentViewProps) {
const professionalDagVisible = orchestrationMode === 'professional-dag';
const resourceHydrationEnabled = orchestrationMode !== 'single-supervisor';
const [mode, setMode] = useState<WorkbenchMode>('resources');
const [runtimeInspectMode, setRuntimeInspectMode] = useState(false);
const [resourceBookState, dispatchResourceBook] = useReducer(
@@ -2146,7 +2145,6 @@ export default function ProjectDevelopmentView({
});
useEffect(() => {
if (!resourceHydrationEnabled) return undefined;
let cancelled = false;
if (sortMode !== 'dependency') {
setResourceGraphState({
@@ -2185,17 +2183,14 @@ export default function ProjectDevelopmentView({
hasSnapshot: canReuseSnapshot,
};
});
const readResourceGraph = () =>
invoke<ProjectResourceGraphReadModel>(
'read_local_project_resource_graph',
{
projectPath,
expectedProjectId: manifest.projectId,
resources: resourceGraphInputs,
},
);
void new Promise<void>((resolve) => setTimeout(resolve, 0))
.then(readResourceGraph)
void invoke<ProjectResourceGraphReadModel>(
'read_local_project_resource_graph',
{
projectPath,
expectedProjectId: manifest.projectId,
resources: resourceGraphInputs,
},
)
.then((readModel) => {
if (!cancelled) {
setResourceGraphState({
@@ -2225,7 +2220,6 @@ export default function ProjectDevelopmentView({
resourceGraphInputs,
resourceGraphProjectScopeKey,
resourceGraphScopeKey,
resourceHydrationEnabled,
sortMode,
]);
@@ -2519,14 +2513,14 @@ export default function ProjectDevelopmentView({
useEffect(() => {
resourceLayoutReadNoticeScopeRef.current = null;
setResourceLayoutReadNotice(null);
}, [manifest.projectId, projectPath, resourceHydrationEnabled]);
}, [manifest.projectId, projectPath]);
/**
* "组件恰好被卸载"
*
*/
useEffect(() => {
setResourceVersionReplacementLineage(null);
}, [manifest.projectId, projectPath, resourceHydrationEnabled]);
}, [manifest.projectId, projectPath]);
useEffect(() => {
const report = dependencyLayout.readReport ?? typeLayout.readReport;
if (
@@ -7196,7 +7190,6 @@ export default function ProjectDevelopmentView({
* effect Promise
*/
useEffect(() => {
if (!resourceHydrationEnabled) return undefined;
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke || !projectPath.trim() || !manifest.projectId.trim()) {
return undefined;
@@ -7206,46 +7199,45 @@ export default function ProjectDevelopmentView({
setResourceAssetGenerationTasksPanelOpen(false);
resourceAssetGenerationPanelSubmissionRef.current = null;
setResourceAssetGenerationPanelReopen(null);
void new Promise<void>((resolve) => setTimeout(resolve, 0)).then(
async () => {
const reportUnavailable = () => {
if (!cancelled) {
setResourceWorkbenchNotice(
'生成任务列表读取失败,暂时无法恢复历史任务',
);
}
};
let records: LocalProjectAssetGenerationTaskRecord[];
try {
const response = await invoke<
LocalProjectAssetGenerationTaskRecord[]
>('list_local_project_asset_generations', { projectPath });
if (!Array.isArray(response)) {
reportUnavailable();
return;
}
records = response;
} catch {
void (async () => {
const reportUnavailable = () => {
if (!cancelled) {
setResourceWorkbenchNotice(
'生成任务列表读取失败,暂时无法恢复历史任务',
);
}
};
let records: LocalProjectAssetGenerationTaskRecord[];
try {
const response = await invoke<LocalProjectAssetGenerationTaskRecord[]>(
'list_local_project_asset_generations',
{ projectPath },
);
if (!Array.isArray(response)) {
reportUnavailable();
return;
}
if (cancelled) {
return;
}
const restored = mergeResourceCanvasAssetGenerationTasksWithRecords(
resourceAssetGenerationTasksRef.current.filter(
(task) => task.projectId === projectId,
),
records,
);
resourceAssetGenerationTasksRef.current = restored;
setResourceAssetGenerationTasks(restored);
},
);
records = response;
} catch {
reportUnavailable();
return;
}
if (cancelled) {
return;
}
const restored = mergeResourceCanvasAssetGenerationTasksWithRecords(
resourceAssetGenerationTasksRef.current.filter(
(task) => task.projectId === projectId,
),
records,
);
resourceAssetGenerationTasksRef.current = restored;
setResourceAssetGenerationTasks(restored);
})();
return () => {
cancelled = true;
};
}, [manifest.projectId, projectPath, resourceHydrationEnabled]);
}, [manifest.projectId, projectPath]);
/**
*
@@ -1564,6 +1564,12 @@ export function registerHomeProjectCreationTests() {
prompt: '你好,今天多少号',
creationType: 'art',
clientTurnId: expect.any(String),
userItem: {
id: expect.stringMatching(/^direct-codex:[A-Za-z0-9-]+:user$/),
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: '你好,今天多少号' }],
},
});
expect(invoke).not.toHaveBeenCalledWith(
'chat_with_game_creator_home_direct_codex',
@@ -1664,6 +1670,12 @@ export function registerHomeProjectCreationTests() {
prompt: '按这个角色做游戏',
creationType: 'game',
clientTurnId: expect.any(String),
userItem: {
id: expect.stringMatching(/^direct-codex:[A-Za-z0-9-]+:user$/),
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: '按这个角色做游戏' }],
},
attachments: [
{
name: '角色参考.png',
@@ -1694,6 +1706,12 @@ export function registerHomeProjectCreationTests() {
projectPath: automaticProjectPath,
prompt: '再补一句玩法',
clientTurnId: expect.any(String),
userItem: {
id: expect.stringMatching(/^direct-codex:[A-Za-z0-9-]+:user$/),
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: '再补一句玩法' }],
},
});
const followUpPayload = invoke.mock.calls.find(
([command, args]) =>
@@ -2433,16 +2451,9 @@ export function registerHomeProjectCreationTests() {
policy: { deniedCommands: [], confirmCommands: [] },
};
}
if (
command === 'read_local_conversation' ||
command === 'read_direct_project_conversation'
) {
return {
path: `${projectPath}/.agent/conversations/project.jsonl`,
agentId: null,
sessionId: null,
messages: [...persistedMessages],
};
if (command === 'read_direct_project_history_slice') {
expect(args).toEqual({ projectPath, limit: 20 });
return { items: [...persistedMessages], hasMore: false };
}
if (command === 'append_local_conversation_message') {
throw new Error(
@@ -2451,18 +2462,14 @@ export function registerHomeProjectCreationTests() {
}
if (command === 'chat_with_game_creator_direct_codex') {
const clientTurnId = String(args?.clientTurnId ?? '');
persistedMessages.push(
{
role: 'user',
content: String(args?.prompt ?? ''),
messageId: `direct-codex:${clientTurnId}:user`,
},
{
role: 'assistant',
content: 'DIRECT_EXISTING_PROJECT_OK',
messageId: `direct-codex:${clientTurnId}:assistant`,
},
);
persistedMessages.push(args?.userItem as Record<string, unknown>, {
role: 'assistant',
type: 'message',
content: [
{ type: 'output_text', text: 'DIRECT_EXISTING_PROJECT_OK' },
],
id: `direct-codex:${clientTurnId}:assistant`,
});
return 'DIRECT_EXISTING_PROJECT_OK';
}
throw new Error(`unexpected invoke ${command}`);
@@ -2486,6 +2493,12 @@ export function registerHomeProjectCreationTests() {
projectPath,
prompt: '继续修改已有项目',
clientTurnId: expect.any(String),
userItem: {
id: expect.stringMatching(/^direct-codex:[A-Za-z0-9-]+:user$/),
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: '继续修改已有项目' }],
},
},
);
});
@@ -2524,16 +2537,9 @@ export function registerHomeProjectCreationTests() {
expect(args).toEqual({ projectPath });
return manifest;
}
if (
command === 'read_local_conversation' ||
command === 'read_direct_project_conversation'
) {
return {
path: `${projectPath}/.agent/conversations/project.jsonl`,
agentId: null,
sessionId: null,
messages: [...persistedMessages],
};
if (command === 'read_direct_project_history_slice') {
expect(args).toEqual({ projectPath, limit: 20 });
return { items: [...persistedMessages], hasMore: false };
}
if (command === 'append_local_permission_log') {
return {};
@@ -2544,49 +2550,30 @@ export function registerHomeProjectCreationTests() {
policy: { deniedCommands: [], confirmCommands: [] },
};
}
if (command === 'append_local_conversation_message') {
const message = args?.message as Record<string, unknown>;
const messageId = String(args?.messageId ?? '');
if (
!messageId ||
!persistedMessages.some(
(candidate) => candidate.messageId === messageId,
)
) {
persistedMessages.push({
schemaVersion: 'game-creator-conversation.v1',
...message,
messageId,
updatedAt: Number(
message.updatedAt ?? persistedMessages.length + 1,
),
});
}
return {
path: `${projectPath}/.agent/conversations/project.jsonl`,
agentId: null,
sessionId: null,
messages: [...persistedMessages],
};
}
if (command === 'chat_with_game_creator_direct_codex') {
const clientTurnId = String(args?.clientTurnId ?? '');
persistedMessages.push(
{
schemaVersion: 'game-creator-conversation.v1',
expect(args).toEqual({
projectPath,
prompt: '生成一个游戏',
clientTurnId: expect.any(String),
userItem: {
id: `direct-codex:${clientTurnId}:user`,
type: 'message',
role: 'user',
content: String(args?.prompt ?? ''),
messageId: `direct-codex:${clientTurnId}:user`,
updatedAt: 1,
content: [{ type: 'input_text', text: '生成一个游戏' }],
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '陶泥儿智能创作 鉴权失败,请重新登录后重试',
messageId: `direct-codex:${clientTurnId}:assistant`,
updatedAt: 2,
},
);
});
persistedMessages.push(args?.userItem as Record<string, unknown>, {
id: `direct-codex:${clientTurnId}:assistant`,
type: 'message',
role: 'assistant',
content: [
{
type: 'output_text',
text: '陶泥儿智能创作 鉴权失败,请重新登录后重试',
},
],
});
throw new Error('codex-app-server-error:unauthorized');
}
throw new Error(`unexpected invoke ${command}`);
@@ -2609,15 +2596,25 @@ export function registerHomeProjectCreationTests() {
await waitFor(() => {
expect(persistedMessages).toHaveLength(2);
});
expect(persistedMessages).toEqual(
expect.arrayContaining([
expect.objectContaining({ role: 'user', content: '生成一个游戏' }),
expect.objectContaining({
role: 'assistant',
content: '陶泥儿智能创作 鉴权失败,请重新登录后重试',
}),
]),
);
expect(persistedMessages).toEqual([
{
id: expect.stringMatching(/^direct-codex:[A-Za-z0-9-]+:user$/),
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: '生成一个游戏' }],
},
{
id: expect.stringMatching(/^direct-codex:[A-Za-z0-9-]+:assistant$/),
type: 'message',
role: 'assistant',
content: [
{
type: 'output_text',
text: '陶泥儿智能创作 鉴权失败,请重新登录后重试',
},
],
},
]);
expect(JSON.stringify(persistedMessages)).not.toContain(
'codex-app-server-error:unauthorized',
);
@@ -7312,6 +7312,17 @@ export function registerProjectSupervisorSurfaceTests() {
projectPath,
prompt: '从旧任务状态继续生成,但使用 direct Codex',
clientTurnId: expect.any(String),
userItem: {
id: expect.stringMatching(/^direct-codex:[A-Za-z0-9-]+:user$/),
type: 'message',
role: 'user',
content: [
{
type: 'input_text',
text: '从旧任务状态继续生成,但使用 direct Codex',
},
],
},
});
expect(invoke).not.toHaveBeenCalledWith(
'cancel_game_creator_agent_runtime_task',
@@ -7802,6 +7813,12 @@ export function registerProjectSupervisorSurfaceTests() {
projectPath,
prompt: '先完成正式客户端玩法拆解',
clientTurnId: firstTurnId,
userItem: {
id: `direct-codex:${firstTurnId}:user`,
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: '先完成正式客户端玩法拆解' }],
},
});
await act(async () => {
@@ -8048,6 +8065,12 @@ export function registerProjectSupervisorSurfaceTests() {
projectPath,
prompt: '补充:优先复用现有素材',
clientTurnId: expect.any(String),
userItem: {
id: expect.stringMatching(/^direct-codex:[A-Za-z0-9-]+:user$/),
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: '补充:优先复用现有素材' }],
},
},
);
});
@@ -8209,8 +8232,9 @@ export function registerProjectSupervisorSurfaceTests() {
});
expect(professionalRuntimeReadCount).toBe(0);
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('read_direct_project_conversation', {
expect(invoke).toHaveBeenCalledWith('read_direct_project_history_slice', {
projectPath,
limit: 20,
}),
);
// 默认任务占位行也不能触发专业 Agent 历史的批量读取。
@@ -8434,6 +8458,12 @@ export function registerProjectSupervisorSurfaceTests() {
projectPath,
prompt: '修改玩家移动脚本',
clientTurnId: expect.any(String),
userItem: {
id: expect.stringMatching(/^direct-codex:[A-Za-z0-9-]+:user$/),
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: '修改玩家移动脚本' }],
},
},
);
});
@@ -5609,3 +5609,9 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- **现象**`Genarrative-Stdb-Module-Publish` 在备份和 SpacetimeDB 就绪后,于 `spacetime publish``Permission denied`
- **原因**Jenkins 以 root 运行时 `${HOME}/data/tmp` 位于 `/root` 下;即使发布临时子目录已 `chown``spacetimedb`,父目录仍不可遍历。
- **处理**:发布给 `--run-as-user` 的 WASM 临时目录改用 `/var/tmp`,继续使用随机目录并在退出时清理。
## Git hook 测试必须清除继承的 Git 仓库环境
- 在 hook 内运行临时仓库测试时,`cwd` 不会覆盖继承的 `GIT_DIR``GIT_WORK_TREE``GIT_INDEX_FILE`。未隔离的 Git/lint-staged 子进程可能向真实仓库提交 fixture,甚至把测试版 ESLint、Prettier 配置带入主分支。
- fixture 子进程统一清除 `GIT_*` 环境,并用一次性外层 linked worktree 验证引用、索引、配置不变;原有工程检查规则保持完整,不能用逐项关闭规则修复 fixture 污染。
- Vitest 的 `toHaveBeenCalledWith` 匹配任意一次调用,失败输出会列出其它命令;应先定位相同命令的真实参数差异,不能由其它调用的序号推断时序故障。
@@ -1,5 +1,9 @@
# AI 游戏创作智能体 App 实施计划
## DirectProject 用户消息契约验证
`chat_with_game_creator_direct_codex` 必须携带 `projectPath``prompt`、稳定的 `clientTurnId` 和完整 `userItem``creationType``attachments` 按实际输入传递。Rust 通过 `projectPath` 解析项目身份,不接收额外 `projectId`。界面测试必须核对 `userItem` 的消息身份、角色、正文及附件内容,拒绝回合用例仍验证实际返回的错误原因。重开项目的历史恢复测试使用 `read_direct_project_history_slice` 的 canonical raw items 与 `hasMore`,首屏 `limit: 20`。资源图和生成任务的读取继续遵守原有工作台恢复逻辑,不因聊天断言失败延迟、关闭或改变它们。
## 2026-09-15 DirectProject 长回合平台会话保活
DirectProject 的生图、素材处理、构建和试玩可能跨越短生命周期 access token 的有效期。普通 `/api/*` 请求和 Codex app-server 已有 401 刷新路径,但 AGC 工具由 Rust 工具桥直接使用客户端当前会话,工具内部的 401 不会自动触发前端刷新。客户端在 DirectProject 回合处于 busy 状态时每 5 分钟调用现有 `requestPlatformSessionRefresh()`;刷新仍复用单飞请求、generation 校验和 native session 安装,不改变凭据来源,也不把 401 降级为成功。刷新失败保持静默,由原始 AGC 工具错误按现有鉴权失败合同返回,避免后台保活覆盖真实错误。
@@ -1,5 +1,11 @@
# 本地开发验证与生产运维
## 构建回归的隔离与发布文件权限
Git hook 的临时仓库测试必须清除子进程继承的仓库定位环境(例如 `GIT_DIR``GIT_WORK_TREE``GIT_INDEX_FILE`);仅设置 `cwd` 不能隔离 Git。回归应从带这些变量的外层仓库运行,验证外层引用、索引与配置不变。临时测试文件必须留在独立目录并清理,不得通过测试生成主仓库提交或覆盖 ESLint、Prettier 配置。修复 lint 配置时保留原有规则、忽略范围与零警告门禁,不以关闭规则代替排障。
Stdb 发布以 root 准备文件、再切换 `spacetimedb` 用户执行时,WASM 必须放在服务用户可遍历的父目录下。生产脚本在 `/var/tmp` 创建随机私有目录并移交给发布用户,退出时清理;不能仅修改子目录所有权后继续从 `/root` 下读取。发布成功的判据仍是完整 Full Build 的 Stdb、API、Web 发布及退出维护模式全部成功。
更新时间:`2026-08-05`
## 标准开发流程
-4
View File
@@ -1,4 +0,0 @@
import { zebra } from './zebra';
import { alpha } from './alpha';
void zebra; void alpha;
// staged ignored change
-2
View File
@@ -1,2 +0,0 @@
const original={value:1}
const keep = 2;
-6
View File
@@ -1,6 +0,0 @@
import { alpha } from './alpha';
import { zebra } from './zebra';
const staged = { value: 1 };
void zebra;
void alpha;
void staged;
+110 -6
View File
@@ -9,7 +9,7 @@ import {
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { homedir } from 'node:os';
import { delimiter, dirname, join, resolve } from 'node:path';
import { test } from 'node:test';
import { fileURLToPath } from 'node:url';
@@ -34,7 +34,7 @@ test('pre-commit hook fixes staged imports and formatting without swallowing uns
'npm run format:staged\n',
);
const tempRepo = mkdtempSync(join(tmpdir(), 'genarrative-git-hooks-'));
const tempRepo = createTempDirectory('genarrative-git-hooks-');
try {
git(tempRepo, 'init', '--quiet');
git(tempRepo, 'config', 'user.email', 'git-hooks-test@example.invalid');
@@ -128,7 +128,7 @@ test('pre-commit hook fixes staged imports and formatting without swallowing uns
cwd: tempRepo,
encoding: 'utf8',
env: {
...process.env,
...isolatedGitEnvironment(),
PATH: `${join(repoRoot, 'node_modules', '.bin')}${delimiter}${process.env.PATH ?? ''}`,
},
input: JSON.stringify(lintStagedConfig),
@@ -162,7 +162,7 @@ test('pre-commit hook fixes staged imports and formatting without swallowing uns
});
test('pre-push runs repository parity only for master updates', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-pre-push-'));
const tempDir = createTempDirectory('genarrative-pre-push-');
try {
const npmLog = join(tempDir, 'repo', 'npm.log');
const tempRepo = join(tempDir, 'repo');
@@ -214,7 +214,7 @@ exit 1
{
cwd: tempRepo,
encoding: 'utf8',
env: process.env,
env: isolatedGitEnvironment(),
},
);
};
@@ -237,6 +237,92 @@ exit 1
}
});
test('hook fixtures do not mutate the calling linked worktree or its index', () => {
const tempDir = createTempDirectory('genarrative-hook-isolation-');
try {
const outerRepo = join(tempDir, 'outer');
const worktree = join(tempDir, 'worktree');
mkdirSync(outerRepo);
git(outerRepo, 'init', '--quiet');
git(outerRepo, 'config', 'user.email', 'outer@example.invalid');
git(outerRepo, 'config', 'user.name', 'Outer Repository');
writeFileSync(join(outerRepo, 'sentinel.txt'), 'committed\n');
git(outerRepo, 'add', 'sentinel.txt');
git(
outerRepo,
'-c',
'commit.gpgsign=false',
'commit',
'--quiet',
'-m',
'sentinel',
);
git(
outerRepo,
'worktree',
'add',
'--quiet',
'-b',
'fixture-caller',
worktree,
);
writeFileSync(join(worktree, 'sentinel.txt'), 'staged\n');
git(worktree, 'add', 'sentinel.txt');
writeFileSync(join(worktree, 'sentinel.txt'), 'unstaged\n');
const gitDir = git(worktree, 'rev-parse', '--absolute-git-dir').trim();
const indexPath = join(gitDir, 'index');
const before = {
refs: git(outerRepo, 'show-ref'),
config: readFileSync(join(outerRepo, '.git', 'config'), 'utf8'),
index: readFileSync(indexPath),
status: git(worktree, 'status', '--porcelain'),
};
const result = spawnSync(
process.execPath,
[
'--test',
'--test-reporter=tap',
'--test-name-pattern=^pre-(commit|push)',
fileURLToPath(import.meta.url),
],
{
cwd: worktree,
encoding: 'utf8',
env: {
...isolatedGitEnvironment(),
NODE_TEST_CONTEXT: undefined,
GIT_DIR: gitDir,
GIT_COMMON_DIR: join(outerRepo, '.git'),
GIT_WORK_TREE: worktree,
GIT_INDEX_FILE: indexPath,
GIT_CONFIG_COUNT: '1',
GIT_CONFIG_KEY_0: 'core.worktree',
GIT_CONFIG_VALUE_0: worktree,
},
},
);
assert.equal(
result.status,
0,
`${result.stdout ?? ''}${result.stderr ?? ''}`,
);
assert.match(result.stdout, /# pass 2\b/u);
assert.equal(git(outerRepo, 'show-ref'), before.refs);
assert.equal(
readFileSync(join(outerRepo, '.git', 'config'), 'utf8'),
before.config,
);
assert.deepEqual(readFileSync(indexPath), before.index);
assert.equal(git(worktree, 'status', '--porcelain'), before.status);
assert.equal(
readFileSync(join(worktree, 'sentinel.txt'), 'utf8'),
'unstaged\n',
);
} finally {
rmSync(tempDir, { force: true, recursive: true });
}
});
test('Gitea Repository checks and master pre-push share the same repository command', () => {
const workflow = readFileSync(
join(repoRoot, '.gitea', 'workflows', 'project-ci.yml'),
@@ -290,5 +376,23 @@ function toBashPath(path) {
}
function git(cwd, ...args) {
return execFileSync('git', args, { cwd, encoding: 'utf8' });
return execFileSync('git', args, {
cwd,
encoding: 'utf8',
env: isolatedGitEnvironment(),
});
}
function isolatedGitEnvironment() {
// Git hooks export repository/index paths that override cwd, including in
// linked worktrees. Fixture Git and lint-staged must never inherit them.
return Object.fromEntries(
Object.entries(process.env).filter(([name]) => !/^GIT_/iu.test(name)),
);
}
function createTempDirectory(prefix) {
const tempRoot = join(homedir(), 'data', 'tmp');
mkdirSync(tempRoot, { recursive: true });
return mkdtempSync(join(tempRoot, prefix));
}
-1
View File
@@ -1 +0,0 @@
baseline