返回 DeepSeek-Reasonix
mockForkWorktree.ts
根目录 / desktop / frontend / src / lib / mockForkWorktree.ts
1 import type { ForkBindings, ForkWorktreeResultView } from "./forkWorktree";
2 import type { ForkAnchorView, ForkCreationView, ForkTargetSetView, ForkTargetView } from "../generated/desktopContract.generated";
3 import type { HistoryMessage, TabMeta } from "./types";
4
5 export function mockForkWorktree(tab: TabMeta): ForkWorktreeResultView {
6 return { tab: { ...tab, workspaceRoot: `${tab.workspaceRoot}-worktree` }, isolated: true, branch: "reasonix/delivery-mock" };
7 }
8
9 export function increaseMockForkTitle(title: string): string {
10 const base = title.trim();
11 const ascii = /^(.*) \(([0-9]+)\)$/.exec(base);
12 if (ascii) return `${ascii[1]} (${BigInt(ascii[2]) + 1n})`;
13 const fullwidth = /^(.*)(([0-9]+))$/.exec(base);
14 if (fullwidth) return `${fullwidth[1]}(${BigInt(fullwidth[2]) + 1n})`;
15 return `${base} (1)`;
16 }
17
18 /**
19 * Fork targets for one mock source, derived the way the host derives them: one
20 * target per user turn, addressed by the message identity of that turn's final
21 * answer. The trailing turn stays open while the tab runs, so its entry explains
22 * itself instead of offering a cut the source cannot prove. A source whose
23 * messages carry no identity answers with an empty, unverifiable set.
24 */
25 export function mockForkTargets(tabId: string, messages: readonly HistoryMessage[], running: boolean): ForkTargetSetView {
26 const targets: ForkTargetView[] = [];
27 let turn = 0;
28 let answer: string | undefined;
29 const close = (open: boolean) => {
30 if (!turn) return;
31 const turnId = `${tabId}:turn-${turn}`;
32 targets.push(!open && answer
33 ? { sourceSessionId: tabId, sessionGeneration: 1, turnId, boundarySequence: turn * 3, turnNumber: turn, status: "committed", messageId: answer, available: true }
34 : { sourceSessionId: tabId, sessionGeneration: 1, turnId, boundarySequence: 0, turnNumber: turn, status: open ? "in_progress" : "committed", available: false, reason: "turn_open" });
35 answer = undefined;
36 };
37 for (const message of messages) {
38 if (message.role === "user") { close(false); turn += 1; continue; }
39 if (message.role === "assistant" && message.messageId && message.content?.trim()) answer = message.messageId;
40 }
41 close(running);
42 return { sourceSessionId: tabId, sessionGeneration: 1, targets, verifiable: messages.some((message) => Boolean(message.messageId)) };
43 }
44
45 // Browser-dev fork fixtures: an attach failure exercises the recovery notice,
46 // and a recordless source exercises the unverifiable boundary.
47 function mockForkFixture(name: string): boolean {
48 return typeof window !== "undefined" && new URLSearchParams(window.location.search).get(name) === "1";
49 }
50
51 /**
52 * The history a dev fork reads, with the identity the real pipeline assigns: a
53 * rendered answer and the fork target that names it resolve through one key. The
54 * recordless fixture drops those identities, which is the unverifiable boundary.
55 */
56 export function withMockHistoryIds(prefix: string, messages: HistoryMessage[]): HistoryMessage[] {
57 if (mockForkFixture("fork-recordless")) return messages;
58 return messages.map((message, index) => message.role === "assistant" && !message.messageId
59 ? { ...message, messageId: `${prefix}:m${index}` } : message);
60 }
61
62 interface MockForkBindings extends ForkBindings {
63 Fork(turn: number): Promise<TabMeta>;
64 ForkTargetsForTab(tabID: string): Promise<ForkTargetSetView>;
65 CreateForkForTab(tabID: string, anchor: ForkAnchorView): Promise<ForkCreationView>;
66 AcknowledgeForkOperation(tabID: string, operationID: string): Promise<void>;
67 }
68
69 export function makeMockForkBindings(
70 getTabs: () => TabMeta[],
71 setTabs: (tabs: TabMeta[]) => void,
72 defaultTitle: string,
73 history: (tabID: string) => Promise<HistoryMessage[]>,
74 attachFailure = mockForkFixture("fork-attach-failure"),
75 ): MockForkBindings {
76 const pendingForks = new Map<string, { operationId: string; sessionId: string }>();
77 const fork = async (_turn: number): Promise<TabMeta> => {
78 const tabs = getTabs();
79 const active = tabs.find((tab) => tab.active) ?? tabs[0];
80 const stamp = Date.now();
81 const tab: TabMeta = {
82 ...active,
83 id: `tab_fork_${stamp}`,
84 topicId: `topic_fork_${stamp}`,
85 topicTitle: increaseMockForkTitle(active.topicTitle || defaultTitle),
86 active: true,
87 running: false,
88 };
89 setTabs([...tabs.map((item) => ({ ...item, active: false })), tab]);
90 return { ...tab };
91 };
92 const forkForTab = async (tabID: string, turn: number): Promise<TabMeta> => {
93 setTabs(getTabs().map((tab) => ({ ...tab, active: tab.id === tabID })));
94 return fork(turn);
95 };
96 return {
97 Fork: fork,
98 ForkForTab: forkForTab,
99 async ForkWorktreeForTab(tabID, turn) {
100 return mockForkWorktree(await forkForTab(tabID, turn));
101 },
102 // The targets come from the same history the transcript renders, so the
103 // entry a developer clicks names the answer message on screen.
104 async ForkTargetsForTab(tabID) {
105 const tabs = getTabs();
106 const tab = tabs.find((candidate) => candidate.id === tabID) ?? tabs.find((candidate) => candidate.active) ?? tabs[0];
107 return mockForkTargets(tab?.id ?? tabID, await history(tab?.id ?? tabID), Boolean(tab?.running));
108 },
109 async CreateForkForTab(tabID, anchor) {
110 if (!tabID || !anchor.turnId) return { opened: false };
111 const key = [anchor.sourceHostId ?? "", anchor.sourceSessionId, anchor.turnId, anchor.boundarySequence].join("\0");
112 let operation = pendingForks.get(key);
113 if (!operation) {
114 const operationId = crypto.randomUUID();
115 operation = { operationId, sessionId: `mock-fork-${anchor.turnId}-${operationId}` };
116 pendingForks.set(key, operation);
117 }
118 const { operationId, sessionId } = operation;
119 if (attachFailure) return { sessionId, operationId, opened: false, error: "conversation fork was created but could not be opened; open the recovery branch from session history" };
120 const tab = await forkForTab(tabID, 0);
121 return { sessionId, operationId, tabId: tab.id, opened: true };
122 },
123 async AcknowledgeForkOperation(_tabID, operationID) {
124 for (const [key, operation] of pendingForks) if (operation.operationId === operationID) pendingForks.delete(key);
125 },
126 };
127 }
128
128 lines TYPESCRIPT