返回 DeepSeek-Reasonix
composer-canonical-inbox.test.tsx
根目录 / desktop / frontend / src / __tests__ / composer-canonical-inbox.test.tsx
1 import assert from "node:assert/strict";
2 import { act } from "react";
3 import { installDom, installBridgeApp, renderComposer } from "./composerInboxHarness";
4 import { makeSessionUIMock } from "../lib/sessionUIMock";
5 import { runtimeStateStore, type RuntimeProjection } from "../lib/runtimeStateStore";
6 import type { InboxTarget } from "../lib/pendingFollowup";
7 import type { InboxQueueRequest } from "../lib/inboxQueueCommands";
8
9 async function waitFor(check: () => boolean) {
10 for (let attempt = 0; attempt < 100 && !check(); attempt++) {
11 await act(async () => { await new Promise(resolve => setTimeout(resolve, 10)); });
12 }
13 assert.ok(check(), document.body.textContent || "composer did not become ready");
14 }
15
16 for (const phase of ["executing", "finishing"] as const) {
17 const dom = installDom();
18 const sessionId = `canonical-${phase}`;
19 const ref = { hostId: "local", sessionId };
20 const route = `session-id:${sessionId}`;
21 const backend = makeSessionUIMock(async () => {});
22 const target: InboxTarget = { tabId: "tab-a", sessionPath: route, generation: 3, selection: 0, remote: false };
23 let queues = 0, steers = 0, lookups = 0, direct = 0;
24 let lost = false, conflict = false, lastKey = "";
25 const receipt = (key: string) => ({ itemId: key, disposition: "queued_followup", position: 1, paused: false });
26 installBridgeApp({
27 ...backend,
28 BeginSessionComposerSubmission: async (...args: Parameters<typeof backend.BeginSessionComposerSubmission>) => {
29 if (conflict) {
30 conflict = false;
31 const state = await backend.GetSessionComposerState(ref);
32 await backend.SaveSessionComposerState({ ref, expectedRevision: state.revision, contentVersion: 1, contentJson: '{"text":"another window"}' });
33 throw new Error("session_operation:input_conflict:The input changed");
34 }
35 return backend.BeginSessionComposerSubmission(...args);
36 },
37 CaptureInboxTarget: async (tabId: string, path: string) => {
38 assert.equal(tabId, "tab-a"); assert.equal(path, route);
39 return target;
40 },
41 EnqueueInboxFollowupForTarget: async (captured: InboxTarget, _display: string, _submit: string, _invocations: unknown, key: string) => {
42 assert.deepEqual(captured, target); queues++; lastKey = key;
43 if (lost) throw new Error("accepted reply lost");
44 return receipt(key);
45 },
46 InboxQueueForTarget: async (captured: InboxTarget, request: InboxQueueRequest) => {
47 assert.deepEqual(captured, target); assert.equal(request.kind, "enqueue_steer");
48 assert.equal(request.turnId, "active-turn"); steers++;
49 return { outcome: "applied", receipt: { ...receipt(request.idempotencyKey!), disposition: "steer_accepted" } };
50 },
51 LookupInboxFollowupForTarget: async (captured: InboxTarget, key: string) => {
52 assert.deepEqual(captured, target); assert.equal(key, lastKey); lookups++;
53 return receipt(key);
54 },
55 ListTabs: async () => [{ id: "tab-a", session: ref, sessionPath: "", turnId: "active-turn" }],
56 InboxSnapshot: async () => ({ revision: 1, sessionPath: route, mutationsSupported: true, items: [], itemsCount: 0, paused: false }),
57 });
58 const projection: RuntimeProjection = { epoch: sessionId, revision: 1, topics: [], sessions: [{
59 tabId: "tab-a", scope: "global", workspaceRoot: "", topicId: "topic", sessionId, sessionPath: "", sessionGeneration: 3,
60 open: true, remote: false, freshness: "synced", state: { schemaVersion: 1, runtimeEpoch: "runtime", activityRevision: 1, revision: 1,
61 phase, running: true, turnId: "active-turn", turnStatus: "running", turnEventSeq: 1, pendingPrompt: false,
62 cancelRequested: false, cancellable: phase === "executing", backgroundJobs: 0, activity: "streaming" },
63 }] };
64 runtimeStateStore.commit(projection);
65 const view = await renderComposer({ running: false, formalSessionRef: ref, sessionKey: sessionId,
66 sessionIdentity: { session: ref, sessionPath: "", sessionGeneration: 3 }, inboxSessionPath: route,
67 onSend: () => { direct++; } });
68 let insert = 0;
69 const input = () => document.querySelector<HTMLTextAreaElement>("textarea.composer__input:not([aria-hidden=true])")!;
70 async function compose(text: string) {
71 await waitFor(() => !!input() && !input().disabled);
72 await view.rerender({ insertRequest: { id: ++insert, text, mode: "replace" } });
73 }
74 async function click(selector: string) {
75 const button = document.querySelector<HTMLButtonElement>(selector);
76 assert.ok(button && !button.disabled, `missing enabled ${selector}`);
77 await act(async () => { button.click(); });
78 }
79 try {
80 await compose("queue while model runs");
81 await click(".composer__btn--send");
82 await waitFor(() => queues === 1 && input().value === "" && !input().disabled);
83 await compose("guide current turn");
84 // Finishing sessions must retain the instruction as a next-turn message.
85 await click(phase === "executing" ? ".composer__queue-steer" : ".composer__btn--send");
86 await waitFor(() => queues + steers === 2 && input().value === "" && !input().disabled);
87 assert.equal(steers, phase === "executing" ? 1 : 0);
88 assert.equal(direct, 0, "canonical runtime phase must prevent an ordinary direct send");
89
90 await compose("retain conflicted guidance");
91 conflict = true;
92 const sendButton = phase === "executing" ? ".composer__queue-steer" : ".composer__btn--send";
93 await click(sendButton);
94 await waitFor(() => !input().disabled);
95 assert.doesNotMatch(document.body.textContent || "", /核实提交状态|使用已保存版本|保留本窗口版本/);
96 assert.equal(input().value, "retain conflicted guidance");
97 assert.equal(queues + steers, 2, "registration rejection never reached the inbox");
98 await click(sendButton);
99 await waitFor(() => queues + steers === 3 && input().value === "" && !input().disabled);
100
101 await compose("recover a lost queue receipt");
102 lost = true;
103 await click(".composer__btn--send");
104 await waitFor(() => input().disabled && !!document.querySelector(".session-draft-surface__error"));
105 const before = queues;
106 await click(".composer__btn--send");
107 await waitFor(() => lookups === 1 && input().value === "" && !input().disabled);
108 assert.equal(queues, before, "receipt recovery must not post the instruction twice");
109 assert.equal((await backend.GetSessionComposerState(ref)).submissionId, undefined);
110 assert.equal(document.querySelector(".session-draft-surface__error"), null);
111 } finally {
112 await act(async () => view.root.unmount());
113 dom.window.close();
114 }
115 }
116 console.log("canonical composer: running queue, current-turn guidance, finishing and lost receipt recovery passed");
117
117 lines Plain Text