import { describe, expect, it } from "vitest"; import initialView from "../../fixtures/player-view/initial.json"; import type { ForkBranchRequest, PlayerView, TurnRequest } from "@contracts"; import { forkDemoBranch, submitDemoTurn } from "./turnAdapter"; function request(intent: TurnRequest["intent"], input: string): TurnRequest { return { storyId: initialView.storyId, branchId: initialView.branchId, expectedNodeId: initialView.nodeId, actionId: `test_${intent}`, intent, input }; } describe("local turn adapter", () => { it("preserves literal player input without inventing additional player speech", async () => { const literalInput = "我把手电筒放在长椅上。"; const result = await submitDemoTurn( request("speak_or_act", literalInput), initialView as PlayerView ); const playerBeats = result.playerView.beats.filter((beat) => beat.speaker === "你"); expect(playerBeats).toHaveLength(1); expect(playerBeats[0]?.text).toBe(literalInput); }); it("accepts an empty Continue intent without creating a player beat", async () => { const result = await submitDemoTurn(request("continue", ""), initialView as PlayerView); expect(result.committedNodeId).toBe("node_002"); expect(result.playerView.beats.some((beat) => beat.speaker === "你")).toBe(false); expect(result.playerView.beats.at(-1)?.speaker).toBe("娜娜"); }); it("mirrors a fork without moving or deleting the source history", async () => { const advanced = await submitDemoTurn( request("speak_or_act", "我先去看看站台。"), initialView as PlayerView ); const forkRequest: ForkBranchRequest = { storyId: advanced.playerView.storyId, currentBranchId: advanced.playerView.branchId, expectedCurrentNodeId: advanced.playerView.nodeId, sourceNodeId: initialView.nodeId, actionId: "fork_adapter_test" }; const forked = await forkDemoBranch(forkRequest, advanced.playerView); expect(forked.branchId).toBe("branch_fork_adapter_test"); expect(forked.playerView.nodeId).toBe(initialView.nodeId); expect(forked.playerView.branchId).toBe(forked.branchId); expect(forked.playerView.history).toHaveLength(1); expect(forked.playerView.history[0]?.isCurrent).toBe(true); expect(advanced.playerView.branchId).toBe(initialView.branchId); expect(advanced.playerView.nodeId).toBe("node_002"); }); it("plays the promise, hidden-search clue, tunnel, and return vertical slice", async () => { const promise = await submitDemoTurn( { ...request("speak_or_act", "我答应你,天亮前一定回来。"), actionId: "slice_promise" }, initialView as PlayerView ); expect(promise.playerView.promises[0]?.status).toBe("accepted"); const clue = await submitDemoTurn( { ...request("continue", ""), branchId: promise.playerView.branchId, expectedNodeId: promise.playerView.nodeId, actionId: "slice_clue" }, promise.playerView ); expect(clue.playerView.knowledge.at(-1)?.title).toBe("封锁隧道的检修门"); expect(clue.playerView.inventory.at(-1)?.name).toBe("半张旧车票"); const tunnel = await submitDemoTurn( { ...request("continue", ""), branchId: clue.playerView.branchId, expectedNodeId: clue.playerView.nodeId, actionId: "slice_tunnel" }, clue.playerView ); expect(tunnel.playerView.history.at(-1)?.label).toBe("进入封锁隧道"); const returned = await submitDemoTurn( { ...request("continue", ""), branchId: tunnel.playerView.branchId, expectedNodeId: tunnel.playerView.nodeId, actionId: "slice_return" }, tunnel.playerView ); expect(returned.playerView.promises[0]?.status).toBe("fulfilled"); expect(returned.playerView.canContinue).toBe(false); expect(returned.playerView.beats.at(-1)?.text).toContain("你回来了"); }); });