fix(settings): preserve regex cancellation and capability truth

This commit is contained in:
morluto committed 2026-11-11 20:16:24 +08:00
1 parent 40b6f1ab21
commit e8908507c8
7 files changed
+134 -68

No files matched your search

+1 -13
View File
@@ -3,7 +3,7 @@ import {
currentAnalysisOperationSnapshot,
currentAnalysisSettingOverride,
} from "../domain/analysisOperationSnapshot.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import type { ConfigurationError } from "../domain/configurationErrors.js";
import { ok, type Result } from "../domain/result.js";
import type { AppConfig } from "./types.js";
import {
@@ -165,18 +165,6 @@ export const runtimeAnalysisConfig = (
};
};
/** Capability text may describe the built-in default. The operation still admits the seed. */
export const capabilityLimit = <Value>(
read: () => Value,
fallback: Value,
): Value => {
try {
return read();
} catch (cause) {
if (cause instanceof ConfigurationError) return fallback;
throw cause;
}
};
export const runtimeAsarHeaderAdmission = (
environment: Readonly<Record<string, string | undefined>>,
read?: AnalysisSettingsReader,
+37
View File
@@ -1,3 +1,5 @@
import { AnalysisSettings } from "../application/AnalysisSettings.js";
import { runtimeAnalysisConfig } from "../config/runtimeAnalysisSettings.js";
import { describe, expect, it, vi } from "vitest";
import { parseConfig } from "../config/parseConfig.js";
@@ -409,3 +411,38 @@ describe("Hopper caller reconciliation failures", () => {
}
});
});
it("reports invalid regex configuration honestly without blocking capability discovery", () => {
const environment = { REA_REGEX_MATCHING_DEADLINE_MS: "invalid" };
const parsed = parseConfig(environment);
if (!parsed.ok) throw parsed.error;
const settings = new AnalysisSettings(environment);
const provider = new HopperProvider(
runtimeAnalysisConfig(parsed.value, settings.read),
silentLogger,
environment,
);
const limitation = () =>
provider
.capabilities()
.find(({ operation }) => operation === "search_strings")
?.limitations.join(" ");
expect(limitation()).toContain("invalid launch configuration");
expect(limitation()).not.toContain("5000 ms matching deadline");
expect(
settings.update({
changes: [
{ setting: "regex.matching_deadline_ms", action: "set", value: 3000 },
],
}).ok,
).toBe(true);
expect(settings.run(limitation)).toContain(
"3000 ms matching deadline (source runtime)",
);
expect(
settings.update({
changes: [{ setting: "regex.matching_deadline_ms", action: "reset" }],
}).ok,
).toBe(true);
expect(limitation()).toContain("invalid launch configuration");
});
+2 -13
View File
@@ -49,9 +49,6 @@ import {
reconciliationChildOptions,
} from "./HopperCallerReconciliation.js";
import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js";
import { capabilityLimit } from "../config/runtimeAnalysisSettings.js";
import { DEFAULT_REGEX_MATCHING_DEADLINE } from "../config/regexMatchingDeadline.js";
import { DEFAULT_REGEX_WORKER_HEAP_REQUEST } from "../config/regexWorkerHeapRequest.js";
import { hopperCapabilities } from "./HopperProviderCapabilities.js";
import { HOPPER_PROVIDER_IDENTITY } from "./HopperProviderMetadata.js";
@@ -94,16 +91,8 @@ export class HopperProvider implements AnalysisProviderCandidate {
capabilities(): readonly CapabilityDescriptor[] {
return hopperCapabilities(
capabilityLimit(() => {
const deadline = this.config.regexMatchingDeadline;
void deadline.deadlineMs;
return deadline;
}, DEFAULT_REGEX_MATCHING_DEADLINE),
capabilityLimit(() => {
const request = this.config.regexWorkerHeapRequest;
void request.heapMiB;
return request;
}, DEFAULT_REGEX_WORKER_HEAP_REQUEST),
this.config.regexMatchingDeadline,
this.config.regexWorkerHeapRequest,
);
}
+2 -13
View File
@@ -27,9 +27,6 @@ import type {
RetainedResourceScope,
} from "../application/AnalysisProvider.js";
import type { AppConfig } from "../config/types.js";
import { DEFAULT_REGEX_MATCHING_DEADLINE } from "../config/regexMatchingDeadline.js";
import { DEFAULT_REGEX_WORKER_HEAP_REQUEST } from "../config/regexWorkerHeapRequest.js";
import { capabilityLimit } from "../config/runtimeAnalysisSettings.js";
import type { ConfigurationError } from "../domain/configurationErrors.js";
import { idaRegistrationDigest } from "./IdaRegistrationIdentity.js";
import { isDeepStrictEqual } from "node:util";
@@ -434,16 +431,8 @@ export class IdaProvider implements AnalysisProviderCandidate {
}
#regexLimitation(): string {
return launchRegexMatchingLimitation(
capabilityLimit(() => {
const deadline = this.config.regexMatchingDeadline;
void deadline.deadlineMs;
return deadline;
}, DEFAULT_REGEX_MATCHING_DEADLINE),
capabilityLimit(() => {
const request = this.config.regexWorkerHeapRequest;
void request.heapMiB;
return request;
}, DEFAULT_REGEX_WORKER_HEAP_REQUEST),
this.config.regexMatchingDeadline,
this.config.regexWorkerHeapRequest,
);
}
#createBoundClient(
+2 -11
View File
@@ -18,16 +18,13 @@ import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { err, ok } from "../domain/result.js";
import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
import {
DEFAULT_REGEX_MATCHING_DEADLINE,
resolveRegexMatchingDeadline,
type RegexMatchingDeadline,
} from "../config/regexMatchingDeadline.js";
import {
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
resolveRegexWorkerHeapRequest,
type RegexWorkerHeapRequest,
} from "../config/regexWorkerHeapRequest.js";
import { capabilityLimit } from "../config/runtimeAnalysisSettings.js";
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import {
kunaFileCleanupError,
@@ -119,14 +116,8 @@ export class KunaProvider implements AnalysisProviderCandidate {
capabilities() {
return kunaCapabilities(
this.inspectAvailability(),
capabilityLimit(() => {
void this.#deadline.deadlineMs;
return this.#deadline;
}, DEFAULT_REGEX_MATCHING_DEADLINE),
capabilityLimit(() => {
void this.#heapRequest.heapMiB;
return this.#heapRequest;
}, DEFAULT_REGEX_WORKER_HEAP_REQUEST),
this.#deadline,
this.#heapRequest,
);
}
+59
View File
@@ -1,3 +1,7 @@
import { AnalysisSettings } from "../application/AnalysisSettings.js";
import { parseConfig } from "../config/parseConfig.js";
import { runtimeAnalysisConfig } from "../config/runtimeAnalysisSettings.js";
import { matchRegexValues } from "./RegexMatcher.js";
import { expect, it } from "vitest";
import {
@@ -481,3 +485,58 @@ it("keeps a selected heap request unless a positive Node flag overrides it", ()
);
expect(projected.details?.limit).toBeUndefined();
});
it.each([
["regex.matching_deadline_ms", "REA_REGEX_MATCHING_DEADLINE_MS", 3000],
["regex.worker_heap_request_mib", "REA_REGEX_WORKER_HEAP_REQUEST_MIB", 128],
] as const)(
"preserves pre-aborted cancellation before invalid %s and rejects the invalid seed until overridden",
async (setting, environmentName, value) => {
const environment = { [environmentName]: "invalid" };
const parsed = parseConfig(environment);
if (!parsed.ok) throw parsed.error;
const settings = new AnalysisSettings(environment);
const config = runtimeAnalysisConfig(parsed.value, settings.read);
const input = {
operation: "search_strings",
pattern: "x",
caseSensitive: true,
values: ["x"],
deadline: config.regexMatchingDeadline,
heapRequest: config.regexWorkerHeapRequest,
};
const controller = new AbortController();
const reason = { stopped: true };
controller.abort(reason);
const cancelled = await settings.run(() =>
matchRegexValues({ ...input, signal: controller.signal }),
);
expect(cancelled).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
if (!cancelled.ok) expect(cancelled.error.cause).toBe(reason);
const rejected = await settings.run(() => matchRegexValues(input));
expect(rejected).toMatchObject({
ok: false,
error: {
_tag: "ConfigurationError",
settings: [expect.objectContaining({ setting: environmentName })],
},
});
expect(
settings.update({ changes: [{ setting, action: "set", value }] }).ok,
).toBe(true);
expect(await settings.run(() => matchRegexValues(input))).toEqual({
ok: true,
value: [0],
});
expect(
settings.update({ changes: [{ setting, action: "reset" }] }).ok,
).toBe(true);
expect(await settings.run(() => matchRegexValues(input))).toMatchObject({
ok: false,
error: { _tag: "ConfigurationError" },
});
},
);
+31 -18
View File
@@ -1,3 +1,4 @@
import { ConfigurationError } from "../domain/configurationErrors.js";
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
import { readFileSync } from "node:fs";
import { Worker } from "node:worker_threads";
@@ -441,12 +442,18 @@ export const regexMatchingLimitation = (
export const launchRegexMatchingLimitation = (
deadline: RegexMatchingDeadline = DEFAULT_REGEX_MATCHING_DEADLINE,
request: RegexWorkerHeapRequest = DEFAULT_REGEX_WORKER_HEAP_REQUEST,
): string =>
regexMatchingLimitation(
resolveLaunchRegexWorkerHeap(launchExecArgv, launchNodeOptions, request),
deadline,
request,
);
): string => {
try {
return regexMatchingLimitation(
resolveLaunchRegexWorkerHeap(launchExecArgv, launchNodeOptions, request),
deadline,
request,
);
} catch (cause: unknown) {
if (!(cause instanceof ConfigurationError)) throw cause;
return "Regex mode is unavailable until its invalid launch configuration is corrected with update_analysis_settings or by restarting REA. Inspect analysis settings for the affected value.";
}
};
/**
* Capability text for the default deadline on this process's launch heap.
@@ -583,24 +590,30 @@ export const matchRegexValues = async (input: {
/** Resolved heap request. Omitted means the 128 MiB default, not ambient `process.env`. */
readonly heapRequest?: RegexWorkerHeapRequest | undefined;
}): Promise<Result<readonly number[], AnalysisError>> => {
const selectedDeadline = input.deadline ?? DEFAULT_REGEX_MATCHING_DEADLINE;
const selectedHeap = input.heapRequest ?? DEFAULT_REGEX_WORKER_HEAP_REQUEST;
// Copy the values this match accepted. A later override reset must not
// rewrite the deadline or heap reported if the match fails.
const deadline: RegexMatchingDeadline = {
deadlineMs: selectedDeadline.deadlineMs,
source: selectedDeadline.source,
};
const heapRequest: RegexWorkerHeapRequest = {
heapMiB: selectedHeap.heapMiB,
source: selectedHeap.source,
};
if (input.signal?.aborted)
return err(
new AnalysisCancelledError(input.operation, {
cause: input.signal.reason,
}),
);
const selectedDeadline = input.deadline ?? DEFAULT_REGEX_MATCHING_DEADLINE;
const selectedHeap = input.heapRequest ?? DEFAULT_REGEX_WORKER_HEAP_REQUEST;
let deadline: RegexMatchingDeadline;
let heapRequest: RegexWorkerHeapRequest;
try {
// Capture the accepted values before work begins; subsequent updates do not alter this match.
deadline = {
deadlineMs: selectedDeadline.deadlineMs,
source: selectedDeadline.source,
};
heapRequest = {
heapMiB: selectedHeap.heapMiB,
source: selectedHeap.source,
};
} catch (cause: unknown) {
if (cause instanceof ConfigurationError) return err(cause);
throw cause;
}
let worker: Worker;
try {
worker = new Worker(WORKER_SOURCE, {