@stigmer/react 3.0.6 → 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-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
- package/agent-instance/AgentInstanceDetailPanel.js +2 -9
- package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
- package/agent-instance/AgentInstanceList.d.ts.map +1 -1
- package/agent-instance/AgentInstanceList.js +2 -9
- 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 +1 -1
- package/index.d.ts.map +1 -1
- package/index.js.map +1 -1
- package/library/InstanceVisibilitySelector.d.ts +23 -9
- package/library/InstanceVisibilitySelector.d.ts.map +1 -1
- package/library/InstanceVisibilitySelector.js +14 -9
- package/library/InstanceVisibilitySelector.js.map +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 +47 -24
- package/library/VisibilitySelector.d.ts.map +1 -1
- package/library/VisibilitySelector.js +137 -115
- package/library/VisibilitySelector.js.map +1 -1
- package/library/visibilityLevels.d.ts +25 -3
- package/library/visibilityLevels.d.ts.map +1 -1
- package/library/visibilityLevels.js +8 -2
- package/library/visibilityLevels.js.map +1 -1
- package/package.json +4 -4
- 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/src/agent-instance/AgentInstanceDetailPanel.tsx +7 -27
- package/src/agent-instance/AgentInstanceList.tsx +7 -27
- 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 +2 -0
- package/src/library/InstanceVisibilitySelector.tsx +27 -9
- package/src/library/VisibilityOptionRow.tsx +244 -0
- package/src/library/VisibilitySelector.tsx +303 -260
- package/src/library/__tests__/VisibilitySelector.test.tsx +256 -0
- package/src/library/visibilityLevels.ts +35 -5
- 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/workflow/instance/CreateWorkflowInstanceDialog.tsx +1 -0
- package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +7 -27
- package/src/workflow/instance/WorkflowInstanceList.tsx +7 -27
- package/styles.css +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 -9
- package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
- package/workflow/instance/WorkflowInstanceList.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceList.js +2 -9
- package/workflow/instance/WorkflowInstanceList.js.map +1 -1
|
@@ -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" } };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { renderHook, act } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Mocks — useSessionPageFlow composes many hooks; we stub them to isolate the
|
|
6
|
+
// host runtime-env behavior (per-follow-up evaluation, host-wins merge, and
|
|
7
|
+
// fail-fast into submitError before any optimistic UI).
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
const mockSendFollowUp = vi.fn();
|
|
11
|
+
|
|
12
|
+
const mockConv = {
|
|
13
|
+
session: { spec: {} },
|
|
14
|
+
isLoading: false,
|
|
15
|
+
completedExecutions: [] as unknown[],
|
|
16
|
+
activeStreamExecution: null,
|
|
17
|
+
workspaceEntries: [] as unknown[],
|
|
18
|
+
submitApproval: vi.fn(),
|
|
19
|
+
sendFollowUp: mockSendFollowUp,
|
|
20
|
+
};
|
|
21
|
+
vi.mock("../useSessionConversation", () => ({
|
|
22
|
+
useSessionConversation: () => mockConv,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("../../hooks", () => ({
|
|
26
|
+
useStigmer: () => ({ agent: { getByReference: vi.fn() } }),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock("../../agent", () => ({
|
|
30
|
+
useDefaultAgent: () => ({ agent: null, isLoading: false, error: null }),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const mockWorkspace = {
|
|
34
|
+
entries: [],
|
|
35
|
+
hasEntries: false,
|
|
36
|
+
toInput: vi.fn().mockReturnValue([]),
|
|
37
|
+
addGitRepo: vi.fn(),
|
|
38
|
+
addLocalPath: vi.fn(),
|
|
39
|
+
removeEntry: vi.fn(),
|
|
40
|
+
clear: vi.fn(),
|
|
41
|
+
};
|
|
42
|
+
vi.mock("../../workspace", () => ({
|
|
43
|
+
useWorkspaceEntries: () => mockWorkspace,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const mockSessionVariables = {
|
|
47
|
+
variables: [],
|
|
48
|
+
isEmpty: true,
|
|
49
|
+
clear: vi.fn(),
|
|
50
|
+
};
|
|
51
|
+
vi.mock("../../execution/useSessionVariables", () => ({
|
|
52
|
+
useSessionVariables: () => mockSessionVariables,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
vi.mock("../usePersistedModel", () => ({
|
|
56
|
+
usePersistedModel: () => ["model-x", vi.fn()] as const,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
vi.mock("../useAgentRefFromSession", () => ({
|
|
60
|
+
useAgentRefFromSession: () => ({ agentRef: null }),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
import { useSessionPageFlow } from "../useSessionPageFlow";
|
|
64
|
+
|
|
65
|
+
const OPTS = { sessionId: "ses_1", org: "acme" };
|
|
66
|
+
|
|
67
|
+
describe("useSessionPageFlow — host runtime env (getRuntimeEnv)", () => {
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
vi.clearAllMocks();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("merges host env into follow-ups, host wins on collisions", async () => {
|
|
73
|
+
const getRuntimeEnv = vi.fn().mockResolvedValue({
|
|
74
|
+
PLATFORM_TOKEN: { value: "fresh-token", isSecret: true },
|
|
75
|
+
});
|
|
76
|
+
const { result } = renderHook(() =>
|
|
77
|
+
useSessionPageFlow({ ...OPTS, getRuntimeEnv }),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
await act(async () => {
|
|
81
|
+
await result.current.handleSubmit("follow up", undefined, {
|
|
82
|
+
runtimeEnv: {
|
|
83
|
+
PLATFORM_TOKEN: { value: "stale-token", isSecret: true },
|
|
84
|
+
USER_VAR: { value: "kept" },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(mockSendFollowUp).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(mockSendFollowUp.mock.calls[0][1].runtimeEnv).toEqual({
|
|
91
|
+
PLATFORM_TOKEN: { value: "fresh-token", isSecret: true },
|
|
92
|
+
USER_VAR: { value: "kept" },
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("evaluates the provider fresh on every follow-up", async () => {
|
|
97
|
+
let mint = 0;
|
|
98
|
+
const getRuntimeEnv = vi.fn(() => ({ TOKEN: { value: `token-${++mint}` } }));
|
|
99
|
+
const { result } = renderHook(() =>
|
|
100
|
+
useSessionPageFlow({ ...OPTS, getRuntimeEnv }),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
await act(async () => {
|
|
104
|
+
await result.current.handleSubmit("first");
|
|
105
|
+
});
|
|
106
|
+
await act(async () => {
|
|
107
|
+
await result.current.handleSubmit("second");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(getRuntimeEnv).toHaveBeenCalledTimes(2);
|
|
111
|
+
expect(mockSendFollowUp.mock.calls[0][1].runtimeEnv).toEqual({
|
|
112
|
+
TOKEN: { value: "token-1" },
|
|
113
|
+
});
|
|
114
|
+
expect(mockSendFollowUp.mock.calls[1][1].runtimeEnv).toEqual({
|
|
115
|
+
TOKEN: { value: "token-2" },
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("blocks the send and sets submitError when the provider throws", async () => {
|
|
120
|
+
const getRuntimeEnv = vi.fn().mockRejectedValue(new Error("token mint failed"));
|
|
121
|
+
const { result } = renderHook(() =>
|
|
122
|
+
useSessionPageFlow({ ...OPTS, getRuntimeEnv }),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await act(async () => {
|
|
126
|
+
await result.current.handleSubmit("follow up");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Nothing was sent: no optimistic message, no session-variable clear.
|
|
130
|
+
expect(mockSendFollowUp).not.toHaveBeenCalled();
|
|
131
|
+
expect(mockSessionVariables.clear).not.toHaveBeenCalled();
|
|
132
|
+
expect(result.current.submitError).toBeInstanceOf(Error);
|
|
133
|
+
expect(result.current.submitError?.message).toBe("token mint failed");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("clears submitError at the start of the next submission", async () => {
|
|
137
|
+
const getRuntimeEnv = vi
|
|
138
|
+
.fn()
|
|
139
|
+
.mockRejectedValueOnce(new Error("transient failure"))
|
|
140
|
+
.mockResolvedValue({ TOKEN: { value: "ok" } });
|
|
141
|
+
const { result } = renderHook(() =>
|
|
142
|
+
useSessionPageFlow({ ...OPTS, getRuntimeEnv }),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await act(async () => {
|
|
146
|
+
await result.current.handleSubmit("fails");
|
|
147
|
+
});
|
|
148
|
+
expect(result.current.submitError).not.toBeNull();
|
|
149
|
+
|
|
150
|
+
await act(async () => {
|
|
151
|
+
await result.current.handleSubmit("succeeds");
|
|
152
|
+
});
|
|
153
|
+
expect(result.current.submitError).toBeNull();
|
|
154
|
+
expect(mockSendFollowUp).toHaveBeenCalledTimes(1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("passes composer env through untouched when no provider is configured", async () => {
|
|
158
|
+
const { result } = renderHook(() => useSessionPageFlow(OPTS));
|
|
159
|
+
|
|
160
|
+
await act(async () => {
|
|
161
|
+
await result.current.handleSubmit("follow up", undefined, {
|
|
162
|
+
runtimeEnv: { USER_VAR: { value: "composer-only" } },
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(mockSendFollowUp.mock.calls[0][1].runtimeEnv).toEqual({
|
|
167
|
+
USER_VAR: { value: "composer-only" },
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who the session organisms (`SessionViewer` / `NewSessionViewer`) are
|
|
3
|
+
* presented to.
|
|
4
|
+
*
|
|
5
|
+
* - `"integrator"` (default) — the full configuration surface: agent
|
|
6
|
+
* picker, MCP servers, skills, and session variables. The presentation
|
|
7
|
+
* used by the Stigmer Console, where the person at the keyboard is
|
|
8
|
+
* composing the session's configuration.
|
|
9
|
+
* - `"endUser"` — a curated, product-embedded chat. The agent (and its
|
|
10
|
+
* MCP servers, skills, and identity) is configured upstream by the
|
|
11
|
+
* embedding platform; the end user chats, picks a model, toggles
|
|
12
|
+
* Agent/Plan mode, and attaches workspaces, but never reconfigures
|
|
13
|
+
* the agent. The organisms lock the agent and hide the integrator
|
|
14
|
+
* pickers, in both the composer and the inspector's Setup tab.
|
|
15
|
+
*
|
|
16
|
+
* A preset rather than individual flags: "end user" is a product intent,
|
|
17
|
+
* and keeping it in one place lets the SDK evolve what that presentation
|
|
18
|
+
* means without breaking embedders.
|
|
19
|
+
*/
|
|
20
|
+
export type SessionAudience = "integrator" | "endUser";
|
package/src/session/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { toProtoExecutionTarget, fromProtoExecutionTarget } from "./execution-target";
|
|
2
2
|
export type { ExecutionTargetOption } from "./execution-target";
|
|
3
3
|
|
|
4
|
+
export type { RuntimeEnvProvider } from "./runtime-env";
|
|
5
|
+
export type { SessionAudience } from "./audience";
|
|
6
|
+
|
|
4
7
|
export { useCreateSession } from "./useCreateSession";
|
|
5
8
|
export type {
|
|
6
9
|
SharedSessionFields,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { EnvVarInput } from "@stigmer/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Host-app callback that supplies environment variables for a single
|
|
5
|
+
* execution.
|
|
6
|
+
*
|
|
7
|
+
* Invoked once per execution, at submit time — never cached — so
|
|
8
|
+
* short-lived credentials (e.g. a freshly minted platform token scoped
|
|
9
|
+
* to the signed-in user) are always current when the execution starts.
|
|
10
|
+
* May return the variables synchronously or via a promise.
|
|
11
|
+
*
|
|
12
|
+
* Host-provided values take precedence over composer-collected env on
|
|
13
|
+
* key collisions: the host injects identity material, and a stale or
|
|
14
|
+
* user-supplied value must never shadow it.
|
|
15
|
+
*
|
|
16
|
+
* If the provider throws (or rejects), the submission is aborted and
|
|
17
|
+
* the error surfaces through the owning flow's error channel — an
|
|
18
|
+
* execution never runs with missing or stale credentials.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <SessionViewer
|
|
23
|
+
* sessionId={id}
|
|
24
|
+
* org={org}
|
|
25
|
+
* getRuntimeEnv={async () => ({
|
|
26
|
+
* PLATFORM_TOKEN: { value: await mintShortLivedToken(), isSecret: true },
|
|
27
|
+
* PLATFORM_ORG: { value: activeOrg },
|
|
28
|
+
* })}
|
|
29
|
+
* />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export type RuntimeEnvProvider = () =>
|
|
33
|
+
| Promise<Record<string, EnvVarInput>>
|
|
34
|
+
| Record<string, EnvVarInput>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolves the effective `runtimeEnv` for one execution by merging
|
|
38
|
+
* composer-collected env with host-provided env from a
|
|
39
|
+
* {@link RuntimeEnvProvider}.
|
|
40
|
+
*
|
|
41
|
+
* Host values win on key collisions (see {@link RuntimeEnvProvider}).
|
|
42
|
+
* Returns `undefined` when neither source contributes any variables,
|
|
43
|
+
* so callers can pass the result straight to execution creation.
|
|
44
|
+
*
|
|
45
|
+
* Provider errors are intentionally not caught here: callers must
|
|
46
|
+
* treat a failure as fatal for the submission. Callers without a
|
|
47
|
+
* provider should pass the composer env through directly rather than
|
|
48
|
+
* paying this function's await.
|
|
49
|
+
*/
|
|
50
|
+
export async function resolveExecutionRuntimeEnv(
|
|
51
|
+
getRuntimeEnv: RuntimeEnvProvider,
|
|
52
|
+
composerEnv: Record<string, EnvVarInput> | undefined,
|
|
53
|
+
): Promise<Record<string, EnvVarInput> | undefined> {
|
|
54
|
+
const hostEnv = await getRuntimeEnv();
|
|
55
|
+
const merged = { ...composerEnv, ...hostEnv };
|
|
56
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
57
|
+
}
|