Files
Genarrative/apps/ai-game-creator-shell/tests/projectResourceLiveUpdateModel.test.ts
menghao 03a4410272
Project CI / Native shell tests (push) Failing after 11m56s
Project CI / Repository checks (push) Successful in 1m5s
Project CI / Frontend tests (push) Successful in 3m20s
Project CI / Backend tests (push) Successful in 4m1s
game agent无限画布开发 (#136)
主要工作是共享同一套“画布内核与通用 UI 源码”,网站和 Tauri 只分别实现宿主适配层。

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Co-authored-by: kdletters <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/136
Co-authored-by: menghao <mh18530625731@163.com>
Co-committed-by: menghao <mh18530625731@163.com>
2026-08-12 10:54:16 +08:00

201 lines
5.7 KiB
TypeScript

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>,
): 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<Parameters<typeof resolveResourceFocusIntent>[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');
});
});