test: fix integration-run failures in provider, model and session-index tests (#1439)

## Summary

Fixes the 23 failures seen with `RUN_INTEGRATION_TESTS=true`. All are
test-side; no adapter changes.

- **Provider tests (20 failures):** since #1340, `providers/set` lists
the gateway's models with `fetch` in the test process.
`vitest.config.ts` only loaded `setup-no-network.ts` outside integration
runs, so tests that point at `https://gateway.example.com` made real
requests and failed with `fetch failed`. The stub now loads in every
run. `gateway-models.ts` is the only in-process `fetch` caller, and no
integration test targets a real gateway.
- **"the real SDK exposes one plain Opus row":** the CLI catalogue now
also lists pinned older models (`claude-opus-5`, `claude-opus-4-8`, …),
and the alias row's display name is "Opus 5.5". The check now covers
only alias rows (`value` starting with `opus`) and still asserts there
is a single row with no 1M variant.
- **session-index `ENOTEMPTY` in teardown:** integration runs leave
`CLAUDE_CODE_EXECUTABLE` unset, so initialize ran the real `claude auth
status` in the background. It wrote into the temp `CLAUDE_CONFIG_DIR`
while `afterEach` removed it. The file now stubs the executable to
`/usr/bin/false`, as the scenario tests do.
- **session-index relocation test:** the wait ended on any removal from
the old path's subscription. That removal can name the other path's
session, so the test now waits for the relocated session's own id.

## Testing

- The six previously failing files pass with
`RUN_INTEGRATION_TESTS=true` (454 tests).
- `session-index.test.ts` passed 3× with integration on and 1× without
(173 tests). Those failures were intermittent, so a few clean runs don't
fully prove the fix.
- `npx vitest run` (3360 tests) and `npm run check` pass.
- The full integration suite was not rerun end to end.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ben Brandt
2026-10-26 14:15:14 +01:00
committed by GitHub
parent d52c7c64d8
commit c9374abfc7
3 changed files with 15 additions and 9 deletions

View File

@@ -238,19 +238,18 @@ it.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
const q = query({ prompt: input(), options: { persistSession: false, abortController } }); const q = query({ prompt: input(), options: { persistSession: false, abortController } });
try { try {
const { models } = await q.initializationResult(); const { models } = await q.initializationResult();
const opusModels = models.filter( // Only alias rows ("opus", "opus[1m]"). The catalogue also lists pinned
(model) => // older versions such as "claude-opus-4-8"; those are separate models.
model.value !== "default" && const opusModels = models.filter((model) => /^opus\b/i.test(model.value));
/\bopus\b/i.test(`${model.value} ${model.resolvedModel} ${model.displayName}`),
);
expect(opusModels).toHaveLength(1); expect(opusModels).toHaveLength(1);
// CLI 2.1.283 dropped the "[1m]" / "(1M context)" spelling from the Opus // CLI 2.1.283 dropped the "[1m]" / "(1M context)" spelling from the Opus
// row: Opus is natively 1M, so the row names the plain model. // row: Opus is natively 1M, so the row names the plain model.
expect(opusModels[0]).toMatchObject({ expect(opusModels[0]).toMatchObject({
value: "opus", value: "opus",
displayName: "Opus", displayName: expect.stringMatching(/^Opus\b/),
resolvedModel: expect.stringMatching(/^claude-opus-[\d-]+$/), resolvedModel: expect.stringMatching(/^claude-opus-[\d-]+$/),
}); });
expect(opusModels[0].displayName).not.toMatch(/\b1m\b/i);
expect(opusModels[0].description).not.toMatch(/\b1m\b/i); expect(opusModels[0].description).not.toMatch(/\b1m\b/i);
} finally { } finally {
clearTimeout(timeout); clearTimeout(timeout);

View File

@@ -69,10 +69,15 @@ beforeEach(async () => {
configDir = fsSync.realpathSync(await fs.mkdtemp(path.join(os.tmpdir(), "session-index-cfg-"))); configDir = fsSync.realpathSync(await fs.mkdtemp(path.join(os.tmpdir(), "session-index-cfg-")));
workspace = fsSync.realpathSync(await fs.mkdtemp(path.join(os.tmpdir(), "session-index-ws-"))); workspace = fsSync.realpathSync(await fs.mkdtemp(path.join(os.tmpdir(), "session-index-ws-")));
process.env.CLAUDE_CONFIG_DIR = configDir; process.env.CLAUDE_CONFIG_DIR = configDir;
// Integration runs leave CLAUDE_CODE_EXECUTABLE unset, so initialize would run
// the real `claude auth status` in the background. It writes into configDir
// while afterEach removes it, and the rm fails with ENOTEMPTY.
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "/usr/bin/false");
vi.clearAllMocks(); vi.clearAllMocks();
}); });
afterEach(async () => { afterEach(async () => {
vi.unstubAllEnvs();
if (previousConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR; if (previousConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
else process.env.CLAUDE_CONFIG_DIR = previousConfigDir; else process.env.CLAUDE_CONFIG_DIR = previousConfigDir;
await fs.rm(configDir, { recursive: true, force: true }); await fs.rm(configDir, { recursive: true, force: true });
@@ -3528,7 +3533,8 @@ describe("paths that share a project directory", () => {
.filter((s) => s.sessionId === mine.id); .filter((s) => s.sessionId === mine.id);
const removed = () => const removed = () =>
changesFor(notifications, fromDotted.subscriptionId).flatMap((c) => c.removed); changesFor(notifications, fromDotted.subscriptionId).flatMap((c) => c.removed);
expect(await waitFor(() => added().length > 0 && removed().length > 0)).toBe(true); // Wait for mine.id itself: the other path's session can be removed first.
expect(await waitFor(() => added().length > 0 && removed().includes(mine.id))).toBe(true);
expect(added().map((s) => [s.sessionId, s.cwd])).toEqual([[mine.id, dashed]]); expect(added().map((s) => [s.sessionId, s.cwd])).toEqual([[mine.id, dashed]]);
// It may also name the other path's session, never listed here. // It may also name the other path's session, never listed here.
expect(removed()).toContain(mine.id); expect(removed()).toContain(mine.id);

View File

@@ -14,8 +14,9 @@ export default defineConfig({
// tests off the machine's real CLI and Claude config, and stops a run // tests off the machine's real CLI and Claude config, and stops a run
// from outliving its test (it wrote into temp config dirs being deleted). // from outliving its test (it wrote into temp config dirs being deleted).
env: usesRealCli ? {} : { CLAUDE_CODE_EXECUTABLE: "/usr/bin/false" }, env: usesRealCli ? {} : { CLAUDE_CODE_EXECUTABLE: "/usr/bin/false" },
// Real-CLI runs may want a real gateway listing. // Applies to real-CLI runs too: the gateway listing runs in the test process,
setupFiles: usesRealCli ? [] : ["src/tests/setup-no-network.ts"], // and tests point `providers/set` at fake URLs like gateway.example.com.
setupFiles: ["src/tests/setup-no-network.ts"],
include: [ include: [
"src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}", "src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}",
// Release tooling lives in scripts/ rather than src/ so it stays outside tsc's // Release tooling lives in scripts/ rather than src/ so it stays outside tsc's