UNPKG

aios-core

Version:

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

569 lines (465 loc) 19.7 kB
/** * @jest-environment node */ const fs = require('fs').promises; const path = require('path'); const os = require('os'); const yaml = require('js-yaml'); const { ProjectStatusLoader } = require('../scripts/project-status-loader'); describe('ProjectStatusLoader', () => { let loader; let testRoot; let cacheFile; beforeEach(async () => { // Use OS temp directory to ensure complete isolation from parent git repo testRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aios-test-')); loader = new ProjectStatusLoader(testRoot); cacheFile = path.join(testRoot, '.aios', 'project-status.yaml'); }); afterEach(async () => { // Cleanup test directory try { await fs.rm(testRoot, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } }); describe('isGitRepository', () => { it('should return false for non-git directory', async () => { // testRoot already created by mkdtemp in beforeEach const isGit = await loader.isGitRepository(); expect(isGit).toBe(false); }); it('should return true for git repository', async () => { // This test requires actual git - skip in CI if git not available // testRoot already created by mkdtemp in beforeEach // Try to initialize git try { const { execa } = require('execa'); await execa('git', ['init'], { cwd: testRoot }); const isGit = await loader.isGitRepository(); expect(isGit).toBe(true); } catch (error) { console.warn('Git not available, skipping test'); expect(true).toBe(true); // Skip test gracefully } }); }); describe('getGitBranch', () => { it('should return unknown on error', async () => { // testRoot already created by mkdtemp in beforeEach const branch = await loader.getGitBranch(); expect(branch).toBe('unknown'); }); it('should detect git branch', async () => { // testRoot already created by mkdtemp in beforeEach try { const { execa } = require('execa'); await execa('git', ['init'], { cwd: testRoot }); // Create initial commit await fs.writeFile(path.join(testRoot, 'test.txt'), 'test'); await execa('git', ['config', 'user.email', 'test@test.com'], { cwd: testRoot }); await execa('git', ['config', 'user.name', 'Test User'], { cwd: testRoot }); await execa('git', ['add', '.'], { cwd: testRoot }); await execa('git', ['commit', '-m', 'Initial commit'], { cwd: testRoot }); const branch = await loader.getGitBranch(); expect(['main', 'master']).toContain(branch); } catch (error) { console.warn('Git not available, skipping test'); expect(true).toBe(true); } }); }); describe('getModifiedFiles', () => { it('should return empty result for non-git repo', async () => { // testRoot already created by mkdtemp in beforeEach const result = await loader.getModifiedFiles(); // Implementation returns { files: [], totalCount: 0 } for non-git repos expect(result.files || result).toEqual([]); }); it('should detect modified files', async () => { // testRoot already created by mkdtemp in beforeEach try { const { execa } = require('execa'); await execa('git', ['init'], { cwd: testRoot }); await execa('git', ['config', 'user.email', 'test@test.com'], { cwd: testRoot }); await execa('git', ['config', 'user.name', 'Test User'], { cwd: testRoot }); // Create and commit initial file await fs.writeFile(path.join(testRoot, 'existing.txt'), 'existing'); await execa('git', ['add', '.'], { cwd: testRoot }); await execa('git', ['commit', '-m', 'Initial'], { cwd: testRoot }); // Modify file await fs.writeFile(path.join(testRoot, 'existing.txt'), 'modified'); const files = await loader.getModifiedFiles(); expect(files).toContain('existing.txt'); } catch (error) { console.warn('Git not available, skipping test'); expect(true).toBe(true); } }); it('should limit to 5 files maximum', async () => { // testRoot already created by mkdtemp in beforeEach try { const { execa } = require('execa'); await execa('git', ['init'], { cwd: testRoot }); await execa('git', ['config', 'user.email', 'test@test.com'], { cwd: testRoot }); await execa('git', ['config', 'user.name', 'Test User'], { cwd: testRoot }); // Create 7 files for (let i = 0; i < 7; i++) { await fs.writeFile(path.join(testRoot, `file${i}.txt`), 'content'); } const files = await loader.getModifiedFiles(); expect(files.length).toBeLessThanOrEqual(5); } catch (error) { console.warn('Git not available, skipping test'); expect(true).toBe(true); } }); }); describe('getRecentCommits', () => { it('should return empty array for non-git repo', async () => { // testRoot already created by mkdtemp in beforeEach const commits = await loader.getRecentCommits(); expect(commits).toEqual([]); }); it('should return empty array for repo with no commits', async () => { // testRoot already created by mkdtemp in beforeEach try { const { execa } = require('execa'); await execa('git', ['init'], { cwd: testRoot }); const commits = await loader.getRecentCommits(); expect(commits).toEqual([]); } catch (error) { console.warn('Git not available, skipping test'); expect(true).toBe(true); } }); it('should limit to 2 commits', async () => { // testRoot already created by mkdtemp in beforeEach try { const { execa } = require('execa'); await execa('git', ['init'], { cwd: testRoot }); await execa('git', ['config', 'user.email', 'test@test.com'], { cwd: testRoot }); await execa('git', ['config', 'user.name', 'Test User'], { cwd: testRoot }); // Create 3 commits for (let i = 0; i < 3; i++) { await fs.writeFile(path.join(testRoot, `file${i}.txt`), 'content'); await execa('git', ['add', '.'], { cwd: testRoot }); await execa('git', ['commit', '-m', `Commit ${i}`], { cwd: testRoot }); } const commits = await loader.getRecentCommits(); expect(commits.length).toBeLessThanOrEqual(2); } catch (error) { console.warn('Git not available, skipping test'); expect(true).toBe(true); } }); }); describe('getCurrentStoryInfo', () => { it('should return null when stories directory missing', async () => { // testRoot already created by mkdtemp in beforeEach const info = await loader.getCurrentStoryInfo(); expect(info).toEqual({ story: null, epic: null }); }); it('should detect InProgress story', async () => { await fs.mkdir(path.join(testRoot, 'docs', 'stories'), { recursive: true }); const storyContent = `# Story 6.1.2.4 **Story ID:** STORY-6.1.2.4 **Epic:** Epic 6.1 - Agent Identity **Status:** InProgress ## Description Test story `; await fs.writeFile(path.join(testRoot, 'docs', 'stories', 'story-6.1.2.4.md'), storyContent); const info = await loader.getCurrentStoryInfo(); expect(info.story).toBe('STORY-6.1.2.4'); expect(info.epic).toContain('Epic 6.1'); }); it('should handle multiple story files correctly', async () => { await fs.mkdir(path.join(testRoot, 'docs', 'stories'), { recursive: true }); // Create one completed story await fs.writeFile( path.join(testRoot, 'docs', 'stories', 'story-1.md'), '**Status:** Completed', ); // Create one in progress story await fs.writeFile( path.join(testRoot, 'docs', 'stories', 'story-2.md'), '**Status:** InProgress\n**Story ID:** STORY-2', ); const info = await loader.getCurrentStoryInfo(); expect(info.story).toBe('STORY-2'); }); }); describe('Cache Management', () => { it('should create cache file on first load', async () => { // testRoot already created by mkdtemp in beforeEach const status = await loader.loadProjectStatus(); // Check cache file exists const cacheExists = await fs .access(cacheFile) .then(() => true) .catch(() => false); expect(cacheExists).toBe(true); }); it('should return cached status within TTL', async () => { // testRoot already created by mkdtemp in beforeEach // First load const status1 = await loader.loadProjectStatus(); const time1 = status1.lastUpdate; // Immediate second load (within TTL) const status2 = await loader.loadProjectStatus(); const time2 = status2.lastUpdate; // Should return same cached status expect(time1).toBe(time2); }); it('should invalidate cache after TTL expires', async () => { // testRoot already created by mkdtemp in beforeEach // Override TTL to 0 seconds for testing loader.cacheTTL = 0; // First load const status1 = await loader.loadProjectStatus(); const time1 = status1.lastUpdate; // Wait 10ms await new Promise((resolve) => setTimeout(resolve, 10)); // Second load (after TTL) const status2 = await loader.loadProjectStatus(); const time2 = status2.lastUpdate; // Should be different timestamps expect(time1).not.toBe(time2); }); it('should clear cache successfully', async () => { // testRoot already created by mkdtemp in beforeEach // Create cache await loader.loadProjectStatus(); // Clear cache const cleared = await loader.clearCache(); expect(cleared).toBe(true); // Check cache file deleted const cacheExists = await fs .access(cacheFile) .then(() => true) .catch(() => false); expect(cacheExists).toBe(false); }); }); describe('Edge Cases', () => { it('should handle detached HEAD state', async () => { // testRoot already created by mkdtemp in beforeEach try { const { execa } = require('execa'); await execa('git', ['init'], { cwd: testRoot }); await execa('git', ['config', 'user.email', 'test@test.com'], { cwd: testRoot }); await execa('git', ['config', 'user.name', 'Test User'], { cwd: testRoot }); // Create commit await fs.writeFile(path.join(testRoot, 'test.txt'), 'test'); await execa('git', ['add', '.'], { cwd: testRoot }); await execa('git', ['commit', '-m', 'Initial'], { cwd: testRoot }); // Get commit hash const { stdout: hash } = await execa('git', ['rev-parse', 'HEAD'], { cwd: testRoot }); // Checkout detached HEAD await execa('git', ['checkout', hash.trim()], { cwd: testRoot }); const branch = await loader.getGitBranch(); expect(typeof branch).toBe('string'); } catch (error) { console.warn('Git not available, skipping test'); expect(true).toBe(true); } }); it('should gracefully handle non-git project', async () => { // testRoot already created by mkdtemp in beforeEach const status = await loader.loadProjectStatus(); expect(status.isGitRepo).toBe(false); expect(status.branch).toBeNull(); expect(status.modifiedFiles).toEqual([]); expect(status.recentCommits).toEqual([]); }); }); describe('formatStatusDisplay', () => { it('should format git status correctly', () => { const status = { isGitRepo: true, branch: 'main', modifiedFiles: ['file1.md', 'file2.js'], recentCommits: ['chore: cleanup', 'feat: new feature'], currentStory: 'STORY-123', lastUpdate: new Date().toISOString(), }; const display = loader.formatStatusDisplay(status); expect(display).toContain('Branch: main'); expect(display).toContain('Modified: file1.md, file2.js'); expect(display).toContain('Recent: chore: cleanup, feat: new feature'); expect(display).toContain('Story: STORY-123'); }); it('should handle non-git repo message', () => { const status = { isGitRepo: false, branch: null, modifiedFiles: [], recentCommits: [], currentStory: null, lastUpdate: new Date().toISOString(), }; const display = loader.formatStatusDisplay(status); expect(display).toContain('Not a git repository'); }); it('should handle clean repository', () => { const status = { isGitRepo: true, branch: 'main', modifiedFiles: [], recentCommits: [], currentStory: null, lastUpdate: new Date().toISOString(), }; const display = loader.formatStatusDisplay(status); expect(display).toContain('Branch: main'); }); // Story 1.5: Worktree Status Integration tests it('should display worktrees summary in status', () => { const status = { isGitRepo: true, branch: 'main', modifiedFiles: [], recentCommits: [], currentStory: null, lastUpdate: new Date().toISOString(), worktrees: { 'STORY-42': { path: '.aios/worktrees/STORY-42', branch: 'auto-claude/STORY-42', createdAt: '2026-01-28T10:00:00Z', lastActivity: '2026-01-28T12:30:00Z', uncommittedChanges: 3, status: 'active', }, 'STORY-43': { path: '.aios/worktrees/STORY-43', branch: 'auto-claude/STORY-43', createdAt: '2026-01-27T10:00:00Z', lastActivity: '2026-01-27T12:30:00Z', uncommittedChanges: 0, status: 'active', }, }, }; const display = loader.formatStatusDisplay(status); expect(display).toContain('Worktrees:'); expect(display).toContain('2/2 active'); expect(display).toContain('1 with changes'); }); it('should not display worktrees section when empty', () => { const status = { isGitRepo: true, branch: 'main', modifiedFiles: [], recentCommits: [], currentStory: null, lastUpdate: new Date().toISOString(), worktrees: {}, }; const display = loader.formatStatusDisplay(status); expect(display).not.toContain('Worktrees:'); }); it('should not display worktrees section when undefined', () => { const status = { isGitRepo: true, branch: 'main', modifiedFiles: [], recentCommits: [], currentStory: null, lastUpdate: new Date().toISOString(), }; const display = loader.formatStatusDisplay(status); expect(display).not.toContain('Worktrees:'); }); }); // Story 1.5: Worktree Status Integration describe('getWorktreesStatus', () => { it('should return null when no worktrees exist', async () => { // testRoot already created by mkdtemp in beforeEach const worktrees = await loader.getWorktreesStatus(); expect(worktrees).toBeNull(); }); it('should return worktree info with required fields', async () => { // testRoot already created by mkdtemp in beforeEach try { const { execa } = require('execa'); await execa('git', ['init'], { cwd: testRoot }); await execa('git', ['config', 'user.email', 'test@test.com'], { cwd: testRoot }); await execa('git', ['config', 'user.name', 'Test User'], { cwd: testRoot }); // Create initial commit (required for worktree) await fs.writeFile(path.join(testRoot, 'test.txt'), 'test'); await execa('git', ['add', '.'], { cwd: testRoot }); await execa('git', ['commit', '-m', 'Initial commit'], { cwd: testRoot }); // Create worktree manually const worktreePath = path.join(testRoot, '.aios', 'worktrees', 'STORY-TEST'); await execa('git', ['worktree', 'add', worktreePath, '-b', 'auto-claude/STORY-TEST'], { cwd: testRoot, }); const worktrees = await loader.getWorktreesStatus(); expect(worktrees).not.toBeNull(); expect(worktrees['STORY-TEST']).toBeDefined(); expect(worktrees['STORY-TEST'].path).toContain('STORY-TEST'); expect(worktrees['STORY-TEST'].branch).toBe('auto-claude/STORY-TEST'); expect(worktrees['STORY-TEST'].createdAt).toBeDefined(); expect(worktrees['STORY-TEST'].lastActivity).toBeDefined(); expect(typeof worktrees['STORY-TEST'].uncommittedChanges).toBe('number'); expect(['active', 'stale']).toContain(worktrees['STORY-TEST'].status); // Cleanup worktree await execa('git', ['worktree', 'remove', worktreePath, '--force'], { cwd: testRoot }); await execa('git', ['branch', '-D', 'auto-claude/STORY-TEST'], { cwd: testRoot }); } catch (error) { console.warn('Git not available, skipping test:', error.message); expect(true).toBe(true); } }); }); describe('generateStatus with worktrees', () => { it('should include worktrees in generated status', async () => { // testRoot already created by mkdtemp in beforeEach try { const { execa } = require('execa'); await execa('git', ['init'], { cwd: testRoot }); await execa('git', ['config', 'user.email', 'test@test.com'], { cwd: testRoot }); await execa('git', ['config', 'user.name', 'Test User'], { cwd: testRoot }); // Create initial commit await fs.writeFile(path.join(testRoot, 'test.txt'), 'test'); await execa('git', ['add', '.'], { cwd: testRoot }); await execa('git', ['commit', '-m', 'Initial commit'], { cwd: testRoot }); // Create worktree const worktreePath = path.join(testRoot, '.aios', 'worktrees', 'STORY-GEN'); await execa('git', ['worktree', 'add', worktreePath, '-b', 'auto-claude/STORY-GEN'], { cwd: testRoot, }); const status = await loader.generateStatus(); expect(status.worktrees).toBeDefined(); expect(status.worktrees['STORY-GEN']).toBeDefined(); // Cleanup await execa('git', ['worktree', 'remove', worktreePath, '--force'], { cwd: testRoot }); await execa('git', ['branch', '-D', 'auto-claude/STORY-GEN'], { cwd: testRoot }); } catch (error) { console.warn('Git not available, skipping test:', error.message); expect(true).toBe(true); } }); it('should not include worktrees key when none exist', async () => { // testRoot already created by mkdtemp in beforeEach try { const { execa } = require('execa'); await execa('git', ['init'], { cwd: testRoot }); await execa('git', ['config', 'user.email', 'test@test.com'], { cwd: testRoot }); await execa('git', ['config', 'user.name', 'Test User'], { cwd: testRoot }); await fs.writeFile(path.join(testRoot, 'test.txt'), 'test'); await execa('git', ['add', '.'], { cwd: testRoot }); await execa('git', ['commit', '-m', 'Initial commit'], { cwd: testRoot }); const status = await loader.generateStatus(); // worktrees should be undefined (not included) when none exist expect(status.worktrees).toBeUndefined(); } catch (error) { console.warn('Git not available, skipping test:', error.message); expect(true).toBe(true); } }); }); });