mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
fix(test): align fixtures and mocks with current types (#3471)
This commit is contained in:
parent
504bff7102
commit
c474a811ba
187 changed files with 1705 additions and 1186 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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] ?? [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"'));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ function createGoRepo(): string {
|
|||
async function runWorker(): Promise<PipelineResult> {
|
||||
return runPipelineFromRepo(createGoRepo(), () => {}, {
|
||||
skipGraphPhases: true,
|
||||
workerThresholdsForTest: { minFiles: 1, minBytes: 1 },
|
||||
workerPoolSize: 2,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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): {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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.*;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ function mkCfg(
|
|||
return {
|
||||
filePath: 't.ts',
|
||||
functionStartLine: 1,
|
||||
functionEndLine: blockCount,
|
||||
functionStartColumn: 0,
|
||||
entryIndex: entry,
|
||||
exitIndex: exit,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ function mkCfg(
|
|||
return {
|
||||
filePath: 't.ts',
|
||||
functionStartLine: 1,
|
||||
functionEndLine: blockCount,
|
||||
functionStartColumn: 0,
|
||||
entryIndex: entry,
|
||||
exitIndex: exit,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ function mkCfg(
|
|||
return {
|
||||
filePath: 't.ts',
|
||||
functionStartLine: 1,
|
||||
functionEndLine: blockCount,
|
||||
functionStartColumn: 0,
|
||||
entryIndex: entry,
|
||||
exitIndex: exit,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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/,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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' }),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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, '');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ describe('indexCommand', () => {
|
|||
const storageFailure = (
|
||||
state: 'empty' | 'owned' | 'unowned',
|
||||
hasCodeIndexDB: boolean,
|
||||
): StorageRequirementError =>
|
||||
): InstanceType<typeof StorageRequirementError> =>
|
||||
new StorageRequirementError(
|
||||
{
|
||||
repoPath: resolvedRepo,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue