diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d91bbad1a..dfc4a4889 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,8 @@ jobs: - run: npm ci - if: github.event_name == 'push' || needs.changes.outputs.code == 'true' run: npm run check:ci + - if: github.event_name == 'push' || needs.changes.outputs.code == 'true' + run: npm run verify:test-discovery - if: github.event_name == 'pull_request' && needs.changes.outputs.code != 'true' run: npm run format:check diff --git a/.oxlintrc.json b/.oxlintrc.json index 7cdc48c82..fab5b7126 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -6,11 +6,19 @@ "max-depth": ["warn", 4], "max-lines": [ "warn", - { "max": 500, "skipBlankLines": true, "skipComments": true } + { + "max": 500, + "skipBlankLines": true, + "skipComments": true + } ], "max-lines-per-function": [ "warn", - { "max": 100, "skipBlankLines": true, "skipComments": true } + { + "max": 100, + "skipBlankLines": true, + "skipComments": true + } ], "max-params": ["warn", 4], "no-var": "warn", @@ -46,13 +54,52 @@ ] } }, + { + "files": ["src/{contracts,domain}/**/*.ts"], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": [ + "**/application/**", + "**/server/**", + "**/cli/**", + "**/main/**", + "**/cli.*", + "**/main.*", + "**/android/**", + "**/artifacts/**", + "**/browser/**", + "**/dotnet/**", + "**/firmware/**", + "**/ghidra/**", + "**/hopper/**", + "**/ida/**", + "**/native/**", + "**/process/**", + "**/reference/**", + "**/windows/**" + ], + "message": "Domain and contracts depend inward; use typed ports for application, caller and provider boundaries." + } + ] + } + ] + } + }, { "files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"], "rules": { "max-lines": "off", "max-lines-per-function": [ "error", - { "max": 150, "skipBlankLines": true, "skipComments": true } + { + "max": 150, + "skipBlankLines": true, + "skipComments": true + } ] } }, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79d513cd5..731cd2cdc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,10 @@ REA welcomes focused bug fixes, documentation improvements, tests, and reverse-engineering workflow enhancements. Open an issue before a large contract or architecture change so its scope can be agreed before implementation. +For capability organization and provider composition, follow the incremental +[migration guide](docs/capability-migration.md). Run `npm run verify:test-discovery` +after adding or moving tests. + When adding or changing an MCP tool, follow the [tool design guide](docs/tool-design.md) and preserve the canonical contracts and generated catalog. ## Development setup diff --git a/docs/capability-migration.md b/docs/capability-migration.md new file mode 100644 index 000000000..e17b700ee --- /dev/null +++ b/docs/capability-migration.md @@ -0,0 +1,79 @@ +# Incremental capability migration + +This working guide tracks the migration in #740. Update it as concrete domains +migrate. Existing public contracts and domain-specific ports remain authoritative; +there is no new universal provider interface or fixed module manifest. + +## Ownership to preserve + +| Responsibility | Current owner | Migration direction | +| --------------------------------------------- | ------------------------------------------- | -------------------------------------------------------------- | +| Analyst semantics and Evidence representation | domain | Keep pure and provider-neutral | +| Named public input/output contracts | contracts | Keep exact CLI/MCP meaning and complete discovery | +| Producer parsing and external tool protocols | provider adapters | Keep behind the relevant typed port | +| Shared analyst workflows | application | Share between CLI and MCP | +| Concrete provider construction | binary runtime, CLI registrars, MCP startup | Consolidate one capability at a time in production composition | +| Investigation Evidence and Unknowns | BinarySessionRecords | Extract one composed record owner after caller migration | +| Target/profile snapshots and invalidation | BinarySessionRecords/BinarySession | Keep binary-owned | +| Subprocess ownership and host primitives | process/windows | Reuse; preserve cancellation and cleanup | +| Public translation | CLI/MCP adapters | Delegate to shared workflows and named contracts | + +Retain exact Evidence IDs, provenance, detached reads, atomic Evidence/Unknown +mutations and optimistic revisions. Preserve current close and failed-cleanup +record clearing while extracting ownership; #721 owns any retention-policy change. +Each CLI invocation and MCP connection keeps its own state. Provider registration +must not launch engines or install prerequisites. + +## Migrate before generalizing + +1. Record the actual callers, state, environment, protocol and verification owner + of the capability being migrated. +2. Move concrete construction behind its existing typed port. Preserve injected + ports, selected environment, queues, failure state and cleanup behavior. +3. Test both caller paths and the applicable package/real-provider boundary. +4. Use independently migrated capabilities to identify repeated metadata and + composition needs. Derive a small source declaration only when those needs + are established; keep unlike target/lifecycle semantics separate. +5. Move a coherent capability's files, fixtures and verification lanes in a + mechanical PR. Update all path-sensitive consumers in that same PR. + +Pure helpers do not need an interface each. A capture-format adapter or typed +factory may be sufficient. Avoid generic JSON execution, external MCP catalog +passthrough, eager provider acquisition, new permission fields or a multi-package +rewrite. Optional loading should preserve successful peers and report the failed +capability without hiding tools from the catalog. + +## Checks before and after a move + +`npm run verify:test-discovery` compares actual Vitest file discovery with every +repository-owned `*.test.ts` under src and tests, including newly added files. The +check requires Git, includes untracked non-ignored tests, and excludes removed +paths during a move. It +rejects missing files and overlapping project ownership. It only lists files; +it does not replace behavior tests or real-provider verification. + +Check imports, project isolation and coverage, development-test selection, +lint exceptions, catalog/generator imports, npm and CI verification entrypoints, +fixture ownership, and installed-package bridge/native path resolution. +Domain and contracts must not import application workflows, caller adapters or +provider implementations. Production composition is a deliberate outer boundary. +Other existing adapter imports will migrate incrementally; do not widen a lint +exception to conceal new protocol code inside application workflows. + +Select verification by the claim changed: schema/composition, package, real +engine or host-native workflow. Record host/target prerequisites and source-owned +fixtures. Run heavy lanes serially with bounded workers/heaps and reuse existing +engines. Recording ports do not prove real provider or platform support. + +## Extension examples to validate during migration + +An implementation of an existing port should require a provider adapter, typed +production factory selection, exact producer parsing/provenance and matching +verification. Existing workflow meaning and public contracts should stay stable. + +A new analyst capability deliberately adds semantics, named contracts, a typed +port/workflow when needed, both caller adapters, availability, documentation and +verification. Its integration is broader than swapping a provider. The migration +should eliminate incidental duplicate wiring, not erase those design decisions. + +Update these examples with real migrated entrypoints before closing #740. diff --git a/package.json b/package.json index d46e50804..cfc5e283e 100644 --- a/package.json +++ b/package.json @@ -175,7 +175,8 @@ "build:ghidra:nativeaot": "node scripts/build-ghidra-nativeaot.mjs", "build:fixtures:nativeaot": "node scripts/generate-nativeaot-fixtures.mjs", "verify:ghidra:nativeaot": "npm run build:cached && node scripts/verify-real-ghidra-nativeaot.mjs", - "verify:browser:scripts": "npm run build:cached && node scripts/verify-browser-scripts.mjs" + "verify:browser:scripts": "npm run build:cached && node scripts/verify-browser-scripts.mjs", + "verify:test-discovery": "node scripts/verify-test-discovery.mjs" }, "lint-staged": { "*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [ diff --git a/scripts/lib/test-discovery.d.mts b/scripts/lib/test-discovery.d.mts new file mode 100644 index 000000000..259a21fbb --- /dev/null +++ b/scripts/lib/test-discovery.d.mts @@ -0,0 +1,14 @@ +/** Compare actual Vitest discovery with repository-owned test paths. */ +export function compareTestDiscovery( + expectedPaths: readonly string[], + discovery: unknown, + root: string, +): { + readonly missing: readonly string[]; + readonly unexpected: readonly string[]; + readonly duplicates: readonly { + readonly file: string; + readonly projects: readonly string[]; + }[]; + readonly discovered: number; +}; diff --git a/scripts/lib/test-discovery.mjs b/scripts/lib/test-discovery.mjs new file mode 100644 index 000000000..dbcafb220 --- /dev/null +++ b/scripts/lib/test-discovery.mjs @@ -0,0 +1,41 @@ +import { relative, resolve } from "node:path"; + +/** Compare actual Vitest discovery with repository-owned test paths. */ +export const compareTestDiscovery = (expectedPaths, discovery, root) => { + if (!Array.isArray(discovery)) + throw new TypeError("Vitest file discovery must be an array"); + const projectsByFile = new Map(); + for (const entry of discovery) { + if ( + entry === null || + typeof entry !== "object" || + typeof entry.file !== "string" || + entry.file.length === 0 || + typeof entry.projectName !== "string" || + entry.projectName.length === 0 + ) + throw new TypeError( + "Vitest discovery requires file and projectName strings", + ); + const path = relative(root, resolve(root, entry.file)).replaceAll( + "\\", + "/", + ); + const projects = projectsByFile.get(path) ?? []; + projects.push(entry.projectName); + projectsByFile.set(path, projects); + } + const expected = new Set(expectedPaths); + const sorted = (values) => [...values].sort(); + return { + missing: sorted(expected).filter((path) => !projectsByFile.has(path)), + unexpected: sorted(projectsByFile.keys()).filter( + (path) => !expected.has(path), + ), + duplicates: sorted(projectsByFile.keys()).flatMap((file) => { + const projects = projectsByFile.get(file); + return projects.length > 1 ? [{ file, projects: sorted(projects) }] : []; + }), + discovered: projectsByFile.size, + }; +}; diff --git a/scripts/verify-test-discovery.mjs b/scripts/verify-test-discovery.mjs new file mode 100644 index 000000000..b5101d255 --- /dev/null +++ b/scripts/verify-test-discovery.mjs @@ -0,0 +1,65 @@ +import { execFile } from "node:child_process"; +import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { promisify } from "node:util"; + +import { compareTestDiscovery } from "./lib/test-discovery.mjs"; + +const execute = promisify(execFile); +const root = resolve(import.meta.dirname, ".."); + +const temporary = await mkdtemp(join(tmpdir(), "rea-test-discovery-")); +try { + const output = join(temporary, "discovery.json"); + await execute( + process.execPath, + [ + join(root, "node_modules/vitest/vitest.mjs"), + "list", + "--filesOnly", + `--json=${output}`, + ], + { cwd: root, maxBuffer: 8 * 1024 * 1024 }, + ); + const files = await execute( + "git", + [ + "ls-files", + "-z", + "--cached", + "--others", + "--exclude-standard", + "--", + "src", + "tests", + ], + { cwd: root, maxBuffer: 8 * 1024 * 1024 }, + ); + const expected = []; + for (const path of files.stdout + .split("\0") + .filter((path) => path.endsWith(".test.ts"))) { + try { + if ((await stat(join(root, path))).isFile()) expected.push(path); + } catch (cause) { + if (cause.code !== "ENOENT") throw cause; + } + } + const report = compareTestDiscovery( + expected, + JSON.parse(await readFile(output, "utf8")), + root, + ); + if ( + report.missing.length || + report.unexpected.length || + report.duplicates.length + ) { + console.error(JSON.stringify(report, null, 2)); + process.exitCode = 1; + } else + console.log(`Discovered all ${report.discovered} test files exactly once.`); +} finally { + await rm(temporary, { recursive: true, force: true }); +} diff --git a/src/application/ReconstructionCoverageService.ts b/src/application/ReconstructionCoverageService.ts index 5706b008e..c947c9a9a 100644 --- a/src/application/ReconstructionCoverageService.ts +++ b/src/application/ReconstructionCoverageService.ts @@ -1,19 +1,10 @@ -import { z } from "zod"; - +import { reconstructionCoverageEvaluationInputSchema } from "../domain/reconstructionCoverageInput.js"; import { AnalysisInputError } from "../domain/analysisErrorCore.js"; import { type AnalysisError } from "../domain/analysisErrorBase.js"; -import { - evaluateReconstructionClosure, - reconstructionCoverageDataSchema, -} from "../domain/reconstructionCoverage.js"; +import { evaluateReconstructionClosure } from "../domain/reconstructionCoverage.js"; import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js"; import { err, ok, type Result } from "../domain/result.js"; -export const reconstructionCoverageEvaluationInputSchema = z.strictObject({ - coverage: reconstructionCoverageDataSchema, - boundary_id: z.string().min(1), -}); - /** Evaluate inline reconstruction coverage against a named boundary. */ export const evaluateReconstructionCoverage = ( rawInput: unknown, diff --git a/src/cli/applicationCommands.ts b/src/cli/applicationCommands.ts index 68aed7911..5f9bbb556 100644 --- a/src/cli/applicationCommands.ts +++ b/src/cli/applicationCommands.ts @@ -1,3 +1,4 @@ +import { reconstructionCoverageEvaluationInputSchema } from "../domain/reconstructionCoverageInput.js"; import { Cli, z } from "incur"; import { @@ -7,10 +8,7 @@ import { traceApplicationFeatureEvidenceValidated, } from "../application/JavaScriptApplicationWorkflowService.js"; import { traceJavaScriptSemanticsEvidenceValidated } from "../application/JavaScriptSemanticTraceService.js"; -import { - evaluateReconstructionCoverage, - reconstructionCoverageEvaluationInputSchema, -} from "../application/ReconstructionCoverageService.js"; +import { evaluateReconstructionCoverage } from "../application/ReconstructionCoverageService.js"; import { buildReconstructionObligationLedgerEvidenceValidated } from "../application/ReconstructionObligationLedgerService.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import { parseCliJsonInput } from "../cliJsonInput.js"; diff --git a/src/contracts/applicationToolContracts.ts b/src/contracts/applicationToolContracts.ts index 7758147cb..e87966931 100644 --- a/src/contracts/applicationToolContracts.ts +++ b/src/contracts/applicationToolContracts.ts @@ -10,7 +10,7 @@ import { traceApplicationFeatureRequestSchema, traceJavaScriptSemanticsRequestSchema, } from "./applicationWorkflowInputContracts.js"; -import { reconstructionCoverageEvaluationInputSchema } from "../application/ReconstructionCoverageService.js"; +import { reconstructionCoverageEvaluationInputSchema } from "../domain/reconstructionCoverageInput.js"; import { reconstructionClosureResultSchema } from "../domain/reconstructionCoverage.js"; import { reconstructionObligationLedgerInputSchema, diff --git a/src/contracts/contractSnapshot.test.ts b/src/contracts/contractSnapshot.test.ts index faa41c879..ead764023 100644 --- a/src/contracts/contractSnapshot.test.ts +++ b/src/contracts/contractSnapshot.test.ts @@ -1,7 +1,5 @@ import { describe, expect, it } from "vitest"; import { z } from "zod"; -import { createCli } from "../cli.js"; -import { createCliInventory } from "../../scripts/lib/product-catalog.mjs"; import { ENHANCED_TOOL_CONTRACTS } from "./enhancedToolContracts.js"; import { OFFICIAL_TOOL_CONTRACTS } from "./officialToolContracts.js"; @@ -19,14 +17,7 @@ import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "./javascriptRunti import { APPLICATION_TOOL_CONTRACTS } from "./applicationToolContracts.js"; import { WEB_SCRIPT_TOOL_CONTRACTS } from "./webScriptToolContracts.js"; import { TOOL_EFFECTS } from "./toolEffects.js"; -import { TOOL_CONTRACTS, toolContract } from "./toolContracts.js"; -import { - CLI_COMMAND_ALIASES, - CLI_COMMAND_NAMES, - CLI_COMMAND_TOOL_ALIASES, - MCP_TOOLS_WITHOUT_DEDICATED_CLI, -} from "../cliCommandNames.js"; - +import { toolContract } from "./toolContracts.js"; const contractJsonSchema = (schema: z.ZodType) => z.toJSONSchema(schema, { target: "draft-07", unrepresentable: "any" }); @@ -135,39 +126,4 @@ describe("tool contract surface", () => { destructiveHint: false, }); }); - - it("accounts for every MCP tool on the CLI or marks it intentionally MCP-only", () => { - const registeredCli = createCliInventory(createCli()); - const registeredCommandNames = new Set([ - ...registeredCli.primary, - ...registeredCli.aliases.map(({ name }) => name), - ]); - const cliToolNames = new Set( - registeredCli.primary.map((command) => command.replaceAll("-", "_")), - ); - for (const [command, names] of Object.entries(CLI_COMMAND_TOOL_ALIASES)) { - expect(registeredCommandNames.has(command)).toBe(true); - for (const name of names) cliToolNames.add(name); - } - for (const command of CLI_COMMAND_NAMES) - expect(registeredCommandNames.has(command)).toBe(true); - for (const command of CLI_COMMAND_ALIASES) - expect(registeredCommandNames.has(command)).toBe(true); - const intentionalMcpOnly = new Set(MCP_TOOLS_WITHOUT_DEDICATED_CLI); - expect( - [...intentionalMcpOnly].filter((name) => cliToolNames.has(name)).sort(), - ).toEqual([]); - const unaccounted = TOOL_CONTRACTS.map(({ name }) => name).filter( - (name) => !cliToolNames.has(name) && !intentionalMcpOnly.has(name), - ); - expect(unaccounted).toEqual([]); - expect(new Set(MCP_TOOLS_WITHOUT_DEDICATED_CLI).size).toBe( - MCP_TOOLS_WITHOUT_DEDICATED_CLI.length, - ); - expect( - MCP_TOOLS_WITHOUT_DEDICATED_CLI.every((name) => - TOOL_CONTRACTS.some((contract) => contract.name === name), - ), - ).toBe(true); - }); }); diff --git a/src/domain/reconstructionCoverageInput.ts b/src/domain/reconstructionCoverageInput.ts new file mode 100644 index 000000000..35e2fc912 --- /dev/null +++ b/src/domain/reconstructionCoverageInput.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; +import { reconstructionCoverageDataSchema } from "./reconstructionCoverage.js"; + +/** Named-boundary input for inline reconstruction coverage evaluation. */ +export const reconstructionCoverageEvaluationInputSchema = z.strictObject({ + coverage: reconstructionCoverageDataSchema, + boundary_id: z.string().min(1), +}); diff --git a/tests/boundary/cli/cliToolCoverage.test.ts b/tests/boundary/cli/cliToolCoverage.test.ts new file mode 100644 index 000000000..729d9bce2 --- /dev/null +++ b/tests/boundary/cli/cliToolCoverage.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; + +import { createCli } from "../../../src/cli.js"; +import { createCliInventory } from "../../../scripts/lib/product-catalog.mjs"; +import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; +import { + CLI_COMMAND_ALIASES, + CLI_COMMAND_NAMES, + CLI_COMMAND_TOOL_ALIASES, + MCP_TOOLS_WITHOUT_DEDICATED_CLI, +} from "../../../src/cliCommandNames.js"; + +describe("CLI canonical tool coverage", () => { + it("accounts for every MCP tool on the CLI or marks it intentionally MCP-only", () => { + const registeredCli = createCliInventory(createCli()); + const registeredCommandNames = new Set([ + ...registeredCli.primary, + ...registeredCli.aliases.map(({ name }) => name), + ]); + const cliToolNames = new Set( + registeredCli.primary.map((command) => command.replaceAll("-", "_")), + ); + for (const [command, names] of Object.entries(CLI_COMMAND_TOOL_ALIASES)) { + expect(registeredCommandNames.has(command)).toBe(true); + for (const name of names) cliToolNames.add(name); + } + for (const command of CLI_COMMAND_NAMES) + expect(registeredCommandNames.has(command)).toBe(true); + for (const command of CLI_COMMAND_ALIASES) + expect(registeredCommandNames.has(command)).toBe(true); + const intentionalMcpOnly = new Set(MCP_TOOLS_WITHOUT_DEDICATED_CLI); + expect( + [...intentionalMcpOnly].filter((name) => cliToolNames.has(name)).sort(), + ).toEqual([]); + const unaccounted = TOOL_CONTRACTS.map(({ name }) => name).filter( + (name) => !cliToolNames.has(name) && !intentionalMcpOnly.has(name), + ); + expect(unaccounted).toEqual([]); + expect(new Set(MCP_TOOLS_WITHOUT_DEDICATED_CLI).size).toBe( + MCP_TOOLS_WITHOUT_DEDICATED_CLI.length, + ); + expect( + MCP_TOOLS_WITHOUT_DEDICATED_CLI.every((name) => + TOOL_CONTRACTS.some((contract) => contract.name === name), + ), + ).toBe(true); + }); +}); diff --git a/tests/evaluation/moduleBoundaries.test.ts b/tests/evaluation/moduleBoundaries.test.ts new file mode 100644 index 000000000..2ef304015 --- /dev/null +++ b/tests/evaluation/moduleBoundaries.test.ts @@ -0,0 +1,44 @@ +import { spawnSync } from "node:child_process"; +import { copyFile, mkdir, writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; + +describe("incremental module import boundaries", () => { + it.each([ + ["src/domain/probe.ts", "../application/Workflow.js", false], + ["src/domain/probe.ts", "../browser/Provider.js", false], + ["src/contracts/probe.ts", "../android/Provider.js", false], + ["src/contracts/probe.ts", "../cli.js", false], + ["src/domain/probe.ts", "./result.js", true], + ["src/contracts/probe.ts", "../domain/result.js", true], + ["src/application/probe.ts", "../domain/result.js", true], + ["src/composition/probe.ts", "../ghidra/Provider.js", true], + ])("checks %s importing %s", async (file, dependency, allowed) => { + const temporary = await createTestTempDirectory("rea-module-boundary-"); + const config = join(temporary, ".oxlintrc.json"); + await copyFile(resolve(".oxlintrc.json"), config); + const fixture = join(temporary, file); + await mkdir(dirname(fixture), { recursive: true }); + await writeFile( + fixture, + `import { value } from ${JSON.stringify(dependency)}; export { value };\n`, + ); + const result = spawnSync( + process.execPath, + [ + resolve("node_modules/oxlint/bin/oxlint"), + "-c", + config, + "--format", + "json", + file, + ], + { cwd: temporary, encoding: "utf8" }, + ); + if (result.error !== undefined) throw result.error; + expect(result.status).toBe(allowed ? 0 : 1); + if (!allowed) expect(result.stdout).toContain("no-restricted-imports"); + }); +}); diff --git a/tests/evaluation/testDiscovery.test.ts b/tests/evaluation/testDiscovery.test.ts new file mode 100644 index 000000000..d4ef90508 --- /dev/null +++ b/tests/evaluation/testDiscovery.test.ts @@ -0,0 +1,56 @@ +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +import { compareTestDiscovery } from "../../scripts/lib/test-discovery.mjs"; + +const root = resolve("test-discovery-fixture"); +const first = "src/domain/sample.test.ts"; +const second = "src/new-provider/sample.test.ts"; + +describe("test discovery migration guard", () => { + it("accepts absolute discovered paths without changing their project ownership", () => { + expect( + compareTestDiscovery( + [first], + [{ file: resolve(root, first), projectName: "domain" }], + root, + ), + ).toEqual({ missing: [], unexpected: [], duplicates: [], discovered: 1 }); + }); + + it("reports a new provider test omitted by the current project globs", () => { + expect( + compareTestDiscovery( + [first, second], + [{ file: first, projectName: "domain" }], + root, + ).missing, + ).toEqual([second]); + }); + + it("reports overlapping project discovery and unexpected files separately", () => { + const report = compareTestDiscovery( + [first], + [ + { file: first, projectName: "domain" }, + { file: first, projectName: "adapters" }, + { file: second, projectName: "adapters" }, + ], + root, + ); + expect(report.duplicates).toEqual([ + { file: first, projects: ["adapters", "domain"] }, + ]); + expect(report.unexpected).toEqual([second]); + }); + + it.each([ + null, + {}, + [{ file: "", projectName: "domain" }], + [{ file: first }], + [{ file: first, projectName: null }], + ])("rejects malformed producer output: %j", (input) => { + expect(() => compareTestDiscovery([first], input, root)).toThrow(TypeError); + }); +}); diff --git a/turbo.json b/turbo.json index f4484b6c3..d43d731d9 100644 --- a/turbo.json +++ b/turbo.json @@ -28,7 +28,8 @@ "tests/**/*.ts", "tsconfig.json", "tsconfig.build.json", - "vitest.config.ts" + "vitest.config.ts", + "scripts/**/*.d.mts" ], "outputs": [".cache/tsconfig.typecheck.tsbuildinfo"] },