UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

611 lines (525 loc) 22.5 kB
/** * Unit Tests: Doctor Check Modules * Story INS-4.1: aios doctor rewrite * Story INS-4.8: 3 new checks (skills-count, commands-count, hooks-claude-count) * * Tests all 15 check modules individually with mocked filesystem. */ const path = require('path'); const fs = require('fs'); // Mock fs for controlled test scenarios jest.mock('fs'); const nodeVersionCheck = require('../../../../../.aios-core/core/doctor/checks/node-version'); const npmPackagesCheck = require('../../../../../.aios-core/core/doctor/checks/npm-packages'); const settingsJsonCheck = require('../../../../../.aios-core/core/doctor/checks/settings-json'); const rulesFilesCheck = require('../../../../../.aios-core/core/doctor/checks/rules-files'); const agentMemoryCheck = require('../../../../../.aios-core/core/doctor/checks/agent-memory'); const entityRegistryCheck = require('../../../../../.aios-core/core/doctor/checks/entity-registry'); const gitHooksCheck = require('../../../../../.aios-core/core/doctor/checks/git-hooks'); const coreConfigCheck = require('../../../../../.aios-core/core/doctor/checks/core-config'); const claudeMdCheck = require('../../../../../.aios-core/core/doctor/checks/claude-md'); const graphDashboardCheck = require('../../../../../.aios-core/core/doctor/checks/graph-dashboard'); const codeIntelCheck = require('../../../../../.aios-core/core/doctor/checks/code-intel'); const ideSyncCheck = require('../../../../../.aios-core/core/doctor/checks/ide-sync'); const skillsCountCheck = require('../../../../../.aios-core/core/doctor/checks/skills-count'); const commandsCountCheck = require('../../../../../.aios-core/core/doctor/checks/commands-count'); const hooksClaudeCountCheck = require('../../../../../.aios-core/core/doctor/checks/hooks-claude-count'); const { loadChecks } = require('../../../../../.aios-core/core/doctor/checks'); const mockContext = { projectRoot: '/mock/project', frameworkRoot: '/mock/framework', options: { fix: false, json: false, dryRun: false, quiet: false }, }; beforeEach(() => { jest.clearAllMocks(); }); describe('node-version check', () => { it('should PASS for current Node.js version (>=18)', async () => { const result = await nodeVersionCheck.run(mockContext); expect(result.check).toBe('node-version'); expect(result.status).toBe('PASS'); expect(result.message).toContain('Node.js'); }); }); describe('npm-packages check', () => { it('should PASS when node_modules exists', async () => { fs.existsSync.mockReturnValue(true); const result = await npmPackagesCheck.run(mockContext); expect(result.status).toBe('PASS'); expect(result.message).toContain('node_modules present'); }); it('should FAIL when node_modules missing', async () => { fs.existsSync.mockReturnValue(false); const result = await npmPackagesCheck.run(mockContext); expect(result.status).toBe('FAIL'); expect(result.fixCommand).toBe('npm install'); }); }); describe('settings-json check', () => { it('should PASS with valid settings and sufficient deny rules', async () => { fs.existsSync.mockReturnValue(true); const mockSettings = { permissions: { deny: new Array(50).fill('Edit(.aios-core/core/)'), allow: ['Edit(docs/)'], }, }; const coreConfig = 'boundary:\n protected:\n - .aios-core/core/**\n exceptions:\n - agents/MEMORY.md'; fs.readFileSync.mockImplementation((p) => { if (p.includes('settings.json')) return JSON.stringify(mockSettings); if (p.includes('core-config')) return coreConfig; return ''; }); const result = await settingsJsonCheck.run(mockContext); expect(result.status).toBe('PASS'); expect(result.message).toContain('50 rules'); }); it('should FAIL when settings.json not found', async () => { fs.existsSync.mockReturnValue(false); const result = await settingsJsonCheck.run(mockContext); expect(result.status).toBe('FAIL'); }); it('should WARN when deny rules below threshold', async () => { fs.existsSync.mockReturnValue(true); const mockSettings = { permissions: { deny: ['one'], allow: [] } }; fs.readFileSync.mockReturnValue(JSON.stringify(mockSettings)); const result = await settingsJsonCheck.run(mockContext); expect(result.status).toBe('WARN'); }); it('should WARN when boundary paths not covered by deny rules', async () => { fs.existsSync.mockReturnValue(true); const mockSettings = { permissions: { deny: new Array(50).fill('Edit(docs/)'), allow: [], }, }; const coreConfig = 'boundary:\n protected:\n - .aios-core/core/**\n - bin/aios.js\n exceptions:\n - test'; fs.readFileSync.mockImplementation((p) => { if (p.includes('settings.json')) return JSON.stringify(mockSettings); if (p.includes('core-config')) return coreConfig; return ''; }); const result = await settingsJsonCheck.run(mockContext); expect(result.status).toBe('WARN'); expect(result.message).toContain('boundary coverage'); }); }); describe('rules-files check', () => { it('should PASS when all 7 rules files exist', async () => { fs.existsSync.mockReturnValue(true); const result = await rulesFilesCheck.run(mockContext); expect(result.status).toBe('PASS'); expect(result.message).toContain('7'); }); it('should FAIL when rules directory missing', async () => { fs.existsSync.mockReturnValue(false); const result = await rulesFilesCheck.run(mockContext); expect(result.status).toBe('FAIL'); }); it('should WARN when some rules missing', async () => { fs.existsSync.mockImplementation((p) => { // Directory exists if (p.endsWith('rules')) return true; // Most files exist except 2 if (p.includes('agent-authority') || p.includes('workflow-execution')) return false; return true; }); const result = await rulesFilesCheck.run(mockContext); expect(result.status).toBe('WARN'); expect(result.message).toContain('Missing 2'); }); }); describe('agent-memory check', () => { it('should PASS when all 10 MEMORY.md files exist', async () => { fs.existsSync.mockReturnValue(true); const result = await agentMemoryCheck.run(mockContext); expect(result.status).toBe('PASS'); expect(result.message).toContain('10/10'); }); it('should WARN when some MEMORY.md files missing', async () => { fs.existsSync.mockImplementation((p) => { if (p.endsWith('agents')) return true; if (p.includes('analyst') || p.includes('ux')) return false; return true; }); const result = await agentMemoryCheck.run(mockContext); expect(result.status).toBe('WARN'); expect(result.message).toContain('8/10'); }); }); describe('entity-registry check', () => { it('should PASS when registry exists and is fresh', async () => { fs.existsSync.mockReturnValue(true); fs.statSync.mockReturnValue({ mtimeMs: Date.now() - 1000 }); fs.readFileSync.mockReturnValue('line1\nline2\nline3'); const result = await entityRegistryCheck.run(mockContext); expect(result.status).toBe('PASS'); }); it('should FAIL when registry not found', async () => { fs.existsSync.mockReturnValue(false); const result = await entityRegistryCheck.run(mockContext); expect(result.status).toBe('FAIL'); }); it('should WARN when registry is stale (>48h)', async () => { fs.existsSync.mockReturnValue(true); fs.statSync.mockReturnValue({ mtimeMs: Date.now() - 72 * 60 * 60 * 1000 }); fs.readFileSync.mockReturnValue('line1\nline2'); const result = await entityRegistryCheck.run(mockContext); expect(result.status).toBe('WARN'); expect(result.message).toContain('72h'); }); }); describe('git-hooks check', () => { it('should PASS when both hooks exist', async () => { fs.existsSync.mockReturnValue(true); const result = await gitHooksCheck.run(mockContext); expect(result.status).toBe('PASS'); }); it('should WARN when .husky directory missing', async () => { fs.existsSync.mockReturnValue(false); const result = await gitHooksCheck.run(mockContext); expect(result.status).toBe('WARN'); }); }); describe('core-config check', () => { it('should PASS when config has all required sections', async () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockReturnValue('boundary:\n test: true\nproject:\n name: test\nide:\n sync: true'); const result = await coreConfigCheck.run(mockContext); expect(result.status).toBe('PASS'); }); it('should FAIL when config missing', async () => { fs.existsSync.mockReturnValue(false); const result = await coreConfigCheck.run(mockContext); expect(result.status).toBe('FAIL'); }); it('should FAIL when missing required sections', async () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockReturnValue('project:\n name: test'); const result = await coreConfigCheck.run(mockContext); expect(result.status).toBe('FAIL'); expect(result.message).toContain('boundary'); }); }); describe('claude-md check', () => { it('should PASS when all sections present', async () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockReturnValue( '## Constitution\n## Framework vs Project Boundary\n## Sistema de Agentes', ); const result = await claudeMdCheck.run(mockContext); expect(result.status).toBe('PASS'); }); it('should WARN when sections missing', async () => { fs.existsSync.mockReturnValue(true); fs.readFileSync.mockReturnValue('## Constitution\nSome content'); const result = await claudeMdCheck.run(mockContext); expect(result.status).toBe('WARN'); expect(result.message).toContain('Missing sections'); }); }); describe('graph-dashboard check', () => { it('should PASS when directory has .js files', async () => { fs.existsSync.mockReturnValue(true); fs.readdirSync.mockReturnValue(['index.js', 'cli.js']); const result = await graphDashboardCheck.run(mockContext); expect(result.status).toBe('PASS'); }); it('should WARN when directory missing', async () => { fs.existsSync.mockReturnValue(false); const result = await graphDashboardCheck.run(mockContext); expect(result.status).toBe('WARN'); }); }); describe('code-intel check', () => { // The code-intel check does a real require() of index.js and triggers // provider auto-detection. Tests that need provider detection must use // the real projectRoot and real fs (jest.requireActual). const realFs = jest.requireActual('fs'); const realProjectRoot = path.join(__dirname, '..', '..', '..', '..', '..'); it('should return INFO when code-intel dir does not exist', async () => { fs.existsSync.mockReturnValue(false); const result = await codeIntelCheck.run(mockContext); expect(result.status).toBe('INFO'); }); it('should WARN when index.js missing but dir exists', async () => { fs.existsSync.mockImplementation((p) => { // Dir exists, but index.js does not if (p.includes('index.js')) return false; return true; }); const result = await codeIntelCheck.run(mockContext); expect(result.status).toBe('WARN'); expect(result.message).toContain('index.js not found'); }); it('should PASS with RegistryProvider when entity-registry exists', async () => { // Use real fs + real project root so require() resolves the actual module // and RegistryProvider can load the real entity-registry.yaml const realContext = { ...mockContext, projectRoot: realProjectRoot, }; // Temporarily restore real fs for this test fs.existsSync.mockImplementation(realFs.existsSync); fs.readFileSync.mockImplementation(realFs.readFileSync); fs.statSync.mockImplementation(realFs.statSync); // Only run if entity-registry actually exists (skip in CI without registry) const registryPath = path.join(realProjectRoot, '.aios-core', 'data', 'entity-registry.yaml'); if (!realFs.existsSync(registryPath)) { return; // skip — no registry available } const result = await codeIntelCheck.run(realContext); expect(result.status).toBe('PASS'); expect(result.message).toContain('RegistryProvider'); }); it('should WARN when provider detection fails (no valid registry)', async () => { // Use real project root so require() works, but mock registry as empty const realContext = { ...mockContext, projectRoot: realProjectRoot, }; // Real existsSync for module resolution, mocked readFileSync for empty registry fs.existsSync.mockImplementation((p) => { if (p.includes('entity-registry.yaml')) return true; return realFs.existsSync(p); }); fs.statSync.mockImplementation((p) => { if (p.includes('entity-registry.yaml')) return { mtimeMs: Date.now(), size: 10 }; return realFs.statSync(p); }); fs.readFileSync.mockImplementation((p, enc) => { if (typeof p === 'string' && p.includes('entity-registry.yaml')) { return 'metadata:\n entityCount: 0'; } return realFs.readFileSync(p, enc); }); const result = await codeIntelCheck.run(realContext); // Without valid entities, provider won't be available → WARN or INFO expect(['WARN', 'INFO']).toContain(result.status); }); }); describe('ide-sync check', () => { it('should PASS when counts match', async () => { fs.existsSync.mockReturnValue(true); fs.readdirSync.mockImplementation((p) => { if (p.includes('commands')) return ['dev.md', 'qa.md']; return ['dev.md', 'qa.md']; }); const result = await ideSyncCheck.run(mockContext); expect(result.status).toBe('PASS'); }); it('should WARN when counts mismatch', async () => { fs.existsSync.mockReturnValue(true); fs.readdirSync.mockImplementation((p) => { if (p.includes('commands')) return ['dev.md', 'qa.md', 'pm.md']; return ['dev.md', 'qa.md']; }); const result = await ideSyncCheck.run(mockContext); expect(result.status).toBe('WARN'); }); }); // === INS-4.8: New checks === describe('skills-count check', () => { it('should PASS when >=7 skills directories with SKILL.md', async () => { fs.existsSync.mockReturnValue(true); const dirs = Array.from({ length: 8 }, (_, i) => ({ name: `skill-${i}`, isDirectory: () => true, isFile: () => false, })); fs.readdirSync.mockReturnValue(dirs); const result = await skillsCountCheck.run(mockContext); expect(result.check).toBe('skills-count'); expect(result.status).toBe('PASS'); expect(result.message).toContain('8'); }); it('should WARN when <7 skills found', async () => { fs.existsSync.mockImplementation((p) => { if (p.includes('skills') && !p.includes('SKILL.md')) return true; if (p.includes('SKILL.md')) return true; return true; }); const dirs = Array.from({ length: 3 }, (_, i) => ({ name: `skill-${i}`, isDirectory: () => true, isFile: () => false, })); fs.readdirSync.mockReturnValue(dirs); const result = await skillsCountCheck.run(mockContext); expect(result.status).toBe('WARN'); expect(result.message).toContain('3/7'); }); it('should FAIL when 0 skills found', async () => { fs.existsSync.mockImplementation((p) => { if (p.includes('SKILL.md')) return false; return true; }); const dirs = [{ name: 'empty', isDirectory: () => true, isFile: () => false }]; fs.readdirSync.mockReturnValue(dirs); const result = await skillsCountCheck.run(mockContext); expect(result.status).toBe('FAIL'); expect(result.fixCommand).toBe('npx aios-core install --force'); }); it('should FAIL when skills directory missing', async () => { fs.existsSync.mockReturnValue(false); const result = await skillsCountCheck.run(mockContext); expect(result.status).toBe('FAIL'); }); }); describe('commands-count check', () => { it('should PASS when >=20 command files', async () => { fs.existsSync.mockReturnValue(true); const files = Array.from({ length: 22 }, (_, i) => ({ name: `cmd-${i}.md`, isDirectory: () => false, isFile: () => true, })); fs.readdirSync.mockReturnValue(files); const result = await commandsCountCheck.run(mockContext); expect(result.check).toBe('commands-count'); expect(result.status).toBe('PASS'); expect(result.message).toContain('22'); }); it('should WARN when 12-19 command files', async () => { fs.existsSync.mockReturnValue(true); const files = Array.from({ length: 15 }, (_, i) => ({ name: `cmd-${i}.md`, isDirectory: () => false, isFile: () => true, })); fs.readdirSync.mockReturnValue(files); const result = await commandsCountCheck.run(mockContext); expect(result.status).toBe('WARN'); expect(result.message).toContain('15/20'); }); it('should FAIL when <12 command files', async () => { fs.existsSync.mockReturnValue(true); const files = Array.from({ length: 5 }, (_, i) => ({ name: `cmd-${i}.md`, isDirectory: () => false, isFile: () => true, })); fs.readdirSync.mockReturnValue(files); const result = await commandsCountCheck.run(mockContext); expect(result.status).toBe('FAIL'); expect(result.message).toContain('5'); }); it('should FAIL when commands directory missing', async () => { fs.existsSync.mockReturnValue(false); const result = await commandsCountCheck.run(mockContext); expect(result.status).toBe('FAIL'); }); }); describe('hooks-claude-count check', () => { it('should PASS when >=2 hook files and registered', async () => { fs.existsSync.mockReturnValue(true); const hookFiles = [ { name: 'enforce-git-push.cjs', isFile: () => true, isDirectory: () => false }, { name: 'pre-commit-check.cjs', isFile: () => true, isDirectory: () => false }, ]; fs.readdirSync.mockReturnValue(hookFiles); const settingsLocal = { hooks: { PreToolUse: [{ command: 'node .claude/hooks/enforce-git-push.cjs' }], PostToolUse: [{ command: 'node .claude/hooks/pre-commit-check.cjs' }], }, }; fs.readFileSync.mockReturnValue(JSON.stringify(settingsLocal)); const result = await hooksClaudeCountCheck.run(mockContext); expect(result.check).toBe('hooks-claude-count'); expect(result.status).toBe('PASS'); expect(result.message).toContain('2'); }); it('should WARN when hooks present but not registered', async () => { fs.existsSync.mockImplementation((p) => { if (p.includes('settings.local.json')) return true; return true; }); const hookFiles = [ { name: 'hook-a.cjs', isFile: () => true, isDirectory: () => false }, { name: 'hook-b.cjs', isFile: () => true, isDirectory: () => false }, ]; fs.readdirSync.mockReturnValue(hookFiles); fs.readFileSync.mockReturnValue(JSON.stringify({ hooks: {} })); const result = await hooksClaudeCountCheck.run(mockContext); expect(result.status).toBe('WARN'); expect(result.message).toContain('not registered'); }); it('should WARN when <2 hook files', async () => { fs.existsSync.mockReturnValue(true); const hookFiles = [ { name: 'single-hook.cjs', isFile: () => true, isDirectory: () => false }, ]; fs.readdirSync.mockReturnValue(hookFiles); fs.readFileSync.mockReturnValue(JSON.stringify({ hooks: {} })); const result = await hooksClaudeCountCheck.run(mockContext); expect(result.status).toBe('WARN'); expect(result.message).toContain('1/2'); }); it('should FAIL when no hook files found', async () => { fs.existsSync.mockReturnValue(true); fs.readdirSync.mockReturnValue([]); const result = await hooksClaudeCountCheck.run(mockContext); expect(result.status).toBe('FAIL'); }); it('should FAIL when hooks directory missing', async () => { fs.existsSync.mockReturnValue(false); const result = await hooksClaudeCountCheck.run(mockContext); expect(result.status).toBe('FAIL'); }); }); // === INS-4.8: Registry and task validation === describe('check registry (INS-4.8)', () => { it('should load 15 checks total', () => { // loadChecks is the real function (not mocked) — verifies registration const checks = loadChecks(); expect(checks).toHaveLength(15); }); it('should include all 3 new checks', () => { const checks = loadChecks(); const names = checks.map((c) => c.name); expect(names).toContain('skills-count'); expect(names).toContain('commands-count'); expect(names).toContain('hooks-claude-count'); }); }); describe('health-check.yaml task (INS-4.8)', () => { it('should NOT have *doctor alias', () => { const realFs = jest.requireActual('fs'); const yaml = realFs.readFileSync( path.join(__dirname, '..', '..', '..', '..', '..', '.aios-core', 'development', 'tasks', 'health-check.yaml'), 'utf8', ); // Verify *doctor is not in the aliases list (only *hc should be) const aliasMatch = yaml.match(/aliases:\s*\n((?:\s+-\s+.*\n)*)/); expect(aliasMatch).toBeTruthy(); expect(aliasMatch[1]).not.toContain('*doctor'); expect(aliasMatch[1]).toContain('*hc'); }); it('should reference aios doctor --json in instructions', () => { const realFs = jest.requireActual('fs'); const yaml = realFs.readFileSync( path.join(__dirname, '..', '..', '..', '..', '..', '.aios-core', 'development', 'tasks', 'health-check.yaml'), 'utf8', ); expect(yaml).toContain('aios doctor --json'); expect(yaml).toContain('npx aios-core doctor --json'); }); it('should have governance_map with all 15 checks', () => { const realFs = jest.requireActual('fs'); const yaml = realFs.readFileSync( path.join(__dirname, '..', '..', '..', '..', '..', '.aios-core', 'development', 'tasks', 'health-check.yaml'), 'utf8', ); const expectedChecks = [ 'settings-json', 'rules-files', 'agent-memory', 'entity-registry', 'git-hooks', 'core-config', 'claude-md', 'ide-sync', 'graph-dashboard', 'code-intel', 'node-version', 'npm-packages', 'skills-count', 'commands-count', 'hooks-claude-count', ]; for (const check of expectedChecks) { expect(yaml).toContain(`${check}:`); } }); });