mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
* docs(plans): add impact file risk plan Capture the evidence, constraints, and verification path for fixing incomparable File and symbol impact risk. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(impact): centralize risk scoring Keep the existing thresholds in one shared scorer and expose a common-axis comparison for targets with unavailable enrichment axes. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(impact): expose incomparable file risk scale Mark File impact results when process and module axes are unavailable, and provide a common-axis score for honest cross-kind comparisons. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(impact): explain cross-kind risk comparisons Surface the common-axis score in CLI and agent guidance while reusing the shared threshold ladder in the web impact tool. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(impact): fail closed when enrichment is incomplete Preserve proved HIGH/CRITICAL process counts, treat failed queries as UNKNOWN, and surface riskScale metadata on MCP, group, CLI, and Graph-RAG File walks. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
143 lines
5.9 KiB
TypeScript
143 lines
5.9 KiB
TypeScript
import { beforeAll, expect, it, vi } from 'vitest';
|
|
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
|
|
import { listRegisteredRepos } from '../../src/storage/repo-manager.js';
|
|
import { withTestLbugDB, type IndexedDBHandle } from '../helpers/test-indexed-db.js';
|
|
|
|
vi.mock('../../src/storage/repo-manager.js', () => ({
|
|
listRegisteredRepos: vi.fn().mockResolvedValue([]),
|
|
cleanupOldKuzuFiles: vi.fn().mockResolvedValue({ found: false, needsReindex: false }),
|
|
findSiblingClones: vi.fn().mockResolvedValue([]),
|
|
}));
|
|
|
|
const fileImporters = Array.from(
|
|
{ length: 13 },
|
|
(_, index) =>
|
|
`CREATE (f:File {id: 'File:src/importer-${index}.ts', name: 'importer-${index}.ts', filePath: 'src/importer-${index}.ts', content: ''})`,
|
|
);
|
|
const fileImportEdges = Array.from(
|
|
{ length: 13 },
|
|
(_, index) =>
|
|
`MATCH (a:File {id:'File:src/importer-${index}.ts'}), (b:File {id:'File:src/crypto.ts'}) CREATE (a)-[:CodeRelation {type:'IMPORTS', confidence:1.0, reason:'import', step:0}]->(b)`,
|
|
);
|
|
const processNodes = Array.from(
|
|
{ length: 4 },
|
|
(_, index) =>
|
|
`CREATE (p:Process {id: 'proc-${index}', label: 'Flow ${index}', heuristicLabel: 'Flow ${index}', processType: 'cross_community', stepCount: 3, communities: [], entryPointId: 'Function:src/entry-${index}.ts:entry${index}', terminalId: 'Function:src/crypto.ts:getEncryptionKey'})`,
|
|
);
|
|
const processEntryPoints = Array.from(
|
|
{ length: 4 },
|
|
(_, index) =>
|
|
`CREATE (ep:Function {id: 'Function:src/entry-${index}.ts:entry${index}', name: 'entry${index}', filePath: 'src/entry-${index}.ts', startLine: 1, endLine: 3, isExported: true, content: '', description: ''})`,
|
|
);
|
|
const processEdges = Array.from(
|
|
{ length: 4 },
|
|
(_, index) =>
|
|
`MATCH (a:Function {id:'Function:src/caller-${index % 2}.ts:caller${index % 2}'}), (p:Process {id:'proc-${index}'}) CREATE (a)-[:CodeRelation {type:'STEP_IN_PROCESS', confidence:1.0, reason:'trace-detection', step:1}]->(p)`,
|
|
);
|
|
|
|
const SEED = [
|
|
`CREATE (f:File {id: 'File:src/crypto.ts', name: 'crypto.ts', filePath: 'src/crypto.ts', content: ''})`,
|
|
...fileImporters,
|
|
...fileImportEdges,
|
|
`CREATE (fn:Function {id: 'Function:src/crypto.ts:getEncryptionKey', name: 'getEncryptionKey', filePath: 'src/crypto.ts', startLine: 1, endLine: 3, isExported: true, content: '', description: ''})`,
|
|
`CREATE (c0:Function {id: 'Function:src/caller-0.ts:caller0', name: 'caller0', filePath: 'src/caller-0.ts', startLine: 1, endLine: 3, isExported: true, content: '', description: ''})`,
|
|
`CREATE (c1:Function {id: 'Function:src/caller-1.ts:caller1', name: 'caller1', filePath: 'src/caller-1.ts', startLine: 1, endLine: 3, isExported: true, content: '', description: ''})`,
|
|
`MATCH (a:Function {id:'Function:src/caller-0.ts:caller0'}), (b:Function {id:'Function:src/crypto.ts:getEncryptionKey'}) CREATE (a)-[:CodeRelation {type:'CALLS', confidence:1.0, reason:'direct', step:0}]->(b)`,
|
|
`MATCH (a:Function {id:'Function:src/caller-1.ts:caller1'}), (b:Function {id:'Function:src/crypto.ts:getEncryptionKey'}) CREATE (a)-[:CodeRelation {type:'CALLS', confidence:1.0, reason:'direct', step:0}]->(b)`,
|
|
...processEntryPoints,
|
|
...processNodes,
|
|
...processEdges,
|
|
];
|
|
|
|
type BackendHandle = IndexedDBHandle & { _backend?: LocalBackend };
|
|
|
|
withTestLbugDB(
|
|
'impact-file-risk-scale',
|
|
(handle) => {
|
|
let backend: LocalBackend;
|
|
beforeAll(() => {
|
|
const ext = handle as BackendHandle;
|
|
if (!ext._backend) throw new Error('LocalBackend not initialized');
|
|
backend = ext._backend;
|
|
});
|
|
|
|
it('marks the wider File score incomparable with the process-rich Function score', async () => {
|
|
const file = await backend.callTool('impact', {
|
|
target: 'crypto.ts',
|
|
kind: 'File',
|
|
direction: 'upstream',
|
|
});
|
|
const fn = await backend.callTool('impact', {
|
|
target: 'getEncryptionKey',
|
|
kind: 'Function',
|
|
direction: 'upstream',
|
|
});
|
|
|
|
expect(file.impactedCount).toBe(13);
|
|
expect(file.risk).toBe('MEDIUM');
|
|
expect(file.riskSharedAxes).toBe('MEDIUM');
|
|
expect(file.target.type).toBe('File');
|
|
expect(file.riskScale).toEqual({
|
|
comparableAcrossKinds: false,
|
|
unusedAxes: [
|
|
{
|
|
axis: 'processes',
|
|
reason: 'file-nodes-have-no-process-or-community-membership',
|
|
},
|
|
{
|
|
axis: 'modules',
|
|
reason: 'file-nodes-have-no-process-or-community-membership',
|
|
},
|
|
],
|
|
});
|
|
expect(file.riskNote).toBeUndefined();
|
|
|
|
expect(fn.impactedCount).toBe(2);
|
|
expect(fn.risk).toBe('HIGH');
|
|
expect(fn.riskSharedAxes).toBe('LOW');
|
|
expect(fn.summary.processes_affected).toBe(4);
|
|
expect(fn.riskScale).toEqual({
|
|
comparableAcrossKinds: true,
|
|
unusedAxes: [],
|
|
});
|
|
expect(fn.riskNote).toBeUndefined();
|
|
});
|
|
|
|
it('marks downstream File risk incomparable on the same seed', async () => {
|
|
const file = await backend.callTool('impact', {
|
|
target: 'crypto.ts',
|
|
kind: 'File',
|
|
direction: 'downstream',
|
|
});
|
|
expect(file.target.type).toBe('File');
|
|
expect(file.riskScale.comparableAcrossKinds).toBe(false);
|
|
expect(file.riskScale.unusedAxes).toEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
axis: 'processes',
|
|
reason: 'file-nodes-have-no-process-or-community-membership',
|
|
},
|
|
]),
|
|
);
|
|
});
|
|
},
|
|
{
|
|
seed: SEED,
|
|
poolAdapter: true,
|
|
afterSetup: async (handle) => {
|
|
vi.mocked(listRegisteredRepos).mockResolvedValue([
|
|
{
|
|
name: 'test-repo',
|
|
path: '/test/repo',
|
|
storagePath: handle.tmpHandle.dbPath,
|
|
indexedAt: new Date().toISOString(),
|
|
lastCommit: 'abc123',
|
|
stats: { files: 14, nodes: 21, communities: 0, processes: 4 },
|
|
},
|
|
]);
|
|
const backend = new LocalBackend();
|
|
await backend.init();
|
|
(handle as BackendHandle)._backend = backend;
|
|
},
|
|
},
|
|
);
|