mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
261 lines
12 KiB
TypeScript
261 lines
12 KiB
TypeScript
/**
|
|
* 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 after symbol resolution but before traversal so a missing or partial
|
|
* `--pdg` layer returns a distinct target-aware guidance note instead of a
|
|
* confusing empty blast radius.
|
|
*
|
|
* 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 traversal
|
|
* - unknown : meta unreadable (null) → inconclusive, via 1 LIMIT 1 probe
|
|
*
|
|
* The `ready` case asserts the layer check lets the call THROUGH to the real
|
|
* traversal, while degraded states return before `_runImpactPDG`.
|
|
*/
|
|
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';
|
|
|
|
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 mockResolvedValue (reset in
|
|
// beforeEach; not Once — the staleness check in ensureInitialized also
|
|
// calls loadMeta and must not starve the PDG caps read of its value).
|
|
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;
|
|
|
|
function expectEmptyPdgParity(result: any): void {
|
|
expect(result.mode).toBe('pdg');
|
|
expect(result.direction).toBe('downstream');
|
|
expect(result.impactedCount).toBe(0);
|
|
expect(result.risk).toBe('UNKNOWN');
|
|
expect(result.byDepth).toEqual({});
|
|
expect(result.byDepthCounts).toEqual({ 1: 0 });
|
|
expect(result.summary).toEqual({ direct: 0, processes_affected: 0, modules_affected: 0 });
|
|
expect(result.affected_processes).toEqual([]);
|
|
expect(result.affected_modules).toEqual([]);
|
|
}
|
|
|
|
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
|
|
// mockResolvedValue 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 target-aware "run analyze --pdg" note', async () => {
|
|
// Readable meta with no `pdg` key ⇒ the layer was never recorded.
|
|
vi.mocked(loadMeta).mockResolvedValue(META(undefined));
|
|
const result = await backend.callTool('impact', {
|
|
target: 'hot',
|
|
direction: 'downstream',
|
|
mode: 'pdg',
|
|
});
|
|
|
|
expect(result.mode).toBe('pdg');
|
|
expect(result.pdgLayer).toBe('no-layer');
|
|
expect(result.target).toEqual({
|
|
id: 'func:hot',
|
|
name: 'hot',
|
|
type: 'Function',
|
|
filePath: 'src/hot.ts',
|
|
});
|
|
expect(result.note).toMatch(/no PDG layer/i);
|
|
expect(result.note).toContain('--pdg');
|
|
// 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);
|
|
expectEmptyPdgParity(result);
|
|
});
|
|
});
|
|
|
|
describe('sub-layer-missing (exactly one cap stamped)', () => {
|
|
it('CDG present, RD absent → names REACHING_DEF as missing', async () => {
|
|
vi.mocked(loadMeta).mockResolvedValue(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.target.filePath).toBe('src/hot.ts');
|
|
expect(result.target.type).toBe('Function');
|
|
expect(result.missingSubLayer).toBe('REACHING_DEF');
|
|
expect(result.note).toMatch(/REACHING_DEF/);
|
|
// Partial layer must NOT be reported as complete (no LOW).
|
|
expect(result.note).not.toMatch(/not yet implemented/i);
|
|
expectEmptyPdgParity(result);
|
|
});
|
|
|
|
it('RD present, CDG absent → names CDG as missing', async () => {
|
|
vi.mocked(loadMeta).mockResolvedValue(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);
|
|
expectEmptyPdgParity(result);
|
|
});
|
|
});
|
|
|
|
describe('ready (both caps stamped)', () => {
|
|
it('falls THROUGH the layer check to the real traversal (U3 _runImpactPDG)', async () => {
|
|
vi.mocked(loadMeta).mockResolvedValue(
|
|
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 traversal.
|
|
expect(result.pdgLayer).toBeUndefined();
|
|
// `hot` has a
|
|
// PDG body (blocks B0→B1) but the only dependent (B1) is itself a seed
|
|
// block of the symbol, so the intra-procedural downstream reachable set
|
|
// is empty — and that is signalled as a real traversal result with the
|
|
// distinct "has a body but no dependence" note, NOT the no-body /
|
|
// degradation path. The load-bearing U2 fact —
|
|
// `ready` does NOT return a degradation note — still holds.
|
|
expect(result.mode).toBe('pdg');
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.note).not.toMatch(/not yet implemented/i);
|
|
expect(Array.isArray(result.reachableBlocks)).toBe(true);
|
|
// Distinct from KTD6 "no PDG body": this symbol HAS a body.
|
|
expect(result.epistemic).not.toBe('no-pdg-body');
|
|
});
|
|
|
|
it('a line-seeded slice over callee-less blocks degrades gracefully (real-DB calleesOfBlocks)', async () => {
|
|
// The seeded BasicBlocks carry no `callees` data (created without the
|
|
// property — the pre-v2 / no-calls reality). A downstream line seed at
|
|
// B0 reaches B1 via the CDG edge, so calleesOfBlocks runs over real
|
|
// seed+reachable blocks; with no callee data it must yield an empty set
|
|
// and degrade to callgraph-equal — no throw, no partial precision.
|
|
vi.mocked(loadMeta).mockResolvedValue(
|
|
META({ maxCdgEdgesPerFunction: 0, maxReachingDefEdgesPerFunction: 0 } as any),
|
|
);
|
|
const result = await backend.callTool('impact', {
|
|
target: 'hot',
|
|
direction: 'downstream',
|
|
mode: 'pdg',
|
|
line: 2,
|
|
});
|
|
// The slice resolved (B1 is downstream-dependent on the line-2 seed) and
|
|
// the call completed without surfacing a callees-query failure.
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.epistemic).toBe('pdg-intra-procedural');
|
|
expect(result.affectedStatementCount).toBeGreaterThanOrEqual(1);
|
|
// No CALLS edge in the fixture and no callee data on the blocks, so the
|
|
// statement-precise inter-procedural reach is empty — precision is null
|
|
// (no reach), never a partial value, and nothing threw.
|
|
expect(result.pdgInterprocedural.statementPrecision).toBeNull();
|
|
});
|
|
});
|
|
|
|
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.target.filePath).toBe('src/hot.ts');
|
|
expect(result.target.type).toBe('Function');
|
|
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);
|
|
expectEmptyPdgParity(result);
|
|
});
|
|
});
|
|
|
|
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;
|
|
},
|
|
},
|
|
);
|