Spaces:
Running
Running
| 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/<id>') | |
| 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); | |
| }); | |
| }); | |
| }); | |