/** * E2E Integration Tests: --skills Flag * * Tests `gitnexus analyze --skills` across 11 supported languages plus * mixed-language and idempotency scenarios. Each language fixture creates * a self-contained git repo with 2 clusters of files containing cross-file * function calls, then runs the full CLI pipeline and verifies SKILL.md * generation and context file updates. * * Uses process.execPath (never 'node' string), no shell: true. * Accepts status === null (timeout) as valid on slow CI runners. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { CLI_SPAWN_PREFIX } from '../helpers/cli-entry.js'; import { spawnSync } from 'child_process'; import path from 'path'; import fs from 'fs'; import os from 'os'; // ============================================================================ // FILE-LOCAL HELPERS // ============================================================================ /** * Spawn the CLI with `analyze --skills` in the given cwd. * Entry point comes from CLI_SPAWN_PREFIX (built dist in CI, tsx-on-source * locally); the tsx path uses an absolute loader URL so it resolves from any cwd. */ function runSkillsCli(cwd: string, timeoutMs = 45000) { return spawnSync(process.execPath, [...CLI_SPAWN_PREFIX, 'analyze', '--skills'], { cwd, encoding: 'utf8', timeout: timeoutMs, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --max-old-space-size=8192`.trim(), }, }); } /** * Create a fixture repo: write files, git init, git add, git commit. * Returns the tmp directory path. */ function createFixtureRepo(prefix: string, files: Record): string { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `skills-e2e-${prefix}-`)); for (const [relPath, content] of Object.entries(files)) { const fullPath = path.join(tmpDir, relPath); fs.mkdirSync(path.dirname(fullPath), { recursive: true }); fs.writeFileSync(fullPath, content, 'utf-8'); } spawnSync('git', ['init'], { cwd: tmpDir, stdio: 'pipe' }); spawnSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe' }); spawnSync('git', ['commit', '-m', 'initial commit'], { cwd: tmpDir, stdio: 'pipe', env: { ...process.env, GIT_AUTHOR_NAME: 'test', GIT_AUTHOR_EMAIL: 'test@test', GIT_COMMITTER_NAME: 'test', GIT_COMMITTER_EMAIL: 'test@test', }, }); return tmpDir; } /** * Assert standard skill file properties: * 1. CLI exits 0 * 2. .gitnexus/ exists * 3. >= minSkills direct SKILL.md files under .claude/skills/gitnexus-area-/ * 4. YAML frontmatter valid * 5. ## Key Files section present * 6. ## How to Explore section present * 7. Content > 200 chars * * Returns false if skill generation was skipped (native parser crash * or Leiden non-determinism producing 0 communities). Callers can * use this to skip dependent assertions. */ function assertSkillFiles( result: ReturnType, tmpDir: string, minSkills = 1, ): boolean { /* CI timeout tolerance */ if (result.status === null) return false; expect( result.status, [ `analyze --skills exited with code ${result.status}`, `stdout: ${result.stdout?.slice(0, 500)}`, `stderr: ${result.stderr?.slice(0, 500)}`, ].join('\n'), ).toBe(0); expect(fs.existsSync(path.join(tmpDir, '.gitnexus'))).toBe(true); const skillsRoot = path.join(tmpDir, '.claude', 'skills'); if (!fs.existsSync(skillsRoot)) { // Native parser may have crashed in worker or Leiden produced 0 communities. // The pipeline still succeeds (exit 0) but no skills are generated. // Skip skill assertions gracefully — this is platform-dependent. return false; } const skillDirs = fs .readdirSync(skillsRoot) .filter( (d) => d.startsWith('gitnexus-area-') && fs.statSync(path.join(skillsRoot, d)).isDirectory(), ); if (skillDirs.length === 0) { // Native parser may have crashed in worker or Leiden produced 0 communities. // Standard skills still create the shared root, so absence is now detected // by the reserved community-skill prefix rather than by the root directory. return false; } const skillFiles: string[] = []; for (const dir of skillDirs) { const skillPath = path.join(skillsRoot, dir, 'SKILL.md'); if (fs.existsSync(skillPath)) { skillFiles.push(skillPath); } } expect(skillFiles.length).toBeGreaterThanOrEqual(minSkills); for (const skillPath of skillFiles) { const content = fs.readFileSync(skillPath, 'utf-8'); expect(content.startsWith('---')).toBe(true); expect(content).toContain('name:'); expect(content).toContain('description:'); expect(content).toContain('## Key Files'); expect(content).toContain('## How to Explore'); expect(content.length).toBeGreaterThan(200); } return true; } /** * Assert CLAUDE.md and AGENTS.md contain generated skill references. * Automatically detects whether community skills were generated by checking * for the reserved direct-child namespace. */ function assertContextFiles(result: ReturnType, tmpDir: string) { if (result.status === null) return; const skillsRoot = path.join(tmpDir, '.claude', 'skills'); const skillsGenerated = fs.existsSync(skillsRoot) && fs.readdirSync(skillsRoot).some((entry) => entry.startsWith('gitnexus-area-')); const claudePath = path.join(tmpDir, 'CLAUDE.md'); expect(fs.existsSync(claudePath)).toBe(true); if (skillsGenerated) { const claudeContent = fs.readFileSync(claudePath, 'utf-8'); expect(claudeContent).toContain('.claude/skills/gitnexus-area-'); } const agentsPath = path.join(tmpDir, 'AGENTS.md'); expect(fs.existsSync(agentsPath)).toBe(true); if (skillsGenerated) { const agentsContent = fs.readFileSync(agentsPath, 'utf-8'); expect(agentsContent).toContain('.claude/skills/gitnexus-area-'); } } // ============================================================================ // DESCRIBE 1: TypeScript // ============================================================================ describe('TypeScript', () => { let tmpDir: string; let result: ReturnType; beforeAll(() => { tmpDir = createFixtureRepo('typescript', { 'src/api/router.ts': ` import { validateRequest } from '../utils/validator'; import { logRequest } from '../utils/logger'; export function createRouter() { validateRequest('route'); logRequest('router init'); return { routes: [] }; } export function registerRoute(path: string) { validateRequest(path); logRequest('register ' + path); return true; } `, 'src/api/controller.ts': ` import { runQuery } from '../data/query'; import { formatResponse } from '../data/format'; export function handleGet(id: string) { const data = runQuery('SELECT * FROM items WHERE id = ' + id); return formatResponse(data); } export function handlePost(body: any) { const result = runQuery('INSERT INTO items VALUES ' + JSON.stringify(body)); return formatResponse(result); } `, 'src/api/middleware.ts': ` import { validateToken } from '../utils/validator'; import { logRequest } from '../utils/logger'; export function authMiddleware(req: any) { validateToken(req.headers.auth); logRequest('auth check'); return true; } export function corsMiddleware(req: any) { logRequest('cors check'); return { allowed: true }; } `, 'src/data/query.ts': ` import { formatResult } from './format'; import { getCached } from './cache'; export function runQuery(sql: string) { const cached = getCached(sql); if (cached) return cached; return formatResult({ sql, rows: [] }); } export function buildQuery(table: string, conditions: any) { return 'SELECT * FROM ' + table; } `, 'src/data/format.ts': ` export function formatResult(data: any) { return { ...data, formatted: true }; } export function formatResponse(data: any) { return { status: 200, body: formatResult(data) }; } export function serializeResult(data: any) { return JSON.stringify(data); } `, 'src/data/cache.ts': ` import { runQuery } from './query'; const cache = new Map(); export function getCached(key: string) { return cache.get(key) || null; } export function warmCache(keys: string[]) { for (const key of keys) { cache.set(key, runQuery(key)); } } `, 'src/utils/logger.ts': ` export function logRequest(msg: string) { console.log('[REQ]', msg); } export function logError(msg: string) { console.error('[ERR]', msg); } export function createLogEntry(level: string, msg: string) { return { level, msg, ts: Date.now() }; } `, 'src/utils/validator.ts': ` export function validateRequest(input: string) { if (!input || input.length === 0) throw new Error('Invalid'); return true; } export function validateToken(token: string) { if (!token || token.length < 10) throw new Error('Invalid token'); return true; } export function sanitize(input: string) { return input.replace(/[<>]/g, ''); } `, 'src/utils/config.ts': ` export function getConfig(key: string) { return process.env[key] || ''; } export function loadEnv() { return { ...process.env }; } export function parseArgs(args: string[]) { return args.reduce((acc: any, arg) => { const [k, v] = arg.split('='); acc[k] = v; return acc; }, {}); } `, }); result = runSkillsCli(tmpDir); }, 120000); afterAll(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** * Verify analyze --skills generates valid SKILL.md files for a * TypeScript repo with 3 clusters of cross-calling functions. */ it('generates skill files', () => { assertSkillFiles(result, tmpDir); }, 50000); /** * Verify CLAUDE.md and AGENTS.md are created and reference generated skills. */ it('context files updated', () => { assertContextFiles(result, tmpDir); }, 50000); }); // ============================================================================ // DESCRIBE 2: JavaScript // ============================================================================ describe('JavaScript', () => { let tmpDir: string; let result: ReturnType; beforeAll(() => { tmpDir = createFixtureRepo('javascript', { 'src/handlers/userHandler.js': ` const { findById } = require('../services/userService'); const { validateInput } = require('../helpers/validator'); function getUser(id) { validateInput(id); return findById(id); } function createUser(data) { validateInput(data.name); return { id: Date.now(), ...data }; } module.exports = { getUser, createUser }; `, 'src/handlers/authHandler.js': ` const { hashPassword, createToken } = require('../services/authService'); function login(username, password) { const hashed = hashPassword(password); return createToken(username); } function logout(token) { return { success: true }; } module.exports = { login, logout }; `, 'src/handlers/errorHandler.js': ` const { logError } = require('../helpers/logger'); function handleError(err) { logError(err.message); return { error: err.message }; } function formatError(err) { logError('format: ' + err.message); return { code: err.code || 500, message: err.message }; } module.exports = { handleError, formatError }; `, 'src/services/userService.js': ` const { formatUser } = require('./formatService'); function findById(id) { const user = { id, name: 'Test' }; return formatUser(user); } function saveUser(user) { return { ...user, saved: true }; } module.exports = { findById, saveUser }; `, 'src/services/authService.js': ` function hashPassword(password) { return 'hashed_' + password; } function createToken(username) { return 'token_' + username + '_' + Date.now(); } function verifyToken(token) { return token.startsWith('token_'); } module.exports = { hashPassword, createToken, verifyToken }; `, 'src/services/formatService.js': ` function formatUser(user) { return { ...user, displayName: user.name.toUpperCase() }; } function formatDate(date) { return new Date(date).toISOString(); } function formatError(err) { return { error: true, message: String(err) }; } module.exports = { formatUser, formatDate, formatError }; `, 'src/helpers/validator.js': ` function validateInput(input) { if (!input) throw new Error('Required'); return true; } function validateEmail(email) { return /^[^@]+@[^@]+$/.test(email); } function sanitize(str) { return String(str).replace(/[<>]/g, ''); } module.exports = { validateInput, validateEmail, sanitize }; `, 'src/helpers/logger.js': ` function logError(msg) { console.error('[ERROR]', msg); } function logInfo(msg) { console.log('[INFO]', msg); } function createEntry(level, msg) { return { level, msg, ts: Date.now() }; } module.exports = { logError, logInfo, createEntry }; `, }); result = runSkillsCli(tmpDir); }, 120000); afterAll(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** * Verify analyze --skills generates valid SKILL.md files for a * JavaScript repo with handler/service/helper clusters. */ it('generates skill files', () => { assertSkillFiles(result, tmpDir); }, 50000); /** * Verify CLAUDE.md and AGENTS.md are created and reference generated skills. */ it('context files updated', () => { assertContextFiles(result, tmpDir); }, 50000); }); // ============================================================================ // DESCRIBE 3: Python // ============================================================================ describe('Python', () => { let tmpDir: string; let result: ReturnType; beforeAll(() => { tmpDir = createFixtureRepo('python', { 'src/auth/__init__.py': '', 'src/auth/login.py': ` from src.auth.hash import hash_password from src.auth.session import create_session def login(username, password): hashed = hash_password(password) session = create_session(username) return session def validate_credentials(username, password): if not username or not password: raise ValueError("Invalid credentials") return True `, 'src/auth/hash.py': ` def hash_password(password): return "hashed_" + password def compare_hash(plain, hashed): return hash_password(plain) == hashed def generate_salt(): return "salt_" + str(id(object())) `, 'src/auth/session.py': ` from src.auth.login import login def create_session(username): return {"user": username, "token": "sess_" + username} def validate_session(session): return session and "token" in session def refresh_session(session): return create_session(session["user"]) `, 'src/database/__init__.py': '', 'src/database/query.py': ` from src.database.format import format_result from src.database.cache import get_cached def run_query(sql): cached = get_cached(sql) if cached: return cached return format_result({"sql": sql, "rows": []}) def build_query(table, conditions): return f"SELECT * FROM {table}" `, 'src/database/format.py': ` def format_result(data): return {**data, "formatted": True} def serialize_result(data): import json return json.dumps(data) def format_error(err): return {"error": str(err)} `, 'src/database/cache.py': ` from src.database.query import run_query _cache = {} def get_cached(key): return _cache.get(key) def warm_cache(keys): for key in keys: _cache[key] = run_query(key) `, 'src/utils/__init__.py': '', 'src/utils/logger.py': ` def log_info(msg): print(f"[INFO] {msg}") def log_error(msg): print(f"[ERROR] {msg}") def create_entry(level, msg): return {"level": level, "msg": msg} `, 'src/utils/validator.py': ` def validate_input(data): if not data: raise ValueError("Input required") return True def sanitize(text): return text.replace("<", "").replace(">", "") def check_length(text, max_len=255): return len(text) <= max_len `, }); result = runSkillsCli(tmpDir); }, 120000); afterAll(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** * Verify analyze --skills generates valid SKILL.md files for a * Python repo with auth/database/utils clusters. */ it('generates skill files', () => { assertSkillFiles(result, tmpDir); }, 50000); /** * Verify CLAUDE.md and AGENTS.md are created and reference generated skills. */ it('context files updated', () => { assertContextFiles(result, tmpDir); }, 50000); }); // ============================================================================ // DESCRIBE 4: Go // ============================================================================ describe('Go', () => { let tmpDir: string; let result: ReturnType; beforeAll(() => { tmpDir = createFixtureRepo('go', { 'go.mod': `module example.com/testapp go 1.21 `, 'cmd/main.go': `package main import ( "example.com/testapp/pkg/handler" ) func main() { handler.HandleGet("1") handler.HandlePost(map[string]string{"name": "test"}) } `, 'pkg/handler/get.go': `package handler import ( "example.com/testapp/pkg/service" ) func HandleGet(id string) map[string]interface{} { user := service.FindUser(id) return service.FormatResponse(user) } `, 'pkg/handler/post.go': `package handler import ( "example.com/testapp/pkg/service" ) func HandlePost(data map[string]string) map[string]interface{} { service.ValidateInput(data) return service.CreateUser(data) } `, 'pkg/service/user.go': `package service import ( "example.com/testapp/pkg/repository" ) func FindUser(id string) map[string]interface{} { return repository.GetByID(id) } func CreateUser(data map[string]string) map[string]interface{} { repository.Save(data) return map[string]interface{}{"created": true} } `, 'pkg/service/format.go': `package service func FormatResponse(data map[string]interface{}) map[string]interface{} { data["formatted"] = true return data } func ValidateInput(data map[string]string) bool { return len(data) > 0 } func Sanitize(input string) string { return input } `, 'pkg/repository/user_repo.go': `package repository func GetByID(id string) map[string]interface{} { return map[string]interface{}{"id": id, "name": "Test"} } func Save(data map[string]string) bool { return true } func Delete(id string) bool { return true } `, 'pkg/models/user.go': `package models type User struct { ID string Name string } func NewUser(id, name string) *User { return &User{ID: id, Name: name} } func (u *User) Validate() bool { return u.ID != "" && u.Name != "" } `, }); result = runSkillsCli(tmpDir); }, 120000); afterAll(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** * Verify analyze --skills generates valid SKILL.md files for a * Go repo with handler/service/repository clusters. */ it('generates skill files', () => { assertSkillFiles(result, tmpDir); }, 50000); /** * Verify CLAUDE.md and AGENTS.md are created and reference generated skills. */ it('context files updated', () => { assertContextFiles(result, tmpDir); }, 50000); }); // ============================================================================ // DESCRIBE 5: Java // ============================================================================ describe('Java', () => { let tmpDir: string; let result: ReturnType; beforeAll(() => { tmpDir = createFixtureRepo('java', { 'src/service/UserService.java': `package service; import repository.UserRepository; import service.Validator; public class UserService { private UserRepository repository = new UserRepository(); private Validator validator = new Validator(); public Object findUser(String id) { validator.validate(id); return repository.getById(id); } public Object createUser(String name) { validator.validate(name); return repository.save(name); } } `, 'src/service/AuthService.java': `package service; public class AuthService { private UserService userService = new UserService(); public Object authenticate(String username, String password) { Object user = userService.findUser(username); return hashPassword(password); } public String hashPassword(String password) { return "hashed_" + password; } } `, 'src/service/Validator.java': `package service; public class Validator { public boolean validate(String input) { if (input == null || input.isEmpty()) { throw new IllegalArgumentException("Invalid input"); } return true; } public String sanitize(String input) { return input.replaceAll("[<>]", ""); } public boolean checkLength(String input, int max) { return input.length() <= max; } } `, 'src/repository/UserRepository.java': `package repository; public class UserRepository extends BaseRepository { public Object getById(String id) { return new Object(); } public Object save(String name) { return new Object(); } public boolean delete(String id) { return true; } } `, 'src/repository/BaseRepository.java': `package repository; public abstract class BaseRepository { public Object[] findAll() { return new Object[0]; } public int count() { return 0; } } `, 'src/model/User.java': `package model; public class User { private String name; public User(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } } `, }); result = runSkillsCli(tmpDir); }, 120000); afterAll(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** * Verify analyze --skills generates valid SKILL.md files for a * Java repo with service/repository/model clusters. */ it('generates skill files', () => { assertSkillFiles(result, tmpDir); }, 50000); /** * Verify CLAUDE.md and AGENTS.md are created and reference generated skills. */ it('context files updated', () => { assertContextFiles(result, tmpDir); }, 50000); }); // ============================================================================ // DESCRIBE 6: Rust // ============================================================================ describe('Rust', () => { let tmpDir: string; let result: ReturnType; beforeAll(() => { tmpDir = createFixtureRepo('rust', { 'Cargo.toml': `[package] name = "testapp" version = "0.1.0" edition = "2021" `, 'src/main.rs': `mod auth; mod data; fn main() { let session = auth::login::login("user", "pass"); let result = data::query::run_query("SELECT 1"); println!("{:?} {:?}", session, result); } `, 'src/auth/mod.rs': `pub mod login; pub mod hash; `, 'src/auth/login.rs': `use crate::auth::hash::hash_password; pub fn login(username: &str, password: &str) -> String { let hashed = hash_password(password); format!("session_{}_{}", username, hashed) } pub fn validate(token: &str) -> bool { token.starts_with("session_") } `, 'src/auth/hash.rs': `pub fn hash_password(password: &str) -> String { format!("hashed_{}", password) } pub fn compare_hash(plain: &str, hashed: &str) -> bool { hash_password(plain) == hashed } pub fn generate_salt() -> String { String::from("random_salt") } `, 'src/data/mod.rs': `pub mod query; pub mod format; `, 'src/data/query.rs': `use crate::data::format::format_result; pub fn run_query(sql: &str) -> String { let raw = format!("result_{}", sql); format_result(&raw) } pub fn build_query(table: &str) -> String { format!("SELECT * FROM {}", table) } `, 'src/data/format.rs': `pub fn format_result(data: &str) -> String { format!("[formatted] {}", data) } pub fn serialize(data: &str) -> String { format!("{{\"data\": \"{}\"}}", data) } pub fn format_error(err: &str) -> String { format!("[ERROR] {}", err) } `, }); result = runSkillsCli(tmpDir); }, 120000); afterAll(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** * Verify analyze --skills generates valid SKILL.md files for a * Rust repo with auth/data module clusters. */ it('generates skill files', () => { assertSkillFiles(result, tmpDir); }, 50000); /** * Verify CLAUDE.md and AGENTS.md are created and reference generated skills. */ it('context files updated', () => { assertContextFiles(result, tmpDir); }, 50000); }); // ============================================================================ // DESCRIBE 7: C# // ============================================================================ describe('CSharp', () => { let tmpDir: string; let result: ReturnType; beforeAll(() => { tmpDir = createFixtureRepo('csharp', { 'Services/UserService.cs': `using System; namespace Services { public class UserService { public object FindUser(string id) { return id; } public object CreateUser(string name) { return name; } public object UpdateUser(string id, string name) { return name; } public bool RemoveUser(string id) { return true; } } public class UserValidator { public bool ValidateUser(string input) { return true; } public string SanitizeUser(string input) { return input; } public bool CheckUserLength(string input) { return true; } } } `, 'Services/AuthService.cs': `using System; namespace Services { public class AuthService { public object Authenticate(string username, string password) { return username; } public string HashPassword(string password) { return password; } public bool VerifyPassword(string hashed) { return true; } public string CreateToken(string username) { return username; } } public class TokenManager { public string GenerateToken(string user) { return user; } public bool ValidateToken(string token) { return true; } public string RefreshToken(string token) { return token; } } } `, 'Services/OrderService.cs': `using System; namespace Services { public class OrderService { public object CreateOrder(string item) { return item; } public object GetOrder(string id) { return id; } public bool CancelOrder(string id) { return true; } public object UpdateOrder(string id, string item) { return item; } } public class OrderValidator { public bool ValidateOrder(string input) { return true; } public string SanitizeOrder(string input) { return input; } } } `, 'Services/EmailService.cs': `using System; namespace Services { public class EmailService { public void SendMail(string to, string body) { } public void SendBulk(string to, string body) { } public string FormatBody(string body) { return body; } public bool ValidateAddress(string addr) { return true; } } } `, 'Data/UserRepo.cs': `using System; namespace Data { public class UserRepo { public object GetById(string id) { return id; } public object Save(string name) { return name; } public object Update(string id, string name) { return name; } public bool Delete(string id) { return true; } public object[] ListAll() { return new object[0]; } } } `, 'Data/OrderRepo.cs': `using System; namespace Data { public class OrderRepo { public object FindOrder(string id) { return id; } public object InsertOrder(string item) { return item; } public bool RemoveOrder(string id) { return true; } public object UpdateOrder(string id, string data) { return data; } public int CountOrders() { return 0; } } } `, 'Data/CacheManager.cs': `using System; namespace Data { public class CacheManager { public object GetCached(string key) { return key; } public void SetCached(string key, object val) { } public void Invalidate(string key) { } public void Clear() { } } public class CacheStats { public int GetHitCount() { return 0; } public int GetMissCount() { return 0; } public double GetHitRate() { return 0.0; } } } `, 'Data/Logger.cs': `using System; namespace Data { public class Logger { public void Info(string msg) { } public void Error(string msg) { } public void Warn(string msg) { } public void Debug(string msg) { } } public class LogFormatter { public string FormatEntry(string level, string msg) { return level + msg; } public string FormatTimestamp() { return ""; } public string FormatStackTrace(string trace) { return trace; } } } `, }); result = runSkillsCli(tmpDir); }, 120000); afterAll(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** * Verify analyze --skills generates valid SKILL.md files for a * C# repo with Services/Data clusters. * * Note: tree-sitter-c-sharp's native N-API addon can crash in forked * workers on some platforms (libc++abi exception). When this happens, * the pipeline falls through with 0 communities and no skills are * generated. assertSkillFiles handles this gracefully. */ it('generates skill files', () => { assertSkillFiles(result, tmpDir); }, 50000); /** * Verify CLAUDE.md and AGENTS.md are created and reference generated skills. */ it('context files updated', () => { assertContextFiles(result, tmpDir); }, 50000); }); // ============================================================================ // DESCRIBE 8: C++ // ============================================================================ describe('CPlusPlus', () => { let tmpDir: string; let result: ReturnType; beforeAll(() => { tmpDir = createFixtureRepo('cpp', { 'src/engine/engine.h': `#ifndef ENGINE_H #define ENGINE_H class Engine { public: void start(); void stop(); }; #endif `, 'src/engine/engine.cpp': `#include "engine.h" #include "../utils/logger.h" #include "../utils/config.h" void Engine::start() { Logger logger; logger.log("Engine starting"); Config config; config.get("engine.mode"); } void Engine::stop() { Logger logger; logger.log("Engine stopping"); } `, 'src/engine/renderer.h': `#ifndef RENDERER_H #define RENDERER_H class Renderer { public: void render(); void clear(); }; #endif `, 'src/engine/renderer.cpp': `#include "renderer.h" #include "engine.h" void Renderer::render() { Engine engine; engine.start(); } void Renderer::clear() { } `, 'src/engine/physics.h': `#ifndef PHYSICS_H #define PHYSICS_H void simulate(); void collide(); #endif `, 'src/engine/physics.cpp': `#include "physics.h" #include "engine.h" #include "../utils/logger.h" void simulate() { Engine engine; engine.stop(); Logger logger; logger.log("simulating"); } void collide() { Logger logger; logger.log("collision detected"); } `, 'src/utils/logger.h': `#ifndef LOGGER_H #define LOGGER_H #include class Logger { public: void log(const std::string& msg); void error(const std::string& msg); void flush(); }; #endif `, 'src/utils/logger.cpp': `#include "logger.h" #include void Logger::log(const std::string& msg) { std::cout << "[LOG] " << msg << std::endl; } void Logger::error(const std::string& msg) { std::cerr << "[ERR] " << msg << std::endl; } void Logger::flush() { std::cout.flush(); } `, 'src/utils/config.h': `#ifndef CONFIG_H #define CONFIG_H #include class Config { public: std::string get(const std::string& key); void set(const std::string& key, const std::string& value); void load(const std::string& path); }; #endif `, 'src/utils/config.cpp': `#include "config.h" std::string Config::get(const std::string& key) { return ""; } void Config::set(const std::string& key, const std::string& value) { } void Config::load(const std::string& path) { } `, 'src/utils/math.h': `#ifndef MATH_H #define MATH_H int clamp(int value, int min, int max); float lerp(float a, float b, float t); double distance(double x1, double y1, double x2, double y2); #endif `, 'src/utils/math.cpp': `#include "math.h" #include int clamp(int value, int min, int max) { if (value < min) return min; if (value > max) return max; return value; } float lerp(float a, float b, float t) { return a + (b - a) * t; } double distance(double x1, double y1, double x2, double y2) { return std::sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)); } `, }); result = runSkillsCli(tmpDir); }, 120000); afterAll(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** * Verify analyze --skills generates valid SKILL.md files for a * C++ repo with engine/utils clusters including headers. */ it('generates skill files', () => { assertSkillFiles(result, tmpDir); }, 50000); /** * Verify CLAUDE.md and AGENTS.md are created and reference generated skills. */ it('context files updated', () => { assertContextFiles(result, tmpDir); }, 50000); }); // ============================================================================ // DESCRIBE 9: C // ============================================================================ describe('C', () => { let tmpDir: string; let result: ReturnType; beforeAll(() => { tmpDir = createFixtureRepo('c', { 'src/core/parser.h': `#ifndef PARSER_H #define PARSER_H void parse(const char* input); void tokenize(const char* input); #endif `, 'src/core/parser.c': `#include "parser.h" #include "../io/reader.h" #include "../io/logger.h" void parse(const char* input) { char* data = read_file(input); log_msg("parsing"); tokenize(data); } void tokenize(const char* input) { log_msg("tokenizing"); } `, 'src/core/lexer.h': `#ifndef LEXER_H #define LEXER_H typedef struct { int type; const char* value; } Token; void lex(const char* input); Token next_token(const char* input); int is_keyword(const char* word); #endif `, 'src/core/lexer.c': `#include "lexer.h" #include "parser.h" #include void lex(const char* input) { parse(input); } Token next_token(const char* input) { Token t; t.type = 0; t.value = input; return t; } int is_keyword(const char* word) { return strcmp(word, "if") == 0 || strcmp(word, "else") == 0; } `, 'src/core/ast.h': `#ifndef AST_H #define AST_H typedef struct ASTNode { int type; struct ASTNode* left; struct ASTNode* right; } ASTNode; ASTNode* create_node(int type); void free_node(ASTNode* node); #endif `, 'src/core/ast.c': `#include "ast.h" #include "lexer.h" #include ASTNode* create_node(int type) { ASTNode* node = (ASTNode*)malloc(sizeof(ASTNode)); node->type = type; node->left = NULL; node->right = NULL; tokenize("ast"); return node; } void free_node(ASTNode* node) { if (node) { free_node(node->left); free_node(node->right); free(node); } } `, 'src/io/reader.h': `#ifndef READER_H #define READER_H char* read_file(const char* path); void close_file(const char* path); int file_exists(const char* path); #endif `, 'src/io/reader.c': `#include "reader.h" #include #include char* read_file(const char* path) { return "file contents"; } void close_file(const char* path) { } int file_exists(const char* path) { FILE* f = fopen(path, "r"); if (f) { fclose(f); return 1; } return 0; } `, 'src/io/writer.h': `#ifndef WRITER_H #define WRITER_H void write_file(const char* path, const char* data); void flush_writer(void); #endif `, 'src/io/writer.c': `#include "writer.h" #include "logger.h" void write_file(const char* path, const char* data) { log_msg("writing file"); } void flush_writer(void) { log_msg("flushing"); } `, 'src/io/logger.h': `#ifndef LOGGER_H #define LOGGER_H void log_msg(const char* msg); void log_error(const char* msg); void log_init(void); #endif `, 'src/io/logger.c': `#include "logger.h" #include void log_msg(const char* msg) { printf("[LOG] %s\\n", msg); } void log_error(const char* msg) { fprintf(stderr, "[ERR] %s\\n", msg); } void log_init(void) { log_msg("logger initialized"); } `, }); result = runSkillsCli(tmpDir); }, 120000); afterAll(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** * Verify analyze --skills generates valid SKILL.md files for a * C repo with core/io clusters including headers. */ it('generates skill files', () => { assertSkillFiles(result, tmpDir); }, 50000); /** * Verify CLAUDE.md and AGENTS.md are created and reference generated skills. */ it('context files updated', () => { assertContextFiles(result, tmpDir); }, 50000); }); // ============================================================================ // DESCRIBE 10: PHP // ============================================================================ describe('PHP', () => { let tmpDir: string; let result: ReturnType; beforeAll(() => { tmpDir = createFixtureRepo('php', { 'src/Controllers/UserController.php': ` $method, 'path' => $path]); } function api_handle_error($error) { log_error($error); return format_error($error); } function api_middleware($request) { validate_input($request); log_request('middleware'); return true; } `, 'src/Services/UserService.php': ` $level, 'msg' => $msg, 'ts' => time()]; } `, 'src/Helpers/formatter.php': ` 200, 'body' => $data, 'formatted' => true]; } function format_error($err) { return ['status' => 500, 'error' => $err]; } function format_date($timestamp) { return date('Y-m-d', $timestamp); } function format_json($data) { return json_encode($data); } `, 'src/Data/database.php': ` { fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** * Verify analyze --skills generates valid SKILL.md files for a * PHP repo with Controllers/Services/Models clusters. */ it('generates skill files', () => { assertSkillFiles(result, tmpDir); }, 50000); /** * Verify CLAUDE.md and AGENTS.md are created and reference generated skills. */ it('context files updated', () => { assertContextFiles(result, tmpDir); }, 50000); }); // ============================================================================ // DESCRIBE 11: Kotlin // ============================================================================ describe('Kotlin', () => { let tmpDir: string; let result: ReturnType; beforeAll(() => { tmpDir = createFixtureRepo('kotlin', { 'src/main/kotlin/service/UserService.kt': `package service fun findUser(id: String): Map { validateInput(id) val result = dbQuery("SELECT * FROM users WHERE id = $id") return formatResponse(result) } fun createUser(name: String): Map { validateInput(name) sanitizeInput(name) dbExecute("INSERT INTO users VALUES ('$name')") logRequest("user created") return formatResponse(mapOf("name" to name)) } fun updateUser(id: String, name: String): Map { validateInput(id) validateInput(name) dbExecute("UPDATE users SET name = '$name' WHERE id = $id") logRequest("user updated") return formatResponse(mapOf("id" to id)) } fun deleteUser(id: String): Boolean { validateInput(id) dbExecute("DELETE FROM users WHERE id = $id") logRequest("user deleted") return true } `, 'src/main/kotlin/service/AuthService.kt': `package service fun authenticate(username: String, password: String): Map { validateInput(username) validateInput(password) val user = findUser(username) val hash = hashPassword(password) return formatResponse(mapOf("user" to user, "token" to createToken(username))) } fun hashPassword(password: String): String { validateInput(password) return "hashed_$password" } fun createToken(username: String): String { validateInput(username) logRequest("token created for $username") return "token_$username" } fun verifyToken(token: String): Boolean { validateInput(token) return token.startsWith("token_") } fun refreshToken(token: String): String { verifyToken(token) return createToken("refreshed") } `, 'src/main/kotlin/service/NotificationService.kt': `package service fun notify(userId: String, message: String) { validateInput(userId) validateInput(message) sendEmail(userId, message) } fun sendEmail(to: String, body: String) { sanitizeInput(body) logRequest("email sent to $to") formatMessage(body) } fun sendAlert(message: String) { logRequest("alert: $message") formatError(message) } `, 'src/main/kotlin/helpers/Validator.kt': `package helpers fun validateInput(input: String): Boolean { if (input.isEmpty()) throw IllegalArgumentException("Invalid") return true } fun sanitizeInput(input: String): String { return input.replace("<", "").replace(">", "") } fun checkLength(input: String, max: Int = 255): Boolean { return input.length <= max } fun normalizeInput(input: String): String { return input.trim().lowercase() } `, 'src/main/kotlin/helpers/Logger.kt': `package helpers fun logRequest(msg: String) { println("[REQ] $msg") } fun logError(msg: String) { System.err.println("[ERR] $msg") } fun logInfo(msg: String) { println("[INFO] $msg") } fun createLogEntry(level: String, msg: String): Map { return mapOf("level" to level, "msg" to msg, "ts" to System.currentTimeMillis()) } `, 'src/main/kotlin/helpers/Formatter.kt': `package helpers fun formatResponse(data: Map): Map { return data + mapOf("formatted" to true, "status" to 200) } fun formatError(err: String): Map { return mapOf("status" to 500, "error" to err) } fun formatMessage(msg: String): String { return "[MSG] $msg" } fun formatDate(timestamp: Long): String { return timestamp.toString() } `, 'src/main/kotlin/data/Database.kt': `package data fun dbQuery(sql: String): Map { logRequest("query: $sql") return mapOf("rows" to emptyList()) } fun dbExecute(sql: String): Boolean { logRequest("execute: $sql") return true } fun dbConnect(url: String): Boolean { return true } fun dbClose() { } `, }); result = runSkillsCli(tmpDir); }, 120000); afterAll(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** * Verify analyze --skills generates valid SKILL.md files for a * Kotlin repo with service/repository clusters. */ it('generates skill files', () => { assertSkillFiles(result, tmpDir); }, 50000); /** * Verify CLAUDE.md and AGENTS.md are created and reference generated skills. */ it('context files updated', () => { assertContextFiles(result, tmpDir); }, 50000); }); // ============================================================================ // DESCRIBE 12: Mixed TypeScript + Python // ============================================================================ describe('Mixed TypeScript + Python', () => { let tmpDir: string; let result: ReturnType; beforeAll(() => { tmpDir = createFixtureRepo('mixed', { 'packages/backend/src/api/router.ts': ` import { validateRequest } from '../utils/validator'; import { logRequest } from '../utils/logger'; export function createRouter() { validateRequest('route'); logRequest('router init'); return { routes: [] }; } export function registerRoute(path: string) { validateRequest(path); logRequest('register ' + path); return true; } `, 'packages/backend/src/api/controller.ts': ` import { runQuery } from '../data/query'; export function handleGet(id: string) { return runQuery('SELECT * FROM items WHERE id = ' + id); } export function handlePost(body: any) { return runQuery('INSERT INTO items VALUES ' + JSON.stringify(body)); } `, 'packages/backend/src/data/query.ts': ` export function runQuery(sql: string) { return { sql, rows: [] }; } export function buildQuery(table: string) { return 'SELECT * FROM ' + table; } `, 'packages/backend/src/utils/validator.ts': ` export function validateRequest(input: string) { if (!input) throw new Error('Invalid'); return true; } export function sanitize(input: string) { return input.replace(/[<>]/g, ''); } `, 'packages/backend/src/utils/logger.ts': ` export function logRequest(msg: string) { console.log('[REQ]', msg); } export function logError(msg: string) { console.error('[ERR]', msg); } `, 'packages/ml/src/pipeline/__init__.py': '', 'packages/ml/src/pipeline/train.py': ` from packages.ml.src.data.loader import load_data, preprocess def train(config): data = load_data("train.csv") processed = preprocess(data) return {"model": "trained", "data": processed} def evaluate(model, test_data): data = load_data("test.csv") return {"accuracy": 0.95} `, 'packages/ml/src/pipeline/predict.py': ` from packages.ml.src.models.model import load_model def predict(input_data): model = load_model("latest") return {"prediction": "result"} def batch_predict(inputs): model = load_model("latest") return [{"prediction": "result"} for _ in inputs] `, 'packages/ml/src/data/__init__.py': '', 'packages/ml/src/data/loader.py': ` def load_data(path): return {"path": path, "rows": []} def preprocess(data): return {**data, "preprocessed": True} def split_data(data, ratio=0.8): return data, data `, 'packages/ml/src/models/__init__.py': '', 'packages/ml/src/models/model.py': ` def load_model(name): return {"name": name, "loaded": True} def save_model(model, path): return True def compile_model(config): return {"compiled": True} `, }); result = runSkillsCli(tmpDir); }, 120000); afterAll(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** * Verify analyze --skills generates at least 1 SKILL.md for a * mixed TypeScript + Python monorepo. Relaxed assertion since Leiden * may or may not form communities spanning both languages. */ it('generates skill files', () => { assertSkillFiles(result, tmpDir, 1); }, 50000); /** * Verify CLAUDE.md and AGENTS.md are created and reference generated skills. */ it('context files updated', () => { assertContextFiles(result, tmpDir); }, 50000); }); // ============================================================================ // DESCRIBE 13: Idempotency // ============================================================================ describe('Idempotency', () => { let tmpDir: string; let result1: ReturnType; let result2: ReturnType; beforeAll(() => { tmpDir = createFixtureRepo('idempotency', { 'src/core/parser.ts': ` import { readFile } from '../io/reader'; import { log } from '../io/logger'; export function parse(input: string) { const data = readFile(input); log('parsing'); return tokenize(data); } export function tokenize(data: string) { log('tokenizing'); return data.split(' '); } `, 'src/core/transformer.ts': ` import { parse } from './parser'; import { validate } from './validator'; export function transform(input: string) { validate(input); const tokens = parse(input); return tokens.map(t => t.toUpperCase()); } export function optimize(input: string) { const tokens = parse(input); return tokens.filter(t => t.length > 0); } `, 'src/core/validator.ts': ` export function validate(input: string) { if (!input) throw new Error('Invalid'); return true; } export function checkSchema(schema: any) { return schema && typeof schema === 'object'; } export function sanitize(input: string) { return input.replace(/[<>]/g, ''); } `, 'src/io/reader.ts': ` export function readFile(path: string) { return 'file contents from ' + path; } export function readStream(path: string) { return { path, stream: true }; } export function close(handle: any) { return true; } `, 'src/io/writer.ts': ` import { log } from './logger'; export function writeFile(path: string, data: string) { log('writing ' + path); return true; } export function flush() { log('flushing'); return true; } `, 'src/io/logger.ts': ` export function log(msg: string) { console.log('[LOG]', msg); } export function logError(msg: string) { console.error('[ERR]', msg); } export function createEntry(level: string, msg: string) { return { level, msg, ts: Date.now() }; } `, }); result1 = runSkillsCli(tmpDir); result2 = runSkillsCli(tmpDir); // 120s to match the other describe hooks in this file. This hook runs // runSkillsCli TWICE, each capped at 45s, so a 90s budget has no headroom // over two worst-case analyzes plus fixture setup and git init — it times // out the *hook* on slow Windows runners (the test below already tolerates // an individual analyze hitting its own 45s timeout via status === null, // but a hook timeout fails before that tolerance can apply). }, 120000); afterAll(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); /** * Running analyze --skills twice should produce stable output: * same number of skill directories, all SKILL.md files valid, * and CLAUDE.md still references generated skills. */ it('second analyze --skills produces stable output', () => { /* CI timeout tolerance */ if (result1.status === null || result2.status === null) return; expect( result1.status, [ `first analyze --skills exited with code ${result1.status}`, `stdout: ${result1.stdout?.slice(0, 500)}`, `stderr: ${result1.stderr?.slice(0, 500)}`, ].join('\n'), ).toBe(0); expect( result2.status, [ `second analyze --skills exited with code ${result2.status}`, `stdout: ${result2.stdout?.slice(0, 500)}`, `stderr: ${result2.stderr?.slice(0, 500)}`, ].join('\n'), ).toBe(0); const skillsRoot = path.join(tmpDir, '.claude', 'skills'); expect(fs.existsSync(skillsRoot)).toBe(true); const skillDirs = fs .readdirSync(skillsRoot) .filter( (d) => d.startsWith('gitnexus-area-') && fs.statSync(path.join(skillsRoot, d)).isDirectory(), ); expect(skillDirs.length).toBeGreaterThanOrEqual(1); /* All SKILL.md files should still have valid frontmatter */ for (const dir of skillDirs) { const skillPath = path.join(skillsRoot, dir, 'SKILL.md'); expect(fs.existsSync(skillPath)).toBe(true); const content = fs.readFileSync(skillPath, 'utf-8'); expect(content.startsWith('---')).toBe(true); expect(content).toContain('name:'); expect(content).toContain('description:'); expect(content.length).toBeGreaterThan(200); } /* CLAUDE.md should still reference generated skills */ const claudePath = path.join(tmpDir, 'CLAUDE.md'); expect(fs.existsSync(claudePath)).toBe(true); const claudeContent = fs.readFileSync(claudePath, 'utf-8'); expect(claudeContent).toContain('.claude/skills/gitnexus-area-'); }, 90000); });