From dbc4d2981c2f47c87a2c19920e9941fa2b222712 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 28 Jul 2026 13:19:11 +0800 Subject: [PATCH] feat(web): route turns through runtime bridge --- fixtures/runtime-state/initial.json | 56 +++++++++++++++++++++++++++++ src/app/bridge.spec.ts | 46 ++++++++++++++++++++++++ src/app/bridge.ts | 20 ++++++++++- src/app/useDemo.ts | 5 ++- 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 fixtures/runtime-state/initial.json create mode 100644 src/app/bridge.spec.ts diff --git a/fixtures/runtime-state/initial.json b/fixtures/runtime-state/initial.json new file mode 100644 index 0000000..4ac586b --- /dev/null +++ b/fixtures/runtime-state/initial.json @@ -0,0 +1,56 @@ +{ + "story_id": "story_nana_demo", + "current_node": "node_001", + "current_branch": "branch_main", + "world_flags": {}, + "relationships": { + "nana->player": { + "affinity": 48, + "trust": 34, + "hope": 31, + "respect": 45, + "intimacy": 26, + "attachment": 42 + } + }, + "relationship_states": [], + "promises": [], + "knowledge": [ + { + "id": "knowledge_station_clock", + "observer": "player", + "subject": "old_station_platform", + "fact": "站钟停在四点十七分,似乎并非自然损坏。", + "certainty": "confirmed", + "source": "玩家进入站台时亲眼观察", + "learned_at": "node_001", + "last_verified_at": "node_001" + } + ], + "items": [ + { + "id": "item_flashlight_001", + "spec_ref": "nana.item.old_flashlight", + "owner": "player", + "holder": "player", + "placement": "bag", + "quantity": 1, + "condition": "worn", + "state_tags": [], + "acquisition": { + "mode": "initial", + "from": null, + "at_node": "node_001" + } + } + ], + "clocks": [ + { + "id": "clock_dawn", + "label": "天亮逼近", + "value": 0, + "max": 6 + } + ], + "checks": [] +} diff --git a/src/app/bridge.spec.ts b/src/app/bridge.spec.ts new file mode 100644 index 0000000..80c23f5 --- /dev/null +++ b/src/app/bridge.spec.ts @@ -0,0 +1,46 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import initialView from "../../fixtures/player-view/initial.json"; +import type { PlayerView, TurnRequest, TurnResult } from "@contracts"; + +const invokeMock = vi.hoisted(() => vi.fn()); + +vi.mock("@tauri-apps/api/core", () => ({ + invoke: invokeMock +})); + +import { submitTurn } from "./bridge"; + +const request: TurnRequest = { + storyId: initialView.storyId, + branchId: initialView.branchId, + expectedNodeId: initialView.nodeId, + actionId: "action_bridge_test", + intent: "continue", + input: "" +}; + +describe("Tauri bridge", () => { + beforeEach(() => { + invokeMock.mockReset(); + window.__TAURI_INTERNALS__ = {}; + }); + + afterEach(() => { + delete window.__TAURI_INTERNALS__; + }); + + it("sends only TurnRequest to the submit_turn command", async () => { + const expected: TurnResult = { + committedNodeId: "node_002", + playerView: { + ...(initialView as PlayerView), + nodeId: "node_002" + } + }; + invokeMock.mockResolvedValue(expected); + + await expect(submitTurn(request, initialView as PlayerView)).resolves.toEqual(expected); + expect(invokeMock).toHaveBeenCalledWith("submit_turn", { request }); + }); +}); diff --git a/src/app/bridge.ts b/src/app/bridge.ts index fd02fd5..646af7d 100644 --- a/src/app/bridge.ts +++ b/src/app/bridge.ts @@ -1,7 +1,15 @@ import { invoke } from "@tauri-apps/api/core"; import initialView from "../../fixtures/player-view/initial.json"; -import type { AppInfo, DemoPackSummary, PlayerView } from "@contracts"; +import type { + AppInfo, + DemoPackSummary, + PlayerView, + TurnRequest, + TurnResult +} from "@contracts"; + +import { submitDemoTurn } from "./turnAdapter"; function isTauri(): boolean { return typeof window !== "undefined" && window.__TAURI_INTERNALS__ !== undefined; @@ -40,3 +48,13 @@ export async function getDemoPackSummary(): Promise { } return invoke("get_demo_pack_summary"); } + +export async function submitTurn( + request: TurnRequest, + currentView: PlayerView +): Promise { + if (!isTauri()) { + return submitDemoTurn(request, currentView); + } + return invoke("submit_turn", { request }); +} diff --git a/src/app/useDemo.ts b/src/app/useDemo.ts index 9083671..1216da9 100644 --- a/src/app/useDemo.ts +++ b/src/app/useDemo.ts @@ -2,8 +2,7 @@ import { onMounted, ref } from "vue"; import type { AppInfo, DemoPackSummary, PlayerView, TurnIntent, TurnRequest } from "@contracts"; -import { getAppInfo, getDemoPackSummary, getDemoPlayerView } from "./bridge"; -import { submitDemoTurn } from "./turnAdapter"; +import { getAppInfo, getDemoPackSummary, getDemoPlayerView, submitTurn as submitRuntimeTurn } from "./bridge"; export function useDemo() { const appInfo = ref(null); @@ -51,7 +50,7 @@ export function useDemo() { lastSubmittedIntent.value = intent; try { - const result = await submitDemoTurn(request, currentView); + const result = await submitRuntimeTurn(request, currentView); playerView.value = result.playerView; } catch (reason) { turnError.value = reason instanceof Error ? reason.message : String(reason);