@stigmer/react 3.12.4 → 3.12.5
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/attachment/AttachmentChipList.js +16 -6
- package/attachment/AttachmentChipList.js.map +1 -1
- package/attachment/index.d.ts +1 -1
- package/attachment/index.d.ts.map +1 -1
- package/attachment/index.js.map +1 -1
- package/attachment/useAttachments.d.ts +51 -8
- package/attachment/useAttachments.d.ts.map +1 -1
- package/attachment/useAttachments.js +74 -3
- package/attachment/useAttachments.js.map +1 -1
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +17 -10
- package/composer/SessionComposer.js.map +1 -1
- package/conversation/ConversationComposer.d.ts +19 -3
- package/conversation/ConversationComposer.d.ts.map +1 -1
- package/conversation/ConversationComposer.js +4 -4
- package/conversation/ConversationComposer.js.map +1 -1
- package/conversation/ConversationTemplatePickerDialog.d.ts +63 -0
- package/conversation/ConversationTemplatePickerDialog.d.ts.map +1 -0
- package/conversation/ConversationTemplatePickerDialog.js +151 -0
- package/conversation/ConversationTemplatePickerDialog.js.map +1 -0
- package/conversation/ConversationsWorkbench.d.ts.map +1 -1
- package/conversation/ConversationsWorkbench.js +16 -5
- package/conversation/ConversationsWorkbench.js.map +1 -1
- package/conversation/index.d.ts +3 -1
- package/conversation/index.d.ts.map +1 -1
- package/conversation/index.js +1 -0
- package/conversation/index.js.map +1 -1
- package/conversation/useConversationParticipation.d.ts +41 -4
- package/conversation/useConversationParticipation.d.ts.map +1 -1
- package/conversation/useConversationParticipation.js +13 -2
- package/conversation/useConversationParticipation.js.map +1 -1
- package/identity-account/AccountPreferencesPanel.d.ts +35 -0
- package/identity-account/AccountPreferencesPanel.d.ts.map +1 -0
- package/identity-account/AccountPreferencesPanel.js +187 -0
- package/identity-account/AccountPreferencesPanel.js.map +1 -0
- package/identity-account/index.d.ts +8 -0
- package/identity-account/index.d.ts.map +1 -1
- package/identity-account/index.js +4 -0
- package/identity-account/index.js.map +1 -1
- package/identity-account/useAccountExecutionDefaults.d.ts +36 -0
- package/identity-account/useAccountExecutionDefaults.d.ts.map +1 -0
- package/identity-account/useAccountExecutionDefaults.js +46 -0
- package/identity-account/useAccountExecutionDefaults.js.map +1 -0
- package/identity-account/useMyIdentityAccount.d.ts +55 -0
- package/identity-account/useMyIdentityAccount.d.ts.map +1 -0
- package/identity-account/useMyIdentityAccount.js +37 -0
- package/identity-account/useMyIdentityAccount.js.map +1 -0
- package/identity-account/useUpdateIdentityAccount.d.ts +42 -0
- package/identity-account/useUpdateIdentityAccount.d.ts.map +1 -0
- package/identity-account/useUpdateIdentityAccount.js +53 -0
- package/identity-account/useUpdateIdentityAccount.js.map +1 -0
- package/index.d.ts +11 -9
- package/index.d.ts.map +1 -1
- package/index.js +8 -5
- package/index.js.map +1 -1
- package/internal/StandingContextField.d.ts +37 -0
- package/internal/StandingContextField.d.ts.map +1 -0
- package/internal/StandingContextField.js +24 -0
- package/internal/StandingContextField.js.map +1 -0
- package/library/InstanceVisibilitySelector.d.ts.map +1 -1
- package/library/InstanceVisibilitySelector.js +9 -2
- package/library/InstanceVisibilitySelector.js.map +1 -1
- package/library/ResourceVisibilityControl.d.ts +13 -3
- package/library/ResourceVisibilityControl.d.ts.map +1 -1
- package/library/ResourceVisibilityControl.js +33 -5
- package/library/ResourceVisibilityControl.js.map +1 -1
- package/library/VisibilityOptionRow.d.ts +6 -0
- package/library/VisibilityOptionRow.d.ts.map +1 -1
- package/library/VisibilityOptionRow.js +11 -1
- package/library/VisibilityOptionRow.js.map +1 -1
- package/library/VisibilitySelector.d.ts.map +1 -1
- package/library/VisibilitySelector.js +14 -2
- package/library/VisibilitySelector.js.map +1 -1
- package/library/index.d.ts +4 -2
- package/library/index.d.ts.map +1 -1
- package/library/index.js +2 -1
- package/library/index.js.map +1 -1
- package/library/useCanSetPublicVisibility.d.ts +30 -0
- package/library/useCanSetPublicVisibility.d.ts.map +1 -0
- package/library/useCanSetPublicVisibility.js +41 -0
- package/library/useCanSetPublicVisibility.js.map +1 -0
- package/library/visibilityLevels.d.ts +32 -1
- package/library/visibilityLevels.d.ts.map +1 -1
- package/library/visibilityLevels.js +34 -14
- package/library/visibilityLevels.js.map +1 -1
- package/mcp-server/useMcpServerConnect.d.ts +8 -5
- package/mcp-server/useMcpServerConnect.d.ts.map +1 -1
- package/mcp-server/useMcpServerConnect.js +2 -1
- package/mcp-server/useMcpServerConnect.js.map +1 -1
- package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -1
- package/mcp-server/useMcpServerOAuthConnect.js +7 -2
- package/mcp-server/useMcpServerOAuthConnect.js.map +1 -1
- package/oauth-app/CreateOAuthAppForm.d.ts +2 -2
- package/oauth-app/CreateOAuthAppForm.d.ts.map +1 -1
- package/oauth-app/CreateOAuthAppForm.js +13 -4
- package/oauth-app/CreateOAuthAppForm.js.map +1 -1
- package/oauth-app/OAuthAppDetailPanel.d.ts.map +1 -1
- package/oauth-app/OAuthAppDetailPanel.js +32 -5
- package/oauth-app/OAuthAppDetailPanel.js.map +1 -1
- package/organization/OrgPreferencesPanel.d.ts +32 -0
- package/organization/OrgPreferencesPanel.d.ts.map +1 -0
- package/organization/OrgPreferencesPanel.js +90 -0
- package/organization/OrgPreferencesPanel.js.map +1 -0
- package/organization/OrgProfilePanel.d.ts.map +1 -1
- package/organization/OrgProfilePanel.js +5 -4
- package/organization/OrgProfilePanel.js.map +1 -1
- package/organization/index.d.ts +2 -0
- package/organization/index.d.ts.map +1 -1
- package/organization/index.js +1 -0
- package/organization/index.js.map +1 -1
- package/package.json +4 -4
- package/session/NewSessionViewer.d.ts +10 -1
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +2 -1
- package/session/NewSessionViewer.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +27 -1
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +45 -8
- package/session/useNewSessionFlow.js.map +1 -1
- package/settings/AccountPreferencesSection.d.ts +3 -0
- package/settings/AccountPreferencesSection.d.ts.map +1 -0
- package/settings/AccountPreferencesSection.js +10 -0
- package/settings/AccountPreferencesSection.js.map +1 -0
- package/settings/OrgPreferencesSection.d.ts +3 -0
- package/settings/OrgPreferencesSection.d.ts.map +1 -0
- package/settings/OrgPreferencesSection.js +13 -0
- package/settings/OrgPreferencesSection.js.map +1 -0
- package/settings/index.d.ts +2 -0
- package/settings/index.d.ts.map +1 -1
- package/settings/index.js +2 -0
- package/settings/index.js.map +1 -1
- package/settings/settings-nav.d.ts.map +1 -1
- package/settings/settings-nav.js +17 -1
- package/settings/settings-nav.js.map +1 -1
- package/src/attachment/AttachmentChipList.tsx +19 -8
- package/src/attachment/__tests__/AttachmentChipList.test.tsx +12 -0
- package/src/attachment/__tests__/useAttachments.test.ts +293 -0
- package/src/attachment/index.ts +1 -0
- package/src/attachment/useAttachments.ts +143 -11
- package/src/composer/SessionComposer.tsx +23 -14
- package/src/conversation/ConversationComposer.tsx +38 -4
- package/src/conversation/ConversationTemplatePickerDialog.tsx +441 -0
- package/src/conversation/ConversationsWorkbench.tsx +40 -4
- package/src/conversation/__tests__/ConversationComposer.test.tsx +56 -1
- package/src/conversation/__tests__/ConversationTemplatePickerDialog.test.tsx +224 -0
- package/src/conversation/__tests__/useConversationParticipation.test.tsx +45 -4
- package/src/conversation/index.ts +4 -0
- package/src/conversation/useConversationParticipation.ts +58 -6
- package/src/identity-account/AccountPreferencesPanel.tsx +446 -0
- package/src/identity-account/__tests__/AccountPreferencesPanel.test.tsx +233 -0
- package/src/identity-account/__tests__/useAccountExecutionDefaults.test.tsx +116 -0
- package/src/identity-account/__tests__/useMyIdentityAccount.test.tsx +121 -0
- package/src/identity-account/index.ts +11 -0
- package/src/identity-account/useAccountExecutionDefaults.ts +63 -0
- package/src/identity-account/useMyIdentityAccount.ts +75 -0
- package/src/identity-account/useUpdateIdentityAccount.ts +73 -0
- package/src/index.ts +24 -2
- package/src/internal/StandingContextField.tsx +92 -0
- package/src/library/InstanceVisibilitySelector.tsx +9 -2
- package/src/library/ResourceVisibilityControl.tsx +46 -13
- package/src/library/VisibilityOptionRow.tsx +28 -6
- package/src/library/VisibilitySelector.tsx +14 -2
- package/src/library/__tests__/VisibilitySelector.test.tsx +48 -1
- package/src/library/__tests__/visibilityLevels.test.ts +67 -1
- package/src/library/index.ts +6 -1
- package/src/library/useCanSetPublicVisibility.ts +56 -0
- package/src/library/visibilityLevels.ts +65 -14
- package/src/mcp-server/__tests__/useMcpServerConnect.asyncLane.test.tsx +117 -0
- package/src/mcp-server/useMcpServerConnect.ts +9 -6
- package/src/mcp-server/useMcpServerOAuthConnect.ts +7 -2
- package/src/oauth-app/CreateOAuthAppForm.tsx +58 -3
- package/src/oauth-app/OAuthAppDetailPanel.tsx +84 -3
- package/src/organization/OrgPreferencesPanel.tsx +221 -0
- package/src/organization/OrgProfilePanel.tsx +5 -4
- package/src/organization/__tests__/OrgPreferencesPanel.test.tsx +145 -0
- package/src/organization/__tests__/OrgProfilePanel.test.tsx +108 -0
- package/src/organization/index.ts +2 -0
- package/src/session/NewSessionViewer.tsx +12 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +151 -0
- package/src/session/useNewSessionFlow.ts +76 -9
- package/src/settings/AccountPreferencesSection.tsx +25 -0
- package/src/settings/OrgPreferencesSection.tsx +35 -0
- package/src/settings/__tests__/settings-nav.test.ts +27 -0
- package/src/settings/index.ts +2 -0
- package/src/settings/settings-nav.ts +18 -0
- package/src/workflow/ApprovalFormBuilder.tsx +5 -27
- package/styles.css +1 -1
- package/workflow/ApprovalFormBuilder.d.ts.map +1 -1
- package/workflow/ApprovalFormBuilder.js +8 -5
- package/workflow/ApprovalFormBuilder.js.map +1 -1
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type FormEvent, useCallback, useEffect, useId, useMemo, useState } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { getUserMessage, toIdentityAccountUpdateInput } from "@stigmer/sdk";
|
|
6
|
+
import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
7
|
+
import { useMyIdentityAccount } from "./useMyIdentityAccount.js";
|
|
8
|
+
import { useUpdateIdentityAccount } from "./useUpdateIdentityAccount.js";
|
|
9
|
+
import { useResourceAvailable, ApiResourceKind } from "../deployment-mode.js";
|
|
10
|
+
import { useModelRegistry } from "../models/index.js";
|
|
11
|
+
import { HARNESS_META, type HarnessOption } from "../models/harness.js";
|
|
12
|
+
import { CloudFeatureNotice } from "../internal/CloudFeatureNotice.js";
|
|
13
|
+
import { StandingContextField } from "../internal/StandingContextField.js";
|
|
14
|
+
import { SpinnerIcon } from "../internal/SpinnerIcon.js";
|
|
15
|
+
|
|
16
|
+
/** Props for {@link AccountPreferencesPanel}. */
|
|
17
|
+
export interface AccountPreferencesPanelProps {
|
|
18
|
+
/** Fired with the updated resource after a successful save. */
|
|
19
|
+
readonly onUpdated?: (account: IdentityAccount) => void;
|
|
20
|
+
/** Additional CSS class names for the root container. */
|
|
21
|
+
readonly className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Self-contained editor for the current user's declared preferences
|
|
26
|
+
* (`IdentityAccountSpec.preferences.standing_context`).
|
|
27
|
+
*
|
|
28
|
+
* The declared text is snapshotted into the user's own eligible agent
|
|
29
|
+
* executions and delivered to the agent as background context.
|
|
30
|
+
* Self-service: the account is resolved via `whoAmI()` and updated
|
|
31
|
+
* through the caller's self-ownership permission — no explicit
|
|
32
|
+
* permission check is needed. On save, calls `identityAccount.update()`
|
|
33
|
+
* with the complete mapped input (full-spec-replace safety) and fires
|
|
34
|
+
* `onUpdated`.
|
|
35
|
+
*
|
|
36
|
+
* Cloud-only: the OSS local server has no IdentityAccount (local mode
|
|
37
|
+
* is single-user, so the organization's preferences cover it). In local
|
|
38
|
+
* mode the panel renders a {@link CloudFeatureNotice} instead.
|
|
39
|
+
*
|
|
40
|
+
* All visual properties flow through `--stgm-*` design tokens. Zero
|
|
41
|
+
* dependencies on Console routing, auth context, or layout — platform
|
|
42
|
+
* builders can embed it directly:
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* <AccountPreferencesPanel />
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function AccountPreferencesPanel({
|
|
50
|
+
onUpdated,
|
|
51
|
+
className,
|
|
52
|
+
}: AccountPreferencesPanelProps) {
|
|
53
|
+
const baseId = useId();
|
|
54
|
+
const available = useResourceAvailable(ApiResourceKind.identity_account);
|
|
55
|
+
|
|
56
|
+
if (!available) {
|
|
57
|
+
return (
|
|
58
|
+
<CloudFeatureNotice className={className}>
|
|
59
|
+
Personal preferences require Stigmer Cloud. Local mode is
|
|
60
|
+
single-user, so the organization's preferences apply to every
|
|
61
|
+
execution — set standing context there instead.
|
|
62
|
+
</CloudFeatureNotice>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<AccountPreferencesForm
|
|
68
|
+
baseId={baseId}
|
|
69
|
+
onUpdated={onUpdated}
|
|
70
|
+
className={className}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Inner form, mounted only when IdentityAccount is available — keeps the
|
|
77
|
+
* data hooks from issuing doomed RPCs against a local server.
|
|
78
|
+
*/
|
|
79
|
+
function AccountPreferencesForm({
|
|
80
|
+
baseId,
|
|
81
|
+
onUpdated,
|
|
82
|
+
className,
|
|
83
|
+
}: {
|
|
84
|
+
readonly baseId: string;
|
|
85
|
+
readonly onUpdated?: (account: IdentityAccount) => void;
|
|
86
|
+
readonly className?: string;
|
|
87
|
+
}) {
|
|
88
|
+
const {
|
|
89
|
+
account,
|
|
90
|
+
isLoading: isFetching,
|
|
91
|
+
error: fetchError,
|
|
92
|
+
refetch,
|
|
93
|
+
} = useMyIdentityAccount();
|
|
94
|
+
|
|
95
|
+
const {
|
|
96
|
+
update,
|
|
97
|
+
isUpdating,
|
|
98
|
+
error: updateError,
|
|
99
|
+
clearError,
|
|
100
|
+
} = useUpdateIdentityAccount();
|
|
101
|
+
|
|
102
|
+
const [standingContext, setStandingContext] = useState("");
|
|
103
|
+
const [defaultHarness, setDefaultHarness] = useState("");
|
|
104
|
+
const [defaultNativeModel, setDefaultNativeModel] = useState("");
|
|
105
|
+
const [defaultCursorModel, setDefaultCursorModel] = useState("");
|
|
106
|
+
|
|
107
|
+
const serverPreferences = account?.spec?.preferences;
|
|
108
|
+
const serverStandingContext = serverPreferences?.standingContext ?? "";
|
|
109
|
+
const serverDefaultHarness = serverPreferences?.defaultHarness ?? "";
|
|
110
|
+
const serverDefaultNativeModel = serverPreferences?.defaultNativeModel ?? "";
|
|
111
|
+
const serverDefaultCursorModel = serverPreferences?.defaultCursorModel ?? "";
|
|
112
|
+
|
|
113
|
+
// Sync the form fields when server data changes.
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!account) return;
|
|
116
|
+
const prefs = account.spec?.preferences;
|
|
117
|
+
setStandingContext(prefs?.standingContext ?? "");
|
|
118
|
+
setDefaultHarness(prefs?.defaultHarness ?? "");
|
|
119
|
+
setDefaultNativeModel(prefs?.defaultNativeModel ?? "");
|
|
120
|
+
setDefaultCursorModel(prefs?.defaultCursorModel ?? "");
|
|
121
|
+
}, [account]);
|
|
122
|
+
|
|
123
|
+
const hasChanges = useMemo(
|
|
124
|
+
() =>
|
|
125
|
+
standingContext.trim() !== serverStandingContext ||
|
|
126
|
+
defaultHarness !== serverDefaultHarness ||
|
|
127
|
+
defaultNativeModel !== serverDefaultNativeModel ||
|
|
128
|
+
defaultCursorModel !== serverDefaultCursorModel,
|
|
129
|
+
[
|
|
130
|
+
standingContext,
|
|
131
|
+
defaultHarness,
|
|
132
|
+
defaultNativeModel,
|
|
133
|
+
defaultCursorModel,
|
|
134
|
+
serverStandingContext,
|
|
135
|
+
serverDefaultHarness,
|
|
136
|
+
serverDefaultNativeModel,
|
|
137
|
+
serverDefaultCursorModel,
|
|
138
|
+
],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const canSubmit = hasChanges && !isUpdating;
|
|
142
|
+
|
|
143
|
+
const handleDiscard = useCallback(() => {
|
|
144
|
+
setStandingContext(serverStandingContext);
|
|
145
|
+
setDefaultHarness(serverDefaultHarness);
|
|
146
|
+
setDefaultNativeModel(serverDefaultNativeModel);
|
|
147
|
+
setDefaultCursorModel(serverDefaultCursorModel);
|
|
148
|
+
clearError();
|
|
149
|
+
}, [
|
|
150
|
+
serverStandingContext,
|
|
151
|
+
serverDefaultHarness,
|
|
152
|
+
serverDefaultNativeModel,
|
|
153
|
+
serverDefaultCursorModel,
|
|
154
|
+
clearError,
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
const handleSubmit = useCallback(
|
|
158
|
+
async (e: FormEvent) => {
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
if (!canSubmit || !account) return;
|
|
161
|
+
|
|
162
|
+
clearError();
|
|
163
|
+
try {
|
|
164
|
+
// update() is a full-spec replace: spread the complete mapped input
|
|
165
|
+
// so unedited spec fields survive. `preferences` is a nested message,
|
|
166
|
+
// so the override spreads the mapper's COMPLETE preferences too —
|
|
167
|
+
// fields this form does not own (added by later phases) survive.
|
|
168
|
+
const mapped = toIdentityAccountUpdateInput(account);
|
|
169
|
+
const updated = await update({
|
|
170
|
+
...mapped,
|
|
171
|
+
preferences: {
|
|
172
|
+
...mapped.preferences,
|
|
173
|
+
standingContext: standingContext.trim() || undefined,
|
|
174
|
+
defaultHarness: defaultHarness || undefined,
|
|
175
|
+
defaultNativeModel: defaultNativeModel || undefined,
|
|
176
|
+
defaultCursorModel: defaultCursorModel || undefined,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
refetch();
|
|
180
|
+
onUpdated?.(updated);
|
|
181
|
+
} catch {
|
|
182
|
+
// error state is managed by useUpdateIdentityAccount
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
[
|
|
186
|
+
canSubmit,
|
|
187
|
+
account,
|
|
188
|
+
standingContext,
|
|
189
|
+
defaultHarness,
|
|
190
|
+
defaultNativeModel,
|
|
191
|
+
defaultCursorModel,
|
|
192
|
+
update,
|
|
193
|
+
clearError,
|
|
194
|
+
refetch,
|
|
195
|
+
onUpdated,
|
|
196
|
+
],
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// -----------------------------------------------------------------------
|
|
200
|
+
// Loading state
|
|
201
|
+
// -----------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
if (isFetching && !account) {
|
|
204
|
+
return (
|
|
205
|
+
<div
|
|
206
|
+
className={cn("stg:space-y-4", className)}
|
|
207
|
+
aria-busy="true"
|
|
208
|
+
aria-label="Loading account preferences"
|
|
209
|
+
>
|
|
210
|
+
<div className="stg:bg-muted-subtle stg:h-28 stg:animate-pulse stg:rounded" />
|
|
211
|
+
<div className="stg:bg-muted-subtle stg:h-8 stg:w-32 stg:animate-pulse stg:rounded" />
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// -----------------------------------------------------------------------
|
|
217
|
+
// Fetch error
|
|
218
|
+
// -----------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
if (fetchError) {
|
|
221
|
+
return (
|
|
222
|
+
<div className={cn("stg:space-y-3", className)} role="alert">
|
|
223
|
+
<p className="stg:text-destructive stg:text-sm">
|
|
224
|
+
{getUserMessage(fetchError)}
|
|
225
|
+
</p>
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
onClick={refetch}
|
|
229
|
+
className={cn(
|
|
230
|
+
"stg:rounded-md stg:px-3 stg:py-1.5 stg:text-xs stg:font-medium",
|
|
231
|
+
"stg:bg-primary stg:text-primary-foreground stg:hover:bg-primary-hover",
|
|
232
|
+
)}
|
|
233
|
+
>
|
|
234
|
+
Retry
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!account) return null;
|
|
241
|
+
|
|
242
|
+
// -----------------------------------------------------------------------
|
|
243
|
+
// Render
|
|
244
|
+
// -----------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<form onSubmit={handleSubmit} className={cn("stg:space-y-4", className)}>
|
|
248
|
+
<StandingContextField
|
|
249
|
+
id={`${baseId}-standing-context`}
|
|
250
|
+
value={standingContext}
|
|
251
|
+
onChange={setStandingContext}
|
|
252
|
+
disabled={isUpdating}
|
|
253
|
+
placeholder={
|
|
254
|
+
"e.g. Keep answers terse. I prefer TypeScript examples. My timezone is IST."
|
|
255
|
+
}
|
|
256
|
+
helperText="Shared with agents as background context — not instructions. Applies only to executions you start and is visible on those execution records."
|
|
257
|
+
/>
|
|
258
|
+
|
|
259
|
+
<div className="stg:space-y-3">
|
|
260
|
+
<div>
|
|
261
|
+
<h3 className="stg:text-xs stg:font-medium stg:text-foreground">
|
|
262
|
+
Execution defaults
|
|
263
|
+
</h3>
|
|
264
|
+
<p className="stg:text-[0.65rem] stg:text-muted-foreground">
|
|
265
|
+
Seed new sessions across your devices and the CLI. An explicit
|
|
266
|
+
pick in the composer always wins for that device.
|
|
267
|
+
</p>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<DefaultHarnessRadioGroup
|
|
271
|
+
baseId={baseId}
|
|
272
|
+
value={defaultHarness}
|
|
273
|
+
onChange={setDefaultHarness}
|
|
274
|
+
disabled={isUpdating}
|
|
275
|
+
/>
|
|
276
|
+
|
|
277
|
+
<div className="stg:grid stg:gap-3 stg:sm:grid-cols-2">
|
|
278
|
+
<DefaultModelSelect
|
|
279
|
+
id={`${baseId}-default-native-model`}
|
|
280
|
+
harness="native"
|
|
281
|
+
label={`Default model — ${HARNESS_META.native.label}`}
|
|
282
|
+
value={defaultNativeModel}
|
|
283
|
+
onChange={setDefaultNativeModel}
|
|
284
|
+
disabled={isUpdating}
|
|
285
|
+
/>
|
|
286
|
+
<DefaultModelSelect
|
|
287
|
+
id={`${baseId}-default-cursor-model`}
|
|
288
|
+
harness="cursor"
|
|
289
|
+
label={`Default model — ${HARNESS_META.cursor.label}`}
|
|
290
|
+
value={defaultCursorModel}
|
|
291
|
+
onChange={setDefaultCursorModel}
|
|
292
|
+
disabled={isUpdating}
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{updateError && (
|
|
298
|
+
<p className="stg:text-destructive stg:text-[0.65rem]" role="alert">
|
|
299
|
+
{getUserMessage(updateError)}
|
|
300
|
+
</p>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
<div className="stg:flex stg:items-center stg:gap-2">
|
|
304
|
+
<button
|
|
305
|
+
type="submit"
|
|
306
|
+
disabled={!canSubmit}
|
|
307
|
+
className={cn(
|
|
308
|
+
"stg:inline-flex stg:items-center stg:gap-1.5 stg:rounded-md stg:px-3 stg:py-1.5 stg:text-xs stg:font-medium",
|
|
309
|
+
"stg:bg-primary stg:text-primary-foreground stg:hover:bg-primary-hover",
|
|
310
|
+
"stg:disabled:pointer-events-none stg:disabled:opacity-40",
|
|
311
|
+
)}
|
|
312
|
+
>
|
|
313
|
+
{isUpdating && <SpinnerIcon size={12} />}
|
|
314
|
+
Save changes
|
|
315
|
+
</button>
|
|
316
|
+
|
|
317
|
+
{hasChanges && !isUpdating && (
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
onClick={handleDiscard}
|
|
321
|
+
className={cn(
|
|
322
|
+
"stg:rounded-md stg:px-3 stg:py-1.5 stg:text-xs",
|
|
323
|
+
"stg:text-muted-foreground stg:hover:text-foreground stg:hover:bg-accent-hover",
|
|
324
|
+
)}
|
|
325
|
+
>
|
|
326
|
+
Discard
|
|
327
|
+
</button>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
330
|
+
</form>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Only harnesses with a shipped runtime are offerable as a default — must
|
|
336
|
+
* stay in lockstep with the proto's `default_harness` in-list validation.
|
|
337
|
+
*/
|
|
338
|
+
const OFFERABLE_HARNESSES: readonly HarnessOption[] = ["native", "cursor"];
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Default-harness choice: Platform default plus each shipped harness.
|
|
342
|
+
* A mutually exclusive, always-visible set — radio group, not a select.
|
|
343
|
+
*/
|
|
344
|
+
function DefaultHarnessRadioGroup({
|
|
345
|
+
baseId,
|
|
346
|
+
value,
|
|
347
|
+
onChange,
|
|
348
|
+
disabled,
|
|
349
|
+
}: {
|
|
350
|
+
readonly baseId: string;
|
|
351
|
+
readonly value: string;
|
|
352
|
+
readonly onChange: (value: string) => void;
|
|
353
|
+
readonly disabled: boolean;
|
|
354
|
+
}) {
|
|
355
|
+
const name = `${baseId}-default-harness`;
|
|
356
|
+
const options: readonly { value: string; label: string }[] = [
|
|
357
|
+
{ value: "", label: "Platform default" },
|
|
358
|
+
...OFFERABLE_HARNESSES.map((h) => ({ value: h, label: HARNESS_META[h].label })),
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<fieldset className="stg:space-y-1" disabled={disabled}>
|
|
363
|
+
<legend className="stg:text-xs stg:font-medium stg:text-foreground">
|
|
364
|
+
Default harness
|
|
365
|
+
</legend>
|
|
366
|
+
<div className="stg:flex stg:flex-wrap stg:gap-x-4 stg:gap-y-1">
|
|
367
|
+
{options.map((option) => (
|
|
368
|
+
<label
|
|
369
|
+
key={option.value}
|
|
370
|
+
className={cn(
|
|
371
|
+
"stg:inline-flex stg:cursor-pointer stg:items-center stg:gap-1.5 stg:text-xs stg:text-foreground",
|
|
372
|
+
disabled && "stg:cursor-default stg:opacity-50",
|
|
373
|
+
)}
|
|
374
|
+
>
|
|
375
|
+
<input
|
|
376
|
+
type="radio"
|
|
377
|
+
name={name}
|
|
378
|
+
value={option.value}
|
|
379
|
+
checked={value === option.value}
|
|
380
|
+
onChange={() => onChange(option.value)}
|
|
381
|
+
disabled={disabled}
|
|
382
|
+
className="stg:accent-primary stg:focus-visible:outline-none stg:focus-visible:ring-1 stg:focus-visible:ring-ring"
|
|
383
|
+
/>
|
|
384
|
+
{option.label}
|
|
385
|
+
</label>
|
|
386
|
+
))}
|
|
387
|
+
</div>
|
|
388
|
+
</fieldset>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Per-harness default model select, fed by the harness-filtered registry.
|
|
394
|
+
*
|
|
395
|
+
* A saved model that the registry no longer lists is rendered as its own
|
|
396
|
+
* "(unavailable)" option so the select never lies about the stored value;
|
|
397
|
+
* composers self-heal such a preference to the platform default on read.
|
|
398
|
+
*/
|
|
399
|
+
function DefaultModelSelect({
|
|
400
|
+
id,
|
|
401
|
+
harness,
|
|
402
|
+
label,
|
|
403
|
+
value,
|
|
404
|
+
onChange,
|
|
405
|
+
disabled,
|
|
406
|
+
}: {
|
|
407
|
+
readonly id: string;
|
|
408
|
+
readonly harness: HarnessOption;
|
|
409
|
+
readonly label: string;
|
|
410
|
+
readonly value: string;
|
|
411
|
+
readonly onChange: (value: string) => void;
|
|
412
|
+
readonly disabled: boolean;
|
|
413
|
+
}) {
|
|
414
|
+
const { models, isLoading } = useModelRegistry({ harness });
|
|
415
|
+
const isStale = value !== "" && !isLoading && !models.some((m) => m.modelId === value);
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<div className="stg:space-y-1">
|
|
419
|
+
<label
|
|
420
|
+
htmlFor={id}
|
|
421
|
+
className="stg:text-xs stg:font-medium stg:text-foreground"
|
|
422
|
+
>
|
|
423
|
+
{label}
|
|
424
|
+
</label>
|
|
425
|
+
<select
|
|
426
|
+
id={id}
|
|
427
|
+
value={value}
|
|
428
|
+
onChange={(e) => onChange(e.target.value)}
|
|
429
|
+
disabled={disabled || isLoading}
|
|
430
|
+
className={cn(
|
|
431
|
+
"stg:w-full stg:rounded-md stg:border stg:border-input stg:bg-background stg:px-2.5 stg:py-1.5 stg:text-xs stg:text-foreground",
|
|
432
|
+
"stg:focus-visible:outline-none stg:focus-visible:ring-1 stg:focus-visible:ring-ring",
|
|
433
|
+
"stg:disabled:pointer-events-none stg:disabled:opacity-50",
|
|
434
|
+
)}
|
|
435
|
+
>
|
|
436
|
+
<option value="">Platform default</option>
|
|
437
|
+
{isStale && <option value={value}>{value} (unavailable)</option>}
|
|
438
|
+
{models.map((model) => (
|
|
439
|
+
<option key={model.modelId} value={model.modelId}>
|
|
440
|
+
{model.displayName || model.modelId}
|
|
441
|
+
</option>
|
|
442
|
+
))}
|
|
443
|
+
</select>
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import {
|
|
5
|
+
IdentityAccountSchema,
|
|
6
|
+
type IdentityAccount,
|
|
7
|
+
} from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
8
|
+
import type { IdentityAccountInput } from "@stigmer/sdk";
|
|
9
|
+
import type { DeploymentMode } from "@stigmer/sdk";
|
|
10
|
+
import { StigmerContext } from "../../context";
|
|
11
|
+
import { DeploymentModeContext } from "../../deployment-mode";
|
|
12
|
+
import { ModelRegistryContext, type ModelRegistryState } from "../../models/ModelRegistryContext";
|
|
13
|
+
import { parseRegistryJson } from "../../models/registry";
|
|
14
|
+
import { AccountPreferencesPanel } from "../AccountPreferencesPanel";
|
|
15
|
+
|
|
16
|
+
const ACCOUNT: IdentityAccount = create(IdentityAccountSchema, {
|
|
17
|
+
metadata: { id: "ia-1", name: "Ada Lovelace", slug: "ada", org: "acme" },
|
|
18
|
+
spec: {
|
|
19
|
+
idpId: "auth0|abc",
|
|
20
|
+
email: "ada@acme.example",
|
|
21
|
+
preferences: { standingContext: "Keep answers terse." },
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const TEST_MODELS = parseRegistryJson({
|
|
26
|
+
models: [
|
|
27
|
+
{ id: "claude-sonnet-4.6", displayName: "Claude Sonnet 4.6", shortDescription: "", speedTier: "fast", provider: "anthropic", harness: "native", costTier: "standard", featured: true, pricing: { inputPricePerMillion: 3, outputPricePerMillion: 15, cacheWritePricePerMillion: 3.75, cacheReadPricePerMillion: 0.3 } },
|
|
28
|
+
{ id: "default", displayName: "Cursor Auto", shortDescription: "", speedTier: "fast", provider: "cursor", harness: "cursor", costTier: "standard", featured: true, pricing: { inputPricePerMillion: 1.25, outputPricePerMillion: 6, cacheWritePricePerMillion: 1.25, cacheReadPricePerMillion: 0.25 } },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const REGISTRY_STATE: ModelRegistryState = {
|
|
33
|
+
models: TEST_MODELS,
|
|
34
|
+
isLoading: false,
|
|
35
|
+
error: null,
|
|
36
|
+
refetch: () => {},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function createMockStigmer(overrides?: {
|
|
40
|
+
whoAmI?: ReturnType<typeof vi.fn>;
|
|
41
|
+
update?: ReturnType<typeof vi.fn>;
|
|
42
|
+
}) {
|
|
43
|
+
return {
|
|
44
|
+
identityAccount: {
|
|
45
|
+
whoAmI: overrides?.whoAmI ?? vi.fn(async () => ACCOUNT),
|
|
46
|
+
update: overrides?.update ?? vi.fn(async () => ACCOUNT),
|
|
47
|
+
},
|
|
48
|
+
} as never;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderPanel(client: unknown, mode: DeploymentMode = "cloud") {
|
|
52
|
+
return render(
|
|
53
|
+
<StigmerContext.Provider value={client as never}>
|
|
54
|
+
<DeploymentModeContext.Provider value={mode}>
|
|
55
|
+
<ModelRegistryContext.Provider value={REGISTRY_STATE}>
|
|
56
|
+
<AccountPreferencesPanel />
|
|
57
|
+
</ModelRegistryContext.Provider>
|
|
58
|
+
</DeploymentModeContext.Provider>
|
|
59
|
+
</StigmerContext.Provider>,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
afterEach(cleanup);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Waits until the form has synced from server data (the panel copies the
|
|
67
|
+
* fetched value into local state in a passive effect, one flush after the
|
|
68
|
+
* field first renders) — interacting earlier races the sync.
|
|
69
|
+
*/
|
|
70
|
+
async function findSyncedField(value: string) {
|
|
71
|
+
const field = await screen.findByLabelText("Standing context");
|
|
72
|
+
await waitFor(() => expect(field).toHaveProperty("value", value));
|
|
73
|
+
return field;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("AccountPreferencesPanel", () => {
|
|
77
|
+
it("renders the cloud notice in local mode without issuing any RPCs", () => {
|
|
78
|
+
const whoAmI = vi.fn(async () => ACCOUNT);
|
|
79
|
+
renderPanel(createMockStigmer({ whoAmI }), "local");
|
|
80
|
+
|
|
81
|
+
expect(screen.getByRole("status")).toBeTruthy();
|
|
82
|
+
expect(screen.queryByLabelText("Standing context")).toBeNull();
|
|
83
|
+
// The inner form must not mount — no doomed whoAmI against a local server.
|
|
84
|
+
expect(whoAmI).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("loads and displays the caller's declared standing context", async () => {
|
|
88
|
+
renderPanel(createMockStigmer());
|
|
89
|
+
|
|
90
|
+
await findSyncedField("Keep answers terse.");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("saves the full mapped input — identity fields survive (wipe-bug guard)", async () => {
|
|
94
|
+
const update = vi.fn(async (_input: IdentityAccountInput) => ACCOUNT);
|
|
95
|
+
renderPanel(createMockStigmer({ update }));
|
|
96
|
+
|
|
97
|
+
const field = await findSyncedField("Keep answers terse.");
|
|
98
|
+
fireEvent.change(field, { target: { value: "Prefer bullet points." } });
|
|
99
|
+
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
100
|
+
|
|
101
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
102
|
+
const input = update.mock.calls[0]![0];
|
|
103
|
+
|
|
104
|
+
expect(input.preferences).toEqual({
|
|
105
|
+
standingContext: "Prefer bullet points.",
|
|
106
|
+
});
|
|
107
|
+
// Fields this form never renders must round-trip untouched.
|
|
108
|
+
expect(input.idpId).toBe("auth0|abc");
|
|
109
|
+
expect(input.email).toBe("ada@acme.example");
|
|
110
|
+
// The update pipeline addresses the resource by org + slug.
|
|
111
|
+
expect(input.org).toBe("acme");
|
|
112
|
+
expect(input.slug).toBe("ada");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("refetches the account after a successful save", async () => {
|
|
116
|
+
const whoAmI = vi.fn(async () => ACCOUNT);
|
|
117
|
+
renderPanel(createMockStigmer({ whoAmI }));
|
|
118
|
+
|
|
119
|
+
const field = await findSyncedField("Keep answers terse.");
|
|
120
|
+
fireEvent.change(field, { target: { value: "New context" } });
|
|
121
|
+
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
122
|
+
|
|
123
|
+
// Initial load + post-save refetch.
|
|
124
|
+
await waitFor(() => expect(whoAmI).toHaveBeenCalledTimes(2));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("shows the fetch error with a retry action when whoAmI fails", async () => {
|
|
128
|
+
const whoAmI = vi.fn(async () => {
|
|
129
|
+
throw new Error("boom");
|
|
130
|
+
});
|
|
131
|
+
renderPanel(createMockStigmer({ whoAmI }));
|
|
132
|
+
|
|
133
|
+
expect(await screen.findByRole("alert")).toBeTruthy();
|
|
134
|
+
expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("execution defaults (oss#293 Phase 1.5)", () => {
|
|
138
|
+
const ACCOUNT_WITH_DEFAULTS: IdentityAccount = create(IdentityAccountSchema, {
|
|
139
|
+
metadata: { id: "ia-1", name: "Ada Lovelace", slug: "ada", org: "acme" },
|
|
140
|
+
spec: {
|
|
141
|
+
idpId: "auth0|abc",
|
|
142
|
+
email: "ada@acme.example",
|
|
143
|
+
preferences: {
|
|
144
|
+
standingContext: "Keep answers terse.",
|
|
145
|
+
defaultHarness: "cursor",
|
|
146
|
+
defaultNativeModel: "claude-sonnet-4.6",
|
|
147
|
+
defaultCursorModel: "default",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("renders the harness radio group and registry-fed model selects", async () => {
|
|
153
|
+
renderPanel(createMockStigmer());
|
|
154
|
+
await findSyncedField("Keep answers terse.");
|
|
155
|
+
|
|
156
|
+
expect(screen.getByRole("radio", { name: "Platform default" })).toBeTruthy();
|
|
157
|
+
expect(screen.getByRole("radio", { name: "Stigmer" })).toBeTruthy();
|
|
158
|
+
expect(screen.getByRole("radio", { name: "Cursor" })).toBeTruthy();
|
|
159
|
+
expect(screen.getByLabelText("Default model — Stigmer")).toBeTruthy();
|
|
160
|
+
expect(screen.getByLabelText("Default model — Cursor")).toBeTruthy();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("reflects the saved defaults in the form", async () => {
|
|
164
|
+
renderPanel(createMockStigmer({ whoAmI: vi.fn(async () => ACCOUNT_WITH_DEFAULTS) }));
|
|
165
|
+
await findSyncedField("Keep answers terse.");
|
|
166
|
+
|
|
167
|
+
await waitFor(() => {
|
|
168
|
+
expect(screen.getByRole("radio", { name: "Cursor" })).toHaveProperty("checked", true);
|
|
169
|
+
expect(screen.getByLabelText("Default model — Stigmer")).toHaveProperty("value", "claude-sonnet-4.6");
|
|
170
|
+
expect(screen.getByLabelText("Default model — Cursor")).toHaveProperty("value", "default");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("saves the complete preferences object — all default fields in one write", async () => {
|
|
175
|
+
const update = vi.fn(async (_input: IdentityAccountInput) => ACCOUNT);
|
|
176
|
+
renderPanel(createMockStigmer({ update }));
|
|
177
|
+
await findSyncedField("Keep answers terse.");
|
|
178
|
+
|
|
179
|
+
fireEvent.click(screen.getByRole("radio", { name: "Cursor" }));
|
|
180
|
+
fireEvent.change(screen.getByLabelText("Default model — Stigmer"), {
|
|
181
|
+
target: { value: "claude-sonnet-4.6" },
|
|
182
|
+
});
|
|
183
|
+
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
184
|
+
|
|
185
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
186
|
+
expect(update.mock.calls[0]![0].preferences).toEqual({
|
|
187
|
+
standingContext: "Keep answers terse.",
|
|
188
|
+
defaultHarness: "cursor",
|
|
189
|
+
defaultNativeModel: "claude-sonnet-4.6",
|
|
190
|
+
defaultCursorModel: undefined,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("editing only standing context preserves the structured defaults (nested wipe-bug guard)", async () => {
|
|
195
|
+
const update = vi.fn(async (_input: IdentityAccountInput) => ACCOUNT_WITH_DEFAULTS);
|
|
196
|
+
renderPanel(
|
|
197
|
+
createMockStigmer({ whoAmI: vi.fn(async () => ACCOUNT_WITH_DEFAULTS), update }),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const field = await findSyncedField("Keep answers terse.");
|
|
201
|
+
fireEvent.change(field, { target: { value: "Terser still." } });
|
|
202
|
+
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
203
|
+
|
|
204
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
205
|
+
expect(update.mock.calls[0]![0].preferences).toEqual({
|
|
206
|
+
standingContext: "Terser still.",
|
|
207
|
+
defaultHarness: "cursor",
|
|
208
|
+
defaultNativeModel: "claude-sonnet-4.6",
|
|
209
|
+
defaultCursorModel: "default",
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("keeps a stale saved model legible as an unavailable option", async () => {
|
|
214
|
+
const stale = create(IdentityAccountSchema, {
|
|
215
|
+
metadata: { id: "ia-1", name: "Ada", slug: "ada", org: "acme" },
|
|
216
|
+
spec: {
|
|
217
|
+
idpId: "auth0|abc",
|
|
218
|
+
preferences: { defaultNativeModel: "retired-model" },
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
renderPanel(createMockStigmer({ whoAmI: vi.fn(async () => stale) }));
|
|
222
|
+
await findSyncedField("");
|
|
223
|
+
|
|
224
|
+
await waitFor(() => {
|
|
225
|
+
const select = screen.getByLabelText("Default model — Stigmer") as HTMLSelectElement;
|
|
226
|
+
expect(select.value).toBe("retired-model");
|
|
227
|
+
expect(
|
|
228
|
+
Array.from(select.options).some((o) => o.text === "retired-model (unavailable)"),
|
|
229
|
+
).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|