fix(test): align fixtures and mocks with current types (#3471)

This commit is contained in:
Gergő Magyar 2026-10-05 04:47:37 +01:00 • committed by GitHub
parent 504bff7102
commit c474a811ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
187 changed files with 1705 additions and 1186 deletions

View file

@ -29,7 +29,7 @@ export interface CfgHarness {
* CFG shape). The parser is created once and reused across parses.
*/
export function makeCfgHarness(
grammar: Parser.Language,
grammar: Parameters<Parser['setLanguage']>[0],
visitor: CfgVisitor<SyntaxNode>,
filePath = 'fixture',
): CfgHarness {

View file

@ -1,4 +1,4 @@
import { vi } from 'vitest';
import { vi, type Mock } from 'vitest';
import type * as SafeParseModule from '../../src/core/tree-sitter/safe-parse.js';
/**
@ -43,7 +43,7 @@ import type * as SafeParseModule from '../../src/core/tree-sitter/safe-parse.js'
* });
*/
export async function buildSafeParseMock(
spy: ReturnType<typeof vi.fn>,
spy: Mock<typeof SafeParseModule.parseSourceSafe>,
): Promise<typeof SafeParseModule> {
const actual = await vi.importActual<typeof SafeParseModule>(
'../../src/core/tree-sitter/safe-parse.js',

View file

@ -30,10 +30,8 @@ export interface ParsedSource {
const PARSE_PLUGINS: NonNullable<Parameters<typeof parse>[1]>['plugins'] = [
'typescript',
'explicitResourceManagement',
'importAttributes',
'decoratorAutoAccessors',
['decorators', { decoratorsBeforeExport: true }],
'decorators',
];
export function forEachChild(node: Node, visit: (child: AstNode) => void): void {

View file

@ -17,7 +17,19 @@ export default class PerfSequencer extends BaseSequencer {
override async shard(specs: TestSpecification[]): Promise<TestSpecification[]> {
const shard = this.ctx.config.shard;
if (!shard) return specs;
const groups = assignShards(specs, shard.count, specWeight, (spec) => spec.moduleId);
const groups = assignShards(
specs,
shard.count,
(spec) => {
const config = spec.project.config;
const fileParallelism =
'fileParallelism' in config && typeof config.fileParallelism === 'boolean'
? config.fileParallelism
: undefined;
return specWeight({ moduleId: spec.moduleId, project: { config: { fileParallelism } } });
},
(spec) => spec.moduleId,
);
return groups[shard.index - 1] ?? [];
}
}

View file

@ -5,7 +5,8 @@
* without touching the filesystem or LadybugDB.
*/
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
import type { KnowledgeGraph, NodeLabel, RelationshipType } from '../../src/core/graph/types.js';
import type { KnowledgeGraph } from '../../src/core/graph/types.js';
import type { NodeLabel, RelationshipType } from 'gitnexus-shared';
export interface TestNodeInput {
id: string;

View file

@ -51,8 +51,8 @@ withTestLbugDB(
});
expect(response.status).toBe(200);
const body = await response.json();
expect(Array.isArray(body.result)).toBe(true);
expect(body.result[0].one).toBe(1);
expect(body).toHaveProperty('result', expect.any(Array));
expect(body).toHaveProperty('result.0.one', 1);
});
it('returns 403 for a write query on read-only HTTP path', async () => {
@ -65,7 +65,10 @@ withTestLbugDB(
});
expect(response.status).toBe(403);
const body = await response.json();
expect(body.error).toContain('Write queries are not allowed');
expect(body).toHaveProperty(
'error',
expect.stringContaining('Write queries are not allowed'),
);
});
it('returns 400 for invalid params payload', async () => {
@ -76,7 +79,7 @@ withTestLbugDB(
});
expect(response.status).toBe(400);
const body = await response.json();
expect(body.error).toContain('"params"');
expect(body).toHaveProperty('error', expect.stringContaining('"params"'));
});
it('returns 400 when cypher is missing', async () => {
@ -87,7 +90,7 @@ withTestLbugDB(
});
expect(response.status).toBe(400);
const body = await response.json();
expect(body.error).toContain('Missing "cypher"');
expect(body).toHaveProperty('error', expect.stringContaining('Missing "cypher"'));
});
});
},

View file

