mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-01 02:01:24 +00:00
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
* feat(analyze): expose process-detection budget overrides (#3313) Operators can raise or lower process count, branching, trace depth, and the entry-point candidate pool via CLI, .gitnexusrc, or GITNEXUS_* without changing shipped defaults. A budget-only change re-detects flows on the next analyze without --force. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(review): say invalid budget flags still honor env A rejected --max-processes value was described as falling back to the built-in default even when GITNEXUS_MAX_* still won the next precedence tier. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(analyze): share process-detection defaults and skip unused walks Keep DEFAULT_CONFIG aligned with the budget resolver and count symbols only when maxProcesses is still dynamic. Co-authored-by: Cursor <cursoragent@cursor.com> * style(analyze): wrap process-detection budget files for prettier Co-authored-by: Cursor <cursoragent@cursor.com> * docs(analyze): name the real process-detection default formula Co-authored-by: Cursor <cursoragent@cursor.com> * docs(analyze): stop calling maxProcesses*2 a hard trace quota Co-authored-by: Cursor <cursoragent@cursor.com> * fix(analyze): say invalid env budget tokens fall back to defaults Co-authored-by: Cursor <cursoragent@cursor.com> * fix(analyze): recertify process-detection after in-place FTS abort (#3324) Persist processDetection.uncertified on the in-place FTS dirty stamp when the budget mismatched so a flagless retry cannot keep rewritten flows. Qualify .gitnexusrc fail-fast copy and tighten related tests. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(analyze): skip live dirty stamp on atomic incremental (#3324) POSIX atomic incremental mutates a staging copy, so stamping live incrementalInProgress before swap made a crash force-rebuild a healthy index. Align analyze --help with CLI > .gitnexusrc > env > default. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(changelog): drop the atomic-incremental dirty-stamp note The code fix stays; Unreleased no longer lists that recovery change. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cli): survive FTS SIGSEGV in --limit e2e CREATE_FTS_INDEX can kill the setup analyze on some WSL hosts (status null). Rebuild with --skip-fts and skip BM25-only query --limit cases unless GITNEXUS_REQUIRE_FTS=1. Refs #3324 Co-authored-by: Cursor <cursoragent@cursor.com> * test(cli): mark update-check child at import Writing refresh-started from fetch() raced a 30s poll against cold tsx boot on a loaded default-project worker. Refs #3324 Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3324) Isolate default-budget FTS crash-marker tests from GITNEXUS_MAX_* env, assert uncertify-before-FTS order and deferred flow detection on park recovery, drop the dangling "then" from entry-point help, and correct stale streamGraphEmit docs without skipping the process-detection stamp. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(changelog): drop Unreleased process-detection notes Keep the #3313 / #3322 code; Unreleased changelog matches main until release. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
235 lines
8.6 KiB
TypeScript
235 lines
8.6 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
buildProcessDetectionPhaseConfig,
|
|
formatInvalidProcessDetectionOverride,
|
|
formatProcessDetectionBudgetBanner,
|
|
formatWholeFlowsMissingRemedies,
|
|
parsePositiveIntegerOverride,
|
|
parseProcessDetectionBudgetStrings,
|
|
processDetectionBudgetMismatch,
|
|
processDetectionEffectiveLimits,
|
|
resolveProcessDetectionBudget,
|
|
toProcessDetectionStamp,
|
|
uncertifyProcessDetectionStamp,
|
|
} from '../../src/core/ingestion/process-detection-budget.js';
|
|
|
|
describe('parsePositiveIntegerOverride', () => {
|
|
it('accepts positive integers and rejects 0 / non-integers', () => {
|
|
const invalid: string[] = [];
|
|
expect(parsePositiveIntegerOverride('25')).toBe(25);
|
|
expect(parsePositiveIntegerOverride(40)).toBe(40);
|
|
expect(parsePositiveIntegerOverride('0', (raw) => invalid.push(raw))).toBeUndefined();
|
|
expect(parsePositiveIntegerOverride('abc', (raw) => invalid.push(raw))).toBeUndefined();
|
|
expect(parsePositiveIntegerOverride('-3', (raw) => invalid.push(raw))).toBeUndefined();
|
|
expect(parsePositiveIntegerOverride('', (raw) => invalid.push(raw))).toBeUndefined();
|
|
expect(invalid).toEqual(['0', 'abc', '-3', '']);
|
|
});
|
|
});
|
|
|
|
describe('resolveProcessDetectionBudget (#3313)', () => {
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it('keeps shipped defaults when nothing is set', () => {
|
|
const resolved = resolveProcessDetectionBudget({}, {});
|
|
expect(resolved.maxProcesses).toBeUndefined();
|
|
expect(resolved.maxProcessBranching).toBe(4);
|
|
expect(resolved.maxProcessTraceDepth).toBe(10);
|
|
expect(resolved.maxEntryPointCandidates).toBe(200);
|
|
expect(resolved.overridden).toEqual({
|
|
maxProcesses: false,
|
|
maxProcessBranching: false,
|
|
maxProcessTraceDepth: false,
|
|
maxEntryPointCandidates: false,
|
|
});
|
|
});
|
|
|
|
it('lets an invalid option fall through to env instead of claiming a hard default', () => {
|
|
const invalid: Array<[string, string]> = [];
|
|
const resolved = resolveProcessDetectionBudget(
|
|
{ maxProcesses: 0 },
|
|
{ GITNEXUS_MAX_PROCESSES: '80' },
|
|
(knob, raw) => invalid.push([knob, raw]),
|
|
);
|
|
expect(resolved.maxProcesses).toBe(80);
|
|
expect(invalid).toEqual([['--max-processes', '0']]);
|
|
expect(formatInvalidProcessDetectionOverride('--max-processes', '0')).toContain(
|
|
'next source (env, then the built-in default)',
|
|
);
|
|
expect(formatInvalidProcessDetectionOverride('GITNEXUS_MAX_PROCESSES', '0')).toContain(
|
|
'the built-in default',
|
|
);
|
|
expect(formatInvalidProcessDetectionOverride('GITNEXUS_MAX_PROCESSES', '0')).not.toContain(
|
|
'next source (env, then the built-in default)',
|
|
);
|
|
});
|
|
|
|
it('lets explicit options beat env (AE3 remainder)', () => {
|
|
const resolved = resolveProcessDetectionBudget(
|
|
{ maxProcesses: 25 },
|
|
{ GITNEXUS_MAX_PROCESSES: '80' },
|
|
);
|
|
expect(resolved.maxProcesses).toBe(25);
|
|
expect(resolved.overridden.maxProcesses).toBe(true);
|
|
});
|
|
|
|
it('reads env when option fields are unset', () => {
|
|
const resolved = resolveProcessDetectionBudget(
|
|
{},
|
|
{
|
|
GITNEXUS_MAX_PROCESSES: '40',
|
|
GITNEXUS_MAX_PROCESS_BRANCHING: '2',
|
|
GITNEXUS_MAX_PROCESS_TRACE_DEPTH: '8',
|
|
GITNEXUS_MAX_ENTRY_POINT_CANDIDATES: '400',
|
|
},
|
|
);
|
|
expect(resolved.maxProcesses).toBe(40);
|
|
expect(resolved.maxProcessBranching).toBe(2);
|
|
expect(resolved.maxProcessTraceDepth).toBe(8);
|
|
expect(resolved.maxEntryPointCandidates).toBe(400);
|
|
});
|
|
|
|
it('warns and falls back on invalid env (AE5)', () => {
|
|
const invalid: Array<[string, string]> = [];
|
|
const zero = resolveProcessDetectionBudget({}, { GITNEXUS_MAX_PROCESSES: '0' }, (knob, raw) =>
|
|
invalid.push([knob, raw]),
|
|
);
|
|
const garbage = resolveProcessDetectionBudget(
|
|
{},
|
|
{ GITNEXUS_MAX_PROCESSES: 'abc' },
|
|
(knob, raw) => invalid.push([knob, raw]),
|
|
);
|
|
expect(zero.maxProcesses).toBeUndefined();
|
|
expect(garbage.maxProcesses).toBeUndefined();
|
|
expect(invalid).toEqual([
|
|
['GITNEXUS_MAX_PROCESSES', '0'],
|
|
['GITNEXUS_MAX_PROCESSES', 'abc'],
|
|
]);
|
|
});
|
|
|
|
it('replaces the dynamic formula only when maxProcesses is explicit', () => {
|
|
const unset = buildProcessDetectionPhaseConfig(
|
|
resolveProcessDetectionBudget({}, {}),
|
|
1000,
|
|
(n) => Math.max(20, Math.round(n / 10)),
|
|
);
|
|
const explicit = buildProcessDetectionPhaseConfig(
|
|
resolveProcessDetectionBudget({ maxProcesses: 5 }, {}),
|
|
1000,
|
|
(n) => Math.max(20, Math.round(n / 10)),
|
|
);
|
|
expect(unset.maxProcesses).toBe(100);
|
|
expect(explicit.maxProcesses).toBe(5);
|
|
expect(explicit.minSteps).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('processDetectionBudgetMismatch (KTD4)', () => {
|
|
const defaults = resolveProcessDetectionBudget({}, {});
|
|
|
|
it('matches a missing stamp against default/dynamic knobs', () => {
|
|
expect(processDetectionBudgetMismatch(undefined, defaults)).toBe(false);
|
|
});
|
|
|
|
it('mismatches a missing stamp when any override is set', () => {
|
|
expect(
|
|
processDetectionBudgetMismatch(
|
|
undefined,
|
|
resolveProcessDetectionBudget({ maxEntryPointCandidates: 400 }, {}),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('mismatches when a present stamp differs', () => {
|
|
const recorded = toProcessDetectionStamp(defaults);
|
|
expect(
|
|
processDetectionBudgetMismatch({ ...recorded, maxEntryPointCandidates: 400 }, defaults),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('mismatches explicit maxProcesses vs later dynamic even when the integer equals the formula', () => {
|
|
const explicit = resolveProcessDetectionBudget({ maxProcesses: 100 }, {});
|
|
expect(processDetectionBudgetMismatch(toProcessDetectionStamp(explicit), defaults)).toBe(true);
|
|
expect(toProcessDetectionStamp(explicit).maxProcesses).toBe(100);
|
|
expect(toProcessDetectionStamp(defaults).maxProcesses).toBe(null);
|
|
});
|
|
|
|
it('matches an identical present stamp', () => {
|
|
const resolved = resolveProcessDetectionBudget(
|
|
{ maxProcesses: 25, maxEntryPointCandidates: 400 },
|
|
{},
|
|
);
|
|
expect(processDetectionBudgetMismatch(toProcessDetectionStamp(resolved), resolved)).toBe(false);
|
|
});
|
|
|
|
it('mismatches an uncertified stamp even when the numeric fields match defaults', () => {
|
|
const recorded = uncertifyProcessDetectionStamp(toProcessDetectionStamp(defaults));
|
|
expect(recorded.uncertified).toBe(true);
|
|
expect(recorded.maxProcesses).toBe(null);
|
|
expect(processDetectionBudgetMismatch(recorded, defaults)).toBe(true);
|
|
expect(processDetectionBudgetMismatch(toProcessDetectionStamp(defaults), defaults)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('warning copy and banner', () => {
|
|
it('maps loud counters to the matching knobs and names the pre-entry trace gate', () => {
|
|
const limits = processDetectionEffectiveLimits(20, resolveProcessDetectionBudget({}, {}));
|
|
expect(limits.maxProcessTraces).toBe(40);
|
|
expect(
|
|
formatWholeFlowsMissingRemedies(
|
|
{ entryPointCandidatesDropped: 210, entryPointsUnexplored: 0, processesDropped: 0 },
|
|
limits,
|
|
410,
|
|
),
|
|
).toContain('--max-entry-point-candidates');
|
|
expect(
|
|
formatWholeFlowsMissingRemedies(
|
|
{ entryPointCandidatesDropped: 210, entryPointsUnexplored: 0, processesDropped: 0 },
|
|
limits,
|
|
410,
|
|
),
|
|
).not.toContain('--max-process-branching');
|
|
expect(
|
|
formatWholeFlowsMissingRemedies(
|
|
{ entryPointCandidatesDropped: 0, entryPointsUnexplored: 3, processesDropped: 1 },
|
|
limits,
|
|
10,
|
|
),
|
|
).toContain('--max-processes');
|
|
expect(
|
|
formatWholeFlowsMissingRemedies(
|
|
{ entryPointCandidatesDropped: 0, entryPointsUnexplored: 3, processesDropped: 1 },
|
|
limits,
|
|
10,
|
|
),
|
|
).toContain('40');
|
|
expect(
|
|
formatWholeFlowsMissingRemedies(
|
|
{ entryPointCandidatesDropped: 0, entryPointsUnexplored: 3, processesDropped: 1 },
|
|
limits,
|
|
10,
|
|
),
|
|
).toContain('next entry is skipped');
|
|
});
|
|
|
|
it('prints a banner only when an override is active', () => {
|
|
expect(formatProcessDetectionBudgetBanner(resolveProcessDetectionBudget({}, {}))).toBeNull();
|
|
expect(
|
|
formatProcessDetectionBudgetBanner(resolveProcessDetectionBudget({ maxProcesses: 25 }, {})),
|
|
).toContain('maxProcesses=25');
|
|
expect(
|
|
formatProcessDetectionBudgetBanner(
|
|
resolveProcessDetectionBudget({ maxProcessBranching: 6 }, {}),
|
|
),
|
|
).toContain('maxProcesses=dynamic (max(20, round(symbols/10)))');
|
|
});
|
|
|
|
it('parses CLI/rc numeric strings without treating 0 as unlimited', () => {
|
|
const parsed = parseProcessDetectionBudgetStrings({
|
|
maxProcesses: '25',
|
|
maxProcessBranching: '0',
|
|
});
|
|
expect(parsed).toEqual({ maxProcesses: 25 });
|
|
});
|
|
});
|