mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(ci): own mock isolation and normalize Git patch directories
Convert native directory separators only at Git patch argument production. Exercise fresh, applied and invalid dependency patches in private copies, preserving protectNTFS, and select the regression for Windows CI. Replace the spelling-based mock inventory with one binding-aware Oxlint rule. Canonical test projects own isolation for both Vitest and lint; external temporary CLI probes avoid concurrent test discovery. Validated actual local Git and Oxlint execution, resolved-policy parity, CI selection and report tests. Native Windows verification remains with CI. Refs: #7050
This commit is contained in:
1 parent
cb26e8ca63
commit
c8269ca497
16 files changed
+463
-48
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { RuleTester } from "oxlint/plugins-dev";
|
||||
|
||||
declare const rule: Parameters<RuleTester["run"]>[1];
|
||||
export default rule;
|
||||
@@ -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" });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user