@stigmer/react 3.12.6 → 3.12.8
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/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js.map +1 -1
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +8 -11
- package/execution/MessageThread.js.map +1 -1
- package/execution/ResultView.d.ts.map +1 -1
- package/execution/ResultView.js +6 -0
- package/execution/ResultView.js.map +1 -1
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +13 -2
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/tool-categories.d.ts +1 -1
- package/execution/tool-categories.d.ts.map +1 -1
- package/execution/tool-categories.js +6 -0
- package/execution/tool-categories.js.map +1 -1
- package/execution/useFileChangeContent.d.ts +2 -6
- package/execution/useFileChangeContent.d.ts.map +1 -1
- package/execution/useFileChangeContent.js +4 -12
- package/execution/useFileChangeContent.js.map +1 -1
- package/identity-account/AccountPreferencesPanel.d.ts +2 -1
- package/identity-account/AccountPreferencesPanel.d.ts.map +1 -1
- package/identity-account/AccountPreferencesPanel.js +41 -3
- package/identity-account/AccountPreferencesPanel.js.map +1 -1
- package/identity-account/useAccountExecutionDefaults.d.ts +7 -0
- package/identity-account/useAccountExecutionDefaults.d.ts.map +1 -1
- package/identity-account/useAccountExecutionDefaults.js +5 -2
- package/identity-account/useAccountExecutionDefaults.js.map +1 -1
- package/index.d.ts +5 -2
- package/index.d.ts.map +1 -1
- package/index.js +5 -1
- package/index.js.map +1 -1
- package/internal/MemoryEnabledRow.d.ts +37 -0
- package/internal/MemoryEnabledRow.d.ts.map +1 -0
- package/internal/MemoryEnabledRow.js +25 -0
- package/internal/MemoryEnabledRow.js.map +1 -0
- package/internal/download.d.ts +8 -0
- package/internal/download.d.ts.map +1 -0
- package/internal/download.js +25 -0
- package/internal/download.js.map +1 -0
- package/library/useExportResource.d.ts.map +1 -1
- package/library/useExportResource.js +3 -17
- package/library/useExportResource.js.map +1 -1
- package/memory/MemoryListPanel.d.ts +35 -0
- package/memory/MemoryListPanel.d.ts.map +1 -0
- package/memory/MemoryListPanel.js +141 -0
- package/memory/MemoryListPanel.js.map +1 -0
- package/memory/MemoryProposalCard.d.ts +35 -0
- package/memory/MemoryProposalCard.d.ts.map +1 -0
- package/memory/MemoryProposalCard.js +73 -0
- package/memory/MemoryProposalCard.js.map +1 -0
- package/memory/index.d.ts +10 -0
- package/memory/index.d.ts.map +1 -0
- package/memory/index.js +14 -0
- package/memory/index.js.map +1 -0
- package/memory/memoryGroups.d.ts +31 -0
- package/memory/memoryGroups.d.ts.map +1 -0
- package/memory/memoryGroups.js +52 -0
- package/memory/memoryGroups.js.map +1 -0
- package/memory/useConfirmMemory.d.ts +31 -0
- package/memory/useConfirmMemory.d.ts.map +1 -0
- package/memory/useConfirmMemory.js +43 -0
- package/memory/useConfirmMemory.js.map +1 -0
- package/memory/useDeleteMemory.d.ts +30 -0
- package/memory/useDeleteMemory.d.ts.map +1 -0
- package/memory/useDeleteMemory.js +42 -0
- package/memory/useDeleteMemory.js.map +1 -0
- package/memory/useMemories.d.ts +33 -0
- package/memory/useMemories.d.ts.map +1 -0
- package/memory/useMemories.js +42 -0
- package/memory/useMemories.js.map +1 -0
- package/memory/useMemory.d.ts +39 -0
- package/memory/useMemory.d.ts.map +1 -0
- package/memory/useMemory.js +43 -0
- package/memory/useMemory.js.map +1 -0
- package/memory/useRejectMemory.d.ts +34 -0
- package/memory/useRejectMemory.d.ts.map +1 -0
- package/memory/useRejectMemory.js +46 -0
- package/memory/useRejectMemory.js.map +1 -0
- package/memory/useUpdateMemoryContent.d.ts +32 -0
- package/memory/useUpdateMemoryContent.d.ts.map +1 -0
- package/memory/useUpdateMemoryContent.js +48 -0
- package/memory/useUpdateMemoryContent.js.map +1 -0
- package/organization/OrgPreferencesPanel.d.ts.map +1 -1
- package/organization/OrgPreferencesPanel.js +36 -4
- package/organization/OrgPreferencesPanel.js.map +1 -1
- package/package.json +4 -4
- package/session/AutoApproveIndicator.d.ts +18 -0
- package/session/AutoApproveIndicator.d.ts.map +1 -0
- package/session/AutoApproveIndicator.js +23 -0
- package/session/AutoApproveIndicator.js.map +1 -0
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +10 -1
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +26 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +28 -22
- package/session/SessionViewer.js.map +1 -1
- package/session/TranscriptExportMenu.d.ts +24 -0
- package/session/TranscriptExportMenu.d.ts.map +1 -0
- package/session/TranscriptExportMenu.js +26 -0
- package/session/TranscriptExportMenu.js.map +1 -0
- package/session/facets/SetupTab.d.ts +43 -6
- package/session/facets/SetupTab.d.ts.map +1 -1
- package/session/facets/SetupTab.js +32 -36
- package/session/facets/SetupTab.js.map +1 -1
- package/session/facets/UsageTab.d.ts +2 -1
- package/session/facets/UsageTab.d.ts.map +1 -1
- package/session/facets/UsageTab.js +11 -5
- package/session/facets/UsageTab.js.map +1 -1
- package/session/facets/index.d.ts +1 -1
- package/session/facets/index.d.ts.map +1 -1
- package/session/facets/primitives.d.ts +58 -0
- package/session/facets/primitives.d.ts.map +1 -0
- package/session/facets/primitives.js +59 -0
- package/session/facets/primitives.js.map +1 -0
- package/session/index.d.ts +5 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js +2 -0
- package/session/index.js.map +1 -1
- package/session/useExportTranscript.d.ts +44 -0
- package/session/useExportTranscript.d.ts.map +1 -0
- package/session/useExportTranscript.js +109 -0
- package/session/useExportTranscript.js.map +1 -0
- package/session/useNewSessionFlow.d.ts +16 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +25 -6
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +7 -19
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionExecutions.d.ts +2 -14
- package/session/useSessionExecutions.d.ts.map +1 -1
- package/session/useSessionExecutions.js +4 -26
- package/session/useSessionExecutions.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +41 -10
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +67 -9
- package/session/useSessionPageFlow.js.map +1 -1
- package/settings/MemorySection.d.ts +12 -0
- package/settings/MemorySection.d.ts.map +1 -0
- package/settings/MemorySection.js +21 -0
- package/settings/MemorySection.js.map +1 -0
- package/settings/index.d.ts +1 -0
- package/settings/index.d.ts.map +1 -1
- package/settings/index.js +1 -0
- package/settings/index.js.map +1 -1
- package/settings/settings-nav.d.ts.map +1 -1
- package/settings/settings-nav.js +2 -1
- package/settings/settings-nav.js.map +1 -1
- package/src/__tests__/public-api.test.ts +21 -0
- package/src/attachment/__tests__/prepare-image.test.ts +12 -6
- package/src/composer/ComposerToolbar.tsx +1 -0
- package/src/execution/MessageThread.tsx +8 -11
- package/src/execution/ResultView.tsx +6 -0
- package/src/execution/ToolCallItem.tsx +18 -1
- package/src/execution/tool-categories.ts +7 -0
- package/src/execution/useFileChangeContent.ts +5 -12
- package/src/identity-account/AccountPreferencesPanel.tsx +89 -2
- package/src/identity-account/__tests__/AccountPreferencesPanel.test.tsx +112 -0
- package/src/identity-account/__tests__/useAccountExecutionDefaults.test.tsx +34 -1
- package/src/identity-account/useAccountExecutionDefaults.ts +12 -2
- package/src/index.ts +38 -0
- package/src/internal/MemoryEnabledRow.tsx +92 -0
- package/src/internal/download.ts +29 -0
- package/src/library/useExportResource.ts +3 -19
- package/src/memory/MemoryListPanel.tsx +532 -0
- package/src/memory/MemoryProposalCard.tsx +181 -0
- package/src/memory/__tests__/MemoryListPanel.test.tsx +259 -0
- package/src/memory/__tests__/MemoryProposalCard.test.tsx +236 -0
- package/src/memory/__tests__/memoryHooks.test.tsx +283 -0
- package/src/memory/index.ts +24 -0
- package/src/memory/memoryGroups.ts +64 -0
- package/src/memory/useConfirmMemory.ts +62 -0
- package/src/memory/useDeleteMemory.ts +61 -0
- package/src/memory/useMemories.ts +66 -0
- package/src/memory/useMemory.ts +80 -0
- package/src/memory/useRejectMemory.ts +65 -0
- package/src/memory/useUpdateMemoryContent.ts +67 -0
- package/src/organization/OrgPreferencesPanel.tsx +52 -3
- package/src/organization/__tests__/OrgPreferencesPanel.test.tsx +65 -0
- package/src/session/AutoApproveIndicator.tsx +45 -0
- package/src/session/NewSessionViewer.tsx +17 -0
- package/src/session/SessionViewer.tsx +78 -41
- package/src/session/TranscriptExportMenu.tsx +78 -0
- package/src/session/__tests__/NewSessionViewer.test.tsx +32 -0
- package/src/session/__tests__/SessionViewer-autoApprove.test.tsx +244 -0
- package/src/session/__tests__/SessionViewer-transcriptExport.test.tsx +189 -0
- package/src/session/__tests__/TranscriptExportMenu.test.tsx +97 -0
- package/src/session/__tests__/useExportTranscript.test.tsx +187 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +89 -0
- package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +263 -2
- package/src/session/facets/SetupTab.tsx +193 -157
- package/src/session/facets/UsageTab.tsx +38 -38
- package/src/session/facets/index.ts +1 -0
- package/src/session/facets/primitives.tsx +169 -0
- package/src/session/index.ts +10 -0
- package/src/session/useExportTranscript.ts +171 -0
- package/src/session/useNewSessionFlow.ts +42 -6
- package/src/session/useSessionConversation.ts +10 -16
- package/src/session/useSessionExecutions.ts +5 -26
- package/src/session/useSessionPageFlow.ts +108 -21
- package/src/settings/MemorySection.tsx +45 -0
- package/src/settings/index.ts +1 -0
- package/src/settings/settings-nav.ts +2 -0
- package/src/switch/Switch.tsx +4 -0
- package/src/usage/useExportCSV.ts +2 -14
- package/src/workflow/__tests__/useWorkflowCanvas.dirty.test.tsx +7 -2
- package/src/workflow/__tests__/useWorkflowCanvas.drag.test.tsx +7 -2
- package/src/workspace/WorkspaceSurface.tsx +26 -3
- package/styles.css +1 -1
- package/switch/Switch.d.ts +3 -1
- package/switch/Switch.d.ts.map +1 -1
- package/switch/Switch.js +2 -2
- package/switch/Switch.js.map +1 -1
- package/usage/useExportCSV.d.ts.map +1 -1
- package/usage/useExportCSV.js +2 -13
- package/usage/useExportCSV.js.map +1 -1
- package/workspace/WorkspaceSurface.js +11 -2
- package/workspace/WorkspaceSurface.js.map +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
6
|
+
import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
|
|
7
|
+
import { MemoryLifecycleState } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/enum_pb";
|
|
8
|
+
import { SpinnerIcon } from "../internal/SpinnerIcon.js";
|
|
9
|
+
import { formatMemoryProvenance } from "./memoryGroups.js";
|
|
10
|
+
import { useConfirmMemory } from "./useConfirmMemory.js";
|
|
11
|
+
import { useMemory } from "./useMemory.js";
|
|
12
|
+
import { useRejectMemory } from "./useRejectMemory.js";
|
|
13
|
+
|
|
14
|
+
/** Props for {@link MemoryProposalCardBody}. */
|
|
15
|
+
export interface MemoryProposalCardBodyProps {
|
|
16
|
+
/** The proposed memory's resource id (from the remember tool's answer). */
|
|
17
|
+
readonly memoryId: string;
|
|
18
|
+
/**
|
|
19
|
+
* The proposed fact as frozen in the tool result — shown VERBATIM
|
|
20
|
+
* (DD-005 D6: what you confirm is what future prompts inject, byte for
|
|
21
|
+
* byte). The live record's text supersedes it once fetched, so an edit
|
|
22
|
+
* made on the memory page is reflected here too.
|
|
23
|
+
*/
|
|
24
|
+
readonly fact: string;
|
|
25
|
+
/** Additional CSS class names for the root container. */
|
|
26
|
+
readonly className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The in-session consent surface for a memory proposal (DD-005 D4): the
|
|
31
|
+
* body of a `remember` tool call's row in the message thread, rendering
|
|
32
|
+
* the proposed fact verbatim with one-click Confirm / Reject actions.
|
|
33
|
+
*
|
|
34
|
+
* Lifecycle honesty across surfaces and reloads: the tool result this
|
|
35
|
+
* renders from is FROZEN at capture time, so the card fetches the
|
|
36
|
+
* record's current state ({@link useMemory}) — a proposal decided from
|
|
37
|
+
* the memory settings page (or another device) shows its decided state
|
|
38
|
+
* here instead of stale action buttons, and a deleted record reads
|
|
39
|
+
* "no longer stored" rather than failing. Rejection is one click, no
|
|
40
|
+
* confirmation dialog (the T04 Cursor lesson: expensive review teaches
|
|
41
|
+
* users to ignore the queue); deletion-with-confirmation lives on the
|
|
42
|
+
* memory page, the catch-up surface.
|
|
43
|
+
*
|
|
44
|
+
* Borderless by design (the {@link ApprovalCardBody} posture): the tool
|
|
45
|
+
* row's card owns the chrome, this component owns only the body. All
|
|
46
|
+
* visual properties flow through `--stgm-*` design tokens.
|
|
47
|
+
*/
|
|
48
|
+
export function MemoryProposalCardBody({
|
|
49
|
+
memoryId,
|
|
50
|
+
fact,
|
|
51
|
+
className,
|
|
52
|
+
}: MemoryProposalCardBodyProps) {
|
|
53
|
+
const { memory, isLoading, notFound, error: fetchError } = useMemory(memoryId);
|
|
54
|
+
const { confirmMemory, isConfirming, error: confirmError } = useConfirmMemory();
|
|
55
|
+
const { rejectMemory, isRejecting, error: rejectError } = useRejectMemory();
|
|
56
|
+
|
|
57
|
+
// The card's own decision lands here so the settled state renders
|
|
58
|
+
// immediately — the fetch stays the authority for every OTHER writer
|
|
59
|
+
// (the memory page, another device), read once on mount.
|
|
60
|
+
const [decided, setDecided] = useState<Memory | null>(null);
|
|
61
|
+
|
|
62
|
+
const act = useCallback(
|
|
63
|
+
async (action: (id: string) => Promise<Memory>) => {
|
|
64
|
+
try {
|
|
65
|
+
setDecided(await action(memoryId));
|
|
66
|
+
} catch {
|
|
67
|
+
// Error state is surfaced via the hooks.
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
[memoryId],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const record = decided ?? memory;
|
|
74
|
+
const state = record?.status?.lifecycleState;
|
|
75
|
+
// The live record's text supersedes the frozen tool result (the subject
|
|
76
|
+
// may have edited the fact on the memory page before deciding).
|
|
77
|
+
const displayFact = record?.spec?.content || fact;
|
|
78
|
+
const provenance = record ? formatMemoryProvenance(record) : null;
|
|
79
|
+
const actionError = confirmError ?? rejectError;
|
|
80
|
+
const isBusy = isConfirming || isRejecting;
|
|
81
|
+
|
|
82
|
+
const showActions =
|
|
83
|
+
!notFound &&
|
|
84
|
+
fetchError == null &&
|
|
85
|
+
(state === undefined ||
|
|
86
|
+
state === MemoryLifecycleState.lifecycle_state_unspecified ||
|
|
87
|
+
state === MemoryLifecycleState.lifecycle_state_proposed);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className={cn("stg:space-y-2", className)} data-cursor-target="memory-proposal">
|
|
91
|
+
<p className="stg:text-sm stg:text-foreground stg:whitespace-pre-wrap">
|
|
92
|
+
{displayFact}
|
|
93
|
+
</p>
|
|
94
|
+
|
|
95
|
+
<p className="stg:text-xs stg:text-muted-foreground">
|
|
96
|
+
{provenance ? `${provenance} · ` : ""}
|
|
97
|
+
Proposed to remember — nothing is stored in your sessions until you
|
|
98
|
+
confirm it.
|
|
99
|
+
</p>
|
|
100
|
+
|
|
101
|
+
{notFound ? (
|
|
102
|
+
<p className="stg:text-xs stg:text-muted-foreground" role="status">
|
|
103
|
+
No longer stored.
|
|
104
|
+
</p>
|
|
105
|
+
) : fetchError ? (
|
|
106
|
+
// The fact stays visible (it is frozen in the tool result); only
|
|
107
|
+
// the actionable state is unavailable. Decide from the memory
|
|
108
|
+
// page instead — the catch-up surface.
|
|
109
|
+
<p className="stg:text-xs stg:text-muted-foreground" role="status">
|
|
110
|
+
Couldn't load this proposal's current state — review it in
|
|
111
|
+
Settings → Memory.
|
|
112
|
+
</p>
|
|
113
|
+
) : showActions ? (
|
|
114
|
+
<div className="stg:flex stg:items-center stg:gap-1.5">
|
|
115
|
+
<ChipButton
|
|
116
|
+
label={`Confirm memory: ${displayFact}`}
|
|
117
|
+
onClick={() => void act(confirmMemory)}
|
|
118
|
+
disabled={isBusy || isLoading}
|
|
119
|
+
emphasis="primary"
|
|
120
|
+
>
|
|
121
|
+
{isConfirming && <SpinnerIcon size={12} />}
|
|
122
|
+
Confirm
|
|
123
|
+
</ChipButton>
|
|
124
|
+
<ChipButton
|
|
125
|
+
label={`Reject memory: ${displayFact}`}
|
|
126
|
+
onClick={() => void act(rejectMemory)}
|
|
127
|
+
disabled={isBusy || isLoading}
|
|
128
|
+
>
|
|
129
|
+
{isRejecting && <SpinnerIcon size={12} />}
|
|
130
|
+
Reject
|
|
131
|
+
</ChipButton>
|
|
132
|
+
</div>
|
|
133
|
+
) : (
|
|
134
|
+
<p className="stg:text-xs stg:text-muted-foreground" role="status">
|
|
135
|
+
{state === MemoryLifecycleState.lifecycle_state_confirmed
|
|
136
|
+
? "Confirmed — recalled in your future sessions."
|
|
137
|
+
: "Rejected — not stored."}
|
|
138
|
+
</p>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{actionError && (
|
|
142
|
+
<p className="stg:text-[0.65rem] stg:text-destructive" role="alert">
|
|
143
|
+
{getUserMessage(actionError)}
|
|
144
|
+
</p>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function ChipButton({
|
|
151
|
+
label,
|
|
152
|
+
onClick,
|
|
153
|
+
disabled,
|
|
154
|
+
emphasis,
|
|
155
|
+
children,
|
|
156
|
+
}: {
|
|
157
|
+
readonly label: string;
|
|
158
|
+
readonly onClick: () => void;
|
|
159
|
+
readonly disabled?: boolean;
|
|
160
|
+
readonly emphasis?: "primary";
|
|
161
|
+
readonly children: React.ReactNode;
|
|
162
|
+
}) {
|
|
163
|
+
return (
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
aria-label={label}
|
|
167
|
+
onClick={onClick}
|
|
168
|
+
disabled={disabled}
|
|
169
|
+
className={cn(
|
|
170
|
+
"stg:inline-flex stg:items-center stg:gap-1 stg:rounded-md stg:px-2.5 stg:py-1 stg:text-xs stg:font-medium",
|
|
171
|
+
emphasis === "primary"
|
|
172
|
+
? "stg:bg-primary stg:text-primary-foreground stg:hover:bg-primary-hover"
|
|
173
|
+
: "stg:border stg:border-input stg:bg-background stg:text-foreground stg:hover:bg-accent stg:hover:text-accent-foreground",
|
|
174
|
+
"stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-ring",
|
|
175
|
+
"stg:disabled:pointer-events-none stg:disabled:opacity-50",
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
178
|
+
{children}
|
|
179
|
+
</button>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
|
|
2
|
+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { create } from "@bufbuild/protobuf";
|
|
6
|
+
import { MemorySchema, type Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
|
|
7
|
+
import { MemoryLifecycleState } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/enum_pb";
|
|
8
|
+
import { MemoryListSchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/io_pb";
|
|
9
|
+
import { StigmerContext } from "../../context";
|
|
10
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
11
|
+
import { MemoryListPanel } from "../MemoryListPanel";
|
|
12
|
+
|
|
13
|
+
function Providers({
|
|
14
|
+
client,
|
|
15
|
+
children,
|
|
16
|
+
}: {
|
|
17
|
+
client: unknown;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<FetchCacheContext.Provider value={null}>
|
|
22
|
+
<StigmerContext.Provider value={client as never}>
|
|
23
|
+
{children}
|
|
24
|
+
</StigmerContext.Provider>
|
|
25
|
+
</FetchCacheContext.Provider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeMemory(
|
|
30
|
+
id: string,
|
|
31
|
+
state: MemoryLifecycleState,
|
|
32
|
+
content: string,
|
|
33
|
+
provenance?: { agentId: string; sessionId: string },
|
|
34
|
+
): Memory {
|
|
35
|
+
return create(MemorySchema, {
|
|
36
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
37
|
+
kind: "Memory",
|
|
38
|
+
metadata: { id, name: id, slug: id, org: "test-org" },
|
|
39
|
+
spec: { content, provenance },
|
|
40
|
+
status: { lifecycleState: state },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const PROPOSED = makeMemory(
|
|
45
|
+
"mem_proposed",
|
|
46
|
+
MemoryLifecycleState.lifecycle_state_proposed,
|
|
47
|
+
"Prefers terse answers.",
|
|
48
|
+
{ agentId: "agt_1", sessionId: "ses_1" },
|
|
49
|
+
);
|
|
50
|
+
const CONFIRMED = makeMemory(
|
|
51
|
+
"mem_confirmed",
|
|
52
|
+
MemoryLifecycleState.lifecycle_state_confirmed,
|
|
53
|
+
"Deploys to us-east-1.",
|
|
54
|
+
);
|
|
55
|
+
const REJECTED = makeMemory(
|
|
56
|
+
"mem_rejected",
|
|
57
|
+
MemoryLifecycleState.lifecycle_state_rejected,
|
|
58
|
+
"Not actually true.",
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
function makeClient(memories: Memory[] = [PROPOSED, CONFIRMED, REJECTED]) {
|
|
62
|
+
return {
|
|
63
|
+
memory: {
|
|
64
|
+
list: vi
|
|
65
|
+
.fn()
|
|
66
|
+
.mockResolvedValue(
|
|
67
|
+
create(MemoryListSchema, { totalCount: memories.length, items: memories }),
|
|
68
|
+
),
|
|
69
|
+
confirm: vi.fn().mockResolvedValue(CONFIRMED),
|
|
70
|
+
reject: vi.fn().mockResolvedValue(REJECTED),
|
|
71
|
+
delete: vi.fn().mockResolvedValue(PROPOSED),
|
|
72
|
+
update: vi.fn().mockResolvedValue(CONFIRMED),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
beforeEach(() => vi.clearAllMocks());
|
|
78
|
+
afterEach(cleanup);
|
|
79
|
+
|
|
80
|
+
describe("MemoryListPanel", () => {
|
|
81
|
+
it("groups pending proposals first, then remembered, then rejected", async () => {
|
|
82
|
+
const client = makeClient();
|
|
83
|
+
render(
|
|
84
|
+
<Providers client={client}>
|
|
85
|
+
<MemoryListPanel org="test-org" />
|
|
86
|
+
</Providers>,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
await waitFor(() =>
|
|
90
|
+
expect(screen.getByText("Prefers terse answers.")).toBeTruthy(),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const headings = screen.getAllByRole("heading", { level: 3 });
|
|
94
|
+
expect(headings.map((h) => h.textContent)).toEqual([
|
|
95
|
+
"Pending proposals",
|
|
96
|
+
"Remembered",
|
|
97
|
+
"Rejected",
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("shows the stored text verbatim with its provenance", async () => {
|
|
102
|
+
const client = makeClient([PROPOSED]);
|
|
103
|
+
render(
|
|
104
|
+
<Providers client={client}>
|
|
105
|
+
<MemoryListPanel org="test-org" />
|
|
106
|
+
</Providers>,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// The review surface never paraphrases: what is confirmed is what
|
|
110
|
+
// future prompts inject, byte for byte (DD-005 D6).
|
|
111
|
+
await waitFor(() =>
|
|
112
|
+
expect(screen.getByText("Prefers terse answers.")).toBeTruthy(),
|
|
113
|
+
);
|
|
114
|
+
expect(
|
|
115
|
+
screen.getByText(/Proposed by agent agt_1 in session ses_1/),
|
|
116
|
+
).toBeTruthy();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("confirms a proposal and refetches", async () => {
|
|
120
|
+
const client = makeClient([PROPOSED]);
|
|
121
|
+
const user = userEvent.setup();
|
|
122
|
+
render(
|
|
123
|
+
<Providers client={client}>
|
|
124
|
+
<MemoryListPanel org="test-org" />
|
|
125
|
+
</Providers>,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
await waitFor(() =>
|
|
129
|
+
expect(screen.getByText("Prefers terse answers.")).toBeTruthy(),
|
|
130
|
+
);
|
|
131
|
+
await user.click(
|
|
132
|
+
screen.getByRole("button", { name: /Confirm memory: Prefers terse answers./ }),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
await waitFor(() => expect(client.memory.confirm).toHaveBeenCalledWith("mem_proposed"));
|
|
136
|
+
await waitFor(() => expect(client.memory.list).toHaveBeenCalledTimes(2));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("rejects with a single click — no confirmation dialog", async () => {
|
|
140
|
+
const client = makeClient([PROPOSED]);
|
|
141
|
+
const user = userEvent.setup();
|
|
142
|
+
render(
|
|
143
|
+
<Providers client={client}>
|
|
144
|
+
<MemoryListPanel org="test-org" />
|
|
145
|
+
</Providers>,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await waitFor(() =>
|
|
149
|
+
expect(screen.getByText("Prefers terse answers.")).toBeTruthy(),
|
|
150
|
+
);
|
|
151
|
+
await user.click(
|
|
152
|
+
screen.getByRole("button", { name: /Reject memory: Prefers terse answers./ }),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// One click, straight to the RPC (DD-005 D4): expensive review
|
|
156
|
+
// teaches users to ignore the queue.
|
|
157
|
+
await waitFor(() => expect(client.memory.reject).toHaveBeenCalledWith("mem_proposed"));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("requires inline confirmation before deleting", async () => {
|
|
161
|
+
const client = makeClient([CONFIRMED]);
|
|
162
|
+
const user = userEvent.setup();
|
|
163
|
+
render(
|
|
164
|
+
<Providers client={client}>
|
|
165
|
+
<MemoryListPanel org="test-org" />
|
|
166
|
+
</Providers>,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
await waitFor(() =>
|
|
170
|
+
expect(screen.getByText("Deploys to us-east-1.")).toBeTruthy(),
|
|
171
|
+
);
|
|
172
|
+
await user.click(
|
|
173
|
+
screen.getByRole("button", { name: /Delete memory: Deploys to us-east-1./ }),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Nothing deleted yet — the row transformed into the inline confirm.
|
|
177
|
+
expect(client.memory.delete).not.toHaveBeenCalled();
|
|
178
|
+
expect(screen.getByText(/Forget this permanently\?/)).toBeTruthy();
|
|
179
|
+
|
|
180
|
+
await user.click(screen.getByRole("button", { name: "Delete" }));
|
|
181
|
+
await waitFor(() =>
|
|
182
|
+
expect(client.memory.delete).toHaveBeenCalledWith("mem_confirmed"),
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("edits a confirmed fact through the wipe-safe update mapper", async () => {
|
|
187
|
+
const client = makeClient([CONFIRMED]);
|
|
188
|
+
const user = userEvent.setup();
|
|
189
|
+
render(
|
|
190
|
+
<Providers client={client}>
|
|
191
|
+
<MemoryListPanel org="test-org" />
|
|
192
|
+
</Providers>,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
await waitFor(() =>
|
|
196
|
+
expect(screen.getByText("Deploys to us-east-1.")).toBeTruthy(),
|
|
197
|
+
);
|
|
198
|
+
await user.click(
|
|
199
|
+
screen.getByRole("button", { name: /Edit memory: Deploys to us-east-1./ }),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const textarea = screen.getByRole("textbox", { name: "Edit memory text" });
|
|
203
|
+
expect((textarea as HTMLTextAreaElement).value).toBe("Deploys to us-east-1.");
|
|
204
|
+
|
|
205
|
+
await user.clear(textarea);
|
|
206
|
+
await user.type(textarea, "Deploys to eu-west-1.");
|
|
207
|
+
await user.click(screen.getByRole("button", { name: "Save memory text" }));
|
|
208
|
+
|
|
209
|
+
await waitFor(() =>
|
|
210
|
+
expect(client.memory.update).toHaveBeenCalledWith(
|
|
211
|
+
expect.objectContaining({
|
|
212
|
+
id: "mem_confirmed",
|
|
213
|
+
org: "test-org",
|
|
214
|
+
content: "Deploys to eu-west-1.",
|
|
215
|
+
}),
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("offers no edit action on proposals — decide first, then edit", async () => {
|
|
221
|
+
const client = makeClient([PROPOSED]);
|
|
222
|
+
render(
|
|
223
|
+
<Providers client={client}>
|
|
224
|
+
<MemoryListPanel org="test-org" />
|
|
225
|
+
</Providers>,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await waitFor(() =>
|
|
229
|
+
expect(screen.getByText("Prefers terse answers.")).toBeTruthy(),
|
|
230
|
+
);
|
|
231
|
+
expect(screen.queryByRole("button", { name: /Edit memory/ })).toBeNull();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("renders the empty state when nothing is remembered", async () => {
|
|
235
|
+
const client = makeClient([]);
|
|
236
|
+
render(
|
|
237
|
+
<Providers client={client}>
|
|
238
|
+
<MemoryListPanel org="test-org" />
|
|
239
|
+
</Providers>,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
await waitFor(() =>
|
|
243
|
+
expect(screen.getByText(/Nothing remembered yet/)).toBeTruthy(),
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("surfaces list errors as an alert", async () => {
|
|
248
|
+
const client = {
|
|
249
|
+
memory: { list: vi.fn().mockRejectedValue(new Error("boom")) },
|
|
250
|
+
};
|
|
251
|
+
render(
|
|
252
|
+
<Providers client={client}>
|
|
253
|
+
<MemoryListPanel org="test-org" />
|
|
254
|
+
</Providers>,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
await waitFor(() => expect(screen.getByRole("alert")).toBeTruthy());
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// The in-session consent surface (DD-005 D4): verbatim fact, one-click
|
|
2
|
+
// Confirm/Reject, and lifecycle honesty — the frozen tool result never
|
|
3
|
+
// shows stale action buttons over a record that was already decided,
|
|
4
|
+
// deleted, or unreachable.
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
|
|
7
|
+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
8
|
+
import userEvent from "@testing-library/user-event";
|
|
9
|
+
import type { ReactNode } from "react";
|
|
10
|
+
import { create } from "@bufbuild/protobuf";
|
|
11
|
+
import { MemorySchema, type Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
|
|
12
|
+
import { MemoryLifecycleState } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/enum_pb";
|
|
13
|
+
import { StigmerError } from "@stigmer/sdk";
|
|
14
|
+
import { StigmerContext } from "../../context";
|
|
15
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
16
|
+
import { MemoryProposalCardBody } from "../MemoryProposalCard";
|
|
17
|
+
|
|
18
|
+
function wrapper(client: unknown) {
|
|
19
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
20
|
+
return (
|
|
21
|
+
<FetchCacheContext.Provider value={null}>
|
|
22
|
+
<StigmerContext.Provider value={client as never}>
|
|
23
|
+
{children}
|
|
24
|
+
</StigmerContext.Provider>
|
|
25
|
+
</FetchCacheContext.Provider>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const FACT = "Prefers concise answers with code examples.";
|
|
31
|
+
|
|
32
|
+
function makeMemory(state: MemoryLifecycleState, content = FACT): Memory {
|
|
33
|
+
return create(MemorySchema, {
|
|
34
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
35
|
+
kind: "Memory",
|
|
36
|
+
metadata: { id: "mem_1", name: "mem_1", slug: "mem-1", org: "test-org" },
|
|
37
|
+
spec: {
|
|
38
|
+
content,
|
|
39
|
+
provenance: { agentId: "agt_1", sessionId: "ses_1" },
|
|
40
|
+
},
|
|
41
|
+
status: { lifecycleState: state },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderCard(client: unknown) {
|
|
46
|
+
return render(<MemoryProposalCardBody memoryId="mem_1" fact={FACT} />, {
|
|
47
|
+
wrapper: wrapper(client),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Waits for the record fetch to settle and the named action to arm. */
|
|
52
|
+
async function waitForAction(name: string): Promise<HTMLElement> {
|
|
53
|
+
let button: HTMLElement | null = null;
|
|
54
|
+
await waitFor(() => {
|
|
55
|
+
button = screen.getByRole("button", { name });
|
|
56
|
+
expect(button.hasAttribute("disabled")).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
return button!;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
beforeEach(() => vi.clearAllMocks());
|
|
62
|
+
afterEach(cleanup);
|
|
63
|
+
|
|
64
|
+
describe("MemoryProposalCardBody", () => {
|
|
65
|
+
it("renders the verbatim fact with Confirm/Reject while the record is proposed", async () => {
|
|
66
|
+
const client = {
|
|
67
|
+
memory: {
|
|
68
|
+
get: vi.fn().mockResolvedValue(
|
|
69
|
+
makeMemory(MemoryLifecycleState.lifecycle_state_proposed),
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
renderCard(client);
|
|
75
|
+
|
|
76
|
+
// The exact stored text — never paraphrased (DD-005 D6).
|
|
77
|
+
expect(screen.getByText(FACT)).toBeTruthy();
|
|
78
|
+
await waitForAction(`Confirm memory: ${FACT}`);
|
|
79
|
+
expect(screen.getByRole("button", { name: `Reject memory: ${FACT}` })).toBeTruthy();
|
|
80
|
+
// Provenance renders beside the fact (DD-005 D6: origin builds trust).
|
|
81
|
+
expect(screen.getByText(/Proposed by agent agt_1/)).toBeTruthy();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("confirms in one click and settles into the confirmed state line", async () => {
|
|
85
|
+
const confirm = vi
|
|
86
|
+
.fn()
|
|
87
|
+
.mockResolvedValue(makeMemory(MemoryLifecycleState.lifecycle_state_confirmed));
|
|
88
|
+
const client = {
|
|
89
|
+
memory: {
|
|
90
|
+
get: vi.fn().mockResolvedValue(
|
|
91
|
+
makeMemory(MemoryLifecycleState.lifecycle_state_proposed),
|
|
92
|
+
),
|
|
93
|
+
confirm,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
renderCard(client);
|
|
98
|
+
const user = userEvent.setup();
|
|
99
|
+
const confirmButton = await waitForAction(`Confirm memory: ${FACT}`);
|
|
100
|
+
|
|
101
|
+
await user.click(confirmButton);
|
|
102
|
+
|
|
103
|
+
expect(confirm).toHaveBeenCalledWith("mem_1");
|
|
104
|
+
await waitFor(() =>
|
|
105
|
+
expect(
|
|
106
|
+
screen.getByText("Confirmed — recalled in your future sessions."),
|
|
107
|
+
).toBeTruthy(),
|
|
108
|
+
);
|
|
109
|
+
expect(screen.queryByRole("button")).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("rejects in ONE click — no confirmation dialog (the T04 Cursor lesson)", async () => {
|
|
113
|
+
const reject = vi
|
|
114
|
+
.fn()
|
|
115
|
+
.mockResolvedValue(makeMemory(MemoryLifecycleState.lifecycle_state_rejected));
|
|
116
|
+
const client = {
|
|
117
|
+
memory: {
|
|
118
|
+
get: vi.fn().mockResolvedValue(
|
|
119
|
+
makeMemory(MemoryLifecycleState.lifecycle_state_proposed),
|
|
120
|
+
),
|
|
121
|
+
reject,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
renderCard(client);
|
|
126
|
+
const user = userEvent.setup();
|
|
127
|
+
const rejectButton = await waitForAction(`Reject memory: ${FACT}`);
|
|
128
|
+
|
|
129
|
+
await user.click(rejectButton);
|
|
130
|
+
|
|
131
|
+
expect(reject).toHaveBeenCalledWith("mem_1");
|
|
132
|
+
await waitFor(() =>
|
|
133
|
+
expect(screen.getByText("Rejected — not stored.")).toBeTruthy(),
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("shows the decided state instead of stale actions when another surface decided it", async () => {
|
|
138
|
+
// The tool result is frozen; the record was confirmed from the
|
|
139
|
+
// memory page (or another device) — the reloaded chip must say so.
|
|
140
|
+
const client = {
|
|
141
|
+
memory: {
|
|
142
|
+
get: vi.fn().mockResolvedValue(
|
|
143
|
+
makeMemory(MemoryLifecycleState.lifecycle_state_confirmed),
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
renderCard(client);
|
|
149
|
+
|
|
150
|
+
await waitFor(() =>
|
|
151
|
+
expect(
|
|
152
|
+
screen.getByText("Confirmed — recalled in your future sessions."),
|
|
153
|
+
).toBeTruthy(),
|
|
154
|
+
);
|
|
155
|
+
expect(screen.queryByRole("button")).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("renders the live record's text when the fact was edited before deciding", async () => {
|
|
159
|
+
const edited = "Prefers concise answers, with Go examples.";
|
|
160
|
+
const client = {
|
|
161
|
+
memory: {
|
|
162
|
+
get: vi.fn().mockResolvedValue(
|
|
163
|
+
makeMemory(MemoryLifecycleState.lifecycle_state_proposed, edited),
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
renderCard(client);
|
|
169
|
+
|
|
170
|
+
await waitFor(() => expect(screen.getByText(edited)).toBeTruthy());
|
|
171
|
+
expect(screen.queryByText(FACT)).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("reads 'no longer stored' for a deleted record — a state, never an error", async () => {
|
|
175
|
+
const client = {
|
|
176
|
+
memory: {
|
|
177
|
+
get: vi
|
|
178
|
+
.fn()
|
|
179
|
+
.mockRejectedValue(new StigmerError("not-found", "memory not found", 5)),
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
renderCard(client);
|
|
184
|
+
|
|
185
|
+
await waitFor(() =>
|
|
186
|
+
expect(screen.getByText("No longer stored.")).toBeTruthy(),
|
|
187
|
+
);
|
|
188
|
+
expect(screen.queryByRole("button")).toBeNull();
|
|
189
|
+
expect(screen.queryByRole("alert")).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("keeps the frozen fact visible when the state fetch fails, pointing at the memory page", async () => {
|
|
193
|
+
const client = {
|
|
194
|
+
memory: {
|
|
195
|
+
get: vi
|
|
196
|
+
.fn()
|
|
197
|
+
.mockRejectedValue(new StigmerError("unavailable", "server down", 14)),
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
renderCard(client);
|
|
202
|
+
|
|
203
|
+
await waitFor(() =>
|
|
204
|
+
expect(screen.getByText(/review it in Settings → Memory/)).toBeTruthy(),
|
|
205
|
+
);
|
|
206
|
+
// The fact is frozen in the tool result — it never disappears.
|
|
207
|
+
expect(screen.getByText(FACT)).toBeTruthy();
|
|
208
|
+
expect(screen.queryByRole("button")).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("surfaces a failed decision inline and keeps the actions available", async () => {
|
|
212
|
+
const client = {
|
|
213
|
+
memory: {
|
|
214
|
+
get: vi.fn().mockResolvedValue(
|
|
215
|
+
makeMemory(MemoryLifecycleState.lifecycle_state_proposed),
|
|
216
|
+
),
|
|
217
|
+
confirm: vi
|
|
218
|
+
.fn()
|
|
219
|
+
.mockRejectedValue(new StigmerError("unavailable", "server down", 14)),
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
renderCard(client);
|
|
224
|
+
const user = userEvent.setup();
|
|
225
|
+
const confirmButton = await waitForAction(`Confirm memory: ${FACT}`);
|
|
226
|
+
|
|
227
|
+
await user.click(confirmButton);
|
|
228
|
+
|
|
229
|
+
await waitFor(() => expect(screen.getByRole("alert")).toBeTruthy());
|
|
230
|
+
expect(
|
|
231
|
+
screen
|
|
232
|
+
.getByRole("button", { name: `Confirm memory: ${FACT}` })
|
|
233
|
+
.hasAttribute("disabled"),
|
|
234
|
+
).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
});
|