The search tool create and edit forms both built a payload carrying api_base, timeout and max_retries read off form values that neither zod schema declares, so all three were always undefined. Drop them. JSON.stringify omits undefined-valued keys, so the request body on the wire is unchanged. SearchToolLiteLLMParams and SearchToolInfo in the page's types.tsx were hand-rolled with a [key: string]: any index signature, which is why a param could go missing from a form with nothing complaining. SearchToolLiteLLMParams is now the generated OpenAPI component and neither type carries an index signature, so the payload builder can only set params the backend declares. Also remove a stray ", ]" text node that rendered as visible garbage next to the connection test dialog's Close button.
6.5 KiB
Never put LiteLLM tokens or API keys in localStorage. localStorage survives browser close. Prefer httpOnly cookies, or sessionStorage at most, understanding that any web storage is readable by injected scripts (XSS), and only httpOnly cookies are not
When you fix lint violations that are grandfathered in eslint-suppressions.json, run eslint . --prune-suppressions and commit the updated baseline so the gate ratchets down instead of leaving a stale suppression
src/lib/http/schema.d.ts is generated from the proxy's OpenAPI spec; never hand-edit it. After changing a backend route or response model that the dashboard consumes, run npm run gen:api and commit the result (CI Check UI API Types Sync enforces this)
Tests come in three tiers, named by the standard definitions. Foo.test.tsx is a unit test: one module, collaborators replaced by doubles, no multi-component tree, and it should run in milliseconds. Foo.integration.test.tsx renders a real component tree with real children and only stubs the network boundary; it costs seconds per case, so it earns its place by proving wiring that a unit test cannot reach. Browser-level tests live in tests/e2e/ui/ as Playwright specs against a live proxy
When a component holds logic worth asserting, extract the logic and unit-test it there rather than driving it through a render. CreateMCPServer is the worked example: its payload building lives in createServerPayload.ts with 46 unit tests that run in single-digit milliseconds, while CreateMCPServer.integration.test.tsx keeps only the cases that prove a form field reaches the right payload key. A test that renders a whole modal to assert the shape of one object belongs in the first category, not the second
Most of the suite predates this split and is not yet classified, so an unsuffixed *.test.tsx is not evidence that a file is really a unit test. Classify what you touch
Assert something the user could perceive, and assert it precisely enough that the test fails when the behaviour breaks. eslint-plugin-testing-library and eslint-plugin-jest-dom enforce the mechanical part of that. Two of the enabled rules exist because the failure they catch is silent rather than cosmetic: await-async-queries catches an unawaited findBy*, whose returned Promise is always truthy and makes the whole assertion vacuous, and no-wait-for-side-effects catches work inside a waitFor callback, which is retried on every poll. Prefer findBy* over waitFor wrapped around getBy*, and keep a waitFor callback to a single assertion
Do not trust eslint --fix for these two plugins. Fixing the suite in bulk produced seven distinct kinds of broken output. Four fail loudly: no-wait-for-side-effects and no-wait-for-multiple-assertions hoist a statement out of the waitFor callback while leaving the const it reads inside, prefer-enabled-disabled drops a closing paren when the subject carries a type assertion, prefer-presence-queries swaps in a query it never destructures, and prefer-in-document collapses getAllBy* to getBy* on a value still indexed as an array. Two fail quietly, which is worse: prefer-checked swaps the checked attribute for the .checked property, and a radio can set one without the other, and prefer-to-have-text-content wraps arbitrary strings in new RegExp() without escaping, so toContain("100K+ requests") becomes a pattern meaning "100 followed by one-or-more K". That last one compiles, lints clean, and keeps passing while no longer asserting what it says. Pass a plain string to toHaveTextContent, which is already a substring match. Run the fixer on a handful of files at a time and read the diff
jest-dom/prefer-to-have-value stays off because its fixer is wrong here, not merely noisy. It matches any attribute whose name contains "value", so it rewrites toHaveAttribute("aria-valuenow", n) into toHaveValue(n), and jest-dom's toHaveValue only supports form controls, so the assertion fails on the role="meter" elements the dashboard renders. Assert ARIA value attributes with toHaveAttribute
Reach for fireEvent.change rather than user.type when a test only needs a field to hold a value. user.type dispatches one event per character and re-renders the whole form each time, which is why a single form test could burn seven seconds. Keep user.type where the typing itself is the behaviour under test: an autocomplete that filters per keystroke, a debounce, a key handler, or any Base UI combobox, whose filter state is driven by real keyboard input and does not react to a raw change event
A test may reach for a component library's own CSS class only when that library exposes no role, label, title or ARIA state to query instead. Check first: the shadcn primitives forward roles and aria-label, and the shared form field associates its label with the control, so both are reachable accessibly. When nothing accessible identifies the element, prefer its data-slot attribute, which the primitives set deliberately and treat as stable. When a label does not resolve, suspect the control rather than the test, since a custom wrapper that destructures props without spreading them drops the id the field generates and leaves the rendered label pointing at nothing
Rules beyond the enabled set were measured against the whole suite and left off rather than recorded in a budget file, because a ceiling that permits a violation anywhere is worse than an honest gap. no-node-access and no-container are the ones worth revisiting first, since they catch the DOM archaeology the rules above only discourage. prefer-implicit-assert and prefer-explicit-assert contradict each other, so neither is enabled
Never run the full unit suite (npx vitest run with no path). It is 380 files and thousands of tests, it saturates the machine for many minutes, and CI runs it anyway. Run only the test files your change touches, plus any file whose failure your change could plausibly explain, by passing explicit paths
Type tests are *.test-d.ts files run by the types vitest project (npm run test:types). Keep them out of the src/app/(dashboard)/ route group. Vitest matches a tsc error back to the test file by path, the parentheses break that match, and ignoreSourceErrors: true then drops the error as if it came from a source file. The test still collects and still reports as passing, so a .test-d.ts under a parenthesized directory is green no matter what it asserts. Confirm any new one has teeth by breaking the type it guards and watching it fail