补齐平台壳宿主消费门禁

新增平台壳草稿通知和角标 HostBridge 同步层

补充真实 Tauri transport 单测和跳一跳完成集成测试

扩展原生壳门禁覆盖 H5 宿主消费链路

更新宿主壳协议、方案和共享决策记录
This commit is contained in:
2026-06-19 03:38:24 +08:00
parent 5ab7b53bf2
commit 49fffbca74
8 changed files with 379 additions and 13 deletions
@@ -9,6 +9,10 @@ import type { PublicUserSummary } from '../../../packages/shared/src/contracts/a
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { CreativeAgentSessionSnapshot } from '../../../packages/shared/src/contracts/creativeAgent';
import type {
HostBridgeCapability,
HostBridgeRequest,
} from '../../../packages/shared/src/contracts/hostBridge';
import type {
CustomWorldAgentSessionSnapshot,
CustomWorldWorkSummary,
@@ -94,6 +98,8 @@ import {
regenerateBabyObjectMatchDraftAssets,
saveBabyObjectMatchDraft,
} from '../../services/edutainment-baby-object';
import { resetHostRuntimeCacheForTest } from '../../services/host-bridge/hostBridge';
import { resetNativeAppHostBridgeForTest } from '../../services/host-bridge/nativeAppHostBridge';
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
import { match3dCreationClient } from '../../services/match3d-creation';
import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime';
@@ -287,6 +293,51 @@ function queryCreationTypeButton(name: string | RegExp) {
});
}
function asTauriInvoke(
invoke: (command: string, args?: Record<string, unknown>) => Promise<unknown>,
) {
return async function tauriInvoke<Result = unknown>(
command: string,
args?: Record<string, unknown>,
) {
return (await invoke(command, args)) as Result;
};
}
function nativeAppPath(capabilities: HostBridgeCapability[] = []) {
const params = new URLSearchParams({
clientRuntime: 'native_app',
hostShell: 'tauri_desktop',
});
if (capabilities.length > 0) {
params.set('hostCapabilities', capabilities.join(','));
}
return `/?${params.toString()}`;
}
function installTauriHostBridgeRecorder() {
const requests: HostBridgeRequest[] = [];
const invoke = vi.fn(async (_command: string, args?: Record<string, unknown>) => {
const request = (args as { request: HostBridgeRequest }).request;
requests.push(request);
return {
bridge: 'GenarrativeHostBridge',
version: 1,
id: request.id,
ok: true,
result: true,
};
});
window.__TAURI__ = {
core: {
invoke: asTauriInvoke(invoke),
},
};
return { invoke, requests };
}
async function openPuzzleFormFromCreateHub(
user: ReturnType<typeof userEvent.setup>,
) {
@@ -3779,6 +3830,10 @@ beforeEach(() => {
afterEach(() => {
vi.unstubAllEnvs();
delete window.__TAURI__;
delete window.ReactNativeWebView;
resetNativeAppHostBridgeForTest();
resetHostRuntimeCacheForTest();
});
test('create tab shows template tabs and embeds puzzle form by default', async () => {
@@ -9057,6 +9112,142 @@ test('completed unpublished jump hop draft opens result page without starting ru
);
});
test('native app jump hop draft completion sends host notification and badge from platform shell', async () => {
const user = userEvent.setup();
const sessionId = 'jump-hop-session-host-bridge-1';
const profileId = 'jump-hop-profile-host-bridge-1';
const work = buildMockJumpHopWork({
summary: {
runtimeKind: 'jump-hop',
workId: 'jump-hop-work-host-bridge-1',
profileId,
ownerUserId: 'user-1',
sourceSessionId: sessionId,
themeText: '桥接测试跳台',
workTitle: '桥接测试跳台',
workDescription: '原生壳桥接测试。',
themeTags: ['桥接'],
difficulty: 'standard',
stylePreset: 'minimal-blocks',
coverImageSrc: null,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-30T10:00:00.000Z',
publishedAt: null,
publishReady: true,
generationStatus: 'ready',
},
draft: {
...buildMockJumpHopWork().draft,
profileId,
themeText: '桥接测试跳台',
workTitle: '桥接测试跳台',
workDescription: '原生壳桥接测试。',
themeTags: ['桥接'],
stylePreset: 'minimal-blocks',
},
});
const createdSession = {
sessionId,
ownerUserId: 'user-1',
status: 'draft' as const,
draft: {
...work.draft,
profileId,
generationStatus: 'draft' as const,
},
createdAt: '2026-05-30T10:00:00.000Z',
updatedAt: '2026-05-30T10:00:00.000Z',
};
const readySession = {
...createdSession,
status: 'ready' as const,
draft: work.draft,
updatedAt: '2026-05-30T10:05:00.000Z',
};
let resolveCompile!: (value: {
actionType: 'compile-draft';
session: typeof readySession;
work: JumpHopWorkProfileResponse;
}) => void;
vi.mocked(fetchCreationEntryConfig).mockResolvedValueOnce({
...testCreationEntryConfig,
creationTypes: [
...testCreationEntryConfig.creationTypes,
{
id: 'jump-hop',
title: '跳一跳',
subtitle: '主题驱动平台跳跃',
badge: '可创建',
imageSrc: '/creation-type-references/jump-hop.webp',
visible: true,
open: true,
sortOrder: 55,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
unifiedCreationSpec: buildTestUnifiedCreationSpec('jump-hop', 5),
},
],
});
vi.mocked(jumpHopClient.createSession).mockResolvedValue({
session: createdSession,
});
vi.mocked(jumpHopClient.executeAction).mockReturnValue(
new Promise((resolve) => {
resolveCompile = resolve;
}),
);
const { requests } = installTauriHostBridgeRecorder();
window.history.replaceState(
null,
'',
nativeAppPath(['app.setBadgeCount', 'notification.showLocal']),
);
render(<TestWrapper withAuth />);
await openCreateTemplateHub(user);
await user.click(await findCreationTypeButton('跳一跳'));
await user.type(await screen.findByLabelText('主题'), '桥接测试跳台');
await user.click(await screen.findByRole('button', { name: '生成' }));
expect(
await screen.findByRole('progressbar', {
name: '跳一跳草稿生成进度',
}),
).toBeTruthy();
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
await openDraftHub(user);
await act(async () => {
resolveCompile({
actionType: 'compile-draft',
session: readySession,
work,
});
});
await waitFor(() => {
expect(
requests.some(
(request) =>
request.method === 'notification.showLocal' &&
(request.payload as { title?: string; body?: string }).title ===
'生成完成' &&
(request.payload as { body?: string }).body?.includes('跳一跳草稿'),
),
).toBe(true);
expect(
requests.some(
(request) =>
request.method === 'app.setBadgeCount' &&
(request.payload as { count?: number }).count === 1,
),
).toBe(true);
});
});
test('embedded puzzle form maps raw bearer token errors to user-facing auth copy', async () => {
const user = userEvent.setup();