返回 DeepSeek-Reasonix
composer-profile-session-navigation.test.tsx
根目录 / desktop / frontend / src / __tests__ / composer-profile-session-navigation.test.tsx
1 import assert from "node:assert/strict";
2 import React, { act, useRef, useState } from "react";
3 import { createRoot } from "react-dom/client";
4 import { JSDOM } from "jsdom";
5 import { useTabProjectionLifecycle } from "../app-runtime/useTabProjectionLifecycle";
6 import { useComposerProfileProjection } from "../app-runtime/useComposerProfileProjection";
7 import { projectControllerProfiles } from "../app-runtime/controllerProfileOwner";
8 import { useSessionOperations } from "../app-runtime/useSessionOperations";
9 import { sessionIdentityKey } from "../app-runtime/sessionTarget";
10 import { useControllerProfileCommands } from "../lib/useControllerProfileCommands";
11 import type { ComposerProfile, UserPlanModeIntents } from "../lib/composerProfile";
12 import type { Meta, TabMeta, ToolApprovalMode } from "../lib/types";
13 import type { RemoteSessionApi } from "../lib/useRemoteSession";
14
15 const dom = new JSDOM("<div id='root'></div>");
16 Object.assign(globalThis, { window: dom.window, document: dom.window.document, IS_REACT_ACT_ENVIRONMENT: true });
17 const root = createRoot(document.getElementById("root")!);
18
19 const pushes: string[] = [];
20 const ports = {
21 model: async () => true,
22 profile: async (_tab: string, _collaboration: string, approval: string) => { pushes.push(approval); return true; },
23 };
24 const remoteSession = { composerProfile: undefined } as unknown as RemoteSessionApi;
25 let patchActive!: ReturnType<typeof useComposerProfileProjection>["patchActiveComposerProfile"];
26
27 function tabFor(sessionId: string, preset: ToolApprovalMode): TabMeta {
28 return { id: "surface", active: true, scope: "project", workspaceRoot: "/w", topicId: "topic-" + sessionId,
29 sessionId, session: { hostId: "local", sessionId }, toolApprovalMode: preset } as unknown as TabMeta;
30 }
31 function metaFor(sessionId: string, preset: ToolApprovalMode): Meta {
32 return { ready: true, sessionId, session: { hostId: "local", sessionId }, toolApprovalMode: preset } as unknown as Meta;
33 }
34
35 // Mirrors the App composition: backend tab and meta projections hydrate the
36 // per-tab profile table, and the controller profile owner pushes it back.
37 function Surface({ tab, meta }: { tab: TabMeta; meta: Meta }) {
38 const [profiles, setProfiles] = useState<Record<string, ComposerProfile>>({});
39 const [, setOrder] = useState<string[]>([]);
40 const planIntentsRef = useRef<UserPlanModeIntents>({});
41 const tabs = [tab];
42 useTabProjectionLifecycle({ tabs, activeTabId: tab.id, activeMeta: tab, meta, planIntentsRef, setOrder, setProfiles });
43 const projection = useComposerProfileProjection({ activeTabId: tab.id, activeTab: tab, meta, profilesByTab: profiles,
44 setProfilesByTab: setProfiles, tabMetas: tabs, remote: false, remoteSession, planIntentsRef });
45 patchActive = projection.patchActiveComposerProfile;
46 const target = { tabId: tab.id, sessionKey: sessionIdentityKey({ tabId: tab.id, session: tab.session, sessionId: tab.sessionId,
47 scope: tab.scope, workspaceRoot: tab.workspaceRoot, topicId: tab.topicId }) };
48 const controllerProfiles = projectControllerProfiles(tabs, profiles, { target, profile: projection.composerProfile, remote: false });
49 const operations = useSessionOperations({ visible: target, resources: controllerProfiles.map(value => value.target) });
50 useControllerProfileCommands({ target, profiles: controllerProfiles, ready: true, remote: false, operations, ports,
51 remoteModel: async () => {}, report: error => { throw error; } });
52 return null;
53 }
54
55 const paint = (tab: TabMeta, meta: Meta) => act(async () => root.render(<Surface tab={tab} meta={meta} />));
56
57 try {
58 for (const tabFirst of [true, false]) {
59 const order = tabFirst ? "tab list before meta" : "meta before tab list";
60 const navigate = async (sessionId: string, preset: ToolApprovalMode, from: { tab: TabMeta; meta: Meta }) => {
61 if (tabFirst) await paint(tabFor(sessionId, preset), from.meta);
62 else await paint(from.tab, metaFor(sessionId, preset));
63 await paint(tabFor(sessionId, preset), metaFor(sessionId, preset));
64 };
65
66 await paint(tabFor("A", "danger-full-access"), metaFor("A", "danger-full-access"));
67 pushes.length = 0;
68 await navigate("B", "workspace-write", { tab: tabFor("A", "danger-full-access"), meta: metaFor("A", "danger-full-access") });
69 assert.ok(!pushes.includes("danger-full-access"), `${order}: A's full access was pushed onto B: ${pushes.join(",")}`);
70
71 await act(async () => patchActive({ toolApprovalMode: "read-only" }, ["toolApprovalMode"]));
72 await paint(tabFor("B", "read-only"), metaFor("B", "read-only"));
73 pushes.length = 0;
74 await navigate("A", "danger-full-access", { tab: tabFor("B", "read-only"), meta: metaFor("B", "read-only") });
75 assert.deepEqual(pushes.filter(approval => approval !== "danger-full-access"), [],
76 `${order}: a push after returning to A overwrote its restored preset: ${pushes.join(",")}`);
77
78 await act(async () => root.render(null));
79 }
80 console.log("composer profile session navigation: no push carries another session's preset");
81 } finally {
82 await act(async () => root.unmount());
83 dom.window.close();
84 }
85
85 lines Plain Text