krona-staff/tests/unit/work-session.test.ts
2026-09-24 17:00:08 +07:00

93 lines
3.3 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { reconcileWorkSession } from '../../src/domain/attendance/work-session.js';
const options = {
now: '2026-09-24T20:00:00.000Z',
openShiftThresholdMinutes: 14 * 60,
};
describe('attendance work-session reconciliation', () => {
it('sums several in/out intervals without inventing time between them', () => {
const result = reconcileWorkSession([
{ id: '1', type: 'check_in', occurredAt: '2026-09-24T01:00:00.000Z' },
{ id: '2', type: 'check_out', occurredAt: '2026-09-24T05:00:00.000Z' },
{ id: '3', type: 'check_in', occurredAt: '2026-09-24T06:00:00.000Z' },
{ id: '4', type: 'check_out', occurredAt: '2026-09-24T10:30:00.000Z' },
], options);
expect(result).toMatchObject({
startedAt: '2026-09-24T01:00:00.000Z',
endedAt: '2026-09-24T10:30:00.000Z',
workedMinutes: 510,
status: 'pending_review',
anomalies: [],
});
});
it('keeps the first check-in and flags a repeated check-in', () => {
const result = reconcileWorkSession([
{ id: '1', type: 'check_in', occurredAt: '2026-09-24T01:00:00.000Z' },
{ id: '2', type: 'check_in', occurredAt: '2026-09-24T01:05:00.000Z' },
{ id: '3', type: 'check_out', occurredAt: '2026-09-24T09:00:00.000Z' },
], options);
expect(result?.workedMinutes).toBe(480);
expect(result?.anomalies).toEqual(['repeated_check_in']);
});
it('keeps a recent unfinished shift open', () => {
const result = reconcileWorkSession([
{ id: '1', type: 'check_in', occurredAt: '2026-09-24T12:00:00.000Z' },
], options);
expect(result).toMatchObject({ status: 'open', endedAt: null, workedMinutes: 0, anomalies: [] });
});
it('flags an old unfinished shift without estimating payable minutes', () => {
const result = reconcileWorkSession([
{ id: '1', type: 'check_in', occurredAt: '2026-09-24T01:00:00.000Z' },
], options);
expect(result).toMatchObject({
status: 'pending_review',
endedAt: null,
workedMinutes: 0,
anomalies: ['missing_check_out'],
});
});
it('auto-closes only after the scheduler cutoff and leaves an audit-worthy anomaly', () => {
const result = reconcileWorkSession([
{ id: '1', type: 'check_in', occurredAt: '2026-09-23T12:00:00.000Z' },
], { ...options, automaticCloseAt: '2026-09-23T19:00:00.000Z' });
expect(result).toMatchObject({
endedAt: '2026-09-23T19:00:00.000Z',
workedMinutes: 420,
status: 'pending_review',
anomalies: ['auto_closed'],
});
});
it('records a check-out without check-in as an anomaly with zero minutes', () => {
const result = reconcileWorkSession([
{ id: '1', type: 'check_out', occurredAt: '2026-09-24T09:00:00.000Z' },
], options);
expect(result).toMatchObject({
startedAt: null,
endedAt: '2026-09-24T09:00:00.000Z',
workedMinutes: 0,
status: 'pending_review',
anomalies: ['missing_check_in'],
});
});
it('rejects invalid timestamps and thresholds', () => {
expect(() => reconcileWorkSession([
{ id: '1', type: 'check_in', occurredAt: 'not-a-date' },
], options)).toThrow('event 1 must be an ISO date-time');
expect(() => reconcileWorkSession([], { ...options, openShiftThresholdMinutes: 0 }))
.toThrow('openShiftThresholdMinutes must be a positive integer');
});
});