Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0600c7d11a | ||
|
|
87d3d6022c | ||
|
|
bc143125ed |
No files matched your search
@@ -169,6 +169,7 @@ cannot partially apply. Clients coordinating writes can supply
|
||||
| `capture.decoder_old_generation_mib` | Next owned capture decoder |
|
||||
| `javascript.analysis_timeout_ms`, `javascript.max_heap_mb` | Next source analysis / new JavaScript worker |
|
||||
| `wasm.command_timeout_ms` | Next WABT operation |
|
||||
| `native.matching_representation_bytes` | Next `match_native_functions` request |
|
||||
|
||||
### Precedence and lifetime
|
||||
|
||||
@@ -243,6 +244,8 @@ Inspection then reports the runtime value while retaining the launch error.
|
||||
Reset exposes the invalid launch state again; it does not repair or replace it
|
||||
with the built-in default. Provider availability and execution must resolve the
|
||||
same effective setting so readiness changes along with repair and reset.
|
||||
`REA_NATIVE_MATCHING_REPRESENTATION_BYTES` is validated the same way, by
|
||||
`match_native_functions` alone.
|
||||
|
||||
## Keep a fixed ceiling honest
|
||||
|
||||
|
||||
@@ -117,12 +117,23 @@ in the JSON for all groups inline. Import the bundle into MCP for views, or
|
||||
extract its matching Evidence record to a JSON file for the existing CLI
|
||||
`inspect-analysis-view` `evidence-file` source. Input bundles are unchanged.
|
||||
|
||||
Representation admission is bounded at 96 MiB before cloning selected observations
|
||||
Representation admission is bounded before cloning selected observations
|
||||
and expanding matching indexes and repeated per-function source links. Work yields to cancellation. No worker/provider
|
||||
concurrency is introduced; this budget is not a bound on total process RSS.
|
||||
The allowance defaults to 96 MiB and is the runtime setting
|
||||
`native.matching_representation_bytes`, from 1 byte to 1 GiB. On MCP, call
|
||||
`update_analysis_settings` with that setting and retry; it applies to the next
|
||||
operation without restarting REA. For CLI or future REA processes, set
|
||||
`REA_NATIVE_MATCHING_REPRESENTATION_BYTES` in the launch environment; `rea
|
||||
setup` copies it into client registrations. An invalid launch value is refused
|
||||
by matching only, and a runtime value repairs it. A refusal carries
|
||||
`details.limit` with the effective value, its source and bounds. Raising the
|
||||
allowance lets matching retain more in the REA process. The 1 GiB maximum is
|
||||
an REA policy, not a host-capacity claim, and peak heap during a request can
|
||||
exceed the allowance.
|
||||
Summary delivery happens after complete matching Evidence is constructed and
|
||||
retained. It reduces the initial reply, but does not avoid that construction or
|
||||
raise the allowance. There is no runtime override. Repeated provenance remains
|
||||
raise the allowance. Repeated provenance remains
|
||||
part of the complete member contract; splitting inventories into separate matches
|
||||
would change global name/body uniqueness and is not an equivalent workaround.
|
||||
|
||||
|
||||
@@ -112,6 +112,13 @@ creation or retry mutations. An internal GraphQL error does not establish API
|
||||
quota exhaustion or prove that query size caused the failure. Publication
|
||||
continues through the pinned release-please action.
|
||||
|
||||
A nonzero manifest version must resolve to a GitHub release or tag before
|
||||
proposal history is collected. If publication has not created that boundary
|
||||
yet, preparation stops before opening or updating a PR; wait for publication
|
||||
before retrying. Missing manifest versions and `0.0.0` still allow an initial
|
||||
release. This prevents historical `Release-As` directives from being replayed
|
||||
as unreleased changes.
|
||||
|
||||
When upgrading release-please, review the upstream manifest implementation,
|
||||
refresh the versioned patch and run the release proposal and workflow boundary
|
||||
tests. Version mismatch or a patch that neither applies cleanly nor matches an
|
||||
|
||||
@@ -105,7 +105,22 @@
|
||||
}
|
||||
--- a/build/src/manifest.js
|
||||
+++ b/build/src/manifest.js
|
||||
@@ -261,9 +261,20 @@
|
||||
@@ -248,6 +248,14 @@
|
||||
releasesFound++;
|
||||
}
|
||||
}
|
||||
+ // A recorded version requires its release boundary, even while a
|
||||
+ // concurrent publisher has not created the matching release/tag yet.
|
||||
+ for (const path of Object.keys(strategiesByPath)) {
|
||||
+ const expectedVersion = this.releasedVersions[path];
|
||||
+ if (expectedVersion && expectedVersion.toString() !== '0.0.0' && !releasesByPath[path]) {
|
||||
+ throw new Error(`Missing expected release for path ${path} at version ${expectedVersion.toString()}: no matching release or tag found. Refusing to bootstrap an already released package.`);
|
||||
+ }
|
||||
+ }
|
||||
const needsBootstrap = releasesFound < expectedReleases;
|
||||
if (releasesFound < expectedReleases) {
|
||||
this.logger.warn(`Expected ${expectedReleases} releases, only found ${releasesFound}`);
|
||||
@@ -261,9 +269,20 @@
|
||||
this.logger.info('Collecting commits since all latest releases');
|
||||
const commits = [];
|
||||
this.logger.debug(`commit search depth: ${this.commitSearchDepth}`);
|
||||
@@ -127,7 +142,7 @@
|
||||
batchSize: this.commitBatchSize,
|
||||
});
|
||||
const releaseShas = new Set(Object.values(releaseShasByPath));
|
||||
@@ -272,6 +283,7 @@
|
||||
@@ -272,6 +291,7 @@
|
||||
// sha => release pull request
|
||||
const releasePullRequestsBySha = {};
|
||||
let releaseCommitsFound = 0;
|
||||
@@ -135,7 +150,7 @@
|
||||
for await (const commit of commitGenerator) {
|
||||
if (releaseShas.has(commit.sha)) {
|
||||
if (commit.pullRequest) {
|
||||
@@ -284,14 +296,17 @@
|
||||
@@ -284,14 +304,17 @@
|
||||
}
|
||||
if (this.lastReleaseSha && this.lastReleaseSha === commit.sha) {
|
||||
this.logger.info(`Using configured lastReleaseSha ${this.lastReleaseSha} as last commit.`);
|
||||
@@ -153,7 +168,7 @@
|
||||
break;
|
||||
}
|
||||
commits.push({
|
||||
@@ -301,16 +316,18 @@
|
||||
@@ -301,16 +324,18 @@
|
||||
pullRequest: commit.pullRequest,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const SELECTED_REGISTRATION_SETTINGS = [
|
||||
"REA_HOPPER_STARTUP_TIMEOUT_MS",
|
||||
"REA_JEB_MCP_TIMEOUT_MS",
|
||||
"REA_MCP_INPUT_SCHEMA_PROFILE",
|
||||
"REA_NATIVE_MATCHING_REPRESENTATION_BYTES",
|
||||
"REA_REGEX_MATCHING_DEADLINE_MS",
|
||||
"REA_REGEX_WORKER_HEAP_REQUEST_MIB",
|
||||
"REA_MCP_INITIALIZE_BUDGET_MS",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { matchNativeFunctions } from "./NativeFunctionMatchingService.js";
|
||||
import { nativeFunctionMatchingSchema } from "../../domain/native/functionMatching.js";
|
||||
import { createEvidence } from "../../domain/evidence.js";
|
||||
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
|
||||
import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js";
|
||||
import { functionDossierSchema } from "../../domain/hopperValues.js";
|
||||
import { procedureInfoSchema } from "../../domain/native/procedureInfo.js";
|
||||
import {
|
||||
@@ -791,6 +792,69 @@ it("cooperatively preserves the exact cancellation reason and rejects expansion
|
||||
expect(large.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
});
|
||||
|
||||
it("enforces the caller-resolved representation allowance and names its setting", async () => {
|
||||
const rows = (count: number) =>
|
||||
Array.from({ length: count }, (_, i) => ({
|
||||
address: `0x${(0x1000 + i * 16).toString(16)}`,
|
||||
value: `function_${i}`,
|
||||
}));
|
||||
const small = {
|
||||
left: nativeMatchingInventory("1", rows(100)),
|
||||
right: nativeMatchingInventory("2", rows(100)),
|
||||
};
|
||||
const lowered = await matchNativeFunctions(small, {
|
||||
representationAllowance: { maximumBytes: 64 * 1024, source: "runtime" },
|
||||
});
|
||||
expect(lowered.ok).toBe(false);
|
||||
if (lowered.ok) throw new Error("Lowered allowance admitted the request");
|
||||
expect(lowered.error).toMatchObject({
|
||||
_tag: "AnalysisResourceConstraintError",
|
||||
reportedLimits: { maximum_bytes: 64 * 1024 },
|
||||
retryable: false,
|
||||
limit: {
|
||||
setting: "native.matching_representation_bytes",
|
||||
effective_value: 64 * 1024,
|
||||
source: "runtime",
|
||||
unit: "bytes",
|
||||
minimum: 1,
|
||||
maximum: 1024 * 1024 * 1024,
|
||||
applies_at: "next_operation",
|
||||
agent_editable: true,
|
||||
},
|
||||
});
|
||||
if (!(lowered.error instanceof AnalysisResourceConstraintError))
|
||||
throw lowered.error;
|
||||
expect(lowered.error.remediationAction).toContain(
|
||||
"update_analysis_settings with setting native.matching_representation_bytes",
|
||||
);
|
||||
expect(lowered.error.remediationAction).toContain(
|
||||
"REA_NATIVE_MATCHING_REPRESENTATION_BYTES",
|
||||
);
|
||||
expect((await matchNativeFunctions(small)).ok).toBe(true);
|
||||
// A request refused by the 96 MiB default fits a raised allowance unchanged.
|
||||
const large = {
|
||||
left: nativeMatchingInventory("1", rows(15_000)),
|
||||
right: nativeMatchingInventory("2", rows(15_000)),
|
||||
};
|
||||
const refused = await matchNativeFunctions(large);
|
||||
expect(refused.ok).toBe(false);
|
||||
if (!refused.ok)
|
||||
expect(refused.error).toMatchObject({
|
||||
limit: { effective_value: 96 * 1024 * 1024, source: "default" },
|
||||
});
|
||||
const raised = await matchNativeFunctions(large, {
|
||||
representationAllowance: {
|
||||
maximumBytes: 256 * 1024 * 1024,
|
||||
source: "setting",
|
||||
},
|
||||
});
|
||||
if (!raised.ok) throw raised.error;
|
||||
expect(
|
||||
nativeFunctionMatchingSchema.parse(raised.value.evidence.normalized_result)
|
||||
.summary.matched,
|
||||
).toBe(15_000);
|
||||
});
|
||||
|
||||
it("keeps duplicate candidate storage linear and emits every input occurrence exactly once", async () => {
|
||||
const entries = Array.from({ length: 1000 }, (_, i) => ({
|
||||
address: `0x${i.toString(16)}`,
|
||||
|
||||
@@ -18,9 +18,15 @@ import { err, ok, type Result } from "../../domain/result.js";
|
||||
import type { EvidenceLookup } from "../EvidenceInputResolver.js";
|
||||
import { resolveNativeMatchingSources } from "./NativeMatchingSources.js";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { NativeMatchingAllowance } from "../../config/nativeMatchingAllowance.js";
|
||||
import { FUNCTION_COMPARISON_PROVIDER } from "../../domain/comparisonEvidenceIdentity.js";
|
||||
|
||||
const OPERATION = "match_native_functions";
|
||||
|
||||
/** Effective admission budget, separate from caller-authored Evidence parameters. */
|
||||
export interface NativeFunctionMatchingOptions extends ExecutionOptions {
|
||||
readonly representationAllowance?: NativeMatchingAllowance;
|
||||
}
|
||||
export interface NativeFunctionMatchingResult {
|
||||
readonly evidence: Evidence;
|
||||
readonly sources: readonly Evidence[];
|
||||
@@ -30,10 +36,14 @@ export interface NativeFunctionMatchingResult {
|
||||
/** Pair existing observations through one source/analysis admission. */
|
||||
export const matchNativeFunctions = async (
|
||||
rawInput: unknown,
|
||||
options?: ExecutionOptions,
|
||||
options?: NativeFunctionMatchingOptions,
|
||||
lookup?: EvidenceLookup,
|
||||
): Promise<Result<NativeFunctionMatchingResult, AnalysisError>> => {
|
||||
const admission = new NativeMatchingAdmission(OPERATION, options?.signal);
|
||||
const admission = new NativeMatchingAdmission(
|
||||
OPERATION,
|
||||
options?.signal,
|
||||
options?.representationAllowance,
|
||||
);
|
||||
try {
|
||||
await setImmediate();
|
||||
await admission.checkpoint();
|
||||
@@ -171,10 +181,14 @@ export const matchNativeFunctionsFromBundles = async (
|
||||
input: MatchNativeFunctionsInput,
|
||||
paths: readonly string[],
|
||||
resources: ArtifactResourceScope,
|
||||
options?: ExecutionOptions,
|
||||
options?: NativeFunctionMatchingOptions,
|
||||
): Promise<Result<NativeFunctionMatchingResult, AnalysisError>> => {
|
||||
try {
|
||||
const admission = new NativeMatchingAdmission(OPERATION, options?.signal);
|
||||
const admission = new NativeMatchingAdmission(
|
||||
OPERATION,
|
||||
options?.signal,
|
||||
options?.representationAllowance,
|
||||
);
|
||||
await admission.admit(input);
|
||||
const loaded = await loadNativeMatchingBundleSources({
|
||||
input,
|
||||
|
||||
@@ -1,31 +1,51 @@
|
||||
import { setImmediate } from "node:timers/promises";
|
||||
import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js";
|
||||
import { fixedNativeAllowance } from "../../domain/native/nativeDeadlines.js";
|
||||
|
||||
const NATIVE_MATCHING_REPRESENTATION_BYTES = 96 * 1024 * 1024;
|
||||
import { analysisSettingLimit } from "../../domain/analysisSettingDefinitions.js";
|
||||
import {
|
||||
DEFAULT_NATIVE_MATCHING_ALLOWANCE,
|
||||
nativeMatchingAllowanceRecovery,
|
||||
nativeMatchingByteAllowance,
|
||||
type NativeMatchingAllowance,
|
||||
} from "../../config/nativeMatchingAllowance.js";
|
||||
|
||||
/** Budget the actual selected observations, before cloning or index allocation. */
|
||||
export class NativeMatchingAdmission {
|
||||
#remaining = NATIVE_MATCHING_REPRESENTATION_BYTES;
|
||||
readonly #maximum: number;
|
||||
readonly #source: NativeMatchingAllowance["source"];
|
||||
#remaining: number;
|
||||
#work = 0;
|
||||
readonly #seen = new WeakSet<object>();
|
||||
constructor(
|
||||
readonly operation: string,
|
||||
readonly signal?: AbortSignal,
|
||||
) {}
|
||||
allowance: NativeMatchingAllowance = DEFAULT_NATIVE_MATCHING_ALLOWANCE,
|
||||
) {
|
||||
// Read the accepted operation's value once, so the refusal reports the
|
||||
// budget this request actually ran under.
|
||||
this.#maximum = allowance.maximumBytes;
|
||||
this.#source = allowance.source;
|
||||
this.#remaining = this.#maximum;
|
||||
}
|
||||
readonly checkpoint = async (bytes = 0): Promise<void> => {
|
||||
this.#remaining -= bytes;
|
||||
if (this.#remaining < 0)
|
||||
throw new AnalysisResourceConstraintError(
|
||||
this.operation,
|
||||
"capacity",
|
||||
`Native function matching exceeds its ${String(NATIVE_MATCHING_REPRESENTATION_BYTES)}-byte (96 MiB) representation allowance. This release has no runtime override.`,
|
||||
{ maximum_bytes: NATIVE_MATCHING_REPRESENTATION_BYTES },
|
||||
fixedNativeAllowance(
|
||||
"The native function-matching representation allowance",
|
||||
"bytes",
|
||||
`${String(NATIVE_MATCHING_REPRESENTATION_BYTES)} bytes`,
|
||||
),
|
||||
`Native function matching exceeds its ${nativeMatchingByteAllowance(this.#maximum)} representation allowance (native.matching_representation_bytes).`,
|
||||
{ maximum_bytes: this.#maximum },
|
||||
{
|
||||
retryable: false,
|
||||
remediationAction: nativeMatchingAllowanceRecovery({
|
||||
maximumBytes: this.#maximum,
|
||||
source: this.#source,
|
||||
}),
|
||||
limit: analysisSettingLimit(
|
||||
"native.matching_representation_bytes",
|
||||
this.#maximum,
|
||||
this.#source,
|
||||
),
|
||||
},
|
||||
);
|
||||
this.signal?.throwIfAborted();
|
||||
if (++this.#work % 256 === 0)
|
||||
|
||||
+1
-1
@@ -147,7 +147,7 @@ export const createCli = (
|
||||
registerFirmwareCommands(cli, logger, environment);
|
||||
registerBinaryDiagnosticsCommands(cli, logger, environment);
|
||||
registerAnalysisViewCommands(cli, logger);
|
||||
registerNativeFunctionMatchingCommands(cli, logger);
|
||||
registerNativeFunctionMatchingCommands(cli, logger, environment);
|
||||
registerFridaCommands(cli, logger, fridaInstrumentation);
|
||||
registerEvidenceCommands(cli, logger);
|
||||
registerEnvironmentCommands(cli, logger, environment);
|
||||
|
||||
@@ -17,11 +17,13 @@ import {
|
||||
} from "./analysisEvidenceDelivery.js";
|
||||
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { parseNativeMatchingAllowance } from "../config/nativeMatchingAllowance.js";
|
||||
|
||||
/** Resolve portable input bundles, then use the same read-only matching service as MCP. */
|
||||
export const registerNativeFunctionMatchingCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.matchNativeFunctions, {
|
||||
description:
|
||||
@@ -75,6 +77,8 @@ export const registerNativeFunctionMatchingCommands = (
|
||||
};
|
||||
const invalid = cliEvidenceDeliveryInputError(delivery);
|
||||
if (invalid !== undefined) return projectAnalysisError(invalid);
|
||||
const allowance = parseNativeMatchingAllowance(environment);
|
||||
if (!allowance.ok) return projectAnalysisError(allowance.error);
|
||||
const resources = new ArtifactResourceScope();
|
||||
let sources: readonly Evidence[] = [];
|
||||
const complete = await finishAnalysisCommand(
|
||||
@@ -83,7 +87,7 @@ export const registerNativeFunctionMatchingCommands = (
|
||||
parsed.data,
|
||||
options.source_bundle,
|
||||
resources,
|
||||
{ signal },
|
||||
{ signal, representationAllowance: allowance.value },
|
||||
);
|
||||
if (!result.ok) return result;
|
||||
sources = result.value.sources;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { regexMatchingDeadlineMsSchema } from "./regexMatchingDeadline.js";
|
||||
import { regexWorkerHeapRequestMiBSchema } from "./regexWorkerHeapRequest.js";
|
||||
import { captureDecoderOldGenerationMiBSchema } from "./captureDecoderHeap.js";
|
||||
import { nativeMatchingRepresentationBytesSchema } from "./nativeMatchingAllowance.js";
|
||||
const environmentSchemas: Readonly<Record<string, z.ZodType<number, string>>> =
|
||||
{
|
||||
REA_ASAR_MAX_HEADER_BYTES: asarMaxHeaderBytesSchema,
|
||||
@@ -22,6 +23,8 @@ const environmentSchemas: Readonly<Record<string, z.ZodType<number, string>>> =
|
||||
REA_REGEX_WORKER_HEAP_REQUEST_MIB: regexWorkerHeapRequestMiBSchema,
|
||||
REA_CAPTURE_DECODER_OLD_GENERATION_MIB:
|
||||
captureDecoderOldGenerationMiBSchema,
|
||||
REA_NATIVE_MATCHING_REPRESENTATION_BYTES:
|
||||
nativeMatchingRepresentationBytesSchema,
|
||||
};
|
||||
export const launchAnalysisSetting = (
|
||||
id: AnalysisSettingId,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import { parseEnvironment } from "./environment.js";
|
||||
import { runtimeNativeMatchingAllowance } from "./runtimeAnalysisSettings.js";
|
||||
import {
|
||||
DEFAULT_NATIVE_MATCHING_ALLOWANCE,
|
||||
NATIVE_MATCHING_ALLOWANCE_SETTING,
|
||||
nativeMatchingRepresentationBytesConstraint,
|
||||
parseNativeMatchingAllowance,
|
||||
} from "./nativeMatchingAllowance.js";
|
||||
import { launchAnalysisSetting } from "./analysisSettings.js";
|
||||
import { explicitSetupRegistrationEnvironment } from "../application/SetupRegistrationEnvironment.js";
|
||||
|
||||
it("keeps the 96 MiB allowance when the launch setting is unset", () => {
|
||||
expect(parseNativeMatchingAllowance({})).toEqual({
|
||||
ok: true,
|
||||
value: DEFAULT_NATIVE_MATCHING_ALLOWANCE,
|
||||
});
|
||||
expect(DEFAULT_NATIVE_MATCHING_ALLOWANCE.maximumBytes).toBe(96 * 1024 * 1024);
|
||||
expect(
|
||||
launchAnalysisSetting("native.matching_representation_bytes", {}),
|
||||
).toEqual({
|
||||
ok: true,
|
||||
value: { value: 96 * 1024 * 1024, source: "default" },
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts decimal byte counts from 1 byte to 1 GiB as a launch setting", () => {
|
||||
for (const bytes of [1, 268_435_456, 1024 * 1024 * 1024]) {
|
||||
const environment = { [NATIVE_MATCHING_ALLOWANCE_SETTING]: String(bytes) };
|
||||
expect(parseNativeMatchingAllowance(environment)).toEqual({
|
||||
ok: true,
|
||||
value: { maximumBytes: bytes, source: "setting" },
|
||||
});
|
||||
expect(
|
||||
launchAnalysisSetting(
|
||||
"native.matching_representation_bytes",
|
||||
environment,
|
||||
),
|
||||
).toEqual({ ok: true, value: { value: bytes, source: "setting" } });
|
||||
}
|
||||
// rea setup forwards the launch seed into client registrations.
|
||||
expect(
|
||||
explicitSetupRegistrationEnvironment({
|
||||
[NATIVE_MATCHING_ALLOWANCE_SETTING]: "268435456",
|
||||
UNRELATED: "x",
|
||||
}),
|
||||
).toEqual({ [NATIVE_MATCHING_ALLOWANCE_SETTING]: "268435456" });
|
||||
});
|
||||
|
||||
it("refuses invalid launch values instead of falling back to the default", () => {
|
||||
for (const value of ["0", "-1", "01", "1.5", "96MiB", "", "1073741825"]) {
|
||||
const environment = { [NATIVE_MATCHING_ALLOWANCE_SETTING]: value };
|
||||
const parsed = parseNativeMatchingAllowance(environment);
|
||||
expect(parsed.ok).toBe(false);
|
||||
if (parsed.ok) continue;
|
||||
expect(parsed.error).toBeInstanceOf(ConfigurationError);
|
||||
expect(parsed.error.message).toBe(
|
||||
nativeMatchingRepresentationBytesConstraint,
|
||||
);
|
||||
expect(
|
||||
launchAnalysisSetting("native.matching_representation_bytes", environment)
|
||||
.ok,
|
||||
).toBe(false);
|
||||
// Operation-local: global startup parsing is unaffected, and a runtime
|
||||
// override repairs the effective allowance without editing the seed.
|
||||
expect(parseEnvironment(environment).ok).toBe(true);
|
||||
expect(runtimeNativeMatchingAllowance(environment)().ok).toBe(false);
|
||||
const overridden = runtimeNativeMatchingAllowance(environment, () => ({
|
||||
revision: 1,
|
||||
overrides: { "native.matching_representation_bytes": 4096 },
|
||||
}))();
|
||||
expect(overridden).toEqual({
|
||||
ok: true,
|
||||
value: { maximumBytes: 4096, source: "runtime" },
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
DEFAULT_NATIVE_MATCHING_REPRESENTATION_BYTES,
|
||||
MAXIMUM_NATIVE_MATCHING_REPRESENTATION_BYTES,
|
||||
} from "../domain/analysisResourceLimits.js";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
export const NATIVE_MATCHING_ALLOWANCE_SETTING =
|
||||
"REA_NATIVE_MATCHING_REPRESENTATION_BYTES";
|
||||
|
||||
/** Effective native function-matching representation allowance and its source. */
|
||||
export interface NativeMatchingAllowance {
|
||||
readonly maximumBytes: number;
|
||||
readonly source: "default" | "setting" | "runtime";
|
||||
}
|
||||
|
||||
export const DEFAULT_NATIVE_MATCHING_ALLOWANCE: NativeMatchingAllowance = {
|
||||
maximumBytes: DEFAULT_NATIVE_MATCHING_REPRESENTATION_BYTES,
|
||||
source: "default",
|
||||
};
|
||||
|
||||
export const nativeMatchingRepresentationBytesConstraint = `${NATIVE_MATCHING_ALLOWANCE_SETTING} must be a positive decimal integer of at most ${String(MAXIMUM_NATIVE_MATCHING_REPRESENTATION_BYTES)} bytes (1 GiB); the default is ${String(DEFAULT_NATIVE_MATCHING_REPRESENTATION_BYTES)} bytes (96 MiB).`;
|
||||
|
||||
/** Optional decimal byte count for the native function-matching allowance. */
|
||||
export const nativeMatchingRepresentationBytesSchema = z
|
||||
.string()
|
||||
.regex(/^[1-9]\d*$/u, {
|
||||
error: nativeMatchingRepresentationBytesConstraint,
|
||||
})
|
||||
.transform(Number)
|
||||
.pipe(
|
||||
z.number().int().max(MAXIMUM_NATIVE_MATCHING_REPRESENTATION_BYTES, {
|
||||
error: nativeMatchingRepresentationBytesConstraint,
|
||||
}),
|
||||
);
|
||||
|
||||
/** Resolve the effective allowance from one launch environment. */
|
||||
export const parseNativeMatchingAllowance = (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): Result<NativeMatchingAllowance, ConfigurationError> => {
|
||||
const value = environment[NATIVE_MATCHING_ALLOWANCE_SETTING];
|
||||
if (value === undefined) return ok(DEFAULT_NATIVE_MATCHING_ALLOWANCE);
|
||||
const parsed = nativeMatchingRepresentationBytesSchema.safeParse(value);
|
||||
return parsed.success
|
||||
? ok({ maximumBytes: parsed.data, source: "setting" })
|
||||
: err(
|
||||
new ConfigurationError(nativeMatchingRepresentationBytesConstraint, {
|
||||
settings: [
|
||||
{
|
||||
setting: NATIVE_MATCHING_ALLOWANCE_SETTING,
|
||||
constraint: nativeMatchingRepresentationBytesConstraint,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const MEBIBYTE = 1024 * 1024;
|
||||
|
||||
/** Display one allowance in MiB and bytes, or bytes alone when not whole MiB. */
|
||||
export const nativeMatchingByteAllowance = (bytes: number): string =>
|
||||
Number.isInteger(bytes / MEBIBYTE)
|
||||
? `${String(bytes / MEBIBYTE)} MiB / ${String(bytes)} bytes`
|
||||
: `${String(bytes)} bytes`;
|
||||
|
||||
/** User-visible recovery for the effective matching representation allowance. */
|
||||
export const nativeMatchingAllowanceRecovery = (
|
||||
allowance: NativeMatchingAllowance,
|
||||
): string =>
|
||||
[
|
||||
`Native function matching is limited to ${nativeMatchingByteAllowance(allowance.maximumBytes)} of representation${allowance.source === "setting" ? ` by ${NATIVE_MATCHING_ALLOWANCE_SETTING}` : ""}.`,
|
||||
`On MCP, call update_analysis_settings with setting native.matching_representation_bytes, action set, and a value in bytes up to ${nativeMatchingByteAllowance(MAXIMUM_NATIVE_MATCHING_REPRESENTATION_BYTES)} (default ${nativeMatchingByteAllowance(DEFAULT_NATIVE_MATCHING_REPRESENTATION_BYTES)}), then retry the original request. It applies to the next operation without restarting REA. For CLI or future REA processes, set ${NATIVE_MATCHING_ALLOWANCE_SETTING} in the launch environment.`,
|
||||
"This budget is not a bound on total process RSS; raising it lets matching retain more representation in the REA process.",
|
||||
"Splitting inventories into separate matches changes global name and body uniqueness, so it is not an equivalent workaround.",
|
||||
"Repeating the unchanged request fails the same way.",
|
||||
].join(" ");
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { AnalysisSettingsReader } from "../domain/analysisSettings.js";
|
||||
import type { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import { ok, type Result } from "../domain/result.js";
|
||||
import type { AppConfig } from "./types.js";
|
||||
import {
|
||||
parseAsarHeaderAdmission,
|
||||
@@ -8,6 +10,10 @@ import {
|
||||
resolveCaptureDecoderHeap,
|
||||
type CaptureDecoderHeap,
|
||||
} from "./captureDecoderHeap.js";
|
||||
import {
|
||||
parseNativeMatchingAllowance,
|
||||
type NativeMatchingAllowance,
|
||||
} from "./nativeMatchingAllowance.js";
|
||||
|
||||
/** Typed views read the enclosing operation snapshot, never the ambient environment. */
|
||||
export const runtimeAnalysisConfig = (
|
||||
@@ -111,3 +117,19 @@ export const runtimeCaptureDecoderHeap = (
|
||||
},
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Resolve the accepted operation's allowance on each call. An invalid launch
|
||||
* value is refused only by the operation it bounds, so MCP stays available
|
||||
* and a runtime override can repair it.
|
||||
*/
|
||||
export const runtimeNativeMatchingAllowance =
|
||||
(
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
read?: AnalysisSettingsReader,
|
||||
): (() => Result<NativeMatchingAllowance, ConfigurationError>) =>
|
||||
() => {
|
||||
const override = read?.().overrides["native.matching_representation_bytes"];
|
||||
return override === undefined
|
||||
? parseNativeMatchingAllowance(environment)
|
||||
: ok({ maximumBytes: override, source: "runtime" });
|
||||
};
|
||||
@@ -14,6 +14,17 @@ export const DEFAULT_ASAR_MAX_HEADER_BYTES = 16 * 1024 * 1024;
|
||||
*/
|
||||
export const MAXIMUM_ASAR_MAX_HEADER_BYTES = 256 * 1024 * 1024;
|
||||
|
||||
/** Native function-matching representation admitted when the setting is unset. */
|
||||
export const DEFAULT_NATIVE_MATCHING_REPRESENTATION_BYTES = 96 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Largest configurable matching admission. This is an REA policy ceiling, not
|
||||
* a claim about host capacity: the allowance approximates the heap
|
||||
* representation matching retains, and peak heap during a request can exceed
|
||||
* it. A lower value only refuses more requests, so the setting has no floor.
|
||||
*/
|
||||
export const MAXIMUM_NATIVE_MATCHING_REPRESENTATION_BYTES = 1024 * 1024 * 1024;
|
||||
|
||||
/** Default absolute deadline for Ghidra import, analysis, bridge, and readiness. */
|
||||
export const DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS = 330_000;
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
CAPTURE_DECODER_SEMI_SPACE_MIB,
|
||||
MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB,
|
||||
DEFAULT_JADX_OPERATION_TIMEOUT_MS,
|
||||
DEFAULT_NATIVE_MATCHING_REPRESENTATION_BYTES,
|
||||
MAXIMUM_NATIVE_MATCHING_REPRESENTATION_BYTES,
|
||||
} from "./analysisResourceLimits.js";
|
||||
import {
|
||||
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS,
|
||||
@@ -192,6 +194,16 @@ export const ANALYSIS_SETTINGS = {
|
||||
operations: ["inspect_wasm_artifact"],
|
||||
parameter: "command_timeout_ms",
|
||||
},
|
||||
"native.matching_representation_bytes": {
|
||||
environment: "REA_NATIVE_MATCHING_REPRESENTATION_BYTES",
|
||||
defaultValue: DEFAULT_NATIVE_MATCHING_REPRESENTATION_BYTES,
|
||||
minimum: 1,
|
||||
maximum: MAXIMUM_NATIVE_MATCHING_REPRESENTATION_BYTES,
|
||||
unit: "bytes",
|
||||
appliesAt: "next_operation",
|
||||
provider: "rea-function-comparison",
|
||||
operations: ["match_native_functions"],
|
||||
},
|
||||
} as const satisfies Record<AnalysisSettingId, SettingDefinition>;
|
||||
export const analysisSettingIds = Object.keys(
|
||||
ANALYSIS_SETTINGS,
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface AnalysisSettingValues {
|
||||
readonly "javascript.analysis_timeout_ms": number;
|
||||
readonly "javascript.max_heap_mb": number;
|
||||
readonly "wasm.command_timeout_ms": number;
|
||||
readonly "native.matching_representation_bytes": number;
|
||||
}
|
||||
export type AnalysisSettingId = keyof AnalysisSettingValues;
|
||||
export interface AnalysisSettingsSnapshot {
|
||||
|
||||
@@ -9,21 +9,34 @@ import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
||||
import { recordEvidenceSources } from "../application/investigation/recordEvidenceSources.js";
|
||||
import { ok } from "../domain/result.js";
|
||||
import type { Logger } from "pino";
|
||||
import { runtimeNativeMatchingAllowance } from "../config/runtimeAnalysisSettings.js";
|
||||
|
||||
/** Bind one named contract to the same retained-Evidence workflow used by CLI. */
|
||||
export const registerNativeFunctionMatchingTool = (
|
||||
server: EvidenceMcpServer,
|
||||
session: BinarySessionPort,
|
||||
logger: Logger,
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): void => {
|
||||
const contract = toolContract("match_native_functions");
|
||||
// Reads each accepted operation's settings snapshot over the launch value.
|
||||
const resolveAllowance = runtimeNativeMatchingAllowance(
|
||||
environment,
|
||||
server.analysisSettings?.read,
|
||||
);
|
||||
const evidenceById = session.evidenceForAnalysis.bind(session);
|
||||
const recordEvidence: BinarySessionPort["recordEvidence"] = (evidence) =>
|
||||
session.recordEvidence(evidence);
|
||||
const handler = createEvidenceToolHandler(
|
||||
server,
|
||||
async (input, options) => {
|
||||
const result = await matchNativeFunctions(input, options, evidenceById);
|
||||
const allowance = resolveAllowance();
|
||||
if (!allowance.ok) return allowance;
|
||||
const result = await matchNativeFunctions(
|
||||
input,
|
||||
{ ...options, representationAllowance: allowance.value },
|
||||
evidenceById,
|
||||
);
|
||||
if (!result.ok) return result;
|
||||
const recorded = recordEvidenceSources(
|
||||
recordEvidence,
|
||||
|
||||
@@ -411,7 +411,12 @@ export const registerSessionTools = (
|
||||
registerProcessComparisonTool(server, session, compareContract);
|
||||
registerArtifactComparisonTool(server, session, compareArtifactsContract);
|
||||
registerFunctionComparisonTool(server, session, compareFunctionsContract);
|
||||
registerNativeFunctionMatchingTool(server, session, logger);
|
||||
registerNativeFunctionMatchingTool(
|
||||
server,
|
||||
session,
|
||||
logger,
|
||||
options.environment,
|
||||
);
|
||||
registerBundleComparisonTool(
|
||||
server,
|
||||
session,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { join } from "node:path";
|
||||
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
|
||||
import { asarHeaderBytes } from "../fixtures/asarHeader.js";
|
||||
import { nativeMatchingNamedInventory } from "../fixtures/nativeFunctionMatching.js";
|
||||
|
||||
it("advertises exact contracts and recovers a full large-index archive over one MCP connection", async () => {
|
||||
const root = await createTestTempDirectory("rea-runtime-settings-");
|
||||
@@ -171,3 +172,117 @@ it.each(["0", "2147483648"])(
|
||||
},
|
||||
30000,
|
||||
);
|
||||
|
||||
it("lowers, restores and seeds the native matching allowance over MCP", async () => {
|
||||
const rows = Array.from({ length: 200 }, (_, i) => ({
|
||||
address: `0x${(0x1000 + i * 16).toString(16)}`,
|
||||
value: `function_${i}`,
|
||||
}));
|
||||
const request = {
|
||||
left: nativeMatchingNamedInventory("1", rows),
|
||||
right: nativeMatchingNamedInventory("2", rows),
|
||||
};
|
||||
const limitOf = (result: {
|
||||
content: readonly { type: string; text?: string }[];
|
||||
}) => {
|
||||
const text = result.content.find((item) => item.type === "text")?.text;
|
||||
if (text === undefined) throw new Error("Missing projected error");
|
||||
return JSON.parse(text).error.details.limit as unknown;
|
||||
};
|
||||
const runtime = await connectLocalToolsMcp();
|
||||
try {
|
||||
expect(
|
||||
(await runtime.call("match_native_functions", request)).isError,
|
||||
).not.toBe(true);
|
||||
await runtime.call("update_analysis_settings", {
|
||||
changes: [
|
||||
{
|
||||
setting: "native.matching_representation_bytes",
|
||||
action: "set",
|
||||
value: 4096,
|
||||
},
|
||||
],
|
||||
});
|
||||
const refused = await runtime.call("match_native_functions", request);
|
||||
expect(refused.isError).toBe(true);
|
||||
expect(limitOf(refused)).toMatchObject({
|
||||
setting: "native.matching_representation_bytes",
|
||||
effective_value: 4096,
|
||||
source: "runtime",
|
||||
agent_editable: true,
|
||||
});
|
||||
await runtime.call("update_analysis_settings", {
|
||||
changes: [
|
||||
{ setting: "native.matching_representation_bytes", action: "reset" },
|
||||
],
|
||||
});
|
||||
expect(
|
||||
(await runtime.call("match_native_functions", request)).isError,
|
||||
).not.toBe(true);
|
||||
} finally {
|
||||
await runtime.client.close();
|
||||
}
|
||||
const seeded = await connectLocalToolsMcp({
|
||||
REA_NATIVE_MATCHING_REPRESENTATION_BYTES: "4096",
|
||||
});
|
||||
try {
|
||||
expect(
|
||||
(
|
||||
await seeded.call("inspect_analysis_settings", {
|
||||
operation: "match_native_functions",
|
||||
})
|
||||
).structuredContent,
|
||||
).toMatchObject({
|
||||
settings: [
|
||||
{
|
||||
setting: "native.matching_representation_bytes",
|
||||
value: 4096,
|
||||
launch_value: 4096,
|
||||
default_value: 100663296,
|
||||
source: "setting",
|
||||
environment: "REA_NATIVE_MATCHING_REPRESENTATION_BYTES",
|
||||
},
|
||||
],
|
||||
});
|
||||
const refused = await seeded.call("match_native_functions", request);
|
||||
expect(refused.isError).toBe(true);
|
||||
expect(limitOf(refused)).toMatchObject({
|
||||
effective_value: 4096,
|
||||
source: "setting",
|
||||
});
|
||||
} finally {
|
||||
await seeded.client.close();
|
||||
}
|
||||
// An invalid seed is refused by matching alone; MCP stays available and a
|
||||
// runtime override repairs the effective allowance.
|
||||
const invalid = await connectLocalToolsMcp({
|
||||
REA_NATIVE_MATCHING_REPRESENTATION_BYTES: "0",
|
||||
});
|
||||
try {
|
||||
expect((await invalid.call("binary_session", {})).isError).not.toBe(true);
|
||||
const refused = await invalid.call("match_native_functions", request);
|
||||
expect(refused.isError).toBe(true);
|
||||
const text = refused.content.find((item) => item.type === "text");
|
||||
if (text?.type !== "text") throw new Error("Missing projected error");
|
||||
expect(JSON.parse(text.text).error).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
details: {
|
||||
settings: [{ setting: "REA_NATIVE_MATCHING_REPRESENTATION_BYTES" }],
|
||||
},
|
||||
});
|
||||
await invalid.call("update_analysis_settings", {
|
||||
changes: [
|
||||
{
|
||||
setting: "native.matching_representation_bytes",
|
||||
action: "set",
|
||||
value: 100663296,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(
|
||||
(await invalid.call("match_native_functions", request)).isError,
|
||||
).not.toBe(true);
|
||||
} finally {
|
||||
await invalid.client.close();
|
||||
}
|
||||
}, 30000);
|
||||
@@ -124,21 +124,24 @@ function historyPage(
|
||||
};
|
||||
}
|
||||
|
||||
function releasePage() {
|
||||
function releaseRecord(version: string, sha = baseline) {
|
||||
const tagName = `rea-agents-${version}`;
|
||||
return {
|
||||
name: version,
|
||||
tag: { name: tagName },
|
||||
tagCommit: { oid: sha },
|
||||
description: "",
|
||||
isDraft: false,
|
||||
url: `https://github.com/fixture/rea/releases/tag/${tagName}`,
|
||||
};
|
||||
}
|
||||
|
||||
function releasePage(releases: ReturnType<typeof releaseRecord>[]) {
|
||||
return {
|
||||
data: {
|
||||
repository: {
|
||||
releases: {
|
||||
nodes: [
|
||||
{
|
||||
name: "6.7.0",
|
||||
tag: { name: "rea-agents-6.7.0" },
|
||||
tagCommit: { oid: baseline },
|
||||
description: "",
|
||||
isDraft: false,
|
||||
url: "https://github.com/fixture/rea/releases/tag/rea-agents-6.7.0",
|
||||
},
|
||||
],
|
||||
nodes: releases,
|
||||
pageInfo: { hasNextPage: false },
|
||||
},
|
||||
},
|
||||
@@ -158,6 +161,29 @@ const defaultConfiguration = {
|
||||
},
|
||||
};
|
||||
|
||||
function releaseFileEntries(
|
||||
configuration: Record<string, unknown>,
|
||||
releasedVersions: Record<string, string>,
|
||||
) {
|
||||
const files: Record<string, unknown> = {
|
||||
"release-please-config.json": configuration,
|
||||
".release-please-manifest.json": releasedVersions,
|
||||
"package.json": {
|
||||
name: "rea-agents",
|
||||
version: releasedVersions["."] ?? "0.0.0",
|
||||
},
|
||||
"pkg/package.json": {
|
||||
name: "rea-agents",
|
||||
version: releasedVersions.pkg ?? "0.0.0",
|
||||
},
|
||||
};
|
||||
return Object.entries(files).map(([path, value], index) => ({
|
||||
path,
|
||||
sha: (index + 1).toString(16).padStart(40, "0"),
|
||||
text: JSON.stringify(value),
|
||||
}));
|
||||
}
|
||||
|
||||
async function releaseFixture({
|
||||
config = {},
|
||||
commits = [
|
||||
@@ -168,6 +194,9 @@ async function releaseFixture({
|
||||
pullRequest = false,
|
||||
historyFailures = [],
|
||||
releaseFailures = [],
|
||||
releasedVersions = { ".": "6.7.0", pkg: "6.7.0" },
|
||||
releases = [releaseRecord("6.7.0")],
|
||||
tags = [],
|
||||
}: {
|
||||
config?: Record<string, unknown>;
|
||||
commits?: CommitRecord[];
|
||||
@@ -175,6 +204,9 @@ async function releaseFixture({
|
||||
pullRequest?: boolean;
|
||||
historyFailures?: HistoryFailure[];
|
||||
releaseFailures?: HistoryFailure["errors"][];
|
||||
releasedVersions?: Record<string, string>;
|
||||
releases?: ReturnType<typeof releaseRecord>[];
|
||||
tags?: { name: string; commit: { sha: string } }[];
|
||||
} = {}) {
|
||||
const requests: { method: string; pathname: string }[] = [];
|
||||
const pages: HistoryVariables[] = [];
|
||||
@@ -182,6 +214,7 @@ async function releaseFixture({
|
||||
const failures = [...historyFailures];
|
||||
const releaseErrors = [...releaseFailures];
|
||||
const configuration = { ...defaultConfiguration, ...config };
|
||||
const entries = releaseFileEntries(configuration, releasedVersions);
|
||||
const github = await upstream.GitHub.create({
|
||||
owner: "fixture",
|
||||
repo: "rea",
|
||||
@@ -196,7 +229,7 @@ async function releaseFixture({
|
||||
method: options.method ?? "GET",
|
||||
pathname: url.pathname,
|
||||
});
|
||||
if (url.pathname === "/repos/fixture/rea/tags") return respond([]);
|
||||
if (url.pathname === "/repos/fixture/rea/tags") return respond(tags);
|
||||
if (url.pathname === "/repos/fixture/rea/releases/generate-notes") {
|
||||
return respond({ body: "Generated release notes" });
|
||||
}
|
||||
@@ -214,7 +247,7 @@ async function releaseFixture({
|
||||
if (query.query.includes("query releases(")) {
|
||||
const errors = releaseErrors.shift();
|
||||
if (errors) return respond({ errors });
|
||||
return respond(releasePage());
|
||||
return respond(releasePage(releases));
|
||||
}
|
||||
if (!query.query.includes("query pullRequestsSince(")) {
|
||||
throw new Error(`Unexpected GraphQL operation: ${query.query}`);
|
||||
@@ -234,17 +267,6 @@ async function releaseFixture({
|
||||
}
|
||||
return respond(historyPage(commits, query.variables, pullRequest));
|
||||
}
|
||||
const files: Record<string, unknown> = {
|
||||
"release-please-config.json": configuration,
|
||||
".release-please-manifest.json": { ".": "6.7.0", pkg: "6.7.0" },
|
||||
"package.json": { name: "rea-agents", version: "6.7.0" },
|
||||
"pkg/package.json": { name: "rea-agents", version: "6.7.0" },
|
||||
};
|
||||
const entries = Object.entries(files).map(([path, value], index) => ({
|
||||
path,
|
||||
sha: (index + 1).toString(16).padStart(40, "0"),
|
||||
text: JSON.stringify(value),
|
||||
}));
|
||||
if (url.pathname === "/repos/fixture/rea/git/trees/main") {
|
||||
return respond({
|
||||
truncated: false,
|
||||
@@ -503,6 +525,100 @@ it("collects beyond 500 commits and rejects an unobserved known release boundary
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects a missing manifest release before replaying a historical version override", async () => {
|
||||
const fixture = await releaseFixture({
|
||||
releasedVersions: { ".": "6.8.0" },
|
||||
releases: [releaseRecord("6.7.0", "d".repeat(40))],
|
||||
commits: [
|
||||
{ sha: latest, message: "fix: current change", files: ["src/change.ts"] },
|
||||
{
|
||||
sha: baseline,
|
||||
message: "chore: release 6.8.0",
|
||||
files: ["package.json"],
|
||||
},
|
||||
{
|
||||
sha: "c".repeat(40),
|
||||
message: "fix: historical version override\n\nRelease-As: 0.4.0",
|
||||
files: ["src/historical.ts"],
|
||||
},
|
||||
],
|
||||
});
|
||||
await expect(fixture.manifest.createPullRequests()).rejects.toThrow(
|
||||
"Missing expected release",
|
||||
);
|
||||
expect(fixture.pages).toEqual([]);
|
||||
expect(
|
||||
fixture.requests.every(
|
||||
(request) => request.method === "GET" || request.pathname === "/graphql",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("uses a tag-only manifest release and excludes older version overrides", async () => {
|
||||
const fixture = await releaseFixture({
|
||||
releasedVersions: { ".": "6.8.0" },
|
||||
releases: [],
|
||||
tags: [{ name: "rea-agents-6.8.0", commit: { sha: baseline } }],
|
||||
commits: [
|
||||
{ sha: latest, message: "fix: current change", files: ["src/change.ts"] },
|
||||
{
|
||||
sha: baseline,
|
||||
message: "chore: release 6.8.0",
|
||||
files: ["package.json"],
|
||||
},
|
||||
{
|
||||
sha: "c".repeat(40),
|
||||
message: "fix: historical version override\n\nRelease-As: 0.4.0",
|
||||
files: ["src/historical.ts"],
|
||||
},
|
||||
],
|
||||
});
|
||||
const candidates = await fixture.manifest.buildPullRequests();
|
||||
expect(candidates).toHaveLength(1);
|
||||
expect(candidates[0]?.body.toString()).toContain("current change");
|
||||
expect(candidates[0]?.body.toString()).not.toContain(
|
||||
"historical version override",
|
||||
);
|
||||
const version = candidates[0]?.updates.find(
|
||||
(update) => update.path === "version.txt",
|
||||
);
|
||||
expect(
|
||||
version?.updater.updateContent("6.8.0 // x-release-please-version"),
|
||||
).toContain("6.9.0");
|
||||
expect(
|
||||
fixture.requests.some(
|
||||
(request) => request.pathname === "/repos/fixture/rea/tags",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["omitted", "0.0.0"])(
|
||||
"preserves initial bootstrap with an %s manifest version",
|
||||
async (version) => {
|
||||
const releasedVersions: Record<string, string> =
|
||||
version === "omitted" ? {} : { ".": version };
|
||||
const fixture = await releaseFixture({
|
||||
releasedVersions,
|
||||
releases: [],
|
||||
commits: [
|
||||
{
|
||||
sha: latest,
|
||||
message: "feat: initial change",
|
||||
files: ["src/change.ts"],
|
||||
},
|
||||
],
|
||||
});
|
||||
const candidates = await fixture.manifest.buildPullRequests();
|
||||
expect(candidates).toHaveLength(1);
|
||||
const versionUpdate = candidates[0]?.updates.find(
|
||||
(update) => update.path === "version.txt",
|
||||
);
|
||||
expect(
|
||||
versionUpdate?.updater.updateContent("0.0.0 // x-release-please-version"),
|
||||
).toContain("1.0.0");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "timestamped GraphQL internal error",
|
||||
@@ -637,6 +753,7 @@ it("preserves a reached explicit last-release override", async () => {
|
||||
|
||||
it("preserves complete mixed bootstrap history with all known release boundaries", async () => {
|
||||
const fixture = await releaseFixture({
|
||||
releasedVersions: { ".": "6.7.0", pkg: "0.0.0" },
|
||||
config: {
|
||||
packages: {
|
||||
".": { "package-name": "rea-agents" },
|
||||
|
||||
Reference in new issue
Block a user