Skip to main content
Glama
analysis-run.aggregate.test.ts21.8 kB
/** * @fileoverview Tests for AnalysisRun aggregate */ import { describe, it, expect, beforeEach } from 'vitest'; import { AnalysisRun } from '../analysis-run.aggregate.js'; import type { CreateAnalysisRunParams, IssueOccurrence, RunSummary, CommitInfo, RunTimestamps, RunStatus, } from '../analysis-run.types.js'; import type { RunId, ProjectKey, GraphQLNodeId, CommitOid, BranchName, AnalyzerShortcode, } from '../../../../types/branded.js'; import { IssueCount } from '../../../value-objects/issue-count.js'; describe('AnalysisRun Aggregate', () => { let validParams: CreateAnalysisRunParams; let runId: RunId; let projectKey: ProjectKey; let repositoryId: GraphQLNodeId; let commitInfo: CommitInfo; beforeEach(() => { runId = 'run-123' as RunId; projectKey = 'test-project' as ProjectKey; repositoryId = 'repo-456' as GraphQLNodeId; commitInfo = { oid: 'abc123' as CommitOid, branch: 'main' as BranchName, baseOid: 'def456' as CommitOid, message: 'Test commit', author: 'Test Author', authoredAt: new Date('2024-01-01'), }; validParams = { runId, projectKey, repositoryId, commitInfo, }; }); const createTestIssue = (id: string): IssueOccurrence => ({ id, issueCode: 'TEST001', analyzerShortcode: 'test-analyzer' as AnalyzerShortcode, category: 'BUG_RISK', severity: 'MAJOR', message: 'Test issue', path: '/test/file.ts', line: 10, column: 5, }); describe('create', () => { it('should create an analysis run with valid parameters', () => { const run = AnalysisRun.create(validParams); expect(run.runId).toBe(runId); expect(run.projectKey).toBe(projectKey); expect(run.repositoryId).toBe(repositoryId); expect(run.commitInfo).toEqual(commitInfo); expect(run.status).toBe('PENDING'); expect(run.issues).toHaveLength(0); expect(run.domainEvents).toHaveLength(1); expect(run.domainEvents[0].eventType).toBe('AnalysisRunCreated'); }); it('should create empty summary', () => { const run = AnalysisRun.create(validParams); const summary = run.summary; expect(summary.totalIntroduced.count).toBe(0); expect(summary.totalResolved.count).toBe(0); expect(summary.totalSuppressed.count).toBe(0); expect(summary.byAnalyzer).toHaveLength(0); expect(summary.byCategory).toHaveLength(0); }); it('should set createdAt timestamp', () => { const before = new Date(); const run = AnalysisRun.create(validParams); const after = new Date(); const timestamps = run.timestamps; expect(timestamps.createdAt.getTime()).toBeGreaterThanOrEqual(before.getTime()); expect(timestamps.createdAt.getTime()).toBeLessThanOrEqual(after.getTime()); expect(timestamps.startedAt).toBeUndefined(); expect(timestamps.finishedAt).toBeUndefined(); }); it('should create run without optional commit info', () => { const minimalCommitInfo: CommitInfo = { oid: 'abc123' as CommitOid, branch: 'main' as BranchName, baseOid: 'def456' as CommitOid, }; const params = { ...validParams, commitInfo: minimalCommitInfo, }; const run = AnalysisRun.create(params); expect(run.commitInfo.message).toBeUndefined(); expect(run.commitInfo.author).toBeUndefined(); expect(run.commitInfo.authoredAt).toBeUndefined(); }); it('should emit AnalysisRunCreated event with correct payload', () => { const run = AnalysisRun.create(validParams); const events = run.domainEvents; expect(events).toHaveLength(1); const event = events[0]; expect(event.eventType).toBe('AnalysisRunCreated'); expect(event.aggregateId).toBe(runId); expect(event.payload).toEqual({ projectKey, commitOid: commitInfo.oid, branch: commitInfo.branch, }); }); }); describe('fromPersistence', () => { it('should recreate run from persistence without events', () => { const issues = [createTestIssue('issue-1'), createTestIssue('issue-2')]; const summary: RunSummary = { totalIntroduced: IssueCount.create(2), totalResolved: IssueCount.create(0), totalSuppressed: IssueCount.create(0), byAnalyzer: [ { analyzerShortcode: 'test-analyzer' as AnalyzerShortcode, introduced: IssueCount.create(2), resolved: IssueCount.create(0), suppressed: IssueCount.create(0), }, ], byCategory: [ { category: 'BUG_RISK', introduced: IssueCount.create(2), resolved: IssueCount.create(0), suppressed: IssueCount.create(0), }, ], }; const timestamps: RunTimestamps = { createdAt: new Date('2024-01-01'), startedAt: new Date('2024-01-01T00:01:00Z'), finishedAt: new Date('2024-01-01T00:05:00Z'), }; const persistenceData = { runId, projectKey, repositoryId, commitInfo, status: 'SUCCESS' as const, timestamps, issues, summary, }; const run = AnalysisRun.fromPersistence(persistenceData); expect(run.runId).toBe(runId); expect(run.projectKey).toBe(projectKey); expect(run.status).toBe('SUCCESS'); expect(run.issues).toHaveLength(2); expect(run.domainEvents).toHaveLength(0); // No events when loading expect(run.timestamps).toEqual(timestamps); expect(run.summary.totalIntroduced.count).toBe(2); }); }); describe('status transitions', () => { describe('start', () => { it('should transition from PENDING to RUNNING', () => { const run = AnalysisRun.create(validParams); run.clearEvents(); const beforeStart = run.timestamps.startedAt; expect(beforeStart).toBeUndefined(); run.start(); expect(run.status).toBe('RUNNING'); expect(run.timestamps.startedAt).toBeDefined(); expect(run.domainEvents).toHaveLength(2); // AnalysisRunStarted + AggregateModified expect(run.domainEvents[0].eventType).toBe('AnalysisRunStarted'); }); it('should throw error when starting from invalid status', () => { const run = AnalysisRun.create(validParams); run.start(); // Try to start again expect(() => run.start()).toThrow('Invalid status transition from RUNNING to RUNNING'); }); }); describe('complete', () => { it('should transition from RUNNING to SUCCESS', () => { const run = AnalysisRun.create(validParams); run.start(); run.clearEvents(); run.complete(); expect(run.status).toBe('SUCCESS'); expect(run.timestamps.finishedAt).toBeDefined(); expect(run.isFinished).toBe(true); expect(run.canModify).toBe(false); expect(run.domainEvents[0].eventType).toBe('AnalysisRunCompleted'); expect(run.domainEvents[0].payload).toMatchObject({ summary: expect.objectContaining({ totalIntroduced: expect.any(Object), totalResolved: expect.any(Object), totalSuppressed: expect.any(Object), }), }); }); it('should throw error when completing from invalid status', () => { const run = AnalysisRun.create(validParams); // Try to complete without starting expect(() => run.complete()).toThrow('Invalid status transition from PENDING to SUCCESS'); }); }); describe('fail', () => { it('should transition from RUNNING to FAILURE with reason', () => { const run = AnalysisRun.create(validParams); run.start(); run.clearEvents(); const reason = 'Analysis engine crashed'; run.fail(reason); expect(run.status).toBe('FAILURE'); expect(run.timestamps.finishedAt).toBeDefined(); expect(run.isFinished).toBe(true); expect(run.domainEvents[0].eventType).toBe('AnalysisRunFailed'); expect(run.domainEvents[0].payload).toEqual({ reason }); }); }); describe('timeout', () => { it('should transition from RUNNING to TIMEOUT', () => { const run = AnalysisRun.create(validParams); run.start(); run.clearEvents(); run.timeout(); expect(run.status).toBe('TIMEOUT'); expect(run.timestamps.finishedAt).toBeDefined(); expect(run.isFinished).toBe(true); expect(run.domainEvents[0].eventType).toBe('AnalysisRunTimedOut'); }); }); describe('cancel', () => { it('should transition from PENDING to CANCELLED', () => { const run = AnalysisRun.create(validParams); run.clearEvents(); const reason = 'User requested cancellation'; run.cancel(reason); expect(run.status).toBe('CANCELLED'); expect(run.timestamps.finishedAt).toBeDefined(); expect(run.isFinished).toBe(true); expect(run.domainEvents[0].eventType).toBe('AnalysisRunCancelled'); expect(run.domainEvents[0].payload).toEqual({ reason }); }); it('should transition from RUNNING to CANCELLED', () => { const run = AnalysisRun.create(validParams); run.start(); run.clearEvents(); const reason = 'Timeout exceeded'; run.cancel(reason); expect(run.status).toBe('CANCELLED'); }); }); describe('skip', () => { it('should transition from PENDING to SKIPPED', () => { const run = AnalysisRun.create(validParams); run.clearEvents(); const reason = 'No changes detected'; run.skip(reason); expect(run.status).toBe('SKIPPED'); expect(run.timestamps.finishedAt).toBeDefined(); expect(run.isFinished).toBe(true); expect(run.domainEvents[0].eventType).toBe('AnalysisRunSkipped'); expect(run.domainEvents[0].payload).toEqual({ reason }); }); }); }); describe('issue management', () => { describe('addIssue', () => { it('should add issue to running run', () => { const run = AnalysisRun.create(validParams); run.start(); run.clearEvents(); const issue = createTestIssue('issue-1'); run.addIssue(issue); expect(run.issues).toHaveLength(1); expect(run.issues[0]).toEqual(issue); expect(run.domainEvents[0].eventType).toBe('IssueAdded'); expect(run.domainEvents[0].payload).toEqual({ issueId: issue.id, issueCode: issue.issueCode, }); }); it('should update summary when adding issues', () => { const run = AnalysisRun.create(validParams); run.start(); const issue1 = createTestIssue('issue-1'); const issue2 = { ...createTestIssue('issue-2'), analyzerShortcode: 'another-analyzer' as AnalyzerShortcode, category: 'SECURITY' as const, }; run.addIssue(issue1); run.addIssue(issue2); const summary = run.summary; expect(summary.totalIntroduced.count).toBe(2); expect(summary.byAnalyzer).toHaveLength(2); expect(summary.byCategory).toHaveLength(2); const testAnalyzerDist = summary.byAnalyzer.find( (d) => d.analyzerShortcode === 'test-analyzer' ); expect(testAnalyzerDist?.introduced.count).toBe(1); const securityDist = summary.byCategory.find((d) => d.category === 'SECURITY'); expect(securityDist?.introduced.count).toBe(1); }); it('should not add duplicate issues', () => { const run = AnalysisRun.create(validParams); run.start(); const issue = createTestIssue('issue-1'); run.addIssue(issue); run.clearEvents(); // Try to add the same issue again run.addIssue(issue); expect(run.issues).toHaveLength(1); expect(run.domainEvents).toHaveLength(0); // No event for duplicate }); it('should throw error when adding issue to finished run', () => { const run = AnalysisRun.create(validParams); run.start(); run.complete(); const issue = createTestIssue('issue-1'); expect(() => run.addIssue(issue)).toThrow('Cannot add issues to a finished run'); }); }); describe('removeIssue', () => { it('should remove existing issue', () => { const run = AnalysisRun.create(validParams); run.start(); const issue = createTestIssue('issue-1'); run.addIssue(issue); run.clearEvents(); run.removeIssue(issue.id); expect(run.issues).toHaveLength(0); expect(run.domainEvents[0].eventType).toBe('IssueRemoved'); expect(run.domainEvents[0].payload).toEqual({ issueId: issue.id }); }); it('should update summary when removing issues', () => { const run = AnalysisRun.create(validParams); run.start(); const issue1 = createTestIssue('issue-1'); const issue2 = createTestIssue('issue-2'); run.addIssue(issue1); run.addIssue(issue2); run.removeIssue(issue1.id); const summary = run.summary; expect(summary.totalIntroduced.count).toBe(1); }); it('should not emit event when removing non-existent issue', () => { const run = AnalysisRun.create(validParams); run.start(); run.clearEvents(); run.removeIssue('non-existent'); expect(run.domainEvents).toHaveLength(0); }); it('should throw error when removing issue from finished run', () => { const run = AnalysisRun.create(validParams); run.start(); run.complete(); expect(() => run.removeIssue('any-id')).toThrow('Cannot remove issues from a finished run'); }); }); }); describe('isFinished and canModify', () => { it('should identify finished statuses correctly', () => { const testCases = [ { status: 'PENDING', isFinished: false, canModify: true }, { status: 'RUNNING', isFinished: false, canModify: true }, { status: 'SUCCESS', isFinished: true, canModify: false }, { status: 'FAILURE', isFinished: true, canModify: false }, { status: 'TIMEOUT', isFinished: true, canModify: false }, { status: 'CANCELLED', isFinished: true, canModify: false }, { status: 'SKIPPED', isFinished: true, canModify: false }, ]; testCases.forEach((testCase) => { const persistenceData = { runId, projectKey, repositoryId, commitInfo, status: testCase.status as RunStatus, timestamps: { createdAt: new Date() }, issues: [], summary: { totalIntroduced: IssueCount.create(0), totalResolved: IssueCount.create(0), totalSuppressed: IssueCount.create(0), byAnalyzer: [], byCategory: [], }, }; const run = AnalysisRun.fromPersistence(persistenceData); expect(run.isFinished).toBe(testCase.isFinished); expect(run.canModify).toBe(testCase.canModify); }); }); }); describe('toPersistence', () => { it('should convert to persistence format', () => { const run = AnalysisRun.create(validParams); run.start(); const issue = createTestIssue('issue-1'); run.addIssue(issue); const persistence = run.toPersistence(); expect(persistence).toEqual({ runId, projectKey, repositoryId, commitInfo, status: 'RUNNING', timestamps: expect.objectContaining({ createdAt: expect.any(Date), startedAt: expect.any(Date), }), issues: [issue], summary: expect.objectContaining({ totalIntroduced: expect.objectContaining({ count: 1 }), byAnalyzer: expect.arrayContaining([ expect.objectContaining({ analyzerShortcode: 'test-analyzer' }), ]), byCategory: expect.arrayContaining([expect.objectContaining({ category: 'BUG_RISK' })]), }), }); }); it('should preserve all data through persistence round-trip', () => { const run = AnalysisRun.create(validParams); run.start(); const issue1 = createTestIssue('issue-1'); const issue2 = createTestIssue('issue-2'); run.addIssue(issue1); run.addIssue(issue2); run.complete(); const persistence = run.toPersistence(); const reconstructed = AnalysisRun.fromPersistence(persistence); expect(reconstructed.runId).toBe(run.runId); expect(reconstructed.status).toBe(run.status); expect(reconstructed.issues).toHaveLength(2); expect(reconstructed.summary.totalIntroduced.count).toBe(2); expect(reconstructed.timestamps).toEqual(run.timestamps); }); }); describe('getters', () => { it('should return immutable commit info', () => { const run = AnalysisRun.create(validParams); const info = run.commitInfo; // Verify it's a copy expect(info).not.toBe(commitInfo); expect(info).toEqual(commitInfo); }); it('should return immutable timestamps', () => { const run = AnalysisRun.create(validParams); const timestamps1 = run.timestamps; const timestamps2 = run.timestamps; // Should return new object each time expect(timestamps1).not.toBe(timestamps2); expect(timestamps1).toEqual(timestamps2); }); it('should return issues as array', () => { const run = AnalysisRun.create(validParams); run.start(); const issue1 = createTestIssue('issue-1'); const issue2 = createTestIssue('issue-2'); run.addIssue(issue1); run.addIssue(issue2); const issues = run.issues; expect(Array.isArray(issues)).toBe(true); expect(issues).toHaveLength(2); expect(issues).toContainEqual(issue1); expect(issues).toContainEqual(issue2); }); it('should return immutable summary', () => { const run = AnalysisRun.create(validParams); const summary = run.summary; // Verify arrays are copies const originalAnalyzerLength = summary.byAnalyzer.length; summary.byAnalyzer.push({ analyzerShortcode: 'test' as AnalyzerShortcode, introduced: IssueCount.create(0), resolved: IssueCount.create(0), suppressed: IssueCount.create(0), }); expect(run.summary.byAnalyzer).toHaveLength(originalAnalyzerLength); }); }); describe('domain events', () => { it('should accumulate multiple events', () => { const run = AnalysisRun.create(validParams); run.clearEvents(); run.start(); run.addIssue(createTestIssue('issue-1')); run.complete(); // Each operation emits 2 events (operation + AggregateModified) expect(run.domainEvents).toHaveLength(6); const eventTypes = run.domainEvents.map((e) => e.eventType); expect(eventTypes).toEqual([ 'AnalysisRunStarted', 'AggregateModified', 'IssueAdded', 'AggregateModified', 'AnalysisRunCompleted', 'AggregateModified', ]); }); it('should clear events when requested', () => { const run = AnalysisRun.create(validParams); expect(run.domainEvents).toHaveLength(1); run.clearEvents(); expect(run.domainEvents).toHaveLength(0); }); }); describe('edge cases', () => { it('should handle empty issues correctly', () => { const run = AnalysisRun.create(validParams); run.start(); run.complete(); const summary = run.summary; expect(summary.totalIntroduced.count).toBe(0); expect(summary.byAnalyzer).toHaveLength(0); expect(summary.byCategory).toHaveLength(0); }); it('should handle multiple issues from same analyzer', () => { const run = AnalysisRun.create(validParams); run.start(); const analyzer = 'test-analyzer' as AnalyzerShortcode; run.addIssue({ ...createTestIssue('issue-1'), analyzerShortcode: analyzer }); run.addIssue({ ...createTestIssue('issue-2'), analyzerShortcode: analyzer }); run.addIssue({ ...createTestIssue('issue-3'), analyzerShortcode: analyzer }); const summary = run.summary; const analyzerDist = summary.byAnalyzer.find((d) => d.analyzerShortcode === analyzer); expect(analyzerDist?.introduced.count).toBe(3); }); it('should handle multiple issues from same category', () => { const run = AnalysisRun.create(validParams); run.start(); const category = 'SECURITY' as const; run.addIssue({ ...createTestIssue('issue-1'), category }); run.addIssue({ ...createTestIssue('issue-2'), category }); const summary = run.summary; const categoryDist = summary.byCategory.find((d) => d.category === category); expect(categoryDist?.introduced.count).toBe(2); }); it('should handle all issue categories', () => { const run = AnalysisRun.create(validParams); run.start(); const categories = [ 'BUG_RISK', 'SECURITY', 'STYLE', 'PERFORMANCE', 'DOCUMENTATION', 'COVERAGE', 'COMPLEXITY', ] as const; categories.forEach((category, index) => { run.addIssue({ ...createTestIssue(`issue-${index}`), category }); }); const summary = run.summary; expect(summary.byCategory).toHaveLength(7); expect(summary.totalIntroduced.count).toBe(7); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sapientpants/deepsource-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server