Files
rea/src/process/RegexMatcher.test.ts
T
morluto f3ed1657f8 feat(mcp): support runtime analysis limit recovery (#7049)
* feat(mcp): support runtime analysis limit recovery

* refactor(config): remove unused limit re-exports

* fix(android): keep fixture release metadata dependency-free

* fix(settings): preserve provider budgets and recovery facts

* fix(tests): resolve lint plugins from their repository owner

* refactor(mcp): verify guidance delivery without frozen wording

* fix(android): align availability with runtime timeout

* test(recovery): assert limit facts instead of frozen wording

* fix(mcp): share runtime settings with the default JADX provider

* test(mcp): bound acknowledgements and release held work on failure

* fix(catalog): declare runtime settings tools as MCP-only
2026-10-25 09:45:26 +08:00

545 lines
17 KiB
TypeScript

import { expect, it } from "vitest";
import {
DEFAULT_REGEX_MATCHING_DEADLINE,
regexMatchingDeadlineRecovery,
} from "../config/regexMatchingDeadline.js";
import {
regexWorkerHeapRequestRecovery,
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
} from "../config/regexWorkerHeapRequest.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
execArgvFromCommandLine,
readLaunchExecArgv,
readLaunchNodeOptions,
regexMatchingLimitation,
regexWorkerHeapError,
resolveLaunchRegexWorkerHeap,
resolveRegexWorkerHeap,
type RegexWorkerHeapLimit,
} from "./RegexMatcher.js";
const effective = (mib: number, overridden: boolean): RegexWorkerHeapLimit => ({
kind: "effective",
mib,
overridden,
});
it.each([
{
name: "uses the 128 MiB request when no flag is set",
execArgv: [],
nodeOptions: undefined,
heap: effective(128, false),
},
{
name: "lets a smaller execArgv flag override the request",
execArgv: ["--max-old-space-size=64"],
nodeOptions: undefined,
heap: effective(64, true),
},
{
name: "lets a larger execArgv flag override the request",
execArgv: ["--require", "./preload.js", "--max_old_space_size=256"],
nodeOptions: undefined,
heap: effective(256, true),
},
{
name: "reads a positive NODE_OPTIONS flag that is absent from execArgv",
execArgv: [],
nodeOptions: "--no-warnings --max-old-space-size=192",
heap: effective(192, true),
},
{
name: "lets the last execArgv flag win over NODE_OPTIONS",
execArgv: ["--max-old-space-size=256"],
nodeOptions: "--max-old-space-size=96",
heap: effective(256, true),
},
{
name: "treats a final zero flag as leaving the 128 MiB request in effect",
execArgv: ["--max-old-space-size=0"],
nodeOptions: "--max-old-space-size=256",
heap: effective(128, false),
},
{
name: "accepts quoted NODE_OPTIONS and a leading plus or zeros",
execArgv: ["--max-old-space-size=+0256"],
nodeOptions: '"--max-old-space-size=144"',
heap: effective(256, true),
},
{
name: "uses the last positive flag",
execArgv: ["--max-old-space-size=256", "--max-old-space-size=96"],
nodeOptions: undefined,
heap: effective(96, true),
},
{
name: "leaves the effective heap unknown for an unparsed flag",
execArgv: ["--max-old-space-size=256.5"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "keeps a later positive flag after an unparsed one",
execArgv: ["--max-old-space-size=256.5", "--max-old-space-size=80"],
nodeOptions: undefined,
heap: effective(80, true),
},
{
name: "leaves an untokenizable NODE_OPTIONS value unknown until execArgv decides",
execArgv: [],
nodeOptions: '"--max-old-space-size=256',
heap: { kind: "unknown" } as const,
},
{
name: "lets execArgv decide when NODE_OPTIONS cannot be tokenized",
execArgv: ["--max-old-space-size=80"],
nodeOptions: '"--max-old-space-size=256',
heap: effective(80, true),
},
{
name: "leaves a percentage flag unestablished instead of the 128 MiB request",
execArgv: ["--max-old-space-size-percentage=50"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "lets a percentage flag win over a later absolute flag",
execArgv: ["--max-old-space-size-percentage=50", "--max-old-space-size=64"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "lets a percentage flag win over an earlier absolute flag",
execArgv: ["--max-old-space-size=64", "--max-old-space-size-percentage=50"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "lets a NODE_OPTIONS percentage win over an execArgv absolute flag",
execArgv: ["--max-old-space-size=64"],
nodeOptions: "--max-old-space-size-percentage=50",
heap: { kind: "unknown" } as const,
},
{
name: "lets an execArgv percentage win over a NODE_OPTIONS absolute flag",
execArgv: ["--max-old-space-size-percentage=50"],
nodeOptions: "--max-old-space-size=64",
heap: { kind: "unknown" } as const,
},
{
name: "recognizes an underscore percentage flag",
execArgv: ["--max_old_space_size_percentage=50"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "recognizes a percentage flag split from its value",
execArgv: [
"--max-old-space-size=64",
"--max-old-space-size-percentage",
"50",
],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "does not let a later absolute flag replace an invalid percentage",
execArgv: ["--max-old-space-size-percentage=0", "--max-old-space-size=64"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "leaves a fractional percentage unestablished",
execArgv: ["--max-old-space-size-percentage=50.5"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "does not treat a final zero absolute flag as 128 MiB after a percentage",
execArgv: ["--max-old-space-size-percentage=50", "--max-old-space-size=0"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "leaves an env-file launch unestablished when NODE_OPTIONS was absent",
execArgv: ["--env-file=rea.env"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "leaves an env-file-if-exists launch unestablished when NODE_OPTIONS was absent",
execArgv: ["--env-file-if-exists=.env"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "does not let an absolute flag hide NODE_OPTIONS supplied by an env file",
execArgv: ["--env-file=rea.env", "--max-old-space-size=64"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "recognizes an env-file flag split from its path",
execArgv: ["--env-file", "rea.env"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "keeps an inherited NODE_OPTIONS value when an env file cannot override it",
execArgv: ["--env-file=rea.env"],
nodeOptions: "--max-old-space-size=64",
heap: effective(64, true),
},
{
name: "lets a later absolute flag win when an inherited NODE_OPTIONS blocks the env file",
execArgv: ["--max-old-space-size=32", "--env-file-if-exists=rea.env"],
nodeOptions: "--max-old-space-size=64",
heap: effective(32, true),
},
{
name: "keeps the 128 MiB request when an empty NODE_OPTIONS blocks the env file",
execArgv: ["--env-file=rea.env"],
nodeOptions: "",
heap: effective(128, false),
},
{
name: "leaves a config-file percentage source unestablished",
execArgv: ["--experimental-config-file=node.config.json"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "leaves a default config-file launch unestablished",
execArgv: ["--experimental-default-config-file"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "recognizes a config-file flag split from its path",
execArgv: ["--experimental-config-file", "node.config.json"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "does not let an absolute flag hide a config-file heap option",
execArgv: [
"--experimental-config-file=node.config.json",
"--max-old-space-size=64",
],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "does not let a recovered NODE_OPTIONS hide a config-file heap option",
execArgv: ["--experimental-default-config-file"],
nodeOptions: "--max-old-space-size=64",
heap: { kind: "unknown" } as const,
},
{
name: "leaves a config-file flag in recovered NODE_OPTIONS unestablished",
execArgv: ["--max-old-space-size=256"],
nodeOptions: "--experimental-config-file=node.config.json",
heap: { kind: "unknown" } as const,
},
{
name: "leaves a positive --max-heap-size unestablished instead of the 128 MiB request",
execArgv: ["--max-heap-size=64"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "recognizes an underscore --max-heap-size",
execArgv: ["--max_heap_size=+064"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "leaves a NODE_OPTIONS --max-heap-size unestablished",
execArgv: [],
nodeOptions: "--max-heap-size=64",
heap: { kind: "unknown" } as const,
},
{
name: "does not let an old-space flag hide a positive --max-heap-size",
execArgv: ["--max-heap-size=64", "--max-old-space-size=256"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "does not let a later --max-heap-size hide an old-space flag",
execArgv: ["--max-old-space-size=64"],
nodeOptions: "--max_heap_size=256",
heap: { kind: "unknown" } as const,
},
{
name: "lets a final zero --max-heap-size clear the total-heap cap",
execArgv: ["--max-heap-size=64", "--max-heap-size=0"],
nodeOptions: undefined,
heap: effective(128, false),
},
{
name: "keeps an old-space flag when a final --max-heap-size is zero",
execArgv: ["--max-old-space-size=256", "--max-heap-size="],
nodeOptions: undefined,
heap: effective(256, true),
},
{
name: "leaves an unparsed --max-heap-size unestablished",
execArgv: ["--max-heap-size=64.5", "--max-old-space-size=80"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
{
name: "does not let a later zero clear an unparsed --max-heap-size",
execArgv: ["--max-heap-size=64.5", "--max-heap-size=0"],
nodeOptions: undefined,
heap: { kind: "unknown" } as const,
},
] as const)(
"resolveRegexWorkerHeap $name",
({ execArgv, nodeOptions, heap }) => {
expect(resolveRegexWorkerHeap(execArgv, nodeOptions)).toEqual(heap);
},
);
it("does not treat a rewritten NODE_OPTIONS value as the launch heap", () => {
const previous = process.env.NODE_OPTIONS;
const rewritten = "--max-old-space-size=61-rewritten";
process.env.NODE_OPTIONS = rewritten;
try {
const launch = readLaunchNodeOptions();
if (process.platform === "linux") {
expect(launch.kind).toBe("recovered");
if (launch.kind === "recovered")
expect(launch.value ?? "").not.toContain("61-rewritten");
} else {
expect(launch).toEqual({ kind: "unrecoverable" });
}
expect(
resolveLaunchRegexWorkerHeap(
{ kind: "recovered", value: ["--max-old-space-size=256"] },
{ kind: "unrecoverable" },
),
).toEqual({ kind: "unknown" });
expect(
resolveLaunchRegexWorkerHeap(
{ kind: "recovered", value: [] },
{ kind: "recovered", value: "--max_heap_size=64" },
),
).toEqual({ kind: "unknown" });
expect(
resolveLaunchRegexWorkerHeap(
{ kind: "unrecoverable" },
{ kind: "recovered", value: "--max-old-space-size=64" },
),
).toEqual({ kind: "unknown" });
if (launch.kind === "recovered") {
expect(launch.value).not.toBe(rewritten);
expect(
resolveLaunchRegexWorkerHeap(
{ kind: "recovered", value: ["--max-old-space-size=32"] },
launch,
),
).toEqual(
resolveRegexWorkerHeap(["--max-old-space-size=32"], launch.value),
);
}
} finally {
if (previous === undefined) delete process.env.NODE_OPTIONS;
else process.env.NODE_OPTIONS = previous;
}
});
const commandLine = (...tokens: readonly string[]): Buffer =>
Buffer.from(`${tokens.join("\0")}\0`);
it("recovers launch exec argv from the command line, not process.execArgv", () => {
expect(
execArgvFromCommandLine(
commandLine(
"node",
"--max-old-space-size=64",
"script.js",
"--max-old-space-size=256",
),
),
).toEqual({ kind: "recovered", value: ["--max-old-space-size=64"] });
expect(
resolveLaunchRegexWorkerHeap(
execArgvFromCommandLine(
commandLine("node", "--max-old-space-size=64", "script.js"),
),
{ kind: "recovered", value: undefined },
),
).toEqual({ kind: "effective", mib: 64, overridden: true });
expect(resolveRegexWorkerHeap([], undefined)).toEqual({
kind: "effective",
mib: 128,
overridden: false,
});
expect(
execArgvFromCommandLine(
commandLine(
"node",
"--require",
"preload.cjs",
"--max_old_space_size=64",
"script.js",
),
),
).toEqual({
kind: "recovered",
value: ["--require", "preload.cjs", "--max_old_space_size=64"],
});
expect(
execArgvFromCommandLine(
commandLine("node", "-e", "code", "--max-old-space-size=80"),
),
).toEqual({
kind: "recovered",
value: ["-e", "code", "--max-old-space-size=80"],
});
expect(
execArgvFromCommandLine(
commandLine("node", "--", "--max-old-space-size=64", "script.js"),
),
).toEqual({ kind: "recovered", value: [] });
expect(
execArgvFromCommandLine(
commandLine("node", "--not-a-real-node-option", "script.js"),
),
).toEqual({ kind: "unrecoverable" });
expect(execArgvFromCommandLine(Buffer.alloc(0))).toEqual({
kind: "unrecoverable",
});
expect(execArgvFromCommandLine(Buffer.from([0xff, 0]))).toEqual({
kind: "unrecoverable",
});
expect(execArgvFromCommandLine(commandLine("rea"))).toEqual({
kind: "unrecoverable",
});
expect(
execArgvFromCommandLine(
Buffer.concat([Buffer.from("rea\0"), Buffer.alloc(32)]),
),
).toEqual({ kind: "unrecoverable" });
expect(
resolveLaunchRegexWorkerHeap(
execArgvFromCommandLine(
Buffer.concat([Buffer.from("rea\0"), Buffer.alloc(32)]),
),
{ kind: "recovered", value: undefined },
),
).toEqual({ kind: "unknown" });
expect(execArgvFromCommandLine(commandLine("node", "script.js"))).toEqual({
kind: "recovered",
value: [],
});
const before = readLaunchExecArgv();
const snapshot = [...process.execArgv];
process.execArgv.length = 0;
try {
expect(process.execArgv).toEqual([]);
expect(readLaunchExecArgv()).toEqual(before);
if (before.kind === "recovered") expect(before.value).toEqual(snapshot);
else expect(before).toEqual({ kind: "unrecoverable" });
} finally {
process.execArgv.push(...snapshot);
}
});
it("describes the effective heap and the configurable matching deadline", () => {
for (const heap of [
effective(128, false),
effective(256, true),
{ kind: "unknown" } as const,
]) {
const limitation = regexMatchingLimitation(heap);
expect(limitation).toContain(
regexMatchingDeadlineRecovery(DEFAULT_REGEX_MATCHING_DEADLINE),
);
expect(limitation).toContain(
regexWorkerHeapRequestRecovery(DEFAULT_REGEX_WORKER_HEAP_REQUEST),
);
const projected = projectAnalysisError(
regexWorkerHeapError("search_procedures", new Error("heap"), heap),
);
if (heap.kind === "effective" && !heap.overridden)
expect(projected.details?.limit).toMatchObject({
setting: "regex.worker_heap_request_mib",
effective_value: 128,
source: "default",
});
else expect(projected.details?.limit).toBeUndefined();
expect(projected.remediation.action).toContain("--max-old-space-size");
expect(projected.remediation.action).toContain(
"--max-old-space-size-percentage",
);
if (heap.kind === "unknown") {
expect(limitation).toContain("--max-old-space-size-percentage");
expect(limitation).toContain("--max-heap-size");
expect(limitation).toContain("--experimental-config-file");
expect(projected.remediation.action).toContain("--max-heap-size");
expect(projected.remediation.action).toContain(
"--experimental-default-config-file",
);
expect(projected.details?.reported_limits).toEqual({
requested_worker_heap_mib: 128,
requested_worker_heap_source: "default",
});
} else {
expect(projected.details).toMatchObject({
reported_limits: {
worker_heap_mib: heap.mib,
requested_worker_heap_mib: 128,
requested_worker_heap_source: "default",
},
});
}
}
});
it("keeps a selected heap request unless a positive Node flag overrides it", () => {
const request = { heapMiB: 256, source: "setting" as const };
expect(resolveRegexWorkerHeap([], undefined, request)).toEqual(
effective(256, false),
);
expect(
resolveRegexWorkerHeap(["--max-old-space-size=64"], undefined, request),
).toEqual(effective(64, true));
const selected = regexMatchingLimitation(
effective(256, false),
DEFAULT_REGEX_MATCHING_DEADLINE,
request,
);
expect(selected).toContain(regexWorkerHeapRequestRecovery(request));
const overridden = regexMatchingLimitation(
effective(64, true),
DEFAULT_REGEX_MATCHING_DEADLINE,
request,
);
expect(overridden).toContain(regexWorkerHeapRequestRecovery(request));
const projected = projectAnalysisError(
regexWorkerHeapError(
"search_procedures",
new Error("heap"),
effective(64, true),
request,
),
);
expect(projected.details).toMatchObject({
reported_limits: {
worker_heap_mib: 64,
requested_worker_heap_mib: 256,
requested_worker_heap_source: "setting",
},
});
expect(projected.remediation.action).toContain(
regexWorkerHeapRequestRecovery(request),
);
expect(projected.details?.limit).toBeUndefined();
});