@stigmer/react 3.12.6 → 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.map +1 -1
- package/composer/ComposerToolbar.js.map +1 -1
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +8 -11
- package/execution/MessageThread.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/execution/useFileChangeContent.d.ts +2 -6
- package/execution/useFileChangeContent.d.ts.map +1 -1
- package/execution/useFileChangeContent.js +4 -12
- package/execution/useFileChangeContent.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 +5 -2
- package/index.d.ts.map +1 -1
- package/index.js +5 -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/internal/download.d.ts +8 -0
- package/internal/download.d.ts.map +1 -0
- package/internal/download.js +25 -0
- package/internal/download.js.map +1 -0
- package/library/useExportResource.d.ts.map +1 -1
- package/library/useExportResource.js +3 -17
- package/library/useExportResource.js.map +1 -1
- package/memory/MemoryListPanel.d.ts +35 -0
- package/memory/MemoryListPanel.d.ts.map +1 -0
- package/memory/MemoryListPanel.js +141 -0
- package/memory/MemoryListPanel.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 +10 -0
- package/memory/index.d.ts.map +1 -0
- package/memory/index.js +14 -0
- package/memory/index.js.map +1 -0
- package/memory/memoryGroups.d.ts +31 -0
- package/memory/memoryGroups.d.ts.map +1 -0
- package/memory/memoryGroups.js +52 -0
- package/memory/memoryGroups.js.map +1 -0
- package/memory/useConfirmMemory.d.ts +31 -0
- package/memory/useConfirmMemory.d.ts.map +1 -0
- package/memory/useConfirmMemory.js +43 -0
- package/memory/useConfirmMemory.js.map +1 -0
- package/memory/useDeleteMemory.d.ts +30 -0
- package/memory/useDeleteMemory.d.ts.map +1 -0
- package/memory/useDeleteMemory.js +42 -0
- package/memory/useDeleteMemory.js.map +1 -0
- package/memory/useMemories.d.ts +33 -0
- package/memory/useMemories.d.ts.map +1 -0
- package/memory/useMemories.js +42 -0
- package/memory/useMemories.js.map +1 -0
- 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/memory/useRejectMemory.d.ts +34 -0
- package/memory/useRejectMemory.d.ts.map +1 -0
- package/memory/useRejectMemory.js +46 -0
- package/memory/useRejectMemory.js.map +1 -0
- package/memory/useUpdateMemoryContent.d.ts +32 -0
- package/memory/useUpdateMemoryContent.d.ts.map +1 -0
- package/memory/useUpdateMemoryContent.js +48 -0
- package/memory/useUpdateMemoryContent.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 -1
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +26 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +28 -22
- package/session/SessionViewer.js.map +1 -1
- package/session/TranscriptExportMenu.d.ts +24 -0
- package/session/TranscriptExportMenu.d.ts.map +1 -0
- package/session/TranscriptExportMenu.js +26 -0
- package/session/TranscriptExportMenu.js.map +1 -0
- 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 +5 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js +2 -0
- package/session/index.js.map +1 -1
- package/session/useExportTranscript.d.ts +44 -0
- package/session/useExportTranscript.d.ts.map +1 -0
- package/session/useExportTranscript.js +109 -0
- package/session/useExportTranscript.js.map +1 -0
- package/session/useNewSessionFlow.d.ts +16 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +25 -6
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +7 -19
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionExecutions.d.ts +2 -14
- package/session/useSessionExecutions.d.ts.map +1 -1
- package/session/useSessionExecutions.js +4 -26
- package/session/useSessionExecutions.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +41 -10
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +67 -9
- package/session/useSessionPageFlow.js.map +1 -1
- package/settings/MemorySection.d.ts +12 -0
- package/settings/MemorySection.d.ts.map +1 -0
- package/settings/MemorySection.js +21 -0
- package/settings/MemorySection.js.map +1 -0
- package/settings/index.d.ts +1 -0
- package/settings/index.d.ts.map +1 -1
- package/settings/index.js +1 -0
- package/settings/index.js.map +1 -1
- package/settings/settings-nav.d.ts.map +1 -1
- package/settings/settings-nav.js +2 -1
- package/settings/settings-nav.js.map +1 -1
- package/src/__tests__/public-api.test.ts +21 -0
- package/src/attachment/__tests__/prepare-image.test.ts +12 -6
- package/src/composer/ComposerToolbar.tsx +1 -0
- package/src/execution/MessageThread.tsx +8 -11
- package/src/execution/ResultView.tsx +6 -0
- package/src/execution/ToolCallItem.tsx +18 -1
- package/src/execution/tool-categories.ts +7 -0
- package/src/execution/useFileChangeContent.ts +5 -12
- 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 +38 -0
- package/src/internal/MemoryEnabledRow.tsx +92 -0
- package/src/internal/download.ts +29 -0
- package/src/library/useExportResource.ts +3 -19
- package/src/memory/MemoryListPanel.tsx +532 -0
- package/src/memory/MemoryProposalCard.tsx +181 -0
- package/src/memory/__tests__/MemoryListPanel.test.tsx +259 -0
- package/src/memory/__tests__/MemoryProposalCard.test.tsx +236 -0
- package/src/memory/__tests__/memoryHooks.test.tsx +283 -0
- package/src/memory/index.ts +24 -0
- package/src/memory/memoryGroups.ts +64 -0
- package/src/memory/useConfirmMemory.ts +62 -0
- package/src/memory/useDeleteMemory.ts +61 -0
- package/src/memory/useMemories.ts +66 -0
- package/src/memory/useMemory.ts +80 -0
- package/src/memory/useRejectMemory.ts +65 -0
- package/src/memory/useUpdateMemoryContent.ts +67 -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 +17 -0
- package/src/session/SessionViewer.tsx +78 -41
- package/src/session/TranscriptExportMenu.tsx +78 -0
- 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 +189 -0
- package/src/session/__tests__/TranscriptExportMenu.test.tsx +97 -0
- package/src/session/__tests__/useExportTranscript.test.tsx +187 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +89 -0
- package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +263 -2
- 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 +10 -0
- package/src/session/useExportTranscript.ts +171 -0
- package/src/session/useNewSessionFlow.ts +42 -6
- package/src/session/useSessionConversation.ts +10 -16
- package/src/session/useSessionExecutions.ts +5 -26
- package/src/session/useSessionPageFlow.ts +108 -21
- package/src/settings/MemorySection.tsx +45 -0
- package/src/settings/index.ts +1 -0
- package/src/settings/settings-nav.ts +2 -0
- package/src/switch/Switch.tsx +4 -0
- package/src/usage/useExportCSV.ts +2 -14
- 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/usage/useExportCSV.d.ts.map +1 -1
- package/usage/useExportCSV.js +2 -13
- package/usage/useExportCSV.js.map +1 -1
- package/workspace/WorkspaceSurface.js +11 -2
- package/workspace/WorkspaceSurface.js.map +1 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// useExportTranscript (stigmer/stigmer#814): the lazy export actions run the
|
|
2
|
+
// REAL canonical assembly (fetchSessionTranscript from @stigmer/sdk) against
|
|
3
|
+
// a fake client, so these tests pin the hook's end-to-end content, filenames,
|
|
4
|
+
// clipboard strategy, and failure honesty — not a mock of the SDK.
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
7
|
+
import { renderHook, act } from "@testing-library/react";
|
|
8
|
+
import { create } from "@bufbuild/protobuf";
|
|
9
|
+
import {
|
|
10
|
+
AgentExecutionSchema,
|
|
11
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
12
|
+
import {
|
|
13
|
+
ExecutionPhase,
|
|
14
|
+
MessageType,
|
|
15
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
16
|
+
import { AgentExecutionListSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/io_pb";
|
|
17
|
+
import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
18
|
+
|
|
19
|
+
const toastSuccess = vi.fn();
|
|
20
|
+
const toastError = vi.fn();
|
|
21
|
+
vi.mock("../../feedback/toast", () => ({
|
|
22
|
+
toast: {
|
|
23
|
+
success: (...args: unknown[]) => toastSuccess(...args),
|
|
24
|
+
error: (...args: unknown[]) => toastError(...args),
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const fakeSession = create(SessionSchema, {
|
|
29
|
+
metadata: { id: "ses_01" },
|
|
30
|
+
spec: { subject: "Fix the flaky test" },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const fakeExecution = create(AgentExecutionSchema, {
|
|
34
|
+
metadata: { id: "aex_01" },
|
|
35
|
+
spec: { sessionId: "ses_01", message: "Why is CI red?" },
|
|
36
|
+
status: {
|
|
37
|
+
phase: ExecutionPhase.EXECUTION_COMPLETED,
|
|
38
|
+
messages: [
|
|
39
|
+
{ type: MessageType.MESSAGE_THINKING, content: "Checking the loop." },
|
|
40
|
+
{ type: MessageType.MESSAGE_AI, content: "Found it." },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let listBySession: (input: unknown) => Promise<unknown>;
|
|
46
|
+
vi.mock("../../hooks", () => ({
|
|
47
|
+
useStigmer: () => ({
|
|
48
|
+
session: { get: () => Promise.resolve(fakeSession) },
|
|
49
|
+
agentExecution: {
|
|
50
|
+
listBySession: (input: unknown) => listBySession(input),
|
|
51
|
+
getArtifactContent: vi.fn(),
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
import { useExportTranscript } from "../useExportTranscript";
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// DOM stubs: clipboard and blob downloads
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
let writeText: ReturnType<typeof vi.fn>;
|
|
63
|
+
let clipboardWrite: ReturnType<typeof vi.fn>;
|
|
64
|
+
let createdBlobs: Blob[];
|
|
65
|
+
let downloadedFilenames: string[];
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
vi.clearAllMocks();
|
|
69
|
+
listBySession = vi.fn(() =>
|
|
70
|
+
Promise.resolve(
|
|
71
|
+
create(AgentExecutionListSchema, {
|
|
72
|
+
totalPages: 1,
|
|
73
|
+
entries: [fakeExecution],
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
writeText = vi.fn(() => Promise.resolve());
|
|
79
|
+
clipboardWrite = vi.fn(() => Promise.resolve());
|
|
80
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
81
|
+
value: { writeText },
|
|
82
|
+
configurable: true,
|
|
83
|
+
});
|
|
84
|
+
vi.stubGlobal("ClipboardItem", undefined);
|
|
85
|
+
|
|
86
|
+
createdBlobs = [];
|
|
87
|
+
downloadedFilenames = [];
|
|
88
|
+
URL.createObjectURL = vi.fn((blob: Blob) => {
|
|
89
|
+
createdBlobs.push(blob);
|
|
90
|
+
return "blob:mock";
|
|
91
|
+
});
|
|
92
|
+
URL.revokeObjectURL = vi.fn();
|
|
93
|
+
vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(function (
|
|
94
|
+
this: HTMLAnchorElement,
|
|
95
|
+
) {
|
|
96
|
+
downloadedFilenames.push(this.download);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
vi.unstubAllGlobals();
|
|
102
|
+
vi.restoreAllMocks();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("useExportTranscript", () => {
|
|
106
|
+
it("is lazy — nothing is fetched until an action runs", () => {
|
|
107
|
+
renderHook(() => useExportTranscript("ses_01"));
|
|
108
|
+
expect(listBySession).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("copies the canonical Markdown via writeText when ClipboardItem is unavailable", async () => {
|
|
112
|
+
const { result } = renderHook(() => useExportTranscript("ses_01"));
|
|
113
|
+
await act(() => result.current.copyMarkdown());
|
|
114
|
+
|
|
115
|
+
expect(writeText).toHaveBeenCalledTimes(1);
|
|
116
|
+
const markdown = writeText.mock.calls[0][0] as string;
|
|
117
|
+
expect(markdown).toContain("# Fix the flaky test");
|
|
118
|
+
expect(markdown).toContain("Why is CI red?");
|
|
119
|
+
// The fidelity headline: thinking is in the export.
|
|
120
|
+
expect(markdown).toContain("**Thinking**");
|
|
121
|
+
expect(markdown).toContain("> Checking the loop.");
|
|
122
|
+
expect(toastSuccess).toHaveBeenCalledWith("Transcript copied to clipboard");
|
|
123
|
+
expect(result.current.error).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("copies via a promise-carrying ClipboardItem when available (the WebKit gesture path)", async () => {
|
|
127
|
+
class FakeClipboardItem {
|
|
128
|
+
constructor(readonly items: Record<string, Promise<Blob>>) {}
|
|
129
|
+
}
|
|
130
|
+
vi.stubGlobal("ClipboardItem", FakeClipboardItem);
|
|
131
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
132
|
+
value: { writeText, write: clipboardWrite },
|
|
133
|
+
configurable: true,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const { result } = renderHook(() => useExportTranscript("ses_01"));
|
|
137
|
+
await act(() => result.current.copyMarkdown());
|
|
138
|
+
|
|
139
|
+
// The write is handed the item synchronously; the content arrives via
|
|
140
|
+
// the promise — writeText (which would lose the gesture) is never used.
|
|
141
|
+
expect(clipboardWrite).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(writeText).not.toHaveBeenCalled();
|
|
143
|
+
const item = clipboardWrite.mock.calls[0][0][0] as InstanceType<
|
|
144
|
+
typeof FakeClipboardItem
|
|
145
|
+
>;
|
|
146
|
+
const blob = await item.items["text/plain"];
|
|
147
|
+
expect(await blob.text()).toContain("# Fix the flaky test");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("downloads Markdown under the subject-slug filename", async () => {
|
|
151
|
+
const { result } = renderHook(() => useExportTranscript("ses_01"));
|
|
152
|
+
await act(() => result.current.downloadMarkdown());
|
|
153
|
+
|
|
154
|
+
expect(downloadedFilenames).toEqual(["fix-the-flaky-test-transcript.md"]);
|
|
155
|
+
expect(await createdBlobs[0].text()).toContain("# Fix the flaky test");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("downloads JSON in the protojson-parity format", async () => {
|
|
159
|
+
const { result } = renderHook(() => useExportTranscript("ses_01"));
|
|
160
|
+
await act(() => result.current.downloadJson());
|
|
161
|
+
|
|
162
|
+
expect(downloadedFilenames).toEqual(["fix-the-flaky-test-transcript.json"]);
|
|
163
|
+
const parsed = JSON.parse(await createdBlobs[0].text());
|
|
164
|
+
expect(parsed.format).toBe("stigmer.ai/session-transcript/v1");
|
|
165
|
+
expect(parsed.turns[0].execution.status.messages[0].content).toBe(
|
|
166
|
+
"Checking the loop.",
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("surfaces fetch failures as a typed error and a toast — never a silent no-op", async () => {
|
|
171
|
+
listBySession = vi.fn(() => Promise.reject(new Error("network down")));
|
|
172
|
+
const { result } = renderHook(() => useExportTranscript("ses_01"));
|
|
173
|
+
await act(() => result.current.downloadMarkdown());
|
|
174
|
+
|
|
175
|
+
expect(result.current.error?.message).toBe("network down");
|
|
176
|
+
expect(toastError).toHaveBeenCalledWith(
|
|
177
|
+
"Couldn't export the transcript: network down",
|
|
178
|
+
);
|
|
179
|
+
expect(downloadedFilenames).toEqual([]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("explains itself when invoked without a session id", async () => {
|
|
183
|
+
const { result } = renderHook(() => useExportTranscript(null));
|
|
184
|
+
await act(() => result.current.downloadMarkdown());
|
|
185
|
+
expect(result.current.error?.message).toMatch(/without a session id/);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -431,6 +431,95 @@ describe("useNewSessionFlow", () => {
|
|
|
431
431
|
expect(execInput.autoApproveAll).toBe(true);
|
|
432
432
|
});
|
|
433
433
|
|
|
434
|
+
it("carries the user's pre-arm toggle into the bootstrap create (#816)", async () => {
|
|
435
|
+
const opts = defaultOptions();
|
|
436
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
437
|
+
|
|
438
|
+
expect(result.current.autoApproveAll).toBe(false);
|
|
439
|
+
act(() => result.current.setAutoApproveAll(true));
|
|
440
|
+
expect(result.current.autoApproveAll).toBe(true);
|
|
441
|
+
|
|
442
|
+
await act(async () => {
|
|
443
|
+
await result.current.submit("Hello");
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const execInput = mockCreateExecution.mock.calls[0][0];
|
|
447
|
+
expect(execInput.autoApproveAll).toBe(true);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("the user's toggle-off beats the host approval default (#816)", async () => {
|
|
451
|
+
const opts = defaultOptions();
|
|
452
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), {
|
|
453
|
+
wrapper: createWrapper(undefined, null, { autoApproveAll: true }),
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
expect(result.current.autoApproveAll).toBe(true);
|
|
457
|
+
act(() => result.current.setAutoApproveAll(false));
|
|
458
|
+
|
|
459
|
+
await act(async () => {
|
|
460
|
+
await result.current.submit("Hello");
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const execInput = mockCreateExecution.mock.calls[0][0];
|
|
464
|
+
expect(execInput.autoApproveAll).toBeUndefined();
|
|
465
|
+
});
|
|
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
|
+
|
|
434
523
|
it("passes cursor harness in the sessionSpec after switching", async () => {
|
|
435
524
|
const opts = defaultOptions();
|
|
436
525
|
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
@@ -11,11 +11,20 @@ import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecutio
|
|
|
11
11
|
const mockSubmitApproval = vi.fn<(t: string, a: ApprovalAction, c?: string) => Promise<void>>();
|
|
12
12
|
const mockSendFollowUp = vi.fn();
|
|
13
13
|
|
|
14
|
+
/** Loosely-typed conversation stub — individual tests mutate the arming and
|
|
15
|
+
* gate fields, and restore them in their afterEach. */
|
|
14
16
|
const mockConv = {
|
|
15
|
-
session: { spec: {} }
|
|
17
|
+
session: { spec: {} } as {
|
|
18
|
+
spec: object;
|
|
19
|
+
metadata?: { labels?: Record<string, string> };
|
|
20
|
+
},
|
|
16
21
|
isLoading: false,
|
|
17
22
|
completedExecutions: [] as unknown[],
|
|
18
|
-
activeStreamExecution: null
|
|
23
|
+
activeStreamExecution: null as {
|
|
24
|
+
spec?: { autoApproveAll?: boolean };
|
|
25
|
+
status?: object;
|
|
26
|
+
} | null,
|
|
27
|
+
pendingApprovals: [] as { toolCallId: string }[],
|
|
19
28
|
workspaceEntries: [] as unknown[],
|
|
20
29
|
submitApproval: mockSubmitApproval,
|
|
21
30
|
sendFollowUp: mockSendFollowUp,
|
|
@@ -221,3 +230,255 @@ describe("useSessionPageFlow — host-set approval default (#302)", () => {
|
|
|
221
230
|
expect(result.current.autoApproveAll).toBe(false);
|
|
222
231
|
});
|
|
223
232
|
});
|
|
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
|
+
|
|
321
|
+
describe("useSessionPageFlow — arming derived from the in-flight run (#816)", () => {
|
|
322
|
+
afterEach(() => {
|
|
323
|
+
mockConv.activeStreamExecution = null;
|
|
324
|
+
mockConv.completedExecutions = [];
|
|
325
|
+
vi.clearAllMocks();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("reflects an active execution armed at create (launcher handoff)", () => {
|
|
329
|
+
mockConv.activeStreamExecution = { spec: { autoApproveAll: true }, status: {} };
|
|
330
|
+
const { result } = renderHook(() => useSessionPageFlow(OPTS));
|
|
331
|
+
expect(result.current.autoApproveAll).toBe(true);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("the user's explicit OFF beats the armed run for follow-up carry", async () => {
|
|
335
|
+
mockConv.activeStreamExecution = { spec: { autoApproveAll: true }, status: {} };
|
|
336
|
+
const { result } = renderHook(() => useSessionPageFlow(OPTS));
|
|
337
|
+
|
|
338
|
+
act(() => {
|
|
339
|
+
result.current.setAutoApproveAll(false);
|
|
340
|
+
});
|
|
341
|
+
expect(result.current.autoApproveAll).toBe(false);
|
|
342
|
+
|
|
343
|
+
await act(async () => {
|
|
344
|
+
await result.current.handleSubmit("after explicit off");
|
|
345
|
+
});
|
|
346
|
+
const followUpOpts = mockSendFollowUp.mock.calls.at(-1)?.[1];
|
|
347
|
+
expect(followUpOpts.autoApproveAll).toBeUndefined();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("never derives from PAST executions (the reset-on-reload consent contract)", () => {
|
|
351
|
+
// A reloaded page whose history contains an armed run must come up at
|
|
352
|
+
// the host default — deriving from history would make the consent
|
|
353
|
+
// silently survive reloads.
|
|
354
|
+
mockConv.completedExecutions = [{ spec: { autoApproveAll: true }, status: {} }];
|
|
355
|
+
const { result } = renderHook(() => useSessionPageFlow(OPTS));
|
|
356
|
+
expect(result.current.autoApproveAll).toBe(false);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe("useSessionPageFlow — armed responder (#816, the walk-away scenario)", () => {
|
|
361
|
+
beforeEach(() => {
|
|
362
|
+
mockSubmitApproval.mockResolvedValue(undefined);
|
|
363
|
+
});
|
|
364
|
+
afterEach(() => {
|
|
365
|
+
mockConv.pendingApprovals = [];
|
|
366
|
+
mockConv.activeStreamExecution = null;
|
|
367
|
+
mockConv.session = { spec: {} };
|
|
368
|
+
vi.clearAllMocks();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("releases a gate that appears while armed", async () => {
|
|
372
|
+
const { rerender } = renderHook(() => useSessionPageFlow(OPTS), {
|
|
373
|
+
wrapper: hostDefaultWrapper(true),
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
mockConv.pendingApprovals = [{ toolCallId: "tc1" }];
|
|
377
|
+
await act(async () => rerender());
|
|
378
|
+
|
|
379
|
+
expect(mockSubmitApproval).toHaveBeenCalledExactlyOnceWith(
|
|
380
|
+
"tc1",
|
|
381
|
+
ApprovalAction.APPROVE_ALL,
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("releases a gate already waiting when the toggle flips ON", async () => {
|
|
386
|
+
mockConv.pendingApprovals = [{ toolCallId: "tc9" }];
|
|
387
|
+
const { result } = renderHook(() => useSessionPageFlow(OPTS));
|
|
388
|
+
expect(mockSubmitApproval).not.toHaveBeenCalled();
|
|
389
|
+
|
|
390
|
+
await act(async () => {
|
|
391
|
+
result.current.setAutoApproveAll(true);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect(mockSubmitApproval).toHaveBeenCalledExactlyOnceWith(
|
|
395
|
+
"tc9",
|
|
396
|
+
ApprovalAction.APPROVE_ALL,
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("submits once per gate across re-renders", async () => {
|
|
401
|
+
const { rerender } = renderHook(() => useSessionPageFlow(OPTS), {
|
|
402
|
+
wrapper: hostDefaultWrapper(true),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
mockConv.pendingApprovals = [{ toolCallId: "tc1" }];
|
|
406
|
+
await act(async () => rerender());
|
|
407
|
+
await act(async () => rerender());
|
|
408
|
+
|
|
409
|
+
expect(mockSubmitApproval).toHaveBeenCalledTimes(1);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("covers a LATER gate of a different class (lease scope contract)", async () => {
|
|
413
|
+
// A gate-time APPROVE_ALL grants a class-scoped lease, so a second gate
|
|
414
|
+
// of another class still parks — the responder must answer it too.
|
|
415
|
+
const { rerender } = renderHook(() => useSessionPageFlow(OPTS), {
|
|
416
|
+
wrapper: hostDefaultWrapper(true),
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
mockConv.pendingApprovals = [{ toolCallId: "tc-shell" }];
|
|
420
|
+
await act(async () => rerender());
|
|
421
|
+
mockConv.pendingApprovals = [{ toolCallId: "tc-write" }];
|
|
422
|
+
await act(async () => rerender());
|
|
423
|
+
|
|
424
|
+
expect(mockSubmitApproval).toHaveBeenCalledTimes(2);
|
|
425
|
+
expect(mockSubmitApproval).toHaveBeenLastCalledWith(
|
|
426
|
+
"tc-write",
|
|
427
|
+
ApprovalAction.APPROVE_ALL,
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("does not retry a failed submission (the card stays for manual action)", async () => {
|
|
432
|
+
mockSubmitApproval.mockRejectedValue(new Error("boom"));
|
|
433
|
+
const { rerender } = renderHook(() => useSessionPageFlow(OPTS), {
|
|
434
|
+
wrapper: hostDefaultWrapper(true),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
mockConv.pendingApprovals = [{ toolCallId: "tc1" }];
|
|
438
|
+
await act(async () => rerender());
|
|
439
|
+
await act(async () => rerender());
|
|
440
|
+
|
|
441
|
+
expect(mockSubmitApproval).toHaveBeenCalledTimes(1);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("stays inert while OFF", async () => {
|
|
445
|
+
const { rerender } = renderHook(() => useSessionPageFlow(OPTS));
|
|
446
|
+
|
|
447
|
+
mockConv.pendingApprovals = [{ toolCallId: "tc1" }];
|
|
448
|
+
await act(async () => rerender());
|
|
449
|
+
|
|
450
|
+
expect(mockSubmitApproval).not.toHaveBeenCalled();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("never fires for a guest, even when the run itself is armed", async () => {
|
|
454
|
+
mockConv.activeStreamExecution = { spec: { autoApproveAll: true }, status: {} };
|
|
455
|
+
mockConv.pendingApprovals = [{ toolCallId: "tc1" }];
|
|
456
|
+
renderHook(() => useSessionPageFlow({ ...OPTS, audience: "guest" }), {
|
|
457
|
+
wrapper: hostDefaultWrapper(true),
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
expect(mockSubmitApproval).not.toHaveBeenCalled();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("never fires for an observer", async () => {
|
|
464
|
+
mockConv.pendingApprovals = [{ toolCallId: "tc1" }];
|
|
465
|
+
renderHook(() => useSessionPageFlow({ ...OPTS, audience: "observer" }), {
|
|
466
|
+
wrapper: hostDefaultWrapper(true),
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
expect(mockSubmitApproval).not.toHaveBeenCalled();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("never fires for a channel-origin session (read-only by construction)", async () => {
|
|
473
|
+
mockConv.session = {
|
|
474
|
+
spec: {},
|
|
475
|
+
metadata: { labels: { "stigmer.ai/channel-id": "ch_1" } },
|
|
476
|
+
};
|
|
477
|
+
mockConv.pendingApprovals = [{ toolCallId: "tc1" }];
|
|
478
|
+
renderHook(() => useSessionPageFlow(OPTS), {
|
|
479
|
+
wrapper: hostDefaultWrapper(true),
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
expect(mockSubmitApproval).not.toHaveBeenCalled();
|
|
483
|
+
});
|
|
484
|
+
});
|