@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.
Files changed (285) hide show
  1. package/composer/SessionComposer.d.ts +37 -5
  2. package/composer/SessionComposer.d.ts.map +1 -1
  3. package/composer/SessionComposer.js +13 -5
  4. package/composer/SessionComposer.js.map +1 -1
  5. package/execution/ArtifactApplyButton.d.ts +25 -0
  6. package/execution/ArtifactApplyButton.d.ts.map +1 -0
  7. package/execution/ArtifactApplyButton.js +21 -0
  8. package/execution/ArtifactApplyButton.js.map +1 -0
  9. package/execution/ArtifactContentBody.d.ts +36 -0
  10. package/execution/ArtifactContentBody.d.ts.map +1 -0
  11. package/execution/ArtifactContentBody.js +62 -0
  12. package/execution/ArtifactContentBody.js.map +1 -0
  13. package/execution/ArtifactDocument.d.ts +48 -0
  14. package/execution/ArtifactDocument.d.ts.map +1 -0
  15. package/execution/ArtifactDocument.js +89 -0
  16. package/execution/ArtifactDocument.js.map +1 -0
  17. package/execution/ArtifactPreviewModal.d.ts +9 -9
  18. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  19. package/execution/ArtifactPreviewModal.js +18 -145
  20. package/execution/ArtifactPreviewModal.js.map +1 -1
  21. package/execution/ArtifactRow.d.ts +49 -0
  22. package/execution/ArtifactRow.d.ts.map +1 -0
  23. package/execution/ArtifactRow.js +56 -0
  24. package/execution/ArtifactRow.js.map +1 -0
  25. package/execution/ArtifactsWidget.d.ts +6 -9
  26. package/execution/ArtifactsWidget.d.ts.map +1 -1
  27. package/execution/ArtifactsWidget.js +15 -19
  28. package/execution/ArtifactsWidget.js.map +1 -1
  29. package/execution/FileChangeProgressBar.d.ts +47 -0
  30. package/execution/FileChangeProgressBar.d.ts.map +1 -0
  31. package/execution/FileChangeProgressBar.js +49 -0
  32. package/execution/FileChangeProgressBar.js.map +1 -0
  33. package/execution/FileChangesView.d.ts.map +1 -1
  34. package/execution/FileChangesView.js +19 -4
  35. package/execution/FileChangesView.js.map +1 -1
  36. package/execution/FileReviewAtoms.d.ts +27 -0
  37. package/execution/FileReviewAtoms.d.ts.map +1 -0
  38. package/execution/FileReviewAtoms.js +64 -0
  39. package/execution/FileReviewAtoms.js.map +1 -0
  40. package/execution/FileReviewCard.d.ts +2 -2
  41. package/execution/FileReviewCard.d.ts.map +1 -1
  42. package/execution/FileReviewCard.js +32 -62
  43. package/execution/FileReviewCard.js.map +1 -1
  44. package/execution/MessageEntry.d.ts +0 -9
  45. package/execution/MessageEntry.d.ts.map +1 -1
  46. package/execution/MessageEntry.js +1 -11
  47. package/execution/MessageEntry.js.map +1 -1
  48. package/execution/MessageThread.d.ts +3 -4
  49. package/execution/MessageThread.d.ts.map +1 -1
  50. package/execution/MessageThread.js +25 -11
  51. package/execution/MessageThread.js.map +1 -1
  52. package/execution/PlanArtifactCard.d.ts +16 -10
  53. package/execution/PlanArtifactCard.d.ts.map +1 -1
  54. package/execution/PlanArtifactCard.js +31 -7
  55. package/execution/PlanArtifactCard.js.map +1 -1
  56. package/execution/PlanCompletionCard.d.ts +4 -4
  57. package/execution/PlanCompletionCard.d.ts.map +1 -1
  58. package/execution/PlanCompletionCard.js +4 -4
  59. package/execution/PlanCompletionCard.js.map +1 -1
  60. package/execution/PlanStreamingCard.d.ts +7 -2
  61. package/execution/PlanStreamingCard.d.ts.map +1 -1
  62. package/execution/PlanStreamingCard.js +11 -6
  63. package/execution/PlanStreamingCard.js.map +1 -1
  64. package/execution/index.d.ts +12 -2
  65. package/execution/index.d.ts.map +1 -1
  66. package/execution/index.js +6 -1
  67. package/execution/index.js.map +1 -1
  68. package/execution/use-build-from-plan-hotkey.d.ts +1 -1
  69. package/execution/use-build-from-plan-hotkey.js +1 -1
  70. package/execution/useArtifactContent.d.ts +6 -1
  71. package/execution/useArtifactContent.d.ts.map +1 -1
  72. package/execution/useArtifactContent.js +7 -2
  73. package/execution/useArtifactContent.js.map +1 -1
  74. package/execution/useArtifactCopy.d.ts +51 -0
  75. package/execution/useArtifactCopy.d.ts.map +1 -0
  76. package/execution/useArtifactCopy.js +75 -0
  77. package/execution/useArtifactCopy.js.map +1 -0
  78. package/execution/useArtifactDownload.d.ts.map +1 -1
  79. package/execution/useArtifactDownload.js +17 -5
  80. package/execution/useArtifactDownload.js.map +1 -1
  81. package/execution/useArtifactInspection.d.ts +85 -0
  82. package/execution/useArtifactInspection.d.ts.map +1 -0
  83. package/execution/useArtifactInspection.js +143 -0
  84. package/execution/useArtifactInspection.js.map +1 -0
  85. package/execution/useCreateAgentExecution.d.ts +16 -3
  86. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  87. package/execution/useCreateAgentExecution.js +1 -0
  88. package/execution/useCreateAgentExecution.js.map +1 -1
  89. package/index.d.ts +4 -4
  90. package/index.d.ts.map +1 -1
  91. package/index.js +3 -3
  92. package/index.js.map +1 -1
  93. package/internal/MermaidDiagram.d.ts +47 -0
  94. package/internal/MermaidDiagram.d.ts.map +1 -0
  95. package/internal/MermaidDiagram.js +118 -0
  96. package/internal/MermaidDiagram.js.map +1 -0
  97. package/internal/file-tree/FileTreeNode.d.ts.map +1 -1
  98. package/internal/file-tree/FileTreeNode.js +2 -2
  99. package/internal/file-tree/FileTreeNode.js.map +1 -1
  100. package/internal/markdown-components.d.ts.map +1 -1
  101. package/internal/markdown-components.js +34 -0
  102. package/internal/markdown-components.js.map +1 -1
  103. package/internal/mermaid-loader.d.ts +26 -0
  104. package/internal/mermaid-loader.d.ts.map +1 -0
  105. package/internal/mermaid-loader.js +34 -0
  106. package/internal/mermaid-loader.js.map +1 -0
  107. package/internal/mermaid-theme-css.d.ts +41 -0
  108. package/internal/mermaid-theme-css.d.ts.map +1 -0
  109. package/internal/mermaid-theme-css.js +179 -0
  110. package/internal/mermaid-theme-css.js.map +1 -0
  111. package/internal/slug.d.ts +9 -5
  112. package/internal/slug.d.ts.map +1 -1
  113. package/internal/slug.js +10 -6
  114. package/internal/slug.js.map +1 -1
  115. package/library/detect-plan-artifact.d.ts +27 -9
  116. package/library/detect-plan-artifact.d.ts.map +1 -1
  117. package/library/detect-plan-artifact.js +26 -6
  118. package/library/detect-plan-artifact.js.map +1 -1
  119. package/library/index.d.ts +1 -1
  120. package/library/index.d.ts.map +1 -1
  121. package/library/index.js +1 -1
  122. package/library/index.js.map +1 -1
  123. package/package.json +5 -4
  124. package/session/PlanEditor.js +1 -1
  125. package/session/PlanEditor.js.map +1 -1
  126. package/session/SessionViewer.d.ts.map +1 -1
  127. package/session/SessionViewer.js +122 -30
  128. package/session/SessionViewer.js.map +1 -1
  129. package/session/artifact-document.d.ts +13 -0
  130. package/session/artifact-document.d.ts.map +1 -0
  131. package/session/artifact-document.js +22 -0
  132. package/session/artifact-document.js.map +1 -0
  133. package/session/facets/ArtifactsTab.d.ts +30 -4
  134. package/session/facets/ArtifactsTab.d.ts.map +1 -1
  135. package/session/facets/ArtifactsTab.js +32 -10
  136. package/session/facets/ArtifactsTab.js.map +1 -1
  137. package/session/index.d.ts +2 -1
  138. package/session/index.d.ts.map +1 -1
  139. package/session/index.js +4 -1
  140. package/session/index.js.map +1 -1
  141. package/session/plan-document.d.ts +14 -2
  142. package/session/plan-document.d.ts.map +1 -1
  143. package/session/plan-document.js +14 -3
  144. package/session/plan-document.js.map +1 -1
  145. package/session/usePlanDraft.d.ts +1 -1
  146. package/session/useSessionArtifacts.d.ts +17 -0
  147. package/session/useSessionArtifacts.d.ts.map +1 -1
  148. package/session/useSessionArtifacts.js +16 -5
  149. package/session/useSessionArtifacts.js.map +1 -1
  150. package/session/useSessionConversation.d.ts +26 -3
  151. package/session/useSessionConversation.d.ts.map +1 -1
  152. package/session/useSessionConversation.js +51 -6
  153. package/session/useSessionConversation.js.map +1 -1
  154. package/session/useSessionPageFlow.d.ts.map +1 -1
  155. package/session/useSessionPageFlow.js +1 -0
  156. package/session/useSessionPageFlow.js.map +1 -1
  157. package/session/useSessionPanel.d.ts +20 -0
  158. package/session/useSessionPanel.d.ts.map +1 -1
  159. package/session/useSessionPanel.js +13 -0
  160. package/session/useSessionPanel.js.map +1 -1
  161. package/session/useSessionRailViews.d.ts +13 -1
  162. package/session/useSessionRailViews.d.ts.map +1 -1
  163. package/session/useSessionRailViews.js +5 -3
  164. package/session/useSessionRailViews.js.map +1 -1
  165. package/src/composer/SessionComposer.tsx +76 -7
  166. package/src/composer/__tests__/SessionComposer-editing.test.tsx +128 -0
  167. package/src/execution/ArtifactApplyButton.tsx +79 -0
  168. package/src/execution/ArtifactContentBody.tsx +255 -0
  169. package/src/execution/ArtifactDocument.tsx +344 -0
  170. package/src/execution/ArtifactPreviewModal.tsx +49 -415
  171. package/src/execution/ArtifactRow.tsx +165 -0
  172. package/src/execution/ArtifactsWidget.tsx +24 -33
  173. package/src/execution/FileChangeProgressBar.tsx +121 -0
  174. package/src/execution/FileChangesView.tsx +18 -4
  175. package/src/execution/FileReviewAtoms.tsx +89 -0
  176. package/src/execution/FileReviewCard.tsx +57 -90
  177. package/src/execution/MessageEntry.tsx +0 -62
  178. package/src/execution/MessageThread.tsx +29 -15
  179. package/src/execution/PlanArtifactCard.tsx +130 -52
  180. package/src/execution/PlanCompletionCard.tsx +5 -5
  181. package/src/execution/PlanStreamingCard.tsx +15 -11
  182. package/src/execution/__tests__/ArtifactContentBody.test.tsx +131 -0
  183. package/src/execution/__tests__/ArtifactDocument.test.tsx +100 -0
  184. package/src/execution/__tests__/ArtifactPreviewModal.test.tsx +6 -6
  185. package/src/execution/__tests__/ArtifactRow.test.tsx +139 -0
  186. package/src/execution/__tests__/ArtifactsWidget.test.tsx +78 -0
  187. package/src/execution/__tests__/FileChangeProgressBar.test.tsx +125 -0
  188. package/src/execution/__tests__/FileReviewCard.test.tsx +66 -7
  189. package/src/execution/__tests__/MessageThread.test.tsx +1 -1
  190. package/src/execution/__tests__/PlanArtifactCard.test.tsx +52 -18
  191. package/src/execution/__tests__/PlanCompletionCard.test.tsx +2 -2
  192. package/src/execution/__tests__/PlanStreamingCard.test.tsx +11 -4
  193. package/src/execution/__tests__/buildThreadItems-plan-completion.test.ts +104 -8
  194. package/src/execution/__tests__/message-entry.test.tsx +56 -39
  195. package/src/execution/__tests__/useArtifactCopy.test.tsx +90 -0
  196. package/src/execution/__tests__/useArtifactDownload.test.tsx +2 -0
  197. package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
  198. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
  199. package/src/execution/index.ts +20 -2
  200. package/src/execution/use-build-from-plan-hotkey.ts +1 -1
  201. package/src/execution/useArtifactContent.ts +7 -0
  202. package/src/execution/useArtifactCopy.ts +109 -0
  203. package/src/execution/useArtifactDownload.ts +17 -5
  204. package/src/execution/useArtifactInspection.ts +252 -0
  205. package/src/execution/useCreateAgentExecution.ts +17 -3
  206. package/src/index.ts +17 -2
  207. package/src/internal/MermaidDiagram.tsx +164 -0
  208. package/src/internal/__tests__/a11y/mermaid-theme.a11y.test.tsx +122 -0
  209. package/src/internal/__tests__/markdown-components.test.tsx +93 -1
  210. package/src/internal/__tests__/mermaid-diagram.test.tsx +282 -0
  211. package/src/internal/file-tree/FileTreeNode.tsx +13 -2
  212. package/src/internal/markdown-components.tsx +36 -1
  213. package/src/internal/mermaid-loader.ts +44 -0
  214. package/src/internal/mermaid-theme-css.ts +178 -0
  215. package/src/internal/slug.ts +10 -6
  216. package/src/library/__tests__/detect-plan-artifact.test.ts +21 -0
  217. package/src/library/detect-plan-artifact.ts +32 -10
  218. package/src/library/index.ts +2 -0
  219. package/src/session/PlanEditor.tsx +1 -1
  220. package/src/session/SessionViewer.tsx +227 -51
  221. package/src/session/__tests__/PlanEditor.test.tsx +4 -4
  222. package/src/session/__tests__/PlanStreamingDocument.test.tsx +1 -1
  223. package/src/session/__tests__/SessionViewer-artifactTabs.test.tsx +210 -0
  224. package/src/session/__tests__/SessionViewer-editResubmit.test.tsx +233 -0
  225. package/src/session/__tests__/SessionViewer-fileReviewDock.test.tsx +57 -0
  226. package/src/session/__tests__/useSessionArtifacts.test.ts +57 -0
  227. package/src/session/__tests__/useSessionConversation.test.tsx +216 -1
  228. package/src/session/__tests__/useSessionPanel.test.ts +70 -1
  229. package/src/session/artifact-document.ts +23 -0
  230. package/src/session/facets/ArtifactsTab.tsx +66 -18
  231. package/src/session/facets/__tests__/ArtifactsTab.test.tsx +111 -47
  232. package/src/session/index.ts +5 -1
  233. package/src/session/plan-document.ts +14 -3
  234. package/src/session/usePlanDraft.ts +1 -1
  235. package/src/session/useSessionArtifacts.ts +16 -5
  236. package/src/session/useSessionConversation.ts +80 -10
  237. package/src/session/useSessionPageFlow.ts +1 -0
  238. package/src/session/useSessionPanel.ts +41 -0
  239. package/src/session/useSessionRailViews.tsx +21 -1
  240. package/src/workspace/EditorTabs.tsx +38 -9
  241. package/src/workspace/ExplorerTree.tsx +10 -4
  242. package/src/workspace/FileViewer.tsx +27 -6
  243. package/src/workspace/WorkspaceContentSearch.tsx +44 -20
  244. package/src/workspace/WorkspaceEditor.tsx +19 -16
  245. package/src/workspace/WorkspaceFileSearch.tsx +51 -28
  246. package/src/workspace/WorkspaceSurface.tsx +106 -48
  247. package/src/workspace/__tests__/EditorTabs.test.tsx +4 -2
  248. package/src/workspace/__tests__/ExplorerTree.test.tsx +52 -0
  249. package/src/workspace/__tests__/FileViewer.test.tsx +18 -0
  250. package/src/workspace/__tests__/WorkspaceContentSearch.test.tsx +38 -5
  251. package/src/workspace/__tests__/WorkspaceEditor-initialPanel.test.tsx +19 -0
  252. package/src/workspace/__tests__/WorkspaceFileSearch.test.tsx +27 -6
  253. package/src/workspace/__tests__/WorkspaceSurface.test.tsx +70 -0
  254. package/src/workspace/__tests__/a11y/harness.tsx +140 -0
  255. package/src/workspace/__tests__/a11y/mock-capabilities.ts +138 -0
  256. package/src/workspace/__tests__/a11y/workspace-editor.a11y.test.tsx +65 -0
  257. package/src/workspace/__tests__/a11y/workspace-explorer-search.a11y.test.tsx +112 -0
  258. package/src/workspace/__tests__/a11y/workspace-viewer-tabs.a11y.test.tsx +69 -0
  259. package/styles.css +1 -1
  260. package/workspace/EditorTabs.d.ts +21 -3
  261. package/workspace/EditorTabs.d.ts.map +1 -1
  262. package/workspace/EditorTabs.js +18 -6
  263. package/workspace/EditorTabs.js.map +1 -1
  264. package/workspace/ExplorerTree.js +8 -4
  265. package/workspace/ExplorerTree.js.map +1 -1
  266. package/workspace/FileViewer.d.ts.map +1 -1
  267. package/workspace/FileViewer.js +19 -8
  268. package/workspace/FileViewer.js.map +1 -1
  269. package/workspace/WorkspaceContentSearch.d.ts.map +1 -1
  270. package/workspace/WorkspaceContentSearch.js +23 -2
  271. package/workspace/WorkspaceContentSearch.js.map +1 -1
  272. package/workspace/WorkspaceEditor.d.ts.map +1 -1
  273. package/workspace/WorkspaceEditor.js +8 -10
  274. package/workspace/WorkspaceEditor.js.map +1 -1
  275. package/workspace/WorkspaceFileSearch.d.ts.map +1 -1
  276. package/workspace/WorkspaceFileSearch.js +23 -3
  277. package/workspace/WorkspaceFileSearch.js.map +1 -1
  278. package/workspace/WorkspaceSurface.d.ts.map +1 -1
  279. package/workspace/WorkspaceSurface.js +54 -18
  280. package/workspace/WorkspaceSurface.js.map +1 -1
  281. package/execution/ArtifactCard.d.ts +0 -81
  282. package/execution/ArtifactCard.d.ts.map +0 -1
  283. package/execution/ArtifactCard.js +0 -130
  284. package/execution/ArtifactCard.js.map +0 -1
  285. 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("labels the expander 'Files' and renders inert list rows with no diff DOM", () => {
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
- expandFiles();
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
- // Filename-first path text, full path available on hover — and no
869
- // link/copy affordance (the list is an inventory, not navigation).
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
- expect(screen.queryByRole("link")).toBeNull();
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
- expandFiles();
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
- expandFiles();
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: /build from plan/i });
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 from plan' action that calls onImplement", () => {
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.getByText("Build from plan"));
99
+ fireEvent.click(screen.getByRole("button", { name: "Build" }));
100
100
  expect(onImplement).toHaveBeenCalledTimes(1);
101
101
  });
102
102
 
103
- it("hides 'Build from plan' when onImplement is not provided", () => {
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.queryByText("Build from plan")).toBeNull();
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.queryByText("Build from plan")).toBeNull();
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 from plan' when disabled", () => {
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.getByText("Build from plan").closest("button")!;
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.getByText("Download"));
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 full (org-gated preview)", () => {
233
- it("opens the shared preview modal when 'Open full' is clicked", () => {
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.getByText("Open full"));
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 'Open full' when org is absent (modal needs org), keeping Build + Download", () => {
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.queryByText("Open full")).toBeNull();
262
- expect(screen.queryByText("Build from plan")).not.toBeNull();
263
- expect(screen.queryByText("Download")).not.toBeNull();
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("replaces the modal secondary with 'Open plan' when onOpenPlan is provided", () => {
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.queryByText("Open full")).toBeNull();
314
+ expect(screen.getAllByRole("button", { name: "Open plan" })).toHaveLength(1);
281
315
 
282
- fireEvent.click(screen.getByText("Open plan"));
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 from plan' button", () => {
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 from plan");
16
+ expect(button!.textContent).toContain("Build");
17
17
 
18
18
  expect(root.textContent).toContain("Plan complete");
19
19
  cleanup();