1import { createElement } from "react";
2import { renderToStaticMarkup } from "react-dom/server";
3import { describe, expect, it } from "vite-plus/test";
4import {
5 ProviderDriverKind,
6 ProviderInstanceId,
7 type ServerProvider,
8 type ServerProviderModel,
9} from "@t3tools/contracts";
10
11import {
12 deriveProviderModelsForDisplay,
13 nextProviderEnvironmentWithFieldValue,
14 providerEnvironmentWithoutNames,
15 ProviderInstanceCard,
16 readProviderEnvironmentVariable,
17} from "./ProviderInstanceCard";
18
19describe("deriveProviderModelsForDisplay", () => {
20 it("uses current config custom models instead of stale live custom rows", () => {
21 const liveModels: ReadonlyArray<ServerProviderModel> = [
22 {
23 slug: "server-model",
24 name: "Server Model",
25 isCustom: false,
26 capabilities: null,
27 },
28 {
29 slug: "removed-custom",
30 name: "Removed Custom",
31 isCustom: true,
32 capabilities: null,
33 },
34 {
35 slug: "kept-custom",
36 name: "Kept Custom",
37 isCustom: true,
38 capabilities: null,
39 },
40 ];
41
42 expect(
43 deriveProviderModelsForDisplay({
44 liveModels,
45 customModels: [{ slug: "kept-custom", name: "kept-custom", capabilities: null }],
46 }).map((model) => model.slug),
47 ).toEqual(["server-model", "kept-custom"]);
48 });
49
50 it("prefers the entry's name and capabilities over the stale live custom row", () => {
51 const liveCapabilities = { optionDescriptors: [] };
52 const customCapabilities = {
53 optionDescriptors: [
54 {
55 id: "reasoningEffort",
56 label: "Reasoning",
57 type: "select" as const,
58 options: [{ id: "high", label: "High", isDefault: true }],
59 currentValue: "high",
60 },
61 ],
62 };
63 const liveModels: ReadonlyArray<ServerProviderModel> = [
64 { slug: "bare", name: "bare", isCustom: true, capabilities: liveCapabilities },
65 { slug: "named", name: "named", isCustom: true, capabilities: liveCapabilities },
66 ];
67
68 const display = deriveProviderModelsForDisplay({
69 liveModels,
70 customModels: [
71 { slug: "bare", name: "bare", capabilities: null },
72 { slug: "named", name: "My Model", capabilities: customCapabilities },
73 ],
74 });
75
76 // A bare entry keeps the driver default the server filled in.
77 expect(display[0]).toEqual({
78 slug: "bare",
79 name: "bare",
80 isCustom: true,
81 capabilities: liveCapabilities,
82 });
83 expect(display[1]).toEqual({
84 slug: "named",
85 name: "My Model",
86 isCustom: true,
87 capabilities: customCapabilities,
88 });
89 });
90
91 it("shows a redacted provider email in the editor header status line", () => {
92 const instanceId = ProviderInstanceId.make("codex");
93 const driver = ProviderDriverKind.make("codex");
94 const liveProvider: ServerProvider = {
95 instanceId,
96 driver,
97 enabled: true,
98 installed: true,
99 version: "1.0.0",
100 status: "ready",
101 auth: { status: "authenticated", email: "developer@example.com" },
102 checkedAt: "2026-08-27T12:00:00.000Z",
103 models: [],
104 slashCommands: [],
105 skills: [],
106 };
107
108 const markup = renderToStaticMarkup(
109 createElement(ProviderInstanceCard, {
110 instanceId,
111 instance: { driver },
112 driverOption: undefined,
113 liveProvider,
114 mode: "editor",
115 onUpdate: () => undefined,
116 hiddenModels: [],
117 favoriteModels: [],
118 modelOrder: [],
119 onHiddenModelsChange: () => undefined,
120 onFavoriteModelsChange: () => undefined,
121 onToggleFavorite: () => undefined,
122 onModelOrderChange: () => undefined,
123 }),
124 );
125
126 expect(markup).toContain("Authenticated as");
127 expect(markup).toContain('aria-label="Toggle account email visibility"');
128 expect(markup).toContain("blur-xs");
129 expect(markup).not.toContain("developer@example.com");
130 });
131 it("surfaces a failed probe message in both the list row and the editor", () => {
132 const instanceId = ProviderInstanceId.make("codex_work");
133 const driver = ProviderDriverKind.make("codex");
134 const message =
135 "Codex app-server provider probe failed: Cannot create Codex shadow home entry 'auth.json' because '/home/me/.codex-t3/work/auth.json' already exists and is not a symlink.";
136 const liveProvider: ServerProvider = {
137 instanceId,
138 driver,
139 enabled: true,
140 installed: true,
141 version: null,
142 status: "error",
143 auth: { status: "unknown" },
144 checkedAt: "2026-08-28T12:00:00.000Z",
145 models: [],
146 slashCommands: [],
147 skills: [],
148 message,
149 };
150 const props = {
151 instanceId,
152 instance: { driver },
153 driverOption: undefined,
154 liveProvider,
155 onUpdate: () => undefined,
156 hiddenModels: [],
157 favoriteModels: [],
158 modelOrder: [],
159 onHiddenModelsChange: () => undefined,
160 onFavoriteModelsChange: () => undefined,
161 onToggleFavorite: () => undefined,
162 onModelOrderChange: () => undefined,
163 } as const;
164
165 for (const mode of ["list", "editor"] as const) {
166 const markup = renderToStaticMarkup(createElement(ProviderInstanceCard, { ...props, mode }));
167 expect(markup).toContain("Unavailable");
168 expect(markup).toContain("is not a symlink");
169 }
170 });
171});
172
173describe("provider environment helpers", () => {
174 const cursorApiKeyField = {
175 name: "CURSOR_API_KEY",
176 label: "Cursor API key",
177 sensitive: true,
178 };
179
180 it("writes dedicated provider secrets as sensitive environment variables", () => {
181 expect(
182 nextProviderEnvironmentWithFieldValue(
183 [{ name: "EXTRA_FLAG", value: "1", sensitive: false }],
184 cursorApiKeyField,
185 " cursor-key ",
186 ),
187 ).toEqual([
188 { name: "EXTRA_FLAG", value: "1", sensitive: false },
189 { name: "CURSOR_API_KEY", value: "cursor-key", sensitive: true },
190 ]);
191 });
192
193 it("replaces redacted provider secrets without preserving redaction markers", () => {
194 expect(
195 nextProviderEnvironmentWithFieldValue(
196 [
197 {
198 name: "CURSOR_API_KEY",
199 value: "",
200 sensitive: true,
201 valueRedacted: true,
202 },
203 ],
204 cursorApiKeyField,
205 "new-key",
206 ),
207 ).toEqual([{ name: "CURSOR_API_KEY", value: "new-key", sensitive: true }]);
208 });
209
210 it("applies the secure field default when replacing an existing non-sensitive value", () => {
211 expect(
212 nextProviderEnvironmentWithFieldValue(
213 [{ name: "OPENAI_API_KEY", value: "old-key", sensitive: false }],
214 {
215 name: "OPENAI_API_KEY",
216 label: "OpenAI API key",
217 },
218 "new-key",
219 ),
220 ).toEqual([{ name: "OPENAI_API_KEY", value: "new-key", sensitive: true }]);
221 });
222
223 it("separates dedicated provider secrets from the generic environment table", () => {
224 const environment = [
225 { name: "CURSOR_API_KEY", value: "cursor-key", sensitive: true },
226 { name: "EXTRA_FLAG", value: "1", sensitive: false },
227 ];
228
229 expect(readProviderEnvironmentVariable(environment, "CURSOR_API_KEY")?.value).toBe(
230 "cursor-key",
231 );
232 expect(providerEnvironmentWithoutNames(environment, new Set(["CURSOR_API_KEY"]))).toEqual([
233 { name: "EXTRA_FLAG", value: "1", sensitive: false },
234 ]);
235 });
236});