@stigmer/react 3.1.2 → 3.1.4
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/SessionComposer.d.ts +37 -5
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +13 -5
- package/composer/SessionComposer.js.map +1 -1
- package/execution/ArtifactApplyButton.d.ts +25 -0
- package/execution/ArtifactApplyButton.d.ts.map +1 -0
- package/execution/ArtifactApplyButton.js +21 -0
- package/execution/ArtifactApplyButton.js.map +1 -0
- package/execution/ArtifactContentBody.d.ts +36 -0
- package/execution/ArtifactContentBody.d.ts.map +1 -0
- package/execution/ArtifactContentBody.js +62 -0
- package/execution/ArtifactContentBody.js.map +1 -0
- package/execution/ArtifactDocument.d.ts +48 -0
- package/execution/ArtifactDocument.d.ts.map +1 -0
- package/execution/ArtifactDocument.js +89 -0
- package/execution/ArtifactDocument.js.map +1 -0
- package/execution/ArtifactPreviewModal.d.ts +9 -9
- package/execution/ArtifactPreviewModal.d.ts.map +1 -1
- package/execution/ArtifactPreviewModal.js +18 -145
- package/execution/ArtifactPreviewModal.js.map +1 -1
- package/execution/ArtifactRow.d.ts +49 -0
- package/execution/ArtifactRow.d.ts.map +1 -0
- package/execution/ArtifactRow.js +56 -0
- package/execution/ArtifactRow.js.map +1 -0
- package/execution/ArtifactsWidget.d.ts +6 -9
- package/execution/ArtifactsWidget.d.ts.map +1 -1
- package/execution/ArtifactsWidget.js +15 -19
- package/execution/ArtifactsWidget.js.map +1 -1
- package/execution/FileChangeProgressBar.d.ts +47 -0
- package/execution/FileChangeProgressBar.d.ts.map +1 -0
- package/execution/FileChangeProgressBar.js +49 -0
- package/execution/FileChangeProgressBar.js.map +1 -0
- package/execution/FileChangesView.d.ts.map +1 -1
- package/execution/FileChangesView.js +19 -4
- package/execution/FileChangesView.js.map +1 -1
- package/execution/FileReviewAtoms.d.ts +27 -0
- package/execution/FileReviewAtoms.d.ts.map +1 -0
- package/execution/FileReviewAtoms.js +64 -0
- package/execution/FileReviewAtoms.js.map +1 -0
- package/execution/FileReviewCard.d.ts +2 -2
- package/execution/FileReviewCard.d.ts.map +1 -1
- package/execution/FileReviewCard.js +32 -62
- package/execution/FileReviewCard.js.map +1 -1
- package/execution/MessageEntry.d.ts +0 -9
- package/execution/MessageEntry.d.ts.map +1 -1
- package/execution/MessageEntry.js +1 -11
- package/execution/MessageEntry.js.map +1 -1
- package/execution/MessageThread.d.ts +3 -4
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +25 -11
- package/execution/MessageThread.js.map +1 -1
- package/execution/PlanArtifactCard.d.ts +16 -10
- package/execution/PlanArtifactCard.d.ts.map +1 -1
- package/execution/PlanArtifactCard.js +31 -7
- package/execution/PlanArtifactCard.js.map +1 -1
- package/execution/PlanCompletionCard.d.ts +4 -4
- package/execution/PlanCompletionCard.d.ts.map +1 -1
- package/execution/PlanCompletionCard.js +4 -4
- package/execution/PlanCompletionCard.js.map +1 -1
- package/execution/PlanStreamingCard.d.ts +7 -2
- package/execution/PlanStreamingCard.d.ts.map +1 -1
- package/execution/PlanStreamingCard.js +11 -6
- package/execution/PlanStreamingCard.js.map +1 -1
- package/execution/index.d.ts +12 -2
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +6 -1
- package/execution/index.js.map +1 -1
- package/execution/use-build-from-plan-hotkey.d.ts +1 -1
- package/execution/use-build-from-plan-hotkey.js +1 -1
- package/execution/useArtifactContent.d.ts +6 -1
- package/execution/useArtifactContent.d.ts.map +1 -1
- package/execution/useArtifactContent.js +7 -2
- package/execution/useArtifactContent.js.map +1 -1
- package/execution/useArtifactCopy.d.ts +51 -0
- package/execution/useArtifactCopy.d.ts.map +1 -0
- package/execution/useArtifactCopy.js +75 -0
- package/execution/useArtifactCopy.js.map +1 -0
- package/execution/useArtifactDownload.d.ts.map +1 -1
- package/execution/useArtifactDownload.js +17 -5
- package/execution/useArtifactDownload.js.map +1 -1
- package/execution/useArtifactInspection.d.ts +85 -0
- package/execution/useArtifactInspection.d.ts.map +1 -0
- package/execution/useArtifactInspection.js +143 -0
- package/execution/useArtifactInspection.js.map +1 -0
- package/execution/useCreateAgentExecution.d.ts +16 -3
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +1 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +3 -3
- package/index.js.map +1 -1
- package/internal/MermaidDiagram.d.ts +47 -0
- package/internal/MermaidDiagram.d.ts.map +1 -0
- package/internal/MermaidDiagram.js +118 -0
- package/internal/MermaidDiagram.js.map +1 -0
- package/internal/file-tree/FileTreeNode.d.ts.map +1 -1
- package/internal/file-tree/FileTreeNode.js +2 -2
- package/internal/file-tree/FileTreeNode.js.map +1 -1
- package/internal/markdown-components.d.ts.map +1 -1
- package/internal/markdown-components.js +34 -0
- package/internal/markdown-components.js.map +1 -1
- package/internal/mermaid-loader.d.ts +26 -0
- package/internal/mermaid-loader.d.ts.map +1 -0
- package/internal/mermaid-loader.js +34 -0
- package/internal/mermaid-loader.js.map +1 -0
- package/internal/mermaid-theme-css.d.ts +41 -0
- package/internal/mermaid-theme-css.d.ts.map +1 -0
- package/internal/mermaid-theme-css.js +179 -0
- package/internal/mermaid-theme-css.js.map +1 -0
- package/internal/slug.d.ts +9 -5
- package/internal/slug.d.ts.map +1 -1
- package/internal/slug.js +10 -6
- package/internal/slug.js.map +1 -1
- package/library/detect-plan-artifact.d.ts +27 -9
- package/library/detect-plan-artifact.d.ts.map +1 -1
- package/library/detect-plan-artifact.js +26 -6
- package/library/detect-plan-artifact.js.map +1 -1
- package/library/index.d.ts +1 -1
- package/library/index.d.ts.map +1 -1
- package/library/index.js +1 -1
- package/library/index.js.map +1 -1
- package/package.json +5 -4
- package/session/PlanEditor.js +1 -1
- package/session/PlanEditor.js.map +1 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +122 -30
- package/session/SessionViewer.js.map +1 -1
- package/session/artifact-document.d.ts +13 -0
- package/session/artifact-document.d.ts.map +1 -0
- package/session/artifact-document.js +22 -0
- package/session/artifact-document.js.map +1 -0
- package/session/facets/ArtifactsTab.d.ts +30 -4
- package/session/facets/ArtifactsTab.d.ts.map +1 -1
- package/session/facets/ArtifactsTab.js +32 -10
- package/session/facets/ArtifactsTab.js.map +1 -1
- package/session/index.d.ts +2 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js +4 -1
- package/session/index.js.map +1 -1
- package/session/plan-document.d.ts +14 -2
- package/session/plan-document.d.ts.map +1 -1
- package/session/plan-document.js +14 -3
- package/session/plan-document.js.map +1 -1
- package/session/usePlanDraft.d.ts +1 -1
- package/session/useSessionArtifacts.d.ts +17 -0
- package/session/useSessionArtifacts.d.ts.map +1 -1
- package/session/useSessionArtifacts.js +16 -5
- package/session/useSessionArtifacts.js.map +1 -1
- package/session/useSessionConversation.d.ts +26 -3
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +51 -6
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +1 -0
- package/session/useSessionPageFlow.js.map +1 -1
- package/session/useSessionPanel.d.ts +20 -0
- package/session/useSessionPanel.d.ts.map +1 -1
- package/session/useSessionPanel.js +13 -0
- package/session/useSessionPanel.js.map +1 -1
- package/session/useSessionRailViews.d.ts +13 -1
- package/session/useSessionRailViews.d.ts.map +1 -1
- package/session/useSessionRailViews.js +5 -3
- package/session/useSessionRailViews.js.map +1 -1
- package/src/composer/SessionComposer.tsx +76 -7
- package/src/composer/__tests__/SessionComposer-editing.test.tsx +128 -0
- package/src/execution/ArtifactApplyButton.tsx +79 -0
- package/src/execution/ArtifactContentBody.tsx +255 -0
- package/src/execution/ArtifactDocument.tsx +344 -0
- package/src/execution/ArtifactPreviewModal.tsx +49 -415
- package/src/execution/ArtifactRow.tsx +165 -0
- package/src/execution/ArtifactsWidget.tsx +24 -33
- package/src/execution/FileChangeProgressBar.tsx +121 -0
- package/src/execution/FileChangesView.tsx +18 -4
- package/src/execution/FileReviewAtoms.tsx +89 -0
- package/src/execution/FileReviewCard.tsx +57 -90
- package/src/execution/MessageEntry.tsx +0 -62
- package/src/execution/MessageThread.tsx +29 -15
- package/src/execution/PlanArtifactCard.tsx +130 -52
- package/src/execution/PlanCompletionCard.tsx +5 -5
- package/src/execution/PlanStreamingCard.tsx +15 -11
- package/src/execution/__tests__/ArtifactContentBody.test.tsx +131 -0
- package/src/execution/__tests__/ArtifactDocument.test.tsx +100 -0
- package/src/execution/__tests__/ArtifactPreviewModal.test.tsx +6 -6
- package/src/execution/__tests__/ArtifactRow.test.tsx +139 -0
- package/src/execution/__tests__/ArtifactsWidget.test.tsx +78 -0
- package/src/execution/__tests__/FileChangeProgressBar.test.tsx +125 -0
- package/src/execution/__tests__/FileReviewCard.test.tsx +66 -7
- package/src/execution/__tests__/MessageThread.test.tsx +1 -1
- package/src/execution/__tests__/PlanArtifactCard.test.tsx +52 -18
- package/src/execution/__tests__/PlanCompletionCard.test.tsx +2 -2
- package/src/execution/__tests__/PlanStreamingCard.test.tsx +11 -4
- package/src/execution/__tests__/buildThreadItems-plan-completion.test.ts +104 -8
- package/src/execution/__tests__/message-entry.test.tsx +56 -39
- package/src/execution/__tests__/useArtifactCopy.test.tsx +90 -0
- package/src/execution/__tests__/useArtifactDownload.test.tsx +2 -0
- package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
- package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
- package/src/execution/index.ts +20 -2
- package/src/execution/use-build-from-plan-hotkey.ts +1 -1
- package/src/execution/useArtifactContent.ts +7 -0
- package/src/execution/useArtifactCopy.ts +109 -0
- package/src/execution/useArtifactDownload.ts +17 -5
- package/src/execution/useArtifactInspection.ts +252 -0
- package/src/execution/useCreateAgentExecution.ts +17 -3
- package/src/index.ts +17 -2
- package/src/internal/MermaidDiagram.tsx +164 -0
- package/src/internal/__tests__/a11y/mermaid-theme.a11y.test.tsx +122 -0
- package/src/internal/__tests__/markdown-components.test.tsx +93 -1
- package/src/internal/__tests__/mermaid-diagram.test.tsx +282 -0
- package/src/internal/file-tree/FileTreeNode.tsx +13 -2
- package/src/internal/markdown-components.tsx +36 -1
- package/src/internal/mermaid-loader.ts +44 -0
- package/src/internal/mermaid-theme-css.ts +178 -0
- package/src/internal/slug.ts +10 -6
- package/src/library/__tests__/detect-plan-artifact.test.ts +21 -0
- package/src/library/detect-plan-artifact.ts +32 -10
- package/src/library/index.ts +2 -0
- package/src/session/PlanEditor.tsx +1 -1
- package/src/session/SessionViewer.tsx +227 -51
- package/src/session/__tests__/PlanEditor.test.tsx +4 -4
- package/src/session/__tests__/PlanStreamingDocument.test.tsx +1 -1
- package/src/session/__tests__/SessionViewer-artifactTabs.test.tsx +210 -0
- package/src/session/__tests__/SessionViewer-editResubmit.test.tsx +233 -0
- package/src/session/__tests__/SessionViewer-fileReviewDock.test.tsx +57 -0
- package/src/session/__tests__/useSessionArtifacts.test.ts +57 -0
- package/src/session/__tests__/useSessionConversation.test.tsx +216 -1
- package/src/session/__tests__/useSessionPanel.test.ts +70 -1
- package/src/session/artifact-document.ts +23 -0
- package/src/session/facets/ArtifactsTab.tsx +66 -18
- package/src/session/facets/__tests__/ArtifactsTab.test.tsx +111 -47
- package/src/session/index.ts +5 -1
- package/src/session/plan-document.ts +14 -3
- package/src/session/usePlanDraft.ts +1 -1
- package/src/session/useSessionArtifacts.ts +16 -5
- package/src/session/useSessionConversation.ts +80 -10
- package/src/session/useSessionPageFlow.ts +1 -0
- package/src/session/useSessionPanel.ts +41 -0
- package/src/session/useSessionRailViews.tsx +21 -1
- package/src/workspace/EditorTabs.tsx +38 -9
- package/src/workspace/ExplorerTree.tsx +10 -4
- package/src/workspace/FileViewer.tsx +27 -6
- package/src/workspace/WorkspaceContentSearch.tsx +44 -20
- package/src/workspace/WorkspaceEditor.tsx +19 -16
- package/src/workspace/WorkspaceFileSearch.tsx +51 -28
- package/src/workspace/WorkspaceSurface.tsx +106 -48
- package/src/workspace/__tests__/EditorTabs.test.tsx +4 -2
- package/src/workspace/__tests__/ExplorerTree.test.tsx +52 -0
- package/src/workspace/__tests__/FileViewer.test.tsx +18 -0
- package/src/workspace/__tests__/WorkspaceContentSearch.test.tsx +38 -5
- package/src/workspace/__tests__/WorkspaceEditor-initialPanel.test.tsx +19 -0
- package/src/workspace/__tests__/WorkspaceFileSearch.test.tsx +27 -6
- package/src/workspace/__tests__/WorkspaceSurface.test.tsx +70 -0
- package/src/workspace/__tests__/a11y/harness.tsx +140 -0
- package/src/workspace/__tests__/a11y/mock-capabilities.ts +138 -0
- package/src/workspace/__tests__/a11y/workspace-editor.a11y.test.tsx +65 -0
- package/src/workspace/__tests__/a11y/workspace-explorer-search.a11y.test.tsx +112 -0
- package/src/workspace/__tests__/a11y/workspace-viewer-tabs.a11y.test.tsx +69 -0
- package/styles.css +1 -1
- package/workspace/EditorTabs.d.ts +21 -3
- package/workspace/EditorTabs.d.ts.map +1 -1
- package/workspace/EditorTabs.js +18 -6
- package/workspace/EditorTabs.js.map +1 -1
- package/workspace/ExplorerTree.js +8 -4
- package/workspace/ExplorerTree.js.map +1 -1
- package/workspace/FileViewer.d.ts.map +1 -1
- package/workspace/FileViewer.js +19 -8
- package/workspace/FileViewer.js.map +1 -1
- package/workspace/WorkspaceContentSearch.d.ts.map +1 -1
- package/workspace/WorkspaceContentSearch.js +23 -2
- package/workspace/WorkspaceContentSearch.js.map +1 -1
- package/workspace/WorkspaceEditor.d.ts.map +1 -1
- package/workspace/WorkspaceEditor.js +8 -10
- package/workspace/WorkspaceEditor.js.map +1 -1
- package/workspace/WorkspaceFileSearch.d.ts.map +1 -1
- package/workspace/WorkspaceFileSearch.js +23 -3
- package/workspace/WorkspaceFileSearch.js.map +1 -1
- package/workspace/WorkspaceSurface.d.ts.map +1 -1
- package/workspace/WorkspaceSurface.js +54 -18
- package/workspace/WorkspaceSurface.js.map +1 -1
- package/execution/ArtifactCard.d.ts +0 -81
- package/execution/ArtifactCard.d.ts.map +0 -1
- package/execution/ArtifactCard.js +0 -130
- package/execution/ArtifactCard.js.map +0 -1
- package/src/execution/ArtifactCard.tsx +0 -320
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, act, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
5
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
6
|
+
import {
|
|
7
|
+
ExecutionArtifactKind,
|
|
8
|
+
ExecutionPhase,
|
|
9
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
10
|
+
import { ARTIFACT_DOCUMENT_ENTRY_ID } from "../artifact-document";
|
|
11
|
+
import { artifactKey, type SessionArtifactEntry } from "../useSessionArtifacts";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Wiring-contract test for the Artifacts facet's open/pin gestures. The
|
|
15
|
+
// `onOpenArtifact` (single-click preview) and `onActivateArtifact` (double-click
|
|
16
|
+
// pin) callbacks must thread from SessionViewer, through the intermediate
|
|
17
|
+
// SessionPanelRegion, into useSessionRailViews' <ArtifactsTab>, and — when
|
|
18
|
+
// invoked — land as an artifact virtual-document tab on the editors store.
|
|
19
|
+
// WorkspaceSurface renders as a prop-capturing probe; the store + panel-region
|
|
20
|
+
// wire between them is the subject under test. Tab MECHANICS (preview-slot
|
|
21
|
+
// reuse, pin promotion) live in workspace-editors-store.test.ts and are not
|
|
22
|
+
// re-tested here.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
type CapturedProps = Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
const threadProps: CapturedProps[] = [];
|
|
28
|
+
vi.mock("../../execution/MessageThread", () => ({
|
|
29
|
+
MessageThread: (props: CapturedProps) => {
|
|
30
|
+
threadProps.push(props);
|
|
31
|
+
return <div data-testid="thread-probe" />;
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const surfaceProps: CapturedProps[] = [];
|
|
36
|
+
vi.mock("../../workspace/WorkspaceSurface", () => ({
|
|
37
|
+
WorkspaceSurface: (props: CapturedProps) => {
|
|
38
|
+
surfaceProps.push(props);
|
|
39
|
+
return <div data-testid="surface-probe" />;
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock("../../composer", async (importOriginal) => {
|
|
44
|
+
const actual = await importOriginal<typeof import("../../composer")>();
|
|
45
|
+
return {
|
|
46
|
+
...actual,
|
|
47
|
+
SessionComposer: () => <div data-testid="composer-probe" />,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const artifact = create(ExecutionArtifactSchema, {
|
|
52
|
+
name: "agent.yaml",
|
|
53
|
+
kind: ExecutionArtifactKind.FILE,
|
|
54
|
+
sizeBytes: 512n,
|
|
55
|
+
sandboxPath: ".stigmer/agent.yaml",
|
|
56
|
+
storageKey: "artifacts/aex_1/agent.yaml",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const artifactExecution = create(AgentExecutionSchema, {
|
|
60
|
+
metadata: { id: "aex_1" },
|
|
61
|
+
status: {
|
|
62
|
+
phase: ExecutionPhase.EXECUTION_COMPLETED,
|
|
63
|
+
artifacts: [artifact],
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const entry: SessionArtifactEntry = {
|
|
68
|
+
artifact,
|
|
69
|
+
executionId: "aex_1",
|
|
70
|
+
isTerminal: true,
|
|
71
|
+
hasNameCollision: false,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const stubWorkspace = {
|
|
75
|
+
entries: [],
|
|
76
|
+
hasEntries: false,
|
|
77
|
+
toInput: vi.fn().mockReturnValue([]),
|
|
78
|
+
addGitRepo: vi.fn(),
|
|
79
|
+
addLocalPath: vi.fn(),
|
|
80
|
+
remove: vi.fn(),
|
|
81
|
+
clear: vi.fn(),
|
|
82
|
+
clearLocal: vi.fn(),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const stubConv = {
|
|
86
|
+
session: { spec: {} },
|
|
87
|
+
isLoading: false,
|
|
88
|
+
loadError: null,
|
|
89
|
+
completedExecutions: [artifactExecution],
|
|
90
|
+
activeStreamExecution: null,
|
|
91
|
+
pendingUserMessage: null,
|
|
92
|
+
workspaceEntries: [],
|
|
93
|
+
fileChangeSets: [],
|
|
94
|
+
submitFileDecision: vi.fn(),
|
|
95
|
+
submittingFileDecisionKeys: new Set<string>(),
|
|
96
|
+
fileDecisionErrors: new Map<string, Error>(),
|
|
97
|
+
sendError: null,
|
|
98
|
+
stopError: null,
|
|
99
|
+
streamError: null,
|
|
100
|
+
reconnectStream: vi.fn(),
|
|
101
|
+
submittingApprovalIds: new Set<string>(),
|
|
102
|
+
approvalErrors: new Map<string, Error>(),
|
|
103
|
+
isStoppable: false,
|
|
104
|
+
isStopping: false,
|
|
105
|
+
isSending: false,
|
|
106
|
+
canSendFollowUp: true,
|
|
107
|
+
isReconnecting: false,
|
|
108
|
+
connectTimedOut: false,
|
|
109
|
+
isSlow: false,
|
|
110
|
+
stop: vi.fn(),
|
|
111
|
+
retryLastSend: vi.fn(),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const stubSessionPageFlow = {
|
|
115
|
+
conv: stubConv,
|
|
116
|
+
harness: "native" as const,
|
|
117
|
+
executionTarget: undefined,
|
|
118
|
+
model: [undefined, vi.fn()] as const,
|
|
119
|
+
interactionMode: ["agent" as const, vi.fn()] as const,
|
|
120
|
+
agentRef: { org: "acme", slug: "support-bot" },
|
|
121
|
+
setAgentRef: vi.fn(),
|
|
122
|
+
resolution: null,
|
|
123
|
+
setResolution: vi.fn(),
|
|
124
|
+
isDefaultAgent: false,
|
|
125
|
+
mcpServerUsages: [],
|
|
126
|
+
setMcpServerUsages: vi.fn(),
|
|
127
|
+
skillRefs: [],
|
|
128
|
+
setSkillRefs: vi.fn(),
|
|
129
|
+
workspace: stubWorkspace,
|
|
130
|
+
sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
|
|
131
|
+
autoApproveAll: false,
|
|
132
|
+
setAutoApproveAll: vi.fn(),
|
|
133
|
+
handleSubmit: vi.fn(),
|
|
134
|
+
submitError: null as Error | null,
|
|
135
|
+
displayExecution: artifactExecution,
|
|
136
|
+
allExecutions: [artifactExecution],
|
|
137
|
+
sandboxWorkspaceRoot: "/home/daytona/workspace",
|
|
138
|
+
};
|
|
139
|
+
vi.mock("../useSessionPageFlow", () => ({
|
|
140
|
+
useSessionPageFlow: () => stubSessionPageFlow,
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
vi.mock("../../hooks", () => ({
|
|
144
|
+
useStigmer: () => ({
|
|
145
|
+
agentExecution: {
|
|
146
|
+
uploadAttachment: vi.fn(),
|
|
147
|
+
getArtifactContent: vi.fn(),
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
import { SessionViewer } from "../SessionViewer";
|
|
153
|
+
|
|
154
|
+
/** The latest editors array the (mocked) WorkspaceSurface received. */
|
|
155
|
+
function latestEditors(): ReadonlyArray<{
|
|
156
|
+
entryId: string;
|
|
157
|
+
path: string;
|
|
158
|
+
preview: boolean;
|
|
159
|
+
}> {
|
|
160
|
+
return (surfaceProps.at(-1)?.editors ?? []) as never;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Read a callback off the rail-view <ArtifactsTab> element the surface received. */
|
|
164
|
+
function artifactsTabCallback(
|
|
165
|
+
name: "onOpenArtifact" | "onActivateArtifact",
|
|
166
|
+
): (entry: SessionArtifactEntry) => void {
|
|
167
|
+
const views = surfaceProps.at(-1)?.extraViews as
|
|
168
|
+
| ReadonlyArray<{ id: string; content: { props: CapturedProps } }>
|
|
169
|
+
| undefined;
|
|
170
|
+
const artifactsView = views?.find((v) => v.id === "artifacts");
|
|
171
|
+
if (!artifactsView) throw new Error("Artifacts rail view not present");
|
|
172
|
+
return artifactsView.content.props[name] as (e: SessionArtifactEntry) => void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
beforeEach(() => {
|
|
176
|
+
threadProps.length = 0;
|
|
177
|
+
surfaceProps.length = 0;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
afterEach(() => {
|
|
181
|
+
cleanup();
|
|
182
|
+
vi.clearAllMocks();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("SessionViewer — Artifacts facet open/pin wiring", () => {
|
|
186
|
+
it("threads onOpenArtifact + onActivateArtifact into the Artifacts rail view", () => {
|
|
187
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
188
|
+
fireEvent.click(screen.getByRole("button", { name: "Show panel" }));
|
|
189
|
+
|
|
190
|
+
expect(typeof artifactsTabCallback("onOpenArtifact")).toBe("function");
|
|
191
|
+
expect(typeof artifactsTabCallback("onActivateArtifact")).toBe("function");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("opening an artifact lands a PREVIEW tab; activating it pins the tab", () => {
|
|
195
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
196
|
+
fireEvent.click(screen.getByRole("button", { name: "Show panel" }));
|
|
197
|
+
|
|
198
|
+
// Single-click preview → one preview tab keyed by the artifact identity.
|
|
199
|
+
act(() => artifactsTabCallback("onOpenArtifact")(entry));
|
|
200
|
+
expect(latestEditors()).toEqual([
|
|
201
|
+
{ entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: artifactKey(artifact), preview: true },
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
// Double-click pin → the same tab, now persistent.
|
|
205
|
+
act(() => artifactsTabCallback("onActivateArtifact")(entry));
|
|
206
|
+
expect(latestEditors()).toEqual([
|
|
207
|
+
{ entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: artifactKey(artifact), preview: false },
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { render, cleanup, act } from "@testing-library/react";
|
|
3
|
+
import { forwardRef, useImperativeHandle } from "react";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Wiring-contract test for edit-and-resubmit (stigmer/stigmer#181): the
|
|
7
|
+
// onEditMessage SessionViewer hands to MessageThread stops the in-flight
|
|
8
|
+
// turn, prefills the composer, and enters editing mode; submitting while
|
|
9
|
+
// editing attaches the superseded execution's id to the submit context;
|
|
10
|
+
// cancelling drops the link so the next send appends normally. MessageThread
|
|
11
|
+
// and SessionComposer render as prop-capturing probes; the viewer's editing
|
|
12
|
+
// state machine is the subject under test.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
type CapturedProps = Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
const threadProps: CapturedProps[] = [];
|
|
18
|
+
vi.mock("../../execution/MessageThread", () => ({
|
|
19
|
+
MessageThread: (props: CapturedProps) => {
|
|
20
|
+
threadProps.push(props);
|
|
21
|
+
return <div data-testid="thread-probe" />;
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("../../workspace/WorkspaceSurface", () => ({
|
|
26
|
+
WorkspaceSurface: () => <div data-testid="surface-probe" />,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const composerProps: CapturedProps[] = [];
|
|
30
|
+
const composerSetMessage = vi.fn();
|
|
31
|
+
const composerFocus = vi.fn();
|
|
32
|
+
vi.mock("../../composer", async (importOriginal) => {
|
|
33
|
+
const actual = await importOriginal<typeof import("../../composer")>();
|
|
34
|
+
const ComposerProbe = forwardRef<unknown, CapturedProps>(
|
|
35
|
+
function ComposerProbe(props, ref) {
|
|
36
|
+
useImperativeHandle(ref, () => ({
|
|
37
|
+
setMessage: composerSetMessage,
|
|
38
|
+
focus: composerFocus,
|
|
39
|
+
submit: vi.fn(),
|
|
40
|
+
}));
|
|
41
|
+
composerProps.push(props);
|
|
42
|
+
return <div data-testid="composer-probe" />;
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
return { ...actual, SessionComposer: ComposerProbe };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const stubConv = {
|
|
49
|
+
session: { spec: {} },
|
|
50
|
+
isLoading: false,
|
|
51
|
+
loadError: null,
|
|
52
|
+
completedExecutions: [],
|
|
53
|
+
activeStreamExecution: { metadata: { id: "exec-old" } },
|
|
54
|
+
pendingUserMessage: null,
|
|
55
|
+
workspaceEntries: [],
|
|
56
|
+
fileChangeSets: [],
|
|
57
|
+
submitFileDecision: vi.fn(),
|
|
58
|
+
submittingFileDecisionKeys: new Set<string>(),
|
|
59
|
+
fileDecisionErrors: new Map<string, Error>(),
|
|
60
|
+
sendError: null,
|
|
61
|
+
stopError: null,
|
|
62
|
+
streamError: null,
|
|
63
|
+
reconnectStream: vi.fn(),
|
|
64
|
+
submittingApprovalIds: new Set<string>(),
|
|
65
|
+
approvalErrors: new Map<string, Error>(),
|
|
66
|
+
isStoppable: true,
|
|
67
|
+
isStopping: false,
|
|
68
|
+
isSending: false,
|
|
69
|
+
canSendFollowUp: false,
|
|
70
|
+
isReconnecting: false,
|
|
71
|
+
connectTimedOut: false,
|
|
72
|
+
isSlow: false,
|
|
73
|
+
stop: vi.fn().mockResolvedValue(undefined),
|
|
74
|
+
retryLastSend: vi.fn(),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const stubSessionPageFlow = {
|
|
78
|
+
conv: stubConv,
|
|
79
|
+
harness: "native" as const,
|
|
80
|
+
executionTarget: undefined,
|
|
81
|
+
model: [undefined, vi.fn()] as const,
|
|
82
|
+
interactionMode: ["agent" as const, vi.fn()] as const,
|
|
83
|
+
agentRef: { org: "acme", slug: "support-bot" },
|
|
84
|
+
setAgentRef: vi.fn(),
|
|
85
|
+
resolution: null,
|
|
86
|
+
setResolution: vi.fn(),
|
|
87
|
+
isDefaultAgent: false,
|
|
88
|
+
mcpServerUsages: [],
|
|
89
|
+
setMcpServerUsages: vi.fn(),
|
|
90
|
+
skillRefs: [],
|
|
91
|
+
setSkillRefs: vi.fn(),
|
|
92
|
+
workspace: {
|
|
93
|
+
entries: [],
|
|
94
|
+
hasEntries: false,
|
|
95
|
+
toInput: vi.fn().mockReturnValue([]),
|
|
96
|
+
addGitRepo: vi.fn(),
|
|
97
|
+
addLocalPath: vi.fn(),
|
|
98
|
+
remove: vi.fn(),
|
|
99
|
+
clear: vi.fn(),
|
|
100
|
+
clearLocal: vi.fn(),
|
|
101
|
+
},
|
|
102
|
+
sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
|
|
103
|
+
autoApproveAll: false,
|
|
104
|
+
setAutoApproveAll: vi.fn(),
|
|
105
|
+
handleSubmit: vi.fn(),
|
|
106
|
+
submitError: null as Error | null,
|
|
107
|
+
displayExecution: null,
|
|
108
|
+
allExecutions: [],
|
|
109
|
+
sandboxWorkspaceRoot: "/home/daytona/workspace",
|
|
110
|
+
};
|
|
111
|
+
vi.mock("../useSessionPageFlow", () => ({
|
|
112
|
+
useSessionPageFlow: () => stubSessionPageFlow,
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
vi.mock("../../hooks", () => ({
|
|
116
|
+
useStigmer: () => ({
|
|
117
|
+
agentExecution: {
|
|
118
|
+
uploadAttachment: vi.fn(),
|
|
119
|
+
getArtifactContent: vi.fn(),
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
import { SessionViewer } from "../SessionViewer";
|
|
125
|
+
|
|
126
|
+
/** The onEditMessage handler threaded down to the (mocked) MessageThread. */
|
|
127
|
+
function capturedOnEditMessage(): ((text: string) => void) | undefined {
|
|
128
|
+
return threadProps.at(-1)?.onEditMessage as
|
|
129
|
+
| ((text: string) => void)
|
|
130
|
+
| undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The latest props the (mocked) SessionComposer received. */
|
|
134
|
+
function latestComposerProps(): CapturedProps {
|
|
135
|
+
return composerProps.at(-1) ?? {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function submitViaComposer(message: string): void {
|
|
139
|
+
const onSubmit = latestComposerProps().onSubmit as (
|
|
140
|
+
m: string,
|
|
141
|
+
model?: string,
|
|
142
|
+
context?: Record<string, unknown>,
|
|
143
|
+
) => void;
|
|
144
|
+
onSubmit(message, undefined, { interactionMode: "agent" });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
threadProps.length = 0;
|
|
149
|
+
composerProps.length = 0;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
afterEach(() => {
|
|
153
|
+
cleanup();
|
|
154
|
+
vi.clearAllMocks();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("SessionViewer — edit-and-resubmit wiring", () => {
|
|
158
|
+
it("wires onEditMessage to the thread while the turn is stoppable", () => {
|
|
159
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
160
|
+
expect(typeof capturedOnEditMessage()).toBe("function");
|
|
161
|
+
expect(latestComposerProps().isEditing).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("Edit stops the turn, prefills the composer, and enters editing mode", () => {
|
|
165
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
166
|
+
|
|
167
|
+
act(() => {
|
|
168
|
+
capturedOnEditMessage()!("original message");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(stubConv.stop).toHaveBeenCalledTimes(1);
|
|
172
|
+
expect(composerSetMessage).toHaveBeenCalledWith("original message");
|
|
173
|
+
expect(composerFocus).toHaveBeenCalledTimes(1);
|
|
174
|
+
expect(latestComposerProps().isEditing).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("submitting while editing attaches the supersede link and exits editing", () => {
|
|
178
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
179
|
+
|
|
180
|
+
act(() => {
|
|
181
|
+
capturedOnEditMessage()!("original message");
|
|
182
|
+
});
|
|
183
|
+
act(() => {
|
|
184
|
+
submitViaComposer("corrected message");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(stubSessionPageFlow.handleSubmit).toHaveBeenCalledTimes(1);
|
|
188
|
+
const [message, , context] =
|
|
189
|
+
stubSessionPageFlow.handleSubmit.mock.calls[0];
|
|
190
|
+
expect(message).toBe("corrected message");
|
|
191
|
+
expect(context).toMatchObject({
|
|
192
|
+
supersedesExecutionId: "exec-old",
|
|
193
|
+
// Composer-provided context fields survive the merge.
|
|
194
|
+
interactionMode: "agent",
|
|
195
|
+
});
|
|
196
|
+
expect(latestComposerProps().isEditing).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("cancelling the edit clears the composer and drops the supersede link", () => {
|
|
200
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
201
|
+
|
|
202
|
+
act(() => {
|
|
203
|
+
capturedOnEditMessage()!("original message");
|
|
204
|
+
});
|
|
205
|
+
expect(latestComposerProps().isEditing).toBe(true);
|
|
206
|
+
|
|
207
|
+
act(() => {
|
|
208
|
+
(latestComposerProps().onCancelEdit as () => void)();
|
|
209
|
+
});
|
|
210
|
+
expect(latestComposerProps().isEditing).toBe(false);
|
|
211
|
+
expect(composerSetMessage).toHaveBeenLastCalledWith("");
|
|
212
|
+
|
|
213
|
+
// A subsequent unrelated send must NOT carry the supersede link.
|
|
214
|
+
act(() => {
|
|
215
|
+
submitViaComposer("unrelated new message");
|
|
216
|
+
});
|
|
217
|
+
const [, , context] = stubSessionPageFlow.handleSubmit.mock.calls[0];
|
|
218
|
+
expect(context).not.toHaveProperty("supersedesExecutionId");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("ordinary sends pass the composer context through untouched", () => {
|
|
222
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
223
|
+
|
|
224
|
+
act(() => {
|
|
225
|
+
submitViaComposer("plain message");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const [message, , context] =
|
|
229
|
+
stubSessionPageFlow.handleSubmit.mock.calls[0];
|
|
230
|
+
expect(message).toBe("plain message");
|
|
231
|
+
expect(context).toEqual({ interactionMode: "agent" });
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -3,6 +3,8 @@ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
|
|
3
3
|
import { create } from "@bufbuild/protobuf";
|
|
4
4
|
import {
|
|
5
5
|
CapturedFileChangeSchema,
|
|
6
|
+
FileChangeProgressSchema,
|
|
7
|
+
FileChangeProgressEntrySchema,
|
|
6
8
|
FileChangeSetSchema,
|
|
7
9
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
8
10
|
import { FileContentSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
@@ -70,6 +72,31 @@ function pendingChangeSet(id: string) {
|
|
|
70
72
|
});
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
function progressSnapshot() {
|
|
76
|
+
return create(FileChangeProgressSchema, {
|
|
77
|
+
changeSetId: "cs-1",
|
|
78
|
+
filesChanged: 2,
|
|
79
|
+
linesAdded: 4,
|
|
80
|
+
linesRemoved: 1,
|
|
81
|
+
entries: [
|
|
82
|
+
create(FileChangeProgressEntrySchema, {
|
|
83
|
+
pathAfter: "a.ts",
|
|
84
|
+
kind: FileChangeKind.ADD,
|
|
85
|
+
linesAdded: 4,
|
|
86
|
+
linesRemoved: 0,
|
|
87
|
+
}),
|
|
88
|
+
create(FileChangeProgressEntrySchema, {
|
|
89
|
+
pathBefore: "b.ts",
|
|
90
|
+
pathAfter: "b.ts",
|
|
91
|
+
kind: FileChangeKind.MODIFY,
|
|
92
|
+
linesAdded: 0,
|
|
93
|
+
linesRemoved: 1,
|
|
94
|
+
}),
|
|
95
|
+
],
|
|
96
|
+
capturedAt: "2026-07-05T00:00:00Z",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
73
100
|
const stubConv = {
|
|
74
101
|
session: { spec: {} },
|
|
75
102
|
isLoading: false,
|
|
@@ -92,6 +119,7 @@ const stubConv = {
|
|
|
92
119
|
submitApproval: vi.fn(),
|
|
93
120
|
submittingApprovalIds: new Set<string>(),
|
|
94
121
|
fileChangeSets: [] as ReturnType<typeof pendingChangeSet>[],
|
|
122
|
+
fileChangeProgress: undefined as ReturnType<typeof progressSnapshot> | undefined,
|
|
95
123
|
submitFileDecision: vi.fn(),
|
|
96
124
|
submittingFileDecisionKeys: new Set<string>(),
|
|
97
125
|
fileDecisionErrors: new Map<string, Error>(),
|
|
@@ -144,6 +172,7 @@ import { SessionViewer } from "../SessionViewer";
|
|
|
144
172
|
beforeEach(() => {
|
|
145
173
|
threadProps.length = 0;
|
|
146
174
|
stubConv.fileChangeSets = [];
|
|
175
|
+
stubConv.fileChangeProgress = undefined;
|
|
147
176
|
stubConv.submitFileDecision = vi.fn();
|
|
148
177
|
});
|
|
149
178
|
|
|
@@ -189,6 +218,34 @@ describe("SessionViewer — file-review dock wiring", () => {
|
|
|
189
218
|
);
|
|
190
219
|
});
|
|
191
220
|
|
|
221
|
+
it("renders no progress bar when no turn is capturing", () => {
|
|
222
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
223
|
+
expect(
|
|
224
|
+
document.querySelector('[data-cursor-target="file-change-progress-bar"]'),
|
|
225
|
+
).toBeNull();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("shows the mid-run progress strip above the composer while a turn is capturing", () => {
|
|
229
|
+
// Progress is present (CAPTURING) and the dock is empty — the two are
|
|
230
|
+
// mutually exclusive per turn, so the strip stands in for the dock.
|
|
231
|
+
stubConv.fileChangeProgress = progressSnapshot();
|
|
232
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
233
|
+
|
|
234
|
+
const bar = document.querySelector('[data-cursor-target="file-change-progress-bar"]');
|
|
235
|
+
expect(bar).toBeTruthy();
|
|
236
|
+
expect(screen.getByText(/2 files changing/)).toBeTruthy();
|
|
237
|
+
expect(
|
|
238
|
+
document.querySelector('[data-cursor-target="file-review-dock"]'),
|
|
239
|
+
).toBeNull();
|
|
240
|
+
|
|
241
|
+
// Pinned in the fixed strip, ordered before the composer input.
|
|
242
|
+
const composer = screen.getByTestId("composer-probe");
|
|
243
|
+
expect(bar!.parentElement).toBe(composer.parentElement);
|
|
244
|
+
expect(
|
|
245
|
+
bar!.compareDocumentPosition(composer) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
246
|
+
).toBeTruthy();
|
|
247
|
+
});
|
|
248
|
+
|
|
192
249
|
it("gives the thread records-only file-review props (no decision callbacks)", () => {
|
|
193
250
|
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
194
251
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { renderHook, cleanup } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
5
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
6
|
+
import {
|
|
7
|
+
ExecutionArtifactKind,
|
|
8
|
+
ExecutionPhase,
|
|
9
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
10
|
+
import { useSessionArtifacts, artifactKey } from "../useSessionArtifacts";
|
|
11
|
+
|
|
12
|
+
function artifact(opts: { name: string; sandboxPath?: string }) {
|
|
13
|
+
return create(ExecutionArtifactSchema, {
|
|
14
|
+
name: opts.name,
|
|
15
|
+
kind: ExecutionArtifactKind.FILE,
|
|
16
|
+
sizeBytes: 8n,
|
|
17
|
+
...(opts.sandboxPath ? { sandboxPath: opts.sandboxPath } : {}),
|
|
18
|
+
storageKey: `artifacts/aex/${opts.name}`,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
afterEach(cleanup);
|
|
23
|
+
|
|
24
|
+
describe("artifactKey", () => {
|
|
25
|
+
it("uses sandbox_path when present (its filesystem identity)", () => {
|
|
26
|
+
expect(artifactKey(artifact({ name: "a.md", sandboxPath: "/w/dir/a.md" }))).toBe(
|
|
27
|
+
"/w/dir/a.md",
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("falls back to name for artifacts without a sandbox path", () => {
|
|
32
|
+
expect(artifactKey(artifact({ name: "a.md" }))).toBe("a.md");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("is the key useSessionArtifacts dedups on (same key → latest wins)", () => {
|
|
36
|
+
const older = create(AgentExecutionSchema, {
|
|
37
|
+
metadata: { id: "aex_1" },
|
|
38
|
+
status: {
|
|
39
|
+
phase: ExecutionPhase.EXECUTION_COMPLETED,
|
|
40
|
+
artifacts: [artifact({ name: "a.md", sandboxPath: "/w/a.md" })],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const newer = create(AgentExecutionSchema, {
|
|
44
|
+
metadata: { id: "aex_2" },
|
|
45
|
+
status: {
|
|
46
|
+
phase: ExecutionPhase.EXECUTION_COMPLETED,
|
|
47
|
+
artifacts: [artifact({ name: "a.md", sandboxPath: "/w/a.md" })],
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const { result } = renderHook(() => useSessionArtifacts([older, newer]));
|
|
52
|
+
expect(result.current.artifacts).toHaveLength(1);
|
|
53
|
+
// The later execution's version wins for a shared key.
|
|
54
|
+
expect(result.current.artifacts[0].executionId).toBe("aex_2");
|
|
55
|
+
expect(artifactKey(result.current.artifacts[0].artifact)).toBe("/w/a.md");
|
|
56
|
+
});
|
|
57
|
+
});
|