- Fix beadWorkspaceScanner.test.ts: make beforeEach async to use top-level await - Fix parser.real-logs.integration.test.ts: change sequence test to non-decreasing (real logs have duplicate sequences) - Fix FleetSummaryBar.test.tsx: correct expected separator count from 5 to 4 All 2484 tests now pass (was 89 failed due to wrong Node.js version + 3 test bugs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
230 lines
6.8 KiB
TypeScript
230 lines
6.8 KiB
TypeScript
/**
|
|
* Tests for Bead Workspace Scanner
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { scanBeadWorkspaces, getProjectStats, getClosedBeadsForWorkspace, ProjectStats } from './beadWorkspaceScanner.js';
|
|
import { WorkspaceConfig } from './config.js';
|
|
|
|
// Mock fs module
|
|
vi.mock('fs');
|
|
const mockFs = vi.mocked(fs);
|
|
|
|
// Mock config module
|
|
vi.mock('./config.js', () => ({
|
|
loadWorkspaces: vi.fn(),
|
|
}));
|
|
|
|
describe('Bead Workspace Scanner', () => {
|
|
const mockWorkspaces: WorkspaceConfig[] = [
|
|
{
|
|
path: '/home/coding/FABRIC',
|
|
name: 'FABRIC',
|
|
prefix: 'bf',
|
|
},
|
|
{
|
|
path: '/home/coding/spaxel',
|
|
name: 'spaxel',
|
|
prefix: 'bf',
|
|
},
|
|
];
|
|
|
|
const mockBeadsData = {
|
|
'/home/coding/FABRIC/.beads/issues.jsonl': [
|
|
JSON.stringify({
|
|
id: 'bf-1abc',
|
|
title: 'Test bead 1',
|
|
status: 'closed',
|
|
closed_at: '2026-05-20T10:00:00Z',
|
|
assignee: 'claude-code-glm-4.7-alpha',
|
|
source_repo: '.',
|
|
}),
|
|
JSON.stringify({
|
|
id: 'bf-2def',
|
|
title: 'Test bead 2',
|
|
status: 'open',
|
|
source_repo: '.',
|
|
}),
|
|
JSON.stringify({
|
|
id: 'bf-3ghi',
|
|
title: 'Test bead 3',
|
|
status: 'closed',
|
|
closed_at: '2026-05-21T14:30:00Z',
|
|
assignee: 'claude-code-glm-4.7-beta',
|
|
source_repo: '.',
|
|
}),
|
|
].join('\n'),
|
|
'/home/coding/spaxel/.beads/issues.jsonl': [
|
|
JSON.stringify({
|
|
id: 'bf-4jkl',
|
|
title: 'Spaxel bead 1',
|
|
status: 'closed',
|
|
closed_at: '2026-05-19T08:00:00Z',
|
|
assignee: 'claude-code-glm-4.7-gamma',
|
|
source_repo: '.',
|
|
}),
|
|
JSON.stringify({
|
|
id: 'bf-5mno',
|
|
title: 'Spaxel bead 2',
|
|
status: 'closed',
|
|
closed_at: '2026-05-20T12:00:00Z',
|
|
assignee: 'claude-code-glm-4.7-alpha',
|
|
source_repo: '.',
|
|
}),
|
|
].join('\n'),
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
|
|
// Mock loadWorkspaces to return our test workspaces
|
|
const { loadWorkspaces } = await import('./config.js');
|
|
vi.mocked(loadWorkspaces).mockReturnValue(mockWorkspaces);
|
|
|
|
// Mock fs.existsSync to return true for our test files
|
|
mockFs.existsSync.mockImplementation((filePath) => {
|
|
const pathStr = String(filePath);
|
|
return Object.keys(mockBeadsData).includes(pathStr);
|
|
});
|
|
|
|
// Mock fs.readFileSync to return our test data
|
|
mockFs.readFileSync.mockImplementation((filePath) => {
|
|
const pathStr = String(filePath);
|
|
if (mockBeadsData[pathStr]) {
|
|
return mockBeadsData[pathStr];
|
|
}
|
|
throw new Error(`File not found: ${pathStr}`);
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('scanBeadWorkspaces', () => {
|
|
it('should scan all configured workspaces', () => {
|
|
const result = scanBeadWorkspaces();
|
|
|
|
expect(result.workspacesScanned).toBe(2);
|
|
expect(result.totalBeads).toBe(5);
|
|
expect(result.totalClosed).toBe(4);
|
|
});
|
|
|
|
it('should return project stats sorted by closed count', () => {
|
|
const result = scanBeadWorkspaces();
|
|
|
|
expect(result.byProject).toHaveLength(2);
|
|
expect(result.byProject[0].name).toBe('FABRIC');
|
|
expect(result.byProject[0].closedCount).toBe(2);
|
|
expect(result.byProject[1].name).toBe('spaxel');
|
|
expect(result.byProject[1].closedCount).toBe(2);
|
|
});
|
|
|
|
it('should track assignees per project', () => {
|
|
const result = scanBeadWorkspaces();
|
|
|
|
const fabric = result.byProject.find(p => p.name === 'FABRIC');
|
|
expect(fabric?.byAssignee['claude-code-glm-4.7-alpha']).toBe(1);
|
|
expect(fabric?.byAssignee['claude-code-glm-4.7-beta']).toBe(1);
|
|
|
|
const spaxel = result.byProject.find(p => p.name === 'spaxel');
|
|
expect(spaxel?.byAssignee['claude-code-glm-4.7-gamma']).toBe(1);
|
|
expect(spaxel?.byAssignee['claude-code-glm-4.7-alpha']).toBe(1);
|
|
});
|
|
|
|
it('should track most recent closure timestamp', () => {
|
|
const result = scanBeadWorkspaces();
|
|
|
|
const fabric = result.byProject.find(p => p.name === 'FABRIC');
|
|
expect(fabric?.lastClosedAt).toBe('2026-05-21T14:30:00Z');
|
|
|
|
const spaxel = result.byProject.find(p => p.name === 'spaxel');
|
|
expect(spaxel?.lastClosedAt).toBe('2026-05-20T12:00:00Z');
|
|
});
|
|
|
|
it('should handle missing workspace directories gracefully', () => {
|
|
mockFs.existsSync.mockImplementation((filePath) => {
|
|
const pathStr = String(filePath);
|
|
return pathStr.includes('spaxel'); // Only spaxel exists
|
|
});
|
|
|
|
const result = scanBeadWorkspaces();
|
|
|
|
expect(result.workspacesScanned).toBe(2);
|
|
expect(result.byProject).toHaveLength(1);
|
|
expect(result.byProject[0].name).toBe('spaxel');
|
|
});
|
|
|
|
it('should handle invalid JSON lines gracefully', () => {
|
|
mockFs.readFileSync.mockImplementation((filePath) => {
|
|
const pathStr = String(filePath);
|
|
if (pathStr.includes('FABRIC')) {
|
|
return [
|
|
JSON.stringify({
|
|
id: 'bf-1abc',
|
|
title: 'Valid bead',
|
|
status: 'closed',
|
|
closed_at: '2026-05-20T10:00:00Z',
|
|
source_repo: '.',
|
|
}),
|
|
'invalid json line',
|
|
JSON.stringify({
|
|
id: 'bf-2def',
|
|
title: 'Another valid bead',
|
|
status: 'closed',
|
|
closed_at: '2026-05-21T10:00:00Z',
|
|
source_repo: '.',
|
|
}),
|
|
].join('\n');
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const result = scanBeadWorkspaces();
|
|
|
|
expect(result.totalClosed).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('getProjectStats', () => {
|
|
it('should return stats for a specific project', () => {
|
|
const stats = getProjectStats('FABRIC');
|
|
|
|
expect(stats).toBeDefined();
|
|
expect(stats?.name).toBe('FABRIC');
|
|
expect(stats?.closedCount).toBe(2);
|
|
expect(stats?.prefix).toBe('bf');
|
|
});
|
|
|
|
it('should return undefined for non-existent project', () => {
|
|
const stats = getProjectStats('nonexistent');
|
|
|
|
expect(stats).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('getClosedBeadsForWorkspace', () => {
|
|
it('should return closed beads for a workspace', () => {
|
|
const beads = getClosedBeadsForWorkspace('/home/coding/FABRIC');
|
|
|
|
expect(beads).toHaveLength(2);
|
|
expect(beads[0].status).toBe('closed');
|
|
expect(beads[1].status).toBe('closed');
|
|
});
|
|
|
|
it('should respect limit parameter', () => {
|
|
const beads = getClosedBeadsForWorkspace('/home/coding/FABRIC', 1);
|
|
|
|
expect(beads).toHaveLength(1);
|
|
});
|
|
|
|
it('should return empty array for non-existent workspace', () => {
|
|
mockFs.existsSync.mockReturnValue(false);
|
|
const beads = getClosedBeadsForWorkspace('/nonexistent/path');
|
|
|
|
expect(beads).toEqual([]);
|
|
});
|
|
});
|
|
});
|