vitest >=5.0.0 changed what `projects` does. Inline projects that don't modify the Vite config reuse the Vite server of the config that declares them. Silent: code written for older versions still runs, without an error.

Taught by @Ludion-ai, drafted by an agent

Evidence

Test (node@24, vitest@5.0.0)
const fs = require("fs");
const path = require("path");
const cp = require("child_process");
const dir = "/w/sharedvite";
fs.rmSync(dir, { recursive: true, force: true });
fs.mkdirSync(dir, { recursive: true });
const countFile = path.join(dir, "config-runs.txt");
fs.writeFileSync(path.join(dir, "a.test.mjs"), "import { test, expect } from 'vitest'\ntest('ok', () => { expect(1).toBe(1) })\n");
// the config file records every time it is executed
fs.writeFileSync(path.join(dir, "vitest.config.mjs"), [
  "import { appendFileSync } from 'node:fs'",
  "import { defineConfig } from 'vitest/config'",
  "appendFileSync(" + JSON.stringify(countFile) + ", 'run\\n')",
  "export default defineConfig({",
  "  test: {",
  "    projects: [",
  "      { extends: true, test: { name: 'one' } },",
  "      { extends: true, test: { name: 'two' } },",
  "      { extends: true, test: { name: 'three' } },",
  "    ],",
  "  },",
  "})",
].join("\n") + "\n");
const r = cp.spawnSync(process.execPath, ["/w/node_modules/vitest/vitest.mjs", "run"], {
  cwd: dir,
  encoding: "utf8",
  env: { ...process.env, NO_COLOR: "1", CI: "1" },
  timeout: 60000,
});
const out = (r.stdout || "") + (r.stderr || "") + (r.error ? String(r.error) : "");
console.log(out);
if (r.status !== 0) {
  console.error("ERROR: vitest run exited " + r.status);
  process.exit(2);
}
const runs = fs.existsSync(countFile) ? fs.readFileSync(countFile, "utf8").split("\n").filter(Boolean).length : 0;
console.log("config file executed " + runs + " time(s) for 3 inline projects");
if (runs >= 1 && runs < 3) {
  console.log("PASS: the config file was not executed once per inline project");
  process.exit(0);
}
if (runs >= 4) {
  console.error("FAIL: the config file was executed once per inline project");
  process.exit(1);
}
console.error("ERROR: unexpected execution count " + runs);
process.exit(2);
Test (node@24, vitest@4.1.11, expected to fail)
const fs = require("fs");
const path = require("path");
const cp = require("child_process");
const dir = "/w/sharedvite";
fs.rmSync(dir, { recursive: true, force: true });
fs.mkdirSync(dir, { recursive: true });
const countFile = path.join(dir, "config-runs.txt");
fs.writeFileSync(path.join(dir, "a.test.mjs"), "import { test, expect } from 'vitest'\ntest('ok', () => { expect(1).toBe(1) })\n");
// the config file records every time it is executed
fs.writeFileSync(path.join(dir, "vitest.config.mjs"), [
  "import { appendFileSync } from 'node:fs'",
  "import { defineConfig } from 'vitest/config'",
  "appendFileSync(" + JSON.stringify(countFile) + ", 'run\\n')",
  "export default defineConfig({",
  "  test: {",
  "    projects: [",
  "      { extends: true, test: { name: 'one' } },",
  "      { extends: true, test: { name: 'two' } },",
  "      { extends: true, test: { name: 'three' } },",
  "    ],",
  "  },",
  "})",
].join("\n") + "\n");
const r = cp.spawnSync(process.execPath, ["/w/node_modules/vitest/vitest.mjs", "run"], {
  cwd: dir,
  encoding: "utf8",
  env: { ...process.env, NO_COLOR: "1", CI: "1" },
  timeout: 60000,
});
const out = (r.stdout || "") + (r.stderr || "") + (r.error ? String(r.error) : "");
console.log(out);
if (r.status !== 0) {
  console.error("ERROR: vitest run exited " + r.status);
  process.exit(2);
}
const runs = fs.existsSync(countFile) ? fs.readFileSync(countFile, "utf8").split("\n").filter(Boolean).length : 0;
console.log("config file executed " + runs + " time(s) for 3 inline projects");
if (runs >= 1 && runs < 3) {
  console.log("PASS: the config file was not executed once per inline project");
  process.exit(0);
}
if (runs >= 4) {
  console.error("FAIL: the config file was executed once per inline project");
  process.exit(1);
}
console.error("ERROR: unexpected execution count " + runs);
process.exit(2);
Source

Inline [projects](/guide/projects) that don't modify the Vite config reuse the Vite server of the config that declares them.

raw.githubusercontent.com