@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
407 lines (339 loc) • 13.4 kB
text/typescript
import { describe, it, expect } from 'vitest';
import {
getNygardTemplate,
getMADRTemplate,
getYStatementTemplate,
renderADRTemplate,
getTemplateFields,
validateTemplateData
} from '../templates.js';
import type { ADR, ADRTemplate } from '../types.js';
describe('ADR Templates', () => {
const baseADR: ADR = {
id: 'ADR-0001',
title: 'Use React for Frontend Framework',
status: 'accepted',
date: new Date('2024-01-15'),
deciders: ['John Doe', 'Jane Smith'],
template: 'nygard',
context: 'We need to choose a frontend framework for our new web application.',
decision: 'We will use React as our frontend framework.',
consequences: 'The team will need to learn React. We will have access to a large ecosystem.',
createdAt: new Date('2024-01-15'),
updatedAt: new Date('2024-01-15'),
statusHistory: []
};
describe('getNygardTemplate', () => {
it('should return Nygard template structure', () => {
const template = getNygardTemplate();
expect(template.name).toBe('nygard');
expect(template.description).toContain('Michael Nygard');
expect(template.fields).toContain('title');
expect(template.fields).toContain('status');
expect(template.fields).toContain('context');
expect(template.fields).toContain('decision');
expect(template.fields).toContain('consequences');
});
it('should include required and optional fields', () => {
const template = getNygardTemplate();
expect(template.requiredFields).toContain('title');
expect(template.requiredFields).toContain('context');
expect(template.requiredFields).toContain('decision');
expect(template.optionalFields).toContain('tags');
expect(template.optionalFields).toContain('relatedTo');
});
});
describe('getMADRTemplate', () => {
it('should return MADR template structure', () => {
const template = getMADRTemplate();
expect(template.name).toBe('madr');
expect(template.description).toContain('Markdown Architectural Decision Records');
expect(template.fields).toContain('decisionDrivers');
expect(template.fields).toContain('consideredOptions');
});
it('should include MADR-specific fields', () => {
const template = getMADRTemplate();
expect(template.requiredFields).toContain('decisionDrivers');
expect(template.fields).toContain('prosAndCons');
expect(template.fields).toContain('links');
});
});
describe('getYStatementTemplate', () => {
it('should return Y-statement template structure', () => {
const template = getYStatementTemplate();
expect(template.name).toBe('y-statement');
expect(template.description).toContain('simple format');
expect(template.fields).toHaveLength(8); // Fewer fields than other templates
});
it('should have minimal required fields', () => {
const template = getYStatementTemplate();
expect(template.requiredFields).toContain('title');
expect(template.requiredFields).toContain('context');
expect(template.requiredFields).toContain('decision');
expect(template.requiredFields).toContain('consequences');
expect(template.requiredFields).not.toContain('decisionDrivers');
});
});
describe('renderADRTemplate', () => {
it('should render Nygard template correctly', () => {
const rendered = renderADRTemplate(baseADR);
expect(rendered).toContain('# ADR-0001: Use React for Frontend Framework');
expect(rendered).toContain('## Status');
expect(rendered).toContain('Accepted');
expect(rendered).toContain('## Context');
expect(rendered).toContain(baseADR.context);
expect(rendered).toContain('## Decision');
expect(rendered).toContain(baseADR.decision);
expect(rendered).toContain('## Consequences');
expect(rendered).toContain(baseADR.consequences);
});
it('should render MADR template with additional fields', () => {
const madrADR: ADR = {
...baseADR,
template: 'madr',
decisionDrivers: ['Performance', 'Developer Experience', 'Community Support'],
consideredOptions: [
{
title: 'React',
description: 'A JavaScript library for building user interfaces',
pros: ['Large ecosystem', 'Good performance', 'Wide adoption'],
cons: ['Learning curve', 'Requires build tooling']
},
{
title: 'Vue',
description: 'The Progressive JavaScript Framework',
pros: ['Easy to learn', 'Good documentation', 'Flexible'],
cons: ['Smaller ecosystem', 'Less job market demand']
}
]
};
const rendered = renderADRTemplate(madrADR);
expect(rendered).toContain('## Decision Drivers');
expect(rendered).toContain('* Performance');
expect(rendered).toContain('* Developer Experience');
expect(rendered).toContain('## Considered Options');
expect(rendered).toContain('* React - A JavaScript library');
expect(rendered).toContain('* Vue - The Progressive JavaScript Framework');
});
it('should render Y-statement template simply', () => {
const yStatementADR: ADR = {
...baseADR,
template: 'y-statement'
};
const rendered = renderADRTemplate(yStatementADR);
expect(rendered).toContain('# ADR-0001: Use React for Frontend Framework');
expect(rendered).toContain('## Status: Accepted');
expect(rendered).toContain(baseADR.context);
expect(rendered).toContain(baseADR.decision);
expect(rendered).toContain(baseADR.consequences);
expect(rendered).not.toContain('## Decision Drivers'); // Y-statement doesn't have this
});
it('should include metadata section', () => {
const rendered = renderADRTemplate(baseADR);
expect(rendered).toContain('**Date**: 2024-01-15');
expect(rendered).toContain('**Deciders**: John Doe, Jane Smith');
});
it('should include status history if present', () => {
const adrWithHistory: ADR = {
...baseADR,
statusHistory: [
{
from: 'proposed',
to: 'accepted',
date: new Date('2024-01-20'),
changedBy: 'John Doe',
reason: 'Approved by architecture board'
}
]
};
const rendered = renderADRTemplate(adrWithHistory);
expect(rendered).toContain('## Status History');
expect(rendered).toContain('proposed → accepted');
expect(rendered).toContain('2024-01-20');
expect(rendered).toContain('John Doe');
expect(rendered).toContain('Approved by architecture board');
});
it('should include tags if present', () => {
const adrWithTags: ADR = {
...baseADR,
tags: ['frontend', 'react', 'framework']
};
const rendered = renderADRTemplate(adrWithTags);
expect(rendered).toContain('**Tags**: frontend, react, framework');
});
it('should include relationships if present', () => {
const adrWithRelations: ADR = {
...baseADR,
supersedes: ['ADR-0000'],
relatedTo: ['ADR-0002', 'ADR-0003']
};
const rendered = renderADRTemplate(adrWithRelations);
expect(rendered).toContain('## Links');
expect(rendered).toContain('* Supersedes: ADR-0000');
expect(rendered).toContain('* Related to: ADR-0002, ADR-0003');
});
it('should render options with pros and cons for MADR', () => {
const madrADR: ADR = {
...baseADR,
template: 'madr',
consideredOptions: [
{
title: 'React',
description: 'Facebook\'s library',
pros: ['Popular', 'Fast'],
cons: ['Complex']
}
],
prosAndCons: [
{
option: 'React',
pros: ['Great ecosystem'],
cons: ['Steep learning curve']
}
]
};
const rendered = renderADRTemplate(madrADR);
expect(rendered).toContain('### React');
expect(rendered).toContain('Facebook\'s library');
expect(rendered).toContain('* Good, because Popular');
expect(rendered).toContain('* Bad, because Complex');
});
it('should format dates consistently', () => {
const rendered = renderADRTemplate(baseADR);
// Should use ISO date format
expect(rendered).toMatch(/\*\*Date\*\*: \d{4}-\d{2}-\d{2}/);
});
});
describe('getTemplateFields', () => {
it('should return fields for Nygard template', () => {
const fields = getTemplateFields('nygard');
expect(fields.required).toContain('title');
expect(fields.required).toContain('context');
expect(fields.required).toContain('decision');
expect(fields.required).toContain('consequences');
expect(fields.optional).toContain('tags');
});
it('should return fields for MADR template', () => {
const fields = getTemplateFields('madr');
expect(fields.required).toContain('decisionDrivers');
expect(fields.optional).toContain('consideredOptions');
expect(fields.optional).toContain('prosAndCons');
});
it('should return fields for Y-statement template', () => {
const fields = getTemplateFields('y-statement');
expect(fields.required).toContain('title');
expect(fields.required).toContain('context');
expect(fields.required).toContain('decision');
expect(fields.optional).toContain('tags');
});
it('should throw error for invalid template', () => {
expect(() => getTemplateFields('invalid' as ADRTemplate))
.toThrow('Unknown template: invalid');
});
});
describe('validateTemplateData', () => {
it('should validate Nygard template data', () => {
const data = {
title: 'Test ADR',
context: 'Test context',
decision: 'Test decision',
consequences: 'Test consequences',
deciders: ['John']
};
const result = validateTemplateData('nygard', data);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should catch missing required fields', () => {
const data = {
title: 'Test ADR',
// Missing context
decision: 'Test decision',
consequences: 'Test consequences'
};
const result = validateTemplateData('nygard', data);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Missing required field: context');
});
it('should validate MADR specific fields', () => {
const data = {
title: 'Test ADR',
context: 'Test context',
decision: 'Test decision',
consequences: 'Test consequences',
deciders: ['John'],
decisionDrivers: ['Performance', 'Cost']
};
const result = validateTemplateData('madr', data);
expect(result.valid).toBe(true);
});
it('should validate array fields', () => {
const data = {
title: 'Test ADR',
context: 'Test context',
decision: 'Test decision',
consequences: 'Test consequences',
deciders: 'John' // Should be array
};
const result = validateTemplateData('nygard', data);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Field "deciders" must be an array');
});
it('should validate consideredOptions structure', () => {
const data = {
title: 'Test ADR',
context: 'Test context',
decision: 'Test decision',
consequences: 'Test consequences',
deciders: ['John'],
decisionDrivers: ['Performance'],
consideredOptions: [
{
title: 'Option 1',
// Missing description
pros: ['Good'],
cons: ['Bad']
}
]
};
const result = validateTemplateData('madr', data);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Option must have title, description, pros, and cons');
});
it('should allow optional fields to be missing', () => {
const data = {
title: 'Test ADR',
context: 'Test context',
decision: 'Test decision',
consequences: 'Test consequences',
deciders: ['John']
// No tags, relatedTo, etc.
};
const result = validateTemplateData('nygard', data);
expect(result.valid).toBe(true);
});
it('should validate Y-statement minimal requirements', () => {
const data = {
title: 'Simple Decision',
context: 'We need to decide',
decision: 'We choose this',
consequences: 'This will happen',
deciders: ['Team']
};
const result = validateTemplateData('y-statement', data);
expect(result.valid).toBe(true);
});
it('should validate empty arrays', () => {
const data = {
title: 'Test ADR',
context: 'Test context',
decision: 'Test decision',
consequences: 'Test consequences',
deciders: [] // Empty array
};
const result = validateTemplateData('nygard', data);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Field "deciders" cannot be empty');
});
});
});