fix(settings): name the real response-timeout defaults

The placeholder said "Default (~120)" while most surfaces fall back to
60 s and the briefing to 180 s. The placeholder and the help line now
interpolate both numbers from the constants the server applies
(PROVIDER_DEFAULT_TIMEOUT_MS, AI_BUDGETS.comprehensive.timeoutMs), in
all seven locales, and a test fails if a locale writes a number of its
own.

Refs #1090
This commit is contained in:
Marc-André Bombeck committed 2026-10-02 23:17:37 +02:00
1 parent 6207101006
commit ae0bdd0390
9 files changed
+101 -16

No files matched your search

@@ -0,0 +1,70 @@
/**
* The response-timeout copy names the defaults the server applies.
*
* The placeholder said "Default (~120)" while most surfaces fell back to 60 s
* and the briefing to 180 s. The numbers now come from the server constants
* through interpolation; this pins that every locale interpolates them instead
* of writing a number of its own, and that the rendered text carries them.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { AI_BUDGETS } from "@/lib/ai/ai-budgets";
import { PROVIDER_DEFAULT_TIMEOUT_MS } from "@/lib/ai/effective-timeout";
import { RESPONSE_TIMEOUT_COPY_PARAMS } from "../response-timeout-card";
const LOCALES = ["de", "en", "es", "fr", "it", "ko", "pl"] as const;
function aiCopy(locale: string): Record<string, string> {
const bundle = JSON.parse(
readFileSync(join(process.cwd(), "messages", `${locale}.json`), "utf8"),
) as { settings: { ai: Record<string, string> } };
return bundle.settings.ai;
}
function render(template: string): string {
return template.replace(/\{(\w+)\}/g, (_, k: string) =>
String(
RESPONSE_TIMEOUT_COPY_PARAMS[
k as keyof typeof RESPONSE_TIMEOUT_COPY_PARAMS
],
),
);
}
describe("response-timeout copy", () => {
it("reads its numbers from the server constants", () => {
expect(RESPONSE_TIMEOUT_COPY_PARAMS.seconds).toBe(
PROVIDER_DEFAULT_TIMEOUT_MS / 1000,
);
expect(RESPONSE_TIMEOUT_COPY_PARAMS.briefingSeconds).toBe(
AI_BUDGETS.comprehensive.timeoutMs! / 1000,
);
expect(RESPONSE_TIMEOUT_COPY_PARAMS.briefingSeconds).toBeGreaterThan(0);
});
it.each(LOCALES)(
"%s interpolates the defaults and writes none itself",
(locale) => {
const copy = aiCopy(locale);
const placeholder = copy.responseTimeoutPlaceholder;
const body = copy.responseTimeoutBody;
expect(placeholder).toContain("{seconds}");
expect(body).toContain("{seconds}");
expect(body).toContain("{briefingSeconds}");
// No literal number outside the placeholders: a hardcoded one would drift.
expect(placeholder.replace(/\{\w+\}/g, "")).not.toMatch(/\d/);
expect(body.replace(/\{\w+\}/g, "")).not.toMatch(/\d/);
expect(render(placeholder)).toContain(
String(PROVIDER_DEFAULT_TIMEOUT_MS / 1000),
);
expect(render(body)).toContain(
String(AI_BUDGETS.comprehensive.timeoutMs! / 1000),
);
},
);
});
@@ -18,12 +18,24 @@ import { Loader2, Save } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AI_BUDGETS } from "@/lib/ai/ai-budgets";
import { PROVIDER_DEFAULT_TIMEOUT_MS } from "@/lib/ai/effective-timeout";
import { apiPatch } from "@/lib/api/api-fetch";
import { useTranslations } from "@/lib/i18n/context";
import { aiInputDependentKeys, invalidateKeys } from "@/lib/query-keys";
import type { UserAIProvider } from "./shared";
/**
* The defaults the copy names, read from the constants the server applies, so
* the placeholder cannot drift from the real fallback again (it said "~120"
* while most surfaces waited 60 s).
*/
export const RESPONSE_TIMEOUT_COPY_PARAMS = {
seconds: PROVIDER_DEFAULT_TIMEOUT_MS / 1000,
briefingSeconds: (AI_BUDGETS.comprehensive.timeoutMs ?? 0) / 1000,
} as const;
export function ResponseTimeoutCard({
userProvider,
}: {
@@ -98,7 +110,7 @@ export function ResponseTimeoutCard({
{t("settings.ai.responseTimeoutHeading")}
</p>
<p className="text-muted-foreground text-xs">
{t("settings.ai.responseTimeoutBody")}
{t("settings.ai.responseTimeoutBody", RESPONSE_TIMEOUT_COPY_PARAMS)}
</p>
</div>
<div>
@@ -113,7 +125,10 @@ export function ResponseTimeoutCard({
max={600}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={t("settings.ai.responseTimeoutPlaceholder")}
placeholder={t(
"settings.ai.responseTimeoutPlaceholder",
RESPONSE_TIMEOUT_COPY_PARAMS,
)}
className="mt-1 sm:max-w-xs"
/>
</div>