@truedat/ai 8.14.2 → 8.14.3
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/package.json +4 -4
- package/src/api.js +15 -0
- package/src/components/AiRoutes.js +8 -0
- package/src/components/__tests__/CustomInstructions.spec.js +263 -0
- package/src/components/constants.js +0 -4
- package/src/components/customInstructions/CustomInstructions.js +178 -0
- package/src/components/customInstructions/useLeaveConfirmation.js +50 -0
- package/src/components/providers/EmbeddingModels.js +336 -0
- package/src/components/providers/HelpIcon.js +38 -0
- package/src/components/providers/ProviderEditor.js +114 -34
- package/src/components/providers/Providers.js +73 -20
- package/src/components/providers/__tests__/EmbeddingModels.spec.js +455 -0
- package/src/components/providers/__tests__/ProviderEditor.spec.js +213 -1
- package/src/components/providers/__tests__/Providers.spec.js +251 -12
- package/src/components/providers/__tests__/__snapshots__/ProviderEditor.spec.js.snap +212 -342
- package/src/components/providers/__tests__/__snapshots__/Providers.spec.js.snap +11 -0
- package/src/hooks/__tests__/useIndices.spec.js +75 -0
- package/src/hooks/__tests__/useProviders.spec.js +14 -0
- package/src/hooks/useCustomInstructions.js +22 -0
- package/src/hooks/useIndices.js +37 -0
- package/src/hooks/useProviders.js +17 -1
- package/src/serverErrors.js +2 -0
|
@@ -3,6 +3,29 @@ import { waitFor } from "@testing-library/react";
|
|
|
3
3
|
import { render, waitForLoad } from "@truedat/test/render";
|
|
4
4
|
import ProviderEditor from "../ProviderEditor";
|
|
5
5
|
|
|
6
|
+
jest.mock("@truedat/ai/hooks/useIndices", () => ({
|
|
7
|
+
useIndices: () => ({ data: [], loading: false, mutate: jest.fn() }),
|
|
8
|
+
useIndexCreate: () => ({ trigger: jest.fn(), isMutating: false }),
|
|
9
|
+
useIndexDelete: () => ({ trigger: jest.fn(), isMutating: false }),
|
|
10
|
+
useIndexEnable: () => ({ trigger: jest.fn(), isMutating: false }),
|
|
11
|
+
useIndexDisable: () => ({ trigger: jest.fn(), isMutating: false }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const mockEnable = jest.fn();
|
|
15
|
+
const mockDisable = jest.fn();
|
|
16
|
+
let mockEnabling = false;
|
|
17
|
+
|
|
18
|
+
jest.mock("@truedat/ai/hooks/useProviders", () => ({
|
|
19
|
+
useProviderEnable: () => ({
|
|
20
|
+
trigger: (...a) => mockEnable(...a),
|
|
21
|
+
isMutating: mockEnabling,
|
|
22
|
+
}),
|
|
23
|
+
useProviderDisable: () => ({
|
|
24
|
+
trigger: (...a) => mockDisable(...a),
|
|
25
|
+
isMutating: false,
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
6
29
|
jest.mock("@truedat/core/hooks", () => ({
|
|
7
30
|
useLocales: jest.fn(() => ({
|
|
8
31
|
loading: false,
|
|
@@ -171,6 +194,196 @@ describe("<ProviderEditor />", () => {
|
|
|
171
194
|
});
|
|
172
195
|
});
|
|
173
196
|
|
|
197
|
+
describe("<ProviderEditor /> enabled provider", () => {
|
|
198
|
+
const ENABLED_AT = "2026-10-29T10:00:00Z";
|
|
199
|
+
const saved = { ...props.selectedProvider, enabled_at: null, agents: null };
|
|
200
|
+
const renderEditor = async (selectedProvider, extra = {}) => {
|
|
201
|
+
const onSubmit = jest.fn();
|
|
202
|
+
const onRefresh = jest.fn();
|
|
203
|
+
const rendered = render(
|
|
204
|
+
<ProviderEditor
|
|
205
|
+
{...props}
|
|
206
|
+
{...{
|
|
207
|
+
selectedProvider,
|
|
208
|
+
onSubmit,
|
|
209
|
+
onRefresh,
|
|
210
|
+
providers: [selectedProvider],
|
|
211
|
+
}}
|
|
212
|
+
{...extra}
|
|
213
|
+
/>,
|
|
214
|
+
renderOpts,
|
|
215
|
+
);
|
|
216
|
+
await waitForLoad(rendered);
|
|
217
|
+
return { ...rendered, onSubmit, onRefresh };
|
|
218
|
+
};
|
|
219
|
+
const toggle = (r) => r.container.querySelector('input[type="checkbox"]');
|
|
220
|
+
|
|
221
|
+
beforeEach(() => {
|
|
222
|
+
mockEnable.mockReset().mockResolvedValue({});
|
|
223
|
+
mockDisable.mockReset().mockResolvedValue({});
|
|
224
|
+
mockEnabling = false;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("has no role dropdown and no default checkbox", async () => {
|
|
228
|
+
const r = await renderEditor(saved);
|
|
229
|
+
expect(r.queryByText("providers.form.agents")).not.toBeInTheDocument();
|
|
230
|
+
expect(r.queryByText("providers.form.default")).not.toBeInTheDocument();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("shows the toggle with the hint behind a ? icon (12-4)", async () => {
|
|
234
|
+
const r = await renderEditor(saved);
|
|
235
|
+
const user = userEvent.setup({ delay: null });
|
|
236
|
+
expect(r.getByText("providers.form.enabled")).toBeVisible();
|
|
237
|
+
expect(r.queryByText("providers.form.enabled.hint")).toBeNull();
|
|
238
|
+
await user.hover(r.getByTestId("help-provider-enabled"));
|
|
239
|
+
expect(await r.findByText("providers.form.enabled.hint")).toBeVisible();
|
|
240
|
+
expect(toggle(r)).not.toBeChecked();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("offers the toggle on a new provider, off when others exist", async () => {
|
|
244
|
+
const draft = { name: "", type: "openai", properties: {} };
|
|
245
|
+
const r = await renderEditor(draft, { providers: [saved] });
|
|
246
|
+
expect(r.getByText("providers.form.enabled")).toBeVisible();
|
|
247
|
+
expect(toggle(r)).not.toBeChecked();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("starts on for the first provider ever created", async () => {
|
|
251
|
+
const draft = { name: "", type: "openai", properties: {} };
|
|
252
|
+
const r = await renderEditor(draft, { providers: [] });
|
|
253
|
+
expect(toggle(r)).toBeChecked();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("passes the enable choice to onSubmit when creating", async () => {
|
|
257
|
+
const draft = {
|
|
258
|
+
name: "new",
|
|
259
|
+
type: "openai",
|
|
260
|
+
properties: { model: "m", organization_key: "o" },
|
|
261
|
+
};
|
|
262
|
+
const r = await renderEditor(draft, { providers: [saved] });
|
|
263
|
+
const user = userEvent.setup({ delay: null });
|
|
264
|
+
await user.click(r.getByText("providers.form.enabled"));
|
|
265
|
+
expect(toggle(r)).toBeChecked();
|
|
266
|
+
expect(mockEnable).not.toHaveBeenCalled();
|
|
267
|
+
await user.type(r.getAllByRole("textbox")[0], "x");
|
|
268
|
+
await user.click(r.getByRole("button", { name: /save/i }));
|
|
269
|
+
await waitFor(() => expect(r.onSubmit).toHaveBeenCalled());
|
|
270
|
+
expect(r.onSubmit.mock.calls[0][1]).toEqual({ enable: true });
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("reads the toggle from the refreshed list, not from selectedProvider (12-5)", async () => {
|
|
274
|
+
const r = await renderEditor(saved, {
|
|
275
|
+
providers: [{ ...saved, enabled_at: ENABLED_AT }],
|
|
276
|
+
});
|
|
277
|
+
expect(toggle(r)).toBeChecked();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("enables at once without marking the form dirty (12-4, 12-5)", async () => {
|
|
281
|
+
const r = await renderEditor(saved);
|
|
282
|
+
const user = userEvent.setup({ delay: null });
|
|
283
|
+
await user.click(r.getByText("providers.form.enabled"));
|
|
284
|
+
await waitFor(() => expect(mockEnable).toHaveBeenCalledWith({ id: 1 }));
|
|
285
|
+
await waitFor(() => expect(r.onRefresh).toHaveBeenCalled());
|
|
286
|
+
expect(mockDisable).not.toHaveBeenCalled();
|
|
287
|
+
expect(r.getByRole("button", { name: /save/i })).toBeDisabled();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("asks for confirmation before disabling (12-5)", async () => {
|
|
291
|
+
const r = await renderEditor({ ...saved, enabled_at: ENABLED_AT });
|
|
292
|
+
const user = userEvent.setup({ delay: null });
|
|
293
|
+
expect(toggle(r)).toBeChecked();
|
|
294
|
+
await user.click(r.getByText("providers.form.enabled"));
|
|
295
|
+
expect(
|
|
296
|
+
r.getByText("providers.enabled.disable.confirm.header"),
|
|
297
|
+
).toBeVisible();
|
|
298
|
+
expect(
|
|
299
|
+
r.getByText("providers.enabled.disable.confirm.content"),
|
|
300
|
+
).toBeVisible();
|
|
301
|
+
expect(mockDisable).not.toHaveBeenCalled();
|
|
302
|
+
await user.click(r.getByRole("button", { name: /modal-negative-action/i }));
|
|
303
|
+
expect(mockDisable).not.toHaveBeenCalled();
|
|
304
|
+
await user.click(r.getByText("providers.form.enabled"));
|
|
305
|
+
await user.click(
|
|
306
|
+
r.getByRole("button", { name: /modal-affirmative-action/i }),
|
|
307
|
+
);
|
|
308
|
+
await waitFor(() => expect(mockDisable).toHaveBeenCalledWith({ id: 1 }));
|
|
309
|
+
await waitFor(() => expect(r.onRefresh).toHaveBeenCalled());
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("shows a server error in the editor (12-5)", async () => {
|
|
313
|
+
mockEnable.mockRejectedValue({
|
|
314
|
+
response: { data: { errors: { enabled_at: ["already taken"] } } },
|
|
315
|
+
});
|
|
316
|
+
const r = await renderEditor(saved);
|
|
317
|
+
const user = userEvent.setup({ delay: null });
|
|
318
|
+
await user.click(r.getByText("providers.form.enabled"));
|
|
319
|
+
expect(await r.findByText("already taken")).toBeVisible();
|
|
320
|
+
expect(r.onRefresh).toHaveBeenCalled();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("lets the editor server error be dismissed", async () => {
|
|
324
|
+
mockEnable.mockRejectedValue({
|
|
325
|
+
response: { data: { errors: { enabled_at: ["already taken"] } } },
|
|
326
|
+
});
|
|
327
|
+
const r = await renderEditor(saved);
|
|
328
|
+
const user = userEvent.setup({ delay: null });
|
|
329
|
+
await user.click(r.getByText("providers.form.enabled"));
|
|
330
|
+
expect(await r.findByText("already taken")).toBeVisible();
|
|
331
|
+
await user.click(r.container.querySelector(".message .close.icon"));
|
|
332
|
+
expect(r.queryByText("already taken")).not.toBeInTheDocument();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("keeps the toggle disabled until the refresh has landed (12-5)", async () => {
|
|
336
|
+
let finishRefresh;
|
|
337
|
+
const onRefresh = jest.fn(
|
|
338
|
+
() => new Promise((resolve) => (finishRefresh = resolve)),
|
|
339
|
+
);
|
|
340
|
+
const r = await renderEditor(saved, { onRefresh });
|
|
341
|
+
const user = userEvent.setup({ delay: null });
|
|
342
|
+
await user.click(r.getByText("providers.form.enabled"));
|
|
343
|
+
await waitFor(() => expect(onRefresh).toHaveBeenCalled());
|
|
344
|
+
expect(toggle(r)).toBeDisabled();
|
|
345
|
+
finishRefresh();
|
|
346
|
+
await waitFor(() => expect(toggle(r)).toBeEnabled());
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("disables the toggle while a request is in flight (12-5)", async () => {
|
|
350
|
+
mockEnabling = true;
|
|
351
|
+
const r = await renderEditor(saved);
|
|
352
|
+
expect(toggle(r)).toBeDisabled();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("sends no is_default, agents, enabled or enabled_at on save (12-7, 12-10)", async () => {
|
|
356
|
+
const r = await renderEditor({
|
|
357
|
+
...saved,
|
|
358
|
+
enabled_at: ENABLED_AT,
|
|
359
|
+
agents: "fast",
|
|
360
|
+
is_default: true,
|
|
361
|
+
enabled: true,
|
|
362
|
+
});
|
|
363
|
+
const user = userEvent.setup({ delay: null });
|
|
364
|
+
await user.type(r.getAllByRole("textbox")[0], "x");
|
|
365
|
+
const save = r.getByRole("button", { name: /save/i });
|
|
366
|
+
await waitFor(() => expect(save).toBeEnabled());
|
|
367
|
+
await user.click(save);
|
|
368
|
+
await waitFor(() => expect(r.onSubmit).toHaveBeenCalled());
|
|
369
|
+
const payload = r.onSubmit.mock.calls[0][0];
|
|
370
|
+
for (const key of ["is_default", "agents", "enabled", "enabled_at"]) {
|
|
371
|
+
expect(payload).not.toHaveProperty(key);
|
|
372
|
+
}
|
|
373
|
+
expect(payload.name).toBe("provider1x");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("shows embedding models only for saved providers", async () => {
|
|
377
|
+
const r = await renderEditor(saved);
|
|
378
|
+
expect(r.getByText("providers.embeddingModels.title")).toBeVisible();
|
|
379
|
+
r.unmount();
|
|
380
|
+
const n = await renderEditor({ name: "", type: "openai", properties: {} });
|
|
381
|
+
expect(
|
|
382
|
+
n.queryByText("providers.embeddingModels.title"),
|
|
383
|
+
).not.toBeInTheDocument();
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
174
387
|
describe("<ProviderEditor /> LLM gateway", () => {
|
|
175
388
|
const gatewayRenderOpts = {
|
|
176
389
|
...renderOpts,
|
|
@@ -288,7 +501,6 @@ describe("<ProviderEditor /> LLM gateway", () => {
|
|
|
288
501
|
expect(rendered.onSubmit.mock.calls[0][0]).toEqual({
|
|
289
502
|
name: "Company models",
|
|
290
503
|
type: "llm_gateway",
|
|
291
|
-
agents: undefined,
|
|
292
504
|
properties: {
|
|
293
505
|
base_url: values[0],
|
|
294
506
|
model: values[1],
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
import userEvent from "@testing-library/user-event";
|
|
1
2
|
import { waitFor } from "@testing-library/react";
|
|
2
3
|
import { renderWithIntl as render } from "@truedat/test/renderWithIntl";
|
|
3
4
|
import Providers from "../Providers";
|
|
4
5
|
|
|
6
|
+
let mockIndices = [];
|
|
7
|
+
|
|
8
|
+
jest.mock("@truedat/ai/hooks/useIndices", () => ({
|
|
9
|
+
useIndices: () => ({ data: mockIndices, loading: false, mutate: jest.fn() }),
|
|
10
|
+
useIndexCreate: () => ({ trigger: jest.fn(), isMutating: false }),
|
|
11
|
+
useIndexDelete: () => ({ trigger: jest.fn(), isMutating: false }),
|
|
12
|
+
useIndexEnable: () => ({ trigger: jest.fn(), isMutating: false }),
|
|
13
|
+
useIndexDisable: () => ({ trigger: jest.fn(), isMutating: false }),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const mockDeleteTrigger = jest.fn();
|
|
17
|
+
const mockCreateTrigger = jest.fn();
|
|
18
|
+
const mockEnableTrigger = jest.fn();
|
|
19
|
+
let mockProviders;
|
|
20
|
+
|
|
5
21
|
jest.mock("@truedat/ai/hooks/useProviders", () => {
|
|
6
22
|
const originalModule = jest.requireActual("@truedat/ai/hooks/useProviders");
|
|
7
23
|
|
|
@@ -9,24 +25,24 @@ jest.mock("@truedat/ai/hooks/useProviders", () => {
|
|
|
9
25
|
__esModule: true,
|
|
10
26
|
...originalModule,
|
|
11
27
|
useProviders: jest.fn(() => ({
|
|
12
|
-
data: {
|
|
13
|
-
data: [
|
|
14
|
-
{
|
|
15
|
-
name: "rm1",
|
|
16
|
-
resource_type: "boolean",
|
|
17
|
-
fields: [{ source: "s1", target: "t1" }],
|
|
18
|
-
selector: { system_external_id: 1 },
|
|
19
|
-
},
|
|
20
|
-
],
|
|
21
|
-
},
|
|
28
|
+
data: { data: mockProviders },
|
|
22
29
|
loading: false,
|
|
30
|
+
mutate: jest.fn(),
|
|
23
31
|
})),
|
|
24
32
|
useProviderCreate: jest.fn(() => ({
|
|
25
|
-
trigger:
|
|
33
|
+
trigger: (...a) => mockCreateTrigger(...a),
|
|
26
34
|
isMutating: false,
|
|
27
35
|
})),
|
|
28
36
|
useProviderDelete: jest.fn(() => ({
|
|
29
|
-
trigger:
|
|
37
|
+
trigger: (...a) => mockDeleteTrigger(...a),
|
|
38
|
+
isMutating: false,
|
|
39
|
+
})),
|
|
40
|
+
useProviderEnable: jest.fn(() => ({
|
|
41
|
+
trigger: (...a) => mockEnableTrigger(...a),
|
|
42
|
+
isMutating: false,
|
|
43
|
+
})),
|
|
44
|
+
useProviderDisable: jest.fn(() => ({
|
|
45
|
+
trigger: jest.fn(),
|
|
30
46
|
isMutating: false,
|
|
31
47
|
})),
|
|
32
48
|
useProviderUpdate: jest.fn(() => ({
|
|
@@ -36,7 +52,89 @@ jest.mock("@truedat/ai/hooks/useProviders", () => {
|
|
|
36
52
|
};
|
|
37
53
|
});
|
|
38
54
|
|
|
55
|
+
const baseProviders = () => [
|
|
56
|
+
{
|
|
57
|
+
id: 1,
|
|
58
|
+
name: "rm1",
|
|
59
|
+
type: "openai",
|
|
60
|
+
enabled_at: null,
|
|
61
|
+
agents: null,
|
|
62
|
+
properties: {},
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
39
66
|
describe("<Providers />", () => {
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
jest.useRealTimers();
|
|
69
|
+
mockProviders = baseProviders();
|
|
70
|
+
mockIndices = [];
|
|
71
|
+
mockDeleteTrigger.mockReset();
|
|
72
|
+
mockCreateTrigger.mockReset();
|
|
73
|
+
mockEnableTrigger.mockReset().mockResolvedValue({});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("creating a provider", () => {
|
|
77
|
+
const create = async (enabledAt) => {
|
|
78
|
+
mockCreateTrigger.mockResolvedValue({
|
|
79
|
+
data: { data: { id: 7, enabled_at: enabledAt } },
|
|
80
|
+
});
|
|
81
|
+
const user = userEvent.setup({ delay: null });
|
|
82
|
+
const r = render(<Providers />, {});
|
|
83
|
+
await user.click(r.getByText("providers.action.new"));
|
|
84
|
+
await user.type(r.getByPlaceholderText("providers.form.name"), "p");
|
|
85
|
+
return { user, r };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
it("enables the new provider when the toggle is on (follow-up)", async () => {
|
|
89
|
+
const { user, r } = await create(null);
|
|
90
|
+
await user.click(r.getByText("providers.form.enabled"));
|
|
91
|
+
await user.type(
|
|
92
|
+
r.getByPlaceholderText("providerProperties.form.model"),
|
|
93
|
+
"m",
|
|
94
|
+
);
|
|
95
|
+
await user.type(
|
|
96
|
+
r.getByPlaceholderText("providerProperties.form.organizationKey"),
|
|
97
|
+
"o",
|
|
98
|
+
);
|
|
99
|
+
await user.click(r.getByRole("button", { name: "actions.save" }));
|
|
100
|
+
await waitFor(() => expect(mockCreateTrigger).toHaveBeenCalled());
|
|
101
|
+
await waitFor(() =>
|
|
102
|
+
expect(mockEnableTrigger).toHaveBeenCalledWith({ id: 7 }),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("does not enable it when the toggle is off", async () => {
|
|
107
|
+
const { user, r } = await create(null);
|
|
108
|
+
await user.type(
|
|
109
|
+
r.getByPlaceholderText("providerProperties.form.model"),
|
|
110
|
+
"m",
|
|
111
|
+
);
|
|
112
|
+
await user.type(
|
|
113
|
+
r.getByPlaceholderText("providerProperties.form.organizationKey"),
|
|
114
|
+
"o",
|
|
115
|
+
);
|
|
116
|
+
await user.click(r.getByRole("button", { name: "actions.save" }));
|
|
117
|
+
await waitFor(() => expect(mockCreateTrigger).toHaveBeenCalled());
|
|
118
|
+
expect(mockEnableTrigger).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("does not enable twice when td-ai already enabled it", async () => {
|
|
122
|
+
mockProviders = [];
|
|
123
|
+
const { user, r } = await create("2026-10-29T10:00:00Z");
|
|
124
|
+
await user.type(
|
|
125
|
+
r.getByPlaceholderText("providerProperties.form.model"),
|
|
126
|
+
"m",
|
|
127
|
+
);
|
|
128
|
+
await user.type(
|
|
129
|
+
r.getByPlaceholderText("providerProperties.form.organizationKey"),
|
|
130
|
+
"o",
|
|
131
|
+
);
|
|
132
|
+
await user.click(r.getByRole("button", { name: "actions.save" }));
|
|
133
|
+
await waitFor(() => expect(mockCreateTrigger).toHaveBeenCalled());
|
|
134
|
+
expect(mockEnableTrigger).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
40
138
|
const renderOpts = {
|
|
41
139
|
messages: {
|
|
42
140
|
en: {
|
|
@@ -72,4 +170,145 @@ describe("<Providers />", () => {
|
|
|
72
170
|
await waitFor(() => expect(queryByText(/lazy/i)).not.toBeInTheDocument());
|
|
73
171
|
expect(container).toMatchSnapshot();
|
|
74
172
|
});
|
|
173
|
+
|
|
174
|
+
it("shows the banner when providers exist and none is enabled (12-3)", () => {
|
|
175
|
+
const { getByText } = render(<Providers />, {});
|
|
176
|
+
expect(getByText("providers.enabled.banner")).toBeVisible();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("hides the banner and badges only the enabled provider (12-1, 12-3)", () => {
|
|
180
|
+
mockProviders = [
|
|
181
|
+
{
|
|
182
|
+
...baseProviders()[0],
|
|
183
|
+
enabled_at: "2026-10-29T10:00:00Z",
|
|
184
|
+
agents: "fast",
|
|
185
|
+
},
|
|
186
|
+
{ ...baseProviders()[0], id: 2, name: "rm2" },
|
|
187
|
+
];
|
|
188
|
+
const { queryByText, getAllByText } = render(<Providers />, {});
|
|
189
|
+
expect(queryByText("providers.enabled.banner")).not.toBeInTheDocument();
|
|
190
|
+
expect(getAllByText("providers.enabled.badge")).toHaveLength(1);
|
|
191
|
+
expect(queryByText("providers.default.badge")).not.toBeInTheDocument();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("does not show the banner without providers (12-3)", () => {
|
|
195
|
+
mockProviders = [];
|
|
196
|
+
const { queryByText } = render(<Providers />, {});
|
|
197
|
+
expect(queryByText("providers.enabled.banner")).not.toBeInTheDocument();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("shows the count of enabled embedding models per provider (12-2)", () => {
|
|
201
|
+
mockProviders = [
|
|
202
|
+
baseProviders()[0],
|
|
203
|
+
{ ...baseProviders()[0], id: 2, name: "rm2" },
|
|
204
|
+
{ ...baseProviders()[0], id: 3, name: "rm3", type: "anthropic" },
|
|
205
|
+
];
|
|
206
|
+
mockIndices = [
|
|
207
|
+
{ id: 1, provider: 1, enabled_at: "2026-10-29T10:00:00Z" },
|
|
208
|
+
{ id: 2, provider: 1, enabled_at: "2026-10-29T10:00:00Z" },
|
|
209
|
+
{ id: 3, provider: 1, enabled_at: null },
|
|
210
|
+
{ id: 4, provider: 2, enabled_at: null },
|
|
211
|
+
{ id: 5, provider: 3, enabled_at: "2026-10-29T10:00:00Z" },
|
|
212
|
+
];
|
|
213
|
+
const { getAllByText, getByText } = render(<Providers />, {});
|
|
214
|
+
const labels = getAllByText("providers.embeddingModels.enabledCount");
|
|
215
|
+
expect(labels).toHaveLength(1);
|
|
216
|
+
expect(labels[0].closest(".item")).toHaveTextContent("rm1");
|
|
217
|
+
expect(labels[0]).toHaveClass("label");
|
|
218
|
+
expect(getByText("rm2").closest(".item")).not.toHaveTextContent(
|
|
219
|
+
"enabledCount",
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("keeps the provider and shows the server error when deleting an enabled one (12-8)", async () => {
|
|
224
|
+
mockProviders = [
|
|
225
|
+
{ ...baseProviders()[0], enabled_at: "2026-10-29T10:00:00Z" },
|
|
226
|
+
];
|
|
227
|
+
mockDeleteTrigger.mockRejectedValue({
|
|
228
|
+
response: {
|
|
229
|
+
data: { errors: { enabled_at: ["disable it first"] } },
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
const user = userEvent.setup({ delay: null });
|
|
233
|
+
const { getByText, getByRole, findByText } = render(<Providers />, {});
|
|
234
|
+
await user.click(getByText("rm1"));
|
|
235
|
+
await user.click(getByRole("button", { name: "actions.delete" }));
|
|
236
|
+
await user.click(
|
|
237
|
+
getByRole("button", { name: /modal-affirmative-action/i }),
|
|
238
|
+
);
|
|
239
|
+
expect(await findByText("disable it first")).toBeVisible();
|
|
240
|
+
expect(getByRole("button", { name: "actions.save" })).toBeInTheDocument();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("shows the models error and keeps the provider when delete fails (06-8)", async () => {
|
|
244
|
+
mockProviders = [{ ...baseProviders()[0], enabled_at: null }];
|
|
245
|
+
mockDeleteTrigger.mockRejectedValue({
|
|
246
|
+
response: {
|
|
247
|
+
data: { errors: { indices: ["provider has embedding models"] } },
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
const user = userEvent.setup({ delay: null });
|
|
251
|
+
const { getByText, getByRole, findByText } = render(<Providers />, {});
|
|
252
|
+
await user.click(getByText("rm1"));
|
|
253
|
+
await user.click(getByRole("button", { name: "actions.delete" }));
|
|
254
|
+
await user.click(
|
|
255
|
+
getByRole("button", { name: /modal-affirmative-action/i }),
|
|
256
|
+
);
|
|
257
|
+
expect(
|
|
258
|
+
await findByText("providers.embeddingModels.error.providerHasModels"),
|
|
259
|
+
).toBeVisible();
|
|
260
|
+
expect(getByRole("button", { name: "actions.save" })).toBeInTheDocument();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("drops the delete error when another provider is selected", async () => {
|
|
264
|
+
mockProviders = [
|
|
265
|
+
{ ...baseProviders()[0], enabled_at: null },
|
|
266
|
+
{ ...baseProviders()[0], id: 2, name: "rm2", enabled_at: null },
|
|
267
|
+
];
|
|
268
|
+
mockDeleteTrigger.mockRejectedValue({
|
|
269
|
+
response: {
|
|
270
|
+
data: { errors: { indices: ["provider has embedding models"] } },
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
const user = userEvent.setup({ delay: null });
|
|
274
|
+
const { getByText, getByRole, findByText, queryByText } = render(
|
|
275
|
+
<Providers />,
|
|
276
|
+
{},
|
|
277
|
+
);
|
|
278
|
+
await user.click(getByText("rm1"));
|
|
279
|
+
await user.click(getByRole("button", { name: "actions.delete" }));
|
|
280
|
+
await user.click(
|
|
281
|
+
getByRole("button", { name: /modal-affirmative-action/i }),
|
|
282
|
+
);
|
|
283
|
+
await findByText("providers.embeddingModels.error.providerHasModels");
|
|
284
|
+
await user.click(getByText("rm2"));
|
|
285
|
+
expect(
|
|
286
|
+
queryByText("providers.embeddingModels.error.providerHasModels"),
|
|
287
|
+
).not.toBeInTheDocument();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("lets the delete error be dismissed", async () => {
|
|
291
|
+
mockProviders = [{ ...baseProviders()[0], enabled_at: null }];
|
|
292
|
+
mockDeleteTrigger.mockRejectedValue({
|
|
293
|
+
response: {
|
|
294
|
+
data: { errors: { indices: ["provider has embedding models"] } },
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
const user = userEvent.setup({ delay: null });
|
|
298
|
+
const { getByText, getByRole, findByText, queryByText, container } = render(
|
|
299
|
+
<Providers />,
|
|
300
|
+
{},
|
|
301
|
+
);
|
|
302
|
+
await user.click(getByText("rm1"));
|
|
303
|
+
await user.click(getByRole("button", { name: "actions.delete" }));
|
|
304
|
+
await user.click(
|
|
305
|
+
getByRole("button", { name: /modal-affirmative-action/i }),
|
|
306
|
+
);
|
|
307
|
+
await findByText("providers.embeddingModels.error.providerHasModels");
|
|
308
|
+
await user.click(container.querySelector(".message .close.icon"));
|
|
309
|
+
expect(
|
|
310
|
+
queryByText("providers.embeddingModels.error.providerHasModels"),
|
|
311
|
+
).not.toBeInTheDocument();
|
|
312
|
+
expect(getByRole("button", { name: "actions.save" })).toBeInTheDocument();
|
|
313
|
+
});
|
|
75
314
|
});
|