mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
f3ed1657f8
* 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
545 lines
17 KiB
TypeScript
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();
|
|
});
|