700 lines
23 KiB
JavaScript
700 lines
23 KiB
JavaScript
/**
|
|
* Tests for CacheManager PRD and Epic Extensions
|
|
*
|
|
* Tests cover:
|
|
* - PRD read/write operations
|
|
* - Epic read/write operations with PRD lineage
|
|
* - Status updates and filtering
|
|
* - User task queries (getPrdsNeedingAttention, getEpicsNeedingAttention)
|
|
* - Extended statistics
|
|
* - Document staleness checking
|
|
* - Atomic file operations
|
|
*
|
|
* Uses real temporary directory for testing actual file I/O operations.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import os from 'os';
|
|
|
|
// Import the CacheManager (CommonJS module)
|
|
const { CacheManager, DOCUMENT_TYPES, CACHE_META_FILENAME } = await import(
|
|
'../../../src/modules/bmm/lib/cache/cache-manager.js'
|
|
);
|
|
|
|
describe('CacheManager PRD/Epic Extensions', () => {
|
|
let cacheManager;
|
|
let testCacheDir;
|
|
|
|
beforeEach(() => {
|
|
// Create a real temporary directory for each test
|
|
testCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bmad-cache-test-'));
|
|
|
|
cacheManager = new CacheManager({
|
|
cacheDir: testCacheDir,
|
|
stalenessThresholdMinutes: 5,
|
|
github: { owner: 'test-org', repo: 'test-repo' }
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up the temporary directory
|
|
if (testCacheDir && fs.existsSync(testCacheDir)) {
|
|
fs.rmSync(testCacheDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// ============ DOCUMENT_TYPES Tests ============
|
|
|
|
describe('DOCUMENT_TYPES', () => {
|
|
it('should define all document types', () => {
|
|
expect(DOCUMENT_TYPES.story).toBe('story');
|
|
expect(DOCUMENT_TYPES.prd).toBe('prd');
|
|
expect(DOCUMENT_TYPES.epic).toBe('epic');
|
|
});
|
|
});
|
|
|
|
// ============ Directory Initialization Tests ============
|
|
|
|
describe('directory initialization', () => {
|
|
it('should create subdirectories for all document types', () => {
|
|
expect(fs.existsSync(path.join(testCacheDir, 'stories'))).toBe(true);
|
|
expect(fs.existsSync(path.join(testCacheDir, 'prds'))).toBe(true);
|
|
expect(fs.existsSync(path.join(testCacheDir, 'epics'))).toBe(true);
|
|
});
|
|
|
|
it('should create meta file on first access', () => {
|
|
cacheManager.loadMeta();
|
|
expect(fs.existsSync(path.join(testCacheDir, CACHE_META_FILENAME))).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ============ Metadata Migration Tests ============
|
|
|
|
describe('metadata migration', () => {
|
|
it('should migrate v1 metadata to v2', () => {
|
|
// Write v1 metadata directly
|
|
const v1Meta = {
|
|
version: '1.0.0',
|
|
stories: { 'story-1': { github_issue: 10 } }
|
|
};
|
|
fs.writeFileSync(
|
|
path.join(testCacheDir, CACHE_META_FILENAME),
|
|
JSON.stringify(v1Meta),
|
|
'utf8'
|
|
);
|
|
|
|
// Create new manager to trigger migration
|
|
const manager = new CacheManager({
|
|
cacheDir: testCacheDir,
|
|
github: {}
|
|
});
|
|
|
|
const meta = manager.loadMeta();
|
|
|
|
expect(meta.version).toBe('2.0.0');
|
|
expect(meta.prds).toBeDefined();
|
|
expect(meta.epics).toBeDefined();
|
|
expect(meta.stories).toEqual({ 'story-1': { github_issue: 10 } });
|
|
});
|
|
|
|
it('should not migrate already v2 metadata', () => {
|
|
// Write v2 metadata directly
|
|
const v2Meta = {
|
|
version: '2.0.0',
|
|
prds: { 'existing-prd': { status: 'approved' } },
|
|
epics: { 'existing-epic': { status: 'draft' } },
|
|
stories: {}
|
|
};
|
|
fs.writeFileSync(
|
|
path.join(testCacheDir, CACHE_META_FILENAME),
|
|
JSON.stringify(v2Meta),
|
|
'utf8'
|
|
);
|
|
|
|
const manager = new CacheManager({
|
|
cacheDir: testCacheDir,
|
|
github: {}
|
|
});
|
|
|
|
const meta = manager.loadMeta();
|
|
|
|
expect(meta.prds['existing-prd'].status).toBe('approved');
|
|
expect(meta.epics['existing-epic'].status).toBe('draft');
|
|
});
|
|
});
|
|
|
|
// ============ PRD Methods Tests ============
|
|
|
|
describe('PRD operations', () => {
|
|
describe('getPrdPath', () => {
|
|
it('should return correct path for PRD', () => {
|
|
const prdPath = cacheManager.getPrdPath('user-auth');
|
|
expect(prdPath).toBe(path.join(testCacheDir, 'prds', 'user-auth.md'));
|
|
});
|
|
});
|
|
|
|
describe('writePrd', () => {
|
|
it('should write PRD content and update metadata', () => {
|
|
const content = '# PRD: User Authentication\n\nThis is the content.';
|
|
const prdMeta = {
|
|
review_issue: 100,
|
|
version: 1,
|
|
status: 'draft',
|
|
stakeholders: ['@alice', '@bob'],
|
|
owner: '@sarah'
|
|
};
|
|
|
|
const result = cacheManager.writePrd('user-auth', content, prdMeta);
|
|
|
|
expect(result.prdKey).toBe('user-auth');
|
|
expect(result.hash).toBeDefined();
|
|
expect(result.hash.length).toBe(64); // SHA-256 hex
|
|
|
|
// Verify file was written
|
|
const prdPath = cacheManager.getPrdPath('user-auth');
|
|
expect(fs.existsSync(prdPath)).toBe(true);
|
|
expect(fs.readFileSync(prdPath, 'utf8')).toBe(content);
|
|
|
|
// Verify metadata was updated
|
|
const meta = cacheManager.loadMeta();
|
|
expect(meta.prds['user-auth'].status).toBe('draft');
|
|
expect(meta.prds['user-auth'].stakeholders).toEqual(['@alice', '@bob']);
|
|
});
|
|
|
|
it('should preserve existing metadata when not provided', () => {
|
|
// First write with full metadata
|
|
cacheManager.writePrd('user-auth', 'Content v1', {
|
|
review_issue: 100,
|
|
version: 2,
|
|
status: 'feedback',
|
|
stakeholders: ['@alice']
|
|
});
|
|
|
|
// Write with partial metadata
|
|
cacheManager.writePrd('user-auth', 'Content v2', { version: 3 });
|
|
|
|
// Verify metadata was merged
|
|
const meta = cacheManager.loadMeta();
|
|
expect(meta.prds['user-auth'].version).toBe(3);
|
|
expect(meta.prds['user-auth'].review_issue).toBe(100);
|
|
expect(meta.prds['user-auth'].stakeholders).toEqual(['@alice']);
|
|
});
|
|
});
|
|
|
|
describe('readPrd', () => {
|
|
it('should return null for non-existent PRD', () => {
|
|
const result = cacheManager.readPrd('non-existent');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should return PRD content with metadata', () => {
|
|
const content = '# PRD: User Auth';
|
|
cacheManager.writePrd('user-auth', content, {
|
|
version: 1,
|
|
status: 'draft'
|
|
});
|
|
|
|
const result = cacheManager.readPrd('user-auth');
|
|
|
|
expect(result.content).toBe(content);
|
|
expect(result.meta.version).toBe(1);
|
|
expect(result.isStale).toBe(false);
|
|
});
|
|
|
|
it('should mark stale PRDs with warning', () => {
|
|
// Write PRD first
|
|
cacheManager.writePrd('user-auth', '# PRD Content', { status: 'draft' });
|
|
|
|
// Manually set old timestamp
|
|
const meta = cacheManager.loadMeta();
|
|
meta.prds['user-auth'].cache_timestamp = '2020-01-01T00:00:00Z';
|
|
cacheManager.saveMeta(meta);
|
|
|
|
const result = cacheManager.readPrd('user-auth');
|
|
|
|
expect(result.isStale).toBe(true);
|
|
expect(result.warning).toContain('stale');
|
|
});
|
|
|
|
it('should ignore staleness when option set', () => {
|
|
// Write PRD first
|
|
cacheManager.writePrd('user-auth', '# PRD Content', { status: 'draft' });
|
|
|
|
// Manually set old timestamp
|
|
const meta = cacheManager.loadMeta();
|
|
meta.prds['user-auth'].cache_timestamp = '2020-01-01T00:00:00Z';
|
|
cacheManager.saveMeta(meta);
|
|
|
|
const result = cacheManager.readPrd('user-auth', { ignoreStale: true });
|
|
|
|
expect(result.isStale).toBe(true);
|
|
expect(result.warning).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('updatePrdStatus', () => {
|
|
it('should update PRD status', () => {
|
|
cacheManager.writePrd('user-auth', '# PRD', { status: 'draft' });
|
|
|
|
cacheManager.updatePrdStatus('user-auth', 'feedback');
|
|
|
|
const meta = cacheManager.loadMeta();
|
|
expect(meta.prds['user-auth'].status).toBe('feedback');
|
|
});
|
|
|
|
it('should throw error for non-existent PRD', () => {
|
|
expect(() => {
|
|
cacheManager.updatePrdStatus('non-existent', 'feedback');
|
|
}).toThrow('PRD not found in cache: non-existent');
|
|
});
|
|
});
|
|
|
|
describe('listCachedPrds', () => {
|
|
it('should return all cached PRD keys', () => {
|
|
cacheManager.writePrd('user-auth', '# PRD 1', { status: 'draft' });
|
|
cacheManager.writePrd('payments', '# PRD 2', { status: 'approved' });
|
|
cacheManager.writePrd('mobile', '# PRD 3', { status: 'feedback' });
|
|
|
|
const prds = cacheManager.listCachedPrds();
|
|
|
|
expect(prds).toContain('user-auth');
|
|
expect(prds).toContain('payments');
|
|
expect(prds).toContain('mobile');
|
|
expect(prds.length).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('getPrdsByStatus', () => {
|
|
it('should filter PRDs by status', () => {
|
|
cacheManager.writePrd('user-auth', '# PRD 1', { status: 'feedback' });
|
|
cacheManager.writePrd('payments', '# PRD 2', { status: 'approved' });
|
|
cacheManager.writePrd('mobile', '# PRD 3', { status: 'feedback' });
|
|
|
|
const feedbackPrds = cacheManager.getPrdsByStatus('feedback');
|
|
|
|
expect(feedbackPrds).toHaveLength(2);
|
|
expect(feedbackPrds.map(p => p.prdKey)).toContain('user-auth');
|
|
expect(feedbackPrds.map(p => p.prdKey)).toContain('mobile');
|
|
});
|
|
});
|
|
|
|
describe('getPrdsNeedingAttention', () => {
|
|
it('should find PRDs needing feedback from user', () => {
|
|
cacheManager.writePrd('user-auth', '# PRD 1', {
|
|
status: 'feedback',
|
|
stakeholders: ['@alice', '@bob']
|
|
});
|
|
cacheManager.writePrd('payments', '# PRD 2', {
|
|
status: 'signoff',
|
|
stakeholders: ['@alice', '@charlie']
|
|
});
|
|
cacheManager.writePrd('mobile', '# PRD 3', {
|
|
status: 'feedback',
|
|
stakeholders: ['@charlie']
|
|
});
|
|
|
|
const tasks = cacheManager.getPrdsNeedingAttention('alice');
|
|
|
|
expect(tasks.pendingFeedback).toHaveLength(1);
|
|
expect(tasks.pendingFeedback[0].prdKey).toBe('user-auth');
|
|
expect(tasks.pendingSignoff).toHaveLength(1);
|
|
expect(tasks.pendingSignoff[0].prdKey).toBe('payments');
|
|
});
|
|
|
|
it('should handle @ prefix in username', () => {
|
|
cacheManager.writePrd('user-auth', '# PRD 1', {
|
|
status: 'feedback',
|
|
stakeholders: ['alice', 'bob']
|
|
});
|
|
|
|
const tasks = cacheManager.getPrdsNeedingAttention('@alice');
|
|
|
|
expect(tasks.pendingFeedback).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('deletePrd', () => {
|
|
it('should delete PRD file and metadata', () => {
|
|
cacheManager.writePrd('user-auth', '# PRD', { status: 'draft' });
|
|
const prdPath = cacheManager.getPrdPath('user-auth');
|
|
|
|
expect(fs.existsSync(prdPath)).toBe(true);
|
|
|
|
cacheManager.deletePrd('user-auth');
|
|
|
|
expect(fs.existsSync(prdPath)).toBe(false);
|
|
expect(cacheManager.loadMeta().prds['user-auth']).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
// ============ Epic Methods Tests ============
|
|
|
|
describe('Epic operations', () => {
|
|
describe('getEpicPath', () => {
|
|
it('should return correct path for Epic', () => {
|
|
const epicPath = cacheManager.getEpicPath('2');
|
|
expect(epicPath).toBe(path.join(testCacheDir, 'epics', 'epic-2.md'));
|
|
});
|
|
});
|
|
|
|
describe('writeEpic', () => {
|
|
it('should write Epic content with PRD lineage', () => {
|
|
const content = '# Epic 2: Core Authentication';
|
|
const epicMeta = {
|
|
github_issue: 50,
|
|
prd_key: 'user-auth',
|
|
version: 1,
|
|
status: 'draft',
|
|
stories: ['2-1-login', '2-2-logout']
|
|
};
|
|
|
|
const result = cacheManager.writeEpic('2', content, epicMeta);
|
|
|
|
expect(result.epicKey).toBe('2');
|
|
expect(result.hash).toBeDefined();
|
|
expect(result.hash.length).toBe(64);
|
|
|
|
// Verify file was written
|
|
const epicPath = cacheManager.getEpicPath('2');
|
|
expect(fs.existsSync(epicPath)).toBe(true);
|
|
expect(fs.readFileSync(epicPath, 'utf8')).toBe(content);
|
|
});
|
|
|
|
it('should track PRD lineage in metadata', () => {
|
|
cacheManager.writeEpic('2', 'Epic content', {
|
|
prd_key: 'user-auth',
|
|
status: 'draft'
|
|
});
|
|
|
|
const meta = cacheManager.loadMeta();
|
|
expect(meta.epics['2'].prd_key).toBe('user-auth');
|
|
});
|
|
});
|
|
|
|
describe('readEpic', () => {
|
|
it('should return null for non-existent Epic', () => {
|
|
const result = cacheManager.readEpic('999');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should return Epic content with metadata', () => {
|
|
const content = '# Epic 2: Auth';
|
|
cacheManager.writeEpic('2', content, {
|
|
prd_key: 'user-auth',
|
|
version: 1,
|
|
status: 'draft'
|
|
});
|
|
|
|
const result = cacheManager.readEpic('2');
|
|
|
|
expect(result.content).toBe(content);
|
|
expect(result.meta.prd_key).toBe('user-auth');
|
|
expect(result.isStale).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('updateEpicStatus', () => {
|
|
it('should update Epic status', () => {
|
|
cacheManager.writeEpic('2', '# Epic', { status: 'draft' });
|
|
|
|
cacheManager.updateEpicStatus('2', 'feedback');
|
|
|
|
const meta = cacheManager.loadMeta();
|
|
expect(meta.epics['2'].status).toBe('feedback');
|
|
});
|
|
|
|
it('should throw error for non-existent Epic', () => {
|
|
expect(() => {
|
|
cacheManager.updateEpicStatus('999', 'feedback');
|
|
}).toThrow('Epic not found in cache: 999');
|
|
});
|
|
});
|
|
|
|
describe('listCachedEpics', () => {
|
|
it('should return all cached Epic keys', () => {
|
|
cacheManager.writeEpic('1', '# Epic 1', { status: 'approved' });
|
|
cacheManager.writeEpic('2', '# Epic 2', { status: 'draft' });
|
|
cacheManager.writeEpic('3', '# Epic 3', { status: 'feedback' });
|
|
|
|
const epics = cacheManager.listCachedEpics();
|
|
|
|
expect(epics).toContain('1');
|
|
expect(epics).toContain('2');
|
|
expect(epics).toContain('3');
|
|
expect(epics.length).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('getEpicsByPrd', () => {
|
|
it('should filter Epics by source PRD', () => {
|
|
cacheManager.writeEpic('1', '# Epic 1', { prd_key: 'user-auth', status: 'approved' });
|
|
cacheManager.writeEpic('2', '# Epic 2', { prd_key: 'user-auth', status: 'draft' });
|
|
cacheManager.writeEpic('3', '# Epic 3', { prd_key: 'payments', status: 'draft' });
|
|
|
|
const authEpics = cacheManager.getEpicsByPrd('user-auth');
|
|
|
|
expect(authEpics).toHaveLength(2);
|
|
expect(authEpics.map(e => e.epicKey)).toContain('1');
|
|
expect(authEpics.map(e => e.epicKey)).toContain('2');
|
|
});
|
|
});
|
|
|
|
describe('getEpicsNeedingAttention', () => {
|
|
it('should find Epics needing feedback from user', () => {
|
|
cacheManager.writeEpic('1', '# Epic 1', {
|
|
status: 'feedback',
|
|
stakeholders: ['@alice', '@bob']
|
|
});
|
|
cacheManager.writeEpic('2', '# Epic 2', {
|
|
status: 'draft',
|
|
stakeholders: ['@alice']
|
|
});
|
|
cacheManager.writeEpic('3', '# Epic 3', {
|
|
status: 'feedback',
|
|
stakeholders: ['@charlie']
|
|
});
|
|
|
|
const tasks = cacheManager.getEpicsNeedingAttention('alice');
|
|
|
|
expect(tasks.pendingFeedback).toHaveLength(1);
|
|
expect(tasks.pendingFeedback[0].epicKey).toBe('1');
|
|
});
|
|
});
|
|
|
|
describe('deleteEpic', () => {
|
|
it('should delete Epic file and metadata', () => {
|
|
cacheManager.writeEpic('2', '# Epic', { status: 'draft' });
|
|
const epicPath = cacheManager.getEpicPath('2');
|
|
|
|
expect(fs.existsSync(epicPath)).toBe(true);
|
|
|
|
cacheManager.deleteEpic('2');
|
|
|
|
expect(fs.existsSync(epicPath)).toBe(false);
|
|
expect(cacheManager.loadMeta().epics['2']).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
// ============ Unified Task Query Tests ============
|
|
|
|
describe('getMyTasks', () => {
|
|
it('should return combined PRD and Epic tasks', () => {
|
|
cacheManager.writePrd('user-auth', '# PRD 1', {
|
|
status: 'feedback',
|
|
stakeholders: ['@alice']
|
|
});
|
|
cacheManager.writePrd('payments', '# PRD 2', {
|
|
status: 'signoff',
|
|
stakeholders: ['@alice']
|
|
});
|
|
cacheManager.writeEpic('2', '# Epic 2', {
|
|
status: 'feedback',
|
|
stakeholders: ['@alice']
|
|
});
|
|
|
|
const tasks = cacheManager.getMyTasks('alice');
|
|
|
|
expect(tasks.prds.pendingFeedback).toHaveLength(1);
|
|
expect(tasks.prds.pendingSignoff).toHaveLength(1);
|
|
expect(tasks.epics.pendingFeedback).toHaveLength(1);
|
|
});
|
|
|
|
it('should return empty arrays when user has no tasks', () => {
|
|
cacheManager.writePrd('user-auth', '# PRD 1', {
|
|
status: 'feedback',
|
|
stakeholders: ['@bob']
|
|
});
|
|
|
|
const tasks = cacheManager.getMyTasks('alice');
|
|
|
|
expect(tasks.prds.pendingFeedback).toHaveLength(0);
|
|
expect(tasks.prds.pendingSignoff).toHaveLength(0);
|
|
expect(tasks.epics.pendingFeedback).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ============ Extended Statistics Tests ============
|
|
|
|
describe('getExtendedStats', () => {
|
|
it('should return comprehensive statistics', () => {
|
|
cacheManager.writeStory('2-1-login', '# Story', { github_issue: 10 });
|
|
cacheManager.writePrd('user-auth', '# PRD 1', { status: 'feedback' });
|
|
cacheManager.writePrd('payments', '# PRD 2', { status: 'approved' });
|
|
cacheManager.writePrd('mobile', '# PRD 3', { status: 'feedback' });
|
|
cacheManager.writeEpic('1', '# Epic 1', { status: 'approved' });
|
|
cacheManager.writeEpic('2', '# Epic 2', { status: 'draft' });
|
|
|
|
const stats = cacheManager.getExtendedStats();
|
|
|
|
expect(stats.story_count).toBe(1);
|
|
expect(stats.prd_count).toBe(3);
|
|
expect(stats.prds_by_status).toEqual({
|
|
feedback: 2,
|
|
approved: 1
|
|
});
|
|
expect(stats.epic_count).toBe(2);
|
|
expect(stats.epics_by_status).toEqual({
|
|
approved: 1,
|
|
draft: 1
|
|
});
|
|
expect(stats.prd_size_kb).toBeGreaterThanOrEqual(0);
|
|
expect(stats.epic_size_kb).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|
|
|
|
// ============ Document Staleness Tests ============
|
|
|
|
describe('_isDocumentStale', () => {
|
|
it('should return true for missing metadata', () => {
|
|
expect(cacheManager._isDocumentStale(null)).toBe(true);
|
|
expect(cacheManager._isDocumentStale({})).toBe(true);
|
|
});
|
|
|
|
it('should return true for old cache timestamp', () => {
|
|
const oldMeta = {
|
|
cache_timestamp: '2020-01-01T00:00:00Z'
|
|
};
|
|
|
|
expect(cacheManager._isDocumentStale(oldMeta)).toBe(true);
|
|
});
|
|
|
|
it('should return false for recent cache timestamp', () => {
|
|
const recentMeta = {
|
|
cache_timestamp: new Date().toISOString()
|
|
};
|
|
|
|
expect(cacheManager._isDocumentStale(recentMeta)).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ============ Atomic Write Tests ============
|
|
|
|
describe('atomic writes', () => {
|
|
it('should write PRD atomically (no temp files left)', () => {
|
|
cacheManager.writePrd('user-auth', '# Content', { status: 'draft' });
|
|
|
|
const prdPath = cacheManager.getPrdPath('user-auth');
|
|
const tempPath = `${prdPath}.tmp`;
|
|
|
|
expect(fs.existsSync(prdPath)).toBe(true);
|
|
expect(fs.existsSync(tempPath)).toBe(false);
|
|
});
|
|
|
|
it('should write Epic atomically (no temp files left)', () => {
|
|
cacheManager.writeEpic('2', '# Content', { status: 'draft' });
|
|
|
|
const epicPath = cacheManager.getEpicPath('2');
|
|
const tempPath = `${epicPath}.tmp`;
|
|
|
|
expect(fs.existsSync(epicPath)).toBe(true);
|
|
expect(fs.existsSync(tempPath)).toBe(false);
|
|
});
|
|
|
|
it('should save metadata atomically (no temp files left)', () => {
|
|
cacheManager.writePrd('user-auth', '# Content', { status: 'draft' });
|
|
|
|
const metaPath = path.join(testCacheDir, CACHE_META_FILENAME);
|
|
const tempPath = `${metaPath}.tmp`;
|
|
|
|
expect(fs.existsSync(metaPath)).toBe(true);
|
|
expect(fs.existsSync(tempPath)).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ============ Edge Cases ============
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle empty stakeholder arrays', () => {
|
|
cacheManager.writePrd('user-auth', '# PRD', {
|
|
status: 'feedback',
|
|
stakeholders: []
|
|
});
|
|
|
|
const tasks = cacheManager.getPrdsNeedingAttention('alice');
|
|
|
|
expect(tasks.pendingFeedback).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle missing stakeholders property', () => {
|
|
cacheManager.writePrd('user-auth', '# PRD', { status: 'feedback' });
|
|
|
|
const tasks = cacheManager.getPrdsNeedingAttention('alice');
|
|
|
|
expect(tasks.pendingFeedback).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle PRDs with no status', () => {
|
|
cacheManager.writePrd('user-auth', '# PRD', { version: 1 });
|
|
|
|
// Status defaults to 'draft' in writePrd
|
|
const feedbackPrds = cacheManager.getPrdsByStatus('feedback');
|
|
expect(feedbackPrds).toHaveLength(0);
|
|
|
|
const draftPrds = cacheManager.getPrdsByStatus('draft');
|
|
expect(draftPrds).toHaveLength(1);
|
|
});
|
|
|
|
it('should handle special characters in content', () => {
|
|
const content = '# PRD: Auth\n\n## Special chars: "quotes", <tags>, & ampersands';
|
|
cacheManager.writePrd('user-auth', content, { status: 'draft' });
|
|
|
|
const result = cacheManager.readPrd('user-auth');
|
|
expect(result.content).toBe(content);
|
|
});
|
|
|
|
it('should handle unicode content', () => {
|
|
const content = '# PRD: 认证系统\n\nUnicode: 日本語, 한국어, emoji 🚀';
|
|
cacheManager.writePrd('unicode-prd', content, { status: 'draft' });
|
|
|
|
const result = cacheManager.readPrd('unicode-prd');
|
|
expect(result.content).toBe(content);
|
|
});
|
|
|
|
it('should handle concurrent writes to different PRDs', () => {
|
|
// Write multiple PRDs in sequence (simulating concurrent writes)
|
|
for (let i = 0; i < 10; i++) {
|
|
cacheManager.writePrd(`prd-${i}`, `# PRD ${i}`, { status: 'draft' });
|
|
}
|
|
|
|
const prds = cacheManager.listCachedPrds();
|
|
expect(prds.length).toBe(10);
|
|
|
|
// Verify all are readable
|
|
for (let i = 0; i < 10; i++) {
|
|
const result = cacheManager.readPrd(`prd-${i}`);
|
|
expect(result.content).toBe(`# PRD ${i}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============ Content Hash Tests ============
|
|
|
|
describe('content hashing', () => {
|
|
it('should generate consistent hash for same content', () => {
|
|
const content = '# PRD Content';
|
|
const result1 = cacheManager.writePrd('prd-1', content, { status: 'draft' });
|
|
const result2 = cacheManager.writePrd('prd-2', content, { status: 'draft' });
|
|
|
|
expect(result1.hash).toBe(result2.hash);
|
|
});
|
|
|
|
it('should generate different hash for different content', () => {
|
|
const result1 = cacheManager.writePrd('prd-1', '# Content A', { status: 'draft' });
|
|
const result2 = cacheManager.writePrd('prd-2', '# Content B', { status: 'draft' });
|
|
|
|
expect(result1.hash).not.toBe(result2.hash);
|
|
});
|
|
|
|
it('should detect content changes via hasContentChanged (story method)', () => {
|
|
cacheManager.writeStory('story-1', '# Original', { github_issue: 10 });
|
|
|
|
expect(cacheManager.hasContentChanged('story-1', '# Original')).toBe(false);
|
|
expect(cacheManager.hasContentChanged('story-1', '# Modified')).toBe(true);
|
|
});
|
|
});
|
|
});
|