@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,139 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
5
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
6
|
+
import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
7
|
+
import { StigmerContext } from "../../context";
|
|
8
|
+
import { ArtifactRow } from "../ArtifactRow";
|
|
9
|
+
|
|
10
|
+
function fileArtifact(name: string, sandboxPath = `.stigmer/${name}`) {
|
|
11
|
+
return create(ExecutionArtifactSchema, {
|
|
12
|
+
name,
|
|
13
|
+
kind: ExecutionArtifactKind.FILE,
|
|
14
|
+
sizeBytes: 2048n,
|
|
15
|
+
sandboxPath,
|
|
16
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dirArtifact(name: string) {
|
|
21
|
+
return create(ExecutionArtifactSchema, {
|
|
22
|
+
name,
|
|
23
|
+
kind: ExecutionArtifactKind.DIRECTORY,
|
|
24
|
+
sizeBytes: 4096n,
|
|
25
|
+
sandboxPath: `.stigmer/${name}`,
|
|
26
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const getArtifactDownloadUrl = vi.fn().mockResolvedValue({ downloadUrl: "" });
|
|
31
|
+
|
|
32
|
+
function createStigmerMock(): Stigmer {
|
|
33
|
+
return {
|
|
34
|
+
agentExecution: { getArtifactDownloadUrl },
|
|
35
|
+
} as unknown as Stigmer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderRow(props: Partial<Parameters<typeof ArtifactRow>[0]> = {}) {
|
|
39
|
+
const merged = {
|
|
40
|
+
artifact: fileArtifact("notes.md"),
|
|
41
|
+
executionId: "aex_1",
|
|
42
|
+
onOpen: vi.fn(),
|
|
43
|
+
...props,
|
|
44
|
+
} as Parameters<typeof ArtifactRow>[0];
|
|
45
|
+
return render(
|
|
46
|
+
<StigmerContext.Provider value={createStigmerMock()}>
|
|
47
|
+
<ul>
|
|
48
|
+
<ArtifactRow {...merged} />
|
|
49
|
+
</ul>
|
|
50
|
+
</StigmerContext.Provider>,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
cleanup();
|
|
56
|
+
getArtifactDownloadUrl.mockClear();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("ArtifactRow — presentation", () => {
|
|
60
|
+
it("renders a file-type icon, the name, and the formatted size", () => {
|
|
61
|
+
renderRow({ artifact: fileArtifact("notes.md") });
|
|
62
|
+
const open = screen.getByText("notes.md").closest("button")!;
|
|
63
|
+
expect(open.querySelector("svg")).toBeTruthy();
|
|
64
|
+
expect(screen.getByText("2.0 KB")).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("appends a trailing slash for a directory artifact", () => {
|
|
68
|
+
renderRow({ artifact: dirArtifact("return-policy") });
|
|
69
|
+
// The name span's own text is "return-policy" + "/".
|
|
70
|
+
expect(screen.getByText("return-policy/")).toBeTruthy();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("shows the parent-directory subtitle only on a name collision", () => {
|
|
74
|
+
const artifact = fileArtifact("agent.yaml", "/workspace/configs/agent.yaml");
|
|
75
|
+
const { rerender } = renderRow({ artifact, hasNameCollision: true });
|
|
76
|
+
expect(screen.getByText("configs/")).toBeTruthy();
|
|
77
|
+
|
|
78
|
+
rerender(
|
|
79
|
+
<StigmerContext.Provider value={createStigmerMock()}>
|
|
80
|
+
<ul>
|
|
81
|
+
<ArtifactRow artifact={artifact} executionId="aex_1" onOpen={vi.fn()} />
|
|
82
|
+
</ul>
|
|
83
|
+
</StigmerContext.Provider>,
|
|
84
|
+
);
|
|
85
|
+
expect(screen.queryByText("configs/")).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("ArtifactRow — nested-interactive avoidance (DD-22)", () => {
|
|
90
|
+
it("renders the Download control as a SIBLING of the open button, never nested", () => {
|
|
91
|
+
renderRow({ artifact: fileArtifact("notes.md") });
|
|
92
|
+
const open = screen.getByText("notes.md").closest("button")!;
|
|
93
|
+
const download = screen.getByLabelText("Download notes.md");
|
|
94
|
+
expect(download.tagName).toBe("BUTTON");
|
|
95
|
+
// A <button> inside a <button> is a WCAG 4.1.2 nested-interactive violation.
|
|
96
|
+
expect(open.contains(download)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("ArtifactRow — open / activate interaction (file-tree parity)", () => {
|
|
101
|
+
it("fires onOpen on a single click of the open target", () => {
|
|
102
|
+
const onOpen = vi.fn();
|
|
103
|
+
renderRow({ onOpen });
|
|
104
|
+
fireEvent.click(screen.getByText("notes.md"));
|
|
105
|
+
expect(onOpen).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("fires onActivate on a double-click when provided", () => {
|
|
109
|
+
const onActivate = vi.fn();
|
|
110
|
+
renderRow({ onActivate });
|
|
111
|
+
fireEvent.doubleClick(screen.getByText("notes.md"));
|
|
112
|
+
expect(onActivate).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("binds no double-click handler when onActivate is omitted (harmless)", () => {
|
|
116
|
+
const onOpen = vi.fn();
|
|
117
|
+
renderRow({ onOpen });
|
|
118
|
+
// With no onActivate, a dblclick fires nothing (onOpen only responds to click).
|
|
119
|
+
fireEvent.doubleClick(screen.getByText("notes.md"));
|
|
120
|
+
expect(onOpen).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("ArtifactRow — download", () => {
|
|
125
|
+
it("mints a download URL for the artifact's storage key on Download click", () => {
|
|
126
|
+
renderRow({ artifact: fileArtifact("notes.md") });
|
|
127
|
+
fireEvent.click(screen.getByLabelText("Download notes.md"));
|
|
128
|
+
expect(getArtifactDownloadUrl).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(getArtifactDownloadUrl.mock.calls[0][0]).toMatchObject({
|
|
130
|
+
executionId: "aex_1",
|
|
131
|
+
storageKey: "artifacts/aex_1/notes.md",
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("labels the Download control 'Download ZIP' for a directory", () => {
|
|
136
|
+
renderRow({ artifact: dirArtifact("return-policy") });
|
|
137
|
+
expect(screen.getByTitle("Download ZIP")).toBeTruthy();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
5
|
+
import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
6
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
7
|
+
import {
|
|
8
|
+
ExecutionArtifactKind,
|
|
9
|
+
ExecutionPhase,
|
|
10
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
11
|
+
import { StigmerContext } from "../../context";
|
|
12
|
+
import { ArtifactsWidget } from "../ArtifactsWidget";
|
|
13
|
+
|
|
14
|
+
function artifact(name: string) {
|
|
15
|
+
return create(ExecutionArtifactSchema, {
|
|
16
|
+
name,
|
|
17
|
+
kind: ExecutionArtifactKind.FILE,
|
|
18
|
+
sizeBytes: 1024n,
|
|
19
|
+
sandboxPath: `.stigmer/${name}`,
|
|
20
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const execution = create(AgentExecutionSchema, {
|
|
25
|
+
metadata: { id: "aex_1" },
|
|
26
|
+
status: {
|
|
27
|
+
phase: ExecutionPhase.EXECUTION_COMPLETED,
|
|
28
|
+
artifacts: [artifact("agent.yaml"), artifact("notes.md")],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** Modal content fetch — keep pending so nothing rejects during the test. */
|
|
33
|
+
function createStigmerMock(): Stigmer {
|
|
34
|
+
return {
|
|
35
|
+
agentExecution: {
|
|
36
|
+
getArtifactContent: vi.fn().mockReturnValue(new Promise(() => {})),
|
|
37
|
+
getArtifactDownloadUrl: vi.fn().mockResolvedValue({ downloadUrl: "" }),
|
|
38
|
+
},
|
|
39
|
+
} as unknown as Stigmer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderWidget(executions = [execution]) {
|
|
43
|
+
return render(
|
|
44
|
+
<StigmerContext.Provider value={createStigmerMock()}>
|
|
45
|
+
<ArtifactsWidget executions={executions} org="acme" />
|
|
46
|
+
</StigmerContext.Provider>,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
afterEach(cleanup);
|
|
51
|
+
|
|
52
|
+
describe("ArtifactsWidget — dense list (panel-less)", () => {
|
|
53
|
+
it("renders nothing when there are no artifacts", () => {
|
|
54
|
+
const { container } = renderWidget([]);
|
|
55
|
+
expect(container.firstChild).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("renders a heading, count, and one dense row per artifact (no ArtifactCard chrome)", () => {
|
|
59
|
+
renderWidget();
|
|
60
|
+
expect(screen.getByRole("heading", { name: "Artifacts" })).toBeTruthy();
|
|
61
|
+
// Count badge reflects the two deduped artifacts.
|
|
62
|
+
expect(screen.getByText("2")).toBeTruthy();
|
|
63
|
+
expect(screen.getAllByRole("listitem")).toHaveLength(2);
|
|
64
|
+
expect(screen.getByText("agent.yaml")).toBeTruthy();
|
|
65
|
+
expect(screen.getByText("notes.md")).toBeTruthy();
|
|
66
|
+
// The retired ArtifactCard rendered a "Preview" button and role="article".
|
|
67
|
+
expect(screen.queryByText("Preview")).toBeNull();
|
|
68
|
+
expect(screen.queryByRole("article")).toBeNull();
|
|
69
|
+
expect(screen.getByLabelText("Download agent.yaml")).toBeTruthy();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("opens the preview modal (the panel-less fallback) on a row click", () => {
|
|
73
|
+
renderWidget();
|
|
74
|
+
expect(document.querySelector("dialog")).toBeNull();
|
|
75
|
+
fireEvent.click(screen.getByText("notes.md"));
|
|
76
|
+
expect(document.querySelector("dialog")).toBeTruthy();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileChangeProgressBar — the live "N files changed so far" strip for a running
|
|
3
|
+
* turn (DD-32). These tests cover its own responsibilities: emptiness, the
|
|
4
|
+
* summary count + aggregate stat, the expandable per-file list with its cap
|
|
5
|
+
* overflow, zero-count hiding, and that only the count is an aria-live region.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
9
|
+
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
10
|
+
import { create } from "@bufbuild/protobuf";
|
|
11
|
+
import {
|
|
12
|
+
FileChangeProgressSchema,
|
|
13
|
+
FileChangeProgressEntrySchema,
|
|
14
|
+
type FileChangeProgress,
|
|
15
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
16
|
+
import { FileChangeKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
17
|
+
import { FileChangeProgressBar } from "../FileChangeProgressBar";
|
|
18
|
+
|
|
19
|
+
function entry(path: string, kind: FileChangeKind, added: number, removed: number) {
|
|
20
|
+
return create(FileChangeProgressEntrySchema, {
|
|
21
|
+
pathBefore: kind === FileChangeKind.ADD ? "" : path,
|
|
22
|
+
pathAfter: kind === FileChangeKind.DELETE ? "" : path,
|
|
23
|
+
kind,
|
|
24
|
+
linesAdded: added,
|
|
25
|
+
linesRemoved: removed,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function progress(overrides: Partial<FileChangeProgress> = {}): FileChangeProgress {
|
|
30
|
+
return create(FileChangeProgressSchema, {
|
|
31
|
+
changeSetId: "exec:0",
|
|
32
|
+
filesChanged: 2,
|
|
33
|
+
linesAdded: 7,
|
|
34
|
+
linesRemoved: 3,
|
|
35
|
+
entries: [
|
|
36
|
+
entry("src/a.ts", FileChangeKind.ADD, 5, 0),
|
|
37
|
+
entry("src/b.ts", FileChangeKind.MODIFY, 2, 3),
|
|
38
|
+
],
|
|
39
|
+
capturedAt: "2026-07-05T00:00:00Z",
|
|
40
|
+
...overrides,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
afterEach(() => cleanup());
|
|
45
|
+
|
|
46
|
+
describe("FileChangeProgressBar", () => {
|
|
47
|
+
it("renders nothing when progress is undefined", () => {
|
|
48
|
+
const { container } = render(<FileChangeProgressBar progress={undefined} />);
|
|
49
|
+
expect(container.firstChild).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("renders nothing when zero files have changed (revert-to-clean)", () => {
|
|
53
|
+
const { container } = render(
|
|
54
|
+
<FileChangeProgressBar
|
|
55
|
+
progress={progress({ filesChanged: 0, entries: [], linesAdded: 0, linesRemoved: 0 })}
|
|
56
|
+
/>,
|
|
57
|
+
);
|
|
58
|
+
expect(container.firstChild).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("shows the summary count and aggregate +N −M", () => {
|
|
62
|
+
render(<FileChangeProgressBar progress={progress()} />);
|
|
63
|
+
expect(screen.getByText(/2 files changing/)).toBeTruthy();
|
|
64
|
+
expect(screen.getByText("+7")).toBeTruthy();
|
|
65
|
+
expect(screen.getByText("-3")).toBeTruthy();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("uses the singular noun for a single file", () => {
|
|
69
|
+
render(
|
|
70
|
+
<FileChangeProgressBar
|
|
71
|
+
progress={progress({ filesChanged: 1, entries: [entry("only.ts", FileChangeKind.ADD, 1, 0)] })}
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
expect(screen.getByText(/1 file changing/)).toBeTruthy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("puts only the count in an aria-live region, not the per-file list", () => {
|
|
78
|
+
render(<FileChangeProgressBar progress={progress()} />);
|
|
79
|
+
const live = screen.getByText(/2 files changing/);
|
|
80
|
+
expect(live.getAttribute("aria-live")).toBe("polite");
|
|
81
|
+
// The expandable list is not part of any live region.
|
|
82
|
+
fireEvent.click(screen.getByRole("button", { name: /2 files changing/ }));
|
|
83
|
+
const link = screen.getByText("a.ts");
|
|
84
|
+
expect(link.closest("[aria-live]")).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("expands to a per-file list on click and collapses again", () => {
|
|
88
|
+
render(<FileChangeProgressBar progress={progress()} />);
|
|
89
|
+
expect(screen.queryByText("a.ts")).toBeNull();
|
|
90
|
+
|
|
91
|
+
const toggle = screen.getByRole("button", { name: /2 files changing/ });
|
|
92
|
+
fireEvent.click(toggle);
|
|
93
|
+
expect(screen.getByText("a.ts")).toBeTruthy();
|
|
94
|
+
expect(screen.getByText("b.ts")).toBeTruthy();
|
|
95
|
+
expect(toggle.getAttribute("aria-expanded")).toBe("true");
|
|
96
|
+
|
|
97
|
+
fireEvent.click(toggle);
|
|
98
|
+
expect(screen.queryByText("a.ts")).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("shows an honest overflow line when the entry list is capped", () => {
|
|
102
|
+
// 5 files changed but only 2 entries carried → "… and 3 more".
|
|
103
|
+
render(<FileChangeProgressBar progress={progress({ filesChanged: 5 })} />);
|
|
104
|
+
fireEvent.click(screen.getByRole("button", { name: /5 files changing/ }));
|
|
105
|
+
expect(screen.getByText(/and 3 more/)).toBeTruthy();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("hides the per-file stat when both counts are zero (binary/secret)", () => {
|
|
109
|
+
render(
|
|
110
|
+
<FileChangeProgressBar
|
|
111
|
+
progress={progress({
|
|
112
|
+
filesChanged: 1,
|
|
113
|
+
linesAdded: 0,
|
|
114
|
+
linesRemoved: 0,
|
|
115
|
+
entries: [entry(".env", FileChangeKind.MODIFY, 0, 0)],
|
|
116
|
+
})}
|
|
117
|
+
/>,
|
|
118
|
+
);
|
|
119
|
+
fireEvent.click(screen.getByRole("button", { name: /1 file changing/ }));
|
|
120
|
+
// The secret path is present…
|
|
121
|
+
expect(screen.getByText(".env")).toBeTruthy();
|
|
122
|
+
// …but no +0 −0 stat is rendered anywhere.
|
|
123
|
+
expect(screen.queryByText("+0")).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -248,6 +248,16 @@ describe("FileReviewCard", () => {
|
|
|
248
248
|
});
|
|
249
249
|
|
|
250
250
|
describe("multi-file set (per-file controls)", () => {
|
|
251
|
+
it("starts a complete multi-file set collapsed in diff mode — auto-expansion is list-mode only", () => {
|
|
252
|
+
// Diff mode's expanded body is every full diff with NO height cap (the
|
|
253
|
+
// workflow file-review list); auto-expanding a multi-file set there
|
|
254
|
+
// would dump every diff open on mount. Only the compact list mode
|
|
255
|
+
// (showDiffs=false, the dock) starts multi-file sets expanded.
|
|
256
|
+
render(<FileReviewCard fileChangeSet={multiChangeSet()} onSubmit={noop} />);
|
|
257
|
+
expect(document.querySelector('[data-cursor-target="file-diff"]')).toBeNull();
|
|
258
|
+
expect(screen.getByRole("button", { name: "Review" })).toBeTruthy();
|
|
259
|
+
});
|
|
260
|
+
|
|
251
261
|
it("renders a Keep/Discard radio per file plus whole-set controls", () => {
|
|
252
262
|
render(<FileReviewCard fileChangeSet={multiChangeSet()} onSubmit={noop} />);
|
|
253
263
|
expandCard();
|
|
@@ -848,11 +858,12 @@ describe("FileReviewCard", () => {
|
|
|
848
858
|
// cases mirror the decision cases above without the diff DOM. The default
|
|
849
859
|
// (showDiffs unset) stays covered by every other suite in this file.
|
|
850
860
|
describe("list mode (showDiffs=false)", () => {
|
|
861
|
+
/** Open a still-collapsed card's file inventory (single complete file). */
|
|
851
862
|
function expandFiles() {
|
|
852
863
|
fireEvent.click(screen.getByRole("button", { name: "Files" }));
|
|
853
864
|
}
|
|
854
865
|
|
|
855
|
-
it("
|
|
866
|
+
it("starts a multi-file set expanded — the per-file controls are the honest decision surface", () => {
|
|
856
867
|
render(
|
|
857
868
|
<FileReviewCard
|
|
858
869
|
fileChangeSet={multiChangeSet()}
|
|
@@ -860,17 +871,65 @@ describe("FileReviewCard", () => {
|
|
|
860
871
|
showDiffs={false}
|
|
861
872
|
/>,
|
|
862
873
|
);
|
|
863
|
-
|
|
874
|
+
// No expander click: the list rows and per-file controls are immediately
|
|
875
|
+
// visible, with no diff DOM (the transcript's stamped rows own the diffs).
|
|
864
876
|
expect(document.querySelector('[data-cursor-target="file-diff"]')).toBeNull();
|
|
865
877
|
expect(
|
|
866
878
|
document.querySelectorAll('[data-cursor-target="file-review-list-row"]'),
|
|
867
879
|
).toHaveLength(2);
|
|
868
|
-
|
|
869
|
-
//
|
|
880
|
+
expect(screen.getByRole("button", { name: "Hide" })).toBeTruthy();
|
|
881
|
+
// Filename-first linked path (FilePathLink: copy fallback without a
|
|
882
|
+
// provider — an accessible button, never a bare span), full path on hover.
|
|
870
883
|
expect(screen.getByText("a.ts")).toBeTruthy();
|
|
871
884
|
expect(screen.getByText("b.ts")).toBeTruthy();
|
|
885
|
+
expect(screen.getByRole("button", { name: /Copy path: src\/a\.ts/ })).toBeTruthy();
|
|
872
886
|
expect(screen.getByTitle("src/a.ts")).toBeTruthy();
|
|
873
|
-
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it("keeps a single complete file collapsed behind the 'Files' expander", () => {
|
|
890
|
+
render(
|
|
891
|
+
<FileReviewCard fileChangeSet={changeSet()} onSubmit={noop} showDiffs={false} />,
|
|
892
|
+
);
|
|
893
|
+
expect(
|
|
894
|
+
document.querySelector('[data-cursor-target="file-review-list-row"]'),
|
|
895
|
+
).toBeNull();
|
|
896
|
+
fireEvent.click(screen.getByRole("button", { name: "Files" }));
|
|
897
|
+
expect(
|
|
898
|
+
document.querySelector('[data-cursor-target="file-review-list-row"]'),
|
|
899
|
+
).toBeTruthy();
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it("shows per-file +N −M from the capture-time counts, hiding the stat when no count exists", () => {
|
|
903
|
+
const counted = create(CapturedFileChangeSchema, {
|
|
904
|
+
id: "fc1",
|
|
905
|
+
pathBefore: "src/a.ts",
|
|
906
|
+
pathAfter: "src/a.ts",
|
|
907
|
+
kind: FileChangeKind.MODIFY,
|
|
908
|
+
captureClass: FileCaptureClass.GIT_TRACKED,
|
|
909
|
+
before: inline("old\n"),
|
|
910
|
+
after: inline("new\n"),
|
|
911
|
+
fileDigest: "d-fc1",
|
|
912
|
+
diffComplete: true,
|
|
913
|
+
linesAdded: 37,
|
|
914
|
+
linesRemoved: 2,
|
|
915
|
+
});
|
|
916
|
+
render(
|
|
917
|
+
<FileReviewCard
|
|
918
|
+
fileChangeSet={multiChangeSet({ fc1: counted })}
|
|
919
|
+
onSubmit={noop}
|
|
920
|
+
showDiffs={false}
|
|
921
|
+
/>,
|
|
922
|
+
);
|
|
923
|
+
const rows = document.querySelectorAll('[data-cursor-target="file-review-list-row"]');
|
|
924
|
+
// fc1 carries counts; fc2 (legacy fixture, zero counts) shows no stat —
|
|
925
|
+
// never a misleading "+0 -0". The bar also aggregates the set's counts,
|
|
926
|
+
// so exactly two stats render: the bar's and fc1's row.
|
|
927
|
+
const stats = document.querySelectorAll('[data-cursor-target="file-review-line-stats"]');
|
|
928
|
+
expect(rows).toHaveLength(2);
|
|
929
|
+
expect(stats).toHaveLength(2);
|
|
930
|
+
expect(stats[0].textContent).toBe("+37 -2");
|
|
931
|
+
expect(stats[1].textContent).toBe("+37 -2");
|
|
932
|
+
expect(screen.getAllByLabelText("37 added, 2 removed")).toHaveLength(2);
|
|
874
933
|
});
|
|
875
934
|
|
|
876
935
|
it("marks each file with its change-kind letter (A/M/D)", () => {
|
|
@@ -899,7 +958,7 @@ describe("FileReviewCard", () => {
|
|
|
899
958
|
showDiffs={false}
|
|
900
959
|
/>,
|
|
901
960
|
);
|
|
902
|
-
|
|
961
|
+
// A multi-file list-mode set starts expanded — the letters are visible.
|
|
903
962
|
expect(screen.getByLabelText("added").textContent).toBe("A");
|
|
904
963
|
expect(screen.getByLabelText("deleted").textContent).toBe("D");
|
|
905
964
|
});
|
|
@@ -937,7 +996,7 @@ describe("FileReviewCard", () => {
|
|
|
937
996
|
showDiffs={false}
|
|
938
997
|
/>,
|
|
939
998
|
);
|
|
940
|
-
|
|
999
|
+
// Starts expanded (multi-file list mode) — the row control is live.
|
|
941
1000
|
fireEvent.click(screen.getByRole("radio", { name: "Keep src/a.ts" }));
|
|
942
1001
|
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
|
|
943
1002
|
scope: FileDecisionScope.FILE,
|
|
@@ -844,7 +844,7 @@ describe("MessageThread", () => {
|
|
|
844
844
|
/>,
|
|
845
845
|
);
|
|
846
846
|
|
|
847
|
-
const buildBtn = screen.getByRole("button", { name:
|
|
847
|
+
const buildBtn = screen.getByRole("button", { name: "Build" });
|
|
848
848
|
expect(buildBtn).toBeTruthy();
|
|
849
849
|
|
|
850
850
|
fireEvent.click(buildBtn);
|
|
@@ -85,7 +85,7 @@ describe("PlanArtifactCard — compact document card", () => {
|
|
|
85
85
|
expect(getArtifactContent).not.toHaveBeenCalled();
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
it("offers exactly one primary 'Build
|
|
88
|
+
it("offers exactly one primary 'Build' action that calls onImplement", () => {
|
|
89
89
|
const onImplement = vi.fn();
|
|
90
90
|
renderCard(
|
|
91
91
|
<PlanArtifactCard
|
|
@@ -96,15 +96,15 @@ describe("PlanArtifactCard — compact document card", () => {
|
|
|
96
96
|
/>,
|
|
97
97
|
);
|
|
98
98
|
|
|
99
|
-
fireEvent.click(screen.
|
|
99
|
+
fireEvent.click(screen.getByRole("button", { name: "Build" }));
|
|
100
100
|
expect(onImplement).toHaveBeenCalledTimes(1);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
it("hides 'Build
|
|
103
|
+
it("hides 'Build' when onImplement is not provided", () => {
|
|
104
104
|
renderCard(
|
|
105
105
|
<PlanArtifactCard executionId="aex_1" artifact={planArtifact} org="acme" />,
|
|
106
106
|
);
|
|
107
|
-
expect(screen.
|
|
107
|
+
expect(screen.queryByRole("button", { name: "Build" })).toBeNull();
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
it("shows the pending label while the approved plan uploads (buildPending)", () => {
|
|
@@ -121,12 +121,12 @@ describe("PlanArtifactCard — compact document card", () => {
|
|
|
121
121
|
|
|
122
122
|
// The primary reads as in-progress and stays disabled — no double-submits
|
|
123
123
|
// while the upload precedes the implement turn.
|
|
124
|
-
expect(screen.
|
|
124
|
+
expect(screen.queryByRole("button", { name: "Build" })).toBeNull();
|
|
125
125
|
const button = screen.getByText("Starting build…").closest("button")!;
|
|
126
126
|
expect(button.disabled).toBe(true);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
it("disables 'Build
|
|
129
|
+
it("disables 'Build' when disabled", () => {
|
|
130
130
|
const onImplement = vi.fn();
|
|
131
131
|
renderCard(
|
|
132
132
|
<PlanArtifactCard
|
|
@@ -138,12 +138,44 @@ describe("PlanArtifactCard — compact document card", () => {
|
|
|
138
138
|
/>,
|
|
139
139
|
);
|
|
140
140
|
|
|
141
|
-
const button = screen.
|
|
141
|
+
const button = screen.getByRole("button", { name: "Build" }) as HTMLButtonElement;
|
|
142
142
|
expect(button.disabled).toBe(true);
|
|
143
143
|
fireEvent.click(button);
|
|
144
144
|
expect(onImplement).not.toHaveBeenCalled();
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
+
it("copies the plan text to the clipboard on demand", async () => {
|
|
148
|
+
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
149
|
+
vi.stubGlobal("navigator", { clipboard: { writeText } });
|
|
150
|
+
const getArtifactContent = vi
|
|
151
|
+
.fn()
|
|
152
|
+
.mockResolvedValue({ content: new TextEncoder().encode("# Plan\n\nsteps") });
|
|
153
|
+
const getArtifactDownloadUrl = vi.fn();
|
|
154
|
+
const stigmer = {
|
|
155
|
+
agentExecution: { getArtifactContent, getArtifactDownloadUrl },
|
|
156
|
+
} as unknown as Stigmer;
|
|
157
|
+
|
|
158
|
+
render(
|
|
159
|
+
withStigmer(
|
|
160
|
+
<PlanArtifactCard executionId="aex_1" artifact={planArtifact} org="acme" />,
|
|
161
|
+
stigmer,
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
fireEvent.click(screen.getByRole("button", { name: "Copy plan" }));
|
|
166
|
+
|
|
167
|
+
// Content is fetched at click time from the stable storage key, then
|
|
168
|
+
// written to the clipboard — the card never pre-fetches on mount.
|
|
169
|
+
expect(getArtifactContent).toHaveBeenCalledTimes(1);
|
|
170
|
+
const req = getArtifactContent.mock.calls[0][0];
|
|
171
|
+
expect(req.executionId).toBe("aex_1");
|
|
172
|
+
expect(req.storageKey).toBe("artifacts/aex_1/plan.md");
|
|
173
|
+
await screen.findByRole("button", { name: "Copied" });
|
|
174
|
+
expect(writeText).toHaveBeenCalledWith("# Plan\n\nsteps");
|
|
175
|
+
|
|
176
|
+
vi.unstubAllGlobals();
|
|
177
|
+
});
|
|
178
|
+
|
|
147
179
|
it("downloads the artifact on demand via a freshly minted URL", async () => {
|
|
148
180
|
const { stigmer, getArtifactDownloadUrl } = createStigmerMock();
|
|
149
181
|
render(
|
|
@@ -152,7 +184,7 @@ describe("PlanArtifactCard — compact document card", () => {
|
|
|
152
184
|
stigmer,
|
|
153
185
|
),
|
|
154
186
|
);
|
|
155
|
-
fireEvent.click(screen.
|
|
187
|
+
fireEvent.click(screen.getByRole("button", { name: "Download plan.md" }));
|
|
156
188
|
// The URL is resolved at click time from the stable storage key — never a
|
|
157
189
|
// baked, expirable URL.
|
|
158
190
|
expect(getArtifactDownloadUrl).toHaveBeenCalledTimes(1);
|
|
@@ -229,8 +261,8 @@ describe("PlanArtifactCard — Cmd/Ctrl+Enter accelerator (card-scoped)", () =>
|
|
|
229
261
|
});
|
|
230
262
|
});
|
|
231
263
|
|
|
232
|
-
describe("PlanArtifactCard — Open
|
|
233
|
-
it("opens the shared preview modal when
|
|
264
|
+
describe("PlanArtifactCard — Open plan modal fallback (org-gated preview)", () => {
|
|
265
|
+
it("opens the shared preview modal when the open icon is clicked (no onOpenPlan)", () => {
|
|
234
266
|
const { stigmer } = createStigmerMock();
|
|
235
267
|
render(
|
|
236
268
|
withStigmer(
|
|
@@ -242,14 +274,14 @@ describe("PlanArtifactCard — Open full (org-gated preview)", () => {
|
|
|
242
274
|
// No dialog before the user opens it.
|
|
243
275
|
expect(document.querySelector("dialog")).toBeNull();
|
|
244
276
|
|
|
245
|
-
fireEvent.click(screen.
|
|
277
|
+
fireEvent.click(screen.getByRole("button", { name: "Open plan" }));
|
|
246
278
|
|
|
247
279
|
const dialog = document.querySelector("dialog");
|
|
248
280
|
expect(dialog).toBeTruthy();
|
|
249
281
|
expect(dialog!.getAttribute("aria-label")).toBe("Preview plan.md");
|
|
250
282
|
});
|
|
251
283
|
|
|
252
|
-
it("hides
|
|
284
|
+
it("hides the open action when org is absent (modal needs org), keeping Build + Download", () => {
|
|
253
285
|
const onImplement = vi.fn();
|
|
254
286
|
renderCard(
|
|
255
287
|
<PlanArtifactCard
|
|
@@ -258,14 +290,16 @@ describe("PlanArtifactCard — Open full (org-gated preview)", () => {
|
|
|
258
290
|
onImplement={onImplement}
|
|
259
291
|
/>,
|
|
260
292
|
);
|
|
261
|
-
expect(screen.
|
|
262
|
-
expect(screen.
|
|
263
|
-
expect(
|
|
293
|
+
expect(screen.queryByRole("button", { name: "Open plan" })).toBeNull();
|
|
294
|
+
expect(screen.queryByRole("button", { name: "Build" })).not.toBeNull();
|
|
295
|
+
expect(
|
|
296
|
+
screen.queryByRole("button", { name: "Download plan.md" }),
|
|
297
|
+
).not.toBeNull();
|
|
264
298
|
});
|
|
265
299
|
});
|
|
266
300
|
|
|
267
301
|
describe("PlanArtifactCard — Open plan (panel-first review)", () => {
|
|
268
|
-
it("
|
|
302
|
+
it("routes the open action to the panel when onOpenPlan is provided (no modal)", () => {
|
|
269
303
|
const onOpenPlan = vi.fn();
|
|
270
304
|
renderCard(
|
|
271
305
|
<PlanArtifactCard
|
|
@@ -277,9 +311,9 @@ describe("PlanArtifactCard — Open plan (panel-first review)", () => {
|
|
|
277
311
|
);
|
|
278
312
|
|
|
279
313
|
// One review affordance, not two: the facet supersedes the modal.
|
|
280
|
-
expect(screen.
|
|
314
|
+
expect(screen.getAllByRole("button", { name: "Open plan" })).toHaveLength(1);
|
|
281
315
|
|
|
282
|
-
fireEvent.click(screen.
|
|
316
|
+
fireEvent.click(screen.getByRole("button", { name: "Open plan" }));
|
|
283
317
|
expect(onOpenPlan).toHaveBeenCalledTimes(1);
|
|
284
318
|
expect(document.querySelector("dialog")).toBeNull();
|
|
285
319
|
});
|
|
@@ -3,7 +3,7 @@ import { render, fireEvent, cleanup } from "@testing-library/react";
|
|
|
3
3
|
import { PlanCompletionCard } from "../PlanCompletionCard";
|
|
4
4
|
|
|
5
5
|
describe("PlanCompletionCard", () => {
|
|
6
|
-
it("renders the card with status text and a 'Build
|
|
6
|
+
it("renders the card with status text and a 'Build' button", () => {
|
|
7
7
|
const { container } = render(<PlanCompletionCard onImplement={() => {}} />);
|
|
8
8
|
|
|
9
9
|
const root = container.firstElementChild as HTMLElement;
|
|
@@ -13,7 +13,7 @@ describe("PlanCompletionCard", () => {
|
|
|
13
13
|
|
|
14
14
|
const button = root.querySelector("button");
|
|
15
15
|
expect(button).toBeTruthy();
|
|
16
|
-
expect(button!.textContent).toContain("Build
|
|
16
|
+
expect(button!.textContent).toContain("Build");
|
|
17
17
|
|
|
18
18
|
expect(root.textContent).toContain("Plan complete");
|
|
19
19
|
cleanup();
|