@stigmer/react 3.0.9-dev.20260616060535 → 3.1.0
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/ApprovalCard.d.ts +47 -3
- package/execution/ApprovalCard.d.ts.map +1 -1
- package/execution/ApprovalCard.js +138 -70
- package/execution/ApprovalCard.js.map +1 -1
- package/execution/ApprovalContext.d.ts +72 -0
- package/execution/ApprovalContext.d.ts.map +1 -0
- package/execution/ApprovalContext.js +41 -0
- package/execution/ApprovalContext.js.map +1 -0
- package/execution/ArtifactCard.d.ts.map +1 -1
- package/execution/ArtifactCard.js +3 -1
- package/execution/ArtifactCard.js.map +1 -1
- package/execution/ArtifactPreviewModal.d.ts.map +1 -1
- package/execution/ArtifactPreviewModal.js +5 -3
- package/execution/ArtifactPreviewModal.js.map +1 -1
- package/execution/EmptyChangeNotice.d.ts +38 -0
- package/execution/EmptyChangeNotice.d.ts.map +1 -0
- package/execution/EmptyChangeNotice.js +27 -0
- package/execution/EmptyChangeNotice.js.map +1 -0
- package/execution/FileChangesView.d.ts +81 -0
- package/execution/FileChangesView.d.ts.map +1 -0
- package/execution/FileChangesView.js +164 -0
- package/execution/FileChangesView.js.map +1 -0
- package/execution/FilePathLink.d.ts +24 -4
- package/execution/FilePathLink.d.ts.map +1 -1
- package/execution/FilePathLink.js +31 -9
- package/execution/FilePathLink.js.map +1 -1
- package/execution/FileReviewCard.d.ts +111 -0
- package/execution/FileReviewCard.d.ts.map +1 -0
- package/execution/FileReviewCard.js +558 -0
- package/execution/FileReviewCard.js.map +1 -0
- package/execution/FileReviewContext.d.ts +35 -0
- package/execution/FileReviewContext.d.ts.map +1 -0
- package/execution/FileReviewContext.js +23 -0
- package/execution/FileReviewContext.js.map +1 -0
- package/execution/FileReviewDock.d.ts +73 -0
- package/execution/FileReviewDock.d.ts.map +1 -0
- package/execution/FileReviewDock.js +65 -0
- package/execution/FileReviewDock.js.map +1 -0
- package/execution/McpToolDetail.d.ts.map +1 -1
- package/execution/McpToolDetail.js +38 -5
- package/execution/McpToolDetail.js.map +1 -1
- package/execution/MessageThread.d.ts +42 -3
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +263 -18
- package/execution/MessageThread.js.map +1 -1
- package/execution/PlanArtifactCard.d.ts.map +1 -1
- package/execution/PlanArtifactCard.js +3 -1
- package/execution/PlanArtifactCard.js.map +1 -1
- package/execution/PlanCompletionCard.d.ts +1 -1
- package/execution/PlanCompletionCard.js +1 -1
- package/execution/ResultView.d.ts +19 -1
- package/execution/ResultView.d.ts.map +1 -1
- package/execution/ResultView.js +141 -40
- package/execution/ResultView.js.map +1 -1
- package/execution/SubAgentSection.d.ts.map +1 -1
- package/execution/SubAgentSection.js +45 -6
- package/execution/SubAgentSection.js.map +1 -1
- package/execution/TerminalSession.d.ts +34 -0
- package/execution/TerminalSession.d.ts.map +1 -0
- package/execution/TerminalSession.js +34 -0
- package/execution/TerminalSession.js.map +1 -0
- package/execution/TodoCard.d.ts +53 -0
- package/execution/TodoCard.d.ts.map +1 -0
- package/execution/TodoCard.js +77 -0
- package/execution/TodoCard.js.map +1 -0
- package/execution/ToolArgsView.d.ts +7 -1
- package/execution/ToolArgsView.d.ts.map +1 -1
- package/execution/ToolArgsView.js +10 -9
- package/execution/ToolArgsView.js.map +1 -1
- package/execution/ToolCallDetail.d.ts +10 -1
- package/execution/ToolCallDetail.d.ts.map +1 -1
- package/execution/ToolCallDetail.js +66 -19
- package/execution/ToolCallDetail.js.map +1 -1
- package/execution/ToolCallGroup.d.ts +28 -18
- package/execution/ToolCallGroup.d.ts.map +1 -1
- package/execution/ToolCallGroup.js +39 -148
- package/execution/ToolCallGroup.js.map +1 -1
- package/execution/ToolCallItem.d.ts +36 -8
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +140 -21
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/ToolRunGroup.d.ts +48 -0
- package/execution/ToolRunGroup.d.ts.map +1 -0
- package/execution/ToolRunGroup.js +168 -0
- package/execution/ToolRunGroup.js.map +1 -0
- package/execution/approval-provenance.d.ts +2 -0
- package/execution/approval-provenance.d.ts.map +1 -0
- package/execution/approval-provenance.js +6 -0
- package/execution/approval-provenance.js.map +1 -0
- package/execution/artifact-utils.d.ts +0 -10
- package/execution/artifact-utils.d.ts.map +1 -1
- package/execution/artifact-utils.js +0 -17
- package/execution/artifact-utils.js.map +1 -1
- package/execution/execution-phases.d.ts +1 -6
- package/execution/execution-phases.d.ts.map +1 -1
- package/execution/execution-phases.js +4 -14
- package/execution/execution-phases.js.map +1 -1
- package/execution/file-path-resolver.d.ts +28 -0
- package/execution/file-path-resolver.d.ts.map +1 -1
- package/execution/file-path-resolver.js +24 -0
- package/execution/file-path-resolver.js.map +1 -1
- package/execution/file-review-status.d.ts +127 -0
- package/execution/file-review-status.d.ts.map +1 -0
- package/execution/file-review-status.js +135 -0
- package/execution/file-review-status.js.map +1 -0
- package/execution/index.d.ts +39 -5
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +23 -3
- package/execution/index.js.map +1 -1
- package/execution/segment-tool-calls.d.ts +46 -0
- package/execution/segment-tool-calls.d.ts.map +1 -0
- package/execution/segment-tool-calls.js +71 -0
- package/execution/segment-tool-calls.js.map +1 -0
- package/execution/tool-categories.d.ts +62 -1
- package/execution/tool-categories.d.ts.map +1 -1
- package/execution/tool-categories.js +96 -2
- package/execution/tool-categories.js.map +1 -1
- package/execution/tool-presenter.d.ts +38 -2
- package/execution/tool-presenter.d.ts.map +1 -1
- package/execution/tool-presenter.js +23 -1
- package/execution/tool-presenter.js.map +1 -1
- package/execution/tool-rendering-primitives.d.ts.map +1 -1
- package/execution/tool-rendering-primitives.js +3 -2
- package/execution/tool-rendering-primitives.js.map +1 -1
- package/execution/useArtifactDownload.d.ts +44 -0
- package/execution/useArtifactDownload.d.ts.map +1 -0
- package/execution/useArtifactDownload.js +80 -0
- package/execution/useArtifactDownload.js.map +1 -0
- package/execution/useArtifactDownloadUrl.d.ts +67 -0
- package/execution/useArtifactDownloadUrl.d.ts.map +1 -0
- package/execution/useArtifactDownloadUrl.js +59 -0
- package/execution/useArtifactDownloadUrl.js.map +1 -0
- package/execution/useFileChangeContent.d.ts +61 -0
- package/execution/useFileChangeContent.d.ts.map +1 -0
- package/execution/useFileChangeContent.js +89 -0
- package/execution/useFileChangeContent.js.map +1 -0
- package/execution/useFileReview.d.ts +89 -0
- package/execution/useFileReview.d.ts.map +1 -0
- package/execution/useFileReview.js +106 -0
- package/execution/useFileReview.js.map +1 -0
- package/execution/useSubmitApproval.d.ts +17 -2
- package/execution/useSubmitApproval.d.ts.map +1 -1
- package/execution/useSubmitApproval.js +27 -23
- package/execution/useSubmitApproval.js.map +1 -1
- package/execution/useToolOutputContent.d.ts +44 -0
- package/execution/useToolOutputContent.d.ts.map +1 -0
- package/execution/useToolOutputContent.js +31 -0
- package/execution/useToolOutputContent.js.map +1 -0
- package/index.d.ts +6 -6
- package/index.d.ts.map +1 -1
- package/index.js +3 -3
- package/index.js.map +1 -1
- package/internal/ApprovalPeekBar.d.ts +23 -0
- package/internal/ApprovalPeekBar.d.ts.map +1 -0
- package/internal/ApprovalPeekBar.js +25 -0
- package/internal/ApprovalPeekBar.js.map +1 -0
- package/internal/BoundedContent.d.ts +49 -0
- package/internal/BoundedContent.d.ts.map +1 -0
- package/internal/BoundedContent.js +50 -0
- package/internal/BoundedContent.js.map +1 -0
- package/internal/DecisionButton.d.ts +47 -0
- package/internal/DecisionButton.d.ts.map +1 -0
- package/internal/DecisionButton.js +42 -0
- package/internal/DecisionButton.js.map +1 -0
- package/internal/InCardDecisionError.d.ts +31 -0
- package/internal/InCardDecisionError.d.ts.map +1 -0
- package/internal/InCardDecisionError.js +23 -0
- package/internal/InCardDecisionError.js.map +1 -0
- package/internal/RevealToggle.d.ts +36 -0
- package/internal/RevealToggle.d.ts.map +1 -0
- package/internal/RevealToggle.js +32 -0
- package/internal/RevealToggle.js.map +1 -0
- package/internal/VirtualizedThread.d.ts +7 -1
- package/internal/VirtualizedThread.d.ts.map +1 -1
- package/internal/VirtualizedThread.js +7 -3
- package/internal/VirtualizedThread.js.map +1 -1
- package/internal/useAutoDisclosure.d.ts +34 -0
- package/internal/useAutoDisclosure.d.ts.map +1 -0
- package/internal/useAutoDisclosure.js +53 -0
- package/internal/useAutoDisclosure.js.map +1 -0
- package/internal/useElapsedSince.d.ts +30 -0
- package/internal/useElapsedSince.d.ts.map +1 -0
- package/internal/useElapsedSince.js +70 -0
- package/internal/useElapsedSince.js.map +1 -0
- package/internal/useIsOverflowing.d.ts +30 -0
- package/internal/useIsOverflowing.d.ts.map +1 -0
- package/internal/useIsOverflowing.js +47 -0
- package/internal/useIsOverflowing.js.map +1 -0
- package/internal/useIsTextTruncated.d.ts +24 -0
- package/internal/useIsTextTruncated.d.ts.map +1 -0
- package/internal/useIsTextTruncated.js +41 -0
- package/internal/useIsTextTruncated.js.map +1 -0
- package/internal/useKeyedSubmission.d.ts +49 -0
- package/internal/useKeyedSubmission.d.ts.map +1 -0
- package/internal/useKeyedSubmission.js +67 -0
- package/internal/useKeyedSubmission.js.map +1 -0
- package/package.json +4 -4
- package/session/SessionViewer.d.ts +2 -2
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +10 -4
- package/session/SessionViewer.js.map +1 -1
- package/session/index.d.ts +2 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js +1 -0
- package/session/index.js.map +1 -1
- package/session/inspector/ChangesTab.d.ts +8 -5
- package/session/inspector/ChangesTab.d.ts.map +1 -1
- package/session/inspector/ChangesTab.js +11 -8
- package/session/inspector/ChangesTab.js.map +1 -1
- package/session/inspector/SessionInspector.d.ts +4 -4
- package/session/inspector/SessionInspector.d.ts.map +1 -1
- package/session/inspector/SessionInspector.js +4 -4
- package/session/inspector/SessionInspector.js.map +1 -1
- package/session/inspector/index.d.ts +0 -2
- package/session/inspector/index.d.ts.map +1 -1
- package/session/inspector/index.js +0 -1
- package/session/inspector/index.js.map +1 -1
- package/session/inspector/useSessionInspector.d.ts +8 -4
- package/session/inspector/useSessionInspector.d.ts.map +1 -1
- package/session/inspector/useSessionInspector.js +27 -20
- package/session/inspector/useSessionInspector.js.map +1 -1
- package/session/useSessionConversation.d.ts +37 -3
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +18 -2
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionFileChanges.d.ts +74 -0
- package/session/useSessionFileChanges.d.ts.map +1 -0
- package/session/useSessionFileChanges.js +178 -0
- package/session/useSessionFileChanges.js.map +1 -0
- package/src/__tests__/styles-border-layer-invariant.test.ts +105 -0
- package/src/execution/ApprovalCard.tsx +368 -230
- package/src/execution/ApprovalContext.ts +109 -0
- package/src/execution/ArtifactCard.tsx +10 -6
- package/src/execution/ArtifactPreviewModal.tsx +12 -6
- package/src/execution/EmptyChangeNotice.tsx +62 -0
- package/src/execution/FileChangesView.tsx +383 -0
- package/src/execution/FilePathLink.tsx +65 -12
- package/src/execution/FileReviewCard.tsx +1118 -0
- package/src/execution/FileReviewContext.ts +53 -0
- package/src/execution/FileReviewDock.tsx +157 -0
- package/src/execution/McpToolDetail.tsx +98 -32
- package/src/execution/MessageThread.tsx +374 -9
- package/src/execution/PlanArtifactCard.tsx +9 -6
- package/src/execution/PlanCompletionCard.tsx +1 -1
- package/src/execution/ResultView.tsx +333 -162
- package/src/execution/SubAgentSection.tsx +58 -11
- package/src/execution/TerminalSession.tsx +91 -0
- package/src/execution/TodoCard.tsx +186 -0
- package/src/execution/ToolArgsView.tsx +28 -17
- package/src/execution/ToolCallDetail.tsx +137 -26
- package/src/execution/ToolCallGroup.tsx +68 -296
- package/src/execution/ToolCallItem.tsx +265 -67
- package/src/execution/ToolRunGroup.tsx +302 -0
- package/src/execution/__tests__/ApprovalCard.test.tsx +395 -5
- package/src/execution/__tests__/ArtifactPreviewModal.test.tsx +0 -1
- package/src/execution/__tests__/EmptyChangeNotice.test.tsx +24 -0
- package/src/execution/__tests__/FileChangesView.test.tsx +296 -0
- package/src/execution/__tests__/FilePathLink.test.tsx +92 -0
- package/src/execution/__tests__/FileReviewCard.test.tsx +1044 -0
- package/src/execution/__tests__/FileReviewDock.test.tsx +134 -0
- package/src/execution/__tests__/McpToolDetail.test.tsx +79 -0
- package/src/execution/__tests__/MessageThread.test.tsx +324 -0
- package/src/execution/__tests__/PlanArtifactCard.test.tsx +37 -16
- package/src/execution/__tests__/ResultView.test.tsx +280 -2
- package/src/execution/__tests__/SubAgentSection.test.tsx +180 -0
- package/src/execution/__tests__/TerminalSession.test.tsx +72 -0
- package/src/execution/__tests__/TodoCard.test.tsx +102 -0
- package/src/execution/__tests__/ToolArgsView.test.tsx +48 -0
- package/src/execution/__tests__/ToolCallDetail.test.tsx +195 -0
- package/src/execution/__tests__/ToolCallGroup.test.tsx +132 -0
- package/src/execution/__tests__/ToolCallItem.test.tsx +521 -0
- package/src/execution/__tests__/ToolRunGroup.test.tsx +137 -0
- package/src/execution/__tests__/blanked-narration.test.tsx +149 -0
- package/src/execution/__tests__/buildThreadItems-approvals.test.ts +304 -0
- package/src/execution/__tests__/buildThreadItems-file-review.test.ts +331 -0
- package/src/execution/__tests__/file-path-resolver.test.ts +44 -0
- package/src/execution/__tests__/file-review-status.test.ts +343 -0
- package/src/execution/__tests__/segment-tool-calls.test.ts +124 -0
- package/src/execution/__tests__/subagent-file-review-badge.test.tsx +273 -0
- package/src/execution/__tests__/thread-keys.test.ts +195 -0
- package/src/execution/__tests__/tool-presenter.test.tsx +95 -0
- package/src/execution/__tests__/useArtifactDownload.test.tsx +67 -0
- package/src/execution/__tests__/useArtifactDownloadUrl.test.tsx +51 -0
- package/src/execution/__tests__/useFileChangeContent.test.tsx +240 -0
- package/src/execution/__tests__/useFileReview.test.tsx +236 -0
- package/src/execution/__tests__/useSubmitApproval.test.tsx +165 -0
- package/src/execution/__tests__/useToolOutputContent.test.tsx +82 -0
- package/src/execution/approval-provenance.ts +8 -0
- package/src/execution/artifact-utils.ts +0 -18
- package/src/execution/execution-phases.ts +4 -16
- package/src/execution/file-path-resolver.ts +38 -0
- package/src/execution/file-review-status.ts +219 -0
- package/src/execution/index.ts +84 -4
- package/src/execution/segment-tool-calls.ts +101 -0
- package/src/execution/tool-categories.ts +128 -1
- package/src/execution/tool-presenter.ts +58 -3
- package/src/execution/tool-rendering-primitives.tsx +11 -14
- package/src/execution/useArtifactDownload.ts +109 -0
- package/src/execution/useArtifactDownloadUrl.ts +116 -0
- package/src/execution/useFileChangeContent.ts +149 -0
- package/src/execution/useFileReview.ts +197 -0
- package/src/execution/useSubmitApproval.ts +49 -27
- package/src/execution/useToolOutputContent.ts +70 -0
- package/src/index.ts +60 -1
- package/src/internal/ApprovalPeekBar.tsx +73 -0
- package/src/internal/BoundedContent.tsx +97 -0
- package/src/internal/DecisionButton.tsx +127 -0
- package/src/internal/InCardDecisionError.tsx +47 -0
- package/src/internal/RevealToggle.tsx +91 -0
- package/src/internal/VirtualizedThread.tsx +28 -2
- package/src/internal/__tests__/ApprovalPeekBar.test.tsx +34 -0
- package/src/internal/__tests__/BoundedContent.test.tsx +92 -0
- package/src/internal/__tests__/DecisionButton.test.tsx +107 -0
- package/src/internal/__tests__/RevealToggle.test.tsx +61 -0
- package/src/internal/__tests__/useAutoDisclosure.test.ts +74 -0
- package/src/internal/__tests__/useElapsedSince.test.tsx +68 -0
- package/src/internal/__tests__/useIsOverflowing.test.ts +36 -0
- package/src/internal/__tests__/useKeyedSubmission.test.tsx +126 -0
- package/src/internal/useAutoDisclosure.ts +58 -0
- package/src/internal/useElapsedSince.ts +72 -0
- package/src/internal/useIsOverflowing.ts +61 -0
- package/src/internal/useIsTextTruncated.ts +55 -0
- package/src/internal/useKeyedSubmission.ts +101 -0
- package/src/session/SessionViewer.tsx +21 -4
- package/src/session/__tests__/SessionViewer-fileReviewDock.test.tsx +198 -0
- package/src/session/__tests__/audienceWiring.test.tsx +4 -0
- package/src/session/__tests__/useSessionFileChanges.test.ts +303 -0
- package/src/session/index.ts +3 -0
- package/src/session/inspector/ChangesTab.tsx +20 -17
- package/src/session/inspector/SessionInspector.tsx +12 -10
- package/src/session/inspector/__tests__/ArtifactsTab.test.tsx +0 -1
- package/src/session/inspector/__tests__/ChangesTab.test.tsx +38 -0
- package/src/session/inspector/__tests__/useSessionInspector.test.ts +46 -61
- package/src/session/inspector/index.ts +0 -3
- package/src/session/inspector/useSessionInspector.ts +27 -20
- package/src/session/useSessionConversation.ts +80 -3
- package/src/session/useSessionFileChanges.ts +207 -0
- package/src/styles.css +35 -10
- package/src/version-history/DiffViewer.tsx +10 -7
- package/src/version-history/UnifiedDiffView.tsx +66 -0
- package/src/version-history/__tests__/DiffViewer.test.tsx +57 -0
- package/src/version-history/__tests__/UnifiedDiffView.test.tsx +50 -0
- package/src/version-history/__tests__/computeDiff.test.ts +35 -0
- package/src/version-history/__tests__/parseUnifiedDiff.test.ts +82 -0
- package/src/version-history/computeDiff.ts +6 -37
- package/src/version-history/diff-hunks.ts +63 -0
- package/src/version-history/index.ts +1 -0
- package/src/version-history/parseUnifiedDiff.ts +34 -0
- package/src/workflow/WorkflowExecutionApprovalCard.tsx +49 -24
- package/src/workflow/WorkflowExecutionTimeline.tsx +15 -4
- package/src/workflow/WorkflowExecutionTimelineEvent.tsx +8 -3
- package/src/workflow/WorkflowExecutionViewer.tsx +72 -28
- package/src/workflow/WorkflowFileReviewList.tsx +152 -0
- package/src/workflow/WorkflowTaskApprovalCard.tsx +37 -85
- package/src/workflow/__tests__/ExecutionInspector.test.tsx +30 -2
- package/src/workflow/__tests__/WorkflowExecutionApprovalCard.test.tsx +129 -0
- package/src/workflow/__tests__/WorkflowFileReviewList.test.tsx +129 -0
- package/src/workflow/__tests__/WorkflowTaskApprovalCard.test.tsx +43 -11
- package/src/workflow/__tests__/useWorkflowExecutionActions.test.tsx +276 -0
- package/src/workflow/execution-inspector/ExecutionInspector.tsx +42 -18
- package/src/workflow/index.ts +5 -0
- package/src/workflow/useWorkflowExecutionActions.ts +177 -29
- package/styles.css +1 -1
- package/version-history/DiffViewer.d.ts.map +1 -1
- package/version-history/DiffViewer.js +9 -3
- package/version-history/DiffViewer.js.map +1 -1
- package/version-history/UnifiedDiffView.d.ts +29 -0
- package/version-history/UnifiedDiffView.d.ts.map +1 -0
- package/version-history/UnifiedDiffView.js +36 -0
- package/version-history/UnifiedDiffView.js.map +1 -0
- package/version-history/computeDiff.d.ts +3 -1
- package/version-history/computeDiff.d.ts.map +1 -1
- package/version-history/computeDiff.js +5 -35
- package/version-history/computeDiff.js.map +1 -1
- package/version-history/diff-hunks.d.ts +21 -0
- package/version-history/diff-hunks.d.ts.map +1 -0
- package/version-history/diff-hunks.js +58 -0
- package/version-history/diff-hunks.js.map +1 -0
- package/version-history/index.d.ts +1 -0
- package/version-history/index.d.ts.map +1 -1
- package/version-history/index.js +1 -0
- package/version-history/index.js.map +1 -1
- package/version-history/parseUnifiedDiff.d.ts +22 -0
- package/version-history/parseUnifiedDiff.d.ts.map +1 -0
- package/version-history/parseUnifiedDiff.js +34 -0
- package/version-history/parseUnifiedDiff.js.map +1 -0
- package/workflow/WorkflowExecutionApprovalCard.d.ts +8 -1
- package/workflow/WorkflowExecutionApprovalCard.d.ts.map +1 -1
- package/workflow/WorkflowExecutionApprovalCard.js +17 -3
- package/workflow/WorkflowExecutionApprovalCard.js.map +1 -1
- package/workflow/WorkflowExecutionTimeline.d.ts +11 -2
- package/workflow/WorkflowExecutionTimeline.d.ts.map +1 -1
- package/workflow/WorkflowExecutionTimeline.js +2 -2
- package/workflow/WorkflowExecutionTimeline.js.map +1 -1
- package/workflow/WorkflowExecutionTimelineEvent.d.ts +4 -1
- package/workflow/WorkflowExecutionTimelineEvent.d.ts.map +1 -1
- package/workflow/WorkflowExecutionTimelineEvent.js +2 -2
- package/workflow/WorkflowExecutionTimelineEvent.js.map +1 -1
- package/workflow/WorkflowExecutionViewer.d.ts.map +1 -1
- package/workflow/WorkflowExecutionViewer.js +18 -10
- package/workflow/WorkflowExecutionViewer.js.map +1 -1
- package/workflow/WorkflowFileReviewList.d.ts +46 -0
- package/workflow/WorkflowFileReviewList.d.ts.map +1 -0
- package/workflow/WorkflowFileReviewList.js +48 -0
- package/workflow/WorkflowFileReviewList.js.map +1 -0
- package/workflow/WorkflowTaskApprovalCard.d.ts +8 -1
- package/workflow/WorkflowTaskApprovalCard.d.ts.map +1 -1
- package/workflow/WorkflowTaskApprovalCard.js +12 -19
- package/workflow/WorkflowTaskApprovalCard.js.map +1 -1
- package/workflow/execution-inspector/ExecutionInspector.d.ts +21 -4
- package/workflow/execution-inspector/ExecutionInspector.d.ts.map +1 -1
- package/workflow/execution-inspector/ExecutionInspector.js +6 -3
- package/workflow/execution-inspector/ExecutionInspector.js.map +1 -1
- package/workflow/index.d.ts +1 -0
- package/workflow/index.d.ts.map +1 -1
- package/workflow/index.js +1 -0
- package/workflow/index.js.map +1 -1
- package/workflow/useWorkflowExecutionActions.d.ts +62 -4
- package/workflow/useWorkflowExecutionActions.d.ts.map +1 -1
- package/workflow/useWorkflowExecutionActions.js +94 -18
- package/workflow/useWorkflowExecutionActions.js.map +1 -1
- package/session/inspector/PlanTab.d.ts +0 -15
- package/session/inspector/PlanTab.d.ts.map +0 -1
- package/session/inspector/PlanTab.js +0 -19
- package/session/inspector/PlanTab.js.map +0 -1
- package/src/session/inspector/PlanTab.tsx +0 -31
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import { FileContentSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
5
|
+
import {
|
|
6
|
+
CapturedFileChangeSchema,
|
|
7
|
+
FileChangeSetSchema,
|
|
8
|
+
FileDecisionSchema,
|
|
9
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
10
|
+
import {
|
|
11
|
+
DiffCompleteness,
|
|
12
|
+
FileCaptureClass,
|
|
13
|
+
FileChangeKind,
|
|
14
|
+
FileChangeSetStatus,
|
|
15
|
+
FileDecisionAction,
|
|
16
|
+
FileDecisionOrigin,
|
|
17
|
+
FileDecisionScope,
|
|
18
|
+
FileReviewBlockReason,
|
|
19
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
20
|
+
import type { UseFileChangeContentReturn } from "../useFileChangeContent";
|
|
21
|
+
|
|
22
|
+
// FileReviewCard renders FileChangeDiff (per file), which pulls in the
|
|
23
|
+
// offload-resolving content hook transitively. Mock it so the diff path renders
|
|
24
|
+
// without artifact-fetch context, then import the component after the mock.
|
|
25
|
+
let mockReturn: UseFileChangeContentReturn;
|
|
26
|
+
vi.mock("../useFileChangeContent", () => ({
|
|
27
|
+
useFileChangeContent: () => mockReturn,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const { FileReviewCard } = await import("../FileReviewCard");
|
|
31
|
+
const { fileDecisionKey } = await import("../useFileReview");
|
|
32
|
+
|
|
33
|
+
function setContent(overrides?: Partial<UseFileChangeContentReturn>) {
|
|
34
|
+
mockReturn = {
|
|
35
|
+
beforeText: "old\n",
|
|
36
|
+
afterText: "new\n",
|
|
37
|
+
isBinary: false,
|
|
38
|
+
isLoading: false,
|
|
39
|
+
error: null,
|
|
40
|
+
isTruncated: false,
|
|
41
|
+
downloadUrl: null,
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
beforeEach(() => setContent());
|
|
47
|
+
afterEach(cleanup);
|
|
48
|
+
|
|
49
|
+
const noop = () => {};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Open the bar's diff expander. A COMPLETE undecided set and a settled record
|
|
53
|
+
* start collapsed (the bar carries the verdict; the transcript's stamped rows
|
|
54
|
+
* carry the previews), so tests that assert on per-file content expand first —
|
|
55
|
+
* exactly the user's own path to the authoritative diffs.
|
|
56
|
+
*/
|
|
57
|
+
function expandCard() {
|
|
58
|
+
fireEvent.click(
|
|
59
|
+
screen.getByRole("button", { name: /^(Review|Show)$/ }),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function inline(value: string, isBinary = false) {
|
|
64
|
+
return create(FileContentSchema, { body: { case: "inline", value }, isBinary });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function capturedChange(opts: {
|
|
68
|
+
id: string;
|
|
69
|
+
path: string;
|
|
70
|
+
fileDigest: string;
|
|
71
|
+
diffComplete?: boolean;
|
|
72
|
+
captureClass?: FileCaptureClass;
|
|
73
|
+
}) {
|
|
74
|
+
return create(CapturedFileChangeSchema, {
|
|
75
|
+
id: opts.id,
|
|
76
|
+
pathBefore: opts.path,
|
|
77
|
+
pathAfter: opts.path,
|
|
78
|
+
kind: FileChangeKind.MODIFY,
|
|
79
|
+
captureClass: opts.captureClass ?? FileCaptureClass.GIT_TRACKED,
|
|
80
|
+
before: inline("old\n"),
|
|
81
|
+
after: inline("new\n"),
|
|
82
|
+
fileDigest: opts.fileDigest,
|
|
83
|
+
diffComplete: opts.diffComplete ?? true,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A binary change (real wire shape): both sides present + flagged, incomplete. */
|
|
88
|
+
function capturedBinaryChange(opts: {
|
|
89
|
+
id: string;
|
|
90
|
+
path: string;
|
|
91
|
+
fileDigest: string;
|
|
92
|
+
captureClass?: FileCaptureClass;
|
|
93
|
+
}) {
|
|
94
|
+
return create(CapturedFileChangeSchema, {
|
|
95
|
+
id: opts.id,
|
|
96
|
+
pathBefore: opts.path,
|
|
97
|
+
pathAfter: opts.path,
|
|
98
|
+
kind: FileChangeKind.MODIFY,
|
|
99
|
+
captureClass: opts.captureClass ?? FileCaptureClass.GIT_TRACKED,
|
|
100
|
+
before: inline("\u0000old", true),
|
|
101
|
+
after: inline("\u0000new", true),
|
|
102
|
+
fileDigest: opts.fileDigest,
|
|
103
|
+
diffComplete: false,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A diff-unavailable change (real wire shape): content-less + incomplete — the
|
|
109
|
+
* shape both the secret gate and the size backstop produce. `blockedReason`
|
|
110
|
+
* carries the honest cause (doc 15); omit it to model a pre-doc-15/unknown row.
|
|
111
|
+
*/
|
|
112
|
+
function capturedUnavailableChange(opts: {
|
|
113
|
+
id: string;
|
|
114
|
+
path: string;
|
|
115
|
+
fileDigest: string;
|
|
116
|
+
captureClass?: FileCaptureClass;
|
|
117
|
+
blockedReason?: FileReviewBlockReason;
|
|
118
|
+
}) {
|
|
119
|
+
return create(CapturedFileChangeSchema, {
|
|
120
|
+
id: opts.id,
|
|
121
|
+
pathBefore: opts.path,
|
|
122
|
+
pathAfter: opts.path,
|
|
123
|
+
kind: FileChangeKind.MODIFY,
|
|
124
|
+
captureClass: opts.captureClass ?? FileCaptureClass.GIT_IGNORED_CAPTURED,
|
|
125
|
+
fileDigest: opts.fileDigest,
|
|
126
|
+
diffComplete: false,
|
|
127
|
+
blockedReason: opts.blockedReason ?? FileReviewBlockReason.UNSPECIFIED,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function fileDecision(fileChangeId: string, action: FileDecisionAction) {
|
|
132
|
+
return create(FileDecisionSchema, {
|
|
133
|
+
changeSetId: "aex-1:0",
|
|
134
|
+
fileChangeId,
|
|
135
|
+
scope: FileDecisionScope.FILE,
|
|
136
|
+
action,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Single-file change set (the whole set IS the file — no per-file controls). */
|
|
141
|
+
function changeSet(opts?: {
|
|
142
|
+
diffCompleteness?: DiffCompleteness;
|
|
143
|
+
change?: ReturnType<typeof capturedChange>;
|
|
144
|
+
}) {
|
|
145
|
+
return create(FileChangeSetSchema, {
|
|
146
|
+
id: "aex-1:0",
|
|
147
|
+
status: FileChangeSetStatus.AWAITING_REVIEW,
|
|
148
|
+
aggregateDigest: "agg-1",
|
|
149
|
+
diffCompleteness: opts?.diffCompleteness ?? DiffCompleteness.COMPLETE,
|
|
150
|
+
changes: [
|
|
151
|
+
opts?.change ??
|
|
152
|
+
capturedChange({ id: "aex-1:0:src/a.ts", path: "src/a.ts", fileDigest: "d-a" }),
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Two-file change set with per-file Keep/Discard controls. */
|
|
158
|
+
function multiChangeSet(opts?: {
|
|
159
|
+
diffCompleteness?: DiffCompleteness;
|
|
160
|
+
fc1?: ReturnType<typeof capturedChange>;
|
|
161
|
+
fc2?: ReturnType<typeof capturedChange>;
|
|
162
|
+
decisions?: ReturnType<typeof fileDecision>[];
|
|
163
|
+
}) {
|
|
164
|
+
return create(FileChangeSetSchema, {
|
|
165
|
+
id: "aex-1:0",
|
|
166
|
+
status: FileChangeSetStatus.AWAITING_REVIEW,
|
|
167
|
+
aggregateDigest: "agg-1",
|
|
168
|
+
diffCompleteness: opts?.diffCompleteness ?? DiffCompleteness.COMPLETE,
|
|
169
|
+
changes: [
|
|
170
|
+
opts?.fc1 ?? capturedChange({ id: "fc1", path: "src/a.ts", fileDigest: "d-fc1" }),
|
|
171
|
+
opts?.fc2 ?? capturedChange({ id: "fc2", path: "src/b.ts", fileDigest: "d-fc2" }),
|
|
172
|
+
],
|
|
173
|
+
decisions: opts?.decisions ?? [],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
describe("FileReviewCard", () => {
|
|
178
|
+
describe("single-file set (bulk only)", () => {
|
|
179
|
+
it("renders the change set summary and file diff", () => {
|
|
180
|
+
render(<FileReviewCard fileChangeSet={changeSet()} onSubmit={noop} />);
|
|
181
|
+
expect(screen.getByText(/awaiting review/i)).toBeTruthy();
|
|
182
|
+
expect(
|
|
183
|
+
screen.getByRole("alert", { name: /review 1 file change/i }),
|
|
184
|
+
).toBeTruthy();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("shows no per-file Keep/Discard radios for a single file", () => {
|
|
188
|
+
render(<FileReviewCard fileChangeSet={changeSet()} onSubmit={noop} />);
|
|
189
|
+
expect(screen.queryByRole("radio")).toBeNull();
|
|
190
|
+
// The bulk label is singular (no misleading "all" for one file).
|
|
191
|
+
expect(screen.getByRole("button", { name: "Approve" })).toBeTruthy();
|
|
192
|
+
expect(screen.getByRole("button", { name: "Reject" })).toBeTruthy();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("submits a CHANGE_SET APPROVE bound to the aggregate digest on Approve", () => {
|
|
196
|
+
const onSubmit = vi.fn();
|
|
197
|
+
render(<FileReviewCard fileChangeSet={changeSet()} onSubmit={onSubmit} />);
|
|
198
|
+
|
|
199
|
+
fireEvent.click(screen.getByRole("button", { name: "Approve" }));
|
|
200
|
+
|
|
201
|
+
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
202
|
+
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
|
|
203
|
+
scope: FileDecisionScope.CHANGE_SET,
|
|
204
|
+
expectedDigest: "agg-1",
|
|
205
|
+
// A COMPLETE set carries no acknowledgment (nothing unreviewable).
|
|
206
|
+
acknowledgeUnreviewable: false,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("submits a CHANGE_SET REJECT on Reject", () => {
|
|
211
|
+
const onSubmit = vi.fn();
|
|
212
|
+
render(<FileReviewCard fileChangeSet={changeSet()} onSubmit={onSubmit} />);
|
|
213
|
+
|
|
214
|
+
fireEvent.click(screen.getByRole("button", { name: "Reject" }));
|
|
215
|
+
|
|
216
|
+
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.REJECT, {
|
|
217
|
+
scope: FileDecisionScope.CHANGE_SET,
|
|
218
|
+
expectedDigest: "agg-1",
|
|
219
|
+
acknowledgeUnreviewable: false,
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("switches a single INCOMPLETE file to the per-file control (no CHANGE_SET footer)", () => {
|
|
224
|
+
// A lone binary set is BINARY_SUMMARY_ONLY, but "Keep all" is a multi-file
|
|
225
|
+
// affordance: with a single file the bulk footer is dropped and the per-file
|
|
226
|
+
// "Keep anyway" / Discard control (DD-16) is the only decision surface.
|
|
227
|
+
render(
|
|
228
|
+
<FileReviewCard
|
|
229
|
+
fileChangeSet={changeSet({
|
|
230
|
+
diffCompleteness: DiffCompleteness.BINARY_SUMMARY_ONLY,
|
|
231
|
+
change: capturedBinaryChange({ id: "aex-1:0:img.png", path: "img.png", fileDigest: "d" }),
|
|
232
|
+
})}
|
|
233
|
+
onSubmit={noop}
|
|
234
|
+
/>,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// No bulk footer for a single incomplete file.
|
|
238
|
+
expect(screen.queryByRole("button", { name: "Approve" })).toBeNull();
|
|
239
|
+
expect(screen.queryByRole("button", { name: "Reject" })).toBeNull();
|
|
240
|
+
// The per-file control is present, with an enabled binary "Keep anyway".
|
|
241
|
+
expect(
|
|
242
|
+
(screen.getByRole("radio", { name: /Keep img.png anyway/i }) as HTMLButtonElement).disabled,
|
|
243
|
+
).toBe(false);
|
|
244
|
+
expect(
|
|
245
|
+
(screen.getByRole("radio", { name: "Discard img.png" }) as HTMLButtonElement).disabled,
|
|
246
|
+
).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("multi-file set (per-file controls)", () => {
|
|
251
|
+
it("renders a Keep/Discard radio per file plus whole-set controls", () => {
|
|
252
|
+
render(<FileReviewCard fileChangeSet={multiChangeSet()} onSubmit={noop} />);
|
|
253
|
+
expandCard();
|
|
254
|
+
|
|
255
|
+
expect(screen.getByRole("radio", { name: "Keep src/a.ts" })).toBeTruthy();
|
|
256
|
+
expect(screen.getByRole("radio", { name: "Discard src/a.ts" })).toBeTruthy();
|
|
257
|
+
expect(screen.getByRole("radio", { name: "Keep src/b.ts" })).toBeTruthy();
|
|
258
|
+
expect(screen.getByRole("radio", { name: "Discard src/b.ts" })).toBeTruthy();
|
|
259
|
+
expect(screen.getByRole("button", { name: "Approve all" })).toBeTruthy();
|
|
260
|
+
expect(screen.getByRole("button", { name: "Reject all" })).toBeTruthy();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("submits a FILE APPROVE bound to the file digest on Keep", () => {
|
|
264
|
+
const onSubmit = vi.fn();
|
|
265
|
+
render(<FileReviewCard fileChangeSet={multiChangeSet()} onSubmit={onSubmit} />);
|
|
266
|
+
expandCard();
|
|
267
|
+
|
|
268
|
+
fireEvent.click(screen.getByRole("radio", { name: "Keep src/a.ts" }));
|
|
269
|
+
|
|
270
|
+
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
|
|
271
|
+
scope: FileDecisionScope.FILE,
|
|
272
|
+
fileChangeId: "fc1",
|
|
273
|
+
expectedDigest: "d-fc1",
|
|
274
|
+
acknowledgeUnreviewable: false,
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("submits a FILE REJECT bound to the file digest on Discard", () => {
|
|
279
|
+
const onSubmit = vi.fn();
|
|
280
|
+
render(<FileReviewCard fileChangeSet={multiChangeSet()} onSubmit={onSubmit} />);
|
|
281
|
+
expandCard();
|
|
282
|
+
|
|
283
|
+
fireEvent.click(screen.getByRole("radio", { name: "Discard src/b.ts" }));
|
|
284
|
+
|
|
285
|
+
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.REJECT, {
|
|
286
|
+
scope: FileDecisionScope.FILE,
|
|
287
|
+
fileChangeId: "fc2",
|
|
288
|
+
expectedDigest: "d-fc2",
|
|
289
|
+
acknowledgeUnreviewable: false,
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("disables Keep (but not Discard) for an incomplete file, leaving complete files keepable", () => {
|
|
294
|
+
const onSubmit = vi.fn();
|
|
295
|
+
render(
|
|
296
|
+
<FileReviewCard
|
|
297
|
+
fileChangeSet={multiChangeSet({
|
|
298
|
+
diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
|
|
299
|
+
fc2: capturedUnavailableChange({ id: "fc2", path: "src/b.ts", fileDigest: "d-fc2" }),
|
|
300
|
+
})}
|
|
301
|
+
onSubmit={onSubmit}
|
|
302
|
+
/>,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// The unavailable file can only be discarded.
|
|
306
|
+
expect(
|
|
307
|
+
(screen.getByRole("radio", { name: "Keep src/b.ts" }) as HTMLButtonElement)
|
|
308
|
+
.disabled,
|
|
309
|
+
).toBe(true);
|
|
310
|
+
expect(
|
|
311
|
+
(screen.getByRole("radio", { name: "Discard src/b.ts" }) as HTMLButtonElement)
|
|
312
|
+
.disabled,
|
|
313
|
+
).toBe(false);
|
|
314
|
+
// The complete file is still keepable individually.
|
|
315
|
+
expect(
|
|
316
|
+
(screen.getByRole("radio", { name: "Keep src/a.ts" }) as HTMLButtonElement)
|
|
317
|
+
.disabled,
|
|
318
|
+
).toBe(false);
|
|
319
|
+
// The whole-set Approve is blocked while the set is not COMPLETE.
|
|
320
|
+
expect(
|
|
321
|
+
(screen.getByRole("button", { name: "Approve all" }) as HTMLButtonElement)
|
|
322
|
+
.disabled,
|
|
323
|
+
).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("shows a committed verdict as checked and flips it on the other option", () => {
|
|
327
|
+
const onSubmit = vi.fn();
|
|
328
|
+
render(
|
|
329
|
+
<FileReviewCard
|
|
330
|
+
fileChangeSet={multiChangeSet({
|
|
331
|
+
decisions: [fileDecision("fc1", FileDecisionAction.APPROVE)],
|
|
332
|
+
})}
|
|
333
|
+
onSubmit={onSubmit}
|
|
334
|
+
/>,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
expect(
|
|
338
|
+
screen.getByRole("radio", { name: "Keep src/a.ts" }).getAttribute("aria-checked"),
|
|
339
|
+
).toBe("true");
|
|
340
|
+
|
|
341
|
+
// Flipping is allowed: clicking Discard records a new (last-wins) decision.
|
|
342
|
+
fireEvent.click(screen.getByRole("radio", { name: "Discard src/a.ts" }));
|
|
343
|
+
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.REJECT, {
|
|
344
|
+
scope: FileDecisionScope.FILE,
|
|
345
|
+
fileChangeId: "fc1",
|
|
346
|
+
expectedDigest: "d-fc1",
|
|
347
|
+
acknowledgeUnreviewable: false,
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("reflects last-write-wins: a later REJECT supersedes an earlier APPROVE", () => {
|
|
352
|
+
render(
|
|
353
|
+
<FileReviewCard
|
|
354
|
+
fileChangeSet={multiChangeSet({
|
|
355
|
+
decisions: [
|
|
356
|
+
fileDecision("fc1", FileDecisionAction.APPROVE),
|
|
357
|
+
fileDecision("fc1", FileDecisionAction.REJECT),
|
|
358
|
+
],
|
|
359
|
+
})}
|
|
360
|
+
onSubmit={noop}
|
|
361
|
+
/>,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
expect(
|
|
365
|
+
screen.getByRole("radio", { name: "Discard src/a.ts" }).getAttribute("aria-checked"),
|
|
366
|
+
).toBe("true");
|
|
367
|
+
expect(
|
|
368
|
+
screen.getByRole("radio", { name: "Keep src/a.ts" }).getAttribute("aria-checked"),
|
|
369
|
+
).toBe("false");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("shows review progress for a partially-decided set", () => {
|
|
373
|
+
render(
|
|
374
|
+
<FileReviewCard
|
|
375
|
+
fileChangeSet={multiChangeSet({
|
|
376
|
+
decisions: [fileDecision("fc1", FileDecisionAction.APPROVE)],
|
|
377
|
+
})}
|
|
378
|
+
onSubmit={noop}
|
|
379
|
+
/>,
|
|
380
|
+
);
|
|
381
|
+
expect(screen.getByText(/1 of 2 files reviewed/i)).toBeTruthy();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("relabels the bulk action to 'remaining' once some files are decided", () => {
|
|
385
|
+
const onSubmit = vi.fn();
|
|
386
|
+
render(
|
|
387
|
+
<FileReviewCard
|
|
388
|
+
fileChangeSet={multiChangeSet({
|
|
389
|
+
decisions: [fileDecision("fc1", FileDecisionAction.APPROVE)],
|
|
390
|
+
})}
|
|
391
|
+
onSubmit={onSubmit}
|
|
392
|
+
/>,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const approveRemaining = screen.getByRole("button", { name: "Approve remaining" });
|
|
396
|
+
expect(approveRemaining).toBeTruthy();
|
|
397
|
+
expect(screen.getByRole("button", { name: "Reject remaining" })).toBeTruthy();
|
|
398
|
+
|
|
399
|
+
fireEvent.click(approveRemaining);
|
|
400
|
+
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
|
|
401
|
+
scope: FileDecisionScope.CHANGE_SET,
|
|
402
|
+
expectedDigest: "agg-1",
|
|
403
|
+
acknowledgeUnreviewable: false,
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("disables a file's options while that file's decision is in flight", () => {
|
|
408
|
+
const submitting = new Set([fileDecisionKey("aex-1:0", "fc1")]);
|
|
409
|
+
render(
|
|
410
|
+
<FileReviewCard
|
|
411
|
+
fileChangeSet={multiChangeSet()}
|
|
412
|
+
onSubmit={noop}
|
|
413
|
+
submittingDecisionKeys={submitting}
|
|
414
|
+
/>,
|
|
415
|
+
);
|
|
416
|
+
expandCard();
|
|
417
|
+
|
|
418
|
+
expect(
|
|
419
|
+
(screen.getByRole("radio", { name: "Keep src/a.ts" }) as HTMLButtonElement)
|
|
420
|
+
.disabled,
|
|
421
|
+
).toBe(true);
|
|
422
|
+
expect(
|
|
423
|
+
(screen.getByRole("radio", { name: "Discard src/a.ts" }) as HTMLButtonElement)
|
|
424
|
+
.disabled,
|
|
425
|
+
).toBe(true);
|
|
426
|
+
// A different file is unaffected.
|
|
427
|
+
expect(
|
|
428
|
+
(screen.getByRole("radio", { name: "Keep src/b.ts" }) as HTMLButtonElement)
|
|
429
|
+
.disabled,
|
|
430
|
+
).toBe(false);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("disables the bulk action while a whole-set decision is in flight", () => {
|
|
434
|
+
const submitting = new Set(["aex-1:0"]);
|
|
435
|
+
render(
|
|
436
|
+
<FileReviewCard
|
|
437
|
+
fileChangeSet={multiChangeSet()}
|
|
438
|
+
onSubmit={noop}
|
|
439
|
+
submittingDecisionKeys={submitting}
|
|
440
|
+
/>,
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
expect(
|
|
444
|
+
(screen.getByRole("button", { name: "Approve all" }) as HTMLButtonElement)
|
|
445
|
+
.disabled,
|
|
446
|
+
).toBe(true);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
describe("decision errors (in-card surfacing)", () => {
|
|
451
|
+
it("renders no error element when the map is empty (default path)", () => {
|
|
452
|
+
const { container } = render(
|
|
453
|
+
<FileReviewCard fileChangeSet={changeSet()} onSubmit={noop} />,
|
|
454
|
+
);
|
|
455
|
+
expect(container.querySelector('[data-cursor-target="file-review-error"]')).toBeNull();
|
|
456
|
+
expect(
|
|
457
|
+
container.querySelector('[data-cursor-target="file-review-file-error"]'),
|
|
458
|
+
).toBeNull();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("surfaces a whole-set failure beside the bulk buttons", () => {
|
|
462
|
+
const decisionErrors = new Map([["aex-1:0", new Error("digest mismatch")]]);
|
|
463
|
+
render(
|
|
464
|
+
<FileReviewCard
|
|
465
|
+
fileChangeSet={changeSet()}
|
|
466
|
+
onSubmit={noop}
|
|
467
|
+
decisionErrors={decisionErrors}
|
|
468
|
+
/>,
|
|
469
|
+
);
|
|
470
|
+
const err = screen.getByText(/Couldn.t submit decision — digest mismatch/);
|
|
471
|
+
expect(err.getAttribute("data-cursor-target")).toBe("file-review-error");
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("surfaces a per-file failure under the right file row only", () => {
|
|
475
|
+
const decisionErrors = new Map([
|
|
476
|
+
[fileDecisionKey("aex-1:0", "fc1"), new Error("network down")],
|
|
477
|
+
]);
|
|
478
|
+
render(
|
|
479
|
+
<FileReviewCard
|
|
480
|
+
fileChangeSet={multiChangeSet()}
|
|
481
|
+
onSubmit={noop}
|
|
482
|
+
decisionErrors={decisionErrors}
|
|
483
|
+
/>,
|
|
484
|
+
);
|
|
485
|
+
expandCard();
|
|
486
|
+
// Exactly one per-file error, and it carries fc1's message.
|
|
487
|
+
const fileErrors = screen.getAllByText(/Couldn.t save/);
|
|
488
|
+
expect(fileErrors).toHaveLength(1);
|
|
489
|
+
expect(fileErrors[0].textContent).toMatch(/network down/);
|
|
490
|
+
// The whole-set error is NOT shown (the failure was per-file).
|
|
491
|
+
expect(screen.queryByText(/Couldn.t submit decision/)).toBeNull();
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe("blocked / partial states (Slice 6)", () => {
|
|
496
|
+
it("labels a binary file and offers an enabled 'Keep anyway' (DD-16)", () => {
|
|
497
|
+
const onSubmit = vi.fn();
|
|
498
|
+
render(
|
|
499
|
+
<FileReviewCard
|
|
500
|
+
fileChangeSet={multiChangeSet({
|
|
501
|
+
diffCompleteness: DiffCompleteness.BINARY_SUMMARY_ONLY,
|
|
502
|
+
fc2: capturedBinaryChange({ id: "fc2", path: "img.png", fileDigest: "d-fc2" }),
|
|
503
|
+
})}
|
|
504
|
+
onSubmit={onSubmit}
|
|
505
|
+
/>,
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
// Target the per-file note specifically ("...to review"), not the bulk
|
|
509
|
+
// keep-all notice which also mentions "no text diff".
|
|
510
|
+
expect(screen.getByText(/Binary file.*no text diff to review/i)).toBeTruthy();
|
|
511
|
+
// A binary is keepable via an explicit, enabled "Keep anyway".
|
|
512
|
+
const keepAnyway = screen.getByRole("radio", {
|
|
513
|
+
name: /Keep img.png anyway/i,
|
|
514
|
+
}) as HTMLButtonElement;
|
|
515
|
+
expect(keepAnyway.disabled).toBe(false);
|
|
516
|
+
expect(
|
|
517
|
+
(screen.getByRole("radio", { name: "Discard img.png" }) as HTMLButtonElement).disabled,
|
|
518
|
+
).toBe(false);
|
|
519
|
+
// The reviewable sibling keeps its plain "Keep".
|
|
520
|
+
expect(
|
|
521
|
+
(screen.getByRole("radio", { name: "Keep src/a.ts" }) as HTMLButtonElement).disabled,
|
|
522
|
+
).toBe(false);
|
|
523
|
+
|
|
524
|
+
// Clicking "Keep anyway" submits an acknowledged FILE approve.
|
|
525
|
+
fireEvent.click(keepAnyway);
|
|
526
|
+
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
|
|
527
|
+
scope: FileDecisionScope.FILE,
|
|
528
|
+
fileChangeId: "fc2",
|
|
529
|
+
expectedDigest: "d-fc2",
|
|
530
|
+
acknowledgeUnreviewable: true,
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("labels a diff-unavailable file and associates the reason via aria-describedby", () => {
|
|
535
|
+
render(
|
|
536
|
+
<FileReviewCard
|
|
537
|
+
fileChangeSet={multiChangeSet({
|
|
538
|
+
diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
|
|
539
|
+
fc2: capturedUnavailableChange({ id: "fc2", path: "src/b.ts", fileDigest: "d-fc2" }),
|
|
540
|
+
})}
|
|
541
|
+
onSubmit={noop}
|
|
542
|
+
/>,
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
const note = screen.getByText(/full diff isn.t available to review/i);
|
|
546
|
+
expect(note).toBeTruthy();
|
|
547
|
+
// The blocked file's radiogroup points at exactly that note (a11y).
|
|
548
|
+
const group = screen.getByRole("radiogroup", { name: "Decision for src/b.ts" });
|
|
549
|
+
const describedBy = group.getAttribute("aria-describedby");
|
|
550
|
+
expect(describedBy).toBeTruthy();
|
|
551
|
+
expect(note.getAttribute("id")).toBe(describedBy);
|
|
552
|
+
// The reviewable sibling's group carries no such description.
|
|
553
|
+
const okGroup = screen.getByRole("radiogroup", { name: "Decision for src/a.ts" });
|
|
554
|
+
expect(okGroup.getAttribute("aria-describedby")).toBeFalsy();
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("gives a secret-withheld file its own honest copy (doc 15)", () => {
|
|
558
|
+
render(
|
|
559
|
+
<FileReviewCard
|
|
560
|
+
fileChangeSet={multiChangeSet({
|
|
561
|
+
diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
|
|
562
|
+
fc2: capturedUnavailableChange({
|
|
563
|
+
id: "fc2",
|
|
564
|
+
path: ".env",
|
|
565
|
+
fileDigest: "d-fc2",
|
|
566
|
+
blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
|
|
567
|
+
}),
|
|
568
|
+
})}
|
|
569
|
+
onSubmit={noop}
|
|
570
|
+
/>,
|
|
571
|
+
);
|
|
572
|
+
expect(screen.getByText(/looks like a secret/i)).toBeTruthy();
|
|
573
|
+
// It is NOT the generic copy — the cause is now honest, not agnostic.
|
|
574
|
+
expect(screen.queryByText(/full diff isn.t available to review/i)).toBeNull();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("gives a size-elided file its own honest copy, distinct from secret (doc 15)", () => {
|
|
578
|
+
render(
|
|
579
|
+
<FileReviewCard
|
|
580
|
+
fileChangeSet={multiChangeSet({
|
|
581
|
+
diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
|
|
582
|
+
fc2: capturedUnavailableChange({
|
|
583
|
+
id: "fc2",
|
|
584
|
+
path: "src/huge.ts",
|
|
585
|
+
fileDigest: "d-fc2",
|
|
586
|
+
blockedReason: FileReviewBlockReason.SIZE_ELIDED,
|
|
587
|
+
}),
|
|
588
|
+
})}
|
|
589
|
+
onSubmit={noop}
|
|
590
|
+
/>,
|
|
591
|
+
);
|
|
592
|
+
expect(screen.getByText(/too large to display/i)).toBeTruthy();
|
|
593
|
+
expect(screen.queryByText(/looks like a secret/i)).toBeNull();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("shows a provenance badge for a gitignored capture", () => {
|
|
597
|
+
render(
|
|
598
|
+
<FileReviewCard
|
|
599
|
+
fileChangeSet={changeSet({
|
|
600
|
+
change: capturedChange({
|
|
601
|
+
id: "x",
|
|
602
|
+
path: ".env.local",
|
|
603
|
+
fileDigest: "d",
|
|
604
|
+
captureClass: FileCaptureClass.GIT_IGNORED_CAPTURED,
|
|
605
|
+
}),
|
|
606
|
+
})}
|
|
607
|
+
onSubmit={noop}
|
|
608
|
+
/>,
|
|
609
|
+
);
|
|
610
|
+
expandCard();
|
|
611
|
+
const badge = screen.getByText("gitignored");
|
|
612
|
+
expect(badge).toBeTruthy();
|
|
613
|
+
expect(badge.getAttribute("aria-label")).toMatch(/ignored by git/i);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it("shows no provenance badge for an ordinary git-tracked change", () => {
|
|
617
|
+
render(<FileReviewCard fileChangeSet={changeSet()} onSubmit={noop} />);
|
|
618
|
+
expect(screen.queryByText("gitignored")).toBeNull();
|
|
619
|
+
expect(screen.queryByText("outside git")).toBeNull();
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("uses unavailable-specific set copy when a diff-unavailable file is present", () => {
|
|
623
|
+
render(
|
|
624
|
+
<FileReviewCard
|
|
625
|
+
fileChangeSet={multiChangeSet({
|
|
626
|
+
diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
|
|
627
|
+
fc2: capturedUnavailableChange({ id: "fc2", path: "src/b.ts", fileDigest: "d-fc2" }),
|
|
628
|
+
})}
|
|
629
|
+
onSubmit={noop}
|
|
630
|
+
/>,
|
|
631
|
+
);
|
|
632
|
+
expect(screen.getByText(/isn.t available to review, so the whole set/i)).toBeTruthy();
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("offers per-file granular control alongside 'Keep all' in a binary-only set (DD-17)", () => {
|
|
636
|
+
// A binary-only set is keepable in one shot, but the per-file escape hatch
|
|
637
|
+
// stays: keep the reviewable text file on its own, keep-anyway or discard
|
|
638
|
+
// the binary. Bulk "Keep all" and the granular rows coexist.
|
|
639
|
+
render(
|
|
640
|
+
<FileReviewCard
|
|
641
|
+
fileChangeSet={multiChangeSet({
|
|
642
|
+
diffCompleteness: DiffCompleteness.BINARY_SUMMARY_ONLY,
|
|
643
|
+
fc2: capturedBinaryChange({ id: "fc2", path: "img.png", fileDigest: "d-fc2" }),
|
|
644
|
+
})}
|
|
645
|
+
onSubmit={noop}
|
|
646
|
+
/>,
|
|
647
|
+
);
|
|
648
|
+
expect(screen.getByRole("button", { name: "Keep all" })).toBeTruthy();
|
|
649
|
+
expect(screen.getByRole("radio", { name: "Keep src/a.ts" })).toBeTruthy();
|
|
650
|
+
expect(screen.getByRole("radio", { name: /Keep img.png anyway/i })).toBeTruthy();
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("keeps a single-file binary via an acknowledged FILE approve; discard still works", () => {
|
|
654
|
+
const onSubmit = vi.fn();
|
|
655
|
+
render(
|
|
656
|
+
<FileReviewCard
|
|
657
|
+
fileChangeSet={changeSet({
|
|
658
|
+
diffCompleteness: DiffCompleteness.BINARY_SUMMARY_ONLY,
|
|
659
|
+
change: capturedBinaryChange({ id: "aex-1:0:img.png", path: "img.png", fileDigest: "d" }),
|
|
660
|
+
})}
|
|
661
|
+
onSubmit={onSubmit}
|
|
662
|
+
/>,
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
// No CHANGE_SET footer for a single incomplete file ("Keep all" is a
|
|
666
|
+
// multi-file affordance); the per-file control carries an enabled "Keep anyway".
|
|
667
|
+
expect(screen.queryByRole("button", { name: "Approve" })).toBeNull();
|
|
668
|
+
fireEvent.click(screen.getByRole("radio", { name: /Keep img.png anyway/i }));
|
|
669
|
+
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
|
|
670
|
+
scope: FileDecisionScope.FILE,
|
|
671
|
+
fileChangeId: "aex-1:0:img.png",
|
|
672
|
+
expectedDigest: "d",
|
|
673
|
+
acknowledgeUnreviewable: true,
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
fireEvent.click(screen.getByRole("radio", { name: "Discard img.png" }));
|
|
677
|
+
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.REJECT, {
|
|
678
|
+
scope: FileDecisionScope.FILE,
|
|
679
|
+
fileChangeId: "aex-1:0:img.png",
|
|
680
|
+
expectedDigest: "d",
|
|
681
|
+
acknowledgeUnreviewable: false,
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("offers an enabled 'Keep all' for a BINARY_SUMMARY_ONLY set that acknowledges the whole set (DD-17)", () => {
|
|
686
|
+
// A set whose only blocker is binary files is keepable in one action: the
|
|
687
|
+
// bulk approve is enabled, relabeled "Keep all", and carries the whole-set
|
|
688
|
+
// acknowledgment bound to the aggregate digest.
|
|
689
|
+
const onSubmit = vi.fn();
|
|
690
|
+
render(
|
|
691
|
+
<FileReviewCard
|
|
692
|
+
fileChangeSet={multiChangeSet({
|
|
693
|
+
diffCompleteness: DiffCompleteness.BINARY_SUMMARY_ONLY,
|
|
694
|
+
fc2: capturedBinaryChange({ id: "fc2", path: "img.png", fileDigest: "d-fc2" }),
|
|
695
|
+
})}
|
|
696
|
+
onSubmit={onSubmit}
|
|
697
|
+
/>,
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
const keepAll = screen.getByRole("button", { name: "Keep all" }) as HTMLButtonElement;
|
|
701
|
+
expect(keepAll.disabled).toBe(false);
|
|
702
|
+
// The notice explains the enabled keep-all rather than blocking it.
|
|
703
|
+
expect(screen.getByText(/Keep all keeps every file as-is/i)).toBeTruthy();
|
|
704
|
+
|
|
705
|
+
fireEvent.click(keepAll);
|
|
706
|
+
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
|
|
707
|
+
scope: FileDecisionScope.CHANGE_SET,
|
|
708
|
+
expectedDigest: "agg-1",
|
|
709
|
+
acknowledgeUnreviewable: true,
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it("still disables the whole-set Approve when a file is truly unavailable (not binary-only)", () => {
|
|
714
|
+
// A PARTIAL_BLOCKED set (a secret/elided file with no keepable bytes) can
|
|
715
|
+
// never be kept in one shot — the bulk Approve stays disabled.
|
|
716
|
+
render(
|
|
717
|
+
<FileReviewCard
|
|
718
|
+
fileChangeSet={multiChangeSet({
|
|
719
|
+
diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
|
|
720
|
+
fc2: capturedUnavailableChange({ id: "fc2", path: "src/b.ts", fileDigest: "d-fc2" }),
|
|
721
|
+
})}
|
|
722
|
+
onSubmit={noop}
|
|
723
|
+
/>,
|
|
724
|
+
);
|
|
725
|
+
expect(
|
|
726
|
+
(screen.getByRole("button", { name: "Approve all" }) as HTMLButtonElement).disabled,
|
|
727
|
+
).toBe(true);
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
describe("read-only settled card (interactive=false)", () => {
|
|
732
|
+
it("shows a passive record with no decision controls", () => {
|
|
733
|
+
render(<FileReviewCard fileChangeSet={changeSet()} interactive={false} />);
|
|
734
|
+
// A neutral group + settled header, never the actionable "Review" alert.
|
|
735
|
+
expect(screen.getByRole("group", { name: /1 file change/i })).toBeTruthy();
|
|
736
|
+
expect(screen.getByText("File changes")).toBeTruthy();
|
|
737
|
+
expect(screen.queryByText("Review file changes")).toBeNull();
|
|
738
|
+
// No decision affordances at all.
|
|
739
|
+
expect(
|
|
740
|
+
screen.queryByRole("button", { name: /approve|reject|keep/i }),
|
|
741
|
+
).toBeNull();
|
|
742
|
+
expect(screen.queryByRole("radio")).toBeNull();
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("renders without an onSubmit callback", () => {
|
|
746
|
+
expect(() =>
|
|
747
|
+
render(<FileReviewCard fileChangeSet={changeSet()} interactive={false} />),
|
|
748
|
+
).not.toThrow();
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it("renders each file's committed verdict (Kept / Discarded)", () => {
|
|
752
|
+
render(
|
|
753
|
+
<FileReviewCard
|
|
754
|
+
fileChangeSet={multiChangeSet({
|
|
755
|
+
decisions: [
|
|
756
|
+
fileDecision("fc1", FileDecisionAction.APPROVE),
|
|
757
|
+
fileDecision("fc2", FileDecisionAction.REJECT),
|
|
758
|
+
],
|
|
759
|
+
})}
|
|
760
|
+
interactive={false}
|
|
761
|
+
/>,
|
|
762
|
+
);
|
|
763
|
+
// The collapsed bar already tells the verdict story in one line…
|
|
764
|
+
expect(screen.getByText("1 kept · 1 discarded")).toBeTruthy();
|
|
765
|
+
// …and expanding shows each file's committed verdict beside its diff.
|
|
766
|
+
expandCard();
|
|
767
|
+
expect(screen.getByText("Kept")).toBeTruthy();
|
|
768
|
+
expect(screen.getByText("Discarded")).toBeTruthy();
|
|
769
|
+
expect(screen.queryByRole("radio")).toBeNull();
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("labels an undecided file 'Not reviewed'", () => {
|
|
773
|
+
render(<FileReviewCard fileChangeSet={changeSet()} interactive={false} />);
|
|
774
|
+
expandCard();
|
|
775
|
+
expect(screen.getByText("Not reviewed")).toBeTruthy();
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it("reads every file as kept when the set was decided via a whole-set approve", () => {
|
|
779
|
+
// A "Keep all" set carries only a CHANGE_SET decision; the settled record
|
|
780
|
+
// folds it onto every file (the reconcile's own precedence) rather than
|
|
781
|
+
// showing "Not reviewed" for files without their own FILE decision.
|
|
782
|
+
const bulkApprove = create(FileDecisionSchema, {
|
|
783
|
+
changeSetId: "aex-1:0",
|
|
784
|
+
scope: FileDecisionScope.CHANGE_SET,
|
|
785
|
+
action: FileDecisionAction.APPROVE,
|
|
786
|
+
});
|
|
787
|
+
render(
|
|
788
|
+
<FileReviewCard
|
|
789
|
+
fileChangeSet={multiChangeSet({ decisions: [bulkApprove] })}
|
|
790
|
+
interactive={false}
|
|
791
|
+
/>,
|
|
792
|
+
);
|
|
793
|
+
expect(screen.getByText("2 kept")).toBeTruthy();
|
|
794
|
+
expandCard();
|
|
795
|
+
expect(screen.getAllByText("Kept")).toHaveLength(2);
|
|
796
|
+
expect(screen.queryByText("Not reviewed")).toBeNull();
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it("labels a policy auto-kept set 'Kept automatically' — never as a human review (DD-28)", () => {
|
|
800
|
+
// The approved-command auto-keep authors a CHANGE_SET approve with origin
|
|
801
|
+
// POLICY_APPROVED_COMMAND. The record must say the platform kept it
|
|
802
|
+
// because the user approved the command — not read like a human decided
|
|
803
|
+
// it here ("2 kept").
|
|
804
|
+
const policyKeep = create(FileDecisionSchema, {
|
|
805
|
+
changeSetId: "aex-1:0",
|
|
806
|
+
scope: FileDecisionScope.CHANGE_SET,
|
|
807
|
+
action: FileDecisionAction.APPROVE,
|
|
808
|
+
origin: FileDecisionOrigin.POLICY_APPROVED_COMMAND,
|
|
809
|
+
});
|
|
810
|
+
render(
|
|
811
|
+
<FileReviewCard
|
|
812
|
+
fileChangeSet={multiChangeSet({ decisions: [policyKeep] })}
|
|
813
|
+
interactive={false}
|
|
814
|
+
/>,
|
|
815
|
+
);
|
|
816
|
+
expect(
|
|
817
|
+
screen.getByText("Kept automatically — produced by a command you approved"),
|
|
818
|
+
).toBeTruthy();
|
|
819
|
+
expect(screen.queryByText("2 kept")).toBeNull();
|
|
820
|
+
// The per-file verdicts still fold as kept (the decision is a real
|
|
821
|
+
// CHANGE_SET approve), so the expanded record stays consistent.
|
|
822
|
+
expandCard();
|
|
823
|
+
expect(screen.getAllByText("Kept")).toHaveLength(2);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it("a human whole-set approve keeps the plain verdict summary (origin USER)", () => {
|
|
827
|
+
const humanKeep = create(FileDecisionSchema, {
|
|
828
|
+
changeSetId: "aex-1:0",
|
|
829
|
+
scope: FileDecisionScope.CHANGE_SET,
|
|
830
|
+
action: FileDecisionAction.APPROVE,
|
|
831
|
+
origin: FileDecisionOrigin.USER,
|
|
832
|
+
});
|
|
833
|
+
render(
|
|
834
|
+
<FileReviewCard
|
|
835
|
+
fileChangeSet={multiChangeSet({ decisions: [humanKeep] })}
|
|
836
|
+
interactive={false}
|
|
837
|
+
/>,
|
|
838
|
+
);
|
|
839
|
+
expect(screen.getByText("2 kept")).toBeTruthy();
|
|
840
|
+
expect(screen.queryByText(/Kept automatically/)).toBeNull();
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
// The session thread passes showDiffs={false}: its stamped edit rows already
|
|
845
|
+
// show every diff in place, so the expanded body is a compact file list and
|
|
846
|
+
// the card is purely the decision surface. Decision semantics (scopes,
|
|
847
|
+
// digests, acknowledgments) must be byte-identical to diff mode — these
|
|
848
|
+
// cases mirror the decision cases above without the diff DOM. The default
|
|
849
|
+
// (showDiffs unset) stays covered by every other suite in this file.
|
|
850
|
+
describe("list mode (showDiffs=false)", () => {
|
|
851
|
+
function expandFiles() {
|
|
852
|
+
fireEvent.click(screen.getByRole("button", { name: "Files" }));
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
it("labels the expander 'Files' and renders inert list rows with no diff DOM", () => {
|
|
856
|
+
render(
|
|
857
|
+
<FileReviewCard
|
|
858
|
+
fileChangeSet={multiChangeSet()}
|
|
859
|
+
onSubmit={noop}
|
|
860
|
+
showDiffs={false}
|
|
861
|
+
/>,
|
|
862
|
+
);
|
|
863
|
+
expandFiles();
|
|
864
|
+
expect(document.querySelector('[data-cursor-target="file-diff"]')).toBeNull();
|
|
865
|
+
expect(
|
|
866
|
+
document.querySelectorAll('[data-cursor-target="file-review-list-row"]'),
|
|
867
|
+
).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).
|
|
870
|
+
expect(screen.getByText("a.ts")).toBeTruthy();
|
|
871
|
+
expect(screen.getByText("b.ts")).toBeTruthy();
|
|
872
|
+
expect(screen.getByTitle("src/a.ts")).toBeTruthy();
|
|
873
|
+
expect(screen.queryByRole("link")).toBeNull();
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it("marks each file with its change-kind letter (A/M/D)", () => {
|
|
877
|
+
const added = create(CapturedFileChangeSchema, {
|
|
878
|
+
id: "fc-add",
|
|
879
|
+
pathAfter: "src/new.ts",
|
|
880
|
+
kind: FileChangeKind.ADD,
|
|
881
|
+
captureClass: FileCaptureClass.GIT_TRACKED,
|
|
882
|
+
after: inline("new\n"),
|
|
883
|
+
fileDigest: "d-add",
|
|
884
|
+
diffComplete: true,
|
|
885
|
+
});
|
|
886
|
+
const deleted = create(CapturedFileChangeSchema, {
|
|
887
|
+
id: "fc-del",
|
|
888
|
+
pathBefore: "src/old.ts",
|
|
889
|
+
kind: FileChangeKind.DELETE,
|
|
890
|
+
captureClass: FileCaptureClass.GIT_TRACKED,
|
|
891
|
+
before: inline("old\n"),
|
|
892
|
+
fileDigest: "d-del",
|
|
893
|
+
diffComplete: true,
|
|
894
|
+
});
|
|
895
|
+
render(
|
|
896
|
+
<FileReviewCard
|
|
897
|
+
fileChangeSet={multiChangeSet({ fc1: added, fc2: deleted })}
|
|
898
|
+
onSubmit={noop}
|
|
899
|
+
showDiffs={false}
|
|
900
|
+
/>,
|
|
901
|
+
);
|
|
902
|
+
expandFiles();
|
|
903
|
+
expect(screen.getByLabelText("added").textContent).toBe("A");
|
|
904
|
+
expect(screen.getByLabelText("deleted").textContent).toBe("D");
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
it("shows a rename's source path in the row", () => {
|
|
908
|
+
const renamed = create(CapturedFileChangeSchema, {
|
|
909
|
+
id: "fc-ren",
|
|
910
|
+
pathBefore: "src/old-name.ts",
|
|
911
|
+
pathAfter: "src/new-name.ts",
|
|
912
|
+
kind: FileChangeKind.RENAME,
|
|
913
|
+
captureClass: FileCaptureClass.GIT_TRACKED,
|
|
914
|
+
before: inline("x\n"),
|
|
915
|
+
after: inline("x\n"),
|
|
916
|
+
fileDigest: "d-ren",
|
|
917
|
+
diffComplete: true,
|
|
918
|
+
});
|
|
919
|
+
render(
|
|
920
|
+
<FileReviewCard
|
|
921
|
+
fileChangeSet={changeSet({ change: renamed })}
|
|
922
|
+
onSubmit={noop}
|
|
923
|
+
showDiffs={false}
|
|
924
|
+
/>,
|
|
925
|
+
);
|
|
926
|
+
expandFiles();
|
|
927
|
+
expect(screen.getByText(/src\/old-name\.ts/)).toBeTruthy();
|
|
928
|
+
expect(screen.getByText("new-name.ts")).toBeTruthy();
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it("submits a FILE APPROVE bound to the file digest from a list row", () => {
|
|
932
|
+
const onSubmit = vi.fn();
|
|
933
|
+
render(
|
|
934
|
+
<FileReviewCard
|
|
935
|
+
fileChangeSet={multiChangeSet()}
|
|
936
|
+
onSubmit={onSubmit}
|
|
937
|
+
showDiffs={false}
|
|
938
|
+
/>,
|
|
939
|
+
);
|
|
940
|
+
expandFiles();
|
|
941
|
+
fireEvent.click(screen.getByRole("radio", { name: "Keep src/a.ts" }));
|
|
942
|
+
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
|
|
943
|
+
scope: FileDecisionScope.FILE,
|
|
944
|
+
fileChangeId: "fc1",
|
|
945
|
+
expectedDigest: "d-fc1",
|
|
946
|
+
acknowledgeUnreviewable: false,
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it("submits a CHANGE_SET APPROVE bound to the aggregate digest from the bar", () => {
|
|
951
|
+
const onSubmit = vi.fn();
|
|
952
|
+
render(
|
|
953
|
+
<FileReviewCard
|
|
954
|
+
fileChangeSet={multiChangeSet()}
|
|
955
|
+
onSubmit={onSubmit}
|
|
956
|
+
showDiffs={false}
|
|
957
|
+
/>,
|
|
958
|
+
);
|
|
959
|
+
fireEvent.click(screen.getByRole("button", { name: "Approve all" }));
|
|
960
|
+
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
|
|
961
|
+
scope: FileDecisionScope.CHANGE_SET,
|
|
962
|
+
expectedDigest: "agg-1",
|
|
963
|
+
acknowledgeUnreviewable: false,
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
it("keeps the acknowledged binary 'Keep anyway' flow, reason note intact", () => {
|
|
968
|
+
const binary = capturedBinaryChange({
|
|
969
|
+
id: "fc-bin",
|
|
970
|
+
path: "logo.png",
|
|
971
|
+
fileDigest: "d-bin",
|
|
972
|
+
});
|
|
973
|
+
const onSubmit = vi.fn();
|
|
974
|
+
render(
|
|
975
|
+
<FileReviewCard
|
|
976
|
+
fileChangeSet={changeSet({
|
|
977
|
+
change: binary,
|
|
978
|
+
diffCompleteness: DiffCompleteness.BINARY_SUMMARY_ONLY,
|
|
979
|
+
})}
|
|
980
|
+
onSubmit={onSubmit}
|
|
981
|
+
showDiffs={false}
|
|
982
|
+
/>,
|
|
983
|
+
);
|
|
984
|
+
// An incomplete set starts expanded in list mode too — with no diff to
|
|
985
|
+
// lean on, the reason note IS the per-file context for the decision.
|
|
986
|
+
expect(document.querySelector('[data-cursor-target="file-diff"]')).toBeNull();
|
|
987
|
+
const note = document.querySelector(
|
|
988
|
+
'[data-cursor-target="file-review-block-reason"]',
|
|
989
|
+
);
|
|
990
|
+
expect(note?.textContent).toMatch(/binary file/i);
|
|
991
|
+
fireEvent.click(screen.getByRole("radio", { name: /keep logo\.png anyway/i }));
|
|
992
|
+
expect(onSubmit).toHaveBeenCalledWith(FileDecisionAction.APPROVE, {
|
|
993
|
+
scope: FileDecisionScope.FILE,
|
|
994
|
+
fileChangeId: "fc-bin",
|
|
995
|
+
expectedDigest: "d-bin",
|
|
996
|
+
acknowledgeUnreviewable: true,
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it("keeps the secret-withheld discard-only flow, honest copy intact", () => {
|
|
1001
|
+
const secret = capturedUnavailableChange({
|
|
1002
|
+
id: "fc-sec",
|
|
1003
|
+
path: ".env",
|
|
1004
|
+
fileDigest: "d-sec",
|
|
1005
|
+
blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
|
|
1006
|
+
});
|
|
1007
|
+
render(
|
|
1008
|
+
<FileReviewCard
|
|
1009
|
+
fileChangeSet={changeSet({
|
|
1010
|
+
change: secret,
|
|
1011
|
+
diffCompleteness: DiffCompleteness.PARTIAL_BLOCKED,
|
|
1012
|
+
})}
|
|
1013
|
+
onSubmit={noop}
|
|
1014
|
+
showDiffs={false}
|
|
1015
|
+
/>,
|
|
1016
|
+
);
|
|
1017
|
+
expect(
|
|
1018
|
+
(screen.getByRole("radio", { name: /keep \.env/i }) as HTMLButtonElement)
|
|
1019
|
+
.disabled,
|
|
1020
|
+
).toBe(true);
|
|
1021
|
+
expect(screen.getByText(/contents withheld/i)).toBeTruthy();
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
it("renders the settled record as verdict-badged list rows, no diffs", () => {
|
|
1025
|
+
render(
|
|
1026
|
+
<FileReviewCard
|
|
1027
|
+
fileChangeSet={multiChangeSet({
|
|
1028
|
+
decisions: [
|
|
1029
|
+
fileDecision("fc1", FileDecisionAction.APPROVE),
|
|
1030
|
+
fileDecision("fc2", FileDecisionAction.REJECT),
|
|
1031
|
+
],
|
|
1032
|
+
})}
|
|
1033
|
+
interactive={false}
|
|
1034
|
+
showDiffs={false}
|
|
1035
|
+
/>,
|
|
1036
|
+
);
|
|
1037
|
+
fireEvent.click(screen.getByRole("button", { name: "Show" }));
|
|
1038
|
+
expect(document.querySelector('[data-cursor-target="file-diff"]')).toBeNull();
|
|
1039
|
+
expect(screen.getByText("Kept")).toBeTruthy();
|
|
1040
|
+
expect(screen.getByText("Discarded")).toBeTruthy();
|
|
1041
|
+
expect(screen.queryByRole("radio")).toBeNull();
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
});
|