first commit

This commit is contained in:
2026-10-03 13:40:26 +02:00
parent 732a5b5a16
commit 566f415fa7
555 changed files with 62704 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
import {
BusinessRuleError,
ValidationError,
} from '@task-stream/api/shared/domain';
import { IssueCreatedEvent } from '../events/issue-created.event';
import { IssueStatusChangedEvent } from '../events/issue-status-changed.event';
import { IssueFactory } from '../factories/issue.factory';
import { replay } from '../testing';
import { Issue } from './issue';
import { IssueLinkType, IssueStatus, IssueType } from './issue.enums';
const factory = new IssueFactory();
const newIssue = () =>
factory.create({
projectId: 'p-1',
projectKey: 'TS',
number: 7,
title: ' Login button misaligned ',
type: IssueType.UI_CHANGE,
reporterId: 'u-qa',
figmaUrl: 'https://www.figma.com/file/abc',
labels: ['UI', 'ui ', 'login'],
});
const moveTo = (issue: Issue, ...statuses: IssueStatus[]) =>
statuses.forEach((s) => issue.changeStatus('u-dev', s, 'because'));
describe('Issue', () => {
it('is created in BACKLOG with a project-scoped key and a creation event', () => {
const issue = newIssue();
const events = issue.getUncommittedEvents();
expect(issue.key).toBe('TS-7');
expect(issue.status).toBe(IssueStatus.BACKLOG);
expect(issue.title).toBe('Login button misaligned');
expect(issue.labels).toEqual(['ui', 'login']);
expect(events).toHaveLength(1);
expect(events[0]).toBeInstanceOf(IssueCreatedEvent);
});
it('rejects non-Figma design links', () => {
expect(() =>
factory.create({
projectId: 'p-1',
projectKey: 'TS',
number: 1,
title: 'x',
type: IssueType.UI_CHANGE,
reporterId: 'u',
figmaUrl: 'https://evil.com/x',
}),
).toThrow(ValidationError);
});
it('follows the QA workflow', () => {
const issue = newIssue();
moveTo(
issue,
IssueStatus.TODO,
IssueStatus.IN_PROGRESS,
IssueStatus.READY_FOR_QA,
IssueStatus.IN_QA,
);
issue.changeStatus(
'u-qa',
IssueStatus.QA_FAILED,
'Still 2px off on Safari',
);
moveTo(
issue,
IssueStatus.IN_PROGRESS,
IssueStatus.READY_FOR_QA,
IssueStatus.IN_QA,
IssueStatus.DONE,
);
expect(issue.status).toBe(IssueStatus.DONE);
});
it('forbids skipping QA and requires a reason when QA fails', () => {
const issue = newIssue();
moveTo(issue, IssueStatus.TODO);
expect(() => issue.changeStatus('u', IssueStatus.DONE)).toThrow(
BusinessRuleError,
);
moveTo(
issue,
IssueStatus.IN_PROGRESS,
IssueStatus.READY_FOR_QA,
IssueStatus.IN_QA,
);
expect(() =>
issue.changeStatus('u-qa', IssueStatus.QA_FAILED, ' '),
).toThrow(/reason/);
});
it('links tickets once and never to itself', () => {
const issue = newIssue();
issue.linkTo('u', IssueLinkType.DEPENDS_ON, { id: 'i-be', key: 'BE-3' });
expect(() =>
issue.linkTo('u', IssueLinkType.DEPENDS_ON, { id: 'i-be', key: 'BE-3' }),
).toThrow(BusinessRuleError);
expect(() =>
issue.linkTo('u', IssueLinkType.RELATES_TO, {
id: issue.id,
key: 'TS-7',
}),
).toThrow(BusinessRuleError);
issue.unlink('u', IssueLinkType.DEPENDS_ON, 'i-be');
expect(issue.links).toHaveLength(0);
});
it('records events only for real changes', () => {
const issue = newIssue();
issue.uncommit();
issue.updateDetails('u', {
title: 'Login button misaligned',
labels: ['login', 'ui'],
});
issue.updateDetails('u', { priority: issue.priority });
expect(issue.getUncommittedEvents()).toHaveLength(1);
});
it('is rebuilt from its event history (event sourcing)', () => {
const issue = newIssue();
moveTo(issue, IssueStatus.TODO, IssueStatus.IN_PROGRESS);
issue.assign('u-qa', 'u-dev');
issue.linkTo('u', IssueLinkType.TESTS, { id: 'i-2', key: 'TS-2' });
const { rebuilt, history } = replay(issue, Issue);
expect(history.map((e) => e.type)).toEqual([
'IssueCreatedEvent',
'IssueStatusChangedEvent',
'IssueStatusChangedEvent',
'IssueAssignedEvent',
'IssueLinkedEvent',
]);
expect(history[1].data).toBeInstanceOf(IssueStatusChangedEvent);
expect(rebuilt.version.value).toBe(5);
expect(rebuilt).toMatchObject({
key: 'TS-7',
status: IssueStatus.IN_PROGRESS,
assigneeId: 'u-dev',
labels: ['ui', 'login'],
links: [
{ type: IssueLinkType.TESTS, targetIssueId: 'i-2', targetKey: 'TS-2' },
],
});
expect(rebuilt.getUncommittedEvents()).toHaveLength(0);
});
});