mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 10:13:47 +10:00
fix(ai): make provider test timeout configurable via AI_TEST_TIMEOUT_MS (#3384)
* fix(ai): make provider test timeout configurable via AI_TEST_TIMEOUT_MS - Problem: the 30s hardcoded timeout is too short for self-hosted deployments with cold-start models (e.g. Ollama). Makes it impossible to pass the provider test (issue #3374). - Fix: read AI_TEST_TIMEOUT_MS from the environment, defaulting to 30_000. Zero behaviour change when the env var is absent. - Verification: existing test asserts "30 seconds" in the timeout message; default is unchanged so the test continues to pass. (CI needs Node 22+ — not available on this host.) * fix(ai): add AI_TEST_TIMEOUT_MS to turbo globalEnv so it reaches the API process - Problem: Turborepo filters env vars not listed in globalEnv, so AI_TEST_TIMEOUT_MS would always be undefined at runtime under turbo dev/start, making the override dead code. - Fix: add AI_TEST_TIMEOUT_MS to the globalEnv array. - Verification: turbo.json validates as valid JSON. * fix(ai): validate AI_TEST_TIMEOUT_MS as a finite non-negative integer * docs(ai): add JSDoc to timeout parser and test helper * test(ai): restore AI_TEST_TIMEOUT_MS after timeout tests - Problem: loadWithTimeout() mutates process.env.AI_TEST_TIMEOUT_MS but nothing restores it, so the last value tested ("999999999999") leaked to every test that runs after this describe block in the same file. - Fix: save the pre-test value and restore it in an afterEach hook. - Verification: pnpm exec vitest run src/features/ai/service.test.ts in packages/api — 18/18 passed. * test(api): isolate AI timeout environment cases --------- Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
co-authored by
Amruth Pillai
parent
e0c2f6d88a
commit
861ba8bf60
@@ -9,6 +9,7 @@ const envMock = vi.hoisted(() => ({
|
||||
vi.mock("@reactive-resume/env/server", () => ({ env: envMock }));
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
@@ -169,6 +170,46 @@ describe("AI provider connection test", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("AI provider test connection timeout", () => {
|
||||
/**
|
||||
* Re-import the service module with `AI_TEST_TIMEOUT_MS` set to a specific value.
|
||||
*
|
||||
* @param value - The environment value to test, or `undefined` to unset it.
|
||||
* @returns The `testConnection` function from the freshly imported module.
|
||||
*/
|
||||
async function loadWithTimeout(value: string | undefined) {
|
||||
vi.stubEnv("AI_TEST_TIMEOUT_MS", value);
|
||||
vi.resetModules();
|
||||
const mod = await import("./service");
|
||||
return mod.testConnection;
|
||||
}
|
||||
|
||||
it("uses a valid custom timeout", async () => {
|
||||
const testConnectionWithEnv = await loadWithTimeout("5000");
|
||||
stubRejectedFetch(new DOMException("The operation was aborted due to timeout", "TimeoutError"));
|
||||
|
||||
await expect(testConnectionWithEnv(testInput())).resolves.toMatchObject({
|
||||
ok: false,
|
||||
message: expect.stringContaining("did not respond within 5 seconds"),
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["negative", "-1"],
|
||||
["fractional", "30.5"],
|
||||
["non-numeric", "not-a-number"],
|
||||
["out-of-range", "999999999999"],
|
||||
])("falls back to the default for %s values", async (_label, value) => {
|
||||
const testConnectionWithEnv = await loadWithTimeout(value);
|
||||
stubRejectedFetch(new DOMException("The operation was aborted due to timeout", "TimeoutError"));
|
||||
|
||||
await expect(testConnectionWithEnv(testInput())).resolves.toMatchObject({
|
||||
ok: false,
|
||||
message: expect.stringContaining("did not respond within 30 seconds"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("AI chat service", () => {
|
||||
it("tests OpenAI-compatible providers without requiring structured output", async () => {
|
||||
const openAiCompatible = stubOpenAICompatibleResponse();
|
||||
|
||||
@@ -90,8 +90,33 @@ type GetModelInput = {
|
||||
const MAX_AI_FILE_BYTES = 10 * 1024 * 1024; // 10MB
|
||||
const MAX_AI_FILE_BASE64_CHARS = Math.ceil((MAX_AI_FILE_BYTES * 4) / 3) + 4;
|
||||
const TEST_CONNECTION_MAX_OUTPUT_TOKENS = 128;
|
||||
|
||||
// AbortSignal.timeout stores the delay as a 32-bit signed integer.
|
||||
const MAX_ABORT_SIGNAL_TIMEOUT_MS = 2_147_483_647;
|
||||
|
||||
/**
|
||||
* Parse `AI_TEST_TIMEOUT_MS` into a safe, finite, non-negative integer.
|
||||
*
|
||||
* Rejects empty, non-numeric, negative, fractional, and out-of-range values
|
||||
* so that `AbortSignal.timeout` never receives an invalid delay.
|
||||
*
|
||||
* @param raw - The raw environment variable value, if set.
|
||||
* @param fallback - Milliseconds to use when `raw` is missing or invalid.
|
||||
* @returns The validated timeout in milliseconds.
|
||||
*/
|
||||
function parseTestConnectionTimeoutMs(raw: string | undefined, fallback: number): number {
|
||||
if (raw === undefined) return fallback;
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed === "") return fallback;
|
||||
if (!/^\d+$/.test(trimmed)) return fallback;
|
||||
const value = Number(trimmed);
|
||||
if (value < 0 || value > MAX_ABORT_SIGNAL_TIMEOUT_MS) return fallback;
|
||||
return value;
|
||||
}
|
||||
|
||||
// Long enough for a cold local model to load, short enough that the UI does not look frozen.
|
||||
const TEST_CONNECTION_TIMEOUT_MS = 30_000;
|
||||
// Self-hosted deployments with cold-start models (e.g. Ollama) can override via AI_TEST_TIMEOUT_MS.
|
||||
const TEST_CONNECTION_TIMEOUT_MS = parseTestConnectionTimeoutMs(process.env.AI_TEST_TIMEOUT_MS, 30_000);
|
||||
const DOCX_DOCUMENT_XML_PATH = "word/document.xml";
|
||||
const ZIP_LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
|
||||
const ZIP_CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
|
||||
|
||||
+2
-1
@@ -87,7 +87,8 @@
|
||||
"FLAG_DISABLE_API_RATE_LIMIT",
|
||||
"FLAG_SHOW_SPONSORS",
|
||||
"FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI",
|
||||
"FLAG_ALLOW_UNSAFE_AI_BASE_URL"
|
||||
"FLAG_ALLOW_UNSAFE_AI_BASE_URL",
|
||||
"AI_TEST_TIMEOUT_MS"
|
||||
],
|
||||
"tasks": {
|
||||
"transit": {
|
||||
|
||||
Reference in New Issue
Block a user