mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
feat(impact): PDG-layer presence + degradation contract (U2)
Extract the 3-state meta-probe from _pdgQueryImpl into pdgLayerStatus, a both-caps helper returning no-layer / sub-layer-missing / ready / unknown without scanning the DB (except one bounded LIMIT 1 probe when meta is unreadable). _pdgQueryImpl now consumes a single-cap projection (pdgStampForMode) byte-identically. Wired into _impactImpl's pdg branch so degraded indexes return a distinct note before the stub. Refs U2
This commit is contained in:
parent
00dc899d34
commit
f85a066d05
3 changed files with 424 additions and 17 deletions
|
|
@ -122,6 +122,159 @@ function validateImpactMode(rawMode: unknown): { mode: ImpactMode } | { error: s
|
|||
error: `Invalid "mode": expected "callgraph" or "pdg", got ${JSON.stringify(rawMode)}.`,
|
||||
};
|
||||
}
|
||||
|
||||
/** The two independently-stamped PDG sub-layers (KTD7). */
|
||||
export type PdgSubLayer = 'CDG' | 'REACHING_DEF';
|
||||
|
||||
/**
|
||||
* Four-state PDG-layer presence/degradation status (KTD7).
|
||||
*
|
||||
* - `'no-layer'` — `meta.pdg` is absent: this repo was never analyzed
|
||||
* with `--pdg` (definitive; established with NO DB scan).
|
||||
* - `'sub-layer-missing'`— exactly one of the two independently-stamped caps
|
||||
* (`maxCdgEdgesPerFunction` / `maxReachingDefEdgesPerFunction`)
|
||||
* is present. `impact`'s PDG mode needs BOTH, so a
|
||||
* partial layer must not be reported as complete; the
|
||||
* missing one is named in `missingSubLayer`.
|
||||
* - `'ready'` — both caps present: the layer is fully stamped.
|
||||
* - `'unknown'` — meta is unreadable (e.g. a seeded test DB with no
|
||||
* `meta.json`). One bounded `LIMIT 1` probe distinguishes
|
||||
* a genuinely edge-free index from a missing one; either
|
||||
* way the conclusion is inconclusive (a missing layer is
|
||||
* indistinguishable from an all-linear one — #2188).
|
||||
*/
|
||||
export interface PdgLayerStatus {
|
||||
state: 'no-layer' | 'sub-layer-missing' | 'ready' | 'unknown';
|
||||
/** Set only for `'sub-layer-missing'` — the cap that was NOT stamped. */
|
||||
missingSubLayer?: PdgSubLayer;
|
||||
/** Human-readable guidance for the degraded states (absent for `'ready'`). */
|
||||
note?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-cap presence read from `meta.pdg`, plus whether meta was readable at all.
|
||||
*
|
||||
* `metaReadable` is the seam between the `'unknown'` state (meta unreadable —
|
||||
* fall through to a DB probe) and the meta-stamped states. When `metaReadable`
|
||||
* is true but `meta.pdg` was absent, both `cdg`/`rd` are `false`.
|
||||
*/
|
||||
interface PdgMetaCaps {
|
||||
metaReadable: boolean;
|
||||
/** `maxCdgEdgesPerFunction !== undefined` (only meaningful when metaReadable). */
|
||||
cdg: boolean;
|
||||
/** `maxReachingDefEdgesPerFunction !== undefined` (only meaningful when metaReadable). */
|
||||
rd: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the two PDG sub-layer caps from the on-disk `meta.json` stamp — the
|
||||
* single shared meta-probe both `_pdgQueryImpl` (one cap) and the PDG impact
|
||||
* mode (both caps) key on. Never scans the DB. An unreadable / missing meta
|
||||
* yields `metaReadable: false` (the `'unknown'` seam); a readable meta with no
|
||||
* `pdg` stamp yields `metaReadable: true` with both caps `false` (no-layer).
|
||||
*/
|
||||
async function readPdgMetaCaps(
|
||||
lbugPath: string,
|
||||
loadMetaFn: typeof loadMeta,
|
||||
): Promise<PdgMetaCaps> {
|
||||
try {
|
||||
const meta = await loadMetaFn(path.dirname(lbugPath));
|
||||
if (!meta) return { metaReadable: false, cdg: false, rd: false };
|
||||
return {
|
||||
metaReadable: true,
|
||||
cdg: meta.pdg?.maxCdgEdgesPerFunction !== undefined,
|
||||
rd: meta.pdg?.maxReachingDefEdgesPerFunction !== undefined,
|
||||
};
|
||||
} catch {
|
||||
// Meta unreadable — the caller decides from the DB (the `'unknown'` state).
|
||||
return { metaReadable: false, cdg: false, rd: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Project the both-caps PDG meta read down to the single mode-relevant cap that
|
||||
* `_pdgQueryImpl` keys on (`controls` → CDG, `flows` → REACHING_DEF), preserving
|
||||
* its established tri-state `boolean | undefined` contract byte-for-byte
|
||||
* (Feasibility Issue 4):
|
||||
* - `false` — meta readable and the relevant cap absent → definitive
|
||||
* no-layer (short-circuits before any DB scan).
|
||||
* - `true` — meta readable and the relevant cap present → proceed.
|
||||
* - `undefined` — meta unreadable → defer to the post-anchored-query probe.
|
||||
*
|
||||
* `_pdgQueryImpl` needs only ONE cap, so it collapses the both-caps read here
|
||||
* rather than consuming `pdgLayerStatus` directly (whose `'unknown'` state does
|
||||
* an upfront global probe — wrong timing/order for the anchored-query path).
|
||||
*/
|
||||
async function pdgStampForMode(
|
||||
lbugPath: string,
|
||||
mode: 'controls' | 'flows',
|
||||
loadMetaFn: typeof loadMeta = loadMeta,
|
||||
): Promise<boolean | undefined> {
|
||||
const caps = await readPdgMetaCaps(lbugPath, loadMetaFn);
|
||||
if (!caps.metaReadable) return undefined;
|
||||
return mode === 'controls' ? caps.cdg : caps.rd;
|
||||
}
|
||||
|
||||
/**
|
||||
* PDG-layer presence/degradation check for the `impact` PDG mode (KTD7).
|
||||
*
|
||||
* Returns the four distinct states WITHOUT scanning the DB except for the single
|
||||
* bounded `LIMIT 1` probe the `'unknown'` (meta-unreadable) case requires. The
|
||||
* caller (`_impactImpl` PDG branch, and the accuracy harness) surfaces a
|
||||
* distinct signal per state so a missing `--pdg` layer / partial layer is never
|
||||
* silently misread as a confident empty blast radius. Impact needs BOTH the CDG
|
||||
* and the REACHING_DEF sub-layer, so a partial stamp degrades, not proceeds.
|
||||
*
|
||||
* Deps are injected (KTD2 extraction discipline) so this can move to a future
|
||||
* `pdg-impact.ts` engine as a move, not a rewrite.
|
||||
*/
|
||||
async function pdgLayerStatus(deps: {
|
||||
lbugPath: string;
|
||||
executeParameterized: typeof executeParameterized;
|
||||
loadMetaFn?: typeof loadMeta;
|
||||
}): Promise<PdgLayerStatus> {
|
||||
const loadMetaFn = deps.loadMetaFn ?? loadMeta;
|
||||
const caps = await readPdgMetaCaps(deps.lbugPath, loadMetaFn);
|
||||
|
||||
if (caps.metaReadable) {
|
||||
// Meta is readable — the stamp is authoritative, no DB scan needed.
|
||||
if (caps.cdg && caps.rd) return { state: 'ready' };
|
||||
if (caps.cdg !== caps.rd) {
|
||||
// Exactly one sub-layer stamped (XOR) — partial layer; impact needs both.
|
||||
const missingSubLayer: PdgSubLayer = caps.cdg ? 'REACHING_DEF' : 'CDG';
|
||||
return {
|
||||
state: 'sub-layer-missing',
|
||||
missingSubLayer,
|
||||
note:
|
||||
`PDG layer is incomplete — the ${missingSubLayer} sub-layer is missing ` +
|
||||
`(impact's PDG mode needs both CDG and REACHING_DEF). ` +
|
||||
`Re-run gitnexus analyze --pdg to record it.`,
|
||||
};
|
||||
}
|
||||
// Neither cap stamped (meta.pdg absent, or present with no caps) → the layer
|
||||
// was never recorded. Definitive, no DB scan.
|
||||
return {
|
||||
state: 'no-layer',
|
||||
note: 'no PDG layer — run gitnexus analyze --pdg to record CDG + REACHING_DEF edges for this repo',
|
||||
};
|
||||
}
|
||||
|
||||
// Meta unreadable (e.g. a seeded test DB): one bounded probe confirms the
|
||||
// layer status is genuinely undeterminable from the DB. A missing layer is
|
||||
// indistinguishable from an all-linear (edge-free) one (#2188), so whether the
|
||||
// probe finds a row or not the note stays inconclusive ("status unknown") —
|
||||
// never the definitive no-layer wording. The probe is bounded (LIMIT 1) and
|
||||
// anchored on the edge-type discriminator, never an unbounded path scan.
|
||||
await deps.executeParameterized(
|
||||
deps.lbugPath,
|
||||
`MATCH (:BasicBlock)-[r:CodeRelation]->(:BasicBlock) WHERE r.type IN ['CDG', 'REACHING_DEF'] RETURN r.type AS type LIMIT 1`,
|
||||
{},
|
||||
);
|
||||
return {
|
||||
state: 'unknown',
|
||||
note: 'PDG layer status unknown — no CDG/REACHING_DEF edges visible and meta is unreadable; was this repo indexed with gitnexus analyze --pdg?',
|
||||
};
|
||||
}
|
||||
// AI context generation is CLI-only (gitnexus analyze)
|
||||
// import { generateAIContextFiles } from '../../cli/ai-context.js';
|
||||
|
||||
|
|
@ -3322,18 +3475,13 @@ export class LocalBackend {
|
|||
// Cheap meta probe: the layer exists iff the pdg stamp carries the
|
||||
// mode-relevant cap (maxCdgEdgesPerFunction for CDG, maxReachingDef…
|
||||
// for REACHING_DEF). Absent ⇒ the no-layer hint without a DB scan.
|
||||
let pdgStamped: boolean | undefined;
|
||||
try {
|
||||
const meta = await loadMeta(path.dirname(repo.lbugPath));
|
||||
if (meta) {
|
||||
pdgStamped =
|
||||
mode === 'controls'
|
||||
? meta.pdg?.maxCdgEdgesPerFunction !== undefined
|
||||
: meta.pdg?.maxReachingDefEdgesPerFunction !== undefined;
|
||||
}
|
||||
} catch {
|
||||
/* meta unreadable — decide from the DB below */
|
||||
}
|
||||
// `pdgStampForMode` is the shared meta read (the both-caps `pdgLayerStatus`
|
||||
// helper consumes the same underlying read for impact); here we project it
|
||||
// down to this one mode's cap, preserving the tri-state `boolean | undefined`
|
||||
// contract byte-for-byte: `false` ⇒ definitive no-layer (short-circuit
|
||||
// below), `true` ⇒ proceed, `undefined` ⇒ meta unreadable, defer to the
|
||||
// post-anchored-query probe (Feasibility Issue 4).
|
||||
const pdgStamped = await pdgStampForMode(repo.lbugPath, mode);
|
||||
if (pdgStamped === false) {
|
||||
return { mode, results: [], total: 0, note: NO_PDG_NOTE };
|
||||
}
|
||||
|
|
@ -4317,10 +4465,35 @@ export class LocalBackend {
|
|||
}
|
||||
}
|
||||
|
||||
// (2) PDG-layer presence probe — STUB for U1; U2 fills in the four-state
|
||||
// degradation contract (no-layer / partial / unknown / ready) here, before
|
||||
// any DB scan, so a missing `--pdg` layer returns a guidance note rather
|
||||
// than a confusing empty traversal. Intentionally a no-op for now.
|
||||
// (2) PDG-layer presence probe (U2, KTD7) — the four-state degradation
|
||||
// contract, BEFORE resolveSymbolCandidates / any traversal. A repo never
|
||||
// analyzed with `--pdg` (no-layer), one with only a partial layer
|
||||
// (sub-layer-missing — impact needs BOTH CDG and REACHING_DEF), or one whose
|
||||
// meta is unreadable (unknown) each returns a distinct guidance note here
|
||||
// rather than a confusing empty blast radius. Only `ready` falls through to
|
||||
// the traversal (the `_runImpactPDG` stub until U3/U4). This fires before the
|
||||
// stub deliberately, so a degraded layer is reported as such, not as
|
||||
// "pdg mode not yet implemented".
|
||||
if (mode === 'pdg') {
|
||||
const layer = await pdgLayerStatus({
|
||||
lbugPath: repo.lbugPath,
|
||||
executeParameterized,
|
||||
});
|
||||
if (layer.state !== 'ready') {
|
||||
return {
|
||||
mode,
|
||||
pdgLayer: layer.state,
|
||||
...(layer.missingSubLayer ? { missingSubLayer: layer.missingSubLayer } : {}),
|
||||
note: layer.note,
|
||||
target: { name: target },
|
||||
direction,
|
||||
// No confident zero: a degraded layer is inconclusive, not "safe to
|
||||
// refactor". UNKNOWN (KTD8) — never LOW (#2129/#1858 false-safe).
|
||||
impactedCount: 0,
|
||||
risk: 'UNKNOWN',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const maxDepth = params.maxDepth || 3;
|
||||
// Map legacy relation type names before filtering (backward compat for OVERRIDES → METHOD_OVERRIDES)
|
||||
|
|
|
|||
219
gitnexus/test/integration/impact-pdg-degradation.test.ts
Normal file
219
gitnexus/test/integration/impact-pdg-degradation.test.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
/**
|
||||
* Integration Tests: `impact` PDG-mode layer degradation contract (U2 / KTD7)
|
||||
*
|
||||
* End-to-end against a REAL LadybugDB, through the full `callTool('impact', …)`
|
||||
* dispatch. Exercises the four-state PDG-layer presence/degradation check
|
||||
* (`pdgLayerStatus`) wired into `_impactImpl`'s PDG branch — the check that
|
||||
* fires BEFORE symbol resolution / traversal so a missing or partial `--pdg`
|
||||
* layer returns a distinct guidance note instead of a confusing empty blast
|
||||
* radius (or the U2-era `_runImpactPDG` "not yet implemented" stub error).
|
||||
*
|
||||
* The four states (KTD7) are driven by what the (mocked) `loadMeta` returns —
|
||||
* matching the seeded-DB reality that there is no on-disk `meta.json`:
|
||||
* - no-layer : meta readable, no `pdg` stamp → run analyze --pdg
|
||||
* - sub-layer-missing : exactly one cap stamped (CDG xor RD) → names the missing one
|
||||
* - ready : both caps stamped → falls through to the stub
|
||||
* - unknown : meta unreadable (null) → inconclusive, via 1 LIMIT 1 probe
|
||||
*
|
||||
* The `_runImpactPDG` traversal is still a stub in U2, so the `ready` case
|
||||
* asserts the layer check let it THROUGH (the stub's "not yet implemented"
|
||||
* sentinel), proving the check is ordered before the stub for the degraded
|
||||
* states and falls through only when the layer is complete.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
|
||||
import type { RepoMeta } from '../../src/storage/repo-manager.js';
|
||||
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
|
||||
import { listRegisteredRepos, loadMeta } from '../../src/storage/repo-manager.js';
|
||||
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
||||
import * as poolAdapter from '../../src/core/lbug/pool-adapter.js';
|
||||
|
||||
vi.mock('../../src/storage/repo-manager.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../src/storage/repo-manager.js')>();
|
||||
return {
|
||||
...actual,
|
||||
listRegisteredRepos: vi.fn().mockResolvedValue([]),
|
||||
cleanupOldKuzuFiles: vi.fn().mockResolvedValue({ found: false, needsReindex: false }),
|
||||
findSiblingClones: vi.fn().mockResolvedValue([]),
|
||||
// Default: meta unreadable (the seeded-DB reality — no on-disk meta.json).
|
||||
// Individual tests override per state via mockResolvedValueOnce.
|
||||
loadMeta: vi.fn().mockResolvedValue(null),
|
||||
};
|
||||
});
|
||||
|
||||
// Minimal seed: one Function symbol (so a `ready` index could resolve it) plus
|
||||
// a single BasicBlock + CDG edge so the `unknown` state's LIMIT 1 probe finds a
|
||||
// row (it must STILL stay inconclusive — a present edge cannot disprove an
|
||||
// edge-free layer / #2188).
|
||||
const SEED = [
|
||||
`CREATE (fn:Function {id: 'func:hot', name: 'hot', filePath: 'src/hot.ts', startLine: 1, endLine: 5, isExported: true, content: 'function hot() {}', description: 'degradation fixture'})`,
|
||||
`CREATE (b0:BasicBlock {id: 'BasicBlock:src/hot.ts:1:0:0', filePath: 'src/hot.ts', startLine: 2, endLine: 2, text: 'if (x)'})`,
|
||||
`CREATE (b1:BasicBlock {id: 'BasicBlock:src/hot.ts:1:0:1', filePath: 'src/hot.ts', startLine: 3, endLine: 3, text: 'doThing();'})`,
|
||||
];
|
||||
const SEED_EDGE = `MATCH (a:BasicBlock {id: 'BasicBlock:src/hot.ts:1:0:0'}), (b:BasicBlock {id: 'BasicBlock:src/hot.ts:1:0:1'})
|
||||
CREATE (a)-[:CodeRelation {type: 'CDG', confidence: 1.0, reason: 'T', step: 0}]->(b)`;
|
||||
|
||||
const META = (pdg?: RepoMeta['pdg']): RepoMeta => ({ pdg } as unknown as RepoMeta);
|
||||
|
||||
withTestLbugDB(
|
||||
'impact-pdg-degradation',
|
||||
(handle) => {
|
||||
let backend: LocalBackend;
|
||||
beforeAll(() => {
|
||||
const ext = handle as typeof handle & { _backend?: LocalBackend };
|
||||
if (!ext._backend) throw new Error('LocalBackend not initialized in afterSetup');
|
||||
backend = ext._backend;
|
||||
});
|
||||
|
||||
// Reset the loadMeta mock to the default (unreadable) before each test so a
|
||||
// mockResolvedValueOnce set in one test never leaks into the next.
|
||||
beforeEach(() => {
|
||||
vi.mocked(loadMeta).mockReset();
|
||||
vi.mocked(loadMeta).mockResolvedValue(null);
|
||||
});
|
||||
|
||||
describe('no-layer (meta readable, no pdg stamp)', () => {
|
||||
it('returns the definitive "run analyze --pdg" note — and does NOT scan the DB', async () => {
|
||||
// Readable meta with no `pdg` key ⇒ the layer was never recorded.
|
||||
vi.mocked(loadMeta).mockResolvedValueOnce(META(undefined));
|
||||
const spy = vi.spyOn(poolAdapter, 'executeParameterized');
|
||||
spy.mockClear();
|
||||
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'hot',
|
||||
direction: 'downstream',
|
||||
mode: 'pdg',
|
||||
});
|
||||
|
||||
// Definitive, meta-derived: no DB probe ran, so executeParameterized was
|
||||
// never called between the spy clear and here (the no-layer branch
|
||||
// returns before the probe AND before resolveSymbolCandidates).
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
spy.mockRestore();
|
||||
|
||||
expect(result.mode).toBe('pdg');
|
||||
expect(result.pdgLayer).toBe('no-layer');
|
||||
expect(result.note).toMatch(/no PDG layer/i);
|
||||
expect(result.note).toContain('--pdg');
|
||||
// Not the stub, not a status-unknown note, not a confident LOW.
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.note).not.toMatch(/status unknown/i);
|
||||
expect(result.note).not.toMatch(/not yet implemented/i);
|
||||
expect(result.risk).toBe('UNKNOWN');
|
||||
expect(result.impactedCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sub-layer-missing (exactly one cap stamped)', () => {
|
||||
it('CDG present, RD absent → names REACHING_DEF as missing', async () => {
|
||||
vi.mocked(loadMeta).mockResolvedValueOnce(META({ maxCdgEdgesPerFunction: 0 } as any));
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'hot',
|
||||
direction: 'downstream',
|
||||
mode: 'pdg',
|
||||
});
|
||||
expect(result.pdgLayer).toBe('sub-layer-missing');
|
||||
expect(result.missingSubLayer).toBe('REACHING_DEF');
|
||||
expect(result.note).toMatch(/REACHING_DEF/);
|
||||
// Partial layer must NOT be reported as complete (not the stub, no LOW).
|
||||
expect(result.note).not.toMatch(/not yet implemented/i);
|
||||
expect(result.risk).toBe('UNKNOWN');
|
||||
});
|
||||
|
||||
it('RD present, CDG absent → names CDG as missing', async () => {
|
||||
vi.mocked(loadMeta).mockResolvedValueOnce(
|
||||
META({ maxReachingDefEdgesPerFunction: 0 } as any),
|
||||
);
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'hot',
|
||||
direction: 'downstream',
|
||||
mode: 'pdg',
|
||||
});
|
||||
expect(result.pdgLayer).toBe('sub-layer-missing');
|
||||
expect(result.missingSubLayer).toBe('CDG');
|
||||
expect(result.note).toMatch(/\bCDG\b/);
|
||||
expect(result.note).not.toMatch(/not yet implemented/i);
|
||||
expect(result.risk).toBe('UNKNOWN');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ready (both caps stamped)', () => {
|
||||
it('falls THROUGH the layer check to the traversal (the U2 _runImpactPDG stub)', async () => {
|
||||
vi.mocked(loadMeta).mockResolvedValueOnce(
|
||||
META({ maxCdgEdgesPerFunction: 0, maxReachingDefEdgesPerFunction: 0 } as any),
|
||||
);
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'hot',
|
||||
direction: 'downstream',
|
||||
mode: 'pdg',
|
||||
});
|
||||
// The layer is complete, so the check did NOT short-circuit: there is no
|
||||
// degradation note / pdgLayer marker — the call reached the stub instead.
|
||||
expect(result.pdgLayer).toBeUndefined();
|
||||
// U2: the traversal is still the stub. Once U3/U4 land this assertion
|
||||
// updates to a real blast radius; the load-bearing fact for U2 is that
|
||||
// `ready` did NOT return a degradation note.
|
||||
expect(result.mode).toBe('pdg');
|
||||
expect(result.error).toMatch(/not yet implemented/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unknown (meta unreadable)', () => {
|
||||
it('returns the inconclusive "status unknown" note via a bounded probe, even with edges present', async () => {
|
||||
// loadMeta defaults to null (unreadable) via beforeEach. The seeded DB
|
||||
// DOES carry a CDG edge, but the note must stay inconclusive — a present
|
||||
// edge cannot prove the layer is complete, and a missing one is
|
||||
// indistinguishable from an edge-free index (#2188).
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'hot',
|
||||
direction: 'downstream',
|
||||
mode: 'pdg',
|
||||
});
|
||||
expect(result.pdgLayer).toBe('unknown');
|
||||
expect(result.note).toMatch(/status unknown/i);
|
||||
expect(result.note).toContain('--pdg');
|
||||
// Inconclusive ≠ definitive no-layer wording.
|
||||
expect(result.note).not.toMatch(/no PDG layer/i);
|
||||
expect(result.note).not.toMatch(/not yet implemented/i);
|
||||
expect(result.risk).toBe('UNKNOWN');
|
||||
expect(result.impactedCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('callgraph mode is unaffected by the PDG-layer probe', () => {
|
||||
it('mode:callgraph never consults the PDG layer (no degradation note)', async () => {
|
||||
// Even with meta unreadable, a callgraph impact resolves the symbol and
|
||||
// returns a real (here: empty-graph) blast radius, never a PDG note.
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'hot',
|
||||
direction: 'downstream',
|
||||
mode: 'callgraph',
|
||||
});
|
||||
expect(result.pdgLayer).toBeUndefined();
|
||||
// The callgraph path never sets a PDG degradation note. (It may carry
|
||||
// its own callgraph-flavored notes, but never the PDG-layer wording.)
|
||||
const note = typeof result.note === 'string' ? result.note : '';
|
||||
expect(note).not.toMatch(/status unknown/i);
|
||||
expect(note).not.toMatch(/no PDG layer/i);
|
||||
});
|
||||
});
|
||||
},
|
||||
{
|
||||
seed: [...SEED, SEED_EDGE],
|
||||
poolAdapter: true,
|
||||
afterSetup: async (handle) => {
|
||||
vi.mocked(listRegisteredRepos).mockResolvedValue([
|
||||
{
|
||||
name: 'degradation-repo',
|
||||
path: '/degradation/repo',
|
||||
storagePath: handle.tmpHandle.dbPath,
|
||||
indexedAt: new Date().toISOString(),
|
||||
lastCommit: 'deg123',
|
||||
stats: { files: 1, nodes: 3, communities: 0, processes: 0 },
|
||||
},
|
||||
]);
|
||||
const backend = new LocalBackend();
|
||||
await backend.init();
|
||||
(handle as any)._backend = backend;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -47,6 +47,14 @@ vi.mock('../../src/storage/repo-manager.js', async (importOriginal) => {
|
|||
listRegisteredRepos: vi.fn().mockResolvedValue([]),
|
||||
cleanupOldKuzuFiles: vi.fn().mockResolvedValue({ found: false, needsReindex: false }),
|
||||
findSiblingClones: vi.fn().mockResolvedValue([]),
|
||||
// U2: expose loadMeta as a spy that delegates to the REAL implementation by
|
||||
// default (so branch-scope resolution, #2106, is unaffected). The
|
||||
// impact-mode block overrides it per-test to stamp a READY PDG layer, so the
|
||||
// U2 layer-presence probe falls THROUGH to the post-check surface (the
|
||||
// `_runImpactPDG` stub / ambiguous fan-out) those tests assert. The
|
||||
// four-state degradation contract itself is covered in
|
||||
// test/integration/impact-pdg-degradation.test.ts.
|
||||
loadMeta: vi.fn(actual.loadMeta),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -97,7 +105,7 @@ import {
|
|||
REPO_ID_HASH_LENGTH,
|
||||
parseListReposPagination,
|
||||
} from '../../src/mcp/local/local-backend.js';
|
||||
import { listRegisteredRepos, cleanupOldKuzuFiles } from '../../src/storage/repo-manager.js';
|
||||
import { listRegisteredRepos, cleanupOldKuzuFiles, loadMeta } from '../../src/storage/repo-manager.js';
|
||||
import { getGitRoot } from '../../src/storage/git.js';
|
||||
import { _captureLogger } from '../../src/core/logger.js';
|
||||
import {
|
||||
|
|
@ -1409,6 +1417,13 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
|
|||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
platformMocks.isVectorExtensionSupportedByPlatform.mockReturnValue(true);
|
||||
// U2: stamp a READY PDG layer (both caps) so the layer-presence probe in
|
||||
// `_impactImpl` falls THROUGH to the mode-dispatch surface these tests pin
|
||||
// (the `_runImpactPDG` stub / the ambiguous fan-out under `mode:'pdg'`).
|
||||
// Degraded-layer behavior is owned by the integration degradation suite.
|
||||
vi.mocked(loadMeta).mockResolvedValue({
|
||||
pdg: { maxCdgEdgesPerFunction: 0, maxReachingDefEdgesPerFunction: 0 },
|
||||
} as any);
|
||||
backend = new LocalBackend();
|
||||
setupSingleRepo();
|
||||
await backend.init();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue