Skip to content

Commit 6389517

Browse files
feat(task): add task automation helpers and tests for status calculation (#1984)
Co-authored-by: Tofik Hasanov <annexcies@gmail.com>
1 parent 1088cd0 commit 6389517

3 files changed

Lines changed: 550 additions & 25 deletions

File tree

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import {
4+
calculateNextDueDate,
5+
getTargetStatus,
6+
type TaskAutomationData,
7+
} from './task-schedule-helpers';
8+
9+
describe('task-schedule-helpers', () => {
10+
describe('getTargetStatus', () => {
11+
describe('No automations configured', () => {
12+
it('should return "todo" when no automations are configured', () => {
13+
const task: TaskAutomationData = {
14+
evidenceAutomations: [],
15+
integrationCheckRuns: [],
16+
};
17+
18+
expect(getTargetStatus(task)).toBe('todo');
19+
});
20+
});
21+
22+
describe('Custom Automations only', () => {
23+
it('should return "done" when all custom automations pass', () => {
24+
const task: TaskAutomationData = {
25+
evidenceAutomations: [
26+
{ id: 'aut_1', runs: [{ evaluationStatus: 'pass' }] },
27+
{ id: 'aut_2', runs: [{ evaluationStatus: 'pass' }] },
28+
],
29+
integrationCheckRuns: [],
30+
};
31+
32+
expect(getTargetStatus(task)).toBe('done');
33+
});
34+
35+
it('should return "failed" when any custom automation fails', () => {
36+
const task: TaskAutomationData = {
37+
evidenceAutomations: [
38+
{ id: 'aut_1', runs: [{ evaluationStatus: 'pass' }] },
39+
{ id: 'aut_2', runs: [{ evaluationStatus: 'fail' }] },
40+
],
41+
integrationCheckRuns: [],
42+
};
43+
44+
expect(getTargetStatus(task)).toBe('failed');
45+
});
46+
47+
it('should return "failed" when custom automation has no runs', () => {
48+
const task: TaskAutomationData = {
49+
evidenceAutomations: [{ id: 'aut_1', runs: [] }],
50+
integrationCheckRuns: [],
51+
};
52+
53+
expect(getTargetStatus(task)).toBe('failed');
54+
});
55+
56+
it('should return "failed" when custom automation has null evaluationStatus', () => {
57+
const task: TaskAutomationData = {
58+
evidenceAutomations: [{ id: 'aut_1', runs: [{ evaluationStatus: null }] }],
59+
integrationCheckRuns: [],
60+
};
61+
62+
expect(getTargetStatus(task)).toBe('failed');
63+
});
64+
65+
it('should only check the latest run for each custom automation', () => {
66+
const task: TaskAutomationData = {
67+
evidenceAutomations: [
68+
{
69+
id: 'aut_1',
70+
runs: [
71+
{ evaluationStatus: 'pass' }, // Latest - pass
72+
{ evaluationStatus: 'fail' }, // Older - fail (should be ignored)
73+
],
74+
},
75+
],
76+
integrationCheckRuns: [],
77+
};
78+
79+
expect(getTargetStatus(task)).toBe('done');
80+
});
81+
});
82+
83+
describe('App Automations only', () => {
84+
it('should return "done" when all app automations succeed', () => {
85+
const task: TaskAutomationData = {
86+
evidenceAutomations: [],
87+
integrationCheckRuns: [
88+
{ checkId: 'github-mfa', status: 'success', createdAt: new Date('2024-01-06') },
89+
{ checkId: 'slack-channels', status: 'success', createdAt: new Date('2024-01-06') },
90+
],
91+
};
92+
93+
expect(getTargetStatus(task)).toBe('done');
94+
});
95+
96+
it('should return "failed" when any app automation fails', () => {
97+
const task: TaskAutomationData = {
98+
evidenceAutomations: [],
99+
integrationCheckRuns: [
100+
{ checkId: 'github-mfa', status: 'success', createdAt: new Date('2024-01-06') },
101+
{ checkId: 'slack-channels', status: 'failed', createdAt: new Date('2024-01-06') },
102+
],
103+
};
104+
105+
expect(getTargetStatus(task)).toBe('failed');
106+
});
107+
108+
it('should return "failed" when app automation is pending', () => {
109+
const task: TaskAutomationData = {
110+
evidenceAutomations: [],
111+
integrationCheckRuns: [
112+
{ checkId: 'github-mfa', status: 'pending', createdAt: new Date('2024-01-06') },
113+
],
114+
};
115+
116+
expect(getTargetStatus(task)).toBe('failed');
117+
});
118+
119+
it('should return "failed" when app automation is running', () => {
120+
const task: TaskAutomationData = {
121+
evidenceAutomations: [],
122+
integrationCheckRuns: [
123+
{ checkId: 'github-mfa', status: 'running', createdAt: new Date('2024-01-06') },
124+
],
125+
};
126+
127+
expect(getTargetStatus(task)).toBe('failed');
128+
});
129+
130+
it('should check latest run for each checkId separately', () => {
131+
const task: TaskAutomationData = {
132+
evidenceAutomations: [],
133+
integrationCheckRuns: [
134+
// Sorted by createdAt desc (latest first)
135+
{
136+
checkId: 'github-mfa',
137+
status: 'success',
138+
createdAt: new Date('2024-01-06T12:00:00'),
139+
},
140+
{
141+
checkId: 'slack-channels',
142+
status: 'failed',
143+
createdAt: new Date('2024-01-06T11:00:00'),
144+
},
145+
{ checkId: 'github-mfa', status: 'failed', createdAt: new Date('2024-01-05T10:00:00') }, // Older, ignored
146+
{
147+
checkId: 'slack-channels',
148+
status: 'success',
149+
createdAt: new Date('2024-01-04T09:00:00'),
150+
}, // Older, ignored
151+
],
152+
};
153+
154+
// github-mfa: latest is success ✓
155+
// slack-channels: latest is failed ✗
156+
expect(getTargetStatus(task)).toBe('failed');
157+
});
158+
159+
it('should not depend on input ordering when selecting latest per checkId', () => {
160+
const task: TaskAutomationData = {
161+
evidenceAutomations: [],
162+
integrationCheckRuns: [
163+
// Unsorted on purpose
164+
{
165+
checkId: 'github-mfa',
166+
status: 'failed',
167+
createdAt: new Date('2024-01-05T10:00:00'),
168+
},
169+
{
170+
checkId: 'slack-channels',
171+
status: 'success',
172+
createdAt: new Date('2024-01-04T09:00:00'),
173+
},
174+
{
175+
checkId: 'slack-channels',
176+
status: 'failed',
177+
createdAt: new Date('2024-01-06T11:00:00'),
178+
}, // Latest for slack-channels
179+
{
180+
checkId: 'github-mfa',
181+
status: 'success',
182+
createdAt: new Date('2024-01-06T12:00:00'),
183+
}, // Latest for github-mfa
184+
],
185+
};
186+
187+
// github-mfa: latest is success ✓
188+
// slack-channels: latest is failed ✗
189+
expect(getTargetStatus(task)).toBe('failed');
190+
});
191+
192+
it('should return "done" when all check types have successful latest runs', () => {
193+
const task: TaskAutomationData = {
194+
evidenceAutomations: [],
195+
integrationCheckRuns: [
196+
// Sorted by createdAt desc (latest first)
197+
{
198+
checkId: 'github-mfa',
199+
status: 'success',
200+
createdAt: new Date('2024-01-06T12:00:00'),
201+
},
202+
{
203+
checkId: 'slack-channels',
204+
status: 'success',
205+
createdAt: new Date('2024-01-06T11:00:00'),
206+
},
207+
{ checkId: 'github-mfa', status: 'failed', createdAt: new Date('2024-01-05T10:00:00') }, // Older, ignored
208+
],
209+
};
210+
211+
expect(getTargetStatus(task)).toBe('done');
212+
});
213+
});
214+
215+
describe('Both Custom and App Automations', () => {
216+
it('should return "done" when both types pass', () => {
217+
const task: TaskAutomationData = {
218+
evidenceAutomations: [{ id: 'aut_1', runs: [{ evaluationStatus: 'pass' }] }],
219+
integrationCheckRuns: [
220+
{ checkId: 'github-mfa', status: 'success', createdAt: new Date('2024-01-06') },
221+
],
222+
};
223+
224+
expect(getTargetStatus(task)).toBe('done');
225+
});
226+
227+
it('should return "failed" when custom passes but app fails', () => {
228+
const task: TaskAutomationData = {
229+
evidenceAutomations: [{ id: 'aut_1', runs: [{ evaluationStatus: 'pass' }] }],
230+
integrationCheckRuns: [
231+
{ checkId: 'github-mfa', status: 'failed', createdAt: new Date('2024-01-06') },
232+
],
233+
};
234+
235+
expect(getTargetStatus(task)).toBe('failed');
236+
});
237+
238+
it('should return "failed" when custom fails but app passes', () => {
239+
const task: TaskAutomationData = {
240+
evidenceAutomations: [{ id: 'aut_1', runs: [{ evaluationStatus: 'fail' }] }],
241+
integrationCheckRuns: [
242+
{ checkId: 'github-mfa', status: 'success', createdAt: new Date('2024-01-06') },
243+
],
244+
};
245+
246+
expect(getTargetStatus(task)).toBe('failed');
247+
});
248+
249+
it('should return "failed" when both fail', () => {
250+
const task: TaskAutomationData = {
251+
evidenceAutomations: [{ id: 'aut_1', runs: [{ evaluationStatus: 'fail' }] }],
252+
integrationCheckRuns: [
253+
{ checkId: 'github-mfa', status: 'failed', createdAt: new Date('2024-01-06') },
254+
],
255+
};
256+
257+
expect(getTargetStatus(task)).toBe('failed');
258+
});
259+
260+
it('should check all custom and all app automations', () => {
261+
const task: TaskAutomationData = {
262+
evidenceAutomations: [
263+
{ id: 'aut_1', runs: [{ evaluationStatus: 'pass' }] },
264+
{ id: 'aut_2', runs: [{ evaluationStatus: 'pass' }] },
265+
{ id: 'aut_3', runs: [{ evaluationStatus: 'pass' }] },
266+
],
267+
integrationCheckRuns: [
268+
{ checkId: 'github-mfa', status: 'success', createdAt: new Date('2024-01-06') },
269+
{ checkId: 'slack-channels', status: 'success', createdAt: new Date('2024-01-06') },
270+
{ checkId: 'jira-issues', status: 'success', createdAt: new Date('2024-01-06') },
271+
],
272+
};
273+
274+
expect(getTargetStatus(task)).toBe('done');
275+
});
276+
});
277+
});
278+
279+
describe('calculateNextDueDate', () => {
280+
const baseDate = new Date('2024-01-15T10:00:00Z');
281+
282+
it('should add 1 day for daily frequency', () => {
283+
const result = calculateNextDueDate(baseDate, 'daily');
284+
expect(result.toISOString()).toBe('2024-01-16T10:00:00.000Z');
285+
});
286+
287+
it('should add 7 days for weekly frequency', () => {
288+
const result = calculateNextDueDate(baseDate, 'weekly');
289+
expect(result.toISOString()).toBe('2024-01-22T10:00:00.000Z');
290+
});
291+
292+
it('should add 1 month for monthly frequency', () => {
293+
const result = calculateNextDueDate(baseDate, 'monthly');
294+
expect(result.toISOString()).toBe('2024-02-15T10:00:00.000Z');
295+
});
296+
297+
it('should add 3 months for quarterly frequency', () => {
298+
const result = calculateNextDueDate(baseDate, 'quarterly');
299+
// Check date components (timezone-safe)
300+
expect(result.getFullYear()).toBe(2024);
301+
expect(result.getMonth()).toBe(3); // April (0-indexed)
302+
expect(result.getDate()).toBe(15);
303+
});
304+
305+
it('should add 12 months for yearly frequency', () => {
306+
const result = calculateNextDueDate(baseDate, 'yearly');
307+
expect(result.toISOString()).toBe('2025-01-15T10:00:00.000Z');
308+
});
309+
310+
it('should handle month rollover (Jan 31 + 1 month = Feb 29 in leap year)', () => {
311+
const jan31 = new Date('2024-01-31T10:00:00Z'); // 2024 is a leap year
312+
const result = calculateNextDueDate(jan31, 'monthly');
313+
// Feb doesn't have 31 days, so it should be Feb 29 (leap year)
314+
expect(result.getMonth()).toBe(1); // February
315+
expect(result.getDate()).toBeLessThanOrEqual(29);
316+
});
317+
318+
it('should handle month rollover (Jan 31 + 1 month = Feb 28 in non-leap year)', () => {
319+
const jan31 = new Date('2023-01-31T10:00:00Z'); // 2023 is not a leap year
320+
const result = calculateNextDueDate(jan31, 'monthly');
321+
expect(result.getMonth()).toBe(1); // February
322+
expect(result.getDate()).toBeLessThanOrEqual(28);
323+
});
324+
});
325+
});

0 commit comments

Comments
 (0)