import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; import { createProjectManifestMergeState, mergeProjectManifestSnapshot, type ProjectManifestSnapshot, resolveResourceFocusIntent, type ResourceFocusIntent, } from '../src/view/project-development/projectResourceLiveUpdateModel'; function manifest(projectId: string, assetIds: string[] = []) { return { schemaVersion: 'game-creation-app.v1', projectId, name: projectId, tasks: [], assets: assetIds.map((id) => ({ id, kind: 'asset', mediaType: 'image/png', localPath: `assets/${id}.png`, source: { kind: 'canvas' as const, taskId: null }, })), versions: [], } as unknown as GameCreationAppManifest; } function snapshot( revision: number, source: ProjectManifestSnapshot['source'], assetIds: string[] = [], ): ProjectManifestSnapshot { const value = manifest('project-live', assetIds); return { projectPath: '/tmp/project-live', projectId: value.projectId, revision, manifest: value, source, commitId: assetIds.length > 0 ? `commit-${assetIds.at(-1)}` : undefined, eventId: source === 'asset-event' ? `event-${revision}` : undefined, }; } function focusIntent( input?: Partial, ): ResourceFocusIntent { return { flowId: 'flow-1', saveAttemptId: 'save-1', sessionId: 'session-1', draftId: 'draft-1', commitId: 'commit-1', projectPath: '/tmp/project-live', projectId: 'project-live', focusGeneration: 3, resourceId: 'asset:new-art', completed: false, ...input, }; } function focusEnvironment( input?: Partial[1]>, ) { return { projectPath: '/tmp/project-live', projectId: 'project-live', flowId: 'flow-1', focusGeneration: 3, projected: true, dependencyLayoutSettled: true, dependencyPositioned: true, typeLayoutSettled: true, typePositioned: true, visible: true, domRendered: true, ...input, }; } describe('project resource live update model', () => { it('deduplicates command-first and event-first commit delivery', () => { const initial = createProjectManifestMergeState(snapshot(4, 'initial')); const command = snapshot(5, 'asset-command', ['new-art']); const event = { ...snapshot(5, 'asset-event', ['new-art']), eventId: 'event-5', }; const commandFirst = mergeProjectManifestSnapshot(initial, command); expect(commandFirst.decision).toBe('accepted'); const commandThenEvent = mergeProjectManifestSnapshot( commandFirst.state, event, ); expect(commandThenEvent.decision).toBe('duplicate'); expect(commandThenEvent.state.seenEventIds).toEqual(['event-5']); const eventFirst = mergeProjectManifestSnapshot(initial, event); expect(eventFirst.decision).toBe('accepted'); expect( mergeProjectManifestSnapshot(eventFirst.state, command).decision, ).toBe('duplicate'); }); it('rejects an old poll and fails closed on a divergent equal revision', () => { const accepted = createProjectManifestMergeState( snapshot(8, 'asset-event', ['new-art']), ); expect( mergeProjectManifestSnapshot(accepted, snapshot(7, 'poll')).decision, ).toBe('stale-revision'); expect( mergeProjectManifestSnapshot( accepted, snapshot(8, 'poll', ['different-art']), ).decision, ).toBe('revision-conflict'); }); it('keeps project switching and user selection generations from stealing focus', () => { expect( resolveResourceFocusIntent( focusIntent(), focusEnvironment({ projectId: 'other-project' }), ), ).toBe('invalid'); expect( resolveResourceFocusIntent( focusIntent(), focusEnvironment({ focusGeneration: 4 }), ), ).toBe('invalid'); }); it('lets only the latest consecutive save intent focus', () => { expect( resolveResourceFocusIntent( focusIntent({ flowId: 'flow-old', focusGeneration: 2 }), focusEnvironment(), ), ).toBe('invalid'); expect(resolveResourceFocusIntent(focusIntent(), focusEnvironment())).toBe( 'focus', ); }); it('distinguishes projection, both layouts, filtering, DOM and one-shot completion', () => { const intent = focusIntent(); expect( resolveResourceFocusIntent( intent, focusEnvironment({ projected: false }), ), ).toBe('wait-projection'); expect( resolveResourceFocusIntent( intent, focusEnvironment({ dependencyLayoutSettled: false }), ), ).toBe('wait-layout'); expect( resolveResourceFocusIntent( intent, focusEnvironment({ typePositioned: false }), ), ).toBe('wait-layout'); expect( resolveResourceFocusIntent(intent, focusEnvironment({ visible: false })), ).toBe('hidden'); expect( resolveResourceFocusIntent( intent, focusEnvironment({ domRendered: false }), ), ).toBe('wait-dom'); expect(resolveResourceFocusIntent(intent, focusEnvironment())).toBe( 'focus', ); expect( resolveResourceFocusIntent( { ...intent, completed: true }, focusEnvironment(), ), ).toBe('completed'); }); it('does not implement the live completion path with reload or project reopen', () => { const source = readFileSync( new URL('../src/view/project-development/index.tsx', import.meta.url), 'utf8', ); expect(source).not.toMatch(/(?:location|window\.location)\.reload\s*\(/u); expect(source).not.toContain('open_local_game_project'); }); });