import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { generateDomainContext, detectEntryPoints, extractFileSignatures, extractMetadata, truncateToFit, } from './domain-context'; function createTempDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'domain-ctx-test-')); } function removeTempDir(dir) { fs.rmSync(dir, { recursive: true, force: true }); } function createFileEntry(tmpDir, relativePath, content) { const fullPath = path.join(tmpDir, relativePath); fs.mkdirSync(path.dirname(fullPath), { recursive: true }); fs.writeFileSync(fullPath, content); return { path: fullPath, relativePath, category: 'code', language: 'typescript', lineCount: content.split('\n').length, }; } describe('domain-context', () => { let tmpDir; beforeEach(() => { tmpDir = createTempDir(); }); afterEach(() => { removeTempDir(tmpDir); }); describe('detectEntryPoints', () => { it('detects Express-style HTTP routes', () => { const file = createFileEntry(tmpDir, 'src/routes.ts', ` import express from 'express'; const app = express(); app.get('/users', (req, res) => { res.json([]); }); app.post('/users', (req, res) => { res.status(201).json({}); }); `); const entryPoints = detectEntryPoints(tmpDir, [file]); expect(entryPoints.length).toBe(2); expect(entryPoints[0].type).toBe('http'); expect(entryPoints[0].description).toBe('Express/Koa route'); expect(entryPoints[0].match).toContain('/users'); expect(entryPoints[0].file).toBe('src/routes.ts'); expect(entryPoints[0].line).toBeGreaterThan(0); expect(entryPoints[0].snippet.length).toBeGreaterThan(0); }); it('detects Python decorator routes', () => { const file = createFileEntry(tmpDir, 'app/views.py', ` from flask import Flask app = Flask(__name__) @app.route('/api/items') def get_items(): return jsonify([]) @app.route('/api/items/') def get_item(id): return jsonify({}) `); // Override language for Python file.language = 'python'; const entryPoints = detectEntryPoints(tmpDir, [file]); expect(entryPoints.length).toBe(2); expect(entryPoints[0].type).toBe('http'); expect(entryPoints[0].description).toBe('Decorator route (Flask/FastAPI/NestJS)'); expect(entryPoints[0].match).toContain('/api/items'); }); it('skips test files in entry point detection', () => { const testFile = createFileEntry(tmpDir, 'src/routes.test.ts', ` import express from 'express'; const app = express(); app.get('/test-route', (req, res) => { res.json([]); }); `); const specFile = createFileEntry(tmpDir, 'src/__tests__/api.ts', ` app.post('/another-route', handler); `); const entryPoints = detectEntryPoints(tmpDir, [testFile, specFile]); expect(entryPoints.length).toBe(0); }); it('detects CLI commands', () => { const file = createFileEntry(tmpDir, 'src/cli.ts', ` program .command('build') .description('Build the project') .action(buildAction); program .command('deploy') .description('Deploy to production') .action(deployAction); `); const entryPoints = detectEntryPoints(tmpDir, [file]); expect(entryPoints.length).toBe(2); expect(entryPoints[0].type).toBe('cli'); expect(entryPoints[0].match).toContain('build'); }); it('detects event listeners', () => { const file = createFileEntry(tmpDir, 'src/events.ts', ` emitter.on('user:created', handleUserCreated); emitter.on('order:completed', handleOrderCompleted); `); const entryPoints = detectEntryPoints(tmpDir, [file]); expect(entryPoints.length).toBe(2); expect(entryPoints[0].type).toBe('event'); }); }); describe('extractFileSignatures', () => { it('extracts file signatures with exports and imports', () => { const file = createFileEntry(tmpDir, 'src/service/user-service.ts', ` import { db } from '../database'; import { UserModel } from '../models/user'; import { hashPassword } from '../utils/crypto'; export class UserService { async createUser(name: string, email: string) { return db.insert({ name, email }); } } export function validateEmail(email: string): boolean { return email.includes('@'); } export const USER_ROLES = ['admin', 'user'] as const; `); const signatures = extractFileSignatures(tmpDir, [file]); expect(signatures.length).toBe(1); expect(signatures[0].file).toBe('src/service/user-service.ts'); expect(signatures[0].exports).toContain('UserService'); expect(signatures[0].exports).toContain('validateEmail'); expect(signatures[0].exports).toContain('USER_ROLES'); expect(signatures[0].imports).toContain('../database'); expect(signatures[0].imports).toContain('../models/user'); expect(signatures[0].imports).toContain('../utils/crypto'); expect(signatures[0].lines).toBeGreaterThan(0); expect(signatures[0].preview.length).toBeGreaterThan(0); expect(signatures[0].preview.length).toBeLessThanOrEqual(500); }); it('prioritizes files with business-logic keywords', () => { const genericFile = createFileEntry(tmpDir, 'src/utils/helpers.ts', ` export function formatDate(d: Date): string { return d.toISOString(); } `); const controllerFile = createFileEntry(tmpDir, 'src/controller/user-controller.ts', ` export class UserController { getUser() {} } `); const signatures = extractFileSignatures(tmpDir, [genericFile, controllerFile]); // Controller should come first due to priority keywords expect(signatures[0].file).toBe('src/controller/user-controller.ts'); }); }); describe('extractMetadata', () => { it('reads package.json metadata', () => { const pkgContent = JSON.stringify({ name: 'my-app', description: 'A test application', scripts: { build: 'tsc', test: 'vitest' }, dependencies: { express: '^4.18.0', lodash: '^4.17.0' }, devDependencies: { typescript: '^5.0.0' }, }); fs.writeFileSync(path.join(tmpDir, 'package.json'), pkgContent); const metadata = extractMetadata(tmpDir); expect(metadata['package.json']).toBeDefined(); const pkg = metadata['package.json']; expect(pkg.name).toBe('my-app'); expect(pkg.description).toBe('A test application'); expect(pkg.scripts).toEqual(['build', 'test']); expect(pkg.dependencies).toEqual(['express', 'lodash']); expect(pkg.devDependencies).toEqual(['typescript']); }); it('reads README.md metadata', () => { fs.writeFileSync(path.join(tmpDir, 'README.md'), '# My Project\n\nThis is a test project.'); const metadata = extractMetadata(tmpDir); expect(metadata['README.md']).toBe('# My Project\n\nThis is a test project.'); }); }); describe('truncateToFit', () => { it('respects 512 KB output cap', () => { // Generate a large domain context that exceeds 512 KB const largeFileTree = []; for (let i = 0; i < 5000; i++) { largeFileTree.push(`src/modules/module-${i}/very-long-file-name-that-takes-up-space-${i}.ts`); } const largeEntryPoints = []; for (let i = 0; i < 200; i++) { largeEntryPoints.push({ file: `src/routes/route-${i}.ts`, line: i, type: 'http', description: 'Express/Koa route', match: `app.get('/api/v1/resources/${i}/details/nested/path')`.slice(0, 120), snippet: 'x'.repeat(300), }); } const largeSignatures = []; for (let i = 0; i < 40; i++) { largeSignatures.push({ file: `src/services/service-${i}.ts`, exports: Array.from({ length: 20 }, (_, j) => `export${j}`), imports: Array.from({ length: 20 }, (_, j) => `./module${j}`), lines: 500, preview: 'y'.repeat(500), }); } const context = { projectRoot: '/tmp/test-project', fileCount: 5000, fileTree: largeFileTree, entryPoints: largeEntryPoints, fileSignatures: largeSignatures, metadata: {}, }; const truncated = truncateToFit(context); const output = JSON.stringify(truncated, null, 2); const byteSize = Buffer.byteLength(output, 'utf-8'); expect(byteSize).toBeLessThanOrEqual(512 * 1024); }); it('does not truncate when under the limit', () => { const context = { projectRoot: '/tmp/small', fileCount: 2, fileTree: ['src/a.ts', 'src/b.ts'], entryPoints: [], fileSignatures: [], metadata: {}, }; const result = truncateToFit(context); expect(result.fileTree).toEqual(['src/a.ts', 'src/b.ts']); }); }); describe('generateDomainContext', () => { it('handles empty file list gracefully', () => { const context = generateDomainContext(tmpDir, []); expect(context.projectRoot).toBe(tmpDir); expect(context.fileCount).toBe(0); expect(context.fileTree).toEqual([]); expect(context.entryPoints).toEqual([]); expect(context.fileSignatures).toEqual([]); }); it('generates complete domain context', () => { const routeFile = createFileEntry(tmpDir, 'src/routes.ts', ` import express from 'express'; const router = express.Router(); router.get('/api/users', getUsers); export function handleRequest() {} `); const serviceFile = createFileEntry(tmpDir, 'src/service.ts', ` import { db } from './db'; export class UserService { findAll() { return db.query('SELECT * FROM users'); } } `); const context = generateDomainContext(tmpDir, [routeFile, serviceFile]); expect(context.fileCount).toBe(2); expect(context.fileTree).toContain('src/routes.ts'); expect(context.fileTree).toContain('src/service.ts'); expect(context.entryPoints.length).toBeGreaterThan(0); expect(context.fileSignatures.length).toBe(2); }); }); });