diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f0a39565a..0d5924c9c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -858,6 +858,7 @@ jobs: src/process/capture/FilesystemSnapshot.test.ts tests/process-global/filesystemSnapshotFallbackCancellation.test.ts tests/boundary/filesystem/typescriptCompiler.test.ts + tests/boundary/filesystem/releaseProposal.test.ts tests/boundary/filesystem/binaryTarget.test.ts src/contracts/sessionPathInputs.test.ts tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts diff --git a/.oxlintrc.json b/.oxlintrc.json index 5cc6ed8952..d41674db8a 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -58,7 +58,8 @@ { "considerDefaultExhaustiveForUnions": true } - ] + ], + "rea/no-shared-module-mock": "error" }, "overrides": [ { diff --git a/docs/testing.md b/docs/testing.md index 1a0014e7f7..5a6fd931a0 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -166,6 +166,13 @@ operation names, schemas or types; display-only selection remains allowed. `rea/no-fixture-root-cleanup` follows `const` fixture-root aliases into teardown deletion; mutable bindings and mixed root arrays remain outside its scope. +`rea/no-shared-module-mock` resolves Vitest import bindings before rejecting +module mocks in projects without per-file isolation. The test-project definitions +own isolation for Vitest and lint; the executable gate checks the resolved policy +and runs the same collector. Aliases and namespace imports count; comments, +shadowed locals, ordinary methods and `spyOn` do not. Negative CLI probes use an +external temporary project so concurrent repository checks cannot discover them. + Focused `.only` declarations, broken assertion promise chains, and unused disable directives are errors. `vitest/valid-expect` warns (`maxArgs: 2`) because valid stored and collected assertions can be awaited later. `import/no-duplicates` warns with diff --git a/scripts/ci/scopes.mjs b/scripts/ci/scopes.mjs index 367a317901..9d6f7c62ce 100644 --- a/scripts/ci/scopes.mjs +++ b/scripts/ci/scopes.mjs @@ -144,6 +144,9 @@ export const SCOPES = { "scripts/windows-capabilities.mjs", "scripts/prepack.mjs", "scripts/prepare.mjs", + "scripts/lib/release-proposal.mjs", + "patches/release-please+17.3.0.patch", + "tests/boundary/filesystem/releaseProposal.test.ts", "scripts/rea.mjs", "scripts/lib/cli-process-environment.mjs", "install.sh", diff --git a/scripts/lib/release-proposal.mjs b/scripts/lib/release-proposal.mjs index 1db7cf973c..1bbb709c61 100644 --- a/scripts/lib/release-proposal.mjs +++ b/scripts/lib/release-proposal.mjs @@ -1,7 +1,7 @@ import { execFile } from "node:child_process"; import { readFile } from "node:fs/promises"; import { createRequire } from "node:module"; -import { dirname, relative } from "node:path"; +import { dirname, relative, sep } from "node:path"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; import { requireCompleteReleaseHistory } from "./release-notes.mjs"; @@ -24,7 +24,13 @@ export async function applyReleasePleasePatch( "The release proposal patch requires release-please 17.3.0", ); } - const args = ["--directory", relative(project, directory), patch]; + // Git prefixes patch filenames with a slash-separated path, even on Windows. + // Split only native separators so POSIX backslashes remain filename bytes. + const args = [ + "--directory", + relative(project, directory).split(sep).join("/"), + patch, + ]; const check = async (reverse) => { try { await execute( diff --git a/scripts/lib/test-projects.d.mts b/scripts/lib/test-projects.d.mts index a9d6ae3563..debca42658 100644 --- a/scripts/lib/test-projects.d.mts +++ b/scripts/lib/test-projects.d.mts @@ -1,6 +1,7 @@ export interface TestProjectDefinition { readonly name: string; readonly group: "source" | "built"; + readonly isolate?: boolean; readonly include: readonly string[]; readonly exclude?: readonly string[]; } diff --git a/scripts/lib/test-projects.mjs b/scripts/lib/test-projects.mjs index 13026a798f..89273948d4 100644 --- a/scripts/lib/test-projects.mjs +++ b/scripts/lib/test-projects.mjs @@ -5,11 +5,13 @@ export const SOURCE_TEST_PROJECTS = [ { name: "domain", group: "source", + isolate: false, include: ["src/{contracts,domain}/**/*.test.ts"], }, { name: "services", group: "source", + isolate: false, include: ["src/application/**/*.test.ts"], }, { @@ -23,17 +25,20 @@ export const SOURCE_TEST_PROJECTS = [ { name: "composition", group: "source", + isolate: false, include: ["tests/composition/**/*.test.ts"], }, { name: "conformance", group: "source", + isolate: false, include: ["tests/conformance/**/*.test.ts"], exclude: ["tests/conformance/providers/hopper/realHopperSearch.test.ts"], }, { name: "evaluation", group: "source", + isolate: false, include: ["tests/evaluation/**/*.test.ts"], }, ]; @@ -55,6 +60,7 @@ export const BUILT_TEST_PROJECTS = [ { name: "cli-boundary", group: "built", + isolate: false, include: ["tests/boundary/cli/**/*.test.ts"], }, { @@ -65,6 +71,7 @@ export const BUILT_TEST_PROJECTS = [ { name: "mcp-boundary", group: "built", + isolate: false, include: ["tests/boundary/mcp/**/*.test.ts"], }, { diff --git a/scripts/lint/index.mjs b/scripts/lint/index.mjs index db0365acfd..66b7f47cd3 100644 --- a/scripts/lint/index.mjs +++ b/scripts/lint/index.mjs @@ -4,6 +4,7 @@ import cancellationCause from "./cancellation-cause.mjs"; import noEvidenceJsonPreadmission from "./rules/no-evidence-json-preadmission.mjs"; import noFixtureRootCleanup from "./rules/no-fixture-root-cleanup.mjs"; import noPositionalToolContract from "./rules/no-positional-tool-contract.mjs"; +import noSharedModuleMock from "./rules/no-shared-module-mock.mjs"; export default { meta: { name: "rea" }, @@ -13,5 +14,6 @@ export default { "no-evidence-json-preadmission": noEvidenceJsonPreadmission, "no-fixture-root-cleanup": noFixtureRootCleanup, "no-positional-tool-contract": noPositionalToolContract, + "no-shared-module-mock": noSharedModuleMock, }, }; diff --git a/scripts/lint/rules/no-shared-module-mock.d.mts b/scripts/lint/rules/no-shared-module-mock.d.mts new file mode 100644 index 0000000000..86d61729e2 --- /dev/null +++ b/scripts/lint/rules/no-shared-module-mock.d.mts @@ -0,0 +1,4 @@ +import type { RuleTester } from "oxlint/plugins-dev"; + +declare const rule: Parameters[1]; +export default rule; diff --git a/scripts/lint/rules/no-shared-module-mock.mjs b/scripts/lint/rules/no-shared-module-mock.mjs new file mode 100644 index 0000000000..c3245f41a2 --- /dev/null +++ b/scripts/lint/rules/no-shared-module-mock.mjs @@ -0,0 +1,116 @@ +import { relative, isAbsolute } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { findBinding } from "../bindings.mjs"; +import { projectForTest } from "../../lib/test-projects.mjs"; + +const repository = fileURLToPath(new URL("../../../", import.meta.url)); +const mockMethods = new Set(["mock", "doMock"]); + +const unwrap = (node) => + node?.type === "ChainExpression" ? node.expression : node; + +const propertyName = (member) => { + if (!member?.computed && member?.property?.type === "Identifier") + return member.property.name; + if (member?.computed && member.property?.type === "Literal") + return member.property.value; + return undefined; +}; + +const importedFromVitest = (sourceCode, identifier) => { + const binding = findBinding(sourceCode, identifier); + const definition = binding?.defs[0]; + if (definition?.type !== "ImportBinding") return undefined; + const declaration = definition.parent ?? definition.node.parent; + if (declaration?.source?.value !== "vitest") return undefined; + return definition.node; +}; + +const isViImport = (sourceCode, identifier) => { + const specifier = importedFromVitest(sourceCode, identifier); + return ( + specifier?.type === "ImportSpecifier" && + (specifier.imported.name ?? specifier.imported.value) === "vi" + ); +}; + +const isVitestNamespace = (sourceCode, identifier) => + importedFromVitest(sourceCode, identifier)?.type === + "ImportNamespaceSpecifier"; + +const isVitestViMember = (sourceCode, node) => { + const member = unwrap(node); + if ( + member?.type !== "MemberExpression" || + propertyName(member) !== "vi" || + member.object?.type !== "Identifier" + ) + return false; + return isVitestNamespace(sourceCode, member.object); +}; + +const isModuleMockCall = (sourceCode, callee) => { + const method = unwrap(callee); + if ( + method?.type !== "MemberExpression" || + !mockMethods.has(propertyName(method)) + ) + return false; + const receiver = unwrap(method.object); + return ( + (receiver?.type === "Identifier" && isViImport(sourceCode, receiver)) || + isVitestViMember(sourceCode, receiver) + ); +}; + +const isolationForFile = (filename, cwd) => { + if (typeof filename !== "string" || !filename.endsWith(".test.ts")) + return undefined; + const testPath = relative(cwd, filename).split("\\").join("/"); + if ( + testPath === "" || + isAbsolute(testPath) || + testPath === ".." || + testPath.startsWith("../") + ) + return undefined; + try { + return projectForTest(testPath).isolate !== false; + } catch (error) { + if ( + error instanceof Error && + error.message.startsWith("No test project owns ") + ) + return undefined; + throw error; + } +}; + +export default { + meta: { + type: "problem", + docs: { + description: + "Keep Vitest module mocks in projects with per-file module isolation.", + }, + schema: [], + messages: { + shared: + "Vitest module mocks need an isolated test project; move the test to an isolated project or replace the module mock with a scoped spy or injected dependency.", + }, + }, + create(context) { + const filename = context.physicalFilename ?? context.filename; + const sourceCode = context.sourceCode; + const cwd = + context.cwd ?? context.getCwd?.() ?? sourceCode.getCwd?.() ?? repository; + if (isolationForFile(filename, cwd) !== false) return {}; + return { + CallExpression(node) { + if (isModuleMockCall(sourceCode, node.callee)) + context.report({ node, messageId: "shared" }); + }, + }; + }, +}; diff --git a/tests/boundary/filesystem/ciChangeScope.test.ts b/tests/boundary/filesystem/ciChangeScope.test.ts index 87b472c1fe..ffee09d45e 100644 --- a/tests/boundary/filesystem/ciChangeScope.test.ts +++ b/tests/boundary/filesystem/ciChangeScope.test.ts @@ -626,6 +626,12 @@ it.each([ "tests/module/example.test.ts", { code: true, runtime: false, browser: false, package: false }, ], + ["scripts/lib/release-proposal.mjs", { code: true, package: true }], + ["patches/release-please+17.3.0.patch", { code: true, package: true }], + [ + "tests/boundary/filesystem/releaseProposal.test.ts", + { code: true, package: true }, + ], ["website/src/index.ts", { code: false, website: true, package: false }], [ "website/public/examples/notes-electron/preload.js", diff --git a/tests/boundary/filesystem/ciTestProjects.test.ts b/tests/boundary/filesystem/ciTestProjects.test.ts index ac5c481dd1..34e9bc60c6 100644 --- a/tests/boundary/filesystem/ciTestProjects.test.ts +++ b/tests/boundary/filesystem/ciTestProjects.test.ts @@ -1,5 +1,5 @@ import { execFileSync } from "node:child_process"; -import { readFile, writeFile } from "node:fs/promises"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; @@ -14,10 +14,55 @@ import { parseDevelopmentTestRequest, } from "../../../scripts/lib/development-tests.mjs"; -import { execFileOutput } from "../../../src/process/ExecFileOutput.js"; +import { + execFileOutput, + execFileOutputFailure, +} from "../../../src/process/ExecFileOutput.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { writeRepositoryOxlintConfiguration } from "../../fixtures/oxlintConfiguration.js"; const repository = process.cwd(); +const oxlint = join(repository, "node_modules/oxlint/bin/oxlint"); +const oxlintConfig = join(repository, ".oxlintrc.json"); + +const runOxlint = async ( + cwd: string, + config: string, + files: readonly string[], +) => { + let stdout: string; + let exitCode: string | number = 0; + try { + ({ stdout } = await execFileOutput( + process.execPath, + [oxlint, "--threads=1", "--config", config, "--format", "json", ...files], + { + cwd, + timeout: 60_000, + maxBuffer: 8 * 1024 * 1024, + stopSignal: "SIGTERM", + }, + )); + } catch (cause) { + const failure = execFileOutputFailure(cause); + if ( + failure === undefined || + failure.code !== 1 || + failure.signal !== null || + failure.killed || + failure.outputTruncated + ) + throw cause; + stdout = failure.stdout; + exitCode = failure.code; + } + return { + exitCode, + report: JSON.parse(stdout) as { + diagnostics?: Array<{ code?: string; severity: string }>; + }, + }; +}; describe("canonical test-project ownership", () => { it("assigns every tracked test file exactly once to source or built", () => { @@ -154,17 +199,22 @@ describe("test module isolation", () => { expect(projectForTest(path)).toMatchObject({ name, group }); }); - it("keeps module mocks out of projects that share one module graph", async () => { - // A mock registered after another file in the same worker loaded the - // module never applies, so the test's outcome depends on file scheduling. - const root = await createTestTempDirectory("rea-vitest-mock-isolation-"); + it("uses one isolation policy for Vitest and binding-aware mock lint", async () => { + const files = execFileSync("git", ["ls-files", "*.test.ts"], { + cwd: repository, + encoding: "utf8", + }) + .trim() + .split("\n") + .filter(Boolean); + const root = await createTestTempDirectory("rea-vitest-project-policy-"); const script = join(root, "project-isolation.mjs"); await writeFile( script, ` import config from ${JSON.stringify(pathToFileURL(join(repository, "vitest.config.ts")).href)}; console.log(JSON.stringify(Object.fromEntries( - config.test.projects.map(project => [project.test.name, project.test.isolate !== false]), + config.test.projects.map(project => [project.test.name, project.test.isolate]), ))); `, ); @@ -179,17 +229,55 @@ describe("test module isolation", () => { }, ); const isolated = JSON.parse(result.stdout) as Record; - const mocking = execFileSync( - "git", - ["grep", "-l", "-E", String.raw`vi\.(mock|doMock)\(`, "--", "*.test.ts"], - { cwd: repository, encoding: "utf8" }, - ) - .trim() - .split("\n") - .filter(Boolean); - expect(mocking.length).toBeGreaterThan(0); + expect(files.length).toBeGreaterThan(0); expect( - mocking.filter((file) => isolated[projectForTest(file).name] !== true), + files.filter((file) => { + const project = projectForTest(file); + return isolated[project.name] !== (project.isolate !== false); + }), + ).toEqual([]); + + const { report } = await runOxlint(repository, oxlintConfig, files); + expect( + report.diagnostics?.filter( + ({ code, severity }) => + code === "rea(no-shared-module-mock)" && severity === "error", + ), + ).toEqual([]); + }); + + it("rejects actual Vitest module-mock aliases only in shared projects", async () => { + const root = await createTestTempDirectory("rea-vitest-mock-probe-"); + const sharedDirectory = join(root, "tests/evaluation"); + const isolatedDirectory = join(root, "tests/process-global"); + await mkdir(sharedDirectory, { recursive: true }); + await mkdir(isolatedDirectory, { recursive: true }); + const config = await writeRepositoryOxlintConfiguration(root); + const probe = async (directory: string, source: string) => { + const path = join(directory, "mock-probe.test.ts"); + await writeFile(path, source); + return runOxlint(root, config, [path]); + }; + const shared = await probe( + sharedDirectory, + 'import { vi as testing } from "vitest"; testing.mock("./dependency.js");', + ); + const isolated = await probe( + isolatedDirectory, + 'import * as testing from "vitest"; testing.vi.doMock("./dependency.js");', + ); + expect(shared.exitCode).toBe(1); + expect( + shared.report.diagnostics?.filter( + ({ code, severity }) => + code === "rea(no-shared-module-mock)" && severity === "error", + ), + ).toHaveLength(1); + expect(isolated.exitCode).toBe(0); + expect( + isolated.report.diagnostics?.filter( + ({ code }) => code === "rea(no-shared-module-mock)", + ), ).toEqual([]); }); }); diff --git a/tests/boundary/filesystem/releaseProposal.test.ts b/tests/boundary/filesystem/releaseProposal.test.ts index cfc6b02867..5f353e5472 100644 --- a/tests/boundary/filesystem/releaseProposal.test.ts +++ b/tests/boundary/filesystem/releaseProposal.test.ts @@ -1,7 +1,14 @@ import { execFile } from "node:child_process"; -import { readFile, writeFile } from "node:fs/promises"; +import { + copyFile, + mkdir, + readFile, + symlink, + writeFile, +} from "node:fs/promises"; import { createRequire } from "node:module"; import { dirname, join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { afterEach, beforeAll, expect, it, vi } from "vitest"; import { z } from "zod"; @@ -27,7 +34,6 @@ const silent = { }; beforeAll(async () => { - await applyReleasePleasePatch(); upstream = await import("release-please"); githubRuntime = require("release-please/build/src/github.js"); }); @@ -667,11 +673,93 @@ it("requires complete local history when sizing the upstream search", async () = ); }); -it("applies the exact dependency patch idempotently and rejects another version", async () => { - await expect(applyReleasePleasePatch()).resolves.toBeUndefined(); - const directory = dirname(require.resolve("release-please/package.json")); - const metadata = await readFile(join(directory, "package.json"), "utf8"); - expect(JSON.parse(metadata).version).toBe("17.3.0"); +const releasePatchFixture = async () => { + const root = await createTestTempDirectory("rea-release-git-patch-"); + const project = join(root, "project space Ω"); + const dependencyPath = "node_modules/release dependency space Ω"; + const directory = join(project, dependencyPath); + const upstreamDirectory = dirname( + require.resolve("release-please/package.json"), + ); + const source = fileURLToPath(new URL("../../../", import.meta.url)); + const patchPath = join(project, "patches/release-please+17.3.0.patch"); + const patch = await readFile( + join(source, "patches/release-please+17.3.0.patch"), + "utf8", + ); + const members = [...patch.matchAll(/^\+\+\+ b\/(.+)$/gmu)].map((match) => { + const member = match[1]; + if (member === undefined) throw new Error("Patch header omitted its path"); + return member; + }); + await mkdir(join(project, "scripts/lib"), { recursive: true }); + await mkdir(dirname(patchPath), { recursive: true }); + await mkdir(directory, { recursive: true }); + await writeFile(join(project, "package.json"), '{"type":"module"}'); + await writeFile(patchPath, patch); + for (const script of ["release-proposal.mjs", "release-notes.mjs"]) + await copyFile( + join(source, "scripts/lib", script), + join(project, "scripts/lib", script), + ); + for (const dependency of ["semver", "zod"]) + await symlink( + dirname(require.resolve(`${dependency}/package.json`)), + join(project, "node_modules", dependency), + "junction", + ); + for (const member of ["package.json", ...members]) { + await mkdir(dirname(join(directory, member)), { recursive: true }); + await copyFile(join(upstreamDirectory, member), join(directory, member)); + } + const environment = { + ...process.env, + GIT_CONFIG_COUNT: "1", + GIT_CONFIG_KEY_0: "core.protectNTFS", + GIT_CONFIG_VALUE_0: "true", + }; + const git = (...args: string[]) => + execute("git", args, { cwd: project, env: environment }); + // Installation owns the patched dependency. Reverse only the private copy + // to obtain the exact upstream input for the fresh-application control. + await git("apply", "--reverse", "--directory", dependencyPath, patchPath); + const apply = () => + execute( + process.execPath, + [ + "--input-type=module", + "-e", + `import { applyReleasePleasePatch } from ${JSON.stringify(pathToFileURL(join(project, "scripts/lib/release-proposal.mjs")).href)}; await applyReleasePleasePatch(${JSON.stringify(directory)});`, + ], + { cwd: project, env: environment, timeout: 10_000 }, + ); + const contents = () => + Promise.all( + members.map((member) => readFile(join(directory, member), "utf8")), + ); + return { directory, apply, contents, git, dependencyPath, patchPath }; +}; + +it("applies the private dependency patch with protected Git paths and is idempotent", async () => { + const fixture = await releasePatchFixture(); + const original = await fixture.contents(); + await fixture.apply(); + const patched = await fixture.contents(); + expect(patched).not.toEqual(original); + expect(patched.join("\n")).toContain("const rootOnlyNode ="); + await fixture.git( + "apply", + "--check", + "--reverse", + "--directory", + fixture.dependencyPath, + fixture.patchPath, + ); + await fixture.apply(); + expect(await fixture.contents()).toEqual(patched); +}); + +it("rejects a different dependency version before patching", async () => { const fixture = await createTestTempDirectory("rea-release-patch-"); await writeFile( join(fixture, "package.json"), @@ -682,22 +770,27 @@ it("applies the exact dependency patch idempotently and rejects another version" ); }); -it("rejects a partial or mismatched dependency patch", async () => { - const directory = dirname(require.resolve("release-please/package.json")); - const manifest = join(directory, "build/src/manifest.js"); - const original = await readFile(manifest, "utf8"); - try { +it.each(["partial", "mismatched"])( + "rejects a %s private dependency patch without changing its files", + async (state) => { + const fixture = await releasePatchFixture(); + const manifest = join(fixture.directory, "build/src/manifest.js"); + const original = await readFile(manifest, "utf8"); + await fixture.apply(); + const patched = await readFile(manifest, "utf8"); await writeFile( manifest, - original.replace( - "const rootOnlyNode =", - "const mismatchedRootOnlyNode =", - ), + state === "partial" + ? original + : patched.replace( + "const rootOnlyNode =", + "const mismatchedRootOnlyNode =", + ), ); - await expect(applyReleasePleasePatch()).rejects.toThrow( + const before = await fixture.contents(); + await expect(fixture.apply()).rejects.toThrow( "does not match the installed source", ); - } finally { - await writeFile(manifest, original); - } -}); + expect(await fixture.contents()).toEqual(before); + }, +); diff --git a/tests/evaluation/noSharedModuleMockRule.test.ts b/tests/evaluation/noSharedModuleMockRule.test.ts new file mode 100644 index 0000000000..e5288b2f86 --- /dev/null +++ b/tests/evaluation/noSharedModuleMockRule.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from "vitest"; +import { fileURLToPath } from "node:url"; +import { RuleTester } from "oxlint/plugins-dev"; + +import rule from "../../scripts/lint/rules/no-shared-module-mock.mjs"; + +RuleTester.describe = describe; +RuleTester.it = it; + +const tester = new RuleTester({ + cwd: fileURLToPath(new URL("../../", import.meta.url)), + languageOptions: { parserOptions: { lang: "ts" } }, +}); +const sharedFilename = fileURLToPath( + new URL("./noSharedModuleMockRule.test.ts", import.meta.url), +); +const isolatedFilename = fileURLToPath( + new URL("../process-global/noSharedModuleMockRule.test.ts", import.meta.url), +); + +tester.run("no-shared-module-mock", rule, { + valid: [ + { + filename: isolatedFilename, + code: 'import { vi } from "vitest"; vi.mock("./dependency.js");', + }, + { + filename: isolatedFilename, + code: 'import * as testing from "vitest"; testing.vi.doMock("./dependency.js");', + }, + { + filename: sharedFilename, + code: 'import { vi } from "vitest"; vi.spyOn(target, "method");', + }, + { + filename: sharedFilename, + code: "const mock = { mock() {} }; mock.mock();", + }, + { + filename: sharedFilename, + code: 'const source = "vi.mock(\\"./dependency.js\\")"; // vi.mock("./dependency.js")', + }, + { + filename: sharedFilename, + code: 'import { vi as testing } from "vitest"; function local(testing: { mock(): void }) { testing.mock(); }', + }, + { + filename: sharedFilename, + code: 'import * as vitest from "vitest"; function local(vitest: { vi: { mock(): void } }) { vitest.vi.mock(); }', + }, + { + filename: fileURLToPath( + new URL("../../tests/unknown/ignored.test.ts", import.meta.url), + ), + code: 'import { vi } from "vitest"; vi.mock("./dependency.js");', + }, + ], + invalid: [ + { + filename: sharedFilename, + code: 'import { vi } from "vitest"; vi.mock("./dependency.js");', + errors: [{ messageId: "shared" }], + }, + { + filename: sharedFilename, + code: 'import { vi as testing } from "vitest"; testing.doMock("./dependency.js");', + errors: [{ messageId: "shared" }], + }, + { + filename: sharedFilename, + code: 'import * as testing from "vitest"; testing.vi.mock("./dependency.js");', + errors: [{ messageId: "shared" }], + }, + { + filename: sharedFilename, + code: 'import * as vitest from "vitest"; vitest.vi["doMock"]("./dependency.js");', + errors: [{ messageId: "shared" }], + }, + ], +}); diff --git a/tests/process-global/curatedWindowsLane.test.ts b/tests/process-global/curatedWindowsLane.test.ts index a926bf9b7f..7dfe059911 100644 --- a/tests/process-global/curatedWindowsLane.test.ts +++ b/tests/process-global/curatedWindowsLane.test.ts @@ -83,6 +83,12 @@ describe("curated Windows test lane", () => { ); }); + it("keeps real Git dependency-patch admission in the Windows lane", async () => { + expect(await explicitPaths("windows")).toContain( + "tests/boundary/filesystem/releaseProposal.test.ts", + ); + }); + it("keeps Windows environment identity and layered setup regressions in the lane", async () => { expect(await explicitPaths("windows")).toEqual( expect.arrayContaining([ diff --git a/vitest.config.ts b/vitest.config.ts index 92519037a8..2da24fbea8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -42,14 +42,12 @@ const PROJECT_POLICIES = { maxWorkers: MAX_TEST_WORKERS, // Domain and contract modules are pure and own no process-global state, so // per-file module isolation adds startup cost without protecting a seam. - isolate: false, }, services: { pool: "threads" as const, maxWorkers: MAX_TEST_WORKERS, // Service tests use recording ports and own no process-global state. Share // their module graph so file startup does not dominate the lane. - isolate: false, }, adapters: { pool: "forks" as const, maxWorkers: MAX_TEST_WORKERS }, composition: { @@ -57,7 +55,6 @@ const PROJECT_POLICIES = { maxWorkers: MAX_TEST_WORKERS, // Composition tests use provider-neutral sessions and test-scoped cleanup; // sharing their immutable module graph avoids per-file startup work. - isolate: false, }, boundary: { pool: "forks" as const, maxWorkers: MAX_TEST_WORKERS }, "cli-boundary": { @@ -65,7 +62,6 @@ const PROJECT_POLICIES = { maxWorkers: MAX_TEST_WORKERS, // CLI tests create fresh command instances or subprocesses and own their // cleanup. Reuse the immutable graph; module mocks belong in process-global. - isolate: false, // The shared source/contract graph also needs transient schema/help space. // Ordinary CI exhausted 384 MiB after earlier files warmed that graph. // Use the measured 512 MiB budget with the same bounded concurrency. @@ -84,7 +80,6 @@ const PROJECT_POLICIES = { maxWorkers: MAX_TEST_WORKERS, // Each file creates and closes an explicit in-memory MCP session. Reuse the // immutable server graph so module startup does not outweigh transport work. - isolate: false, }, acceptance: { pool: "forks" as const, maxWorkers: MAX_TEST_WORKERS }, "process-global": { pool: "forks" as const, maxWorkers: MAX_TEST_WORKERS }, @@ -92,13 +87,11 @@ const PROJECT_POLICIES = { pool: "threads" as const, maxWorkers: MAX_TEST_WORKERS, // Contract cases use recording providers and own no process-global state. - isolate: false, }, evaluation: { pool: "threads" as const, maxWorkers: MAX_TEST_WORKERS, // Evaluator tests inspect deterministic inputs without mutating shared state. - isolate: false, }, }; @@ -132,6 +125,7 @@ const projects = TEST_PROJECTS.map((project) => { name: project.name, include: [...project.include], ...(project.exclude ? { exclude: [...project.exclude] } : {}), + isolate: project.isolate !== false, ...policy, }, };