@stigmer/react 3.12.7 → 3.12.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/composer/ComposerToolbar.d.ts +1 -19
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +2 -7
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +0 -10
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +2 -2
- package/composer/SessionComposer.js.map +1 -1
- package/composer/index.d.ts +0 -1
- package/composer/index.d.ts.map +1 -1
- package/composer/index.js.map +1 -1
- package/execution/ResultView.d.ts.map +1 -1
- package/execution/ResultView.js +6 -0
- package/execution/ResultView.js.map +1 -1
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +13 -2
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/tool-categories.d.ts +1 -1
- package/execution/tool-categories.d.ts.map +1 -1
- package/execution/tool-categories.js +6 -0
- package/execution/tool-categories.js.map +1 -1
- package/identity-account/AccountPreferencesPanel.d.ts +2 -1
- package/identity-account/AccountPreferencesPanel.d.ts.map +1 -1
- package/identity-account/AccountPreferencesPanel.js +41 -3
- package/identity-account/AccountPreferencesPanel.js.map +1 -1
- package/identity-account/useAccountExecutionDefaults.d.ts +7 -0
- package/identity-account/useAccountExecutionDefaults.d.ts.map +1 -1
- package/identity-account/useAccountExecutionDefaults.js +5 -2
- package/identity-account/useAccountExecutionDefaults.js.map +1 -1
- package/index.d.ts +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/internal/MemoryEnabledRow.d.ts +37 -0
- package/internal/MemoryEnabledRow.d.ts.map +1 -0
- package/internal/MemoryEnabledRow.js +25 -0
- package/internal/MemoryEnabledRow.js.map +1 -0
- package/memory/MemoryProposalCard.d.ts +35 -0
- package/memory/MemoryProposalCard.d.ts.map +1 -0
- package/memory/MemoryProposalCard.js +73 -0
- package/memory/MemoryProposalCard.js.map +1 -0
- package/memory/index.d.ts +2 -0
- package/memory/index.d.ts.map +1 -1
- package/memory/index.js +2 -0
- package/memory/index.js.map +1 -1
- package/memory/useMemory.d.ts +39 -0
- package/memory/useMemory.d.ts.map +1 -0
- package/memory/useMemory.js +43 -0
- package/memory/useMemory.js.map +1 -0
- package/organization/OrgPreferencesPanel.d.ts.map +1 -1
- package/organization/OrgPreferencesPanel.js +36 -4
- package/organization/OrgPreferencesPanel.js.map +1 -1
- package/package.json +4 -4
- package/session/AutoApproveIndicator.d.ts +18 -0
- package/session/AutoApproveIndicator.d.ts.map +1 -0
- package/session/AutoApproveIndicator.js +23 -0
- package/session/AutoApproveIndicator.js.map +1 -0
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +10 -9
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +21 -9
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +31 -21
- package/session/SessionViewer.js.map +1 -1
- package/session/facets/SetupTab.d.ts +43 -6
- package/session/facets/SetupTab.d.ts.map +1 -1
- package/session/facets/SetupTab.js +32 -36
- package/session/facets/SetupTab.js.map +1 -1
- package/session/facets/UsageTab.d.ts +2 -1
- package/session/facets/UsageTab.d.ts.map +1 -1
- package/session/facets/UsageTab.js +11 -5
- package/session/facets/UsageTab.js.map +1 -1
- package/session/facets/index.d.ts +1 -1
- package/session/facets/index.d.ts.map +1 -1
- package/session/facets/primitives.d.ts +58 -0
- package/session/facets/primitives.d.ts.map +1 -0
- package/session/facets/primitives.js +59 -0
- package/session/facets/primitives.js.map +1 -0
- package/session/index.d.ts +1 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +9 -4
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +20 -9
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +28 -10
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +16 -10
- package/session/useSessionPageFlow.js.map +1 -1
- package/src/attachment/__tests__/prepare-image.test.ts +12 -6
- package/src/composer/ComposerToolbar.tsx +0 -57
- package/src/composer/SessionComposer.tsx +1 -13
- package/src/composer/index.ts +0 -1
- package/src/execution/ResultView.tsx +6 -0
- package/src/execution/ToolCallItem.tsx +18 -1
- package/src/execution/tool-categories.ts +7 -0
- package/src/identity-account/AccountPreferencesPanel.tsx +89 -2
- package/src/identity-account/__tests__/AccountPreferencesPanel.test.tsx +112 -0
- package/src/identity-account/__tests__/useAccountExecutionDefaults.test.tsx +34 -1
- package/src/identity-account/useAccountExecutionDefaults.ts +12 -2
- package/src/index.ts +5 -1
- package/src/internal/MemoryEnabledRow.tsx +92 -0
- package/src/memory/MemoryProposalCard.tsx +181 -0
- package/src/memory/__tests__/MemoryProposalCard.test.tsx +236 -0
- package/src/memory/__tests__/memoryHooks.test.tsx +63 -0
- package/src/memory/index.ts +5 -0
- package/src/memory/useMemory.ts +80 -0
- package/src/organization/OrgPreferencesPanel.tsx +52 -3
- package/src/organization/__tests__/OrgPreferencesPanel.test.tsx +65 -0
- package/src/session/AutoApproveIndicator.tsx +45 -0
- package/src/session/NewSessionViewer.tsx +18 -15
- package/src/session/SessionViewer.tsx +70 -32
- package/src/session/__tests__/NewSessionViewer.test.tsx +32 -0
- package/src/session/__tests__/SessionViewer-autoApprove.test.tsx +244 -0
- package/src/session/__tests__/SessionViewer-transcriptExport.test.tsx +60 -22
- package/src/session/__tests__/useNewSessionFlow.test.tsx +56 -0
- package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +87 -0
- package/src/session/facets/SetupTab.tsx +193 -157
- package/src/session/facets/UsageTab.tsx +38 -38
- package/src/session/facets/index.ts +1 -0
- package/src/session/facets/primitives.tsx +169 -0
- package/src/session/index.ts +1 -0
- package/src/session/useNewSessionFlow.ts +30 -15
- package/src/session/useSessionPageFlow.ts +44 -20
- package/src/switch/Switch.tsx +4 -0
- package/src/workflow/__tests__/useWorkflowCanvas.dirty.test.tsx +7 -2
- package/src/workflow/__tests__/useWorkflowCanvas.drag.test.tsx +7 -2
- package/src/workspace/WorkspaceSurface.tsx +26 -3
- package/styles.css +1 -1
- package/switch/Switch.d.ts +3 -1
- package/switch/Switch.d.ts.map +1 -1
- package/switch/Switch.js +2 -2
- package/switch/Switch.js.map +1 -1
- package/workspace/WorkspaceSurface.js +11 -2
- package/workspace/WorkspaceSurface.js.map +1 -1
- package/src/composer/__tests__/ComposerToolbar-autoApprove.test.tsx +0 -80
- package/src/session/__tests__/SessionViewer-autoApproveToggle.test.tsx +0 -178
|
@@ -33,6 +33,7 @@ const emptyWorkspace = {
|
|
|
33
33
|
clear: vi.fn(),
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
const setAutoApproveAll = vi.fn();
|
|
36
37
|
const stubEmptyFlow = {
|
|
37
38
|
harness: "native" as const,
|
|
38
39
|
setHarness: vi.fn(),
|
|
@@ -48,6 +49,8 @@ const stubEmptyFlow = {
|
|
|
48
49
|
setSkillRefs: vi.fn(),
|
|
49
50
|
workspace: emptyWorkspace,
|
|
50
51
|
sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
|
|
52
|
+
autoApproveAll: false,
|
|
53
|
+
setAutoApproveAll,
|
|
51
54
|
isSubmitting: false,
|
|
52
55
|
submitError: null,
|
|
53
56
|
submit: vi.fn(),
|
|
@@ -125,6 +128,35 @@ describe("NewSessionViewer — persistent panel chip", () => {
|
|
|
125
128
|
});
|
|
126
129
|
});
|
|
127
130
|
|
|
131
|
+
describe("NewSessionViewer — auto-approve placement (#816 rework)", () => {
|
|
132
|
+
const INDICATOR_TEXT = "Auto-approving tool calls for this session";
|
|
133
|
+
|
|
134
|
+
it("shows no indicator while auto-approve is off — the launcher stays clean", () => {
|
|
135
|
+
render(<NewSessionViewer org="acme" onSessionCreated={vi.fn()} />);
|
|
136
|
+
expect(screen.queryByText(INDICATOR_TEXT)).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("shows the armed-only indicator when a seed pre-arms the session, with Turn off", () => {
|
|
140
|
+
mockFlow = { ...stubEmptyFlow, autoApproveAll: true };
|
|
141
|
+
render(<NewSessionViewer org="acme" onSessionCreated={vi.fn()} />);
|
|
142
|
+
|
|
143
|
+
expect(screen.getByText(INDICATOR_TEXT)).toBeTruthy();
|
|
144
|
+
fireEvent.click(screen.getByRole("button", { name: "Turn off" }));
|
|
145
|
+
expect(setAutoApproveAll).toHaveBeenCalledExactlyOnceWith(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("offers the auto-approve switch in the Config facet, wired to the flow", () => {
|
|
149
|
+
render(<NewSessionViewer org="acme" onSessionCreated={vi.fn()} />);
|
|
150
|
+
|
|
151
|
+
fireEvent.click(screen.getByRole("button", { name: "Show panel" }));
|
|
152
|
+
const toggle = screen.getByRole("switch", { name: "Auto-approve" });
|
|
153
|
+
expect(toggle.getAttribute("aria-checked")).toBe("false");
|
|
154
|
+
|
|
155
|
+
fireEvent.click(toggle);
|
|
156
|
+
expect(setAutoApproveAll).toHaveBeenCalledExactlyOnceWith(true);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
128
160
|
describe("NewSessionViewer — composer stays centered", () => {
|
|
129
161
|
// The composer's wrapper is the probe's parent (wrapper > h1 + composer +
|
|
130
162
|
// footer). `my-auto` is unconditional safe-centering; the old `my-6`
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Wiring-contract tests for auto-approve placement (the #816 rework):
|
|
6
|
+
//
|
|
7
|
+
// - The composer carries NO auto-approve control — the session-level switch
|
|
8
|
+
// lives in the Config facet (SetupTab receives the `autoApprove` group for
|
|
9
|
+
// every audience that may submit approvals, never observers; guests never
|
|
10
|
+
// render the panel at all).
|
|
11
|
+
// - The armed-only AutoApproveIndicator above the composer is the
|
|
12
|
+
// always-visible disclosure: present exactly while the state is armed,
|
|
13
|
+
// with a one-click "Turn off"; never rendered for observers.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
type CapturedProps = Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
const composerProps: CapturedProps[] = [];
|
|
19
|
+
vi.mock("../../composer", async (importOriginal) => {
|
|
20
|
+
const actual = await importOriginal<typeof import("../../composer")>();
|
|
21
|
+
return {
|
|
22
|
+
...actual,
|
|
23
|
+
SessionComposer: (props: CapturedProps) => {
|
|
24
|
+
composerProps.push(props);
|
|
25
|
+
return <div data-testid="composer-probe" />;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
vi.mock("../../execution/MessageThread", () => ({
|
|
31
|
+
MessageThread: () => <div data-testid="thread-probe" />,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("../../execution/FileReviewDock", () => ({
|
|
35
|
+
FileReviewDock: () => <div data-testid="file-review-dock-probe" />,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// SetupTab renders for real: the facet switch is asserted through its
|
|
39
|
+
// actual DOM (a switch labeled "Auto-approve" in the Run Config section),
|
|
40
|
+
// reached by opening the panel and selecting the Config rail view.
|
|
41
|
+
|
|
42
|
+
const stubWorkspace = {
|
|
43
|
+
entries: [],
|
|
44
|
+
hasEntries: false,
|
|
45
|
+
toInput: vi.fn().mockReturnValue([]),
|
|
46
|
+
addGitRepo: vi.fn(),
|
|
47
|
+
addLocalPath: vi.fn(),
|
|
48
|
+
removeEntry: vi.fn(),
|
|
49
|
+
clear: vi.fn(),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const stubConv = {
|
|
53
|
+
session: { spec: {} } as Record<string, unknown>,
|
|
54
|
+
isLoading: false,
|
|
55
|
+
loadError: null,
|
|
56
|
+
completedExecutions: [],
|
|
57
|
+
activeStreamExecution: null,
|
|
58
|
+
activePhase: null,
|
|
59
|
+
isStreaming: false,
|
|
60
|
+
isConnecting: false,
|
|
61
|
+
sendFollowUp: vi.fn(),
|
|
62
|
+
canSendFollowUp: true,
|
|
63
|
+
isSending: false,
|
|
64
|
+
sendError: null,
|
|
65
|
+
clearSendError: vi.fn(),
|
|
66
|
+
pendingUserMessage: null,
|
|
67
|
+
workspaceEntries: [],
|
|
68
|
+
mcpServerUsages: [],
|
|
69
|
+
skillRefs: [],
|
|
70
|
+
pendingApprovals: [],
|
|
71
|
+
submitApproval: vi.fn(),
|
|
72
|
+
submittingApprovalIds: new Set<string>(),
|
|
73
|
+
fileChangeSets: [],
|
|
74
|
+
submitFileDecision: vi.fn(),
|
|
75
|
+
submittingFileDecisionKeys: new Set<string>(),
|
|
76
|
+
fileDecisionErrors: new Map<string, Error>(),
|
|
77
|
+
streamError: null,
|
|
78
|
+
reconnectStream: vi.fn(),
|
|
79
|
+
approvalError: null,
|
|
80
|
+
retryLastSend: vi.fn(),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const setAutoApproveAll = vi.fn();
|
|
84
|
+
const stubSessionPageFlow = {
|
|
85
|
+
conv: stubConv,
|
|
86
|
+
harness: "native" as const,
|
|
87
|
+
executionTarget: undefined,
|
|
88
|
+
model: [undefined, vi.fn()] as const,
|
|
89
|
+
interactionMode: ["agent" as const, vi.fn()] as const,
|
|
90
|
+
agentRef: { org: "acme", slug: "support-bot" },
|
|
91
|
+
setAgentRef: vi.fn(),
|
|
92
|
+
resolution: null,
|
|
93
|
+
setResolution: vi.fn(),
|
|
94
|
+
isDefaultAgent: false,
|
|
95
|
+
mcpServerUsages: [],
|
|
96
|
+
setMcpServerUsages: vi.fn(),
|
|
97
|
+
skillRefs: [],
|
|
98
|
+
setSkillRefs: vi.fn(),
|
|
99
|
+
workspace: stubWorkspace,
|
|
100
|
+
sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
|
|
101
|
+
autoApproveAll: false,
|
|
102
|
+
setAutoApproveAll,
|
|
103
|
+
submitApproval: vi.fn(),
|
|
104
|
+
handleSubmit: vi.fn(),
|
|
105
|
+
submitError: null as Error | null,
|
|
106
|
+
displayExecution: null,
|
|
107
|
+
allExecutions: [],
|
|
108
|
+
sandboxWorkspaceRoot: undefined,
|
|
109
|
+
};
|
|
110
|
+
vi.mock("../useSessionPageFlow", () => ({
|
|
111
|
+
useSessionPageFlow: () => stubSessionPageFlow,
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
vi.mock("../../hooks", () => ({
|
|
115
|
+
useStigmer: () => ({
|
|
116
|
+
agentExecution: {
|
|
117
|
+
uploadAttachment: vi.fn(),
|
|
118
|
+
getArtifactContent: vi.fn(),
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
import { SessionViewer } from "../SessionViewer";
|
|
124
|
+
|
|
125
|
+
const INDICATOR_TEXT = "Auto-approving tool calls for this session";
|
|
126
|
+
|
|
127
|
+
function lastComposerProps(): CapturedProps {
|
|
128
|
+
expect(composerProps.length).toBeGreaterThan(0);
|
|
129
|
+
return composerProps.at(-1)!;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Selects the panel's Config facet (the rail's radio). */
|
|
133
|
+
function openConfigFacet() {
|
|
134
|
+
fireEvent.click(screen.getByRole("radio", { name: "Config" }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function facetSwitch(): HTMLElement | null {
|
|
138
|
+
return screen.queryByRole("switch", { name: "Auto-approve" });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
composerProps.length = 0;
|
|
143
|
+
stubConv.session = { spec: {} };
|
|
144
|
+
stubSessionPageFlow.autoApproveAll = false;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterEach(() => {
|
|
148
|
+
cleanup();
|
|
149
|
+
vi.clearAllMocks();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("SessionViewer — composer stays clean (#816 rework)", () => {
|
|
153
|
+
it("hands the composer no auto-approve control at all", () => {
|
|
154
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
155
|
+
|
|
156
|
+
expect("autoApprove" in lastComposerProps()).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("SessionViewer — Config facet auto-approve switch (#816 rework)", () => {
|
|
161
|
+
it("renders the switch reflecting the flow state, and flipping calls the flow (integrator)", () => {
|
|
162
|
+
render(<SessionViewer sessionId="ses_1" org="acme" defaultPanelOpen />);
|
|
163
|
+
openConfigFacet();
|
|
164
|
+
|
|
165
|
+
const toggle = facetSwitch();
|
|
166
|
+
expect(toggle).not.toBeNull();
|
|
167
|
+
expect(toggle!.getAttribute("aria-checked")).toBe("false");
|
|
168
|
+
fireEvent.click(toggle!);
|
|
169
|
+
expect(setAutoApproveAll).toHaveBeenCalledExactlyOnceWith(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("mirrors an armed flow state into the switch", () => {
|
|
173
|
+
stubSessionPageFlow.autoApproveAll = true;
|
|
174
|
+
render(<SessionViewer sessionId="ses_1" org="acme" defaultPanelOpen />);
|
|
175
|
+
openConfigFacet();
|
|
176
|
+
|
|
177
|
+
expect(facetSwitch()!.getAttribute("aria-checked")).toBe("true");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("endUser gets the switch — the walk-away persona is an end user", () => {
|
|
181
|
+
render(
|
|
182
|
+
<SessionViewer
|
|
183
|
+
sessionId="ses_1"
|
|
184
|
+
org="acme"
|
|
185
|
+
audience="endUser"
|
|
186
|
+
defaultPanelOpen
|
|
187
|
+
/>,
|
|
188
|
+
);
|
|
189
|
+
openConfigFacet();
|
|
190
|
+
|
|
191
|
+
expect(facetSwitch()).not.toBeNull();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("observers never get the switch — the approval-submission withhold", () => {
|
|
195
|
+
render(
|
|
196
|
+
<SessionViewer
|
|
197
|
+
sessionId="ses_1"
|
|
198
|
+
org="acme"
|
|
199
|
+
audience="observer"
|
|
200
|
+
defaultPanelOpen
|
|
201
|
+
/>,
|
|
202
|
+
);
|
|
203
|
+
openConfigFacet();
|
|
204
|
+
|
|
205
|
+
expect(facetSwitch()).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("channel-origin sessions never get the switch (observer by construction)", () => {
|
|
209
|
+
stubConv.session = {
|
|
210
|
+
spec: {},
|
|
211
|
+
metadata: { labels: { "stigmer.ai/channel-id": "chn_1" } },
|
|
212
|
+
};
|
|
213
|
+
render(<SessionViewer sessionId="ses_1" org="acme" defaultPanelOpen />);
|
|
214
|
+
openConfigFacet();
|
|
215
|
+
|
|
216
|
+
expect(facetSwitch()).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("SessionViewer — armed-only indicator (#816 rework)", () => {
|
|
221
|
+
it("absent while auto-approve is off — the composer stays clean", () => {
|
|
222
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
223
|
+
|
|
224
|
+
expect(screen.queryByText(INDICATOR_TEXT)).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("renders while armed, and Turn off flips the flow state", () => {
|
|
228
|
+
stubSessionPageFlow.autoApproveAll = true;
|
|
229
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
230
|
+
|
|
231
|
+
expect(screen.getByText(INDICATOR_TEXT)).toBeTruthy();
|
|
232
|
+
fireEvent.click(screen.getByRole("button", { name: "Turn off" }));
|
|
233
|
+
expect(setAutoApproveAll).toHaveBeenCalledExactlyOnceWith(false);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("never renders for observers — they cannot arm or disarm anything", () => {
|
|
237
|
+
stubSessionPageFlow.autoApproveAll = true;
|
|
238
|
+
render(
|
|
239
|
+
<SessionViewer sessionId="ses_1" org="acme" audience="observer" />,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect(screen.queryByText(INDICATOR_TEXT)).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
// SessionViewer's built-in transcript export (stigmer/stigmer#814
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// SessionViewer's built-in transcript export (stigmer/stigmer#814, placement
|
|
2
|
+
// reworked with #816): export lives in the Config facet's Transcript section
|
|
3
|
+
// for viewers with the session panel, and the header TranscriptExportMenu
|
|
4
|
+
// remains ONLY as the panel-less fallback (guests, panel="none") — so
|
|
5
|
+
// wherever a conversation is viewed, its transcript stays reachable, without
|
|
6
|
+
// a floating header button on surfaces that have a proper home for it.
|
|
6
7
|
|
|
7
8
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
8
|
-
import { render, screen, cleanup } from "@testing-library/react";
|
|
9
|
+
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
|
9
10
|
|
|
10
11
|
vi.mock("../../composer", async (importOriginal) => {
|
|
11
12
|
const actual = await importOriginal<typeof import("../../composer")>();
|
|
@@ -20,9 +21,10 @@ vi.mock("../../execution/MessageThread", () => ({
|
|
|
20
21
|
vi.mock("../../execution/FileReviewDock", () => ({
|
|
21
22
|
FileReviewDock: () => <div data-testid="file-review-dock-probe" />,
|
|
22
23
|
}));
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
|
|
25
|
+
// SetupTab renders for real: the facet's Transcript section is asserted
|
|
26
|
+
// through its actual DOM (the "Copy transcript" action row), reached by
|
|
27
|
+
// opening the panel and selecting the Config rail view.
|
|
26
28
|
|
|
27
29
|
const stubWorkspace = {
|
|
28
30
|
entries: [],
|
|
@@ -107,45 +109,81 @@ vi.mock("../../hooks", () => ({
|
|
|
107
109
|
|
|
108
110
|
import { SessionViewer } from "../SessionViewer";
|
|
109
111
|
|
|
110
|
-
afterEach(
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
cleanup();
|
|
114
|
+
vi.clearAllMocks();
|
|
115
|
+
});
|
|
111
116
|
|
|
112
|
-
function
|
|
117
|
+
function headerExportTrigger(): HTMLElement | null {
|
|
113
118
|
return screen.queryByRole("button", { name: "Export transcript" });
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
/** Selects the panel's Config facet (the rail's radio). */
|
|
122
|
+
function openConfigFacet() {
|
|
123
|
+
fireEvent.click(screen.getByRole("radio", { name: "Config" }));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function facetCopyAction(): HTMLElement | null {
|
|
127
|
+
return screen.queryByRole("button", { name: "Copy transcript" });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
describe("SessionViewer transcript export placement", () => {
|
|
131
|
+
it("panel-enabled viewers export from the Config facet, not the header", () => {
|
|
132
|
+
render(<SessionViewer sessionId="ses_1" org="acme" defaultPanelOpen />);
|
|
133
|
+
expect(headerExportTrigger()).toBeNull();
|
|
134
|
+
openConfigFacet();
|
|
135
|
+
expect(facetCopyAction()).not.toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("observers keep facet export — a bulk copy of what they already see", () => {
|
|
139
|
+
render(
|
|
140
|
+
<SessionViewer
|
|
141
|
+
sessionId="ses_1"
|
|
142
|
+
org="acme"
|
|
143
|
+
audience="observer"
|
|
144
|
+
defaultPanelOpen
|
|
145
|
+
/>,
|
|
146
|
+
);
|
|
147
|
+
openConfigFacet();
|
|
148
|
+
expect(facetCopyAction()).not.toBeNull();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("panel=\"none\" hosts fall back to the header menu", () => {
|
|
152
|
+
render(<SessionViewer sessionId="ses_1" org="acme" panel="none" />);
|
|
153
|
+
expect(headerExportTrigger()).not.toBeNull();
|
|
120
154
|
});
|
|
121
155
|
|
|
122
|
-
it("
|
|
123
|
-
render(<SessionViewer sessionId="ses_1" org="acme" audience="
|
|
124
|
-
expect(
|
|
156
|
+
it("guests fall back to the header menu (no panel to reach the facet)", () => {
|
|
157
|
+
render(<SessionViewer sessionId="ses_1" org="acme" audience="guest" />);
|
|
158
|
+
expect(headerExportTrigger()).not.toBeNull();
|
|
125
159
|
});
|
|
126
160
|
|
|
127
|
-
it("composes with host headerActions
|
|
161
|
+
it("the header fallback composes with host headerActions", () => {
|
|
128
162
|
render(
|
|
129
163
|
<SessionViewer
|
|
130
164
|
sessionId="ses_1"
|
|
131
165
|
org="acme"
|
|
166
|
+
panel="none"
|
|
132
167
|
headerActions={<button>Share</button>}
|
|
133
168
|
/>,
|
|
134
169
|
);
|
|
135
170
|
expect(screen.getByRole("button", { name: "Share" })).toBeTruthy();
|
|
136
|
-
expect(
|
|
171
|
+
expect(headerExportTrigger()).not.toBeNull();
|
|
137
172
|
});
|
|
138
173
|
|
|
139
|
-
it("
|
|
174
|
+
it("explicit opt-out removes BOTH surfaces, leaving host actions intact", () => {
|
|
140
175
|
render(
|
|
141
176
|
<SessionViewer
|
|
142
177
|
sessionId="ses_1"
|
|
143
178
|
org="acme"
|
|
144
179
|
transcriptExport={false}
|
|
180
|
+
defaultPanelOpen
|
|
145
181
|
headerActions={<button>Share</button>}
|
|
146
182
|
/>,
|
|
147
183
|
);
|
|
148
|
-
expect(
|
|
184
|
+
expect(headerExportTrigger()).toBeNull();
|
|
185
|
+
openConfigFacet();
|
|
186
|
+
expect(facetCopyAction()).toBeNull();
|
|
149
187
|
expect(screen.getByRole("button", { name: "Share" })).toBeTruthy();
|
|
150
188
|
});
|
|
151
189
|
});
|
|
@@ -464,6 +464,62 @@ describe("useNewSessionFlow", () => {
|
|
|
464
464
|
expect(execInput.autoApproveAll).toBeUndefined();
|
|
465
465
|
});
|
|
466
466
|
|
|
467
|
+
it("arms from the account's default_auto_approve preference", async () => {
|
|
468
|
+
const opts = {
|
|
469
|
+
...defaultOptions(),
|
|
470
|
+
accountDefaults: { autoApprove: true },
|
|
471
|
+
};
|
|
472
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
473
|
+
|
|
474
|
+
expect(result.current.autoApproveAll).toBe(true);
|
|
475
|
+
|
|
476
|
+
await act(async () => {
|
|
477
|
+
await result.current.submit("Hello");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const execInput = mockCreateExecution.mock.calls[0][0];
|
|
481
|
+
expect(execInput.autoApproveAll).toBe(true);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("a LATE-arriving account preference still arms (derived, not initialized)", () => {
|
|
485
|
+
// whoAmI resolves after mount — the seed must be part of the
|
|
486
|
+
// derivation so it cannot be missed by a useState initializer.
|
|
487
|
+
const initialProps: { accountDefaults?: { autoApprove?: boolean } } = {};
|
|
488
|
+
const { result, rerender } = renderHook(
|
|
489
|
+
({ accountDefaults }: typeof initialProps) =>
|
|
490
|
+
useNewSessionFlow({ ...defaultOptions(), accountDefaults }),
|
|
491
|
+
{ wrapper: createWrapper(), initialProps },
|
|
492
|
+
);
|
|
493
|
+
expect(result.current.autoApproveAll).toBe(false);
|
|
494
|
+
|
|
495
|
+
rerender({ accountDefaults: { autoApprove: true } });
|
|
496
|
+
expect(result.current.autoApproveAll).toBe(true);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("a late arrival never overrides the user's explicit OFF", () => {
|
|
500
|
+
const initialProps: { accountDefaults?: { autoApprove?: boolean } } = {};
|
|
501
|
+
const { result, rerender } = renderHook(
|
|
502
|
+
({ accountDefaults }: typeof initialProps) =>
|
|
503
|
+
useNewSessionFlow({ ...defaultOptions(), accountDefaults }),
|
|
504
|
+
{ wrapper: createWrapper(), initialProps },
|
|
505
|
+
);
|
|
506
|
+
act(() => result.current.setAutoApproveAll(false));
|
|
507
|
+
|
|
508
|
+
rerender({ accountDefaults: { autoApprove: true } });
|
|
509
|
+
expect(result.current.autoApproveAll).toBe(false);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("guests never inherit the account auto-approve preference", () => {
|
|
513
|
+
const opts = {
|
|
514
|
+
...defaultOptions(),
|
|
515
|
+
audience: "guest" as const,
|
|
516
|
+
accountDefaults: { autoApprove: true },
|
|
517
|
+
};
|
|
518
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
519
|
+
|
|
520
|
+
expect(result.current.autoApproveAll).toBe(false);
|
|
521
|
+
});
|
|
522
|
+
|
|
467
523
|
it("passes cursor harness in the sessionSpec after switching", async () => {
|
|
468
524
|
const opts = defaultOptions();
|
|
469
525
|
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
@@ -231,6 +231,93 @@ describe("useSessionPageFlow — host-set approval default (#302)", () => {
|
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
+
describe("useSessionPageFlow — account preference seed (default_auto_approve)", () => {
|
|
235
|
+
beforeEach(() => {
|
|
236
|
+
mockSubmitApproval.mockResolvedValue(undefined);
|
|
237
|
+
});
|
|
238
|
+
afterEach(() => {
|
|
239
|
+
vi.clearAllMocks();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("arms autoApproveAll from the account preference", () => {
|
|
243
|
+
const { result } = renderHook(() =>
|
|
244
|
+
useSessionPageFlow({ ...OPTS, accountDefaults: { autoApprove: true } }),
|
|
245
|
+
);
|
|
246
|
+
expect(result.current.autoApproveAll).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("a LATE-arriving preference still applies (whoAmI resolves async)", () => {
|
|
250
|
+
// The preference is part of the derivation, not a mount-time
|
|
251
|
+
// initializer — a value arriving after mount must not be missed.
|
|
252
|
+
const initialProps: { accountDefaults?: { autoApprove?: boolean } } = {};
|
|
253
|
+
const { result, rerender } = renderHook(
|
|
254
|
+
({ accountDefaults }: typeof initialProps) =>
|
|
255
|
+
useSessionPageFlow({ ...OPTS, accountDefaults }),
|
|
256
|
+
{ initialProps },
|
|
257
|
+
);
|
|
258
|
+
expect(result.current.autoApproveAll).toBe(false);
|
|
259
|
+
|
|
260
|
+
rerender({ accountDefaults: { autoApprove: true } });
|
|
261
|
+
expect(result.current.autoApproveAll).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("a late arrival never overrides an explicit in-session OFF", () => {
|
|
265
|
+
const initialProps: { accountDefaults?: { autoApprove?: boolean } } = {};
|
|
266
|
+
const { result, rerender } = renderHook(
|
|
267
|
+
({ accountDefaults }: typeof initialProps) =>
|
|
268
|
+
useSessionPageFlow({ ...OPTS, accountDefaults }),
|
|
269
|
+
{ initialProps },
|
|
270
|
+
);
|
|
271
|
+
act(() => {
|
|
272
|
+
result.current.setAutoApproveAll(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
rerender({ accountDefaults: { autoApprove: true } });
|
|
276
|
+
expect(result.current.autoApproveAll).toBe(false);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("the user's Turn off beats the preference for this session only", async () => {
|
|
280
|
+
const { result } = renderHook(() =>
|
|
281
|
+
useSessionPageFlow({ ...OPTS, accountDefaults: { autoApprove: true } }),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
act(() => {
|
|
285
|
+
result.current.setAutoApproveAll(false);
|
|
286
|
+
});
|
|
287
|
+
expect(result.current.autoApproveAll).toBe(false);
|
|
288
|
+
|
|
289
|
+
await act(async () => {
|
|
290
|
+
await result.current.handleSubmit("after turn off");
|
|
291
|
+
});
|
|
292
|
+
const followUpOpts = mockSendFollowUp.mock.calls.at(-1)?.[1];
|
|
293
|
+
expect(followUpOpts.autoApproveAll).toBeUndefined();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("carries the preference-armed state into follow-ups", async () => {
|
|
297
|
+
const { result } = renderHook(() =>
|
|
298
|
+
useSessionPageFlow({ ...OPTS, accountDefaults: { autoApprove: true } }),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
await act(async () => {
|
|
302
|
+
await result.current.handleSubmit("first message");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const followUpOpts = mockSendFollowUp.mock.calls[0][1];
|
|
306
|
+
expect(followUpOpts.autoApproveAll).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("guests never inherit the account preference", () => {
|
|
310
|
+
const { result } = renderHook(() =>
|
|
311
|
+
useSessionPageFlow({
|
|
312
|
+
...OPTS,
|
|
313
|
+
audience: "guest",
|
|
314
|
+
accountDefaults: { autoApprove: true },
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
expect(result.current.autoApproveAll).toBe(false);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
234
321
|
describe("useSessionPageFlow — arming derived from the in-flight run (#816)", () => {
|
|
235
322
|
afterEach(() => {
|
|
236
323
|
mockConv.activeStreamExecution = null;
|