/**
* Tests for weekly report generator
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { generateWeeklyReport, formatWeeklyReport } from '../reportGenerator.js';
import {
complexNote,
bugFixNote,
refactoringNote,
testingNote,
documentationNote,
} from '../../__fixtures__/sessionNotes.js';
import type { SessionNote } from '../../types/session.js';
describe('generateWeeklyReport', () => {
describe('week boundary calculation', () => {
it('calculates week starting Monday', () => {
// 2024-01-17 was a Wednesday
const wednesday = new Date('2024-01-17T12:00:00Z');
const notes: SessionNote[] = [
{
summary: 'Monday work',
timestamp: '2024-01-15T10:00:00Z', // Monday of that week
},
{
summary: 'Sunday work',
timestamp: '2024-01-21T10:00:00Z', // Sunday of that week
},
];
const report = generateWeeklyReport(notes, wednesday);
expect(report.sessions).toHaveLength(2);
});
it('excludes notes before week start', () => {
const wednesday = new Date('2024-01-17T12:00:00Z');
const notes: SessionNote[] = [
{
summary: 'Previous week',
timestamp: '2024-01-14T10:00:00Z', // Sunday of previous week
},
{
summary: 'This week',
timestamp: '2024-01-15T10:00:00Z', // Monday of this week
},
];
const report = generateWeeklyReport(notes, wednesday);
expect(report.sessions).toHaveLength(1);
expect(report.sessions[0].summary).toBe('This week');
});
it('excludes notes after week end', () => {
const wednesday = new Date('2024-01-17T12:00:00Z');
const notes: SessionNote[] = [
{
summary: 'This week',
timestamp: '2024-01-20T10:00:00Z', // Saturday
},
{
summary: 'Next week',
timestamp: '2024-01-22T10:00:00Z', // Monday of next week
},
];
const report = generateWeeklyReport(notes, wednesday);
expect(report.sessions).toHaveLength(1);
expect(report.sessions[0].summary).toBe('This week');
});
it('uses current date when week not specified', () => {
const now = new Date();
const notes: SessionNote[] = [
{
summary: 'Recent work',
timestamp: now.toISOString(),
},
];
const report = generateWeeklyReport(notes);
expect(report.sessions).toHaveLength(1);
});
it('handles Sunday as start date (should go back to previous Monday)', () => {
// 2024-01-21 was a Sunday
const sunday = new Date('2024-01-21T12:00:00Z');
const notes: SessionNote[] = [
{
summary: 'Monday work',
timestamp: '2024-01-15T10:00:00Z',
},
];
const report = generateWeeklyReport(notes, sunday);
// Should include Monday 1/15 in the week
expect(report.sessions).toHaveLength(1);
});
});
describe('statistics calculation', () => {
it('counts total sessions', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [
{ summary: 'Work 1', timestamp: '2024-01-15T10:00:00Z' },
{ summary: 'Work 2', timestamp: '2024-01-16T10:00:00Z' },
{ summary: 'Work 3', timestamp: '2024-01-17T10:00:00Z' },
];
const report = generateWeeklyReport(notes, monday);
expect(report.totalSessions).toBe(3);
});
it('counts total files changed', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [
{
summary: 'Work 1',
timestamp: '2024-01-15T10:00:00Z',
fileChanges: [
{ path: 'a.ts', type: 'modified' },
{ path: 'b.ts', type: 'created' },
],
},
{
summary: 'Work 2',
timestamp: '2024-01-16T10:00:00Z',
fileChanges: [{ path: 'c.ts', type: 'modified' }],
},
];
const report = generateWeeklyReport(notes, monday);
expect(report.totalFiles).toBe(3);
});
it('counts total commands executed', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [
{
summary: 'Work 1',
timestamp: '2024-01-15T10:00:00Z',
commands: [
{ command: 'npm install' },
{ command: 'npm build' },
],
},
{
summary: 'Work 2',
timestamp: '2024-01-16T10:00:00Z',
commands: [{ command: 'npm test' }],
},
];
const report = generateWeeklyReport(notes, monday);
expect(report.totalCommands).toBe(3);
});
it('handles notes without file changes', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [
{ summary: 'Work 1', timestamp: '2024-01-15T10:00:00Z' },
];
const report = generateWeeklyReport(notes, monday);
expect(report.totalFiles).toBe(0);
});
it('handles notes without commands', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [
{ summary: 'Work 1', timestamp: '2024-01-15T10:00:00Z' },
];
const report = generateWeeklyReport(notes, monday);
expect(report.totalCommands).toBe(0);
});
});
describe('project breakdown', () => {
it('counts sessions by project', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [
{ summary: 'Work', timestamp: '2024-01-15T10:00:00Z', projectName: 'app-a' },
{ summary: 'Work', timestamp: '2024-01-16T10:00:00Z', projectName: 'app-a' },
{ summary: 'Work', timestamp: '2024-01-17T10:00:00Z', projectName: 'app-b' },
];
const report = generateWeeklyReport(notes, monday);
expect(report.projectBreakdown['app-a']).toBe(2);
expect(report.projectBreakdown['app-b']).toBe(1);
});
it('uses "Unknown" for notes without project', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [
{ summary: 'Work', timestamp: '2024-01-15T10:00:00Z' },
];
const report = generateWeeklyReport(notes, monday);
expect(report.projectBreakdown['Unknown']).toBe(1);
});
});
describe('pattern breakdown', () => {
it('counts sessions by pattern', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [
{ ...bugFixNote, timestamp: '2024-01-15T10:00:00Z' },
{ ...complexNote, timestamp: '2024-01-16T10:00:00Z' },
{ ...refactoringNote, timestamp: '2024-01-17T10:00:00Z' },
];
const report = generateWeeklyReport(notes, monday);
expect(report.patternBreakdown['bug-fix']).toBe(1);
expect(report.patternBreakdown['new-feature']).toBe(1);
expect(report.patternBreakdown['refactoring']).toBe(1);
});
it('initializes all patterns to 0', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [];
const report = generateWeeklyReport(notes, monday);
expect(report.patternBreakdown['new-feature']).toBe(0);
expect(report.patternBreakdown['bug-fix']).toBe(0);
expect(report.patternBreakdown['refactoring']).toBe(0);
expect(report.patternBreakdown['documentation']).toBe(0);
expect(report.patternBreakdown['configuration']).toBe(0);
expect(report.patternBreakdown['testing']).toBe(0);
expect(report.patternBreakdown['mixed']).toBe(0);
});
});
describe('complexity breakdown', () => {
it('counts sessions by complexity', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [
{
summary: 'Simple',
timestamp: '2024-01-15T10:00:00Z',
analysis: {
pattern: 'bug-fix',
patternConfidence: 90,
complexity: 'simple',
fileCount: 20,
keyFiles: [],
},
},
{
summary: 'Moderate',
timestamp: '2024-01-16T10:00:00Z',
analysis: {
pattern: 'new-feature',
patternConfidence: 85,
complexity: 'moderate',
fileCount: 50,
keyFiles: [],
},
},
{
summary: 'Complex',
timestamp: '2024-01-17T10:00:00Z',
analysis: {
pattern: 'refactoring',
patternConfidence: 80,
complexity: 'complex',
fileCount: 75,
keyFiles: [],
},
},
];
const report = generateWeeklyReport(notes, monday);
expect(report.complexityBreakdown['simple']).toBe(1);
expect(report.complexityBreakdown['moderate']).toBe(1);
expect(report.complexityBreakdown['complex']).toBe(1);
});
it('initializes all complexity levels to 0', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [];
const report = generateWeeklyReport(notes, monday);
expect(report.complexityBreakdown['simple']).toBe(0);
expect(report.complexityBreakdown['moderate']).toBe(0);
expect(report.complexityBreakdown['complex']).toBe(0);
});
});
describe('date range', () => {
it('includes start and end dates', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [];
const report = generateWeeklyReport(notes, monday);
expect(report.startDate).toBeTruthy();
expect(report.endDate).toBeTruthy();
expect(new Date(report.startDate).getTime()).toBeLessThan(new Date(report.endDate).getTime());
});
it('spans exactly 7 days', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [];
const report = generateWeeklyReport(notes, monday);
const start = new Date(report.startDate);
const end = new Date(report.endDate);
const diffDays = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
expect(diffDays).toBe(7);
});
});
describe('edge cases', () => {
it('handles empty notes array', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const report = generateWeeklyReport([], monday);
expect(report.totalSessions).toBe(0);
expect(report.totalFiles).toBe(0);
expect(report.totalCommands).toBe(0);
expect(report.sessions).toEqual([]);
});
it('handles notes without analysis', () => {
const monday = new Date('2024-01-15T00:00:00Z');
const notes: SessionNote[] = [
{ summary: 'Work', timestamp: '2024-01-15T10:00:00Z' },
];
const report = generateWeeklyReport(notes, monday);
expect(report.totalSessions).toBe(1);
});
});
});
describe('formatWeeklyReport', () => {
const mockReport = {
startDate: '2024-01-15T00:00:00Z',
endDate: '2024-01-22T00:00:00Z',
totalSessions: 5,
totalFiles: 20,
totalCommands: 15,
projectBreakdown: {
'my-app': 3,
'other-app': 2,
},
patternBreakdown: {
'new-feature': 2,
'bug-fix': 2,
'refactoring': 1,
'documentation': 0,
'configuration': 0,
'testing': 0,
'mixed': 0,
},
complexityBreakdown: {
'simple': 2,
'moderate': 2,
'complex': 1,
},
sessions: [
{ ...complexNote, timestamp: '2024-01-20T14:45:00Z' },
{ ...bugFixNote, timestamp: '2024-01-18T09:15:00Z' },
],
};
describe('structure', () => {
it('includes title', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toContain('# Weekly Coding Report');
});
it('includes date range', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toContain('##');
expect(markdown).toMatch(/\d+\/\d+\/\d+/); // Date format
});
it('includes summary section', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toContain('## Summary');
expect(markdown).toContain('**Total Sessions**: 5');
expect(markdown).toContain('**Files Changed**: 20');
expect(markdown).toContain('**Commands Executed**: 15');
});
it('includes projects section', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toContain('## Projects');
expect(markdown).toContain('**my-app**: 3 sessions');
expect(markdown).toContain('**other-app**: 2 sessions');
});
it('includes work patterns section', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toContain('## Work Patterns');
expect(markdown).toMatch(/New Feature.*2 sessions/);
expect(markdown).toMatch(/Bug Fix.*2 sessions/);
});
it('includes complexity distribution', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toContain('## Complexity Distribution');
expect(markdown).toMatch(/Simple.*2 sessions/);
expect(markdown).toMatch(/Moderate.*2 sessions/);
expect(markdown).toMatch(/Complex.*1 sessions/);
});
it('includes sessions list', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toContain('## Sessions This Week');
expect(markdown).toContain('my-app');
});
});
describe('project section', () => {
it('sorts projects by session count', () => {
const markdown = formatWeeklyReport(mockReport);
const myAppIndex = markdown.indexOf('**my-app**');
const otherAppIndex = markdown.indexOf('**other-app**');
expect(myAppIndex).toBeLessThan(otherAppIndex);
});
it('omits projects section when empty', () => {
const emptyReport = {
...mockReport,
projectBreakdown: {},
};
const markdown = formatWeeklyReport(emptyReport);
expect(markdown).not.toContain('## Projects');
});
});
describe('work patterns', () => {
it('includes emojis for patterns', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toMatch(/🆕.*New Feature/);
expect(markdown).toMatch(/🐛.*Bug Fix/);
expect(markdown).toMatch(/♻️.*Refactoring/);
});
it('omits patterns with zero sessions', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).not.toContain('Documentation: 0');
expect(markdown).not.toContain('Configuration: 0');
});
it('sorts patterns by count', () => {
const report = {
...mockReport,
patternBreakdown: {
'new-feature': 5,
'bug-fix': 1,
'refactoring': 0,
'documentation': 0,
'configuration': 0,
'testing': 0,
'mixed': 0,
},
};
const markdown = formatWeeklyReport(report);
const newFeatureIndex = markdown.indexOf('New Feature');
const bugFixIndex = markdown.indexOf('Bug Fix');
expect(newFeatureIndex).toBeLessThan(bugFixIndex);
});
});
describe('complexity distribution', () => {
it('includes emojis for complexity levels', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toMatch(/🟢.*Simple/);
expect(markdown).toMatch(/🟡.*Moderate/);
expect(markdown).toMatch(/🔴.*Complex/);
});
it('capitalizes complexity levels', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toContain('Simple');
expect(markdown).toContain('Moderate');
expect(markdown).toContain('Complex');
});
it('omits levels with zero sessions', () => {
const report = {
...mockReport,
complexityBreakdown: {
'simple': 3,
'moderate': 0,
'complex': 0,
},
};
const markdown = formatWeeklyReport(report);
expect(markdown).toContain('Simple');
expect(markdown).not.toContain('Moderate: 0');
});
});
describe('sessions list', () => {
it('numbers sessions sequentially', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toMatch(/1\./);
expect(markdown).toMatch(/2\./);
});
it('includes project name', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toContain('**my-app**');
});
it('includes date and time', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toMatch(/Jan \d+, \d{2}:\d{2}/);
});
it('includes summary indented', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toMatch(/\n [A-Z]/); // Indented text
});
it('uses topic as session title when available', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toContain('OAuth2 Authentication');
});
it('falls back to summary when no topic', () => {
const report = {
...mockReport,
sessions: [
{
summary: 'Work on feature',
timestamp: '2024-01-20T10:00:00Z',
projectName: 'my-app',
},
],
};
const markdown = formatWeeklyReport(report);
expect(markdown).toMatch(/Implemented new authentication feature|Work on feature/);
});
it('truncates long summaries as title', () => {
const report = {
...mockReport,
sessions: [
{
summary: 'A very long summary that should be truncated '.repeat(5),
timestamp: '2024-01-20T10:00:00Z',
projectName: 'my-app',
},
],
};
const markdown = formatWeeklyReport(report);
expect(markdown).toContain('...');
});
it('uses "General" for notes without project', () => {
const report = {
...mockReport,
sessions: [
{
summary: 'Work',
timestamp: '2024-01-20T10:00:00Z',
},
],
};
const markdown = formatWeeklyReport(report);
expect(markdown).toContain('**General**');
});
});
describe('markdown validity', () => {
it('produces valid markdown', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toBeTruthy();
expect(markdown.length).toBeGreaterThan(100);
});
it('has proper heading hierarchy', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toMatch(/^# /m);
expect(markdown).toMatch(/^## /m);
});
it('has proper list formatting', () => {
const markdown = formatWeeklyReport(mockReport);
expect(markdown).toMatch(/^- /m);
});
});
});