Merge branch 'main' into fix/rust-axum-route-extraction-1504

This commit is contained in:
Gergő Magyar 2026-05-08 09:13:19 +01:00 committed by GitHub
commit 02db84b4cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 482 additions and 18 deletions

View file

@ -91,6 +91,25 @@ function clampCrossDepth(raw: unknown): { depth: number; warning?: string } {
return { depth: d };
}
/**
* Clamp the impact timeout to a sane bounded range. Callers can feed this
* via tool params, so an unclamped value lets a single request hold a
* timer slot for an arbitrarily long duration (CodeQL js/resource-
* exhaustion). 100ms lower bound preserves test-suite scenarios that
* exercise tight timeouts; 5min upper bound is well above any legitimate
* single-impact compute. Applied at the validate boundary so the
* downstream `deadline` (Date.now() + timeoutMs) and the local-leg
* `setTimeout` see the same clamped value earlier shapes had a 1hr
* outer cap and a 5min inner clamp that disagreed.
*/
export const IMPACT_TIMEOUT_MIN_MS = 100;
export const IMPACT_TIMEOUT_MAX_MS = 5 * 60 * 1_000;
export function clampTimeout(timeoutMs: number): number {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return IMPACT_TIMEOUT_MIN_MS;
return Math.min(IMPACT_TIMEOUT_MAX_MS, Math.max(IMPACT_TIMEOUT_MIN_MS, Math.trunc(timeoutMs)));
}
export function validateGroupImpactParams(params: Record<string, unknown>):
| {
ok: true;
@ -143,13 +162,19 @@ export function validateGroupImpactParams(params: Record<string, unknown>):
const service = normalizeServicePrefix(params.service);
const subgroup = typeof params.subgroup === 'string' ? params.subgroup : undefined;
let timeoutMs =
// Clamp at the validate boundary so the downstream `deadline` (line
// ~366) and `safeLocalImpact`'s `setTimeout` both see a single
// bounded value. Without this, the outer deadline budgeted Phase-2
// cross-repo fanout up to 1hr while only the inner setTimeout was
// capped to 5min — the two halves of CodeQL #184's mitigation
// disagreed.
const rawTimeoutMs =
typeof params.timeoutMs === 'number' && params.timeoutMs > 0
? params.timeoutMs
: typeof params.timeout === 'number' && params.timeout > 0
? params.timeout
: DEFAULT_LOCAL_IMPACT_TIMEOUT_MS;
if (timeoutMs > 3_600_000) timeoutMs = 3_600_000;
const timeoutMs = clampTimeout(rawTimeoutMs);
return {
ok: true,
@ -191,12 +216,13 @@ async function safeLocalImpact(
impactParams: Parameters<GroupToolPort['impact']>[1],
timeoutMs: number,
): Promise<{ value: unknown; timedOut: boolean }> {
const safeTimeoutMs = clampTimeout(timeoutMs);
let timer: ReturnType<typeof setTimeout> | undefined;
const impactP = port.impact(repo, impactParams).catch((err) => ({
error: err instanceof Error ? err.message : String(err),
}));
const timeoutP = new Promise<'timeout'>((resolve) => {
timer = setTimeout(() => resolve('timeout'), timeoutMs);
timer = setTimeout(() => resolve('timeout'), safeTimeoutMs);
});
const won = await Promise.race([
impactP.then((v) => ({ tag: 'impact' as const, v })),
@ -212,6 +238,65 @@ async function safeLocalImpact(
return { value: won.v, timedOut: false };
}
/**
* Race a single Phase-2 `impactByUid` call against a remaining-budget
* timer. The Codex adversarial review on PR #1331 surfaced that the
* fanout loop only checked `Date.now() > deadline` *between* neighbor
* calls once `await port.impactByUid(...)` was reached, a hung
* neighbor could pin the request indefinitely, and slow neighbors
* could compound past the 5-min `IMPACT_TIMEOUT_MAX_MS` cap.
*
* This helper wraps each call: a `setTimeout(remainingMs)` aborts an
* `AbortController` whose signal is forwarded to `impactByUid`, and a
* `Promise.race` resolves to `{ timedOut: true }` when the timer
* fires before the call completes. Implementors that ignore the
* signal (current local backend) still see their await resolved by
* the race; full cooperative cancellation inside the BFS is a future
* follow-up. On rejection, the value is `null` (matching the
* fanout's existing `if (fan == null)` truncation contract).
*
* Exported for direct unit testing the helper IS the load-bearing
* mitigation surface, so the U3 regression test pins it directly
* rather than driving the full `runGroupImpact` path.
*/
export async function safeNeighborImpact(
port: GroupToolPort,
repoId: string,
uid: string,
direction: string,
opts: {
maxDepth: number;
relationTypes: string[];
minConfidence: number;
includeTests: boolean;
},
remainingMs: number,
): Promise<{ value: unknown; timedOut: boolean }> {
const controller = new AbortController();
let timer: ReturnType<typeof setTimeout> | undefined;
const callP = port
.impactByUid(repoId, uid, direction, { ...opts, signal: controller.signal })
.catch(() => null);
const timeoutP = new Promise<'timeout'>((resolve) => {
timer = setTimeout(
() => {
controller.abort();
resolve('timeout');
},
Math.max(0, remainingMs),
);
});
const won = await Promise.race([
callP.then((v) => ({ tag: 'impact' as const, v })),
timeoutP.then(() => ({ tag: 'timeout' as const })),
]);
if (timer !== undefined) clearTimeout(timer);
if (won.tag === 'timeout') {
return { value: null, timedOut: true };
}
return { value: won.v, timedOut: false };
}
export function collectImpactSymbolUids(
local: unknown,
servicePrefix: string | undefined,
@ -476,7 +561,8 @@ export async function runGroupImpact(
if (seen.has(key)) continue;
seen.add(key);
if (Date.now() > deadline) {
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) {
truncatedRepos.push(n.neighborRepo);
continue;
}
@ -492,13 +578,25 @@ export async function runGroupImpact(
continue;
}
const fan = await deps.port.impactByUid(neighborHandle.id, n.neighborUid, direction, {
maxDepth,
relationTypes: relationTypes ?? [],
minConfidence,
includeTests,
});
if (fan == null) {
// Phase-2 hardening: race each impactByUid against a per-call
// timeout derived from the remaining budget. Without this wrap a
// single hung neighbor would pin the request past the clamped
// timeout, which Codex's adversarial review on PR #1331 flagged
// as the still-open half of CodeQL #184 / js/resource-exhaustion.
const { value: fan, timedOut: neighborTimedOut } = await safeNeighborImpact(
deps.port,
neighborHandle.id,
n.neighborUid,
direction,
{
maxDepth,
relationTypes: relationTypes ?? [],
minConfidence,
includeTests,
},
remainingMs,
);
if (neighborTimedOut || fan == null) {
truncatedRepos.push(n.neighborRepo);
continue;
}

View file

@ -31,6 +31,32 @@ interface ImportedSymbol {
filePath: string;
}
/**
* Linear-time `[package].name = "..."` lookup. The previous regex
* `^\[package\]\s*\n(?:[^\[]*?\n)*?name\s*=\s*"([^"]+)"` had a nested
* lazy quantifier on `\n` that CodeQL js/redos flagged as exponential
* on inputs like `[package]\n` + many bare `\n`. We walk lines
* explicitly: scan from the first `[package]` header until we hit the
* next `[...]` section header, looking for the `name = "..."` line.
* O(n) with the line count.
*
* Exported so the U8 ReDoS regression test can drive the production
* line-walk directly with adversarial fixtures (multi-line strings,
* trailing sections, etc.) instead of duplicating it inline.
*/
export function parseCargoPackageName(content: string): string | null {
const lines = content.split('\n');
const packageStart = lines.findIndex((l) => l.trim() === '[package]');
if (packageStart < 0) return null;
for (let i = packageStart + 1; i < lines.length; i++) {
const line = lines[i].trimStart();
if (line.startsWith('[')) break; // hit the next section header
const m = /^name\s*=\s*"([^"]+)"/.exec(line);
if (m) return m[1];
}
return null;
}
/**
* Parse a Cargo.toml to extract the crate name and workspace dependency
* names. Uses simple line-based parsing no TOML library needed for
@ -47,12 +73,9 @@ async function parseCrateManifest(
return null;
}
let name = '';
const name = parseCargoPackageName(content) ?? '';
const workspaceDeps: string[] = [];
const nameMatch = content.match(/^\[package\]\s*\n(?:[^\[]*?\n)*?name\s*=\s*"([^"]+)"/m);
if (nameMatch) name = nameMatch[1];
// Match dependencies that use workspace = true, which indicates they
// are workspace-internal deps:
// dep_name = { workspace = true }

View file

@ -65,6 +65,15 @@ export interface GroupToolPort {
relationTypes: string[];
minConfidence: number;
includeTests: boolean;
// Optional cancellation signal. Callers (notably the cross-impact
// Phase-2 fanout) wrap this call in a Promise.race against a
// setTimeout-driven AbortController so a single hung neighbor
// cannot exceed the request's clamped timeout budget. Implementors
// may honor the signal cooperatively or simply let the caller's
// race resolve the await — the latter is sufficient for the
// resource-exhaustion mitigation. When the signal is absent or
// already aborted at call time, behavior is unchanged.
signal?: AbortSignal;
},
): Promise<unknown | null>;
context(

View file

@ -369,9 +369,20 @@ const RE_USE_AFTER =
/\bUSE\s+(?:AFTER\s+)?(?:STANDARD\s+)?(?:EXCEPTION|ERROR)\s+ON\s+([A-Z][A-Z0-9-]+|INPUT|OUTPUT|I-O|EXTEND)\b/i;
// SET statement (condition, index)
const RE_SET_TO_TRUE = /\bSET\s+((?:[A-Z][A-Z0-9-]+(?:\s+OF\s+[A-Z][A-Z0-9-]+)?\s+)+)TO\s+TRUE\b/i;
const RE_SET_INDEX =
/\bSET\s+((?:[A-Z][A-Z0-9-]+\s+)+)(TO|UP\s+BY|DOWN\s+BY)\s+(\d+|[A-Z][A-Z0-9-]+)/i;
//
// Catastrophic-backtracking note (CodeQL js/redos): the previous shape
// `((?:[A-Z][A-Z0-9-]+(?:\s+OF\s+[A-Z][A-Z0-9-]+)?\s+)+)TO\s+TRUE`
// nested `\s+` quantifiers across alternations and was exponential on
// inputs like "SET a OF a OF a ... TO TRUE". Replaced with a lazy
// dot-match bounded by the explicit `\s+TO\s+TRUE` suffix — `.+?` is
// O(n) with the trailing anchor, and the captured group is parsed
// downstream the same way as before.
// Exported so the U8 ReDoS regression test can pin the exact production
// pattern. Direct import is the only way to ensure the test's
// pathological-input timing assertion exercises the production regex
// instead of an inline copy that drifts.
export const RE_SET_TO_TRUE = /\bSET\s+(.+?)\s+TO\s+TRUE\b/i;
export const RE_SET_INDEX = /\bSET\s+(.+?)\s+(TO|UP\s+BY|DOWN\s+BY)\s+(\d+|[A-Z][A-Z0-9-]+)/i;
// INITIALIZE statement — data reset (captures targets before REPLACING/WITH clause)
const RE_INITIALIZE = /\bINITIALIZE\s+([\s\S]*?)(?=\bREPLACING\b|\bWITH\b|\.\s*$|$)/i;

View file

@ -2984,8 +2984,14 @@ export class LocalBackend {
relationTypes: string[];
minConfidence: number;
includeTests: boolean;
signal?: AbortSignal;
},
): Promise<any | null> {
// Honor an already-aborted signal at the entry boundary as a fast
// path. Cooperative cancellation inside _runImpactBFS is out of
// scope — the caller's Promise.race against the same signal
// resolves the await regardless of how long this body runs.
if (opts.signal?.aborted) return null;
try {
await this.refreshRepos();
await this.ensureInitialized(repoId);

View file

@ -0,0 +1,121 @@
/**
* Phase-2 fanout timeout regression test.
*
* Codex adversarial review on PR #1331 surfaced that `validateGroupImpactParams`
* clamps `timeoutMs` and `safeLocalImpact` enforces it on the local leg, but
* the Phase-2 cross-repo fanout (`cross-impact.ts:521-526`) awaits each
* `port.impactByUid(...)` call without a per-call timeout. A single hung
* neighbor pins the request indefinitely; multiple slow neighbors compound
* past the clamped budget because each starts before `Date.now() > deadline`.
*
* This test pins the contract of the mitigation: a `safeNeighborImpact`
* helper that races `port.impactByUid` against a remaining-budget timer
* and returns `{ value: null, timedOut: true }` when the call cannot
* complete in time.
*
* Direct import + named symbol so this is a real regression net no
* `??`-fallback or dynamic-import dance (the U8 false-green pattern).
*/
import { describe, expect, it } from 'vitest';
import { safeNeighborImpact } from '../../../src/core/group/cross-impact.js';
import type { GroupToolPort } from '../../../src/core/group/service.js';
const minimalOpts = {
maxDepth: 3,
relationTypes: [] as string[],
minConfidence: 0,
includeTests: false,
};
function makePort(impactByUid: GroupToolPort['impactByUid']): GroupToolPort {
return {
resolveRepo: async () => {
throw new Error('not used');
},
impact: async () => {
throw new Error('not used');
},
query: async () => {
throw new Error('not used');
},
context: async () => {
throw new Error('not used');
},
impactByUid,
};
}
describe('safeNeighborImpact — Phase-2 fanout per-call timeout', () => {
it('returns timedOut=true when impactByUid never resolves, within ~remainingMs', async () => {
// Hung neighbor: the promise never resolves. Without the timeout wrap
// this would hang the test runner.
const port = makePort(() => new Promise(() => {}));
const start = performance.now();
const result = await safeNeighborImpact(port, 'repo-id', 'uid:1', 'upstream', minimalOpts, 150);
const elapsedMs = performance.now() - start;
expect(result.timedOut).toBe(true);
expect(result.value).toBeNull();
// Allow generous slack for slow CI; the contract is "bounded", not
// "exactly remainingMs". A regression that drops the timeout entirely
// would hang far past 1500ms; a regression that uses the wrong unit
// (seconds vs ms) would fire much faster.
expect(elapsedMs).toBeGreaterThanOrEqual(140);
expect(elapsedMs).toBeLessThan(1500);
});
it('returns the resolved value and timedOut=false on a fast happy path', async () => {
const fakeFan = { byDepth: { 1: [{ id: 'u1' }] } };
const port = makePort(async () => fakeFan);
const result = await safeNeighborImpact(
port,
'repo-id',
'uid:1',
'upstream',
minimalOpts,
1000,
);
expect(result.timedOut).toBe(false);
expect(result.value).toBe(fakeFan);
});
it('returns timedOut=true immediately when remainingMs is 0 and the call still hangs', async () => {
// Defensive: even if the caller passes 0, the helper must not block.
const port = makePort(() => new Promise(() => {}));
const start = performance.now();
const result = await safeNeighborImpact(port, 'repo-id', 'uid:1', 'upstream', minimalOpts, 0);
const elapsedMs = performance.now() - start;
expect(result.timedOut).toBe(true);
expect(result.value).toBeNull();
// 0ms timeout fires on the next tick — should be well under 50ms even on slow CI.
expect(elapsedMs).toBeLessThan(50);
});
it('does not compound across calls — two hung neighbors complete within ~2× remainingMs total', async () => {
// The contract is per-call timeout. Two sequential hung calls should
// total ~2× remainingMs, not (numNeighbors × remainingMs² / 2) or
// anything compounding. A regression that shares one timer across
// calls would pass the first test but fail this one.
const port = makePort(() => new Promise(() => {}));
const start = performance.now();
const r1 = await safeNeighborImpact(port, 'repo', 'u1', 'upstream', minimalOpts, 100);
const r2 = await safeNeighborImpact(port, 'repo', 'u2', 'upstream', minimalOpts, 100);
const elapsedMs = performance.now() - start;
expect(r1.timedOut).toBe(true);
expect(r2.timedOut).toBe(true);
expect(elapsedMs).toBeGreaterThanOrEqual(180);
expect(elapsedMs).toBeLessThan(1000);
});
it('propagates an immediate rejection from impactByUid as timedOut=false with null value', async () => {
// If the port itself rejects (rather than hangs), the helper should
// surface that as a non-timeout failure — the existing fanout block
// already handles `if (fan == null)` truncation, so returning null
// here keeps that path intact.
const port = makePort(async () => {
throw new Error('connection refused');
});
const result = await safeNeighborImpact(port, 'repo', 'u1', 'upstream', minimalOpts, 1000);
expect(result.timedOut).toBe(false);
expect(result.value).toBeNull();
});
});

View file

@ -0,0 +1,196 @@
/**
* Regression tests for U8 closes:
* #186 js/redos rust-workspace-extractor.ts
* #187 js/redos cobol-preprocessor.ts
* #184 js/resource-exhaustion cross-impact.ts
*
* These tests import the production symbols directly. A previous shape
* dynamic-imported names that did not exist (`extractRustWorkspace` vs.
* the real `extractRustWorkspaceLinks`) and `??`-fell-back to inline
* regex copies, so the tests stayed green even when the production
* fixes regressed. Static imports + named symbols make a regression in
* any of the three sites a hard test failure.
*/
import { describe, expect, it } from 'vitest';
import { RE_SET_TO_TRUE, RE_SET_INDEX } from '../../src/core/ingestion/cobol/cobol-preprocessor.js';
import { parseCargoPackageName } from '../../src/core/group/extractors/rust-workspace-extractor.js';
import {
clampTimeout,
IMPACT_TIMEOUT_MIN_MS,
IMPACT_TIMEOUT_MAX_MS,
} from '../../src/core/group/cross-impact.js';
/**
* Time a single regex.exec call. Used by the linearity tests below to
* compute a 10k/5k ratio in addition to the absolute <500ms bound.
*
* Ratio assertions catch sub-exponential O(n²) regressions that fit
* inside the absolute cap on warm CI; the absolute cap catches
* catastrophic backtracking on cold CI. Two complementary signals.
*/
function timeRegex(re: RegExp, input: string): number {
// Reset regex.lastIndex for global/sticky regexes — ours are not, but
// be defensive in case future shape changes add the `g` flag.
re.lastIndex = 0;
const start = performance.now();
re.exec(input);
return performance.now() - start;
}
function timeFn<T>(fn: () => T): number {
const start = performance.now();
fn();
return performance.now() - start;
}
// Linear scaling is ~2.0× when input doubles; 3.0× allows generous
// slack for CI-runner GC and tier-up jitter. An O(n²) regression on a
// 2× input takes ~4× as long, well outside this bound.
const LINEAR_RATIO_BOUND = 3.0;
/**
* Minimum elapsed time (in ms) below which `performance.now()` ratios
* are dominated by scheduler jitter and become meaningless. When both
* timed runs come in below this floor, we skip the ratio assertion
* the absolute <500ms bound still catches catastrophic backtracking,
* and the next CI run will measure higher absolute times that the
* ratio assertion can evaluate reliably.
*
* Calibrated empirically: a flake on macOS reported ratio 5.29×
* between two sub-millisecond measurements (~0.5ms vs ~2.6ms), both
* genuinely linear but indistinguishable from noise. 5ms is a
* comfortable floor where individual measurements are well-separated
* from the ~10-100µs `performance.now()` resolution band.
*/
const RATIO_MEASUREMENT_FLOOR_MS = 5;
/**
* Assert linear scaling between two timed runs on inputs that differ
* by 2×. When measurements are too small to be reliable, the ratio
* assertion is skipped (the absolute bound still fires elsewhere).
*/
function assertSubLinearRatio(elapsedSmall: number, elapsedLarge: number, label: string): void {
if (elapsedSmall < RATIO_MEASUREMENT_FLOOR_MS && elapsedLarge < RATIO_MEASUREMENT_FLOOR_MS) {
// Both runs completed faster than the noise floor — the ratio is
// not meaningful. The absolute <500ms bound elsewhere in this
// describe block still pins linearity; we skip rather than risk a
// flake on a genuinely-linear implementation.
return;
}
const ratio = elapsedLarge / Math.max(elapsedSmall, 0.001);
if (ratio >= LINEAR_RATIO_BOUND) {
throw new Error(
`${label}: ratio ${ratio.toFixed(2)}× exceeds bound ${LINEAR_RATIO_BOUND}× ` +
`(small=${elapsedSmall.toFixed(2)}ms, large=${elapsedLarge.toFixed(2)}ms)`,
);
}
}
describe('cobol-preprocessor RE_SET_TO_TRUE — linear time on pathological input', () => {
it('matches in <500ms on 50k repetitions of "A OF A " AND 100k/50k ratio is sub-linear when measurable', () => {
// 50k/100k repetitions chosen so timings exceed the
// RATIO_MEASUREMENT_FLOOR_MS noise floor on typical CI hardware.
// Pre-fix nested-quantifier shape would be exponential here; the
// post-fix `.+?` shape is linear (~2× when input doubles).
const inputSmall = 'SET ' + 'A OF A '.repeat(50_000) + 'TO TRUE';
const inputLarge = 'SET ' + 'A OF A '.repeat(100_000) + 'TO TRUE';
const elapsedSmall = timeRegex(RE_SET_TO_TRUE, inputSmall);
const elapsedLarge = timeRegex(RE_SET_TO_TRUE, inputLarge);
expect(RE_SET_TO_TRUE.exec(inputSmall)).not.toBeNull();
expect(elapsedSmall).toBeLessThan(500);
expect(elapsedLarge).toBeLessThan(500);
assertSubLinearRatio(elapsedSmall, elapsedLarge, 'RE_SET_TO_TRUE');
});
it('still matches a normal SET ... TO TRUE statement', () => {
const m = RE_SET_TO_TRUE.exec('SET WS-FLAG TO TRUE');
expect(m).not.toBeNull();
expect(m?.[1]).toBe('WS-FLAG');
});
});
describe('cobol-preprocessor RE_SET_INDEX — linear time on pathological input', () => {
it('rejects in <500ms on 50k tokens with no valid suffix AND 100k/50k ratio is sub-linear when measurable', () => {
// Forces backtracking against the (TO|UP\s+BY|DOWN\s+BY) alternation
// — the richer pathological surface of the two regexes.
const inputSmall = 'SET ' + 'A '.repeat(50_000) + 'X';
const inputLarge = 'SET ' + 'A '.repeat(100_000) + 'X';
const elapsedSmall = timeRegex(RE_SET_INDEX, inputSmall);
const elapsedLarge = timeRegex(RE_SET_INDEX, inputLarge);
expect(RE_SET_INDEX.exec(inputSmall)).toBeNull();
expect(elapsedSmall).toBeLessThan(500);
expect(elapsedLarge).toBeLessThan(500);
assertSubLinearRatio(elapsedSmall, elapsedLarge, 'RE_SET_INDEX');
});
it('still matches a normal SET INDEX statement', () => {
const m = RE_SET_INDEX.exec('SET WS-IDX TO 5');
expect(m).not.toBeNull();
expect(m?.[1]).toBe('WS-IDX');
expect(m?.[2]).toBe('TO');
expect(m?.[3]).toBe('5');
});
});
describe('rust-workspace parseCargoPackageName — linear-time line walk', () => {
it('extracts the package name in <500ms on 100k blank lines AND 200k/100k ratio is sub-linear when measurable', () => {
// 100k/200k blank lines chosen so timings exceed the
// RATIO_MEASUREMENT_FLOOR_MS noise floor. Earlier 10k/20k pairing
// produced sub-millisecond measurements where scheduler jitter
// dominated and the ratio became meaningless (a real macOS run
// saw 5.29× between two genuinely-linear sub-ms measurements).
const cargoTomlSmall =
'[package]\n' + '\n'.repeat(100_000) + 'name = "myrepo"\nversion = "0.1.0"\n';
const cargoTomlLarge =
'[package]\n' + '\n'.repeat(200_000) + 'name = "myrepo"\nversion = "0.1.0"\n';
const elapsedSmall = timeFn(() => parseCargoPackageName(cargoTomlSmall));
const elapsedLarge = timeFn(() => parseCargoPackageName(cargoTomlLarge));
expect(parseCargoPackageName(cargoTomlSmall)).toBe('myrepo');
expect(elapsedSmall).toBeLessThan(500);
expect(elapsedLarge).toBeLessThan(500);
assertSubLinearRatio(elapsedSmall, elapsedLarge, 'parseCargoPackageName');
});
it('returns null when [package] section is absent', () => {
expect(parseCargoPackageName('[workspace]\nmembers = ["a"]\n')).toBeNull();
});
it('stops at the next section header (does not pick up a name= from a later section)', () => {
const toml = '[package]\nversion = "1.0"\n[other]\nname = "wrong"\n';
expect(parseCargoPackageName(toml)).toBeNull();
});
it('extracts the name from a normal [package] section', () => {
const toml = '[package]\nname = "real-crate"\nversion = "0.1.0"\n';
expect(parseCargoPackageName(toml)).toBe('real-crate');
});
});
describe('cross-impact clampTimeout — bounds user-supplied impact timeouts', () => {
it('rejects negative and zero timeouts, returning MIN', () => {
expect(clampTimeout(0)).toBe(IMPACT_TIMEOUT_MIN_MS);
expect(clampTimeout(-1)).toBe(IMPACT_TIMEOUT_MIN_MS);
expect(clampTimeout(-999_999)).toBe(IMPACT_TIMEOUT_MIN_MS);
});
it('rejects NaN/Infinity, returning MIN', () => {
expect(clampTimeout(NaN)).toBe(IMPACT_TIMEOUT_MIN_MS);
expect(clampTimeout(Infinity)).toBe(IMPACT_TIMEOUT_MIN_MS);
expect(clampTimeout(-Infinity)).toBe(IMPACT_TIMEOUT_MIN_MS);
});
it('caps very large timeouts at MAX (5 minutes)', () => {
expect(clampTimeout(999_999_999)).toBe(IMPACT_TIMEOUT_MAX_MS);
expect(clampTimeout(IMPACT_TIMEOUT_MAX_MS + 1)).toBe(IMPACT_TIMEOUT_MAX_MS);
});
it('passes through a reasonable timeout unchanged (truncated to integer)', () => {
expect(clampTimeout(30_000)).toBe(30_000);
expect(clampTimeout(30_500.7)).toBe(30_500);
});
it('floors below-MIN positive values to MIN', () => {
expect(clampTimeout(50)).toBe(IMPACT_TIMEOUT_MIN_MS);
expect(clampTimeout(0.1)).toBe(IMPACT_TIMEOUT_MIN_MS);
});
});