Add generationStatus and match3d/runtime fixes

Introduce persistent generationStatus to work summaries (puzzle & match3d) and propagate generation recovery rules across docs and frontend/backends so "generating" is restored from server-side work summary rather than ephemeral front-end notices. Update API server image/asset handling (improve match3d material sheet green/alpha decontamination and promote generatedItemAssets background fields) and add runtime improvements: alpha-based hotspot hit-testing, tray insertion/three-match animation behavior, and session re-read on client-side VectorEngine timeouts/lock-screen interruptions. Many docs, tests and related frontend modules updated/added to reflect these contract and behavior changes.
This commit is contained in:
2026-05-16 22:59:02 +08:00
parent bb60ca91ef
commit a45e358e83
42 changed files with 3872 additions and 443 deletions

View File

@@ -567,6 +567,96 @@ describe('PuzzleResultView', () => {
).toHaveProperty('disabled', true);
});
test('keeps the current level dialog open when another level generation completes', () => {
const base = createSession();
const firstLevel = base.draft!.levels![0]!;
const generatingSecondLevel = {
...firstLevel,
levelId: 'puzzle-level-2',
levelName: '第二关',
pictureDescription: '第二关画面正在生成。',
candidates: [],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'generating' as const,
};
const localThirdLevel = {
...firstLevel,
levelId: 'puzzle-level-3',
levelName: '第三关',
pictureDescription: '第三关初稿。',
candidates: [],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'idle' as const,
};
const completedSecondLevel = {
...generatingSecondLevel,
candidates: [
{
candidateId: 'candidate-level-2',
imageSrc: '/puzzle/level-2.png',
assetId: 'asset-level-2',
prompt: '第二关画面',
actualPrompt: null,
sourceType: 'generated' as const,
selected: true,
},
],
selectedCandidateId: 'candidate-level-2',
coverImageSrc: '/puzzle/level-2.png',
coverAssetId: 'asset-level-2',
generationStatus: 'ready' as const,
};
const { rerender } = render(
<PuzzleResultView
session={createSession({
draft: {
...base.draft!,
levels: [firstLevel, generatingSecondLevel, localThirdLevel],
},
})}
onBack={() => {}}
onExecuteAction={() => {}}
/>,
);
openPuzzleLevelsTab();
fireEvent.click(screen.getByText('第三关'));
const dialog = screen.getByRole('dialog', { name: '关卡详情' });
fireEvent.change(within(dialog).getByLabelText('画面描述'), {
target: { value: '正在编辑第三关的信息。' },
});
rerender(
<PuzzleResultView
session={createSession({
draft: {
...base.draft!,
levels: [firstLevel, completedSecondLevel],
},
updatedAt: '2026-05-14T10:00:00.000Z',
})}
onBack={() => {}}
onExecuteAction={() => {}}
/>,
);
const currentDialog = screen.getByRole('dialog', { name: '关卡详情' });
expect(within(currentDialog).getByLabelText('关卡名称')).toHaveProperty(
'value',
'第三关',
);
expect(within(currentDialog).getByLabelText('画面描述')).toHaveProperty(
'value',
'正在编辑第三关的信息。',
);
expect(within(currentDialog).queryByDisplayValue('第二关')).toBeNull();
});
test('publishes with work info and serialized levels', () => {
const onExecuteAction = vi.fn();