@stigmer/react 3.0.5 → 3.0.7-dev.20260611143057
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/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +1 -1
- package/agent/AgentDetailView.js.map +1 -1
- package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
- package/agent-instance/AgentInstanceDetailPanel.js +2 -13
- package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
- package/agent-instance/AgentInstanceList.d.ts.map +1 -1
- package/agent-instance/AgentInstanceList.js +2 -13
- package/agent-instance/AgentInstanceList.js.map +1 -1
- package/agent-instance/CreateAgentInstanceDialog.d.ts.map +1 -1
- package/agent-instance/CreateAgentInstanceDialog.js +1 -1
- package/agent-instance/CreateAgentInstanceDialog.js.map +1 -1
- package/composer/SessionComposer.d.ts +14 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +15 -9
- package/composer/SessionComposer.js.map +1 -1
- package/index.d.ts +3 -3
- package/index.d.ts.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/library/InstanceVisibilitySelector.d.ts +30 -23
- package/library/InstanceVisibilitySelector.d.ts.map +1 -1
- package/library/InstanceVisibilitySelector.js +22 -145
- package/library/InstanceVisibilitySelector.js.map +1 -1
- package/library/ResourceVisibilityControl.d.ts +23 -6
- package/library/ResourceVisibilityControl.d.ts.map +1 -1
- package/library/ResourceVisibilityControl.js +38 -9
- package/library/ResourceVisibilityControl.js.map +1 -1
- package/library/ScopeToggle.d.ts +1 -1
- package/library/ScopeToggle.js +1 -1
- package/library/VisibilityOptionRow.d.ts +52 -0
- package/library/VisibilityOptionRow.d.ts.map +1 -0
- package/library/VisibilityOptionRow.js +92 -0
- package/library/VisibilityOptionRow.js.map +1 -0
- package/library/VisibilitySelector.d.ts +98 -0
- package/library/VisibilitySelector.d.ts.map +1 -0
- package/library/VisibilitySelector.js +193 -0
- package/library/VisibilitySelector.js.map +1 -0
- package/library/index.d.ts +4 -2
- package/library/index.d.ts.map +1 -1
- package/library/index.js +2 -1
- package/library/index.js.map +1 -1
- package/library/useUpdateVisibility.d.ts +5 -4
- package/library/useUpdateVisibility.d.ts.map +1 -1
- package/library/useUpdateVisibility.js +5 -4
- package/library/useUpdateVisibility.js.map +1 -1
- package/library/visibilityLevels.d.ts +96 -0
- package/library/visibilityLevels.d.ts.map +1 -0
- package/library/visibilityLevels.js +97 -0
- package/library/visibilityLevels.js.map +1 -0
- package/mcp-server/McpServerDetailView.d.ts +1 -11
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +3 -6
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/package.json +4 -4
- package/resource-detail/types.d.ts +1 -1
- package/session/NewSessionViewer.d.ts +32 -1
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +20 -9
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +24 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +18 -12
- package/session/SessionViewer.js.map +1 -1
- package/session/audience.d.ts +21 -0
- package/session/audience.d.ts.map +1 -0
- package/session/audience.js +2 -0
- package/session/audience.js.map +1 -0
- package/session/index.d.ts +2 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js.map +1 -1
- package/session/runtime-env.d.ts +47 -0
- package/session/runtime-env.d.ts.map +1 -0
- package/session/runtime-env.js +20 -0
- package/session/runtime-env.js.map +1 -0
- package/session/useNewSessionFlow.d.ts +25 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +20 -8
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +27 -2
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +34 -13
- package/session/useSessionPageFlow.js.map +1 -1
- package/skill/SkillDetailView.d.ts.map +1 -1
- package/skill/SkillDetailView.js +1 -1
- package/skill/SkillDetailView.js.map +1 -1
- package/src/agent/AgentDetailView.tsx +1 -0
- package/src/agent-instance/AgentInstanceDetailPanel.tsx +7 -32
- package/src/agent-instance/AgentInstanceList.tsx +7 -32
- package/src/agent-instance/CreateAgentInstanceDialog.tsx +1 -0
- package/src/composer/SessionComposer.tsx +30 -8
- package/src/composer/__tests__/SessionComposer-lockAgent.test.tsx +150 -0
- package/src/index.ts +10 -2
- package/src/library/InstanceVisibilitySelector.tsx +44 -283
- package/src/library/ResourceVisibilityControl.tsx +54 -8
- package/src/library/ScopeToggle.tsx +1 -1
- package/src/library/VisibilityOptionRow.tsx +244 -0
- package/src/library/VisibilitySelector.tsx +436 -0
- package/src/library/__tests__/VisibilitySelector.test.tsx +256 -0
- package/src/library/index.ts +13 -2
- package/src/library/useUpdateVisibility.ts +5 -4
- package/src/library/visibilityLevels.ts +174 -0
- package/src/mcp-server/McpServerDetailView.tsx +10 -35
- package/src/resource-detail/types.ts +1 -1
- package/src/session/NewSessionViewer.tsx +61 -12
- package/src/session/SessionViewer.tsx +51 -15
- package/src/session/__tests__/audienceWiring.test.tsx +274 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +122 -0
- package/src/session/__tests__/useSessionPageFlow.runtimeEnv.test.tsx +170 -0
- package/src/session/audience.ts +20 -0
- package/src/session/index.ts +3 -0
- package/src/session/runtime-env.ts +57 -0
- package/src/session/useNewSessionFlow.ts +44 -9
- package/src/session/useSessionPageFlow.ts +65 -17
- package/src/skill/SkillDetailView.tsx +1 -0
- package/src/workflow/WorkflowDetailView.tsx +1 -0
- package/src/workflow/instance/CreateWorkflowInstanceDialog.tsx +1 -0
- package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +7 -32
- package/src/workflow/instance/WorkflowInstanceList.tsx +7 -32
- package/styles.css +1 -1
- package/workflow/WorkflowDetailView.d.ts.map +1 -1
- package/workflow/WorkflowDetailView.js +1 -1
- package/workflow/WorkflowDetailView.js.map +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.d.ts.map +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.js +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.js.map +1 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.js +2 -13
- package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
- package/workflow/instance/WorkflowInstanceList.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceList.js +2 -13
- package/workflow/instance/WorkflowInstanceList.js.map +1 -1
- package/library/VisibilityToggle.d.ts +0 -53
- package/library/VisibilityToggle.d.ts.map +0 -1
- package/library/VisibilityToggle.js +0 -100
- package/library/VisibilityToggle.js.map +0 -1
- package/src/library/VisibilityToggle.tsx +0 -280
|
@@ -17,6 +17,8 @@ import { SessionComposer } from "../composer";
|
|
|
17
17
|
import { SecretFlowErrorGuide, isSecretFlowError } from "../error";
|
|
18
18
|
import { useSessionPageFlow } from "./useSessionPageFlow";
|
|
19
19
|
import { SessionInspector } from "./inspector/SessionInspector";
|
|
20
|
+
import type { RuntimeEnvProvider } from "./runtime-env";
|
|
21
|
+
import type { SessionAudience } from "./audience";
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Message submitted when the user implements a plan. References the published
|
|
@@ -67,6 +69,27 @@ export interface SessionViewerProps {
|
|
|
67
69
|
* expandable file tree. (DD-004 capability injection, DD-011 opt-in.)
|
|
68
70
|
*/
|
|
69
71
|
readonly workspaceFileLister?: WorkspaceFileLister;
|
|
72
|
+
/**
|
|
73
|
+
* Supplies host-app environment variables for every follow-up
|
|
74
|
+
* execution (e.g. short-lived credentials for MCP tools, minted as
|
|
75
|
+
* the signed-in user). Evaluated fresh at each send; host values win
|
|
76
|
+
* over composer-collected env on key collisions. If the provider
|
|
77
|
+
* throws, the send is blocked and an error banner is shown — see
|
|
78
|
+
* {@link RuntimeEnvProvider}.
|
|
79
|
+
*/
|
|
80
|
+
readonly getRuntimeEnv?: RuntimeEnvProvider;
|
|
81
|
+
/**
|
|
82
|
+
* Presentation audience for the viewer. `"endUser"` locks the
|
|
83
|
+
* session's agent and hides the MCP server, skill, and
|
|
84
|
+
* session-variable configuration in both the composer and the
|
|
85
|
+
* inspector's Setup tab — for product-embedded chat where the agent
|
|
86
|
+
* is configured upstream by the platform. The model selector,
|
|
87
|
+
* interaction mode, attachments, and workspace picker remain. See
|
|
88
|
+
* {@link SessionAudience}.
|
|
89
|
+
*
|
|
90
|
+
* @default "integrator"
|
|
91
|
+
*/
|
|
92
|
+
readonly audience?: SessionAudience;
|
|
70
93
|
/**
|
|
71
94
|
* Slot for host-injected header actions (e.g., Share button with
|
|
72
95
|
* PermissionGate). Rendered in the top-right corner of the viewer.
|
|
@@ -126,12 +149,15 @@ export function SessionViewer({
|
|
|
126
149
|
enableLocal = false,
|
|
127
150
|
onBrowseLocalFolder,
|
|
128
151
|
workspaceFileLister,
|
|
152
|
+
getRuntimeEnv,
|
|
153
|
+
audience = "integrator",
|
|
129
154
|
headerActions,
|
|
130
155
|
onApplied,
|
|
131
156
|
className,
|
|
132
157
|
}: SessionViewerProps) {
|
|
133
|
-
const flow = useSessionPageFlow({ sessionId, org });
|
|
158
|
+
const flow = useSessionPageFlow({ sessionId, org, getRuntimeEnv });
|
|
134
159
|
const { conv } = flow;
|
|
160
|
+
const isEndUser = audience === "endUser";
|
|
135
161
|
|
|
136
162
|
const [modelId, setModelId] = flow.model;
|
|
137
163
|
const [interactionMode, setInteractionMode] = flow.interactionMode;
|
|
@@ -202,6 +228,7 @@ export function SessionViewer({
|
|
|
202
228
|
enableLocal={enableLocal}
|
|
203
229
|
onBrowseLocalFolder={onBrowseLocalFolder}
|
|
204
230
|
onBuildFromPlan={handleBuildFromPlan}
|
|
231
|
+
isEndUser={isEndUser}
|
|
205
232
|
/>
|
|
206
233
|
}
|
|
207
234
|
secondary={
|
|
@@ -216,6 +243,7 @@ export function SessionViewer({
|
|
|
216
243
|
gitHubConnection={gitHubConnection}
|
|
217
244
|
onBrowseLocalFolder={onBrowseLocalFolder}
|
|
218
245
|
workspaceFileLister={workspaceFileLister}
|
|
246
|
+
isEndUser={isEndUser}
|
|
219
247
|
/>
|
|
220
248
|
}
|
|
221
249
|
/>
|
|
@@ -241,6 +269,7 @@ interface ConversationColumnProps {
|
|
|
241
269
|
readonly enableLocal: boolean;
|
|
242
270
|
readonly onBrowseLocalFolder?: () => Promise<string | null>;
|
|
243
271
|
readonly onBuildFromPlan: () => void;
|
|
272
|
+
readonly isEndUser: boolean;
|
|
244
273
|
}
|
|
245
274
|
|
|
246
275
|
function ConversationColumn({
|
|
@@ -256,8 +285,10 @@ function ConversationColumn({
|
|
|
256
285
|
enableLocal,
|
|
257
286
|
onBrowseLocalFolder,
|
|
258
287
|
onBuildFromPlan,
|
|
288
|
+
isEndUser,
|
|
259
289
|
}: ConversationColumnProps) {
|
|
260
290
|
const { conv } = flow;
|
|
291
|
+
const sendError = flow.submitError ?? conv.sendError ?? conv.approvalError;
|
|
261
292
|
|
|
262
293
|
return (
|
|
263
294
|
<div className="flex h-full min-w-0 flex-col">
|
|
@@ -282,9 +313,7 @@ function ConversationColumn({
|
|
|
282
313
|
onReconnect={conv.reconnectStream}
|
|
283
314
|
/>
|
|
284
315
|
)}
|
|
285
|
-
{
|
|
286
|
-
<SendErrorBanner error={(conv.sendError ?? conv.approvalError)!} />
|
|
287
|
-
)}
|
|
316
|
+
{sendError && <SendErrorBanner error={sendError} />}
|
|
288
317
|
{flow.autoApproveAll && (
|
|
289
318
|
<AutoApproveIndicator onTurnOff={() => flow.setAutoApproveAll(false)} />
|
|
290
319
|
)}
|
|
@@ -309,11 +338,12 @@ function ConversationColumn({
|
|
|
309
338
|
onAgentRefChange={flow.setAgentRef}
|
|
310
339
|
onAgentResolutionChange={flow.setResolution}
|
|
311
340
|
isDefaultAgent={flow.isDefaultAgent}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
341
|
+
lockAgent={isEndUser}
|
|
342
|
+
mcpServerUsages={isEndUser ? undefined : flow.mcpServerUsages}
|
|
343
|
+
onMcpServerUsagesChange={isEndUser ? undefined : flow.setMcpServerUsages}
|
|
344
|
+
skillRefs={isEndUser ? undefined : flow.skillRefs}
|
|
345
|
+
onSkillRefsChange={isEndUser ? undefined : flow.setSkillRefs}
|
|
346
|
+
sessionVariables={isEndUser ? undefined : flow.sessionVariables}
|
|
317
347
|
className="px-4 py-3"
|
|
318
348
|
/>
|
|
319
349
|
</div>
|
|
@@ -337,6 +367,7 @@ interface InspectorPanelProps {
|
|
|
337
367
|
readonly gitHubConnection?: UseGitHubConnectionReturn;
|
|
338
368
|
readonly onBrowseLocalFolder?: () => Promise<string | null>;
|
|
339
369
|
readonly workspaceFileLister?: WorkspaceFileLister;
|
|
370
|
+
readonly isEndUser: boolean;
|
|
340
371
|
}
|
|
341
372
|
|
|
342
373
|
function InspectorPanel({
|
|
@@ -350,6 +381,7 @@ function InspectorPanel({
|
|
|
350
381
|
gitHubConnection,
|
|
351
382
|
onBrowseLocalFolder,
|
|
352
383
|
workspaceFileLister,
|
|
384
|
+
isEndUser,
|
|
353
385
|
}: InspectorPanelProps) {
|
|
354
386
|
const selectedItem = useSelectedThreadItem();
|
|
355
387
|
|
|
@@ -390,16 +422,20 @@ function InspectorPanel({
|
|
|
390
422
|
harness: flow.harness,
|
|
391
423
|
executionTarget: flow.executionTarget,
|
|
392
424
|
modelId: flow.model[0],
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
425
|
+
// End users see the configuration but cannot strip it — the Setup
|
|
426
|
+
// tab renders read-only without mutation callbacks (DD-011).
|
|
427
|
+
mutations: isEndUser
|
|
428
|
+
? undefined
|
|
429
|
+
: {
|
|
430
|
+
onRemoveAgent: flow.isDefaultAgent ? undefined : handleRemoveAgent,
|
|
431
|
+
onRemoveMcp: handleRemoveMcp,
|
|
432
|
+
onRemoveSkill: handleRemoveSkill,
|
|
433
|
+
},
|
|
398
434
|
}),
|
|
399
435
|
[
|
|
400
436
|
flow.agentRef, flow.isDefaultAgent, flow.mcpServerUsages, flow.skillRefs,
|
|
401
437
|
flow.sessionVariables, flow.harness, flow.executionTarget, flow.model,
|
|
402
|
-
handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
|
|
438
|
+
isEndUser, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
|
|
403
439
|
],
|
|
404
440
|
);
|
|
405
441
|
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Wiring-contract tests for the `audience` preset on the session organisms.
|
|
6
|
+
//
|
|
7
|
+
// The composer and inspector are replaced with prop-capturing probes so the
|
|
8
|
+
// tests assert exactly what `audience="endUser"` maps to: a locked agent,
|
|
9
|
+
// unwired MCP/skill/session-variable pickers, and a read-only Setup tab.
|
|
10
|
+
// The molecules' own behavior is covered by their co-located tests.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
type CapturedProps = Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
const composerProps: CapturedProps[] = [];
|
|
16
|
+
vi.mock("../../composer", async (importOriginal) => {
|
|
17
|
+
const actual = await importOriginal<typeof import("../../composer")>();
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
SessionComposer: (props: CapturedProps) => {
|
|
21
|
+
composerProps.push(props);
|
|
22
|
+
return <div data-testid="composer-probe" />;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const inspectorProps: CapturedProps[] = [];
|
|
28
|
+
vi.mock("../inspector/SessionInspector", () => ({
|
|
29
|
+
SessionInspector: (props: CapturedProps) => {
|
|
30
|
+
inspectorProps.push(props);
|
|
31
|
+
return <div data-testid="inspector-probe" />;
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("../../execution/MessageThread", () => ({
|
|
36
|
+
MessageThread: () => <div data-testid="thread-probe" />,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Flow stubs — the organisms own these hooks; stub them to a loaded state.
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
const stubWorkspace = {
|
|
44
|
+
entries: [],
|
|
45
|
+
hasEntries: false,
|
|
46
|
+
toInput: vi.fn().mockReturnValue([]),
|
|
47
|
+
addGitRepo: vi.fn(),
|
|
48
|
+
addLocalPath: vi.fn(),
|
|
49
|
+
removeEntry: vi.fn(),
|
|
50
|
+
clear: vi.fn(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const stubSessionVariables = { entries: [], isEmpty: true, clear: vi.fn() };
|
|
54
|
+
|
|
55
|
+
const stubNewSessionFlow = {
|
|
56
|
+
harness: "native" as const,
|
|
57
|
+
setHarness: vi.fn(),
|
|
58
|
+
modelId: undefined,
|
|
59
|
+
setModelId: vi.fn(),
|
|
60
|
+
agentRef: { org: "acme", slug: "support-bot" },
|
|
61
|
+
setAgentRef: vi.fn(),
|
|
62
|
+
resolution: null,
|
|
63
|
+
setResolution: vi.fn(),
|
|
64
|
+
mcpServerUsages: [],
|
|
65
|
+
setMcpServerUsages: vi.fn(),
|
|
66
|
+
skillRefs: [],
|
|
67
|
+
setSkillRefs: vi.fn(),
|
|
68
|
+
workspace: stubWorkspace,
|
|
69
|
+
sessionVariables: stubSessionVariables,
|
|
70
|
+
isSubmitting: false,
|
|
71
|
+
submitError: null,
|
|
72
|
+
submit: vi.fn(),
|
|
73
|
+
};
|
|
74
|
+
const mockUseNewSessionFlow = vi.fn((_options: unknown) => stubNewSessionFlow);
|
|
75
|
+
vi.mock("../useNewSessionFlow", () => ({
|
|
76
|
+
useNewSessionFlow: (options: unknown) => mockUseNewSessionFlow(options),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
const stubConv = {
|
|
80
|
+
session: { spec: {} },
|
|
81
|
+
isLoading: false,
|
|
82
|
+
loadError: null,
|
|
83
|
+
completedExecutions: [],
|
|
84
|
+
activeStreamExecution: null,
|
|
85
|
+
activePhase: null,
|
|
86
|
+
isStreaming: false,
|
|
87
|
+
isConnecting: false,
|
|
88
|
+
sendFollowUp: vi.fn(),
|
|
89
|
+
canSendFollowUp: true,
|
|
90
|
+
isSending: false,
|
|
91
|
+
sendError: null,
|
|
92
|
+
clearSendError: vi.fn(),
|
|
93
|
+
pendingUserMessage: null,
|
|
94
|
+
workspaceEntries: [],
|
|
95
|
+
mcpServerUsages: [],
|
|
96
|
+
skillRefs: [],
|
|
97
|
+
pendingApprovals: [],
|
|
98
|
+
submitApproval: vi.fn(),
|
|
99
|
+
submittingApprovalIds: new Set<string>(),
|
|
100
|
+
streamError: null,
|
|
101
|
+
reconnectStream: vi.fn(),
|
|
102
|
+
approvalError: null,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const stubSessionPageFlow = {
|
|
106
|
+
conv: stubConv,
|
|
107
|
+
harness: "native" as const,
|
|
108
|
+
executionTarget: undefined,
|
|
109
|
+
model: [undefined, vi.fn()] as const,
|
|
110
|
+
interactionMode: ["agent" as const, vi.fn()] as const,
|
|
111
|
+
agentRef: { org: "acme", slug: "support-bot" },
|
|
112
|
+
setAgentRef: vi.fn(),
|
|
113
|
+
resolution: null,
|
|
114
|
+
setResolution: vi.fn(),
|
|
115
|
+
isDefaultAgent: false,
|
|
116
|
+
mcpServerUsages: [],
|
|
117
|
+
setMcpServerUsages: vi.fn(),
|
|
118
|
+
skillRefs: [],
|
|
119
|
+
setSkillRefs: vi.fn(),
|
|
120
|
+
workspace: stubWorkspace,
|
|
121
|
+
sessionVariables: stubSessionVariables,
|
|
122
|
+
autoApproveAll: false,
|
|
123
|
+
setAutoApproveAll: vi.fn(),
|
|
124
|
+
submitApproval: vi.fn(),
|
|
125
|
+
handleSubmit: vi.fn(),
|
|
126
|
+
submitError: null as Error | null,
|
|
127
|
+
displayExecution: null,
|
|
128
|
+
allExecutions: [],
|
|
129
|
+
sandboxWorkspaceRoot: undefined,
|
|
130
|
+
};
|
|
131
|
+
const mockUseSessionPageFlow = vi.fn((_options: unknown) => stubSessionPageFlow);
|
|
132
|
+
vi.mock("../useSessionPageFlow", () => ({
|
|
133
|
+
useSessionPageFlow: (options: unknown) => mockUseSessionPageFlow(options),
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
import { NewSessionViewer } from "../NewSessionViewer";
|
|
137
|
+
import { SessionViewer } from "../SessionViewer";
|
|
138
|
+
|
|
139
|
+
const AGENT_REF = { org: "acme", slug: "support-bot" };
|
|
140
|
+
|
|
141
|
+
function lastComposerProps(): CapturedProps {
|
|
142
|
+
expect(composerProps.length).toBeGreaterThan(0);
|
|
143
|
+
return composerProps.at(-1)!;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function lastSessionConfig(): { mutations?: unknown } {
|
|
147
|
+
expect(inspectorProps.length).toBeGreaterThan(0);
|
|
148
|
+
return inspectorProps.at(-1)!.sessionConfig as { mutations?: unknown };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
composerProps.length = 0;
|
|
153
|
+
inspectorProps.length = 0;
|
|
154
|
+
stubSessionPageFlow.submitError = null;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
afterEach(() => {
|
|
158
|
+
cleanup();
|
|
159
|
+
vi.clearAllMocks();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("NewSessionViewer — audience wiring", () => {
|
|
163
|
+
it("integrator (default): full configuration surface, agent unlocked", () => {
|
|
164
|
+
render(
|
|
165
|
+
<NewSessionViewer
|
|
166
|
+
org="acme"
|
|
167
|
+
onSessionCreated={vi.fn()}
|
|
168
|
+
initialAgentRef={AGENT_REF}
|
|
169
|
+
/>,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const props = lastComposerProps();
|
|
173
|
+
expect(props.lockAgent).toBe(false);
|
|
174
|
+
expect(props.onMcpServerUsagesChange).toBeDefined();
|
|
175
|
+
expect(props.onSkillRefsChange).toBeDefined();
|
|
176
|
+
expect(props.sessionVariables).toBeDefined();
|
|
177
|
+
expect(lastSessionConfig().mutations).toBeDefined();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("endUser with a pinned agent: locked agent, integrator pickers unwired", () => {
|
|
181
|
+
render(
|
|
182
|
+
<NewSessionViewer
|
|
183
|
+
org="acme"
|
|
184
|
+
onSessionCreated={vi.fn()}
|
|
185
|
+
audience="endUser"
|
|
186
|
+
initialAgentRef={AGENT_REF}
|
|
187
|
+
/>,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const props = lastComposerProps();
|
|
191
|
+
expect(props.lockAgent).toBe(true);
|
|
192
|
+
expect(props.onMcpServerUsagesChange).toBeUndefined();
|
|
193
|
+
expect(props.onSkillRefsChange).toBeUndefined();
|
|
194
|
+
expect(props.sessionVariables).toBeUndefined();
|
|
195
|
+
// End-user controls survive the curation.
|
|
196
|
+
expect(props.showHarnessSelector).toBe(true);
|
|
197
|
+
expect(props.showInteractionModePicker).toBe(true);
|
|
198
|
+
// Setup tab is read-only: no remove affordances for pinned config.
|
|
199
|
+
expect(lastSessionConfig().mutations).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("endUser without a pinned agent: pickers hidden but agent not locked", () => {
|
|
203
|
+
render(
|
|
204
|
+
<NewSessionViewer
|
|
205
|
+
org="acme"
|
|
206
|
+
onSessionCreated={vi.fn()}
|
|
207
|
+
audience="endUser"
|
|
208
|
+
/>,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(lastComposerProps().lockAgent).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("forwards getRuntimeEnv and defaultHarness to the flow", () => {
|
|
215
|
+
const getRuntimeEnv = vi.fn();
|
|
216
|
+
render(
|
|
217
|
+
<NewSessionViewer
|
|
218
|
+
org="acme"
|
|
219
|
+
onSessionCreated={vi.fn()}
|
|
220
|
+
getRuntimeEnv={getRuntimeEnv}
|
|
221
|
+
defaultHarness="cursor"
|
|
222
|
+
/>,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(mockUseNewSessionFlow).toHaveBeenCalledWith(
|
|
226
|
+
expect.objectContaining({ getRuntimeEnv, defaultHarness: "cursor" }),
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("SessionViewer — audience wiring", () => {
|
|
232
|
+
it("integrator (default): full configuration surface, agent unlocked", () => {
|
|
233
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
234
|
+
|
|
235
|
+
const props = lastComposerProps();
|
|
236
|
+
expect(props.lockAgent).toBe(false);
|
|
237
|
+
expect(props.onMcpServerUsagesChange).toBeDefined();
|
|
238
|
+
expect(props.onSkillRefsChange).toBeDefined();
|
|
239
|
+
expect(props.sessionVariables).toBeDefined();
|
|
240
|
+
expect(lastSessionConfig().mutations).toBeDefined();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("endUser: locked agent, integrator pickers unwired, read-only Setup tab", () => {
|
|
244
|
+
render(<SessionViewer sessionId="ses_1" org="acme" audience="endUser" />);
|
|
245
|
+
|
|
246
|
+
const props = lastComposerProps();
|
|
247
|
+
expect(props.lockAgent).toBe(true);
|
|
248
|
+
expect(props.onMcpServerUsagesChange).toBeUndefined();
|
|
249
|
+
expect(props.onSkillRefsChange).toBeUndefined();
|
|
250
|
+
expect(props.sessionVariables).toBeUndefined();
|
|
251
|
+
// End-user controls survive the curation.
|
|
252
|
+
expect(props.showInteractionModePicker).toBe(true);
|
|
253
|
+
expect(lastSessionConfig().mutations).toBeUndefined();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("forwards getRuntimeEnv to the flow", () => {
|
|
257
|
+
const getRuntimeEnv = vi.fn();
|
|
258
|
+
render(
|
|
259
|
+
<SessionViewer sessionId="ses_1" org="acme" getRuntimeEnv={getRuntimeEnv} />,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
expect(mockUseSessionPageFlow).toHaveBeenCalledWith(
|
|
263
|
+
expect.objectContaining({ getRuntimeEnv }),
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("renders the flow's submitError through the send-error banner", () => {
|
|
268
|
+
stubSessionPageFlow.submitError = new Error("token mint failed");
|
|
269
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
270
|
+
|
|
271
|
+
const alert = screen.getByRole("alert");
|
|
272
|
+
expect(alert.textContent).toContain("token mint failed");
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -468,6 +468,128 @@ describe("useNewSessionFlow", () => {
|
|
|
468
468
|
});
|
|
469
469
|
});
|
|
470
470
|
|
|
471
|
+
describe("host runtime env (getRuntimeEnv)", () => {
|
|
472
|
+
it("merges host env into the first execution, host wins on collisions", async () => {
|
|
473
|
+
const getRuntimeEnv = vi.fn().mockResolvedValue({
|
|
474
|
+
PLATFORM_TOKEN: { value: "fresh-token", isSecret: true },
|
|
475
|
+
});
|
|
476
|
+
const opts = { ...defaultOptions(), getRuntimeEnv };
|
|
477
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
478
|
+
|
|
479
|
+
await act(async () => {
|
|
480
|
+
await result.current.submit("Hello", undefined, {
|
|
481
|
+
runtimeEnv: {
|
|
482
|
+
PLATFORM_TOKEN: { value: "stale-token", isSecret: true },
|
|
483
|
+
USER_VAR: { value: "kept" },
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
expect(getRuntimeEnv).toHaveBeenCalledTimes(1);
|
|
489
|
+
const execInput = mockCreateExecution.mock.calls[0][0];
|
|
490
|
+
expect(execInput.runtimeEnv).toEqual({
|
|
491
|
+
PLATFORM_TOKEN: { value: "fresh-token", isSecret: true },
|
|
492
|
+
USER_VAR: { value: "kept" },
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("evaluates the provider fresh on every submission", async () => {
|
|
497
|
+
let mint = 0;
|
|
498
|
+
const getRuntimeEnv = vi.fn(() => ({
|
|
499
|
+
TOKEN: { value: `token-${++mint}` },
|
|
500
|
+
}));
|
|
501
|
+
const opts = { ...defaultOptions(), getRuntimeEnv };
|
|
502
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
503
|
+
|
|
504
|
+
await act(async () => {
|
|
505
|
+
await result.current.submit("First");
|
|
506
|
+
});
|
|
507
|
+
await act(async () => {
|
|
508
|
+
await result.current.submit("Second");
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
expect(getRuntimeEnv).toHaveBeenCalledTimes(2);
|
|
512
|
+
expect(mockCreateExecution.mock.calls[0][0].runtimeEnv).toEqual({
|
|
513
|
+
TOKEN: { value: "token-1" },
|
|
514
|
+
});
|
|
515
|
+
expect(mockCreateExecution.mock.calls[1][0].runtimeEnv).toEqual({
|
|
516
|
+
TOKEN: { value: "token-2" },
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("aborts before session creation when the provider throws", async () => {
|
|
521
|
+
const getRuntimeEnv = vi.fn().mockRejectedValue(new Error("token mint failed"));
|
|
522
|
+
const opts = { ...defaultOptions(), getRuntimeEnv };
|
|
523
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
524
|
+
|
|
525
|
+
await act(async () => {
|
|
526
|
+
await result.current.submit("Hello");
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// No orphan session, no execution — the failure is fully pre-flight.
|
|
530
|
+
expect(mockCreateSession).not.toHaveBeenCalled();
|
|
531
|
+
expect(mockCreateExecution).not.toHaveBeenCalled();
|
|
532
|
+
expect(result.current.submitError).toContain("token mint failed");
|
|
533
|
+
expect(opts.onError).toHaveBeenCalled();
|
|
534
|
+
expect(result.current.isSubmitting).toBe(false);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("passes composer env through untouched when no provider is configured", async () => {
|
|
538
|
+
const opts = defaultOptions();
|
|
539
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
540
|
+
|
|
541
|
+
await act(async () => {
|
|
542
|
+
await result.current.submit("Hello", undefined, {
|
|
543
|
+
runtimeEnv: { USER_VAR: { value: "composer-only" } },
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
expect(mockCreateExecution.mock.calls[0][0].runtimeEnv).toEqual({
|
|
548
|
+
USER_VAR: { value: "composer-only" },
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
describe("defaultHarness", () => {
|
|
554
|
+
it("seeds the embedder default when no harness is stored", () => {
|
|
555
|
+
const opts = { ...defaultOptions(), defaultHarness: "cursor" as const };
|
|
556
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
557
|
+
|
|
558
|
+
expect(result.current.harness).toBe("cursor");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("stored user choice outranks the embedder default", () => {
|
|
562
|
+
localStorage.setItem(STORAGE_KEY_HARNESS, "native");
|
|
563
|
+
const opts = { ...defaultOptions(), defaultHarness: "cursor" as const };
|
|
564
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
565
|
+
|
|
566
|
+
expect(result.current.harness).toBe("native");
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it("does not persist the seeded default — only explicit choices", () => {
|
|
570
|
+
const opts = { ...defaultOptions(), defaultHarness: "cursor" as const };
|
|
571
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
572
|
+
|
|
573
|
+
// Seeding must not masquerade as a user choice, otherwise the
|
|
574
|
+
// embedder default would stop applying after the first visit.
|
|
575
|
+
expect(localStorage.getItem(STORAGE_KEY_HARNESS)).toBeNull();
|
|
576
|
+
|
|
577
|
+
act(() => result.current.setHarness("native"));
|
|
578
|
+
expect(localStorage.getItem(STORAGE_KEY_HARNESS)).toBe("native");
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("submits sessions with the seeded default harness", async () => {
|
|
582
|
+
const opts = { ...defaultOptions(), defaultHarness: "cursor" as const };
|
|
583
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
584
|
+
|
|
585
|
+
await act(async () => {
|
|
586
|
+
await result.current.submit("Hello");
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
expect(mockCreateSession.mock.calls[0][0].harness).toBe("cursor");
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
471
593
|
describe("submit while default agent is loading", () => {
|
|
472
594
|
it("awaits default agent and creates session when fetch resolves", async () => {
|
|
473
595
|
const resolvedAgent = { status: { defaultInstanceId: "awaited-inst" } };
|