03a4410272
主要工作是共享同一套“画布内核与通用 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>
201 lines
5.7 KiB
TypeScript
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');
|
|
});
|
|
});
|