From 2223da19166a3ec485d98a0d005113a9d0358f37 Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <1705004398@qq.com> Date: Tue, 14 Jul 2026 21:08:09 +0000 Subject: [PATCH 1/3] test: reduce test suite runtime --- package.json | 1 + scripts/build-cache.mjs | 103 +++++++++++++ scripts/generate-package-metadata.mjs | 18 ++- scripts/record-build-cache.mjs | 7 + tests/artifactProvider.test.ts | 24 ++-- tests/bridgeRegex.test.ts | 200 ++++++++++++++++++++++++-- tests/browserCli.test.ts | 196 +++++++++++++------------ tests/cliOutput.test.ts | 118 ++++++++------- tests/contractSnapshot.test.ts | 39 ++--- tests/enhancedTools.test.ts | 9 +- tests/fixtures/bridgeRegexProbe.py | 10 +- tests/providerArchitecture.test.ts | 32 +++-- tsconfig.build.json | 2 + vitest.config.ts | 30 +++- 14 files changed, 582 insertions(+), 207 deletions(-) create mode 100644 scripts/build-cache.mjs create mode 100644 scripts/record-build-cache.mjs diff --git a/package.json b/package.json index e3a3148a4..4e2f33542 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "scripts": { "prebuild": "node scripts/generate-package-metadata.mjs", "build": "tsc -p tsconfig.build.json", + "postbuild": "node scripts/record-build-cache.mjs", "check": "npm run typecheck && npm run lint && npm run format:check && npm test", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/scripts/build-cache.mjs b/scripts/build-cache.mjs new file mode 100644 index 000000000..9ab4bfc9d --- /dev/null +++ b/scripts/build-cache.mjs @@ -0,0 +1,103 @@ +import { createHash } from "node:crypto"; +import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +const CACHE_DIRECTORY = ["node_modules", ".cache"]; +const BUILD_INFO_NAME = "rea-tsconfig-build.tsbuildinfo"; +const MANIFEST_NAME = "rea-tsconfig-build-outputs.json"; + +const isMissing = (cause) => + typeof cause === "object" && + cause !== null && + "code" in cause && + cause.code === "ENOENT"; + +const outputFiles = async (root) => { + const sourceRoot = join(root, "src"); + const entries = await readdir(sourceRoot, { recursive: true }); + return entries + .filter((entry) => entry.endsWith(".ts") && !entry.endsWith(".d.ts")) + .sort() + .map((entry) => { + const output = `${entry.slice(0, -3)}.js`; + return { output, path: join(root, "dist", output) }; + }); +}; + +const digest = async (path) => + createHash("sha256") + .update(await readFile(path)) + .digest("hex"); + +const cachePath = (root, name) => join(root, ...CACHE_DIRECTORY, name); + +const invalidate = async (root) => { + await Promise.all([ + rm(cachePath(root, BUILD_INFO_NAME), { force: true }), + rm(cachePath(root, MANIFEST_NAME), { force: true }), + ]); +}; + +const readManifest = async (root) => { + try { + const parsed = JSON.parse( + await readFile(cachePath(root, MANIFEST_NAME), "utf8"), + ); + if ( + typeof parsed !== "object" || + parsed === null || + parsed.version !== 1 || + typeof parsed.outputs !== "object" || + parsed.outputs === null || + Array.isArray(parsed.outputs) + ) + return undefined; + return parsed.outputs; + } catch (cause) { + if (isMissing(cause) || cause instanceof SyntaxError) return undefined; + throw cause; + } +}; + +/** Invalidates incremental state when generated JavaScript was removed or changed. */ +export const validateBuildCache = async (root) => { + const [files, manifest] = await Promise.all([ + outputFiles(root), + readManifest(root), + ]); + if (manifest === undefined || Object.keys(manifest).length !== files.length) { + await invalidate(root); + return; + } + const matches = await Promise.all( + files.map(async ({ output, path }) => { + if (typeof manifest[output] !== "string") return false; + try { + return (await digest(path)) === manifest[output]; + } catch (cause) { + if (isMissing(cause)) return false; + throw cause; + } + }), + ); + if (!matches.every(Boolean)) await invalidate(root); +}; + +/** Records generated JavaScript so a later incremental build can verify its cache. */ +export const recordBuildCache = async (root) => { + const files = await outputFiles(root); + const entries = await Promise.all( + files.map(async ({ output, path }) => [output, await digest(path)]), + ); + const source = `${JSON.stringify({ version: 1, outputs: Object.fromEntries(entries) })}\n`; + const directory = join(root, ...CACHE_DIRECTORY); + await mkdir(directory, { recursive: true }); + const path = cachePath(root, MANIFEST_NAME); + let existing; + try { + existing = await readFile(path, "utf8"); + } catch (cause) { + if (!isMissing(cause)) throw cause; + } + if (existing !== source) await writeFile(path, source, "utf8"); +}; diff --git a/scripts/generate-package-metadata.mjs b/scripts/generate-package-metadata.mjs index aa15cbe54..43ced3f84 100644 --- a/scripts/generate-package-metadata.mjs +++ b/scripts/generate-package-metadata.mjs @@ -2,6 +2,8 @@ import { readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { validateBuildCache } from "./build-cache.mjs"; + const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const packageJson = JSON.parse( await readFile(join(root, "package.json"), "utf8"), @@ -32,4 +34,18 @@ export const PACKAGE_METADATA = { skillVersion: ${JSON.stringify(skillVersion)}, } as const; `; -await writeFile(join(root, "src/generatedPackageMetadata.ts"), source, "utf8"); +const outputPath = join(root, "src/generatedPackageMetadata.ts"); +let existingSource; +try { + existingSource = await readFile(outputPath, "utf8"); +} catch (cause) { + if ( + typeof cause !== "object" || + cause === null || + !("code" in cause) || + cause.code !== "ENOENT" + ) + throw cause; +} +if (existingSource !== source) await writeFile(outputPath, source, "utf8"); +await validateBuildCache(root); diff --git a/scripts/record-build-cache.mjs b/scripts/record-build-cache.mjs new file mode 100644 index 000000000..06b0ed8ad --- /dev/null +++ b/scripts/record-build-cache.mjs @@ -0,0 +1,7 @@ +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { recordBuildCache } from "./build-cache.mjs"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +await recordBuildCache(root); diff --git a/tests/artifactProvider.test.ts b/tests/artifactProvider.test.ts index b83495aa4..eb88b50e9 100644 --- a/tests/artifactProvider.test.ts +++ b/tests/artifactProvider.test.ts @@ -314,17 +314,19 @@ describe("artifact graph provider", () => { const client = new ArtifactProvider().createClient( target(root, "directory"), ); - const first = await client.execute( - "inventory_artifact", - artifactInventoryInputSchema.parse({ occurrence_limit: 500 }), - ); - const secondPage = await client.execute( - "inventory_artifact", - artifactInventoryInputSchema.parse({ - occurrence_offset: 500, - occurrence_limit: 500, - }), - ); + const [first, secondPage] = await Promise.all([ + client.execute( + "inventory_artifact", + artifactInventoryInputSchema.parse({ occurrence_limit: 500 }), + ), + client.execute( + "inventory_artifact", + artifactInventoryInputSchema.parse({ + occurrence_offset: 500, + occurrence_limit: 500, + }), + ), + ]); expect(first.ok && secondPage.ok).toBe(true); if (!first.ok || !secondPage.ok) return; const left = artifactInventoryResultSchema.parse(first.value.result); diff --git a/tests/bridgeRegex.test.ts b/tests/bridgeRegex.test.ts index 1d70a59c0..b882a6d59 100644 --- a/tests/bridgeRegex.test.ts +++ b/tests/bridgeRegex.test.ts @@ -1,11 +1,9 @@ -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { z } from "zod"; -const execFileAsync = promisify(execFile); const bridgePath = fileURLToPath( new URL("../bridge/hopper_bridge.py", import.meta.url), ); @@ -49,6 +47,7 @@ const probeResultSchema = z.union([ matchResultSchema, searchResultSchema, ]); +type ProbeResult = z.infer; type ProbeInput = | { @@ -63,15 +62,194 @@ type ProbeInput = readonly params: Readonly>; }; -const probe = async (input: ProbeInput) => { - const result = await execFileAsync( - "python3", - [probePath, bridgePath, JSON.stringify(input)], - { timeout: 3_000, maxBuffer: 1024 * 1024 }, - ); - return probeResultSchema.parse(JSON.parse(result.stdout)); +interface PendingProbe { + readonly resolve: (result: ProbeResult) => void; + readonly reject: (error: Error) => void; + timeout: NodeJS.Timeout | undefined; +} + +const PROBE_TIMEOUT_MS = 3_000; +const PROBE_BUFFER_LIMIT = 1_024 * 1_024; +const pendingProbes: PendingProbe[] = []; +let probeProcess: ChildProcessWithoutNullStreams | undefined; +let probeStdout = ""; +let probeStderr = ""; +let probeFailure: Error | undefined; +let stoppingProbe = false; + +const failureWithStderr = (message: string): Error => { + const stderr = probeStderr.trim(); + return new Error(stderr.length === 0 ? message : `${message}: ${stderr}`); }; +const rejectPendingProbes = (error: Error): void => { + for (const pending of pendingProbes.splice(0)) { + if (pending.timeout !== undefined) clearTimeout(pending.timeout); + pending.reject(error); + } +}; + +const failProbeProcess = (error: Error): void => { + probeFailure ??= error; + rejectPendingProbes(probeFailure); + if ( + !stoppingProbe && + probeProcess?.exitCode === null && + probeProcess.signalCode === null + ) + probeProcess.kill("SIGKILL"); +}; + +const acceptProbeLine = (line: string): void => { + const pending = pendingProbes.shift(); + if (pending === undefined) { + failProbeProcess( + failureWithStderr("Python probe returned an extra result"), + ); + return; + } + if (pending.timeout !== undefined) clearTimeout(pending.timeout); + try { + const decoded: unknown = JSON.parse(line); + pending.resolve(probeResultSchema.parse(decoded)); + } catch (cause: unknown) { + const error = + cause instanceof Error + ? cause + : failureWithStderr("Python probe returned an invalid result"); + pending.reject(error); + failProbeProcess(error); + } +}; + +const acceptProbeOutput = (chunk: string): void => { + probeStdout += chunk; + if (Buffer.byteLength(probeStdout) > PROBE_BUFFER_LIMIT) { + failProbeProcess(failureWithStderr("Python probe output exceeded 1 MiB")); + return; + } + let newline = probeStdout.indexOf("\n"); + while (newline >= 0) { + const line = probeStdout.slice(0, newline).replace(/\r$/u, ""); + probeStdout = probeStdout.slice(newline + 1); + acceptProbeLine(line); + newline = probeStdout.indexOf("\n"); + } +}; + +const startProbeProcess = async (): Promise => { + stoppingProbe = false; + probeFailure = undefined; + probeStdout = ""; + probeStderr = ""; + const child = spawn("python3", [probePath, bridgePath], { + stdio: ["pipe", "pipe", "pipe"], + }); + probeProcess = child; + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", acceptProbeOutput); + child.stderr.on("data", (chunk: string) => { + probeStderr += chunk; + if (Buffer.byteLength(probeStderr) > PROBE_BUFFER_LIMIT) + failProbeProcess(new Error("Python probe stderr exceeded 1 MiB")); + }); + child.stdin.on("error", (error) => { + if (!stoppingProbe) failProbeProcess(error); + }); + child.on("error", (error) => { + if (!stoppingProbe) failProbeProcess(error); + }); + child.on("exit", (code, signal) => { + if (!stoppingProbe) + failProbeProcess( + failureWithStderr( + `Python probe exited before responding (code ${String(code)}, signal ${String(signal)})`, + ), + ); + }); + await new Promise((resolve, reject) => { + const onSpawn = (): void => { + child.off("error", onError); + resolve(); + }; + const onError = (error: Error): void => { + child.off("spawn", onSpawn); + reject(error); + }; + child.once("spawn", onSpawn); + child.once("error", onError); + }); +}; + +const hasExited = (child: ChildProcessWithoutNullStreams): boolean => + child.exitCode !== null || child.signalCode !== null; + +const waitForExit = ( + child: ChildProcessWithoutNullStreams, + timeoutMs: number, +): Promise => { + if (hasExited(child)) return Promise.resolve(true); + return new Promise((resolve) => { + const onExit = (): void => { + clearTimeout(timeout); + resolve(true); + }; + const timeout = setTimeout(() => { + child.off("exit", onExit); + resolve(false); + }, timeoutMs); + child.once("exit", onExit); + }); +}; + +const stopProbeProcess = async (): Promise => { + const child = probeProcess; + if (child === undefined) return; + stoppingProbe = true; + rejectPendingProbes(new Error("Python probe stopped before responding")); + if (!hasExited(child)) child.stdin.end(); + if (!(await waitForExit(child, 500))) child.kill("SIGTERM"); + if (!(await waitForExit(child, 500))) child.kill("SIGKILL"); + if (!(await waitForExit(child, 500))) + throw new Error("Python probe did not stop after SIGKILL"); + probeProcess = undefined; +}; + +const probe = (input: ProbeInput): Promise => { + const child = probeProcess; + if (probeFailure !== undefined) return Promise.reject(probeFailure); + if (child === undefined || hasExited(child)) + return Promise.reject(new Error("Python probe is not running")); + return new Promise((resolve, reject) => { + const pending: PendingProbe = { resolve, reject, timeout: undefined }; + pending.timeout = setTimeout(() => { + failProbeProcess( + new Error( + `Python probe request timed out after ${String(PROBE_TIMEOUT_MS)} ms`, + ), + ); + }, PROBE_TIMEOUT_MS); + pendingProbes.push(pending); + child.stdin.write(`${JSON.stringify(input)}\n`, (error) => { + if (error !== null && error !== undefined) failProbeProcess(error); + }); + }); +}; + +beforeAll(startProbeProcess); +beforeEach(async () => { + if ( + probeFailure === undefined && + probeProcess !== undefined && + !hasExited(probeProcess) + ) + return; + await stopProbeProcess(); + await startProbeProcess(); +}); +afterAll(stopProbeProcess); + const invalidRequest = (action: "match" | "search", message: string) => ({ action, ok: false, diff --git a/tests/browserCli.test.ts b/tests/browserCli.test.ts index 36880527a..85338a399 100644 --- a/tests/browserCli.test.ts +++ b/tests/browserCli.test.ts @@ -34,10 +34,30 @@ describe("browser CLI parity", () => { browser.allowedOrigin, ]), }; - const listed = await runCli( - ["list-browser-targets", browser.endpoint, "--approved", "--json"], - environment, - ); + const [listed, inspected] = await Promise.all([ + runCli( + ["list-browser-targets", browser.endpoint, "--approved", "--json"], + environment, + ), + runCli( + [ + "inspect-web-page", + browser.endpoint, + "allowed-page", + "--approved", + "--observation-ms", + "0", + "--include-console-text", + "--console-text-approved", + "--include-json-body-shapes", + "--json-body-schema-approved", + "--include-websocket-shapes", + "--websocket-shape-approved", + "--json", + ], + environment, + ), + ]); expect(listed).toMatchObject({ operation: "list_browser_targets", provider: { id: "rea-cdp-browser" }, @@ -45,24 +65,6 @@ describe("browser CLI parity", () => { targets: { items: [{ target_id: "allowed-page" }] }, }, }); - const inspected = await runCli( - [ - "inspect-web-page", - browser.endpoint, - "allowed-page", - "--approved", - "--observation-ms", - "0", - "--include-console-text", - "--console-text-approved", - "--include-json-body-shapes", - "--json-body-schema-approved", - "--include-websocket-shapes", - "--websocket-shape-approved", - "--json", - ], - environment, - ); expect(inspected).toMatchObject({ operation: "inspect_web_page", provider: { id: "rea-cdp-browser" }, @@ -79,15 +81,30 @@ describe("browser CLI parity", () => { }, }); const inspection = normalizedResult(inspected); - const compared = await runCli( - [ - "compare-web-captures", - JSON.stringify({ inspection }), - JSON.stringify({ inspection }), - "--json", - ], - environment, - ); + const [compared, analyzed] = await Promise.all([ + runCli( + [ + "compare-web-captures", + JSON.stringify({ inspection }), + JSON.stringify({ inspection }), + "--json", + ], + environment, + ), + runCli( + [ + "analyze-web-bundle", + browser.endpoint, + "allowed-page", + "--approved", + "--source-capture-approved", + "--observation-ms", + "0", + "--json", + ], + environment, + ), + ]); expect(compared).toMatchObject({ operation: "compare_web_captures", normalized_result: { @@ -97,19 +114,6 @@ describe("browser CLI parity", () => { }, }, }); - const analyzed = await runCli( - [ - "analyze-web-bundle", - browser.endpoint, - "allowed-page", - "--approved", - "--source-capture-approved", - "--observation-ms", - "0", - "--json", - ], - environment, - ); expect(analyzed).toMatchObject({ operation: "analyze_web_bundle", normalized_result: { @@ -117,18 +121,32 @@ describe("browser CLI parity", () => { observations: { source_maps: { status: "not_requested" } }, }, }); - const observedSession = await runCli( - [ - "observe-web-session", - browser.endpoint, - "allowed-page", - "--approved", - "--observation-ms", - "5", - "--json", - ], - environment, - ); + const [observedSession, webMcp] = await Promise.all([ + runCli( + [ + "observe-web-session", + browser.endpoint, + "allowed-page", + "--approved", + "--observation-ms", + "5", + "--json", + ], + environment, + ), + runCli( + [ + "discover-webmcp-tools", + browser.endpoint, + "allowed-page", + "--approved", + "--observation-ms", + "0", + "--json", + ], + environment, + ), + ]); expect(observedSession).toMatchObject({ operation: "observe_web_session", normalized_result: { @@ -138,18 +156,6 @@ describe("browser CLI parity", () => { ]), }, }); - const webMcp = await runCli( - [ - "discover-webmcp-tools", - browser.endpoint, - "allowed-page", - "--approved", - "--observation-ms", - "0", - "--json", - ], - environment, - ); expect(webMcp).toMatchObject({ operation: "discover_webmcp_tools", normalized_result: { @@ -158,17 +164,34 @@ describe("browser CLI parity", () => { }, }, }); - const screenshot = await runCli( - [ - "capture-web-screenshot", - browser.endpoint, - "allowed-page", - "--approved", - "--screenshot-approved", - "--json", - ], - environment, - ); + const [screenshot, policy] = await Promise.all([ + runCli( + [ + "capture-web-screenshot", + browser.endpoint, + "allowed-page", + "--approved", + "--screenshot-approved", + "--json", + ], + environment, + ), + runCli( + [ + "policy", + "explain", + "browser_observe", + "--origins", + browser.endpoint, + "--origins", + browser.allowedOrigin, + "--network", + "loopback", + "--json", + ], + environment, + ), + ]); const artifact = screenshotArtifact(screenshot); expect(artifact).toMatchObject({ media_type: "image/png", bytes: 70 }); const visual = await runCli( @@ -184,21 +207,6 @@ describe("browser CLI parity", () => { operation: "compare_web_screenshots", normalized_result: { status: "identical", changed_pixels: 0 }, }); - const policy = await runCli( - [ - "policy", - "explain", - "browser_observe", - "--origins", - browser.endpoint, - "--origins", - browser.allowedOrigin, - "--network", - "loopback", - "--json", - ], - environment, - ); expect(policy).toEqual({ allowed: true, grant_id: "administrator:browser_observe", diff --git a/tests/cliOutput.test.ts b/tests/cliOutput.test.ts index 5373a070d..1af676f01 100644 --- a/tests/cliOutput.test.ts +++ b/tests/cliOutput.test.ts @@ -66,33 +66,35 @@ describe("CLI output boundary", () => { it( "sanitizes a real missing-argument dispatcher failure", async () => { - await expect( - execFileAsync(process.execPath, ["scripts/rea.mjs", "analyze"], { - cwd: process.cwd(), + await Promise.all([ + expect( + execFileAsync(process.execPath, ["scripts/rea.mjs", "analyze"], { + cwd: process.cwd(), + }), + ).rejects.toMatchObject({ + stdout: + 'code: VALIDATION_ERROR\nmessage: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n', }), - ).rejects.toMatchObject({ - stdout: - 'code: VALIDATION_ERROR\nmessage: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n', - }); - await expect( - execFileAsync( - process.execPath, - ["scripts/rea.mjs", "--full-output", "--json", "analyze"], - { cwd: process.cwd() }, - ), - ).rejects.toMatchObject({ - stdout: expect.not.stringContaining("fieldErrors"), - }); - await expect( - execFileAsync( - process.execPath, - ["scripts/rea.mjs", "--full-output", "analyze"], - { cwd: process.cwd() }, - ), - ).rejects.toMatchObject({ - stdout: - 'ok: false\nerror:\n code: VALIDATION_ERROR\n message: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n', - }); + expect( + execFileAsync( + process.execPath, + ["scripts/rea.mjs", "--full-output", "--json", "analyze"], + { cwd: process.cwd() }, + ), + ).rejects.toMatchObject({ + stdout: expect.not.stringContaining("fieldErrors"), + }), + expect( + execFileAsync( + process.execPath, + ["scripts/rea.mjs", "--full-output", "analyze"], + { cwd: process.cwd() }, + ), + ).rejects.toMatchObject({ + stdout: + 'ok: false\nerror:\n code: VALIDATION_ERROR\n message: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n', + }), + ]); }, CLI_INTEGRATION_TIMEOUT_MS, ); @@ -108,22 +110,24 @@ describe("CLI output boundary", () => { await createPackageWithOptions(source, archive, { unpack: "*.js" }); await writeFile(join(`${archive}.unpacked`, "main.js"), "changed();\n"); - for (const flags of [["--json"], ["--full-output", "--json"]]) { - const execution = execFileAsync(process.execPath, [ - "scripts/rea.mjs", - ...flags, - "inventory-artifact", - archive, - ]); - const failure = await execution.catch((cause: unknown) => cause); - expect(failure).toMatchObject({ code: 1 }); - const { stdout } = failure as { readonly stdout: string }; - const output = JSON.stringify(JSON.parse(stdout) as unknown); - expect(output).toContain('"logical_path":"main.js"'); - expect(output).toMatch(/"declared_sha256":"[a-f0-9]{64}"/u); - expect(output).toMatch(/"calculated_sha256":"[a-f0-9]{64}"/u); - expect(output).toContain('"unpacked":true'); - } + await Promise.all( + [["--json"], ["--full-output", "--json"]].map(async (flags) => { + const execution = execFileAsync(process.execPath, [ + "scripts/rea.mjs", + ...flags, + "inventory-artifact", + archive, + ]); + const failure = await execution.catch((cause: unknown) => cause); + expect(failure).toMatchObject({ code: 1 }); + const { stdout } = failure as { readonly stdout: string }; + const output = JSON.stringify(JSON.parse(stdout) as unknown); + expect(output).toContain('"logical_path":"main.js"'); + expect(output).toMatch(/"declared_sha256":"[a-f0-9]{64}"/u); + expect(output).toMatch(/"calculated_sha256":"[a-f0-9]{64}"/u); + expect(output).toContain('"unpacked":true'); + }), + ); }, CLI_INTEGRATION_TIMEOUT_MS, ); @@ -143,18 +147,24 @@ describe("CLI output boundary", () => { ["--token-count", "--json"], ]; - for (const flags of variants) { - await expect( - execFileAsync(process.execPath, [ - "scripts/rea.mjs", - ...flags, - "investigate-versions", - "/tmp/left", - "/tmp/right", - "/tmp/workspace.json", - ]), - ).rejects.toMatchObject({ code: 1, stdout: expect.any(String) }); - } + for (let index = 0; index < variants.length; index += 2) + await Promise.all( + variants.slice(index, index + 2).map(async (flags) => { + await expect( + execFileAsync(process.execPath, [ + "scripts/rea.mjs", + ...flags, + "investigate-versions", + "/tmp/left", + "/tmp/right", + "/tmp/workspace.json", + ]), + ).rejects.toMatchObject({ + code: 1, + stdout: expect.any(String), + }); + }), + ); }, CLI_INTEGRATION_TIMEOUT_MS, ); diff --git a/tests/contractSnapshot.test.ts b/tests/contractSnapshot.test.ts index ff56c53ba..0d7b68041 100644 --- a/tests/contractSnapshot.test.ts +++ b/tests/contractSnapshot.test.ts @@ -16,6 +16,25 @@ import { sessionOutputSchemas, } from "../src/contracts/toolOutputSchemas.js"; +const convertContractJsonSchema = (schema: z.ZodType) => + z.toJSONSchema(schema, { + target: "draft-07", + unrepresentable: "any", + }); + +const jsonSchemaCache = new WeakMap< + z.ZodType, + ReturnType +>(); + +const contractJsonSchema = (schema: z.ZodType) => { + const cached = jsonSchemaCache.get(schema); + if (cached !== undefined) return cached; + const converted = convertContractJsonSchema(schema); + jsonSchemaCache.set(schema, converted); + return converted; +}; + const emptySchemaPaths = (value: unknown, path = "$"): string[] => { if (Array.isArray(value)) return value.flatMap((item, index) => @@ -41,14 +60,8 @@ describe("tool contract surface", () => { ]; expect(contracts).toHaveLength(60); for (const contract of contracts) { - const inputSchema = z.toJSONSchema(contract.inputSchema, { - target: "draft-07", - unrepresentable: "any", - }); - const outputSchema = z.toJSONSchema(contract.outputSchema, { - target: "draft-07", - unrepresentable: "any", - }); + const inputSchema = contractJsonSchema(contract.inputSchema); + const outputSchema = contractJsonSchema(contract.outputSchema); expect(inputSchema.type).toBe("object"); expect(outputSchema.type).toBe("object"); expect(typeof contract.annotations.idempotentHint).toBe("boolean"); @@ -128,10 +141,7 @@ describe("tool contract surface", () => { ...ENHANCED_TOOL_CONTRACTS, ...SESSION_TOOL_CONTRACTS, ]) { - const schema = z.toJSONSchema(contract.outputSchema, { - target: "draft-07", - unrepresentable: "any", - }); + const schema = contractJsonSchema(contract.outputSchema); expect(JSON.stringify(schema)).not.toContain('"result":{}'); expect(contract.description.length).toBeGreaterThan(100); expect(contract.description).toMatch(/[.;]/u); @@ -150,10 +160,7 @@ describe("tool contract surface", () => { ]; expect(contracts).toHaveLength(78); for (const contract of contracts) { - const schema = z.toJSONSchema(contract.outputSchema, { - target: "draft-07", - unrepresentable: "any", - }); + const schema = contractJsonSchema(contract.outputSchema); expect(emptySchemaPaths(schema), contract.name).toEqual([]); } }); diff --git a/tests/enhancedTools.test.ts b/tests/enhancedTools.test.ts index 4b1a3b01f..70ab4c67c 100644 --- a/tests/enhancedTools.test.ts +++ b/tests/enhancedTools.test.ts @@ -214,12 +214,11 @@ describe("enhanced MCP tools", () => { ["analyze_function", { procedure: "0x1" }], ["trace_feature", { query: "hello", max_operations: 10 }], ] as const; - const results = []; - for (const [name, arguments_] of calls) { - results.push( + const results = await Promise.all( + calls.map(async ([name, arguments_]) => jsonResult(await client.callTool({ name, arguments: arguments_ })), - ); - } + ), + ); expect(results[0]).toMatchObject({ count: 1 }); expect(results[1]).toMatchObject({ count: 1 }); expect(results[2]).toMatchObject({ count: 1 }); diff --git a/tests/fixtures/bridgeRegexProbe.py b/tests/fixtures/bridgeRegexProbe.py index 48509dba0..d4655670b 100644 --- a/tests/fixtures/bridgeRegexProbe.py +++ b/tests/fixtures/bridgeRegexProbe.py @@ -58,5 +58,11 @@ def _probe(namespace, payload): if __name__ == "__main__": bridge = _load_bridge(sys.argv[1]) - request = json.loads(sys.argv[2]) - print(json.dumps(_probe(bridge, request), sort_keys=True)) + if len(sys.argv) >= 3: + print(json.dumps(_probe(bridge, json.loads(sys.argv[2])), sort_keys=True)) + else: + for line in sys.stdin: + if not line.strip(): + continue + request = json.loads(line) + print(json.dumps(_probe(bridge, request), sort_keys=True), flush=True) diff --git a/tests/providerArchitecture.test.ts b/tests/providerArchitecture.test.ts index 54844b7db..2d8e768b5 100644 --- a/tests/providerArchitecture.test.ts +++ b/tests/providerArchitecture.test.ts @@ -13,18 +13,26 @@ const sourceRoot = new URL("../src/", import.meta.url); describe("provider-neutral architecture", () => { it("keeps Hopper imports behind its adapter and composition root", async () => { const forbiddenRoots = ["domain", "contracts", "server", "application"]; - const violations: string[] = []; - for (const root of forbiddenRoots) { - for (const file of await typescriptFiles( - fileURLToPath(new URL(`${root}/`, sourceRoot)), - )) { - if (root === "application" && file.endsWith("/runtime.ts")) continue; - const source = await readFile(file, "utf8"); - if (/from\s+["'][^"']*\/hopper\//u.test(source)) { - violations.push(relative(fileURLToPath(sourceRoot), file)); - } - } - } + const inspected = await Promise.all( + forbiddenRoots.map(async (root) => { + const files = await typescriptFiles( + fileURLToPath(new URL(`${root}/`, sourceRoot)), + ); + return Promise.all( + files.map(async (file) => { + if (root === "application" && file.endsWith("/runtime.ts")) + return undefined; + const source = await readFile(file, "utf8"); + return /from\s+["'][^"']*\/hopper\//u.test(source) + ? relative(fileURLToPath(sourceRoot), file) + : undefined; + }), + ); + }), + ); + const violations = inspected + .flat() + .filter((violation) => violation !== undefined); expect(violations).toEqual([]); }); diff --git a/tsconfig.build.json b/tsconfig.build.json index 49b57cbfe..f58161bd5 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,7 +1,9 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "incremental": true, "rootDir": "src", + "tsBuildInfoFile": "node_modules/.cache/rea-tsconfig-build.tsbuildinfo", "types": ["node"] }, "include": ["src/**/*.ts"], diff --git a/vitest.config.ts b/vitest.config.ts index 90c93393d..f439c57c5 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,36 @@ +import { availableParallelism } from "node:os"; + import { defineConfig } from "vitest/config"; +const TEST_FILES = ["tests/**/*.test.ts"]; +const PROCESS_CAPTURE_TEST = "tests/processCapture.test.ts"; +const dualCoreProjects = + availableParallelism() === 2 + ? { + maxWorkers: 2, + projects: [ + { + test: { + name: "parallel", + include: TEST_FILES, + exclude: [PROCESS_CAPTURE_TEST], + }, + }, + { + test: { + name: "process-capture", + include: [PROCESS_CAPTURE_TEST], + fileParallelism: false, + }, + }, + ], + } + : {}; + export default defineConfig({ test: { - include: ["tests/**/*.test.ts"], + include: TEST_FILES, + ...dualCoreProjects, retry: 2, reporters: ["default", "verbose"], coverage: { From 40c6f60ac9a4b86f4cbe28a72e7692e1acf761b1 Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <1705004398@qq.com> Date: Tue, 14 Jul 2026 20:32:34 +0000 Subject: [PATCH 2/3] fix(process): restrict cleanup to captured group leaders --- src/application/ProcessCaptureLifecycle.ts | 8 ++------ src/application/ProcessHarness.ts | 21 +++++++++++---------- src/application/ProcessOwnership.ts | 17 +++++++++++++++++ tests/processOwnership.test.ts | 13 +++++++++++++ 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/application/ProcessCaptureLifecycle.ts b/src/application/ProcessCaptureLifecycle.ts index afe98cc5b..17839556e 100644 --- a/src/application/ProcessCaptureLifecycle.ts +++ b/src/application/ProcessCaptureLifecycle.ts @@ -285,7 +285,7 @@ export const releaseProcessResources = async (options: { readonly checkpoints: ProcessCheckpoints | undefined; readonly runId: string; readonly temporaryRoot: string; - readonly sampledProcessGroupIds: readonly number[]; + readonly capturedProcessGroupIds: readonly number[]; }): Promise => { for (const timer of options.timers) clearTimeout(timer); let failure: string | undefined; @@ -310,11 +310,7 @@ export const releaseProcessResources = async (options: { failure ??= "filesystem checkpoint cleanup failed"; } if (options.terminal !== undefined && process.platform !== "win32") { - const processGroupIds = new Set([ - options.terminal.pid, - ...options.sampledProcessGroupIds, - ]); - for (const processGroupId of processGroupIds) { + for (const processGroupId of new Set(options.capturedProcessGroupIds)) { const cleaned = await cleanupOwnedProcessGroup({ runId: options.runId, leaderPid: processGroupId, diff --git a/src/application/ProcessHarness.ts b/src/application/ProcessHarness.ts index 7cc48b33e..d16502f8b 100644 --- a/src/application/ProcessHarness.ts +++ b/src/application/ProcessHarness.ts @@ -25,6 +25,7 @@ import { type CommandShimReplay, } from "./CommandShimReplay.js"; import { normalizeProcessText } from "./ProcessNormalization.js"; +import { selectCapturedProcessGroupIds } from "./ProcessOwnership.js"; import { awaitTerminalExit, buildCaptureResult, @@ -157,7 +158,8 @@ const cleanupFailedStartup = async (options: { options.terminal?.kill("SIGKILL"); const cleanupFailure = await releaseProcessResources({ ...options, - sampledProcessGroupIds: [], + capturedProcessGroupIds: + options.terminal === undefined ? [] : [options.terminal.pid], }); if (cleanupFailure !== undefined) throw new ProcessCaptureError(cleanupFailure, { @@ -319,9 +321,13 @@ const finishProcessRun = async (options: { checkpoints: options.runtime?.checkpoints, runId: options.runId, temporaryRoot: options.temporaryRoot, - sampledProcessGroupIds: options.samples.flatMap(({ process_group_id }) => - process_group_id === null ? [] : [process_group_id], - ), + capturedProcessGroupIds: + options.runtime === undefined + ? [] + : selectCapturedProcessGroupIds( + options.runtime.terminal.pid, + options.samples, + ), }); let { capture, executionFailure } = options; if (capture !== undefined) { @@ -365,12 +371,7 @@ const completeCapture = async (options: { if (reason === "cancelled") throw processCaptureCancelled(); const settlement = await observeSettlement( options.runId, - [ - runtime.terminal.pid, - ...options.samples.flatMap(({ process_group_id }) => - process_group_id === null ? [] : [process_group_id], - ), - ], + selectCapturedProcessGroupIds(runtime.terminal.pid, options.samples), scenario.settle_ms, ); runtime.checkpoints.trigger("settled"); diff --git a/src/application/ProcessOwnership.ts b/src/application/ProcessOwnership.ts index 9302d0d2f..d2c1e1e21 100644 --- a/src/application/ProcessOwnership.ts +++ b/src/application/ProcessOwnership.ts @@ -1,6 +1,7 @@ import { execFile } from "node:child_process"; import { readFile } from "node:fs/promises"; import { promisify } from "node:util"; +import type { ProcessSample } from "../domain/processCapture.js"; const execFileAsync = promisify(execFile); @@ -40,6 +41,22 @@ export type ProcessGroupObservation = | { readonly state: "alive" } | { readonly state: "unverifiable"; readonly reason: string }; +/** Select the PTY root group and groups led by an observed captured process. */ +export const selectCapturedProcessGroupIds = ( + rootPid: number, + samples: readonly Pick[], +): readonly number[] => { + const processGroupIds = new Set([rootPid]); + for (const sample of samples) { + // A sampled member may transiently inherit an unrelated group. POSIX group + // leaders have pid === pgid, so only that observation establishes that the + // group leader itself belonged to the captured tree. Token checks below + // remain the final authority immediately before observation or signaling. + if (sample.pid === sample.process_group_id) processGroupIds.add(sample.pid); + } + return [...processGroupIds]; +}; + /** Parse the NUL-delimited Linux process environment without nameless keys. */ export const parseProcessEnvironment = ( value: string, diff --git a/tests/processOwnership.test.ts b/tests/processOwnership.test.ts index 09f0bd985..cae1559eb 100644 --- a/tests/processOwnership.test.ts +++ b/tests/processOwnership.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import { cleanupOwnedProcessGroup, parseProcessEnvironment, + selectCapturedProcessGroupIds, type ProcessOwnershipHost, } from "../src/application/ProcessOwnership.js"; @@ -37,6 +38,18 @@ const host = ( }; describe("owned process-group cleanup", () => { + it("excludes sampled groups whose leader was outside the captured tree", () => { + expect( + selectCapturedProcessGroupIds(100, [ + { pid: 100, process_group_id: 42 }, + { pid: 100, process_group_id: 100 }, + { pid: 101, process_group_id: 42 }, + { pid: 102, process_group_id: 102 }, + { pid: 103, process_group_id: 102 }, + ]), + ).toEqual([100, 102]); + }); + it("drops nameless Linux environment entries", () => { expect( parseProcessEnvironment("=ignored\0REA_PROCESS_RUN_ID=owned\0EMPTY=\0"), From f46fc7f1f3a265123c69d007c64c928a8974d98f Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <1705004398@qq.com> Date: Tue, 14 Jul 2026 21:33:25 +0000 Subject: [PATCH 3/3] test: serialize subprocess-heavy integrations --- tests/browserCli.test.ts | 196 +++++++++++++++++++-------------------- tests/cliOutput.test.ts | 118 +++++++++++------------ vitest.config.ts | 12 ++- 3 files changed, 157 insertions(+), 169 deletions(-) diff --git a/tests/browserCli.test.ts b/tests/browserCli.test.ts index 85338a399..36880527a 100644 --- a/tests/browserCli.test.ts +++ b/tests/browserCli.test.ts @@ -34,30 +34,10 @@ describe("browser CLI parity", () => { browser.allowedOrigin, ]), }; - const [listed, inspected] = await Promise.all([ - runCli( - ["list-browser-targets", browser.endpoint, "--approved", "--json"], - environment, - ), - runCli( - [ - "inspect-web-page", - browser.endpoint, - "allowed-page", - "--approved", - "--observation-ms", - "0", - "--include-console-text", - "--console-text-approved", - "--include-json-body-shapes", - "--json-body-schema-approved", - "--include-websocket-shapes", - "--websocket-shape-approved", - "--json", - ], - environment, - ), - ]); + const listed = await runCli( + ["list-browser-targets", browser.endpoint, "--approved", "--json"], + environment, + ); expect(listed).toMatchObject({ operation: "list_browser_targets", provider: { id: "rea-cdp-browser" }, @@ -65,6 +45,24 @@ describe("browser CLI parity", () => { targets: { items: [{ target_id: "allowed-page" }] }, }, }); + const inspected = await runCli( + [ + "inspect-web-page", + browser.endpoint, + "allowed-page", + "--approved", + "--observation-ms", + "0", + "--include-console-text", + "--console-text-approved", + "--include-json-body-shapes", + "--json-body-schema-approved", + "--include-websocket-shapes", + "--websocket-shape-approved", + "--json", + ], + environment, + ); expect(inspected).toMatchObject({ operation: "inspect_web_page", provider: { id: "rea-cdp-browser" }, @@ -81,30 +79,15 @@ describe("browser CLI parity", () => { }, }); const inspection = normalizedResult(inspected); - const [compared, analyzed] = await Promise.all([ - runCli( - [ - "compare-web-captures", - JSON.stringify({ inspection }), - JSON.stringify({ inspection }), - "--json", - ], - environment, - ), - runCli( - [ - "analyze-web-bundle", - browser.endpoint, - "allowed-page", - "--approved", - "--source-capture-approved", - "--observation-ms", - "0", - "--json", - ], - environment, - ), - ]); + const compared = await runCli( + [ + "compare-web-captures", + JSON.stringify({ inspection }), + JSON.stringify({ inspection }), + "--json", + ], + environment, + ); expect(compared).toMatchObject({ operation: "compare_web_captures", normalized_result: { @@ -114,6 +97,19 @@ describe("browser CLI parity", () => { }, }, }); + const analyzed = await runCli( + [ + "analyze-web-bundle", + browser.endpoint, + "allowed-page", + "--approved", + "--source-capture-approved", + "--observation-ms", + "0", + "--json", + ], + environment, + ); expect(analyzed).toMatchObject({ operation: "analyze_web_bundle", normalized_result: { @@ -121,32 +117,18 @@ describe("browser CLI parity", () => { observations: { source_maps: { status: "not_requested" } }, }, }); - const [observedSession, webMcp] = await Promise.all([ - runCli( - [ - "observe-web-session", - browser.endpoint, - "allowed-page", - "--approved", - "--observation-ms", - "5", - "--json", - ], - environment, - ), - runCli( - [ - "discover-webmcp-tools", - browser.endpoint, - "allowed-page", - "--approved", - "--observation-ms", - "0", - "--json", - ], - environment, - ), - ]); + const observedSession = await runCli( + [ + "observe-web-session", + browser.endpoint, + "allowed-page", + "--approved", + "--observation-ms", + "5", + "--json", + ], + environment, + ); expect(observedSession).toMatchObject({ operation: "observe_web_session", normalized_result: { @@ -156,6 +138,18 @@ describe("browser CLI parity", () => { ]), }, }); + const webMcp = await runCli( + [ + "discover-webmcp-tools", + browser.endpoint, + "allowed-page", + "--approved", + "--observation-ms", + "0", + "--json", + ], + environment, + ); expect(webMcp).toMatchObject({ operation: "discover_webmcp_tools", normalized_result: { @@ -164,34 +158,17 @@ describe("browser CLI parity", () => { }, }, }); - const [screenshot, policy] = await Promise.all([ - runCli( - [ - "capture-web-screenshot", - browser.endpoint, - "allowed-page", - "--approved", - "--screenshot-approved", - "--json", - ], - environment, - ), - runCli( - [ - "policy", - "explain", - "browser_observe", - "--origins", - browser.endpoint, - "--origins", - browser.allowedOrigin, - "--network", - "loopback", - "--json", - ], - environment, - ), - ]); + const screenshot = await runCli( + [ + "capture-web-screenshot", + browser.endpoint, + "allowed-page", + "--approved", + "--screenshot-approved", + "--json", + ], + environment, + ); const artifact = screenshotArtifact(screenshot); expect(artifact).toMatchObject({ media_type: "image/png", bytes: 70 }); const visual = await runCli( @@ -207,6 +184,21 @@ describe("browser CLI parity", () => { operation: "compare_web_screenshots", normalized_result: { status: "identical", changed_pixels: 0 }, }); + const policy = await runCli( + [ + "policy", + "explain", + "browser_observe", + "--origins", + browser.endpoint, + "--origins", + browser.allowedOrigin, + "--network", + "loopback", + "--json", + ], + environment, + ); expect(policy).toEqual({ allowed: true, grant_id: "administrator:browser_observe", diff --git a/tests/cliOutput.test.ts b/tests/cliOutput.test.ts index 1af676f01..5373a070d 100644 --- a/tests/cliOutput.test.ts +++ b/tests/cliOutput.test.ts @@ -66,35 +66,33 @@ describe("CLI output boundary", () => { it( "sanitizes a real missing-argument dispatcher failure", async () => { - await Promise.all([ - expect( - execFileAsync(process.execPath, ["scripts/rea.mjs", "analyze"], { - cwd: process.cwd(), - }), - ).rejects.toMatchObject({ - stdout: - 'code: VALIDATION_ERROR\nmessage: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n', + await expect( + execFileAsync(process.execPath, ["scripts/rea.mjs", "analyze"], { + cwd: process.cwd(), }), - expect( - execFileAsync( - process.execPath, - ["scripts/rea.mjs", "--full-output", "--json", "analyze"], - { cwd: process.cwd() }, - ), - ).rejects.toMatchObject({ - stdout: expect.not.stringContaining("fieldErrors"), - }), - expect( - execFileAsync( - process.execPath, - ["scripts/rea.mjs", "--full-output", "analyze"], - { cwd: process.cwd() }, - ), - ).rejects.toMatchObject({ - stdout: - 'ok: false\nerror:\n code: VALIDATION_ERROR\n message: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n', - }), - ]); + ).rejects.toMatchObject({ + stdout: + 'code: VALIDATION_ERROR\nmessage: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n', + }); + await expect( + execFileAsync( + process.execPath, + ["scripts/rea.mjs", "--full-output", "--json", "analyze"], + { cwd: process.cwd() }, + ), + ).rejects.toMatchObject({ + stdout: expect.not.stringContaining("fieldErrors"), + }); + await expect( + execFileAsync( + process.execPath, + ["scripts/rea.mjs", "--full-output", "analyze"], + { cwd: process.cwd() }, + ), + ).rejects.toMatchObject({ + stdout: + 'ok: false\nerror:\n code: VALIDATION_ERROR\n message: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n', + }); }, CLI_INTEGRATION_TIMEOUT_MS, ); @@ -110,24 +108,22 @@ describe("CLI output boundary", () => { await createPackageWithOptions(source, archive, { unpack: "*.js" }); await writeFile(join(`${archive}.unpacked`, "main.js"), "changed();\n"); - await Promise.all( - [["--json"], ["--full-output", "--json"]].map(async (flags) => { - const execution = execFileAsync(process.execPath, [ - "scripts/rea.mjs", - ...flags, - "inventory-artifact", - archive, - ]); - const failure = await execution.catch((cause: unknown) => cause); - expect(failure).toMatchObject({ code: 1 }); - const { stdout } = failure as { readonly stdout: string }; - const output = JSON.stringify(JSON.parse(stdout) as unknown); - expect(output).toContain('"logical_path":"main.js"'); - expect(output).toMatch(/"declared_sha256":"[a-f0-9]{64}"/u); - expect(output).toMatch(/"calculated_sha256":"[a-f0-9]{64}"/u); - expect(output).toContain('"unpacked":true'); - }), - ); + for (const flags of [["--json"], ["--full-output", "--json"]]) { + const execution = execFileAsync(process.execPath, [ + "scripts/rea.mjs", + ...flags, + "inventory-artifact", + archive, + ]); + const failure = await execution.catch((cause: unknown) => cause); + expect(failure).toMatchObject({ code: 1 }); + const { stdout } = failure as { readonly stdout: string }; + const output = JSON.stringify(JSON.parse(stdout) as unknown); + expect(output).toContain('"logical_path":"main.js"'); + expect(output).toMatch(/"declared_sha256":"[a-f0-9]{64}"/u); + expect(output).toMatch(/"calculated_sha256":"[a-f0-9]{64}"/u); + expect(output).toContain('"unpacked":true'); + } }, CLI_INTEGRATION_TIMEOUT_MS, ); @@ -147,24 +143,18 @@ describe("CLI output boundary", () => { ["--token-count", "--json"], ]; - for (let index = 0; index < variants.length; index += 2) - await Promise.all( - variants.slice(index, index + 2).map(async (flags) => { - await expect( - execFileAsync(process.execPath, [ - "scripts/rea.mjs", - ...flags, - "investigate-versions", - "/tmp/left", - "/tmp/right", - "/tmp/workspace.json", - ]), - ).rejects.toMatchObject({ - code: 1, - stdout: expect.any(String), - }); - }), - ); + for (const flags of variants) { + await expect( + execFileAsync(process.execPath, [ + "scripts/rea.mjs", + ...flags, + "investigate-versions", + "/tmp/left", + "/tmp/right", + "/tmp/workspace.json", + ]), + ).rejects.toMatchObject({ code: 1, stdout: expect.any(String) }); + } }, CLI_INTEGRATION_TIMEOUT_MS, ); diff --git a/vitest.config.ts b/vitest.config.ts index f439c57c5..7b15580eb 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,6 +4,12 @@ import { defineConfig } from "vitest/config"; const TEST_FILES = ["tests/**/*.test.ts"]; const PROCESS_CAPTURE_TEST = "tests/processCapture.test.ts"; +const SERIAL_INTEGRATION_TESTS = [ + "tests/browserCli.test.ts", + "tests/cliOutput.test.ts", + PROCESS_CAPTURE_TEST, + "tests/runtime.test.ts", +]; const dualCoreProjects = availableParallelism() === 2 ? { @@ -13,13 +19,13 @@ const dualCoreProjects = test: { name: "parallel", include: TEST_FILES, - exclude: [PROCESS_CAPTURE_TEST], + exclude: SERIAL_INTEGRATION_TESTS, }, }, { test: { - name: "process-capture", - include: [PROCESS_CAPTURE_TEST], + name: "serial-integration", + include: SERIAL_INTEGRATION_TESTS, fileParallelism: false, }, },