GitNexus/gitnexus/test/integration/ignore-and-skip-e2e.test.ts

214 lines
8.5 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import {
walkRepositoryPaths,
readFileContents,
} from '../../src/core/ingestion/filesystem-walker.js';
import { parseFilesWithWorkers } from '../helpers/worker-parse.js';
import { isLanguageAvailable } from '../../src/core/tree-sitter/parser-loader.js';
import { SupportedLanguages } from '../../src/config/supported-languages.js';
// ============================================================================
// E2E: .gitignore + .gitnexusignore + unsupported language skip
// ============================================================================
describe('ignore + language-skip E2E', () => {
let tmpDir: string;
beforeAll(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-e2e-ignore-skip-'));
// Create directory structure
await fs.mkdir(path.join(tmpDir, 'src'), { recursive: true });
await fs.mkdir(path.join(tmpDir, 'data'), { recursive: true });
await fs.mkdir(path.join(tmpDir, 'vendor'), { recursive: true });
// .gitignore — excludes data/ and *.log
await fs.writeFile(path.join(tmpDir, '.gitignore'), 'data/\n*.log\n');
// .gitnexusignore — excludes vendor/
await fs.writeFile(path.join(tmpDir, '.gitnexusignore'), 'vendor/\n');
// Source files (should be indexed)
await fs.writeFile(
path.join(tmpDir, 'src', 'index.ts'),
"import { greet } from './greet';\n\nexport function main(): string {\n return greet();\n}\n",
);
await fs.writeFile(
path.join(tmpDir, 'src', 'greet.ts'),
"export function greet(): string {\n return 'hello';\n}\n",
);
await fs.writeFile(
path.join(tmpDir, 'src', 'service.ts'),
'export class UserService { load(): string { return "loaded"; } }\n',
);
await fs.writeFile(
path.join(tmpDir, 'src', 'service.d.ts'),
'export declare class UserService { load(): string; }\n',
);
await fs.writeFile(
path.join(tmpDir, 'src', 'vite-env.d.ts'),
'declare const APP_ENV: string;\n',
);
await fs.writeFile(path.join(tmpDir, 'src', 'esm-service.mts'), 'export class EsmService {}\n');
await fs.writeFile(
path.join(tmpDir, 'src', 'esm-service.d.mts'),
'export declare class EsmService {}\n',
);
await fs.writeFile(path.join(tmpDir, 'src', 'cjs-service.cts'), 'export class CjsService {}\n');
await fs.writeFile(
path.join(tmpDir, 'src', 'cjs-service.d.cts'),
'export declare class CjsService {}\n',
);
await fs.writeFile(
path.join(tmpDir, 'src', 'ambient.d.mts'),
'export declare class AmbientEsmService {}\n',
);
await fs.writeFile(
path.join(tmpDir, 'src', 'ambient.d.cts'),
'export declare class AmbientCjsService {}\n',
);
// Swift file — triggers language skip when grammar unavailable
await fs.writeFile(
path.join(tmpDir, 'src', 'App.swift'),
'class App {\n func run() {\n print("running")\n }\n}\n',
);
// Files that should be excluded
await fs.writeFile(path.join(tmpDir, 'data', 'seed.json'), '{}');
await fs.writeFile(path.join(tmpDir, 'vendor', 'lib.js'), 'var x = 1;\n');
await fs.writeFile(path.join(tmpDir, 'debug.log'), 'debug log entry\n');
});
afterAll(async () => {
try {
await fs.rm(tmpDir, { recursive: true, force: true });
} catch {
/* best-effort */
}
});
// ── File Discovery ──────────────────────────────────────────────────
describe('file discovery (walkRepositoryPaths)', () => {
it('includes source files from src/', async () => {
const files = await walkRepositoryPaths(tmpDir);
const paths = files.map((f) => f.path.replace(/\\/g, '/'));
expect(paths).toContain('src/index.ts');
expect(paths).toContain('src/greet.ts');
expect(paths).toContain('src/service.ts');
expect(paths).not.toContain('src/service.d.ts');
expect(paths).toContain('src/vite-env.d.ts');
expect(paths).toContain('src/esm-service.mts');
expect(paths).not.toContain('src/esm-service.d.mts');
expect(paths).toContain('src/cjs-service.cts');
expect(paths).not.toContain('src/cjs-service.d.cts');
expect(paths).toContain('src/ambient.d.mts');
expect(paths).toContain('src/ambient.d.cts');
});
it('includes .swift files (discovery does not filter by language)', async () => {
const files = await walkRepositoryPaths(tmpDir);
const paths = files.map((f) => f.path.replace(/\\/g, '/'));
// Swift file should be discovered — language skip happens at parse time
expect(paths).toContain('src/App.swift');
});
it('excludes gitignored directories (data/)', async () => {
const files = await walkRepositoryPaths(tmpDir);
const paths = files.map((f) => f.path.replace(/\\/g, '/'));
expect(paths.every((p) => !p.includes('data/'))).toBe(true);
});
it('excludes gitignored file patterns (*.log)', async () => {
const files = await walkRepositoryPaths(tmpDir);
const paths = files.map((f) => f.path.replace(/\\/g, '/'));
expect(paths.every((p) => !p.endsWith('.log'))).toBe(true);
});
it('excludes gitnexusignored directories (vendor/)', async () => {
const files = await walkRepositoryPaths(tmpDir);
const paths = files.map((f) => f.path.replace(/\\/g, '/'));
expect(paths.every((p) => !p.includes('vendor/'))).toBe(true);
});
});
// ── Parsing ─────────────────────────────────────────────────────────
describe('parsing (processParsing)', () => {
it('parses TypeScript files into graph nodes and skips Swift gracefully', async () => {
// Phase 1: discover files
const scannedFiles = await walkRepositoryPaths(tmpDir);
const relativePaths = scannedFiles.map((f) => f.path);
// Phase 2: read contents
const contentMap = await readFileContents(tmpDir, relativePaths);
const files = Array.from(contentMap.entries()).map(([p, content]) => ({
path: p,
content,
}));
// Phase 3: parse through the worker pool (the sole parse path).
// Should NOT throw even if the Swift grammar is unavailable — the
// worker skips files whose native parser can't load.
const { graph } = await parseFilesWithWorkers(files);
// TypeScript files should produce Function nodes
const nodes = graph.nodes;
const functionNodes = nodes.filter((n) => n.label === 'Function');
const functionNames = functionNodes.map((n) => n.properties.name);
expect(functionNames).toContain('main');
expect(functionNames).toContain('greet');
const userServiceNodes = nodes.filter(
(node) => node.label === 'Class' && node.properties.name === 'UserService',
);
expect(userServiceNodes).toHaveLength(1);
expect(userServiceNodes[0].properties.filePath).toBe('src/service.ts');
expect(nodes.some((node) => node.properties.filePath === 'src/service.d.ts')).toBe(false);
expect(
nodes.filter((node) => node.label === 'Class' && node.properties.name === 'EsmService'),
).toHaveLength(1);
expect(
nodes.filter((node) => node.label === 'Class' && node.properties.name === 'CjsService'),
).toHaveLength(1);
expect(
nodes.filter(
(node) => node.label === 'Class' && node.properties.name === 'AmbientEsmService',
),
).toHaveLength(1);
expect(
nodes.filter(
(node) => node.label === 'Class' && node.properties.name === 'AmbientCjsService',
),
).toHaveLength(1);
// Function nodes should reference the correct source files
const fnFilePaths = functionNodes.map((n) =>
(n.properties.filePath as string).replace(/\\/g, '/'),
);
expect(fnFilePaths.some((p) => p.includes('index.ts'))).toBe(true);
expect(fnFilePaths.some((p) => p.includes('greet.ts'))).toBe(true);
// Swift behavior depends on grammar availability
if (!isLanguageAvailable(SupportedLanguages.Swift)) {
// No Swift-sourced nodes should appear in the graph
const swiftNodes = nodes.filter((n) =>
(n.properties.filePath as string | undefined)?.endsWith('.swift'),
);
expect(swiftNodes).toHaveLength(0);
}
// If Swift IS available, Swift nodes may appear — that's fine
});
});
});