@stigmer/react 3.1.2 → 3.1.3
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 +6 -5
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +1 -1
- package/composer/SessionComposer.js.map +1 -1
- package/execution/ArtifactPreviewModal.d.ts +5 -5
- package/execution/ArtifactPreviewModal.js +4 -4
- package/execution/ArtifactPreviewModal.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 +4 -0
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +2 -0
- 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/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/useCreateAgentExecution.d.ts +5 -3
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +3 -3
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/index.js.map +1 -1
- 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/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 +4 -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 +32 -13
- package/session/SessionViewer.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/useSessionConversation.d.ts +9 -2
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +19 -2
- package/session/useSessionConversation.js.map +1 -1
- package/src/composer/SessionComposer.tsx +7 -6
- package/src/execution/ArtifactPreviewModal.tsx +9 -9
- 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__/ArtifactPreviewModal.test.tsx +6 -6
- 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 +6 -37
- package/src/execution/__tests__/useArtifactCopy.test.tsx +90 -0
- package/src/execution/__tests__/useArtifactDownload.test.tsx +2 -0
- package/src/execution/index.ts +6 -0
- package/src/execution/use-build-from-plan-hotkey.ts +1 -1
- package/src/execution/useArtifactCopy.ts +109 -0
- package/src/execution/useArtifactDownload.ts +17 -5
- package/src/execution/useCreateAgentExecution.ts +5 -3
- package/src/index.ts +6 -0
- package/src/internal/file-tree/FileTreeNode.tsx +13 -2
- 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 +50 -18
- package/src/session/__tests__/PlanEditor.test.tsx +4 -4
- package/src/session/__tests__/PlanStreamingDocument.test.tsx +1 -1
- package/src/session/__tests__/SessionViewer-fileReviewDock.test.tsx +57 -0
- package/src/session/__tests__/useSessionConversation.test.tsx +44 -0
- package/src/session/facets/__tests__/ArtifactsTab.test.tsx +7 -7
- package/src/session/plan-document.ts +14 -3
- package/src/session/usePlanDraft.ts +1 -1
- package/src/session/useSessionConversation.ts +32 -4
- 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
|
@@ -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();
|
|
@@ -5,7 +5,7 @@ import { PlanStreamingCard } from "../PlanStreamingCard";
|
|
|
5
5
|
afterEach(cleanup);
|
|
6
6
|
|
|
7
7
|
describe("PlanStreamingCard — live plan stand-in", () => {
|
|
8
|
-
it("is a busy region with the streaming title,
|
|
8
|
+
it("is a busy region with the streaming title, status, and live size", () => {
|
|
9
9
|
render(
|
|
10
10
|
<PlanStreamingCard
|
|
11
11
|
title="Refactor the auth flow"
|
|
@@ -17,11 +17,18 @@ describe("PlanStreamingCard — live plan stand-in", () => {
|
|
|
17
17
|
const region = screen.getByRole("region", { name: "Plan being written" });
|
|
18
18
|
expect(region.getAttribute("aria-busy")).toBe("true");
|
|
19
19
|
expect(region.textContent).toContain("Refactor the auth flow");
|
|
20
|
-
expect(region.textContent).toContain("plan.md");
|
|
21
20
|
expect(region.textContent).toContain("Writing…");
|
|
22
21
|
expect(region.textContent).toContain("2.0 KB");
|
|
23
22
|
});
|
|
24
23
|
|
|
24
|
+
it("shows no filename mid-stream (the final name is not known yet)", () => {
|
|
25
|
+
render(
|
|
26
|
+
<PlanStreamingCard title="Refactor the auth flow" sizeBytes={2048} onOpenPlan={vi.fn()} />,
|
|
27
|
+
);
|
|
28
|
+
const region = screen.getByRole("region", { name: "Plan being written" });
|
|
29
|
+
expect(region.textContent).not.toContain("plan.md");
|
|
30
|
+
});
|
|
31
|
+
|
|
25
32
|
it("falls back to 'Writing plan…' until the title has streamed in", () => {
|
|
26
33
|
render(<PlanStreamingCard sizeBytes={12} onOpenPlan={vi.fn()} />);
|
|
27
34
|
expect(screen.getByText("Writing plan…")).toBeTruthy();
|
|
@@ -37,8 +44,8 @@ describe("PlanStreamingCard — live plan stand-in", () => {
|
|
|
37
44
|
|
|
38
45
|
it("offers no Download or Build actions — nothing final exists yet", () => {
|
|
39
46
|
render(<PlanStreamingCard sizeBytes={64} onOpenPlan={vi.fn()} />);
|
|
40
|
-
expect(screen.
|
|
41
|
-
expect(screen.queryByText("Build
|
|
47
|
+
expect(screen.queryByRole("button", { name: /download/i })).toBeNull();
|
|
48
|
+
expect(screen.queryByText("Build")).toBeNull();
|
|
42
49
|
});
|
|
43
50
|
|
|
44
51
|
it("renders without an action when onOpenPlan is absent", () => {
|
|
@@ -638,7 +638,7 @@ describe("buildThreadItems plan-writing (live streaming collapse)", () => {
|
|
|
638
638
|
});
|
|
639
639
|
});
|
|
640
640
|
|
|
641
|
-
describe("buildThreadItems build-from-plan prompt", () => {
|
|
641
|
+
describe("buildThreadItems build-from-plan prompt suppression", () => {
|
|
642
642
|
function promptItem(items: readonly ThreadItem[], execId: string) {
|
|
643
643
|
return items.find(
|
|
644
644
|
(i): i is Extract<ThreadItem, { kind: "message" }> =>
|
|
@@ -646,7 +646,7 @@ describe("buildThreadItems build-from-plan prompt", () => {
|
|
|
646
646
|
);
|
|
647
647
|
}
|
|
648
648
|
|
|
649
|
-
it("
|
|
649
|
+
it("synthesizes NO prompt item for a build turn (the label is machine-written, not user prose)", () => {
|
|
650
650
|
const exec = makeExecution({
|
|
651
651
|
id: "exec-build",
|
|
652
652
|
specMessage: "Build from plan",
|
|
@@ -656,14 +656,17 @@ describe("buildThreadItems build-from-plan prompt", () => {
|
|
|
656
656
|
});
|
|
657
657
|
|
|
658
658
|
const items = buildThreadItems([exec], null, null, false, undefined);
|
|
659
|
-
const prompt = promptItem(items, "exec-build");
|
|
660
659
|
|
|
661
|
-
expect(
|
|
662
|
-
|
|
663
|
-
expect(
|
|
660
|
+
expect(promptItem(items, "exec-build")).toBeUndefined();
|
|
661
|
+
// The turn's agent output still renders — only the fake prompt is gone.
|
|
662
|
+
expect(
|
|
663
|
+
items.some(
|
|
664
|
+
(i) => i.kind === "message" && i.message.content === "Implementing…",
|
|
665
|
+
),
|
|
666
|
+
).toBe(true);
|
|
664
667
|
});
|
|
665
668
|
|
|
666
|
-
it("
|
|
669
|
+
it("still synthesizes the prompt for an ordinary turn", () => {
|
|
667
670
|
const exec = makeExecution({
|
|
668
671
|
id: "exec-normal",
|
|
669
672
|
interactionMode: InteractionMode.AGENT,
|
|
@@ -674,6 +677,99 @@ describe("buildThreadItems build-from-plan prompt", () => {
|
|
|
674
677
|
const prompt = promptItem(items, "exec-normal");
|
|
675
678
|
|
|
676
679
|
expect(prompt).toBeDefined();
|
|
677
|
-
expect(prompt!.
|
|
680
|
+
expect(prompt!.message.content).toBe("test message");
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it("offers no retryMessage on a failed build turn's error item (a plain resend would drop the flag)", () => {
|
|
684
|
+
const exec = makeExecution({
|
|
685
|
+
id: "exec-build-fail",
|
|
686
|
+
specMessage: "Build from plan",
|
|
687
|
+
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
688
|
+
interactionMode: InteractionMode.AGENT,
|
|
689
|
+
buildFromPlan: true,
|
|
690
|
+
});
|
|
691
|
+
exec.status!.error = "runner crashed";
|
|
692
|
+
|
|
693
|
+
const items = buildThreadItems([exec], null, null, false, undefined);
|
|
694
|
+
const errorItem = items.find(
|
|
695
|
+
(i): i is Extract<ThreadItem, { kind: "execution-error" }> =>
|
|
696
|
+
i.kind === "execution-error",
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
expect(errorItem).toBeDefined();
|
|
700
|
+
expect(errorItem!.retryMessage).toBeUndefined();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it("keeps retryMessage on an ordinary failed turn", () => {
|
|
704
|
+
const exec = makeExecution({
|
|
705
|
+
id: "exec-fail",
|
|
706
|
+
specMessage: "do the thing",
|
|
707
|
+
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
708
|
+
interactionMode: InteractionMode.AGENT,
|
|
709
|
+
});
|
|
710
|
+
exec.status!.error = "runner crashed";
|
|
711
|
+
|
|
712
|
+
const items = buildThreadItems([exec], null, null, false, undefined);
|
|
713
|
+
const errorItem = items.find(
|
|
714
|
+
(i): i is Extract<ThreadItem, { kind: "execution-error" }> =>
|
|
715
|
+
i.kind === "execution-error",
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
expect(errorItem!.retryMessage).toBe("do the thing");
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
describe("buildThreadItems pending-bubble tail guard", () => {
|
|
723
|
+
function pendingItem(items: readonly ThreadItem[]) {
|
|
724
|
+
return items.find(
|
|
725
|
+
(i): i is Extract<ThreadItem, { kind: "message" }> =>
|
|
726
|
+
i.kind === "message" && i.key === "pending-user-turn",
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
it("suppresses the pending bubble once the same turn has streamed in (succeeded send)", () => {
|
|
731
|
+
const exec = makeExecution({
|
|
732
|
+
id: "exec-1",
|
|
733
|
+
specMessage: "hello there",
|
|
734
|
+
interactionMode: InteractionMode.AGENT,
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
const items = buildThreadItems(
|
|
738
|
+
[exec],
|
|
739
|
+
null,
|
|
740
|
+
"hello there",
|
|
741
|
+
false,
|
|
742
|
+
undefined,
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
expect(pendingItem(items)).toBeUndefined();
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it("renders a FAILED pending message even when an older execution carries identical text", () => {
|
|
749
|
+
// Two build attempts of the same plan share the message "Build from
|
|
750
|
+
// plan". A failed second send created no execution, so the first build
|
|
751
|
+
// turn must never swallow the failed bubble — the failure would
|
|
752
|
+
// otherwise render nothing at all (build turns synthesize no prompt).
|
|
753
|
+
const firstBuild = makeExecution({
|
|
754
|
+
id: "exec-build-1",
|
|
755
|
+
specMessage: "Build from plan",
|
|
756
|
+
interactionMode: InteractionMode.AGENT,
|
|
757
|
+
buildFromPlan: true,
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
const items = buildThreadItems(
|
|
761
|
+
[firstBuild],
|
|
762
|
+
null,
|
|
763
|
+
"Build from plan",
|
|
764
|
+
false,
|
|
765
|
+
undefined,
|
|
766
|
+
undefined,
|
|
767
|
+
true, // pendingMessageFailed
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
const failed = pendingItem(items);
|
|
771
|
+
expect(failed).toBeDefined();
|
|
772
|
+
expect(failed!.isFailed).toBe(true);
|
|
773
|
+
expect(failed!.message.content).toBe("Build from plan");
|
|
678
774
|
});
|
|
679
775
|
});
|
|
@@ -195,51 +195,20 @@ describe("MessageEntry — Human messages", () => {
|
|
|
195
195
|
});
|
|
196
196
|
|
|
197
197
|
// ---------------------------------------------------------------------------
|
|
198
|
-
// Build-from-plan
|
|
198
|
+
// Build-from-plan turns — no special human-message treatment
|
|
199
199
|
// ---------------------------------------------------------------------------
|
|
200
200
|
|
|
201
|
-
describe("MessageEntry — Build-from-plan
|
|
202
|
-
it("renders
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
const chip = queryArticle(container, "Build from plan");
|
|
209
|
-
expect(chip).not.toBeNull();
|
|
210
|
-
expect(chip!.textContent).toContain("Build from plan");
|
|
211
|
-
// The ordinary prompt bubble must not render alongside the chip.
|
|
212
|
-
expect(queryArticle(container, "User message")).toBeNull();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("keeps the ordinary bubble when the flag is absent", () => {
|
|
201
|
+
describe("MessageEntry — Build-from-plan turns", () => {
|
|
202
|
+
it("renders any human message as the ordinary prompt bubble (the thread hides build turns before they reach MessageEntry)", () => {
|
|
203
|
+
// The retired chip treatment lived here; today buildThreadItems skips
|
|
204
|
+
// synthesizing the prompt item for a build turn entirely, so a human
|
|
205
|
+
// message that DOES reach MessageEntry is always ordinary prose.
|
|
216
206
|
const msg = makeMessage(MessageType.MESSAGE_HUMAN, "Build from plan");
|
|
217
207
|
const { container } = render(<MessageEntry message={msg} />);
|
|
218
208
|
|
|
219
209
|
expect(queryArticle(container, "User message")).not.toBeNull();
|
|
220
210
|
expect(queryArticle(container, "Build from plan")).toBeNull();
|
|
221
211
|
});
|
|
222
|
-
|
|
223
|
-
it("never offers Edit on the chip (the label is not user prose)", () => {
|
|
224
|
-
const msg = makeMessage(MessageType.MESSAGE_HUMAN, "Build from plan");
|
|
225
|
-
const { container } = render(
|
|
226
|
-
<MessageEntry message={msg} isBuildFromPlan onEdit={() => {}} />,
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
expect(
|
|
230
|
-
container.querySelector('[aria-label="Edit message"]'),
|
|
231
|
-
).toBeNull();
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("ignores the flag on non-human messages", () => {
|
|
235
|
-
const msg = makeMessage(MessageType.MESSAGE_AI, "Implementing…");
|
|
236
|
-
const { container } = render(
|
|
237
|
-
<MessageEntry message={msg} isBuildFromPlan />,
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
expect(queryArticle(container, "AI response")).not.toBeNull();
|
|
241
|
-
expect(queryArticle(container, "Build from plan")).toBeNull();
|
|
242
|
-
});
|
|
243
212
|
});
|
|
244
213
|
|
|
245
214
|
// ---------------------------------------------------------------------------
|