From 8d2e0363d7db50de18af606e219e13b62b9e7bf6 Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Tue, 27 Oct 2026 08:15:29 +0900 Subject: [PATCH 1/3] test(providers): drain ASL fixtures and use server budgets --- .../providers/aistupidlevel-provider.test.ts | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/tests/providers/aistupidlevel-provider.test.ts b/tests/providers/aistupidlevel-provider.test.ts index efcbe30ecfe..ebea1dc96c1 100644 --- a/tests/providers/aistupidlevel-provider.test.ts +++ b/tests/providers/aistupidlevel-provider.test.ts @@ -19,7 +19,8 @@ import { isModelVisionSidecarConsumer, isVisionSidecarConsumer } from "../../src import { requiresVisionPreprocessing } from "../../src/vision/plan"; import { withStubbedProviderFetch } from "../helpers/catalog-provider-fetch"; import { installIsolatedCodexHome, type IsolatedCodexHome } from "../helpers/isolated-codex-home"; -import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { removeTreeAfterBackgroundWork } from "../helpers/remove-tree"; +import { SERVER_BUDGET_MS } from "../helpers/test-budget"; const originalFetch = globalThis.fetch; const baseUrl = "https://aistupidlevel.info/v1"; @@ -125,7 +126,7 @@ describe("AI Stupid Level text-only contract for pinned models", () => { const IMAGE = "data:image/png;base64,aGVsbG8taW1hZ2UtYnl0ZXM="; let previousHome: string | undefined; let home = ""; - let codexHome: IsolatedCodexHome; + let codexHome: IsolatedCodexHome | null = null; let mainCalls: Array>; beforeEach(() => { @@ -154,13 +155,25 @@ describe("AI Stupid Level text-only contract for pinned models", () => { }) as typeof fetch; }); - afterEach(() => { + afterEach(async () => { clearModelCache(); resetVisionDescriptionCache(); - if (previousHome === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = previousHome; - codexHome.restore(); - removeTreeWithRetry(home); + try { + // A case can leave tracked teardown work (config-dir ACL hardening, an icacls + // reap) holding a file in either fixture tree. Settle and reap before removing, + // and remove both through the same drain-aware path: codexHome.restore() would + // delete its tree synchronously ahead of the drain and swallow a sharing + // violation. The env restore is unconditional so a retained, named tree never + // leaks into the next case's config resolution. + if (home) await removeTreeAfterBackgroundWork(home); + if (codexHome) await removeTreeAfterBackgroundWork(codexHome.path); + } finally { + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + codexHome?.restoreEnvironment(); + home = ""; + codexHome = null; + } }); test("a pinned live id needs vision preprocessing at the preset destination", () => { @@ -225,5 +238,5 @@ describe("AI Stupid Level text-only contract for pinned models", () => { } finally { await server.stop(true); } - }); + }, SERVER_BUDGET_MS); // the file's only cases binding a real server; Windows startup measured ~4.8s of Bun's 5s default. }); From 13e2c00da93baecacc93c412330c98dc5df664a1 Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Tue, 27 Oct 2026 01:14:06 +0000 Subject: [PATCH 2/3] test: preserve both fixture cleanup attempts and errors Always attempt both temporary tree removals, restore the fixture environment, and preserve the original single error or both causes when cleanup fails. Cover first, second and combined cleanup failures without changing product behavior or the existing HTTP test budget. --- .../providers/aistupidlevel-provider.test.ts | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/tests/providers/aistupidlevel-provider.test.ts b/tests/providers/aistupidlevel-provider.test.ts index ebea1dc96c1..1f54f2db99c 100644 --- a/tests/providers/aistupidlevel-provider.test.ts +++ b/tests/providers/aistupidlevel-provider.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdtempSync } from "node:fs"; +import { existsSync, mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { saveConfig } from "../../src/config"; @@ -125,12 +125,14 @@ describe("AI Stupid Level text-only contract for pinned models", () => { const PINNED = "claude-sonnet-4-6"; const IMAGE = "data:image/png;base64,aGVsbG8taW1hZ2UtYnl0ZXM="; let previousHome: string | undefined; + let previousCodexHome: string | undefined; let home = ""; let codexHome: IsolatedCodexHome | null = null; let mainCalls: Array>; beforeEach(() => { previousHome = process.env.OPENCODEX_HOME; + previousCodexHome = process.env.CODEX_HOME; codexHome = installIsolatedCodexHome("ocx-asl-text-only-codex-"); home = mkdtempSync(join(tmpdir(), "ocx-asl-text-only-")); process.env.OPENCODEX_HOME = home; @@ -155,9 +157,8 @@ describe("AI Stupid Level text-only contract for pinned models", () => { }) as typeof fetch; }); - afterEach(async () => { - clearModelCache(); - resetVisionDescriptionCache(); + async function cleanupFixture(removeTree = removeTreeAfterBackgroundWork): Promise { + const failures: unknown[] = []; try { // A case can leave tracked teardown work (config-dir ACL hardening, an icacls // reap) holding a file in either fixture tree. Settle and reap before removing, @@ -165,17 +166,64 @@ describe("AI Stupid Level text-only contract for pinned models", () => { // delete its tree synchronously ahead of the drain and swallow a sharing // violation. The env restore is unconditional so a retained, named tree never // leaks into the next case's config resolution. - if (home) await removeTreeAfterBackgroundWork(home); - if (codexHome) await removeTreeAfterBackgroundWork(codexHome.path); + for (const root of [home, codexHome?.path]) { + if (!root) continue; + try { await removeTree(root); } + catch (error) { failures.push(error); } + } + // A sharing violation in one tree must not strand the other. Preserve a + // single error's identity, and report both causes when both removals fail. + if (failures.length === 1) throw failures[0]; + if (failures.length > 1) throw new AggregateError(failures, "AI Stupid Level fixture cleanup failed"); } finally { if (previousHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousHome; codexHome?.restoreEnvironment(); + } + } + + afterEach(async () => { + clearModelCache(); + resetVisionDescriptionCache(); + try { + await cleanupFixture(); + } finally { home = ""; codexHome = null; } }); + test.each(["home", "codex", "both"] as const)( + "attempts both removals and restores the environment when %s cleanup fails", + async failingRoot => { + const roots = [home, codexHome!.path]; + const attempted: string[] = []; + const homeError = new Error("fixture home removal failed"); + const codexError = new Error("fixture Codex home removal failed"); + const expectedError = failingRoot === "codex" ? codexError : homeError; + const cleanupError = await cleanupFixture(async root => { + attempted.push(root); + if (root === roots[0] && failingRoot !== "codex") throw homeError; + if (root === roots[1] && failingRoot !== "home") throw codexError; + await removeTreeAfterBackgroundWork(root); + }).catch(error => error); + if (failingRoot === "both") { + expect(cleanupError).toBeInstanceOf(AggregateError); + expect(cleanupError.errors).toHaveLength(2); + expect(cleanupError.errors[0]).toBe(homeError); + expect(cleanupError.errors[1]).toBe(codexError); + } else { + expect(cleanupError).toBe(expectedError); + } + expect(attempted).toEqual(roots); + expect(process.env.OPENCODEX_HOME).toBe(previousHome); + expect(process.env.CODEX_HOME).toBe(previousCodexHome); + expect(existsSync(roots[0]!)).toBe(failingRoot !== "codex"); + expect(existsSync(roots[1]!)).toBe(failingRoot !== "home"); + // afterEach retries any deliberately retained tree through the real remover. + }, + ); + test("a pinned live id needs vision preprocessing at the preset destination", () => { const saved = config(); // Registry-only: the saved row does not carry the declaration; routing backfills it. From 758a760fe97e72322e923d12832f02af7d2f5c89 Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Tue, 27 Oct 2026 02:18:51 +0000 Subject: [PATCH 3/3] test(providers): budget both ASL fixture cleanup sequences --- tests/providers/aistupidlevel-provider.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/providers/aistupidlevel-provider.test.ts b/tests/providers/aistupidlevel-provider.test.ts index 1f54f2db99c..6c9a9471ad3 100644 --- a/tests/providers/aistupidlevel-provider.test.ts +++ b/tests/providers/aistupidlevel-provider.test.ts @@ -182,6 +182,7 @@ describe("AI Stupid Level text-only contract for pinned models", () => { } } + // Cover both drain/retry sequences with reap headroom; a stuck ACL child must still time out. afterEach(async () => { clearModelCache(); resetVisionDescriptionCache(); @@ -191,7 +192,7 @@ describe("AI Stupid Level text-only contract for pinned models", () => { home = ""; codexHome = null; } - }); + }, 3 * SERVER_BUDGET_MS); test.each(["home", "codex", "both"] as const)( "attempts both removals and restores the environment when %s cleanup fails",