@stigmer/react 3.1.4 → 3.1.6
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/execution/FileReviewContext.d.ts +15 -1
- package/execution/FileReviewContext.d.ts.map +1 -1
- package/execution/FileReviewContext.js +20 -1
- package/execution/FileReviewContext.js.map +1 -1
- package/execution/MessageThread.d.ts +65 -2
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +42 -23
- package/execution/MessageThread.js.map +1 -1
- package/execution/TodoCard.d.ts +8 -0
- package/execution/TodoCard.d.ts.map +1 -1
- package/execution/TodoCard.js +6 -4
- package/execution/TodoCard.js.map +1 -1
- package/execution/TodoList.d.ts +20 -1
- package/execution/TodoList.d.ts.map +1 -1
- package/execution/TodoList.js +9 -6
- package/execution/TodoList.js.map +1 -1
- package/execution/ToolCallDetail.d.ts +3 -2
- package/execution/ToolCallDetail.d.ts.map +1 -1
- package/execution/ToolCallDetail.js +49 -16
- package/execution/ToolCallDetail.js.map +1 -1
- package/execution/WriteBackCard.d.ts +7 -1
- package/execution/WriteBackCard.d.ts.map +1 -1
- package/execution/WriteBackCard.js +10 -2
- package/execution/WriteBackCard.js.map +1 -1
- package/execution/file-review-status.d.ts +15 -0
- package/execution/file-review-status.d.ts.map +1 -1
- package/execution/file-review-status.js +22 -0
- package/execution/file-review-status.js.map +1 -1
- package/execution/index.d.ts +5 -5
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +3 -3
- package/execution/index.js.map +1 -1
- package/github/useGitHubFileReader.d.ts +4 -2
- package/github/useGitHubFileReader.d.ts.map +1 -1
- package/github/useGitHubFileReader.js +16 -3
- package/github/useGitHubFileReader.js.map +1 -1
- package/github/useGitHubTreeLister.d.ts +2 -1
- package/github/useGitHubTreeLister.d.ts.map +1 -1
- package/github/useGitHubTreeLister.js +7 -3
- package/github/useGitHubTreeLister.js.map +1 -1
- package/index.d.ts +3 -3
- package/index.d.ts.map +1 -1
- package/index.js +3 -3
- package/index.js.map +1 -1
- package/internal/VirtualizedThread.d.ts +3 -2
- package/internal/VirtualizedThread.d.ts.map +1 -1
- package/internal/VirtualizedThread.js +3 -2
- package/internal/VirtualizedThread.js.map +1 -1
- package/package.json +4 -4
- package/session/SessionViewer.d.ts +11 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +24 -5
- package/session/SessionViewer.js.map +1 -1
- package/session/facets/ChangesTab.d.ts +24 -6
- package/session/facets/ChangesTab.d.ts.map +1 -1
- package/session/facets/ChangesTab.js +15 -6
- package/session/facets/ChangesTab.js.map +1 -1
- package/session/index.d.ts +1 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js +1 -0
- package/session/index.js.map +1 -1
- package/session/useSessionRailViews.d.ts +16 -1
- package/session/useSessionRailViews.d.ts.map +1 -1
- package/session/useSessionRailViews.js +9 -4
- package/session/useSessionRailViews.js.map +1 -1
- package/session/useWorkspaceReadRefs.d.ts +45 -0
- package/session/useWorkspaceReadRefs.d.ts.map +1 -0
- package/session/useWorkspaceReadRefs.js +104 -0
- package/session/useWorkspaceReadRefs.js.map +1 -0
- package/src/execution/FileReviewContext.ts +27 -1
- package/src/execution/MessageThread.tsx +110 -22
- package/src/execution/TodoCard.tsx +20 -4
- package/src/execution/TodoList.tsx +26 -4
- package/src/execution/ToolCallDetail.tsx +69 -16
- package/src/execution/WriteBackCard.tsx +18 -4
- package/src/execution/__tests__/ToolCallDetail.test.tsx +188 -2
- package/src/execution/__tests__/WriteBackCard.test.tsx +83 -0
- package/src/execution/__tests__/file-review-status.test.ts +47 -0
- package/src/execution/__tests__/message-thread-slots.test.tsx +463 -0
- package/src/execution/file-review-status.ts +24 -0
- package/src/execution/index.ts +13 -3
- package/src/github/__tests__/useGitHubFileReader.test.ts +35 -2
- package/src/github/__tests__/useGitHubTreeLister.test.ts +14 -0
- package/src/github/useGitHubFileReader.ts +19 -6
- package/src/github/useGitHubTreeLister.ts +7 -3
- package/src/index.ts +4 -0
- package/src/internal/VirtualizedThread.tsx +5 -1
- package/src/session/SessionViewer.tsx +42 -2
- package/src/session/__tests__/useSessionRailViews.test.tsx +12 -0
- package/src/session/__tests__/useWorkspaceReadRefs.test.ts +245 -0
- package/src/session/facets/ChangesTab.tsx +43 -6
- package/src/session/facets/__tests__/ChangesTab.test.tsx +23 -0
- package/src/session/index.ts +2 -0
- package/src/session/useSessionRailViews.tsx +31 -3
- package/src/session/useWorkspaceReadRefs.ts +118 -0
- package/src/workflow/picker/__tests__/recents.test.ts +4 -4
- package/src/workspace/FileViewer.tsx +144 -38
- package/src/workspace/WorkspaceFileReader.ts +21 -0
- package/src/workspace/__tests__/FileViewer.test.tsx +138 -3
- package/src/workspace/__tests__/useWorkspaceFileContent.test.tsx +20 -0
- package/src/workspace/__tests__/workspaceListingCache.test.ts +114 -0
- package/src/workspace/index.ts +4 -1
- package/src/workspace/useWorkspaceEntries.ts +11 -0
- package/src/workspace/useWorkspaceFileContent.ts +4 -1
- package/src/workspace/useWorkspaceFileSearch.ts +8 -3
- package/src/workspace/useWorkspaceFiles.ts +3 -2
- package/src/workspace/workspaceListingCache.ts +21 -7
- package/styles.css +1 -1
- package/workspace/FileViewer.d.ts +1 -1
- package/workspace/FileViewer.d.ts.map +1 -1
- package/workspace/FileViewer.js +55 -5
- package/workspace/FileViewer.js.map +1 -1
- package/workspace/WorkspaceFileReader.d.ts +17 -0
- package/workspace/WorkspaceFileReader.d.ts.map +1 -1
- package/workspace/WorkspaceFileReader.js +18 -0
- package/workspace/WorkspaceFileReader.js.map +1 -1
- package/workspace/index.d.ts +1 -1
- package/workspace/index.d.ts.map +1 -1
- package/workspace/index.js +1 -1
- package/workspace/index.js.map +1 -1
- package/workspace/useWorkspaceEntries.d.ts +11 -0
- package/workspace/useWorkspaceEntries.d.ts.map +1 -1
- package/workspace/useWorkspaceEntries.js.map +1 -1
- package/workspace/useWorkspaceFileContent.d.ts.map +1 -1
- package/workspace/useWorkspaceFileContent.js +4 -1
- package/workspace/useWorkspaceFileContent.js.map +1 -1
- package/workspace/useWorkspaceFileSearch.d.ts.map +1 -1
- package/workspace/useWorkspaceFileSearch.js +8 -3
- package/workspace/useWorkspaceFileSearch.js.map +1 -1
- package/workspace/useWorkspaceFiles.d.ts +2 -1
- package/workspace/useWorkspaceFiles.d.ts.map +1 -1
- package/workspace/useWorkspaceFiles.js +3 -2
- package/workspace/useWorkspaceFiles.js.map +1 -1
- package/workspace/workspaceListingCache.d.ts +1 -1
- package/workspace/workspaceListingCache.d.ts.map +1 -1
- package/workspace/workspaceListingCache.js +20 -7
- package/workspace/workspaceListingCache.js.map +1 -1
|
@@ -1,15 +1,47 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
|
|
2
2
|
import { render, cleanup } from "@testing-library/react";
|
|
3
3
|
import { create, type JsonObject } from "@bufbuild/protobuf";
|
|
4
4
|
import {
|
|
5
5
|
ToolCallSchema,
|
|
6
6
|
type ToolCall,
|
|
7
7
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
8
|
+
import {
|
|
9
|
+
CapturedFileChangeSchema,
|
|
10
|
+
FileChangeSetSchema,
|
|
11
|
+
type FileChangeSet,
|
|
12
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
8
13
|
import {
|
|
9
14
|
ApprovalPolicySource,
|
|
15
|
+
FileChangeKind,
|
|
16
|
+
FileReviewBlockReason,
|
|
17
|
+
ToolCallStatus,
|
|
10
18
|
ToolKind,
|
|
11
19
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
12
|
-
import {
|
|
20
|
+
import type { UseFileChangeContentReturn } from "../useFileChangeContent";
|
|
21
|
+
|
|
22
|
+
// Mock the diff-body content resolver (the seam FileChangesView.test.tsx also
|
|
23
|
+
// mocks): FileChangeDiff calls it unconditionally, and its real implementation
|
|
24
|
+
// needs a Stigmer client for offloaded bodies — irrelevant to these tests.
|
|
25
|
+
let mockContent: UseFileChangeContentReturn;
|
|
26
|
+
|
|
27
|
+
vi.mock("../useFileChangeContent", () => ({
|
|
28
|
+
useFileChangeContent: () => mockContent,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const { ToolCallDetail } = await import("../ToolCallDetail");
|
|
32
|
+
const { FileReviewContext } = await import("../FileReviewContext");
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
mockContent = {
|
|
36
|
+
beforeText: "alpha\n",
|
|
37
|
+
afterText: "captured-after\n",
|
|
38
|
+
isBinary: false,
|
|
39
|
+
isLoading: false,
|
|
40
|
+
error: null,
|
|
41
|
+
isTruncated: false,
|
|
42
|
+
downloadUrl: null,
|
|
43
|
+
};
|
|
44
|
+
});
|
|
13
45
|
|
|
14
46
|
afterEach(cleanup);
|
|
15
47
|
|
|
@@ -21,6 +53,9 @@ function makeToolCall(opts: {
|
|
|
21
53
|
approvalPolicySource?: ApprovalPolicySource;
|
|
22
54
|
startedAt?: string;
|
|
23
55
|
completedAt?: string;
|
|
56
|
+
status?: ToolCallStatus;
|
|
57
|
+
error?: string;
|
|
58
|
+
fileChangeSetId?: string;
|
|
24
59
|
}): ToolCall {
|
|
25
60
|
return create(ToolCallSchema, {
|
|
26
61
|
id: opts.name,
|
|
@@ -32,6 +67,9 @@ function makeToolCall(opts: {
|
|
|
32
67
|
opts.approvalPolicySource ?? ApprovalPolicySource.UNSPECIFIED,
|
|
33
68
|
startedAt: opts.startedAt ?? "",
|
|
34
69
|
completedAt: opts.completedAt ?? "",
|
|
70
|
+
status: opts.status ?? ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
71
|
+
error: opts.error ?? "",
|
|
72
|
+
fileChangeSetId: opts.fileChangeSetId ?? "",
|
|
35
73
|
});
|
|
36
74
|
}
|
|
37
75
|
|
|
@@ -193,3 +231,151 @@ describe("ToolCallDetail — edit body has no duplicate stats", () => {
|
|
|
193
231
|
expect(container.textContent).not.toContain("+1");
|
|
194
232
|
});
|
|
195
233
|
});
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Write body — captured diff on a stamped row, single-bounded fallback
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
describe("ToolCallDetail — write body", () => {
|
|
240
|
+
const SET_ID = "aex-1:0";
|
|
241
|
+
// BoundedContent's collapsed pixel clamp (PREVIEW_MAX_HEIGHT). Its presence/
|
|
242
|
+
// absence is how these tests pin WHICH truncation mechanism bounds the body —
|
|
243
|
+
// jsdom has no layout, so the reveal control itself only appears for the
|
|
244
|
+
// line-count mechanism (CollapsibleCode), never for the pixel clamp.
|
|
245
|
+
const CLAMP = ".max-h-48";
|
|
246
|
+
|
|
247
|
+
/** A change set capturing one reviewable modify of `path`. */
|
|
248
|
+
function capturedSet(
|
|
249
|
+
path = "notes.md",
|
|
250
|
+
overrides?: { diffComplete?: boolean; blockedReason?: FileReviewBlockReason },
|
|
251
|
+
): FileChangeSet {
|
|
252
|
+
return create(FileChangeSetSchema, {
|
|
253
|
+
id: SET_ID,
|
|
254
|
+
changes: [
|
|
255
|
+
create(CapturedFileChangeSchema, {
|
|
256
|
+
id: `${SET_ID}:${path}`,
|
|
257
|
+
pathBefore: path,
|
|
258
|
+
pathAfter: path,
|
|
259
|
+
kind: FileChangeKind.MODIFY,
|
|
260
|
+
diffComplete: overrides?.diffComplete ?? true,
|
|
261
|
+
blockedReason:
|
|
262
|
+
overrides?.blockedReason ?? FileReviewBlockReason.UNSPECIFIED,
|
|
263
|
+
}),
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function writeCall(opts?: Partial<Parameters<typeof makeToolCall>[0]>): ToolCall {
|
|
269
|
+
return makeToolCall({
|
|
270
|
+
name: "write_file",
|
|
271
|
+
toolKind: ToolKind.FILE_WRITE,
|
|
272
|
+
args: { file_path: "notes.md", contents: "proposed body\n" },
|
|
273
|
+
result: "Successfully wrote to 'notes.md'",
|
|
274
|
+
...opts,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function renderWithSet(toolCall: ToolCall, set: FileChangeSet | null) {
|
|
279
|
+
const changeSetsById = new Map(set ? [[SET_ID, set]] : []);
|
|
280
|
+
return render(
|
|
281
|
+
<FileReviewContext.Provider value={{ changeSetsById }}>
|
|
282
|
+
<ToolCallDetail toolCall={toolCall} />
|
|
283
|
+
</FileReviewContext.Provider>,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
it("renders the captured net diff (not the Content block) on a stamped row", () => {
|
|
288
|
+
const { container } = renderWithSet(
|
|
289
|
+
writeCall({ fileChangeSetId: SET_ID }),
|
|
290
|
+
capturedSet(),
|
|
291
|
+
);
|
|
292
|
+
// The captured before/after renders as a real diff table...
|
|
293
|
+
expect(container.querySelector('[data-cursor-target="file-diff"]')).not.toBeNull();
|
|
294
|
+
expect(container.textContent).toContain("captured-after");
|
|
295
|
+
// ...bounded by the shared pixel clamp, with the +N -M stats the write
|
|
296
|
+
// row's header does not carry, and without the fallback's raw args body.
|
|
297
|
+
// (The diff table itself carries a hidden a11y column header named
|
|
298
|
+
// "Content", so the discriminator is the body text, not the label.)
|
|
299
|
+
expect(container.querySelector(CLAMP)).not.toBeNull();
|
|
300
|
+
expect(container.textContent).toContain("+1");
|
|
301
|
+
expect(container.textContent).not.toContain("proposed body");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("renders exactly one reveal control for the content fallback (double-expander regression)", () => {
|
|
305
|
+
const longBody = Array.from({ length: 55 }, (_, i) => `line ${i + 1}`).join("\n");
|
|
306
|
+
const { container, getAllByRole } = render(
|
|
307
|
+
<ToolCallDetail
|
|
308
|
+
toolCall={writeCall({ args: { file_path: "notes.md", contents: longBody } })}
|
|
309
|
+
/>,
|
|
310
|
+
);
|
|
311
|
+
// The file view self-truncates (line-count mechanism, one control)...
|
|
312
|
+
const reveals = getAllByRole("button").filter((b) =>
|
|
313
|
+
/^Show /.test(b.textContent ?? ""),
|
|
314
|
+
);
|
|
315
|
+
expect(reveals).toHaveLength(1);
|
|
316
|
+
expect(reveals[0].textContent).toContain("Show all 55 lines");
|
|
317
|
+
// ...and is never ALSO wrapped in BoundedContent's pixel clamp.
|
|
318
|
+
expect(container.querySelector(CLAMP)).toBeNull();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("falls back to the Content block for every unresolvable capture", () => {
|
|
322
|
+
const cases: Array<[string, ToolCall, FileChangeSet | null]> = [
|
|
323
|
+
["unstamped row", writeCall(), capturedSet()],
|
|
324
|
+
["set missing from context", writeCall({ fileChangeSetId: SET_ID }), null],
|
|
325
|
+
[
|
|
326
|
+
"path not in set",
|
|
327
|
+
writeCall({ fileChangeSetId: SET_ID }),
|
|
328
|
+
capturedSet("other.md"),
|
|
329
|
+
],
|
|
330
|
+
[
|
|
331
|
+
"non-reviewable (secret-withheld) change",
|
|
332
|
+
writeCall({ fileChangeSetId: SET_ID }),
|
|
333
|
+
capturedSet("notes.md", {
|
|
334
|
+
diffComplete: false,
|
|
335
|
+
blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
|
|
336
|
+
}),
|
|
337
|
+
],
|
|
338
|
+
];
|
|
339
|
+
for (const [label, toolCall, set] of cases) {
|
|
340
|
+
const { container } = renderWithSet(toolCall, set);
|
|
341
|
+
expect(container.textContent, label).toContain("Content");
|
|
342
|
+
expect(container.textContent, label).toContain("proposed body");
|
|
343
|
+
expect(
|
|
344
|
+
container.querySelector('[data-cursor-target="file-diff"]'),
|
|
345
|
+
label,
|
|
346
|
+
).toBeNull();
|
|
347
|
+
cleanup();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("keeps a FAILED write's error view inside the pixel clamp (bounding-rule lock)", () => {
|
|
352
|
+
const { container } = render(
|
|
353
|
+
<ToolCallDetail
|
|
354
|
+
toolCall={writeCall({
|
|
355
|
+
status: ToolCallStatus.TOOL_CALL_FAILED,
|
|
356
|
+
error: "EACCES: permission denied, open 'notes.md'",
|
|
357
|
+
})}
|
|
358
|
+
/>,
|
|
359
|
+
);
|
|
360
|
+
expect(container.textContent).toContain("permission denied");
|
|
361
|
+
// The error <pre> has no internal truncation, so BoundedContent bounds it.
|
|
362
|
+
expect(container.querySelector(CLAMP)).not.toBeNull();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("leaves a stamped EDIT row on its per-call args diff, never the captured net diff", () => {
|
|
366
|
+
const edit = makeToolCall({
|
|
367
|
+
name: "edit_file",
|
|
368
|
+
toolKind: ToolKind.FILE_EDIT,
|
|
369
|
+
args: { file_path: "notes.md", old_string: "alpha", new_string: "edit-args-b" },
|
|
370
|
+
result: "Successfully replaced 1 occurrence in 'notes.md'",
|
|
371
|
+
fileChangeSetId: SET_ID,
|
|
372
|
+
});
|
|
373
|
+
const { container } = renderWithSet(edit, capturedSet());
|
|
374
|
+
// The per-call diff from the row's own args renders...
|
|
375
|
+
expect(container.textContent).toContain("edit-args-b");
|
|
376
|
+
// ...not the change set's captured net content...
|
|
377
|
+
expect(container.textContent).not.toContain("captured-after");
|
|
378
|
+
// ...still inside the shared pixel clamp (unchanged behavior).
|
|
379
|
+
expect(container.querySelector(CLAMP)).not.toBeNull();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WriteBackCard — phase-honest rendering of a workspace write-back record.
|
|
3
|
+
* The high-stakes matrix is the error treatment: FAILED errors are
|
|
4
|
+
* destructive, while a PUSHED record carrying a PR error renders it as a
|
|
5
|
+
* degraded notice with the branch info intact (the branch IS live).
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
8
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
9
|
+
import { create } from "@bufbuild/protobuf";
|
|
10
|
+
import {
|
|
11
|
+
WorkspaceWriteBackSchema,
|
|
12
|
+
WorkspaceWriteBackPhase,
|
|
13
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
|
|
14
|
+
import { WriteBackCard } from "../WriteBackCard";
|
|
15
|
+
|
|
16
|
+
function makeWriteBack(overrides: Record<string, unknown> = {}) {
|
|
17
|
+
return create(WorkspaceWriteBackSchema, {
|
|
18
|
+
workspaceEntryName: "acme/api",
|
|
19
|
+
branchName: "stigmer/ses-01test",
|
|
20
|
+
baseBranch: "main",
|
|
21
|
+
commitSha: "abc123",
|
|
22
|
+
diffSummary: " 2 files changed",
|
|
23
|
+
phase: WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_PR_CREATED,
|
|
24
|
+
pullRequestUrl: "https://github.com/acme/api/pull/12",
|
|
25
|
+
pullRequestNumber: 12,
|
|
26
|
+
...overrides,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
afterEach(() => cleanup());
|
|
31
|
+
|
|
32
|
+
describe("WriteBackCard", () => {
|
|
33
|
+
it("renders branch, diff summary, and the PR link for a PR_CREATED record", () => {
|
|
34
|
+
render(<WriteBackCard writeBack={makeWriteBack()} />);
|
|
35
|
+
expect(screen.getByText("stigmer/ses-01test")).toBeTruthy();
|
|
36
|
+
expect(screen.getByText(/2 files changed/)).toBeTruthy();
|
|
37
|
+
const link = screen.getByRole("link", { name: /view pr #12/i });
|
|
38
|
+
expect(link.getAttribute("href")).toBe("https://github.com/acme/api/pull/12");
|
|
39
|
+
expect(screen.getByText("PR Created")).toBeTruthy();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders a FAILED record's error destructively", () => {
|
|
43
|
+
render(
|
|
44
|
+
<WriteBackCard
|
|
45
|
+
writeBack={makeWriteBack({
|
|
46
|
+
phase: WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_FAILED,
|
|
47
|
+
error: "git push failed: connection reset",
|
|
48
|
+
pullRequestUrl: "",
|
|
49
|
+
pullRequestNumber: 0,
|
|
50
|
+
})}
|
|
51
|
+
/>,
|
|
52
|
+
);
|
|
53
|
+
const error = screen.getByText(/git push failed/);
|
|
54
|
+
expect(error.className).toContain("text-destructive");
|
|
55
|
+
expect(screen.getByText("Failed")).toBeTruthy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("renders a PUSHED record's PR error as a degraded notice, branch info intact", () => {
|
|
59
|
+
render(
|
|
60
|
+
<WriteBackCard
|
|
61
|
+
writeBack={makeWriteBack({
|
|
62
|
+
phase: WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_PUSHED,
|
|
63
|
+
error: "No GitHub token available to open a pull request.",
|
|
64
|
+
pullRequestUrl: "",
|
|
65
|
+
pullRequestNumber: 0,
|
|
66
|
+
})}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
69
|
+
// The branch is live — never styled as a failure.
|
|
70
|
+
expect(screen.getByText("stigmer/ses-01test")).toBeTruthy();
|
|
71
|
+
expect(screen.getByText("Pushed")).toBeTruthy();
|
|
72
|
+
const notice = screen.getByText(/no github token/i);
|
|
73
|
+
expect(notice.className).toContain("text-status-degraded");
|
|
74
|
+
expect(notice.className).not.toContain("text-destructive");
|
|
75
|
+
expect(screen.queryByRole("link")).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("renders no error block for a clean record", () => {
|
|
79
|
+
const { container } = render(<WriteBackCard writeBack={makeWriteBack()} />);
|
|
80
|
+
expect(container.querySelector(".bg-destructive-subtle")).toBeNull();
|
|
81
|
+
expect(container.querySelector(".bg-status-degraded-subtle")).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
changeSetReviewability,
|
|
25
25
|
deriveEffectiveVerdicts,
|
|
26
26
|
fileReviewability,
|
|
27
|
+
fileReviewRowChange,
|
|
27
28
|
fileReviewRowState,
|
|
28
29
|
} from "../file-review-status";
|
|
29
30
|
|
|
@@ -291,6 +292,52 @@ describe("changeForRowPath", () => {
|
|
|
291
292
|
});
|
|
292
293
|
});
|
|
293
294
|
|
|
295
|
+
describe("fileReviewRowChange", () => {
|
|
296
|
+
it("returns null with no set or path", () => {
|
|
297
|
+
const set = reviewSet({
|
|
298
|
+
status: FileChangeSetStatus.AWAITING_REVIEW,
|
|
299
|
+
changes: [fileChange("f1", "a.ts")],
|
|
300
|
+
});
|
|
301
|
+
expect(fileReviewRowChange(undefined, "a.ts")).toBeNull();
|
|
302
|
+
expect(fileReviewRowChange(set, null)).toBeNull();
|
|
303
|
+
expect(fileReviewRowChange(set, "")).toBeNull();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("resolves a reviewable change by row path (same matcher as the badge)", () => {
|
|
307
|
+
const set = reviewSet({
|
|
308
|
+
status: FileChangeSetStatus.AWAITING_REVIEW,
|
|
309
|
+
changes: [fileChange("f1", "src/a.ts")],
|
|
310
|
+
});
|
|
311
|
+
expect(fileReviewRowChange(set, "src/a.ts")?.id).toBe("f1");
|
|
312
|
+
expect(fileReviewRowChange(set, "/home/user/ws/src/a.ts")?.id).toBe("f1");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("returns null for a path absent from the set", () => {
|
|
316
|
+
const set = reviewSet({
|
|
317
|
+
status: FileChangeSetStatus.AWAITING_REVIEW,
|
|
318
|
+
changes: [fileChange("f1", "a.ts")],
|
|
319
|
+
});
|
|
320
|
+
expect(fileReviewRowChange(set, "gone.ts")).toBeNull();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("returns null for a non-reviewable change (honest degradation)", () => {
|
|
324
|
+
const set = reviewSet({
|
|
325
|
+
status: FileChangeSetStatus.AWAITING_REVIEW,
|
|
326
|
+
changes: [
|
|
327
|
+
create(CapturedFileChangeSchema, {
|
|
328
|
+
id: "f1",
|
|
329
|
+
pathBefore: "secret.env",
|
|
330
|
+
pathAfter: "secret.env",
|
|
331
|
+
kind: FileChangeKind.MODIFY,
|
|
332
|
+
diffComplete: false,
|
|
333
|
+
blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
|
|
334
|
+
}),
|
|
335
|
+
],
|
|
336
|
+
});
|
|
337
|
+
expect(fileReviewRowChange(set, "secret.env")).toBeNull();
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
294
341
|
describe("fileReviewRowState", () => {
|
|
295
342
|
it("returns null with no set (not yet projected / unknown id)", () => {
|
|
296
343
|
expect(fileReviewRowState(undefined, "a.ts")).toBeNull();
|