diff --git a/__tests__/client/restore.test.ts b/__tests__/client/restore.test.ts index 00d31fb..4fb10a7 100644 --- a/__tests__/client/restore.test.ts +++ b/__tests__/client/restore.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from 'bun:test' import { restoreConversations } from '@/client/lib/restore' import type { SessionHistory } from '@/client/lib/schemas' +import { sessionHistorySchema } from '@/client/lib/schemas' const history = (histories: SessionHistory['histories']): SessionHistory => ({ sessionId: '8571c162-a7d4-4be9-a14c-2d4ea2780d4f', @@ -145,3 +146,34 @@ describe('restoreConversations', () => { expect(Object.keys(result)).toEqual(['a']) }) }) + +/* + * 相手のIDは三種類ある。uuid の人物、決め打ちの `victim`、そして作者が書いた場所の + * ローカルID。履歴は聞き込みの画面へ入った瞬間に読むので、ここで受けそこねると + * 一手も打たないうちに画面ごと落ちる——実際、場所を足したときに `victim` までしか + * 許しておらず、場所を持つ事件が開いた時点で必ず落ちた。 + */ +describe('sessionHistorySchema の相手ID', () => { + const body = (characterId: string) => ({ + sessionId: '8571c162-a7d4-4be9-a14c-2d4ea2780d4f', + histories: [{ characterId, exchanges: [] }], + }) + + test('人物の uuid を受ける', () => { + expect( + sessionHistorySchema.safeParse(body('7f97837b-ef8f-46ff-a199-377926e8fb75')).success, + ).toBe(true) + }) + + test('遺体の victim を受ける', () => { + expect(sessionHistorySchema.safeParse(body('victim')).success).toBe(true) + }) + + test('場所のローカルIDを受ける', () => { + expect(sessionHistorySchema.safeParse(body('choba')).success).toBe(true) + }) + + test('三者のどれでもない文字列は弾く', () => { + expect(sessionHistorySchema.safeParse(body('帳場')).success).toBe(false) + }) +}) diff --git a/src/client/lib/schemas.ts b/src/client/lib/schemas.ts index 87bf28f..b125521 100644 --- a/src/client/lib/schemas.ts +++ b/src/client/lib/schemas.ts @@ -4,7 +4,7 @@ import { floorPlanSchema } from '~/db/floor-plan' import { gameModeSchema, hintSchema } from '~/db/game-mode' import { llmProviderSchema, settableLlmRoleSchema } from '~/db/llm-catalog' import { investigablePlaceSchema } from '~/db/place' -import { VICTIM_ID } from '~/db/scenario-definition' +import { placeIdSchema, VICTIM_ID } from '~/db/scenario-definition' /** * サーバのレスポンスは fetch の時点では unknown。 @@ -303,10 +303,15 @@ export const historyExchangeSchema = z.object({ /** * 話しかけた相手のID。 * - * 登場人物は uuid だが、被害者だけは決め打ちの `victim`(採番する先が一人しか無い)。 - * ここを uuid で縛ると、遺体を調べたセッションが復元できずに画面ごと落ちる。 + * 登場人物は uuid、被害者は決め打ちの `victim`、場所は作者が書いたローカルID。 + * 三者は形で見分けられるので、どれを指しているかは常に決まる(`placeIdSchema`)。 + * + * ここを狭く縛ると、その相手を含むセッションが復元できずに**画面ごと落ちる**。 + * 履歴は聞き込みの画面へ入った瞬間に読むので、一手も打たないうちに落ちる。 + * 実際、場所を足したとき `victim` までしか許しておらず、場所を持つ事件が + * 開いた時点で必ず落ちた。相手を増やしたらここも必ず増やすこと。 */ -const subjectIdSchema = z.union([z.uuid(), z.literal(VICTIM_ID)]) +const subjectIdSchema = z.union([z.uuid(), z.literal(VICTIM_ID), placeIdSchema]) export const sessionHistorySchema = z.object({ sessionId: z.uuid(),