This commit is contained in:
2026-04-19 20:33:18 +08:00
parent 692643136f
commit 67c584b4df
123 changed files with 11898 additions and 4082 deletions

View File

@@ -2030,10 +2030,169 @@ test('profile dashboard aggregates wallet, play time and played works at the acc
});
});
test('profile save archives list worlds by last played time and can resume a selected archive', async () => {
await withTestServer('profile-save-archives', async ({ baseUrl }) => {
const user = await authEntry(baseUrl, 'archive_user', 'secret123');
const firstSaveResponse = await httpRequest(
`${baseUrl}/api/runtime/save/snapshot`,
withBearer(user.token, {
method: 'PUT',
body: JSON.stringify({
savedAt: '2026-04-19T08:00:00.000Z',
bottomTab: 'adventure',
currentStory: {
text: '潮声还在旧灯塔下回荡。',
options: [],
},
gameState: {
worldType: 'CUSTOM',
playerCurrency: 120,
runtimeStats: {
playTimeMs: 5400000,
},
storyEngineMemory: {
continueGameDigest: '回到裂潮边城的旧灯塔继续追查假航灯。',
},
customWorldProfile: {
id: 'world-aurora',
name: '裂潮边城',
summary: '潮声与城线之间的冷铁边疆。',
},
},
}),
}),
);
assert.equal(firstSaveResponse.status, 200);
const secondSaveResponse = await httpRequest(
`${baseUrl}/api/runtime/save/snapshot`,
withBearer(user.token, {
method: 'PUT',
body: JSON.stringify({
savedAt: '2026-04-19T10:15:00.000Z',
bottomTab: 'inventory',
currentStory: {
text: '江湖新章的风雨夜刚刚开始。',
options: [],
},
gameState: {
worldType: 'WUXIA',
playerCurrency: 86,
runtimeStats: {
playTimeMs: 900000,
},
currentScenePreset: {
name: '江湖新章',
summary: '雨夜客栈里的新委托。',
},
},
}),
}),
);
assert.equal(secondSaveResponse.status, 200);
const listResponse = await httpRequest(
`${baseUrl}/api/runtime/profile/save-archives`,
withBearer(user.token),
);
const listPayload = (await listResponse.json()) as {
entries: Array<{
worldKey: string;
worldName: string;
summaryText: string;
lastPlayedAt: string;
}>;
};
assert.equal(listResponse.status, 200);
assert.deepEqual(
listPayload.entries.map((entry) => entry.worldKey),
['builtin:WUXIA', 'custom:world-aurora'],
);
assert.equal(listPayload.entries[0]?.worldName, '江湖新章');
assert.equal(
listPayload.entries[1]?.summaryText,
'回到裂潮边城的旧灯塔继续追查假航灯。',
);
assert.equal(
listPayload.entries[0]?.lastPlayedAt,
'2026-04-19T10:15:00.000Z',
);
const resumeResponse = await httpRequest(
`${baseUrl}/api/runtime/profile/save-archives/${encodeURIComponent('custom:world-aurora')}`,
withBearer(user.token, {
method: 'POST',
}),
);
const resumePayload = (await resumeResponse.json()) as {
entry: {
worldKey: string;
};
snapshot: {
bottomTab: string;
gameState: {
playerCurrency: number;
customWorldProfile: {
id: string;
name: string;
} | null;
};
};
};
assert.equal(resumeResponse.status, 200);
assert.equal(resumePayload.entry.worldKey, 'custom:world-aurora');
assert.equal(resumePayload.snapshot.bottomTab, 'adventure');
assert.equal(resumePayload.snapshot.gameState.playerCurrency, 120);
assert.equal(
resumePayload.snapshot.gameState.customWorldProfile?.id,
'world-aurora',
);
const currentSnapshotResponse = await httpRequest(
`${baseUrl}/api/runtime/save/snapshot`,
withBearer(user.token),
);
const currentSnapshotPayload = (await currentSnapshotResponse.json()) as {
bottomTab: string;
gameState: {
playerCurrency: number;
customWorldProfile: {
id: string;
} | null;
};
};
assert.equal(currentSnapshotResponse.status, 200);
assert.equal(currentSnapshotPayload.bottomTab, 'adventure');
assert.equal(currentSnapshotPayload.gameState.playerCurrency, 120);
assert.equal(
currentSnapshotPayload.gameState.customWorldProfile?.id,
'world-aurora',
);
const dashboardResponse = await httpRequest(
`${baseUrl}/api/runtime/profile/dashboard`,
withBearer(user.token),
);
const dashboardPayload = (await dashboardResponse.json()) as {
walletBalance: number;
totalPlayTimeMs: number;
playedWorldCount: number;
};
assert.equal(dashboardResponse.status, 200);
assert.equal(dashboardPayload.walletBalance, 86);
assert.equal(dashboardPayload.totalPlayTimeMs, 6300000);
assert.equal(dashboardPayload.playedWorldCount, 2);
});
});
test('custom worlds stay private until published and then appear in the public gallery', async () => {
await withTestServer('custom-world-gallery', async ({ baseUrl }) => {
const owner = await authEntry(baseUrl, 'gallery_owner', 'secret123');
const viewer = await authEntry(baseUrl, 'gallery_viewer', 'secret123');
const upsertResponse = await httpRequest(
`${baseUrl}/api/runtime/custom-world-library/world-a`,
@@ -2084,15 +2243,11 @@ test('custom worlds stay private until published and then appear in the public g
const galleryBeforePublish = await httpRequest(
`${baseUrl}/api/runtime/custom-world-gallery`,
{
headers: {
Authorization: `Bearer ${viewer.token}`,
},
},
);
const galleryBeforePayload = (await galleryBeforePublish.json()) as {
entries: unknown[];
};
assert.equal(galleryBeforePublish.status, 200);
assert.deepEqual(galleryBeforePayload.entries, []);
const publishResponse = await httpRequest(
@@ -2114,11 +2269,6 @@ test('custom worlds stay private until published and then appear in the public g
const galleryAfterPublish = await httpRequest(
`${baseUrl}/api/runtime/custom-world-gallery`,
{
headers: {
Authorization: `Bearer ${viewer.token}`,
},
},
);
const galleryAfterPayload = (await galleryAfterPublish.json()) as {
entries: Array<{
@@ -2139,11 +2289,6 @@ test('custom worlds stay private until published and then appear in the public g
const galleryDetail = await httpRequest(
`${baseUrl}/api/runtime/custom-world-gallery/${encodeURIComponent(galleryAfterPayload.entries[0]?.ownerUserId || '')}/${encodeURIComponent(galleryAfterPayload.entries[0]?.profileId || '')}`,
{
headers: {
Authorization: `Bearer ${viewer.token}`,
},
},
);
const galleryDetailPayload = (await galleryDetail.json()) as {
entry: {
@@ -2175,11 +2320,6 @@ test('custom worlds stay private until published and then appear in the public g
const galleryAfterUnpublish = await httpRequest(
`${baseUrl}/api/runtime/custom-world-gallery`,
{
headers: {
Authorization: `Bearer ${viewer.token}`,
},
},
);
const galleryAfterUnpublishPayload =
(await galleryAfterUnpublish.json()) as {