返回 DeepSeek-Reasonix
remoteInteractionScenarios.tsx
根目录 / desktop / frontend / src / test-support / remoteInteractionScenarios.tsx
1 import { act } from "react";
2 import { createRoot, type Root } from "react-dom/client";
3 import type { TabMeta } from "../lib/types";
4 import type { AppBindings } from "../lib/bridge";
5
6 type Assert = (value: boolean, label: string) => void;
7 type Emit = (tabId: string, channel: "event" | "state", payload: unknown) => void;
8
9 export function makeExactRemoteInteractionBindings(options: {
10 tape: string[];
11 failApproval: () => boolean;
12 failAnswer: () => boolean;
13 waitApproval: () => Promise<void>;
14 waitAnswer: () => Promise<void>;
15 waitForm: () => Promise<void>;
16 resolved: (tabId: string, promptId: string, turnId?: string) => void;
17 }): Pick<AppBindings, "ResolveRemoteTabPromptExact" | "SubmitRemoteTabExtensionFormExact"> {
18 return {
19 async ResolveRemoteTabPromptExact(target, answer) {
20 if (target.kind === "plan") options.tape.push(`plan-decision:${target.tabId}:${target.promptId}:${answer.action ?? ""}:${answer.feedback ?? ""}`);
21 else if (target.kind === "ask") {
22 options.tape.push(`answer:${target.tabId}:${target.promptId}:${JSON.stringify(answer.questions ?? [])}`);
23 if (options.failAnswer()) throw new Error("remote answer failed");
24 await options.waitAnswer();
25 } else if (target.kind === "approval" || target.kind === "recovery") {
26 const decision = answer.allow ? answer.persist ? "persist" : answer.session ? "session" : "allow" : "deny";
27 options.tape.push(`approve:${target.tabId}:${target.promptId}:${decision}`);
28 if (options.failApproval()) throw new Error("tunnel write failed");
29 await options.waitApproval();
30 } else options.tape.push(`mcp-answer:${target.tabId}:${target.promptId}:${answer.action ?? ""}`);
31 options.resolved(target.tabId, target.promptId, target.turnId);
32 },
33 async SubmitRemoteTabExtensionFormExact(target, values) {
34 options.tape.push(`extension-form:${target.tabId}:${target.pluginId}:${target.surfaceId}:${JSON.stringify(values)}`);
35 await options.waitForm();
36 },
37 };
38 }
39
40 export async function runRemoteExtensionFormScenarios(options: {
41 emit: Emit;
42 flush: () => Promise<void>;
43 ok: Assert;
44 tape: string[];
45 blockForm: (blocked: boolean) => void;
46 releaseForm: () => void;
47 }): Promise<void> {
48 const { emit, flush, ok, tape } = options;
49 await act(async () => {
50 emit("tab-remote-1", "event", { kind: "extension_surface", extension: {
51 pluginId: "remote-plugin", surfaceId: "setup", generation: 1, formInstanceId: "setup-instance", kind: "form",
52 form: { title: "Remote setup", fields: [{ key: "region", label: "Region", kind: "input", required: true, default: "us-west" }] },
53 } });
54 await flush();
55 });
56 const form = document.querySelector(".extension-form");
57 ok(Boolean(form) && document.body.textContent?.includes("Remote setup") === true, "remote extension form renders on the shared surface");
58 await act(async () => { form?.closest(".prompt-shelf")?.querySelector<HTMLButtonElement>(".decision-confirm-bar__confirm")?.click(); await flush(); });
59 ok(tape.includes('extension-form:tab-remote-1:remote-plugin:setup:{"region":"us-west"}'), "remote extension form submits through the Serve proxy");
60 ok(!document.querySelector(".extension-form"), "remote extension form clears after an accepted submission");
61
62 options.blockForm(true);
63 await act(async () => {
64 emit("tab-remote-1", "event", { kind: "extension_surface", extension: {
65 pluginId: "remote-plugin", surfaceId: "reused", generation: 7, formInstanceId: "old-instance", kind: "form",
66 form: { title: "Old publication", fields: [{ key: "value", label: "Value", kind: "input", default: "old" }] },
67 } });
68 await flush();
69 });
70 await act(async () => { document.querySelector(".extension-form")?.closest(".prompt-shelf")?.querySelector<HTMLButtonElement>(".decision-confirm-bar__confirm")?.click(); await Promise.resolve(); });
71 ok(document.querySelector(".extension-form")?.closest(".prompt-shelf")?.querySelector<HTMLButtonElement>(".decision-confirm-bar__confirm")?.disabled === true, "remote form busy state belongs to the submitting publication");
72 await act(async () => {
73 emit("tab-remote-1", "event", { kind: "extension_surface", extension: {
74 pluginId: "remote-plugin", surfaceId: "reused", generation: 7, formInstanceId: "replacement-instance", kind: "form",
75 form: { title: "Replacement publication", fields: [{ key: "value", label: "Value", kind: "input", default: "new" }] },
76 } });
77 await flush();
78 });
79 ok(document.body.textContent?.includes("Replacement publication") === true && document.querySelector(".extension-form")?.closest(".prompt-shelf")?.querySelector<HTMLButtonElement>(".decision-confirm-bar__confirm")?.disabled === false, "a replacement remote form does not inherit the old instance busy state");
80 options.blockForm(false);
81 await act(async () => { options.releaseForm(); await flush(); });
82 ok(document.body.textContent?.includes("Replacement publication") === true, "late completion from the old form instance cannot clear its replacement");
83 await act(async () => { document.querySelector(".extension-form")?.closest(".prompt-shelf")?.querySelector<HTMLButtonElement>(".decision-confirm-bar__confirm")?.click(); await flush(); });
84 ok(!document.querySelector(".extension-form"), "replacement remote form clears only after its own submission");
85 }
86
87 export async function runLegacyRemoteCapabilityScenario(options: {
88 baseTab: TabMeta;
89 render: (root: Root, tab: TabMeta) => void;
90 emit: Emit;
91 flush: () => Promise<void>;
92 ok: Assert;
93 tape: string[];
94 }): Promise<void> {
95 const container = document.createElement("div");
96 document.body.appendChild(container);
97 const root = createRoot(container);
98 const tab: TabMeta = { ...options.baseTab, id: "tab-legacy-capabilities", sessionId: "legacy-session", sessionGeneration: 1,
99 interactionTargetSupported: false, extensionFormInstanceSupported: false };
100 await act(async () => { options.render(root, tab); await options.flush(); });
101 await act(async () => {
102 options.emit(tab.id, "event", { kind: "approval_request", approval: { id: "legacy-approval", tool: "bash", subject: "must remain fenced" } });
103 options.emit(tab.id, "event", { kind: "extension_surface", extension: {
104 pluginId: "legacy-plugin", surfaceId: "legacy-form", generation: 1, formInstanceId: "legacy-instance", kind: "form",
105 form: { title: "Legacy form", fields: [{ key: "value", label: "Value", kind: "input", default: "blocked" }] },
106 } });
107 await options.flush();
108 });
109 const approval = container.querySelector<HTMLButtonElement>(".remote-surface__approval .prompt-action");
110 const form = container.querySelector(".extension-form")?.closest(".prompt-shelf")?.querySelector<HTMLButtonElement>(".decision-confirm-bar__confirm");
111 options.ok(container.textContent?.includes("must be upgraded") === true, "old Serve shows an upgrade notice only for unsafe interactive cards");
112 options.ok(approval?.closest("fieldset")?.disabled === true && form?.disabled === true, "old Serve keeps exact prompt and form actions disabled");
113 const before = options.tape.filter((entry) => entry.includes(tab.id) && (entry.startsWith("approve:") || entry.startsWith("extension-form:"))).length;
114 await act(async () => { approval?.click(); form?.click(); await options.flush(); });
115 options.ok(options.tape.filter((entry) => entry.includes(tab.id) && (entry.startsWith("approve:") || entry.startsWith("extension-form:"))).length === before, "old Serve card clicks never fall back to legacy mutation endpoints");
116 await act(async () => root.unmount());
117 container.remove();
118 }
119
119 lines Plain Text