Fixes the operator-reported MiniMax configuration bug: the API key appeared to be 'not saved' on every refresh.
The api backend is correct — direct repro against api-v2.truckwash.io (2026-08-10 07:58) showed 200 on POST and `isSet: true` on subsequent GET for both `{variable, value}` and raw-key payload shapes. The bugs were all on the frontend.
## What was actually broken
1. **Read response shape mismatch (root cause).** `ConfigurationXLVask.refreshMiniMaxStatus` parsed the GET response as an object, but the endpoint returns `[{module, variable, type, value, isSecret, isSet}]`. `typeof array === 'string'` is false → `minimaxApiKeyIsSet` was reset to `false` after every reload.
2. **Inline edit-save flow never notified the parent.** `ConfigurationSecretKey` had no event out, so the inline edit-and-save on the api_key field always left `isSet=false` (and the warning visible) until the user fully reloaded the page.
3. **MiniMax 'Enable' toggle was bound to a function reference.** `:value="SessionUser.superUser.modules.minimax.config.enabled.get ? true : false"` evaluates as `function ? true : false` = `true` (every function is truthy), so the switch always rendered as on.
4. **Hardcoded English warning text** in `ConfigurationSecretKey.vue` — i18n-v2 violation.
5. **Missing key registration in `xlvask/Config.vue.keys`.** PR #269 added a switch for `minimax_integration_enabled` on `xlvask.config.keys`, but never registered the key — accessing `.set` on `undefined.set` throws `TypeError` and aborts the Vue render mid-tree. Production build #c353bfa only renders 3 of 4 categories because of this.
## Changes
- `ConfigurationSecretKey.vue` — emits `saved` after a successful `onSave`; stays in edit mode + surfaces error on failure. Warning title/body come from `useI18n` (`configuration.secret_key_not_set` + `common.warning`) with optional prop overrides.
- `ConfigurationXLVask.vue` — `extractConfigEntry` helper unwraps the array response and trusts the explicit `isSet` flag. The MiniMax enable toggle reads `minimaxEnabled` (real boolean) and re-fetches via `onMiniMaxEnabledSwitch` (optimistic rollback on failure). After re-authenticate/remove/inline-save the parent re-fetches status so the UI matches persistence.
- `xlvask/Config.vue.keys` — registers the missing `minimax_integration_enabled` key.
- New i18n key `configuration.secret_key_not_set` + global shared alias; added to da/de/en/no/sv.
- New `tests/unit/configuration-secret-key.spec.js` (4 tests).
## Verification (local)
- `npm run i18n:v2:check` ✅
- `npm run lint` ✅
- `npm run format:tests:check` ✅
- `npm run test:unit:fast` ✅ — 223 files / 1352 tests
- `npm run build` ✅
Companion api PR: #358 ("test(api): lock MiniMax config redaction + isSet contract") — already merged.
92 lines
3.9 KiB
JavaScript
92 lines
3.9 KiB
JavaScript
// @vitest-environment jsdom
|
|
|
|
import { flushPromises, mount } from "@vue/test-utils";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("vue-i18n", () => ({
|
|
useI18n: () => ({
|
|
t: (key, fallback) => (typeof fallback === "string" ? fallback : key),
|
|
}),
|
|
}));
|
|
|
|
const parseError = vi.fn();
|
|
const getError = vi.fn();
|
|
vi.mock("@/components/request/HandleGlobalError.vue", () => ({
|
|
parseError: (...args) => parseError(...args),
|
|
getError: (...args) => getError(...args),
|
|
}));
|
|
|
|
import ConfigurationSecretKey from "@/components/displays/superuser/configuration/ConfigurationSecretKey.vue";
|
|
|
|
const findSaveButton = (wrapper) => wrapper.findAll("a.button").find((b) => b.find("i.fa-save").exists());
|
|
const findEditButton = (wrapper) => wrapper.findAll("a.button").find((b) => b.find("i.fa-edit").exists());
|
|
|
|
const mountKey = (props = {}) =>
|
|
mount(ConfigurationSecretKey, {
|
|
props: {
|
|
title: "API key",
|
|
description: "desc",
|
|
...props,
|
|
},
|
|
});
|
|
|
|
describe("ConfigurationSecretKey", () => {
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("shows the warning when not set, hidden view when set", () => {
|
|
const unset = mountKey({ isSet: false });
|
|
expect(unset.find(".message.is-warning").exists()).toBe(true);
|
|
const setWrapper = mountKey({ isSet: true });
|
|
expect(setWrapper.find(".message.is-warning").exists()).toBe(false);
|
|
expect(setWrapper.find('input[value="Hidden"]').exists()).toBe(true);
|
|
});
|
|
|
|
it("emits 'saved' after a successful onSave and exits edit mode", async () => {
|
|
const onSave = vi.fn().mockResolvedValue({ ok: true });
|
|
const wrapper = mountKey({ onSave, isSet: false });
|
|
// The input is disabled until the user enters edit mode — click Edit first,
|
|
// then type, then Save. This matches the production UX for ConfigurationXLVask.
|
|
await findEditButton(wrapper).trigger("click");
|
|
await wrapper.find('input[type="text"]').setValue("super-secret");
|
|
await findSaveButton(wrapper).trigger("click");
|
|
await flushPromises();
|
|
expect(onSave).toHaveBeenCalledWith("super-secret");
|
|
expect(wrapper.emitted("saved")).toBeTruthy();
|
|
expect(wrapper.emitted("saved").length).toBe(1);
|
|
expect(wrapper.emitted("saved")[0][0]).toEqual(expect.objectContaining({ value: "super-secret" }));
|
|
// Edit mode exited — the Save button is gone, the Edit button is back.
|
|
expect(findSaveButton(wrapper)).toBeUndefined();
|
|
expect(findEditButton(wrapper).exists()).toBe(true);
|
|
// Whether the "Hidden" view shows after a save depends on the parent updating
|
|
// isSet; that's the caller's responsibility and is exercised in the
|
|
// ConfigurationXLVask end-to-end coverage.
|
|
});
|
|
|
|
it("stays in edit mode and surfaces the error when onSave rejects", async () => {
|
|
const failure = new Error("boom");
|
|
const onSave = vi.fn().mockRejectedValue(failure);
|
|
getError.mockReturnValue("boom");
|
|
const wrapper = mountKey({ onSave, isSet: false });
|
|
await findEditButton(wrapper).trigger("click");
|
|
await wrapper.find('input[type="text"]').setValue("super-secret");
|
|
await findSaveButton(wrapper).trigger("click");
|
|
await flushPromises();
|
|
expect(onSave).toHaveBeenCalledWith("super-secret");
|
|
expect(wrapper.emitted("saved")).toBeFalsy();
|
|
expect(parseError).toHaveBeenCalledWith(failure, expect.any(String));
|
|
// Edit mode preserved so the user can retry.
|
|
expect(findSaveButton(wrapper).exists()).toBe(true);
|
|
expect(wrapper.find(".help.is-danger").exists()).toBe(true);
|
|
});
|
|
|
|
it("renders localized warning message instead of hardcoded English", () => {
|
|
// useI18n mock returns the fallback for any unknown key, so the warning body
|
|
// should reflect whatever the caller passed via the i18n key fallback path.
|
|
const wrapper = mountKey({ isSet: false });
|
|
const body = wrapper.find(".message.is-warning .message-body").text();
|
|
expect(body).toContain("The secret key is not set");
|
|
});
|
|
});
|