@ -490,6 +490,7 @@ describe('U4 (#2085 M5) — emitFileCdg', () => {
const unsound: FunctionCfg = {
filePath: 'spin.ts',
functionStartLine: 1,
functionEndLine: 4,
functionStartColumn: 0,
entryIndex: 0,
exitIndex: 3,

View file

@ -68,7 +68,7 @@ describe('clean --stale leftover branch slots (#3331)', () => {
}
vi.spyOn(process, 'cwd').mockReturnValue(repo);
vi.spyOn(console, 'log').mockImplementation(() => {});
return { dir, storagePath };
return { repo, dir, storagePath };
}
it('reclaims a slot after the git branch is deleted', async () => {

View file

@ -3,7 +3,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../types/pipeline.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
import { DATA_ROUTE_TABLE_SOURCE } from '../../src/core/ingestion/route-extractors/data-route-table.js';
import {
loadParseCache,

View file

@ -20,7 +20,7 @@
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../types/pipeline.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
import { DISPATCH_GUARD_SOURCE } from '../../src/core/ingestion/route-extractors/dispatch-guard.js';
const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'dispatch-guard-app');

View file

@ -13,7 +13,7 @@
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../types/pipeline.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'django-subdir-app');

View file

@ -59,7 +59,6 @@ describe('doc-comment description end-to-end (issue #2270)', () => {
it('surfaces an exported function JSDoc as its node description through the pipeline', async () => {
const result: PipelineResult = await runPipelineFromRepo(createTsRepo(), () => {}, {
skipGraphPhases: true,
workerThresholdsForTest: { minFiles: 1, minBytes: 1 },
workerPoolSize: 2,
});
@ -79,7 +78,6 @@ describe('doc-comment description end-to-end (issue #2270)', () => {
async () => {
const result: PipelineResult = await runPipelineFromRepo(createSwiftRepo(), () => {}, {
skipGraphPhases: true,
workerThresholdsForTest: { minFiles: 1, minBytes: 1 },
workerPoolSize: 2,
});

View file

@ -1,7 +1,7 @@
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../types/pipeline.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
const EXPO_APP = path.resolve(__dirname, '..', 'fixtures', 'expo-app');

View file

@ -21,7 +21,7 @@ import * as os from 'node:os';
import Parser from 'tree-sitter';
import Python from 'tree-sitter-python';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../types/pipeline.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
import { PYTHON_HTTP_PLUGIN } from '../../src/core/group/extractors/http-patterns/python.js';
import {
loadParseCache,
@ -141,6 +141,7 @@ describe('FastAPI composed route constants — ingestion↔group parity (#2391 R
return p.parse(src);
};
const ctx = PYTHON_HTTP_PLUGIN.prepareRepo?.({
repoPath: '/test-repo',
files: Object.keys(files),
parser,
readFile: (r) => files[r] ?? null,

View file

@ -46,7 +46,7 @@
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../types/pipeline.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'fastapi-prefix-app');

View file

@ -92,7 +92,7 @@ describe('warm MCP session observes an in-place --repair-fts rebuild (#2767)', (
await tmpHandle.cleanup();
});
it.each(['all', 'Function'] as const)(
it.for(['all', 'Function'] as const)(
'a warm session repairs missing %s indexes without restarting',
{ timeout: 60_000 },
async (missing, ctx) => {

View file

@ -32,7 +32,6 @@ function createGoRepo(): string {
async function runWorker(): Promise<PipelineResult> {
return runPipelineFromRepo(createGoRepo(), () => {}, {
skipGraphPhases: true,
workerThresholdsForTest: { minFiles: 1, minBytes: 1 },
workerPoolSize: 2,
});
}

View file

@ -32,7 +32,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared';
import { makeScopeId, type ParsedFile, type SymbolDefinition } from 'gitnexus-shared';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import {
emitGoScopeCaptures,
@ -587,14 +587,17 @@ function generateSyntheticInterfaceData(interfaceCount: number, structCount: num
return [
{
filePath: 'repo.go',
language: 'go',
moduleScope: makeScopeId({
filePath: 'repo.go',
range: { startLine: 0, startCol: 0, endLine: 0, endCol: 0 },
kind: 'Module',
}),
scopes: [],
imports: [],
parsedImports: [],
localDefs: defs,
referenceSites: [],
},
] as ParsedFile[];
];
}
/**

View file

@ -447,7 +447,14 @@ describe('native string projections after checkpointed deletion (#3354)', () =>
for (const order of ['', ' ORDER BY n.startLine']) {
const rows = await read(`MATCH (n:Function) ${projection}${order}`);
expect(new Set(rows.map((row) => row.id)).size).toBe(surviving.length);
expect(rows.sort((a, b) => a.startLine - b.startLine)).toEqual(surviving);
expect(
rows.sort((a, b) => {
if (typeof a.startLine !== 'number' || typeof b.startLine !== 'number') {
throw new Error('Expected numeric startLine values from the database');
}
return a.startLine - b.startLine;
}),
).toEqual(surviving);
}
}
// Point lookups independently verify values in the affected segments;

View file

@ -124,12 +124,15 @@ withTestLbugDB(
it('marks impact as a lower bound when scope extraction omitted files', async () => {
vi.mocked(loadMeta).mockResolvedValueOnce({
repoPath: '/test/repo',
lastCommit: 'abc123',
indexedAt: new Date().toISOString(),
scopeExtractionReceipt: 1,
scopeExtractionFailures: {
total: 2,
paths: ['src/broken-a.ts', 'src/broken-b.ts'],
},
} as Awaited<ReturnType<typeof loadMeta>>);
});
const result = await backend.callTool('impact', {
target: 'formatDate',
@ -143,12 +146,15 @@ withTestLbugDB(
it('never renders repository-controlled failure paths in boundary prose', async () => {
vi.mocked(loadMeta).mockResolvedValueOnce({
repoPath: '/test/repo',
lastCommit: 'abc123',
indexedAt: new Date().toISOString(),
scopeExtractionReceipt: 1,
scopeExtractionFailures: {
total: 1,
paths: ['src/`break`\n\u001b[31m\u202e\u200binject.ts'],
},
} as Awaited<ReturnType<typeof loadMeta>>);
});
const result = await backend.callTool('impact', {
target: 'formatDate',
@ -239,12 +245,15 @@ withTestLbugDB(
it('context() reports persisted scope extraction omissions as a lower bound', async () => {
vi.mocked(loadMeta).mockResolvedValueOnce({
repoPath: '/test/repo',
lastCommit: 'abc123',
indexedAt: new Date().toISOString(),
scopeExtractionReceipt: 1,
scopeExtractionFailures: {
total: 2,
paths: ['src/broken-a.ts', 'src/broken-b.ts'],
},
} as Awaited<ReturnType<typeof loadMeta>>);
});
const result = await backend.callTool('context', {
name: 'formatDate',

View file

@ -279,6 +279,7 @@ withTestLbugDB(
const result = await downstream();
const localRisk = String(result.risk);
expect(localRisk).toBe('UNKNOWN');
if (localRisk !== 'UNKNOWN') throw new Error(`Expected UNKNOWN risk, got ${localRisk}`);
// No cross-repo hits → mergeRisk returns localRisk verbatim: 'UNKNOWN',
// NEVER coerced to a confident 'LOW' (the false-safe this guards).
expect(mergeRisk(localRisk, [])).toBe('UNKNOWN');

View file

@ -10,7 +10,7 @@ import os from 'node:os';
import path from 'node:path';
import { requireVendoredGrammar } from '../../src/core/tree-sitter/vendored-grammars.js';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../types/pipeline.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
import {
loadParseCache,
PARSE_CACHE_VERSION,

View file

@ -356,7 +356,9 @@ withTestLbugDB(
const result = await backend.callTool('tool_map', {});
expect(result).not.toHaveProperty('error');
const tools = new Map(result.tools.map((tool: any) => [tool.name, tool]));
const toolEntries: Array<{ name: string; description: string; flows: string[] }> =
result.tools;
const tools = new Map(toolEntries.map((tool) => [tool.name, tool]));
expect(tools.get('alpha')?.description).toBe('Calls chain A.');
expect(tools.get('beta')?.description).toBe('Calls chain B.');
expect(tools.get('alpha')?.flows).toEqual(['AlphaFlow']);

View file

@ -18,7 +18,7 @@ interface FakeChecker {
}
interface FakeLogger {
info: ReturnType<typeof vi.fn>;
info: ReturnType<typeof vi.fn<(bindings: Record<string, unknown>, message: string) => unknown>>;
}
function checker(initial: UpdateState | null): {

View file

@ -21,7 +21,7 @@ import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import { generateId } from '../../src/lib/utils.js';
import { routeNodeKey } from '../../src/core/ingestion/route-extractors/route-path.js';
import type { PipelineResult } from '../../types/pipeline.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'multi-verb-route-app');

View file

@ -134,7 +134,7 @@ withTestLbugDB(
// An MCP client may send {"name":"pdg_query"} with no `arguments` field;
// the dispatch then hands `params: undefined` to the impl. It must
// default to {} and surface the mode-validation error, not a TypeError.
const result = await backend.callTool('pdg_query');
const result = await backend.callTool('pdg_query', undefined);
expect(result).toHaveProperty('error');
expect(result.error).toMatch(/mode/i);
});

View file

@ -323,7 +323,12 @@ function callableTargetQualifiedNames(
): string[] {
return getRelationships(result, 'CALLS')
.filter((edge) => edge.source === source && edge.rel.reason === 'callable-value-flow')
.map((edge) => result.graph.getNode(edge.rel.targetId)?.properties.qualifiedName ?? edge.target)
.map((edge) => {
const qualifiedName = result.graph.getNode(edge.rel.targetId)?.properties.qualifiedName;
if (qualifiedName == null) return edge.target;
if (typeof qualifiedName !== 'string') throw new Error('Expected a string qualified name');
return qualifiedName;
})
.sort();
}
@ -347,7 +352,10 @@ describe('callable value flow', () => {
it('does not overstate Objective-C callable-value-flow coverage', () => {
expect(CALLABLE_FLOW_PROVIDER_COVERAGE[SupportedLanguages.ObjectiveC]).toBe('not-applicable');
expect(
PROVIDER_FLOW_CASES.some(({ language }) => language === SupportedLanguages.ObjectiveC),
PROVIDER_FLOW_CASES.some(
({ language }: { language: SupportedLanguages }) =>
language === SupportedLanguages.ObjectiveC,
),
).toBe(false);
});

View file

@ -1757,7 +1757,11 @@ describe('C++ template specialization disambiguation across files', () => {
expect(targetOwnerEdge).toBeDefined();
expect(sourceOwnerEdge!.rel.sourceId).toBe(targetOwnerEdge!.rel.sourceId);
const ownerNode = result.graph.getNode(sourceOwnerEdge!.rel.sourceId);
const fp = ownerNode?.properties.templateArguments?.join(',');
const templateArguments = ownerNode?.properties.templateArguments;
if (templateArguments != null && !Array.isArray(templateArguments)) {
throw new Error('Expected template arguments to be an array');
}
const fp = Array.isArray(templateArguments) ? templateArguments.join(',') : undefined;
if (fp) ownerFingerprints.add(fp);
}
expect(ownerFingerprints).toEqual(new Set(['User', 'Order']));
@ -2125,14 +2129,15 @@ describe('C++ ambiguous integer-width overloads', () => {
});
it('records a structured suppression reason for normalization ambiguity', () => {
const outcomes = getResolutionOutcomes(result).filter(
(o) =>
o.kind === 'suppressed' &&
o.name === 'process' &&
o.phase === 'receiver-bound-calls' &&
o.filePath.endsWith('caller.cpp') &&
o.reason === 'overload-ambiguous-normalization',
);
const outcomes = getResolutionOutcomes(result)
.filter((o) => o.kind === 'suppressed')
.filter(
(o) =>
o.name === 'process' &&
o.phase === 'receiver-bound-calls' &&
o.filePath.endsWith('caller.cpp') &&
o.reason === 'overload-ambiguous-normalization',
);
expect(outcomes.length).toBeGreaterThan(0);
expect(outcomes[0]?.candidateIds.length).toBe(2);
@ -2214,13 +2219,12 @@ describe('C++ overload resolution — conversion-rank disambiguation (#1578)', (
});
it('records a structured suppression reason for conversion-rank ties', () => {
const outcomes = getResolutionOutcomes(result).filter(
(o) =>
o.kind === 'suppressed' &&
o.name === 'h' &&
o.phase === 'free-call-fallback' &&
o.reason === 'conversion-rank-tied',
);
const outcomes = getResolutionOutcomes(result)
.filter((o) => o.kind === 'suppressed')
.filter(
(o) =>
o.name === 'h' && o.phase === 'free-call-fallback' && o.reason === 'conversion-rank-tied',
);
expect(outcomes.length).toBeGreaterThan(0);
expect(outcomes[0]?.candidateIds.length).toBe(2);
@ -3256,13 +3260,14 @@ describe('C++ ADL — non-function ordinary lookup suppresses ADL', () => {
});
it('records a structured suppression reason for ADL blocker lookup', () => {
const outcomes = getResolutionOutcomes(result).filter(
(o) =>
o.kind === 'suppressed' &&
o.name === 'record' &&
o.phase === 'free-call-fallback' &&
o.reason === 'adl-ordinary-lookup-blocked',
);
const outcomes = getResolutionOutcomes(result)
.filter((o) => o.kind === 'suppressed')
.filter(
(o) =>
o.name === 'record' &&
o.phase === 'free-call-fallback' &&
o.reason === 'adl-ordinary-lookup-blocked',
);
expect(outcomes.length).toBeGreaterThan(0);
expect(outcomes[0]?.candidateIds.length).toBe(0);
@ -3552,13 +3557,14 @@ describe('C++ inline namespace — ambiguous same-name across inline children (#
});
it('records a structured suppression reason for inline namespace ambiguity', () => {
const outcomes = getResolutionOutcomes(result).filter(
(o) =>
o.kind === 'suppressed' &&
o.name === 'foo' &&
o.phase === 'receiver-bound-calls' &&
o.reason === 'inline-ns-ambiguous',
);
const outcomes = getResolutionOutcomes(result)
.filter((o) => o.kind === 'suppressed')
.filter(
(o) =>
o.name === 'foo' &&
o.phase === 'receiver-bound-calls' &&
o.reason === 'inline-ns-ambiguous',
);
expect(outcomes.length).toBeGreaterThan(0);
expect(outcomes[0]?.candidateIds.length).toBe(0);

View file

@ -413,6 +413,7 @@ describe('JavaScript plain-object property access (A1/A5)', () => {
});
interface PropNode {
readonly id: string;
readonly label: string;
readonly properties: Record<string, unknown>;
}

View file

@ -1146,6 +1146,7 @@ describe('Python mixin self-dispatch', () => {
expect(
getResolutionOutcomes(result).some(
(outcome) =>
outcome.kind === 'suppressed' &&
outcome.filePath === 'mixins.py' &&
outcome.name === 'class_only' &&
outcome.reason === 'receiver-unresolved',
@ -1202,11 +1203,9 @@ describe('Python mixin self-dispatch', () => {
it('records only the expected mixin dispatch gaps and partial coverage', () => {
const unresolvedSites = getResolutionOutcomes(result)
.filter((outcome) => outcome.kind === 'suppressed')
.filter(
(outcome) =>
outcome.kind === 'suppressed' &&
outcome.reason === 'receiver-unresolved' &&
outcome.filePath === 'mixins.py',
(outcome) => outcome.reason === 'receiver-unresolved' && outcome.filePath === 'mixins.py',
)
.map((outcome) => `${outcome.range.startLine}:${outcome.name}`)
.sort();
@ -1325,6 +1324,7 @@ describe('Python mixin self-dispatch', () => {
expect(
getResolutionOutcomes(result).some(
(outcome) =>
outcome.kind === 'suppressed' &&
outcome.filePath === 'mixins.py' &&
outcome.name === 'duplicate_hook' &&
outcome.reason === 'member-lookup-ambiguous',
@ -1378,12 +1378,9 @@ describe('Python unproven subtype methods', () => {
expect(calls.map((call) => call.rel.targetId)).toEqual([
expect.stringContaining('Concrete.hook'),
]);
const unresolved = getResolutionOutcomes(result).filter(
(outcome) =>
outcome.kind === 'suppressed' &&
outcome.name === 'hook' &&
outcome.reason === 'receiver-unresolved',
);
const unresolved = getResolutionOutcomes(result)
.filter((outcome) => outcome.kind === 'suppressed')
.filter((outcome) => outcome.name === 'hook' && outcome.reason === 'receiver-unresolved');
expect(unresolved.flatMap((outcome) => outcome.candidateIds).sort()).toEqual([
// AbstractWorker.hook (line 10) and Receiverless.hook (line 13).
'def:worker.py#10:4:Method:hook',
@ -1419,6 +1416,7 @@ describe('Python unproven subtype methods', () => {
]);
expect(
getResolutionOutcomes(result)
.filter((outcome) => outcome.kind === 'suppressed')
.filter((outcome) => outcome.name === 'hook' && outcome.reason === 'receiver-unresolved')
.flatMap((outcome) => outcome.candidateIds),
).toEqual([expect.stringMatching(/:Class:Base$/)]);
@ -1459,6 +1457,7 @@ class MissingHook(Mixin):
expect(
getResolutionOutcomes(result).some(
(outcome) =>
outcome.kind === 'suppressed' &&
outcome.name === 'hook' &&
outcome.reason === 'receiver-unresolved' &&
outcome.candidateIds.some((id) => id.endsWith(':Class:MissingHook')),
@ -1547,7 +1546,10 @@ class Mixin:
);
expect(
getResolutionOutcomes(result).some(
(outcome) => outcome.name === target && outcome.reason === 'receiver-unresolved',
(outcome) =>
outcome.kind === 'suppressed' &&
outcome.name === target &&
outcome.reason === 'receiver-unresolved',
),
).toBe(true);
}
@ -1647,7 +1649,10 @@ class ReboundProperty:
);
expect(
getResolutionOutcomes(result).some(
(outcome) => outcome.name === target && outcome.reason === 'receiver-unresolved',
(outcome) =>
outcome.kind === 'suppressed' &&
outcome.name === target &&
outcome.reason === 'receiver-unresolved',
),
).toBe(true);
}
@ -1660,13 +1665,17 @@ class ReboundProperty:
expect(
getResolutionOutcomes(result).some(
(outcome) =>
outcome.name === 'wrapped_helper' && outcome.reason === 'receiver-unresolved',
outcome.kind === 'suppressed' &&
outcome.name === 'wrapped_helper' &&
outcome.reason === 'receiver-unresolved',
),
).toBe(true);
expect(
getResolutionOutcomes(result).some(
(outcome) =>
outcome.name === 'rebound_helper' && outcome.reason === 'receiver-unresolved',
outcome.kind === 'suppressed' &&
outcome.name === 'rebound_helper' &&
outcome.reason === 'receiver-unresolved',
),
).toBe(true);
} finally {
@ -1752,6 +1761,7 @@ class Worker(Mixin):
expect(
getResolutionOutcomes(result).some(
(outcome) =>
outcome.kind === 'suppressed' &&
outcome.filePath === 'case.py' &&
outcome.name === 'hook' &&
outcome.reason === 'receiver-unresolved',
@ -1760,6 +1770,7 @@ class Worker(Mixin):
expect(
getResolutionOutcomes(result).some(
(outcome) =>
outcome.kind === 'suppressed' &&
outcome.filePath === 'case.py' &&
outcome.name === 'own_hook' &&
outcome.reason === 'receiver-unresolved',
@ -1768,6 +1779,7 @@ class Worker(Mixin):
expect(
getResolutionOutcomes(result).some(
(outcome) =>
outcome.kind === 'suppressed' &&
outcome.filePath === 'case.py' &&
outcome.name === 'unicode_helper' &&
outcome.reason === 'receiver-unresolved',
@ -1776,6 +1788,7 @@ class Worker(Mixin):
expect(
getResolutionOutcomes(result).some(
(outcome) =>
outcome.kind === 'suppressed' &&
outcome.filePath === 'case.py' &&
outcome.name === 'nested_hook' &&
outcome.reason === 'receiver-unresolved',
@ -1855,6 +1868,7 @@ class DirectWorker(HookMixin, external.Parent, First, Second):
expect(
getResolutionOutcomes(result).some(
(outcome) =>
outcome.kind === 'suppressed' &&
outcome.name === 'hook' &&
outcome.reason === 'receiver-unresolved' &&
outcome.candidateIds.some((id) => id.endsWith(':Class:Worker')),
@ -1896,6 +1910,7 @@ class DirectWorker(HookMixin):
expect(
getResolutionOutcomes(result).some(
(outcome) =>
outcome.kind === 'suppressed' &&
outcome.name === 'hook' &&
outcome.reason === 'receiver-unresolved' &&
outcome.candidateIds.some((id) => id.endsWith(':Class:Worker')),

View file

@ -47,7 +47,7 @@
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'node:path';
import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../../types/pipeline.js';
import type { PipelineResult } from '../../../src/types/pipeline.js';
const LANG_FIXTURES = path.resolve(__dirname, '..', '..', 'fixtures', 'lang-resolution');

View file

@ -186,9 +186,9 @@ describe('shared sibling store analyze (#3352)', () => {
const conn = new lbug.Connection(db);
let rows: { p: string }[];
try {
rows = (await (
await conn.query('MATCH (f:File) RETURN f.filePath AS p ORDER BY p')
).getAll()) as { p: string }[];
const result = await conn.query('MATCH (f:File) RETURN f.filePath AS p ORDER BY p');
if (Array.isArray(result)) throw new Error('Expected a single query result');
rows = (await result.getAll()) as { p: string }[];
} finally {
await conn.close();
await db.close();

View file

@ -41,9 +41,9 @@ const queryNames = async (graph: string): Promise<string[]> => {
const db = new lbug.Database(graph, 0, true, true);
const conn = new lbug.Connection(db);
try {
const rows = (await (
await conn.query('MATCH (f:Function) RETURN f.name AS n ORDER BY n')
).getAll()) as { n: string }[];
const result = await conn.query('MATCH (f:Function) RETURN f.name AS n ORDER BY n');
if (Array.isArray(result)) throw new Error('Expected a single query result');
const rows = (await result.getAll()) as { n: string }[];
return rows.map((r) => r.n);
} finally {
await conn.close();

View file

@ -367,8 +367,14 @@ describe('FTS opt-out analysis lifecycle (#3091)', () => {
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body.warning).toContain('FTS disabled for this index');
expect(body.warning).not.toMatch(/failed to load|indexes missing|repair-fts/);
expect(body).toHaveProperty(
'warning',
expect.stringContaining('FTS disabled for this index'),
);
expect(body).toHaveProperty(
'warning',
expect.not.stringMatching(/failed to load|indexes missing|repair-fts/),
);
}
} finally {
if (server.exitCode === null) server.kill('SIGTERM');

View file

@ -1,7 +1,7 @@
import { beforeAll, describe, expect, it } from 'vitest';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../types/pipeline.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'spring-bean-app');

View file

@ -4,7 +4,7 @@ import { beforeAll, describe, expect, it, vi } from 'vitest';
import type { GraphNode, GraphRelationship } from 'gitnexus-shared';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import { SPRING_CONFIG_DESCRIPTION } from '../../src/core/ingestion/frameworks/spring/config-bindings.js';
import type { PipelineResult } from '../../types/pipeline.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
import { createTempDir } from '../helpers/test-db.js';
const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'spring-config-app');

View file

@ -10,7 +10,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../types/pipeline.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
const ORDER_API = `package com.example;
import org.springframework.web.bind.annotation.*;

View file

@ -13,7 +13,7 @@
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../types/pipeline.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'spring-route-app');

View file

@ -76,7 +76,7 @@ export function handle(req: any, db: any, map: Map<string, string>, task: any, r
.map((rel) => {
const sink = result.graph.getNode(rel.targetId);
const decoded = decodeTaintPath(rel.reason);
if (!decoded.ok) {
if (decoded.ok === false) {
throw new Error(`invalid TAINTED reason for ${rel.id}: ${decoded.error}`);
}
return {

View file

@ -81,7 +81,9 @@ describe('watch filesystem integration', () => {
const loop = await startWatchFileLoop(
repo,
30,
async (paths) => batches.push([...paths]),
async (paths) => {
batches.push([...paths]);
},
(error) => {
throw error;
},
@ -166,7 +168,9 @@ describe('watch filesystem integration', () => {
const loop = await startWatchFileLoop(
repo,
25,
async (paths) => batches.push([...paths]),
async (paths) => {
batches.push([...paths]);
},
(error) => {
throw error;
},
@ -224,7 +228,9 @@ describe('watch filesystem integration', () => {
const loop = await startWatchFileLoop(
repo,
25,
async (paths) => batches.push([...paths]),
async (paths) => {
batches.push([...paths]);
},
(_error, paths) => errors.push([...paths]),
);
loops.push(loop);
@ -252,7 +258,9 @@ describe('watch filesystem integration', () => {
const loop = await startWatchFileLoop(
repo,
25,
async (paths) => batches.push([...paths]),
async (paths) => {
batches.push([...paths]);
},
(error) => {
throw error;
},

View file

@ -1367,7 +1367,7 @@ describe('worker pool integration', () => {
]);
// Deterministically: a.ts crashes round 1, b.ts crashes round 2.
const quarantine = (pool.getQuarantinedPaths?.() ?? []).sort();
const quarantine = [...(pool.getQuarantinedPaths?.() ?? [])].sort();
expect(quarantine).toEqual(['a.ts', 'b.ts']);
// All non-quarantined files eventually parsed by the survivor slot.
const allPaths = results.flatMap((r) => r.paths).sort();

View file

@ -1667,7 +1667,7 @@ describe('count-only reindex does not churn the committed block (#2907)', () =>
describe('--no-stats drops the per-cluster symbol counts too (#2907)', () => {
const stats = { nodes: 10, edges: 20, processes: 3 };
const skills = [{ label: 'ingestion', name: 'p-ingestion', symbolCount: 120 }];
const skills = [{ label: 'ingestion', name: 'p-ingestion', symbolCount: 120, fileCount: 10 }];
it('omits the count under --no-stats and keeps it otherwise', () => {
const lean = generateGitNexusContent('P', stats, { generatedSkills: skills, noStats: true });

View file

@ -152,7 +152,11 @@ const invokeDelete = (route: string, jobId: string): { statusCode: number; body:
return this;
},
};
handler({ params: { jobId } }, res);
handler(
{ params: { jobId } } as unknown as express.Request,
res as unknown as express.Response,
vi.fn(),
);
return res;
};

View file

@ -22,8 +22,12 @@ const {
getDefaultBranchMock,
} = vi.hoisted(() => ({
runFullAnalysisMock: vi.fn(),
generateAIContextFilesMock: vi.fn(async () => ({ files: [] as string[] })),
refreshBaseRefLineMock: vi.fn(async () => ({ files: [] as string[] })),
generateAIContextFilesMock: vi.fn<
typeof import('../../src/cli/ai-context.js').generateAIContextFiles
>(async () => ({ files: [] as string[] })),
refreshBaseRefLineMock: vi.fn<typeof import('../../src/cli/ai-context.js').refreshBaseRefLine>(
async () => ({ files: [] as string[] }),
),
generateSkillFilesMock: vi.fn(async () => ({
skills: [{ name: 'c', label: 'Community', symbolCount: 1, fileCount: 1 }],
outputPath: '/repo/.claude/skills',

View file

@ -30,12 +30,15 @@ const resolveEmbeddingRuntimeMock = vi.fn<() => { source: string } | null>(() =>
source: 'package',
}));
const isPrefixRuntimeLoadableMock = vi.fn(() => true);
const installEmbeddingRuntimeMock = vi.fn(async () => undefined);
const installEmbeddingRuntimeMock = vi.fn<
typeof import('../../src/core/embeddings/runtime-install.js').installEmbeddingRuntime
>(async () => undefined);
vi.mock('../../src/core/embeddings/runtime-install.js', async (importOriginal) => ({
...(await importOriginal<typeof import('../../src/core/embeddings/runtime-install.js')>()),
resolveEmbeddingRuntime: () => resolveEmbeddingRuntimeMock(),
isPrefixRuntimeLoadable: () => isPrefixRuntimeLoadableMock(),
installEmbeddingRuntime: (...args: unknown[]) => installEmbeddingRuntimeMock(...args),
installEmbeddingRuntime: (...args: Parameters<typeof installEmbeddingRuntimeMock>) =>
installEmbeddingRuntimeMock(...args),
getEmbeddingRuntimeDir: () => '/fake/embedding-runtime',
}));

View file

@ -38,12 +38,15 @@ const resolveEmbeddingRuntimeMock = vi.fn<() => { source: string } | null>(() =>
source: 'package',
}));
const isPrefixRuntimeLoadableMock = vi.fn(() => true);
const installEmbeddingRuntimeMock = vi.fn(async () => undefined);
const installEmbeddingRuntimeMock = vi.fn<
typeof import('../../src/core/embeddings/runtime-install.js').installEmbeddingRuntime
>(async () => undefined);
vi.mock('../../src/core/embeddings/runtime-install.js', async (importOriginal) => ({
...(await importOriginal<typeof import('../../src/core/embeddings/runtime-install.js')>()),
resolveEmbeddingRuntime: () => resolveEmbeddingRuntimeMock(),
isPrefixRuntimeLoadable: () => isPrefixRuntimeLoadableMock(),
installEmbeddingRuntime: (...args: unknown[]) => installEmbeddingRuntimeMock(...args),
installEmbeddingRuntime: (...args: Parameters<typeof installEmbeddingRuntimeMock>) =>
installEmbeddingRuntimeMock(...args),
getEmbeddingRuntimeDir: () => '/fake/embedding-runtime',
}));

View file

@ -3,7 +3,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
const { runFullAnalysisMock, generateAIContextFilesMock, generateSkillFilesMock, cliErrorMock } =
vi.hoisted(() => {
const runFullAnalysisMock = vi.fn();
const generateAIContextFilesMock = vi.fn(async () => ({ files: [] as string[] }));
const generateAIContextFilesMock = vi.fn<
typeof import('../../src/cli/ai-context.js').generateAIContextFiles
>(async () => ({ files: [] as string[] }));
const generateSkillFilesMock = vi.fn(async () => ({
skills: [{ name: 'c', label: 'Community', symbolCount: 1, fileCount: 1 }],
outputPath: '/repo/.claude/skills',

View file

@ -23,6 +23,7 @@ import type { AnalyzerRunnerIdentity } from '../../src/storage/repo-manager.js';
import { IndexLockTimeoutError, type LockRecord } from '../../src/storage/index-lock.js';
const baseResult: AnalyzeResult = {
rebuildReasons: [],
repoName: 'repo',
repoPath: '/repo',
storagePath: '/repo/.gitnexus',

View file

@ -29,6 +29,7 @@ function hostileResult(): AnalyzeResult {
};
graph.self = graph; // circular — JSON.stringify throws
return {
rebuildReasons: [],
repoName: 'demo',
repoPath: '/repos/demo',
storagePath: '/repos/demo/.gitnexus',
@ -83,8 +84,16 @@ describe('#2112: analyze-worker IPC projection', () => {
properties: { name: `n${i}`, filePath: 'x.ts' },
});
}
graph.addRelationship({ id: 'n0->n1', source: 'n0', target: 'n1', type: 'CALLS' });
graph.addRelationship({
id: 'n0->n1',
sourceId: 'n0',
targetId: 'n1',
type: 'CALLS',
confidence: 1,
reason: '',
});
const result: AnalyzeResult = {
rebuildReasons: [],
repoName: 'demo',
repoPath: '/r',
storagePath: '/r/.gitnexus',

View file

@ -1,3 +1,4 @@
import type { AnalyzeUploadDeps } from '../../src/server/analyze-upload.js';
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import path from 'node:path';
import fs from 'node:fs/promises';
@ -88,8 +89,13 @@ function uniqueTop(): string {
describe('createAnalyzeUploadHandler', () => {
it('ingests, promotes the inner folder, and launches analysis (202)', async () => {
const top = uniqueTop();
const createJob = vi.fn(() => ({ id: 'job-1', status: 'queued' }));
const launch = vi.fn((_j, dir: string) => promoted.push(dir));
const createJob = vi.fn<AnalyzeUploadDeps['createJob']>(() => ({
id: 'job-1',
status: 'queued',
}));
const launch = vi.fn<AnalyzeUploadDeps['launch']>((_j, dir) => {
promoted.push(dir);
});
const failJob = vi.fn();
const handler = createAnalyzeUploadHandler({ createJob, launch, failJob });
@ -118,10 +124,12 @@ describe('createAnalyzeUploadHandler', () => {
it('maps a busy job (createJob throws "already in progress") to 409 and promotes nothing', async () => {
const top = uniqueTop();
const createJob = vi.fn(() => {
const createJob = vi.fn<AnalyzeUploadDeps['createJob']>(() => {
throw new Error('Analysis already in progress for another repository');
});
const launch = vi.fn((_j, dir: string) => promoted.push(dir));
const launch = vi.fn<AnalyzeUploadDeps['launch']>((_j, dir) => {
promoted.push(dir);
});
const failJob = vi.fn();
const handler = createAnalyzeUploadHandler({ createJob, launch, failJob });
@ -142,8 +150,8 @@ describe('createAnalyzeUploadHandler', () => {
});
it('rejects a traversal path in the manifest (400) without launching', async () => {
const createJob = vi.fn(() => ({ id: 'j', status: 'queued' }));
const launch = vi.fn();
const createJob = vi.fn<AnalyzeUploadDeps['createJob']>(() => ({ id: 'j', status: 'queued' }));
const launch = vi.fn<AnalyzeUploadDeps['launch']>();
const failJob = vi.fn();
const handler = createAnalyzeUploadHandler({ createJob, launch, failJob });
@ -162,8 +170,8 @@ describe('createAnalyzeUploadHandler', () => {
});
it('rejects an un-nameable top folder (Windows-reserved → 400)', async () => {
const createJob = vi.fn(() => ({ id: 'j', status: 'queued' }));
const launch = vi.fn();
const createJob = vi.fn<AnalyzeUploadDeps['createJob']>(() => ({ id: 'j', status: 'queued' }));
const launch = vi.fn<AnalyzeUploadDeps['launch']>();
const failJob = vi.fn();
const handler = createAnalyzeUploadHandler({ createJob, launch, failJob });
@ -182,8 +190,13 @@ describe('createAnalyzeUploadHandler', () => {
it('strips a crafted .gitnexus index from the promoted upload', async () => {
const top = uniqueTop();
const createJob = vi.fn(() => ({ id: 'job-x', status: 'queued' }));
const launch = vi.fn((_j, dir: string) => promoted.push(dir));
const createJob = vi.fn<AnalyzeUploadDeps['createJob']>(() => ({
id: 'job-x',
status: 'queued',
}));
const launch = vi.fn<AnalyzeUploadDeps['launch']>((_j, dir) => {
promoted.push(dir);
});
const failJob = vi.fn();
const handler = createAnalyzeUploadHandler({ createJob, launch, failJob });
@ -204,8 +217,8 @@ describe('createAnalyzeUploadHandler', () => {
});
it('rejects a single-segment manifest before creating a job (no slot taken)', async () => {
const createJob = vi.fn(() => ({ id: 'j', status: 'queued' }));
const launch = vi.fn();
const createJob = vi.fn<AnalyzeUploadDeps['createJob']>(() => ({ id: 'j', status: 'queued' }));
const launch = vi.fn<AnalyzeUploadDeps['launch']>();
const failJob = vi.fn();
const handler = createAnalyzeUploadHandler({ createJob, launch, failJob });
@ -224,8 +237,8 @@ describe('createAnalyzeUploadHandler', () => {
});
it('rejects a multi-top-folder manifest (would silently drop folders)', async () => {
const createJob = vi.fn(() => ({ id: 'j', status: 'queued' }));
const launch = vi.fn();
const createJob = vi.fn<AnalyzeUploadDeps['createJob']>(() => ({ id: 'j', status: 'queued' }));
const launch = vi.fn<AnalyzeUploadDeps['launch']>();
const failJob = vi.fn();
const handler = createAnalyzeUploadHandler({ createJob, launch, failJob });
@ -245,9 +258,12 @@ describe('createAnalyzeUploadHandler', () => {
it('releases the single slot (failJob) when a step fails after createJob', async () => {
const top = uniqueTop();
const createJob = vi.fn(() => ({ id: 'job-fail', status: 'queued' }));
const createJob = vi.fn<AnalyzeUploadDeps['createJob']>(() => ({
id: 'job-fail',
status: 'queued',
}));
// launch throws AFTER createJob + promote — the slot must be released.
const launch = vi.fn((_j, dir: string) => {
const launch = vi.fn<AnalyzeUploadDeps['launch']>((_j, dir: string) => {
promoted.push(dir);
throw new Error('worker fork blew up');
});

View file

@ -328,7 +328,7 @@ async function invoke(route: string, query: Record<string, unknown> = {}) {
this.emit('finish');
},
});
await handler(req, res);
await handler(req as unknown as express.Request, res as unknown as express.Response, vi.fn());
expect(res.statusCode, JSON.stringify(res.body)).toBe(route === '/api/embed' ? 202 : 200);
return res;
}

View file

@ -10,7 +10,7 @@ const { lbugMocks } = vi.hoisted(() => ({
}));
vi.mock('../../src/core/lbug/lbug-adapter.js', async (importOriginal) => {
const actual = await importOriginal();
const actual = await importOriginal<typeof import('../../src/core/lbug/lbug-adapter.js')>();
return { ...actual, ...lbugMocks };
});

View file

@ -98,7 +98,7 @@ async function runWithOnDiskPdgState(options: {
getCurrentCommit: vi.fn(async () => 'commit-1'),
runAnalysis,
registerRepo: vi.fn(async () => 'repo'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -145,11 +145,11 @@ describe('auto-sync runner', () => {
it('runs clone, analyzes changed commits, registers the repo, and syncs changed groups', async () => {
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'qts_account'),
loadState: vi.fn(async () => ({
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({
'/tmp/repos/gitee.com/qts_server/qts_account|master': {
codeCommitId: 'commit-1',
analyzedCommitId: 'commit-1',
@ -229,11 +229,11 @@ describe('auto-sync runner', () => {
await fs.writeFile(path.join(targetDir, '.gitnexusrc'), '{"embeddings": true}');
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => targetDir),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'qts_account'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -268,12 +268,12 @@ describe('auto-sync runner', () => {
const stateKey = '/tmp/repos/gitee.com/qts_server/qts_account|master';
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-1'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-1'),
getExistingPdgMode: vi.fn(async () => false),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'qts_account'),
loadState: vi.fn(async () => ({
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({
[stateKey]: {
codeCommitId: 'commit-1',
analyzedCommitId: 'commit-1',
@ -318,12 +318,12 @@ describe('auto-sync runner', () => {
};
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
getExistingPdgMode: vi.fn(async () => true),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'qts_account'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -411,12 +411,12 @@ describe('auto-sync runner', () => {
const warn = vi.fn();
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-1'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-1'),
getExistingPdgMode: vi.fn(async () => true),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'qts_account'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -452,7 +452,7 @@ describe('auto-sync runner', () => {
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'qts_account'),
resolveBranchPlacement: vi.fn(async () => ({ branch: 'master' })),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => true),
@ -480,11 +480,11 @@ describe('auto-sync runner', () => {
it('syncs a group when a repo is newly added to the group', async () => {
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'qts_account'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => true),
@ -508,11 +508,11 @@ describe('auto-sync runner', () => {
it('syncs a group after successful re-analysis even when membership already exists', async () => {
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-3'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-3'),
runAnalysis: vi.fn(async () => ({ stats: { files: 2 } }) as any),
registerRepo: vi.fn(async () => 'qts_account'),
loadState: vi.fn(async () => ({
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({
'/tmp/repos/gitee.com/qts_server/qts_account|master': {
codeCommitId: 'commit-2',
analyzedCommitId: 'commit-2',
@ -547,12 +547,14 @@ describe('auto-sync runner', () => {
projects: [
{
localPath: '/tmp/repos-a',
overwriteLocalChanges: false,
groupName: 'back_end',
branches: ['main'],
remoteUrls: ['git@github.com:team-a/service.git'],
},
{
localPath: '/tmp/repos-b',
overwriteLocalChanges: false,
groupName: 'back_end',
branches: ['main'],
remoteUrls: ['git@gitlab.com:team-b/service.git'],
@ -565,11 +567,11 @@ describe('auto-sync runner', () => {
root: localPath,
})),
cloneOrPull: vi.fn(async (_url, targetDir) => targetDir),
getCurrentBranch: vi.fn(() => 'main'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'main'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'service'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => true),
@ -618,11 +620,11 @@ describe('auto-sync runner', () => {
it('skips analysis when commit id has not changed', async () => {
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-1'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-1'),
runAnalysis: vi.fn(),
registerRepo: vi.fn(),
loadState: vi.fn(async () => ({
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({
'/tmp/repos/gitee.com/qts_server/qts_account|master': {
codeCommitId: 'commit-1',
analyzedCommitId: 'commit-1',
@ -664,11 +666,11 @@ describe('auto-sync runner', () => {
.mockResolvedValueOnce(undefined);
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-1'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-1'),
runAnalysis: vi.fn(),
registerRepo: vi.fn(),
loadState: vi.fn(async () => structuredClone(persistedState)),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => structuredClone(persistedState)),
saveState: vi.fn(async (state) => {
persistedState = structuredClone(state);
}),
@ -705,11 +707,11 @@ describe('auto-sync runner', () => {
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'qts_account'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -763,11 +765,11 @@ describe('auto-sync runner', () => {
const cloneOrPull = vi.fn(async () => '/tmp/repos/gitlab.mycompany.com/group/repo');
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull,
getCurrentBranch: vi.fn(() => 'main'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'main'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'repo'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -826,11 +828,11 @@ describe('auto-sync runner', () => {
const runAnalysis = vi.fn(async () => ({ stats: { files: 1 } }) as any);
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis,
registerRepo: vi.fn(async () => 'qts_account'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -873,11 +875,11 @@ describe('auto-sync runner', () => {
if (options?.branch === 'missing') throw new Error('remote branch not found');
return '/tmp/repos/gitee.com/qts_server/qts_account';
}),
getCurrentBranch: vi.fn(() => 'develop'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'develop'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'qts_account'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -940,7 +942,7 @@ describe('auto-sync runner', () => {
getCurrentCommit: vi.fn(),
runAnalysis: vi.fn(),
registerRepo: vi.fn(),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -980,11 +982,11 @@ describe('auto-sync runner', () => {
const warnLogger = vi.fn();
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'develop'),
getCurrentBranch: vi.fn(async () => 'develop'),
getCurrentCommit: vi.fn(),
runAnalysis: vi.fn(),
registerRepo: vi.fn(),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => true),
@ -1010,11 +1012,11 @@ describe('auto-sync runner', () => {
const warnLogger = vi.fn();
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => undefined),
getCurrentBranch: vi.fn(async () => undefined),
getCurrentCommit: vi.fn(),
runAnalysis: vi.fn(),
registerRepo: vi.fn(),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => true),
@ -1055,13 +1057,13 @@ describe('auto-sync runner', () => {
if (remoteUrl.includes('failing_sync')) throw new Error('sync failed');
return '/tmp/repos/gitee.com/qts_server/qts_account';
}),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => {
throw new Error('analysis failed');
}),
registerRepo: vi.fn(),
loadState: vi.fn(async () => ({
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({
'/tmp/repos/gitee.com/qts_server/qts_account|master': {
codeCommitId: 'commit-1',
analyzedCommitId: 'commit-1',
@ -1113,11 +1115,11 @@ describe('auto-sync runner', () => {
it('records the resolved target directory when a post-sync operation fails', async () => {
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async (_url, targetDir) => targetDir),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => {
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => {
throw new Error('git log failed');
}),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
getAvailableMemoryGB: vi.fn(() => 8),
@ -1153,11 +1155,11 @@ describe('auto-sync runner', () => {
return cloneRoot;
}),
cloneOrPull: vi.fn(async (_url, targetDir) => targetDir),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'repo'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -1180,13 +1182,13 @@ describe('auto-sync runner', () => {
const errorLogger = vi.fn();
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async (_url, targetDir) => targetDir),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => {
throw new Error('registry busy');
}),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -1220,11 +1222,11 @@ describe('auto-sync runner', () => {
const errorLogger = vi.fn();
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'qts_account'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -1279,13 +1281,13 @@ describe('auto-sync runner', () => {
events.push(`clone-end:${remoteUrl}`);
return remoteUrl.includes('/one.git') ? '/tmp/repos/one' : '/tmp/repos/two';
}),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn((repoPath) =>
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async (repoPath: string) =>
repoPath.endsWith('/one') ? 'one-commit' : 'two-commit',
),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'repo'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -1323,11 +1325,11 @@ describe('auto-sync runner', () => {
};
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async (_url, targetDir) => targetDir),
getCurrentBranch: vi.fn(() => 'main'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'main'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async (_path, _meta, options) => options?.name ?? 'repo'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -1367,14 +1369,14 @@ describe('auto-sync runner', () => {
};
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/github.com/owner/repo'),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
syncGroupByName: vi.fn(async () => {}),
getAvailableMemoryGB: vi.fn(() => 8),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'abc'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'abc'),
runAnalysis: vi.fn(async () => ({ stats: {} })),
registerRepo: vi.fn(async () => 'repo'),
});
@ -1399,13 +1401,13 @@ describe('auto-sync runner', () => {
const errorLogger = vi.fn();
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => {
throw new Error('parser crashed\nwith stack');
}),
registerRepo: vi.fn(),
loadState: vi.fn(async () => ({
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({
'/tmp/repos/gitee.com/qts_server/qts_account|master': {
codeCommitId: 'commit-1',
analyzedCommitId: 'commit-1',
@ -1460,13 +1462,13 @@ describe('auto-sync runner', () => {
);
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-1'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-1'),
runAnalysis: vi.fn(async () => {
throw leftover;
}),
registerRepo: vi.fn(),
loadState: vi.fn(async () => ({
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({
'/tmp/repos/gitee.com/qts_server/qts_account|master': {
codeCommitId: 'commit-1',
analyzedCommitId: 'commit-1',
@ -1513,8 +1515,8 @@ describe('auto-sync runner', () => {
it('does not count a retryable live-holder index-lock timeout toward the analyze failure threshold', async () => {
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-1'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-1'),
runAnalysis: vi.fn(async () => {
throw new AutoSyncAnalysisError('waited for the index lock', {
code: 'index-lock-timeout',
@ -1522,7 +1524,7 @@ describe('auto-sync runner', () => {
});
}),
registerRepo: vi.fn(),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -1550,8 +1552,8 @@ describe('auto-sync runner', () => {
it('still counts a non-retryable index-lock guard timeout toward the analyze failure threshold', async () => {
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-1'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-1'),
runAnalysis: vi.fn(async () => {
throw new AutoSyncAnalysisError('index lock guard timeout', {
code: 'index-lock-timeout',
@ -1559,7 +1561,7 @@ describe('auto-sync runner', () => {
});
}),
registerRepo: vi.fn(),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -1586,13 +1588,13 @@ describe('auto-sync runner', () => {
it('records a null analysis failure without masking it with a TypeError', async () => {
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => {
throw null;
}),
registerRepo: vi.fn(),
loadState: vi.fn(async () => ({})),
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({})),
saveState: vi.fn(async () => {}),
writeCommitInfo: vi.fn(async () => {}),
addRepoToGroup: vi.fn(async () => false),
@ -1621,11 +1623,11 @@ describe('auto-sync runner', () => {
const errorLogger = vi.fn();
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'qts_account'),
loadState: vi.fn(async () => ({
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({
'/tmp/repos/gitee.com/qts_server/qts_account|master': {
codeCommitId: 'commit-1',
analyzedCommitId: 'commit-1',
@ -1673,11 +1675,11 @@ describe('auto-sync runner', () => {
it('clears prior analyze failure count after a successful analyze', async () => {
const deps: Partial<AutoSyncRunDeps> = withCloneRoot({
cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'),
getCurrentBranch: vi.fn(() => 'master'),
getCurrentCommit: vi.fn(() => 'commit-2'),
getCurrentBranch: vi.fn(async () => 'master'),
getCurrentCommit: vi.fn(async () => 'commit-2'),
runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
registerRepo: vi.fn(async () => 'qts_account'),
loadState: vi.fn(async () => ({
loadState: vi.fn<AutoSyncRunDeps['loadState']>(async () => ({
'/tmp/repos/gitee.com/qts_server/qts_account|master': {
codeCommitId: 'commit-1',
analyzedCommitId: 'commit-1',

View file

@ -287,7 +287,7 @@ describe('auto-sync', () => {
const loaded = await loadAutoSyncConfig();
expect(loaded.ok).toBe(false);
if (loaded.ok) throw new Error('expected invalid config');
if (loaded.ok !== false) throw new Error('expected invalid config');
expect(loaded.message).toContain('analyze_failure_threshold must be an integer >= 2');
});
@ -307,7 +307,7 @@ describe('auto-sync', () => {
const loaded = await loadAutoSyncConfig();
expect(loaded.ok).toBe(false);
if (loaded.ok) throw new Error('expected invalid config');
if (loaded.ok !== false) throw new Error('expected invalid config');
expect(loaded.reason).toBe('invalid');
expect(loaded.message).toContain('[auto-sync] Invalid watch_config.yml:');
expect(loaded.message).toContain('sync_interval_minutes must be a positive integer');
@ -330,7 +330,7 @@ describe('auto-sync', () => {
const loaded = await loadAutoSyncConfig();
expect(loaded.ok).toBe(false);
if (loaded.ok) throw new Error('expected invalid config');
if (loaded.ok !== false) throw new Error('expected invalid config');
expect(loaded.message).toContain('local_path must be an absolute path');
});
@ -740,7 +740,7 @@ describe('auto-sync', () => {
const loaded = await loadAutoSyncConfig();
expect(loaded.ok).toBe(false);
if (loaded.ok) throw new Error('expected invalid config');
if (loaded.ok !== false) throw new Error('expected invalid config');
expect(loaded.message).toContain('must not set both branch and branches');
});

View file

@ -1,4 +1,5 @@
import { describe, it, expect } from 'vitest';
import type { SyntaxNode } from 'tree-sitter';
import { routeRubyCall } from '../../../src/core/ingestion/call-routing.js';
// ── Mock AST node helpers ────────────────────────────────────────────────────
@ -164,67 +165,73 @@ function makeNamedSibling(type = 'expression_statement'): MockNode {
return { type, text: '', isNamed: true };
}
// These routing fixtures intentionally provide only the traversed AST fields.
// Keep the native SyntaxNode assertion at the test boundary.
function syntaxNode(node: MockNode): SyntaxNode {
return node as unknown as SyntaxNode;
}
// ── require / require_relative ───────────────────────────────────────────────
describe('routeRubyCall — require / require_relative', () => {
it('require with a valid string path returns import with isRelative=false', () => {
const node = makeRequireCallNode('net/http');
const result = routeRubyCall('require', node);
const result = routeRubyCall('require', syntaxNode(node));
expect(result).toEqual({ kind: 'import', importPath: 'net/http', isRelative: false });
});
it('require_relative without leading dot prepends "./"', () => {
const node = makeRequireCallNode('models/user');
const result = routeRubyCall('require_relative', node);
const result = routeRubyCall('require_relative', syntaxNode(node));
expect(result).toEqual({ kind: 'import', importPath: './models/user', isRelative: true });
});
it('require_relative with path already starting with "." does not double-prepend', () => {
const node = makeRequireCallNode('./helpers/formatter');
const result = routeRubyCall('require_relative', node);
const result = routeRubyCall('require_relative', syntaxNode(node));
expect(result).toEqual({ kind: 'import', importPath: './helpers/formatter', isRelative: true });
});
it('require_relative with "../" prefix is left unchanged', () => {
const node = makeRequireCallNode('../shared/utils');
const result = routeRubyCall('require_relative', node);
const result = routeRubyCall('require_relative', syntaxNode(node));
expect(result).toEqual({ kind: 'import', importPath: '../shared/utils', isRelative: true });
});
it('returns skip when there is no string_content node (non-literal argument)', () => {
const node = makeRequireCallNodeNoContent();
expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' });
expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' });
});
it('returns skip when import path is an empty string', () => {
const node = makeRequireCallNode('');
expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' });
expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' });
});
it('returns skip when import path contains a control character (\\x00)', () => {
const node = makeRequireCallNode('some\x00path');
expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' });
expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' });
});
it('returns skip when import path contains a newline control character (\\n)', () => {
const node = makeRequireCallNode('path\ninjection');
expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' });
expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' });
});
it('returns skip when import path exceeds 1024 characters', () => {
const longPath = 'a'.repeat(1025);
const node = makeRequireCallNode(longPath);
expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' });
expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' });
});
it('accepts import path of exactly 1024 characters', () => {
const maxPath = 'a'.repeat(1024);
const node = makeRequireCallNode(maxPath);
const result = routeRubyCall('require', node);
const result = routeRubyCall('require', syntaxNode(node));
expect(result).toEqual({ kind: 'import', importPath: maxPath, isRelative: false });
});
@ -240,13 +247,13 @@ describe('routeRubyCall — require / require_relative', () => {
text: '',
childForFieldName: (name: string) => (name === 'arguments' ? argList : undefined),
};
expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' });
expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' });
});
it('returns skip when childForFieldName is absent (undefined callNode fields)', () => {
// callNode has no childForFieldName method at all
const node: MockNode = { type: 'call', text: '' };
expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' });
expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' });
});
});
@ -259,7 +266,7 @@ describe('routeRubyCall — require / require_relative', () => {
describe('routeRubyCall — include / extend / prepend (heritage owned by scope-resolution)', () => {
it('include returns skip (heritage emitted by scope-resolution, not a call edge)', () => {
const node = makeHeritageCallNode([makeConstantArg('Serializable')], 'class', 'User');
expect(routeRubyCall('include', node)).toEqual({ kind: 'skip' });
expect(routeRubyCall('include', syntaxNode(node))).toEqual({ kind: 'skip' });
});
it('extend returns skip (heritage emitted by scope-resolution, not a call edge)', () => {
@ -268,12 +275,12 @@ describe('routeRubyCall — include / extend / prepend (heritage owned by scope-
'class',
'Post',
);
expect(routeRubyCall('extend', node)).toEqual({ kind: 'skip' });
expect(routeRubyCall('extend', syntaxNode(node))).toEqual({ kind: 'skip' });
});
it('prepend returns skip (heritage emitted by scope-resolution, not a call edge)', () => {
const node = makeHeritageCallNode([makeConstantArg('Instrumented')], 'class', 'Service');
expect(routeRubyCall('prepend', node)).toEqual({ kind: 'skip' });
expect(routeRubyCall('prepend', syntaxNode(node))).toEqual({ kind: 'skip' });
});
});
@ -282,7 +289,7 @@ describe('routeRubyCall — include / extend / prepend (heritage owned by scope-
describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
it('attr_accessor with a single symbol returns a property item', () => {
const node = makeAccessorCallNode([makeSimpleSymbol('name', 5)]);
const result = routeRubyCall('attr_accessor', node);
const result = routeRubyCall('attr_accessor', syntaxNode(node));
expect(result).toEqual({
kind: 'properties',
@ -292,7 +299,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
it('attr_reader sets accessorType to "attr_reader"', () => {
const node = makeAccessorCallNode([makeSimpleSymbol('age', 3)]);
const result = routeRubyCall('attr_reader', node);
const result = routeRubyCall('attr_reader', syntaxNode(node));
expect(result).toEqual({
kind: 'properties',
@ -302,7 +309,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
it('attr_writer sets accessorType to "attr_writer"', () => {
const node = makeAccessorCallNode([makeSimpleSymbol('email', 7)]);
const result = routeRubyCall('attr_writer', node);
const result = routeRubyCall('attr_writer', syntaxNode(node));
expect(result).toEqual({
kind: 'properties',
@ -319,7 +326,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
endPosition: { row: 2 },
};
const node = makeAccessorCallNode([symNode]);
const result = routeRubyCall('attr_accessor', node);
const result = routeRubyCall('attr_accessor', syntaxNode(node));
expect(result).toMatchObject({ kind: 'properties', items: [{ propName: 'title' }] });
});
@ -333,7 +340,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
endPosition: { row: 1 },
};
const node = makeAccessorCallNode([symNode]);
const result = routeRubyCall('attr_accessor', node);
const result = routeRubyCall('attr_accessor', syntaxNode(node));
expect(result).toMatchObject({ kind: 'properties', items: [{ propName: 'status' }] });
});
@ -345,7 +352,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
makeSimpleSymbol('dob', 10),
];
const node = makeAccessorCallNode(args);
const result = routeRubyCall('attr_accessor', node);
const result = routeRubyCall('attr_accessor', syntaxNode(node));
expect(result).toEqual({
kind: 'properties',
@ -360,7 +367,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
it('extracts simple YARD @return [Type] from preceding comment', () => {
const comment = makeCommentNode('# @return [Address]');
const node = makeAccessorCallNode([makeSimpleSymbol('address', 20)], [comment]);
const result = routeRubyCall('attr_accessor', node);
const result = routeRubyCall('attr_accessor', syntaxNode(node));
expect(result).toMatchObject({
kind: 'properties',
@ -372,7 +379,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
// The regex captures "Array<User>"; the simple match grabs the first uppercase word "Array"
const comment = makeCommentNode('# @return [Array<User>]');
const node = makeAccessorCallNode([makeSimpleSymbol('users', 15)], [comment]);
const result = routeRubyCall('attr_accessor', node);
const result = routeRubyCall('attr_accessor', syntaxNode(node));
expect(result).toMatchObject({
kind: 'properties',
@ -383,7 +390,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
it('extracts type from YARD comment with extra whitespace inside brackets', () => {
const comment = makeCommentNode('# @return [ Integer ]');
const node = makeAccessorCallNode([makeSimpleSymbol('count', 8)], [comment]);
const result = routeRubyCall('attr_accessor', node);
const result = routeRubyCall('attr_accessor', syntaxNode(node));
expect(result).toMatchObject({
kind: 'properties',
@ -393,7 +400,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
it('does not set declaredType when no YARD comment precedes the call', () => {
const node = makeAccessorCallNode([makeSimpleSymbol('score', 12)]);
const result = routeRubyCall('attr_accessor', node);
const result = routeRubyCall('attr_accessor', syntaxNode(node));
expect(result).toMatchObject({ kind: 'properties', items: [{ propName: 'score' }] });
const item = (result as any).items[0];
@ -403,7 +410,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
it('does not set declaredType when comment has no @return annotation', () => {
const comment = makeCommentNode('# This accessor stores the user name');
const node = makeAccessorCallNode([makeSimpleSymbol('user_name', 9)], [comment]);
const result = routeRubyCall('attr_accessor', node);
const result = routeRubyCall('attr_accessor', syntaxNode(node));
const item = (result as any).items[0];
expect(item.declaredType).toBeUndefined();
@ -413,7 +420,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
// e.g. "@return [string]" — lowercase first char fails the simple = raw.match(/^([A-Z]\w*)/)
const comment = makeCommentNode('# @return [string]');
const node = makeAccessorCallNode([makeSimpleSymbol('label', 4)], [comment]);
const result = routeRubyCall('attr_accessor', node);
const result = routeRubyCall('attr_accessor', syntaxNode(node));
const item = (result as any).items[0];
expect(item.declaredType).toBeUndefined();
@ -426,7 +433,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
const named = makeNamedSibling();
const node = makeAccessorCallNode([makeSimpleSymbol('owner', 6)], [yardComment, named]);
// named is last in the array → becomes direct previousSibling
const result = routeRubyCall('attr_accessor', node);
const result = routeRubyCall('attr_accessor', syntaxNode(node));
const item = (result as any).items[0];
expect(item.declaredType).toBeUndefined();
@ -438,7 +445,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
const comment = makeCommentNode('# @return [Order]');
// siblings in order oldest→newest; the last becomes the direct previousSibling
const node = makeAccessorCallNode([makeSimpleSymbol('order', 30)], [comment, unnamedNode]);
const result = routeRubyCall('attr_accessor', node);
const result = routeRubyCall('attr_accessor', syntaxNode(node));
expect(result).toMatchObject({
kind: 'properties',
@ -450,12 +457,12 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
// Only an identifier node — not a symbol
const identArg: MockNode = { type: 'identifier', text: 'some_var' };
const node = makeAccessorCallNode([identArg]);
expect(routeRubyCall('attr_accessor', node)).toEqual({ kind: 'skip' });
expect(routeRubyCall('attr_accessor', syntaxNode(node))).toEqual({ kind: 'skip' });
});
it('returns skip when arg list is empty', () => {
const node = makeAccessorCallNode([]);
expect(routeRubyCall('attr_accessor', node)).toEqual({ kind: 'skip' });
expect(routeRubyCall('attr_accessor', syntaxNode(node))).toEqual({ kind: 'skip' });
});
it('records correct startLine and endLine from symbol node positions', () => {
@ -466,7 +473,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
endPosition: { row: 42 },
};
const node = makeAccessorCallNode([sym]);
const result = routeRubyCall('attr_accessor', node);
const result = routeRubyCall('attr_accessor', syntaxNode(node));
expect(result).toMatchObject({
kind: 'properties',
@ -480,19 +487,19 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => {
describe('routeRubyCall — default (unknown method name)', () => {
it('returns {kind: "call"} for an arbitrary method name', () => {
const node: MockNode = { type: 'call', text: '' };
expect(routeRubyCall('some_method', node)).toEqual({ kind: 'call' });
expect(routeRubyCall('some_method', syntaxNode(node))).toEqual({ kind: 'call' });
});
it('returns {kind: "call"} for an empty method name string', () => {
const node: MockNode = { type: 'call', text: '' };
expect(routeRubyCall('', node)).toEqual({ kind: 'call' });
expect(routeRubyCall('', syntaxNode(node))).toEqual({ kind: 'call' });
});
it('returns {kind: "call"} for a realistic method name (save, render, etc.)', () => {
const node: MockNode = { type: 'call', text: '' };
expect(routeRubyCall('render', node)).toEqual({ kind: 'call' });
expect(routeRubyCall('save', node)).toEqual({ kind: 'call' });
expect(routeRubyCall('destroy', node)).toEqual({ kind: 'call' });
expect(routeRubyCall('render', syntaxNode(node))).toEqual({ kind: 'call' });
expect(routeRubyCall('save', syntaxNode(node))).toEqual({ kind: 'call' });
expect(routeRubyCall('destroy', syntaxNode(node))).toEqual({ kind: 'call' });
});
});
@ -501,13 +508,13 @@ describe('routeRubyCall — default (unknown method name)', () => {
describe('routeRubyCall passthrough', () => {
it('routeRubyCall delegates correctly for require', () => {
const node = makeRequireCallNode('json');
const result = routeRubyCall('require', node);
const result = routeRubyCall('require', syntaxNode(node));
expect(result).toEqual({ kind: 'import', importPath: 'json', isRelative: false });
});
it('routeRubyCall returns {kind: "call"} for an unknown method name', () => {
const node: MockNode = { type: 'call', text: '' };
const result = routeRubyCall('render', node);
const result = routeRubyCall('render', syntaxNode(node));
expect(result).toEqual({ kind: 'call' });
});
});

View file

@ -29,12 +29,12 @@ const { lbugMocks } = vi.hoisted(() => ({
}));
vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => {
const actual = await importOriginal();
const actual = await importOriginal<typeof import('../../src/core/lbug/pool-adapter.js')>();
return { ...actual, ...lbugMocks };
});
vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => {
const actual = await importOriginal();
const actual = await importOriginal<typeof import('../../src/mcp/core/lbug-adapter.js')>();
return { ...actual, ...lbugMocks };
});

View file

@ -30,13 +30,13 @@ const { lbugMocks } = vi.hoisted(() => ({
}));
vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => {
const actual = await importOriginal();
const actual = await importOriginal<typeof import('../../src/core/lbug/pool-adapter.js')>();
return { ...actual, ...lbugMocks };
});
// Re-export shim must resolve to the same mocks
vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => {
const actual = await importOriginal();
const actual = await importOriginal<typeof import('../../src/mcp/core/lbug-adapter.js')>();
return { ...actual, ...lbugMocks };
});
@ -2560,9 +2560,7 @@ describe('LocalBackend.callTool', () => {
// `oldName` must sit on the file's 0-based line 1 for the definition edit to
// fire. (#2380: the mock previously put it on line 0, which stopped matching
// once context() went 1-based.)
const readSpy = vi
.spyOn(fsPromises, 'readFile')
.mockResolvedValue('\nfunction oldName() {}\n' as unknown as Buffer);
const readSpy = vi.spyOn(fsPromises, 'readFile').mockResolvedValue('\nfunction oldName() {}\n');
const writeSpy = vi
.spyOn(fsPromises, 'writeFile')
.mockRejectedValue(new Error('EACCES: permission denied'));
@ -3034,7 +3032,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
it('mode absent → callgraph result (target populated, no mode-error, BFS runs)', async () => {
resolveSingleTarget();
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
const bfsSpy = vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
);
const result = await backend.callTool('impact', { target: 'main', direction: 'upstream' });
// A clean callgraph result carries no mode error and runs the BFS.
expect(result.error ?? '').not.toMatch(/Invalid "mode"/);
@ -3062,7 +3063,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
it("mode:'pdg' routes to the PDG traversal and attaches interprocedural symbol reach", async () => {
resolveSingleTarget();
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
const bfsSpy = vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
);
const result = await backend.callTool('impact', {
target: 'main',
direction: 'upstream',
@ -3079,7 +3083,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
it("mode:'pdg' labels interprocedural symbols as a callgraph bridge", async () => {
resolveSingleTarget();
vi.spyOn(backend as any, '_runImpactBFS').mockResolvedValueOnce({
vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
).mockResolvedValueOnce({
target: { id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' },
direction: 'downstream',
impactedCount: 1,
@ -3118,7 +3125,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
it("mode:'pdg' preserves unproven bridge evidence when call-site proof is unavailable", async () => {
resolveSingleTarget();
vi.spyOn(backend as any, '_runImpactBFS').mockResolvedValueOnce({
vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
).mockResolvedValueOnce({
target: { id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' },
direction: 'downstream',
impactedCount: 1,
@ -3159,7 +3169,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
'invalid mode %j → structured {error}, never a callgraph result (KTD5 anti-silent-fallback)',
async (bad) => {
resolveSingleTarget();
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
const bfsSpy = vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
);
const result = await backend.callTool('impact', {
target: 'main',
direction: 'upstream',
@ -3176,7 +3189,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
'line param with mode:%j → structured {error} (line is PDG-only), never a callgraph result',
async (mode) => {
resolveSingleTarget();
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
const bfsSpy = vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
);
const result = await backend.callTool('impact', {
target: 'main',
direction: 'upstream',
@ -3199,7 +3215,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
'mode:%j + adapter-materialized line:0 is treated as omitted and runs the BFS (#2279)',
async (mode) => {
resolveSingleTarget();
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
const bfsSpy = vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
);
const result = await backend.callTool('impact', {
target: 'main',
direction: 'upstream',
@ -3296,7 +3315,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
affectedStatementCount: 1,
criterionLine: 8,
});
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
const bfsSpy = vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
);
const result = await backend.callTool('impact', {
target: 'main',
direction: 'downstream',
@ -3349,7 +3371,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
affectedStatementCount: 0,
criterionLine: 8,
});
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
const bfsSpy = vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
);
await backend.callTool('impact', {
target: 'main',
direction: 'downstream',
@ -3426,7 +3451,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
affectedStatementCount: 1,
criterionLine: 8,
});
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
const bfsSpy = vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
);
const cap = _captureLogger();
try {
const result = await backend.callTool('impact', {
@ -3482,7 +3510,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
affectedStatementCount: 1,
criterionLine: 8,
});
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
const bfsSpy = vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
);
const cap = _captureLogger('debug');
try {
const result = await backend.callTool('impact', {
@ -3529,7 +3560,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
affectedStatementCount: 1,
criterionLine: 8,
});
vi.spyOn(backend as any, '_runImpactBFS');
vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
);
const cap = _captureLogger();
try {
await backend.callTool('impact', {
@ -3548,7 +3582,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
it("mode:'pdg' + crossDepth → hard {error} (single-repo PDG impact)", async () => {
resolveSingleTarget();
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
const bfsSpy = vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
);
const result = await backend.callTool('impact', {
target: 'main',
direction: 'upstream',
@ -3565,17 +3602,22 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
['minConfidence', { minConfidence: 0.5 }, (opts: any) => opts.minConfidence],
])("mode:'pdg' + %s feeds the interprocedural symbol reach", async (_label, extra, readOpt) => {
resolveSingleTarget();
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS').mockResolvedValueOnce({
target: { id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' },
direction: 'upstream',
impactedCount: 0,
risk: 'LOW',
summary: { direct: 0, processes_affected: 0, modules_affected: 0 },
byDepthCounts: {},
affected_processes: [],
affected_modules: [],
byDepth: {},
});
const bfsSpy = vi
.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
)
.mockResolvedValueOnce({
target: { id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' },
direction: 'upstream',
impactedCount: 0,
risk: 'LOW',
summary: { direct: 0, processes_affected: 0, modules_affected: 0 },
byDepthCounts: {},
affected_processes: [],
affected_modules: [],
byDepth: {},
});
const result = await backend.callTool('impact', {
target: 'main',
direction: 'upstream',
@ -3606,7 +3648,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
startLine: 8,
},
]);
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
const bfsSpy = vi.spyOn(
backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] },
'_runImpactBFS',
);
const result = await backend.callTool('impact', {
target: 'login',
direction: 'upstream',

View file

@ -1,3 +1,4 @@
import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js';
import { describe, it, expect, vi } from 'vitest';
import { createRequire } from 'node:module';
import { requireVendoredGrammar } from '../../../src/core/tree-sitter/vendored-grammars.js';

View file

@ -116,7 +116,7 @@ describe('ControlFlowContext', () => {
it('labeled break/continue resolve to the labeled loop, not the nearest', () => {
const ctx = new ControlFlowContext();
ctx.pushLoop(/*outer*/ 100, 200, 'outer');
ctx.pushLoop(/*outer*/ 100, 200, ['outer']);
ctx.pushLoop(/*inner*/ 110, 210);
expect(ctx.breakTarget('outer')).toBe(200);
expect(ctx.continueTarget('outer')).toBe(100);

View file

@ -49,6 +49,7 @@ function mkCfg(
return {
filePath: 't.ts',
functionStartLine: 1,
functionEndLine: blockCount,
functionStartColumn: 0,
entryIndex: entry,
exitIndex: exit,

View file

@ -36,6 +36,7 @@ function mkCfg(
return {
filePath: 't.ts',
functionStartLine: 1,
functionEndLine: blockCount,
functionStartColumn: 0,
entryIndex: entry,
exitIndex: exit,

View file

@ -51,6 +51,7 @@ function mkCfg(
return {
filePath: 't.ts',
functionStartLine: 1,
functionEndLine: blockCount,
functionStartColumn: 0,
entryIndex: entry,
exitIndex: exit,

View file

@ -96,7 +96,9 @@ describe('CLI commands', () => {
describe('optional parser dependencies', () => {
it('loads vendored grammars from vendor/ — never file: optionalDependencies (#1728) nor a node_modules copy (#2111)', async () => {
const pkg = await import('../../package.json', { with: { type: 'json' } });
const optional = pkg.default.optionalDependencies ?? {};
const optional =
(pkg.default as { optionalDependencies?: Record<string, string> }).optionalDependencies ??
{};
expect(optional['tree-sitter-dart']).toBeUndefined();
expect(optional['tree-sitter-proto']).toBeUndefined();
expect(optional['tree-sitter-swift']).toBeUndefined();
@ -142,8 +144,12 @@ describe('CLI commands', () => {
expect(pkg.default.scripts.postinstall).toContain('build-tree-sitter-grammars.cjs');
expect(swiftPkg.default.version).toBe('0.7.1');
// No scripts.install / dependencies inside vendor/ (#836 / #1728 hygiene).
expect(swiftPkg.default.scripts?.install).toBeUndefined();
expect(swiftPkg.default.dependencies).toBeUndefined();
expect(
(swiftPkg.default as { scripts?: Record<string, string> }).scripts?.install,
).toBeUndefined();
expect(
(swiftPkg.default as { dependencies?: Record<string, string> }).dependencies,
).toBeUndefined();
expect(swiftPkg.default.peerDependencies['tree-sitter']).toContain('^0.21.1');
// Swift is now unified with Dart/Proto/Kotlin/C: the grammar SOURCE is
// vendored so build-tree-sitter-grammars.cjs can source-build the binding
@ -164,7 +170,9 @@ describe('CLI commands', () => {
const kotlinPkg = await import('../../vendor/tree-sitter-kotlin/package.json', {
with: { type: 'json' },
});
const optional = pkg.default.optionalDependencies ?? {};
const optional =
(pkg.default as { optionalDependencies?: Record<string, string> }).optionalDependencies ??
{};
// Kotlin is now VENDORED (like Swift/Dart/Proto), not a third-party npm
// optionalDependency. Its prebuilds are GitNexus-cross-built (upstream
// ships source only) and loaded from vendor/ by absolute path (#2111).
@ -172,8 +180,12 @@ describe('CLI commands', () => {
expect(pkg.default.scripts.postinstall).toContain('build-tree-sitter-grammars.cjs');
expect(kotlinPkg.default.version).toBe('0.4.0');
// No scripts.install / dependencies inside vendor/ (#836 / #1728 hygiene).
expect(kotlinPkg.default.scripts?.install).toBeUndefined();
expect(kotlinPkg.default.dependencies).toBeUndefined();
expect(
(kotlinPkg.default as { scripts?: Record<string, string> }).scripts?.install,
).toBeUndefined();
expect(
(kotlinPkg.default as { dependencies?: Record<string, string> }).dependencies,
).toBeUndefined();
expect(kotlinPkg.default.peerDependencies['tree-sitter']).toContain('^0.21');
});
@ -182,15 +194,21 @@ describe('CLI commands', () => {
const zigPkg = await import('../../vendor/tree-sitter-zig/package.json', {
with: { type: 'json' },
});
const optional = pkg.default.optionalDependencies ?? {};
const optional =
(pkg.default as { optionalDependencies?: Record<string, string> }).optionalDependencies ??
{};
expect(optional['@tree-sitter-grammars/tree-sitter-zig']).toBeUndefined();
expect(Object.keys(pkg.default.overrides ?? {})).not.toContain(
'@tree-sitter-grammars/tree-sitter-zig',
);
expect(pkg.default.scripts.postinstall).toContain('build-tree-sitter-grammars.cjs');
expect(zigPkg.default.version).toBe('1.1.2');
expect(zigPkg.default.scripts?.install).toBeUndefined();
expect(zigPkg.default.dependencies).toBeUndefined();
expect(
(zigPkg.default as { scripts?: Record<string, string> }).scripts?.install,
).toBeUndefined();
expect(
(zigPkg.default as { dependencies?: Record<string, string> }).dependencies,
).toBeUndefined();
expect(zigPkg.default.peerDependencies['tree-sitter']).toContain('^0.21');
});
@ -205,8 +223,12 @@ describe('CLI commands', () => {
expect(pkg.default.dependencies['tree-sitter-c']).toBeUndefined();
expect(pkg.default.scripts.postinstall).toContain('build-tree-sitter-grammars.cjs');
expect(cPkg.default.version).toBe('0.21.4');
expect(cPkg.default.scripts?.install).toBeUndefined();
expect(cPkg.default.dependencies).toBeUndefined();
expect(
(cPkg.default as { scripts?: Record<string, string> }).scripts?.install,
).toBeUndefined();
expect(
(cPkg.default as { dependencies?: Record<string, string> }).dependencies,
).toBeUndefined();
});
});

View file

@ -10,7 +10,7 @@
*/
import { describe, it, expect } from 'vitest';
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
import type { GraphNode, GraphRelationship } from '../../src/core/graph/types.js';
import type { GraphNode, GraphRelationship } from 'gitnexus-shared';
import { processCommunities } from '../../src/core/ingestion/community-processor.js';
// ============================================================================

View file

@ -5,7 +5,7 @@ import { join } from 'node:path';
import { Worker } from 'node:worker_threads';
import { describe, it, expect, vi } from 'vitest';
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
import type { GraphNode, GraphRelationship } from '../../src/core/graph/types.js';
import type { GraphNode, GraphRelationship } from 'gitnexus-shared';
import {
getCommunityColor,
COMMUNITY_COLORS,

View file

@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';
import Parser from 'tree-sitter';
import TypeScript from 'tree-sitter-typescript';
import type { ParsedImport } from 'gitnexus-shared';
import type { NodeLabel, ParsedImport } from 'gitnexus-shared';
import { extractConvexEndpointProperties } from '../../src/core/ingestion/languages/typescript/convex-endpoint-metadata.js';
import type { SyntaxNode } from '../../src/core/ingestion/utils/ast-helpers.js';
@ -27,18 +27,19 @@ const namedImport = (
importedName: string,
localName = importedName,
): ParsedImport => ({
kind: localName === importedName ? 'named' : 'alias',
...(localName === importedName
? { kind: 'named' as const }
: { kind: 'alias' as const, alias: localName }),
targetRaw,
importedName,
localName,
...(localName === importedName ? {} : { alias: localName }),
});
function extract(
source: string,
imports: readonly ParsedImport[],
isExported = true,
nodeLabel = 'Const',
nodeLabel: NodeLabel = 'Const',
definitionType = 'export_statement',
) {
return extractConvexEndpointProperties({

View file

@ -211,7 +211,7 @@ describe('stripUeMacros — tree-sitter extraction (end-to-end)', () => {
*/
function extractClassNames(source: string): string[] {
const parser = new Parser();
parser.setLanguage(CPP as unknown as Parser.Language);
parser.setLanguage(CPP);
const tree = parser.parse(source);
const names: string[] = [];
const stack: Parser.SyntaxNode[] = [tree.rootNode];
@ -249,7 +249,7 @@ describe('stripUeMacros — tree-sitter extraction (end-to-end)', () => {
const src = `UCLASS()\nclass BRAWLUI_API UMyClass : public UObject\n{\n GENERATED_BODY()\n public:\n void Run();\n};`;
const out = stripUeMacros(src);
const parser = new Parser();
parser.setLanguage(CPP as unknown as Parser.Language);
parser.setLanguage(CPP);
const tree = parser.parse(out);
const stack: Parser.SyntaxNode[] = [tree.rootNode];
let runLine: number | undefined;

View file

@ -39,7 +39,7 @@ function row(id: string, fill: number, hash = `hash-${id}`) {
}
describe('embedding-restore-spill (#3306)', () => {
const spills: Array<{ path: string }> = [];
const spills: Array<Parameters<typeof disposeEmbeddingSpill>[0]> = [];
afterEach(() => {
for (const spill of spills) disposeEmbeddingSpill(spill);
spills.length = 0;
@ -170,7 +170,7 @@ describe('embedding-restore-spill (#3306)', () => {
const shortPath = path.join(os.tmpdir(), `gitnexus-embed-restore-short-${process.pid}.bin`);
writeFileSync(shortPath, Buffer.from('NOPE'));
spills.push({ path: shortPath });
spills.push({ path: shortPath, dims: DIMS, rowCount: 1 });
expect(() => readSpillVectors({ path: shortPath, dims: DIMS, rowCount: 1 }, [0])).toThrow(
/invalid embedding spill header/,
);
@ -181,7 +181,7 @@ describe('embedding-restore-spill (#3306)', () => {
badMagic.writeUInt32LE(DIMS, 5);
const badPath = path.join(os.tmpdir(), `gitnexus-embed-restore-bad-${process.pid}.bin`);
writeFileSync(badPath, badMagic);
spills.push({ path: badPath });
spills.push({ path: badPath, dims: DIMS, rowCount: 1 });
expect(() => readSpillVectors({ path: badPath, dims: DIMS, rowCount: 1 }, [0])).toThrow(
/invalid embedding spill header/,
);

View file

@ -3,6 +3,8 @@ import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Mock } from 'vitest';
import type { ForkImpl } from '../../src/core/embeddings/embedding-sidecar-client.js';
import type { ChildProcess } from 'node:child_process';
import type {
SidecarRequest,
@ -57,7 +59,7 @@ describe('embedding sidecar client', () => {
const hostPlatform = process.platform;
const hostArch = process.arch;
let forkMock: ReturnType<typeof vi.fn>;
let forkMock: Mock<ForkImpl>;
let children: FakeChild[];
beforeEach(async () => {
@ -66,7 +68,7 @@ describe('embedding sidecar client', () => {
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
Object.defineProperty(process, 'arch', { value: 'x64', configurable: true });
children = [];
forkMock = vi.fn((_script: string, _args: string[], _opts: unknown) => {
forkMock = vi.fn<ForkImpl>((_script, _args, _opts) => {
const child = new FakeChild();
children.push(child);
return child as unknown as ChildProcess;

View file

@ -13,13 +13,15 @@ import type { LoggerCapture } from '../../src/core/logger.js';
const resolveEmbeddingRuntimeMock = vi.fn<() => { source: string } | null>();
const isPrefixRuntimeLoadableMock = vi.fn(() => true);
const installEmbeddingRuntimeMock = vi.fn(async () => undefined);
const installEmbeddingRuntimeMock = vi.fn<
typeof import('../../src/core/embeddings/runtime-install.js').installEmbeddingRuntime
>(async () => undefined);
vi.mock('../../src/core/embeddings/runtime-install.js', async (importOriginal) => ({
...(await importOriginal<typeof import('../../src/core/embeddings/runtime-install.js')>()),
resolveEmbeddingRuntime: () => resolveEmbeddingRuntimeMock(),
isPrefixRuntimeLoadable: () => isPrefixRuntimeLoadableMock(),
installEmbeddingRuntime: (opts?: unknown) => installEmbeddingRuntimeMock(opts),
installEmbeddingRuntime: installEmbeddingRuntimeMock,
getEmbeddingRuntimeDir: () => '/fake/embedding-runtime',
getEmbeddingStackSpecs: () => ({ '@huggingface/transformers': '^4.1.0' }),
}));

View file

@ -29,7 +29,7 @@ vi.mock('../../src/storage/repo-manager.js', async (importOriginal) => {
};
});
import { LocalBackend } from '../../src/mcp/local/local-backend';
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
describe('ensureInitialized reinit watermark (tri-review NEW-7)', () => {
const poolKey = '/tmp/nonexistent-repo/.gitnexus/lbug';

View file

@ -1,3 +1,4 @@
import { SupportedLanguages } from 'gitnexus-shared';
import { describe, it, expect } from 'vitest';
import {
calculateEntryPointScore,
@ -8,34 +9,58 @@ import {
describe('calculateEntryPointScore', () => {
describe('base scoring', () => {
it('returns 0 for functions with no outgoing calls', () => {
const result = calculateEntryPointScore('handler', 'typescript', true, 0, 0);
const result = calculateEntryPointScore('handler', SupportedLanguages.TypeScript, true, 0, 0);
expect(result.score).toBe(0);
expect(result.reasons).toContain('no-outgoing-calls');
});
it('calculates base score as calleeCount / (callerCount + 1)', () => {
const result = calculateEntryPointScore('doStuff', 'typescript', false, 0, 5);
const result = calculateEntryPointScore(
'doStuff',
SupportedLanguages.TypeScript,
false,
0,
5,
);
// base = 5 / (0 + 1) = 5, no export bonus, no name bonus
expect(result.score).toBe(5);
});
it('reduces score for functions with many callers', () => {
const few = calculateEntryPointScore('doStuff', 'typescript', false, 1, 5);
const many = calculateEntryPointScore('doStuff', 'typescript', false, 10, 5);
const few = calculateEntryPointScore('doStuff', SupportedLanguages.TypeScript, false, 1, 5);
const many = calculateEntryPointScore('doStuff', SupportedLanguages.TypeScript, false, 10, 5);
expect(few.score).toBeGreaterThan(many.score);
});
});
describe('export multiplier', () => {
it('applies 2.0 multiplier for exported functions', () => {
const exported = calculateEntryPointScore('doStuff', 'typescript', true, 0, 4);
const notExported = calculateEntryPointScore('doStuff', 'typescript', false, 0, 4);
const exported = calculateEntryPointScore(
'doStuff',
SupportedLanguages.TypeScript,
true,
0,
4,
);
const notExported = calculateEntryPointScore(
'doStuff',
SupportedLanguages.TypeScript,
false,
0,
4,
);
expect(exported.score).toBe(notExported.score * 2);
expect(exported.reasons).toContain('exported');
});
it('does not add exported reason when not exported', () => {
const result = calculateEntryPointScore('doStuff', 'typescript', false, 0, 4);
const result = calculateEntryPointScore(
'doStuff',
SupportedLanguages.TypeScript,
false,
0,
4,
);
expect(result.reasons).not.toContain('exported');
});
});
@ -44,7 +69,7 @@ describe('calculateEntryPointScore', () => {
it.each(['main', 'init', 'bootstrap', 'start', 'run', 'setup', 'configure'])(
'recognizes "%s" as entry point pattern',
(name) => {
const result = calculateEntryPointScore(name, 'typescript', false, 0, 3);
const result = calculateEntryPointScore(name, SupportedLanguages.TypeScript, false, 0, 3);
expect(result.reasons).toContain('entry-pattern');
},
);
@ -64,13 +89,19 @@ describe('calculateEntryPointScore', () => {
'fireEvent',
'emitEvent',
])('recognizes "%s" as entry point pattern', (name) => {
const result = calculateEntryPointScore(name, 'typescript', false, 0, 3);
const result = calculateEntryPointScore(name, SupportedLanguages.TypeScript, false, 0, 3);
expect(result.reasons).toContain('entry-pattern');
});
it('applies 1.5x name multiplier for entry patterns', () => {
const matching = calculateEntryPointScore('handleLogin', 'typescript', false, 0, 4);
const plain = calculateEntryPointScore('doStuff', 'typescript', false, 0, 4);
const matching = calculateEntryPointScore(
'handleLogin',
SupportedLanguages.TypeScript,
false,
0,
4,
);
const plain = calculateEntryPointScore('doStuff', SupportedLanguages.TypeScript, false, 0, 4);
// matching gets 1.5x, plain gets 1.0x
expect(matching.score).toBe(plain.score * 1.5);
});
@ -78,63 +109,81 @@ describe('calculateEntryPointScore', () => {
describe('language-specific patterns', () => {
it('recognizes React hooks for TypeScript', () => {
const result = calculateEntryPointScore('useEffect', 'typescript', false, 0, 2);
const result = calculateEntryPointScore(
'useEffect',
SupportedLanguages.TypeScript,
false,
0,
2,
);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes React hooks for JavaScript', () => {
const result = calculateEntryPointScore('useState', 'javascript', false, 0, 2);
const result = calculateEntryPointScore(
'useState',
SupportedLanguages.JavaScript,
false,
0,
2,
);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Python REST patterns', () => {
const result = calculateEntryPointScore('get_users', 'python', false, 0, 2);
const result = calculateEntryPointScore('get_users', SupportedLanguages.Python, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Java servlet patterns', () => {
const result = calculateEntryPointScore('doGet', 'java', false, 0, 2);
const result = calculateEntryPointScore('doGet', SupportedLanguages.Java, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Go handler patterns', () => {
const result = calculateEntryPointScore('NewServer', 'go', false, 0, 2);
const result = calculateEntryPointScore('NewServer', SupportedLanguages.Go, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Rust entry patterns', () => {
const result = calculateEntryPointScore('handle_request', 'rust', false, 0, 2);
const result = calculateEntryPointScore(
'handle_request',
SupportedLanguages.Rust,
false,
0,
2,
);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Swift UIKit lifecycle', () => {
const result = calculateEntryPointScore('viewDidLoad', 'swift', false, 0, 2);
const result = calculateEntryPointScore('viewDidLoad', SupportedLanguages.Swift, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Swift SwiftUI body', () => {
const result = calculateEntryPointScore('body', 'swift', false, 0, 2);
const result = calculateEntryPointScore('body', SupportedLanguages.Swift, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes PHP Laravel patterns', () => {
// __invoke starts with '_' which matches utility pattern first
const result = calculateEntryPointScore('handle', 'php', false, 0, 2);
const result = calculateEntryPointScore('handle', SupportedLanguages.PHP, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes PHP RESTful resource methods', () => {
const result = calculateEntryPointScore('index', 'php', false, 0, 2);
const result = calculateEntryPointScore('index', SupportedLanguages.PHP, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes C# ASP.NET patterns', () => {
const result = calculateEntryPointScore('GetUsers', 'csharp', false, 0, 2);
const result = calculateEntryPointScore('GetUsers', SupportedLanguages.CSharp, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes C main entry point', () => {
const result = calculateEntryPointScore('main', 'c', false, 0, 2);
const result = calculateEntryPointScore('main', SupportedLanguages.C, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
@ -152,7 +201,7 @@ describe('calculateEntryPointScore', () => {
'session_create',
'window_resize',
])('recognizes C pattern "%s"', (name) => {
const result = calculateEntryPointScore(name, 'c', false, 0, 2);
const result = calculateEntryPointScore(name, SupportedLanguages.C, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
@ -167,7 +216,7 @@ describe('calculateEntryPointScore', () => {
'OnEventReceived',
'on_click',
])('recognizes C++ pattern "%s"', (name) => {
const result = calculateEntryPointScore(name, 'cpp', false, 0, 2);
const result = calculateEntryPointScore(name, SupportedLanguages.CPlusPlus, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
@ -186,7 +235,7 @@ describe('calculateEntryPointScore', () => {
'module',
'AuthService',
])('recognizes Kotlin pattern "%s"', (name) => {
const result = calculateEntryPointScore(name, 'kotlin', false, 0, 2);
const result = calculateEntryPointScore(name, SupportedLanguages.Kotlin, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
@ -197,14 +246,14 @@ describe('calculateEntryPointScore', () => {
it.each(['main', 'build', 'createState', 'initState', 'dispose', 'runApp', 'onEvent'])(
'recognizes Dart pattern "%s"',
(name) => {
const result = calculateEntryPointScore(name, 'dart', false, 0, 2);
const result = calculateEntryPointScore(name, SupportedLanguages.Dart, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
},
);
// Ruby-specific patterns (Rails callable/job/service objects)
it.each(['call', 'perform', 'execute'])('recognizes Ruby pattern "%s"', (name) => {
const result = calculateEntryPointScore(name, 'ruby', false, 0, 2);
const result = calculateEntryPointScore(name, SupportedLanguages.Ruby, false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
});
@ -226,15 +275,21 @@ describe('calculateEntryPointScore', () => {
'cloneDeep',
'mergeObjects',
])('penalizes utility function "%s"', (name) => {
const result = calculateEntryPointScore(name, 'typescript', false, 0, 3);
const result = calculateEntryPointScore(name, SupportedLanguages.TypeScript, false, 0, 3);
expect(result.reasons).toContain('utility-pattern');
// 0.3 multiplier
const plain = calculateEntryPointScore('doStuff', 'typescript', false, 0, 3);
const plain = calculateEntryPointScore('doStuff', SupportedLanguages.TypeScript, false, 0, 3);
expect(result.score).toBeLessThan(plain.score);
});
it('penalizes private-by-convention functions', () => {
const result = calculateEntryPointScore('_internal', 'typescript', false, 0, 3);
const result = calculateEntryPointScore(
'_internal',
SupportedLanguages.TypeScript,
false,
0,
3,
);
expect(result.reasons).toContain('utility-pattern');
});
});
@ -243,7 +298,7 @@ describe('calculateEntryPointScore', () => {
it('boosts Next.js page entry points', () => {
const result = calculateEntryPointScore(
'render',
'typescript',
SupportedLanguages.TypeScript,
true,
0,
3,
@ -256,7 +311,7 @@ describe('calculateEntryPointScore', () => {
it('does not apply framework bonus for non-framework paths', () => {
const result = calculateEntryPointScore(
'render',
'typescript',
SupportedLanguages.TypeScript,
true,
0,
3,
@ -271,7 +326,7 @@ describe('calculateEntryPointScore', () => {
// handleLogin: entry pattern (1.5x) + exported (2.0x) + base
const result = calculateEntryPointScore(
'handleLogin',
'typescript',
SupportedLanguages.TypeScript,
true,
0,
4,

View file

@ -84,7 +84,8 @@ describe('fetch reason field parsing', () => {
describe('confidence derivation from fetchCount', () => {
it('high confidence when fetchCount is undefined (single fetch)', () => {
const isMultiFetch = (undefined ?? 1) > 1;
const fetchCount: number | undefined = undefined;
const isMultiFetch = (fetchCount ?? 1) > 1;
expect(isMultiFetch).toBe(false);
});

View file

@ -62,7 +62,8 @@ const createMockContext = (): FieldExtractorContext => ({
fileScope: () => new Map(),
allScopes: () => new Map(),
constructorTypeMap: new Map(),
} as TypeEnvironment,
flush: () => {},
} satisfies TypeEnvironment,
symbolTable: createSemanticModel().symbols,
filePath: 'test.ts',
language: SupportedLanguages.TypeScript,
@ -1327,7 +1328,7 @@ describeKotlin('GenericFieldExtractor — Kotlin (F52 companion)', () => {
/** The first companion_object node in `src`. */
function companion(src: string): Parser.SyntaxNode {
parser.setLanguage(Kotlin as Parser.Language);
parser.setLanguage(Kotlin);
const tree = parser.parse(src);
let found: Parser.SyntaxNode | undefined;
const walk = (n: Parser.SyntaxNode) => {
@ -1401,7 +1402,7 @@ describeKotlin('GenericFieldExtractor — Kotlin (F52 companion)', () => {
/** Parse `src` and return the first node of the given type (depth-first). */
function firstNodeOfType(src: string, type: string): Parser.SyntaxNode {
parser.setLanguage(Kotlin as Parser.Language);
parser.setLanguage(Kotlin);
const tree = parser.parse(src);
let found: Parser.SyntaxNode | undefined;
const walk = (n: Parser.SyntaxNode) => {
@ -1436,7 +1437,7 @@ describeSwift('GenericFieldExtractor — Swift (F75 protocol property requiremen
/** Parse `src` and return the first class/protocol declaration node. */
function declNode(src: string): Parser.SyntaxNode {
parser.setLanguage(Swift as Parser.Language);
parser.setLanguage(Swift);
const tree = parser.parse(src);
const node = tree.rootNode.child(0);
if (!node) throw new Error('no declaration node');

View file

@ -227,14 +227,14 @@ describe('detectFrameworkFromPath', () => {
it('detects main.rs', () => {
const result = detectFrameworkFromPath('src/main.rs');
expect(result).not.toBeNull();
expect(result!.framework).toBe('rust');
expect(result!.framework).toBe(SupportedLanguages.Rust);
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('detects bin folder', () => {
const result = detectFrameworkFromPath('src/bin/cli.rs');
expect(result).not.toBeNull();
expect(result!.framework).toBe('rust');
expect(result!.framework).toBe(SupportedLanguages.Rust);
});
});
@ -325,52 +325,53 @@ describe('detectFrameworkFromPath', () => {
});
describe('detectFrameworkFromAST', () => {
// Deliberately invalid enum inputs exercise runtime tolerance at the public boundary.
it('returns null for empty inputs', () => {
expect(detectFrameworkFromAST('', '')).toBeNull();
expect(detectFrameworkFromAST('typescript', '')).toBeNull();
expect(detectFrameworkFromAST('', 'some code')).toBeNull();
expect(detectFrameworkFromAST('' as SupportedLanguages, '')).toBeNull();
expect(detectFrameworkFromAST(SupportedLanguages.TypeScript, '')).toBeNull();
expect(detectFrameworkFromAST('' as SupportedLanguages, 'some code')).toBeNull();
});
it('detects NestJS decorators in TypeScript', () => {
const result = detectFrameworkFromAST('typescript', '@Controller("/users")');
const result = detectFrameworkFromAST(SupportedLanguages.TypeScript, '@Controller("/users")');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nestjs');
expect(result!.entryPointMultiplier).toBe(3.2);
});
it('detects NestJS decorators in JavaScript', () => {
const result = detectFrameworkFromAST('javascript', '@Get("/")');
const result = detectFrameworkFromAST(SupportedLanguages.JavaScript, '@Get("/")');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nestjs');
});
it('detects FastAPI decorators in Python', () => {
const result = detectFrameworkFromAST('python', '@app.get("/users")');
const result = detectFrameworkFromAST(SupportedLanguages.Python, '@app.get("/users")');
expect(result).not.toBeNull();
expect(result!.framework).toBe('fastapi');
});
it('detects Flask decorators in Python', () => {
const result = detectFrameworkFromAST('python', '@app.route("/users")');
const result = detectFrameworkFromAST(SupportedLanguages.Python, '@app.route("/users")');
expect(result).not.toBeNull();
expect(result!.framework).toBe('flask');
});
it('detects Spring annotations in Java', () => {
const result = detectFrameworkFromAST('java', '@RestController');
const result = detectFrameworkFromAST(SupportedLanguages.Java, '@RestController');
expect(result).not.toBeNull();
expect(result!.framework).toBe('spring');
});
it('detects ASP.NET attributes in C#', () => {
const result = detectFrameworkFromAST('csharp', '[ApiController]');
const result = detectFrameworkFromAST(SupportedLanguages.CSharp, '[ApiController]');
expect(result).not.toBeNull();
expect(result!.framework).toBe('aspnet');
});
it('detects Laravel route definitions in PHP', () => {
const result = detectFrameworkFromAST(
'php',
SupportedLanguages.PHP,
"Route::get('/users', [UserController::class, 'index'])",
);
expect(result).not.toBeNull();
@ -378,27 +379,33 @@ describe('detectFrameworkFromAST', () => {
});
it('detects Actix-web route attributes in Rust', () => {
const result = detectFrameworkFromAST('rust', '#[get("/")]');
const result = detectFrameworkFromAST(SupportedLanguages.Rust, '#[get("/")]');
expect(result).not.toBeNull();
expect(result!.framework).toBe('actix-web');
});
it('returns null for language with no matching pattern', () => {
expect(detectFrameworkFromAST('c', 'int main() { return 0; }')).toBeNull();
expect(detectFrameworkFromAST(SupportedLanguages.C, 'int main() { return 0; }')).toBeNull();
});
it('is case-insensitive', () => {
const result = detectFrameworkFromAST('TypeScript', '@controller("/")');
const result = detectFrameworkFromAST('TypeScript' as SupportedLanguages, '@controller("/")');
expect(result).not.toBeNull();
});
it('detects Expo Router useRouter hook', () => {
const result = detectFrameworkFromAST('typescript', 'const router = useRouter()');
const result = detectFrameworkFromAST(
SupportedLanguages.TypeScript,
'const router = useRouter()',
);
expect(result).not.toBeNull();
expect(result!.framework).toBe('expo-router');
});
it('detects Expo Router router.push', () => {
const result = detectFrameworkFromAST('javascript', "router.push('/settings')");
const result = detectFrameworkFromAST(
SupportedLanguages.JavaScript,
"router.push('/settings')",
);
expect(result).not.toBeNull();
expect(result!.framework).toBe('expo-router');
});

View file

@ -196,6 +196,9 @@ describe('git utilities', () => {
stdout: '',
stderr: '',
error: Object.assign(new Error('spawn git ENOENT'), { code: 'ENOENT' }),
pid: 0,
output: ['', '', ''],
signal: null,
} as ReturnType<typeof spawnSync>);
expect(listLocalHeads('/missing-git')).toBeNull();
});

View file

@ -34,7 +34,7 @@ const graphWith = (
): Pick<KnowledgeGraph, 'forEachRelationshipFields'> => ({
forEachRelationshipFields(fn) {
for (const [type, count] of Object.entries(byType)) {
for (let i = 0; i < (count ?? 0); i++) fn('src', 'dst', type as RelationshipType, 1);
for (let i = 0; i < (count ?? 0); i++) fn('src', 'dst', type as RelationshipType, 1, '');
}
},
});

View file

@ -6,7 +6,7 @@
*/
import { describe, it, expect } from 'vitest';
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
import type { GraphNode, GraphRelationship } from '../../src/core/graph/types.js';
import type { GraphNode, GraphRelationship } from 'gitnexus-shared';
function makeNode(id: string, name: string, filePath: string = 'src/test.ts'): GraphNode {
return {
@ -410,8 +410,9 @@ describe('createKnowledgeGraph', () => {
const node: Parameters<typeof g.addNode>[0] = {
id: 'cluster:x',
label: 'Community',
properties: { name: 'x' },
properties: { name: 'x', filePath: '' },
};
Reflect.deleteProperty(node.properties, 'filePath');
g.addNode(node);
g.addNode(makeNode('fn:a', 'a', 'src/a.ts'));

View file

@ -60,7 +60,7 @@ describe('cross-impact', () => {
direction: 'sideways',
});
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error).toContain('direction');
if (r.ok === false) expect(r.error).toContain('direction');
});
it('test_validateGroupImpactParams_clamps_crossDepth_and_warns', () => {
@ -110,7 +110,7 @@ describe('cross-impact', () => {
direction: 'upstream',
});
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error).toBe('target or target_uid is required');
if (r.ok === false) expect(r.error).toBe('target or target_uid is required');
});
it('test_collectImpactSymbolUids_respects_service_prefix', () => {

View file

@ -34,6 +34,7 @@ function run(files: Record<string, string>): RunResult {
};
const readFile = (rel: string): string | null => files[rel] ?? null;
const ctx = PYTHON_HTTP_PLUGIN.prepareRepo?.({
repoPath: '/repo',
files: Object.keys(files),
parser,
readFile,

View file

@ -1391,6 +1391,7 @@ service Foo { rpc Bar (Req) returns (Res); }`;
describe('resolveProtoConflict', () => {
const makeInfo = (pkg: string, protoPath: string): ProtoServiceInfo => ({
package: pkg,
javaPackage: '',
serviceName: 'Svc',
methods: ['Do'],
protoPath,

View file

@ -29,7 +29,6 @@ import {
expandJavaWildcardStaticImports,
extractJavaModuleConstants,
foldJavaOperands,
type RepoConstants,
} from '../../../src/core/ingestion/route-extractors/java-const-resolver.js';
const parser = new Parser();
@ -42,6 +41,7 @@ const parse = (src: string): Parser.Tree => parseSource(parser, src);
/** Group side: prepareRepo + a 3-argument scan over every .java file. */
function groupProviders(files: Record<string, string>): string[] {
const ctx = JAVA_HTTP_PLUGIN.prepareRepo?.({
repoPath: '/repo',
files: Object.keys(files),
parser: new Parser(),
readFile: (rel: string) => files[rel] ?? null,
@ -59,7 +59,7 @@ function groupProviders(files: Record<string, string>): string[] {
/** Ingestion side: extract routes, then fold operands against the same map. */
function ingestionRoutes(files: Record<string, string>): string[] {
const repo: RepoConstants = new Map();
const repo = new Map<string, ReturnType<typeof extractJavaModuleConstants>>();
for (const [rel, src] of Object.entries(files)) {
repo.set(rel, extractJavaModuleConstants(parse(src)));
}

View file

@ -61,7 +61,7 @@ const describeKotlin = Kotlin && KOTLIN_HTTP_PLUGIN ? describe : describe.skip;
const plugin = KOTLIN_HTTP_PLUGIN as HttpLanguagePlugin;
const parseSource = (p: Parser, src: string): Parser.Tree => {
p.setLanguage(Kotlin as Parser.Language);
p.setLanguage(Kotlin);
return p.parse(src);
};

View file

@ -303,7 +303,7 @@ describe('GroupService', () => {
const { cleanup, tmpDir } = makeTmpGroup();
try {
vi.stubEnv('GITNEXUS_HOME', tmpDir);
const query = vi.fn(async () => ({ processes: [] }));
const query = vi.fn<GroupToolPort['query']>(async () => ({ processes: [] }));
const svc = new GroupService(makePort({ query }));
await svc.groupQuery({ name: 'test-group', query: 'auth flow' });
expect(query).toHaveBeenCalled();
@ -320,7 +320,7 @@ describe('GroupService', () => {
const { cleanup, tmpDir } = makeTmpGroup();
try {
vi.stubEnv('GITNEXUS_HOME', tmpDir);
const query = vi.fn(async () => ({ processes: [] }));
const query = vi.fn<GroupToolPort['query']>(async () => ({ processes: [] }));
const svc = new GroupService(makePort({ query }));
await svc.groupQuery({
name: 'test-group',
@ -342,7 +342,7 @@ describe('GroupService', () => {
const { cleanup, tmpDir } = makeTmpGroup();
try {
vi.stubEnv('GITNEXUS_HOME', tmpDir);
const query = vi.fn(async () => ({ processes: [] }));
const query = vi.fn<GroupToolPort['query']>(async () => ({ processes: [] }));
const svc = new GroupService(makePort({ query }));
const infiniteLimit = await svc.groupQuery({
@ -403,7 +403,7 @@ describe('GroupService', () => {
const { cleanup, tmpDir } = makeTmpGroup();
try {
vi.stubEnv('GITNEXUS_HOME', tmpDir);
const query = vi.fn(async () => ({ processes: [] }));
const query = vi.fn<GroupToolPort['query']>(async () => ({ processes: [] }));
const svc = new GroupService(makePort({ query }));
await svc.groupQuery({ name: 'test-group', query: 'auth flow', chain_depth: 2 });
expect(query).toHaveBeenCalled();
@ -575,7 +575,7 @@ repos:
const { cleanup, tmpDir } = makeTmpGroup();
try {
vi.stubEnv('GITNEXUS_HOME', tmpDir);
const context = vi.fn(async () => ({ status: 'found' }));
const context = vi.fn<GroupToolPort['context']>(async () => ({ status: 'found' }));
const svc = new GroupService(makePort({ context }));
const r = await svc.groupContext({
name: 'test-group',
@ -594,7 +594,7 @@ repos:
const { cleanup, tmpDir } = makeTmpGroup();
try {
vi.stubEnv('GITNEXUS_HOME', tmpDir);
const context = vi.fn(async () => ({
const context = vi.fn<GroupToolPort['context']>(async () => ({
status: 'found',
symbol: { filePath: 'services/auth/x.ts', uid: 'u1', name: 'X' },
}));

View file

@ -54,10 +54,12 @@ const grpcExtract = vi.fn();
// Bound through an arrow so the test body can read its calls: which repos the
// deferred manifest phase re-opens is the observable side of dropping a failed
// repo's handle, and a `vi.fn()` created inside the factory is unreachable here.
const initLbugMock = vi.fn(async () => {});
const initLbugMock = vi.fn<typeof import('../../../src/core/lbug/pool-adapter.js').initLbug>(
async () => true,
);
vi.mock('../../../src/core/lbug/pool-adapter.js', () => ({
initLbug: (...args: unknown[]) => initLbugMock(...args),
initLbug: initLbugMock,
executeParameterized: vi.fn(async () => []),
pinRepo: vi.fn(() => () => {}),
getMaxResidentRepos: vi.fn(() => 5),

View file

@ -9,10 +9,12 @@ import type { GroupConfig } from '../../../src/core/group/types.js';
import { GroupService } from '../../../src/core/group/service.js';
import type { GroupToolPort } from '../../../src/core/group/service.js';
const initLbugMock = vi.fn(async () => {});
const initLbugMock = vi.fn<typeof import('../../../src/core/lbug/pool-adapter.js').initLbug>(
async () => true,
);
vi.mock('../../../src/core/lbug/pool-adapter.js', () => ({
initLbug: (...args: unknown[]) => initLbugMock(...args),
initLbug: (...args: Parameters<typeof initLbugMock>) => initLbugMock(...args),
executeParameterized: vi.fn(async () => []),
pinRepo: vi.fn(() => () => {}),
getMaxResidentRepos: vi.fn(() => 5),

View file

@ -1438,6 +1438,7 @@ describe('syncGroup windowed manifest resolution (issue #2189 / PR #2191 review)
const n = (initCounts.get(id) ?? 0) + 1;
initCounts.set(id, n);
if (id === 'app-repo-2' && n === 2) throw new Error('window init boom');
return true;
});
const execSpy = vi.spyOn(poolAdapter, 'executeParameterized').mockResolvedValue([]);
const pinSpy = vi.spyOn(poolAdapter, 'pinRepo').mockImplementation(() => {

View file

@ -509,24 +509,17 @@ class Outer {
});
it('returns null for a node without parent', () => {
// Simulate a node with null parent
const fakeNode = { parent: null };
const result = findEnclosingClassId(fakeNode, filePath);
const root = parseCode(TypeScript.typescript, 'const value = 1;').rootNode;
expect(root.parent).toBeNull();
const result = findEnclosingClassId(root, filePath);
expect(result).toBeNull();
});
it('skips containers without a name node', () => {
// Simulate AST nodes where the class container has no name
const fakeClassNode = {
type: 'class_declaration',
childForFieldName: () => null,
children: [],
parent: null,
};
const fakeChild = {
parent: fakeClassNode,
};
const result = findEnclosingClassId(fakeChild, filePath);
const tree = parseCode(TypeScript.typescript, 'const Anonymous = class { method() {} };');
const method = findNode(tree.rootNode, (node) => node.type === 'method_definition');
expect(method).not.toBeNull();
const result = findEnclosingClassId(method!, filePath);
// The class has no name, so should return null
expect(result).toBeNull();
});

View file

@ -738,7 +738,7 @@ describe.skipIf(!isLinux)('Linux cmdline-first DB-owner scan (#2180)', () => {
err.code = code;
throw err;
}
return (realReaddir as (...a: unknown[]) => unknown)(p, ...rest);
return realReaddir(p, ...rest);
});
cleanups.push(() => spy.mockRestore());
// White-box: assert the exported tri-state verdict directly (the

View file

@ -9,7 +9,7 @@ const executeParameterizedMock = vi.fn();
// imports) and the re-export shim (mcp/core/lbug-adapter.js) so the mocks intercept
// regardless of import path.
vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => {
const actual = await importOriginal();
const actual = await importOriginal<typeof import('../../src/core/lbug/pool-adapter.js')>();
return {
...actual,
initLbug: vi.fn(),
@ -20,7 +20,7 @@ vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => {
};
});
vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => {
const actual = await importOriginal();
const actual = await importOriginal<typeof import('../../src/mcp/core/lbug-adapter.js')>();
return {
...actual,
initLbug: vi.fn(),
@ -31,7 +31,7 @@ vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => {
};
});
import { LocalBackend } from '../../src/mcp/local/local-backend';
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
describe('impact: batching and grouping', () => {
beforeEach(() => {

View file

@ -4,7 +4,7 @@ const executeQueryMock = vi.fn();
const executeParameterizedMock = vi.fn();
vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => {
const actual = await importOriginal();
const actual = await importOriginal<typeof import('../../src/core/lbug/pool-adapter.js')>();
return {
...actual,
initLbug: vi.fn(),
@ -15,7 +15,7 @@ vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => {
};
});
vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => {
const actual = await importOriginal();
const actual = await importOriginal<typeof import('../../src/mcp/core/lbug-adapter.js')>();
return {
...actual,
initLbug: vi.fn(),
@ -26,8 +26,8 @@ vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => {
};
});
import { LocalBackend } from '../../src/mcp/local/local-backend';
import { collectImpactSymbolUids } from '../../src/core/group/cross-impact';
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
import { collectImpactSymbolUids } from '../../src/core/group/cross-impact.js';
function makeBackend() {
const backend = new LocalBackend();

View file

@ -11,7 +11,6 @@
// `npm test`.
import { describe, expect, it } from 'vitest';
// @ts-expect-error — .mjs pure-JS harness module, no types (intentional; build-free).
import * as M from '../../bench/impact-pdg/measure.mjs';
interface ReachedItem {

View file

@ -9,7 +9,6 @@
// `node --import tsx bench/impact-pdg/name-collision.mjs`, never in `npm test`.
import { describe, expect, it } from 'vitest';
// @ts-expect-error — .mjs pure-JS module, no types; intentional (build-free harness).
import * as M from '../../bench/impact-pdg/name-collision.mjs';
interface ReachedItem {

View file

@ -9,7 +9,6 @@
// `measure.mjs`, never in `npm test`.
import { describe, it, expect } from 'vitest';
// @ts-expect-error — .mjs pure-JS module, no types; intentional (build-free harness).
import * as M from '../../bench/impact-pdg/metrics.mjs';
const k = (sym: string, file = 'src/a.ts') => M.symbolKey(sym, file);

View file

@ -11,7 +11,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
const executeParameterizedMock = vi.fn();
vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => {
const actual = await importOriginal();
const actual = await importOriginal<typeof import('../../src/core/lbug/pool-adapter.js')>();
return {
...(actual as object),
initLbug: vi.fn(),
@ -22,7 +22,7 @@ vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => {
};
});
vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => {
const actual = await importOriginal();
const actual = await importOriginal<typeof import('../../src/mcp/core/lbug-adapter.js')>();
return {
...(actual as object),
initLbug: vi.fn(),
@ -33,7 +33,7 @@ vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => {
};
});
import { LocalBackend } from '../../src/mcp/local/local-backend';
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
const REPO = {
id: 'repo',

View file

@ -26,10 +26,8 @@ const minimalResult = (overrides: Partial<ParseWorkerResult> = {}): ParseWorkerR
nodes: [],
relationships: [],
symbols: [],
imports: [],
calls: [],
assignments: [],
heritage: [],
routes: [],
fetchCalls: [],
fetchWrapperDefs: [],
@ -897,15 +895,12 @@ describe('loadParseCache / saveParseCache (round-trip)', () => {
['k2', 'v2'],
]);
const innerSet = new Set<string>(['s1', 's2']);
const fake = minimalResult({
fileCount: 9,
imports: [
{
typeBindings: innerMap,
extras: innerSet,
} as unknown as ParseWorkerResult['imports'][number],
],
});
// Exercise opaque structured-clone data without relying on a removed
// worker field (ParsedFiles intentionally live in a separate store).
const fake = {
...minimalResult({ fileCount: 9 }),
serializationProbe: { typeBindings: innerMap, extras: innerSet },
};
const key = 'f'.repeat(64);
const cache: ParseCache = {
version: PARSE_CACHE_VERSION,
@ -920,17 +915,29 @@ describe('loadParseCache / saveParseCache (round-trip)', () => {
expect(names.some((n) => n.endsWith('.json') && n !== 'index.json')).toBe(false);
const loaded = await loadParseCacheChunk(cache, key);
expect(loaded?.[0]?.fileCount).toBe(9);
const smuggled = loaded?.[0]?.imports[0] as unknown as {
typeBindings?: unknown;
extras?: unknown;
};
const restored = loaded?.[0];
if (!restored || !('serializationProbe' in restored)) {
throw new Error('Missing serialized probe');
}
const smuggled = restored.serializationProbe;
if (
typeof smuggled !== 'object' ||
smuggled === null ||
!('typeBindings' in smuggled) ||
!('extras' in smuggled)
) {
throw new Error('Invalid serialized probe');
}
expect(smuggled.typeBindings).toBeInstanceOf(Map);
expect([...(smuggled.typeBindings as Map<string, string>)]).toEqual([
expect(smuggled.extras).toBeInstanceOf(Set);
if (!(smuggled.typeBindings instanceof Map) || !(smuggled.extras instanceof Set)) {
throw new Error('Structured-clone collections were not preserved');
}
expect([...smuggled.typeBindings]).toEqual([
['k1', 'v1'],
['k2', 'v2'],
]);
expect(smuggled.extras).toBeInstanceOf(Set);
expect([...(smuggled.extras as Set<string>)].sort()).toEqual(['s1', 's2']);
expect([...smuggled.extras].sort()).toEqual(['s1', 's2']);
} finally {
await rm(dir, { recursive: true, force: true });
}

View file

@ -105,7 +105,11 @@ describe('incremental graph identity before publication', () => {
label: 'Tool',
properties: { name: '王', filePath: 'src/Å.ts' },
});
graph.addNode({ id: 'Destination:topic', label: 'Destination', properties: { name: 'topic' } });
graph.addNode({
id: 'Destination:topic',
label: 'Destination',
properties: { name: 'topic', filePath: '' },
});
graph.addNode({
id: 'Function:src/Å.ts:王\u0000\uD800',
label: 'Function',
@ -244,7 +248,7 @@ describe('incremental graph identity before publication', () => {
const recovered = await runFullAnalysis(repo.dbPath, options, callbacks);
expect(recovered.incrementalStats).toBeUndefined();
expect((await loadMeta(storagePath))?.incrementalInProgress).toBeUndefined();
await adapter.initLbug(lbugPath, { readOnly: true });
await adapter.initLbug(lbugPath);
await expect(
reconcileGraphNodeIdentities(
recovered.pipelineResult.graph,

View file

@ -71,7 +71,7 @@ describe('indexCommand', () => {
const storageFailure = (
state: 'empty' | 'owned' | 'unowned',
hasCodeIndexDB: boolean,
): StorageRequirementError =>
): InstanceType<typeof StorageRequirementError> =>
new StorageRequirementError(
{
repoPath: resolvedRepo,

View file

@ -45,8 +45,6 @@ describe('Python notebook scope captures', () => {
expect(fnCapture?.['@scope.function']?.range.startLine).toBe(expectedJson + 1);
const parsed = extractParsedFile(pythonProvider, notebook, 'analysis.ipynb');
expect(parsed).toBeDefined();
expect(parsed!.localDefs.some((d) => d.qualifiedName === 'train' || d.name === 'train')).toBe(
true,
);
expect(parsed!.localDefs.some((d) => d.qualifiedName === 'train')).toBe(true);
});
});

View file

@ -3,7 +3,7 @@ import type { GraphNode } from 'gitnexus-shared';
import { javaProvider } from '../../src/core/ingestion/languages/java.js';
import { javaRuntimeSymbolStrategy } from '../../src/core/ingestion/languages/java/spring-actuator.js';
function method(name: string, properties: GraphNode['properties'] = {}): GraphNode {
function method(name: string, properties: Partial<GraphNode['properties']> = {}): GraphNode {
return {
id: `method:${name}:${JSON.stringify(properties.parameterTypes)}`,
label: 'Method',

Some files were not shown because too many files have changed in this diff Show more