168 lines
5.5 KiB
TypeScript
168 lines
5.5 KiB
TypeScript
import { confirmedOperationRanges, operationIntervals, planOperationRead } from './operations-sync-plan';
|
|
|
|
describe('confirmed operation range projection', () => {
|
|
it('clips overlaps in source order without merging and respects account and instrument scope', () => {
|
|
const day = (value: number) => new Date(Date.UTC(2026, 0, value));
|
|
const target = { accountId: 'a', from: day(2), to: day(5) };
|
|
const sources = [
|
|
{ ...target, from: day(3), to: day(6) },
|
|
{ ...target, from: day(1), to: day(4) },
|
|
{ ...target, instrumentUid: 'uid' },
|
|
{ ...target, accountId: 'other' },
|
|
{ ...target, from: day(5), to: day(6) },
|
|
{ ...target, from: day(1), to: day(2) },
|
|
];
|
|
const before = structuredClone(sources);
|
|
expect(confirmedOperationRanges([target], sources)).toEqual([
|
|
{ ...target, from: day(3) },
|
|
{ ...target, to: day(4) },
|
|
]);
|
|
expect(confirmedOperationRanges([{ ...target, instrumentUid: 'uid' }], sources)).toEqual([
|
|
{ ...target, from: day(3) },
|
|
{ ...target, to: day(4) },
|
|
target,
|
|
]);
|
|
expect(sources).toEqual(before);
|
|
});
|
|
});
|
|
|
|
describe('operations calendar windows', () => {
|
|
it('merges overlaps before splitting leap February and keeps precise Moscow-derived boundaries', () => {
|
|
const result = operationIntervals([
|
|
{
|
|
accountId: 'a',
|
|
from: new Date('2024-02-01T00:00:00+03:00'),
|
|
to: new Date('2024-03-01T00:00:00+03:00'),
|
|
},
|
|
{
|
|
accountId: 'a',
|
|
from: new Date('2024-02-10Z'),
|
|
to: new Date('2024-04-01Z'),
|
|
},
|
|
]);
|
|
expect(
|
|
result.map(({ from, to }) => [from.toISOString(), to.toISOString()]),
|
|
).toEqual([
|
|
['2024-01-31T21:00:00.000Z', '2024-02-01T00:00:00.000Z'],
|
|
['2024-02-01T00:00:00.000Z', '2024-03-01T00:00:00.000Z'],
|
|
['2024-03-01T00:00:00.000Z', '2024-04-01T00:00:00.000Z'],
|
|
]);
|
|
});
|
|
|
|
it('does not combine account or instrument predicates and rejects invalid ranges', () => {
|
|
const range = {
|
|
from: new Date('2026-12-15Z'),
|
|
to: new Date('2027-01-15Z'),
|
|
};
|
|
const result = operationIntervals([
|
|
{ accountId: 'a', ...range },
|
|
{ accountId: 'b', ...range },
|
|
{ accountId: 'a', instrumentUid: 'uid', ...range },
|
|
]);
|
|
expect(result).toHaveLength(6);
|
|
expect(() =>
|
|
operationIntervals([{ accountId: 'a', from: range.to, to: range.from }]),
|
|
).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('operation completeness and freshness', () => {
|
|
const from = new Date('2026-09-01Z');
|
|
const asOf = new Date('2026-09-25T12:00:00Z');
|
|
const covered = { accountId: 'a', from, to: asOf, asOf, fetchedAt: asOf };
|
|
|
|
it.each([1, 3599, 3600])(
|
|
'preserves the observed complete end after %s seconds, separately from TTL',
|
|
(seconds) => {
|
|
const now = new Date(asOf.valueOf() + seconds * 1000);
|
|
const plan = planOperationRead(
|
|
[{ accountId: 'a', from, to: now }],
|
|
[covered],
|
|
now,
|
|
{ movingEnd: true },
|
|
);
|
|
expect(plan.intervals[0]).toMatchObject({
|
|
availability: 'complete',
|
|
asOf,
|
|
to: asOf,
|
|
stale: seconds >= 3600,
|
|
});
|
|
expect(plan.load).toEqual(
|
|
[],
|
|
);
|
|
},
|
|
);
|
|
|
|
it('preserves a cursor observed end after the clock and calendar advance', () => {
|
|
const cursorEnd = new Date(asOf.valueOf() + 1000);
|
|
const plan = planOperationRead(
|
|
[{ accountId: 'a', from, to: cursorEnd }],
|
|
[covered],
|
|
new Date('2026-10-01Z'),
|
|
{ movingEnd: true },
|
|
);
|
|
expect(plan.intervals[0]).toMatchObject({
|
|
availability: 'complete',
|
|
to: asOf,
|
|
asOf,
|
|
});
|
|
expect(plan.load).toEqual([]);
|
|
});
|
|
it('does not hide earlier holes or reuse an instrument subset as whole-account completeness', () => {
|
|
const now = new Date(asOf.valueOf() + 1000);
|
|
const partial = { ...covered, from: new Date('2026-09-10Z') };
|
|
const plan = planOperationRead(
|
|
[{ accountId: 'a', from, to: now }],
|
|
[partial],
|
|
now,
|
|
{ movingEnd: true },
|
|
);
|
|
expect(plan.intervals[0]).toMatchObject({
|
|
availability: 'partial',
|
|
stale: false,
|
|
});
|
|
expect(plan.load).toEqual([{ accountId: 'a', from, to: partial.from }]);
|
|
expect(
|
|
planOperationRead(
|
|
[{ accountId: 'a', from, to: now }],
|
|
[{ ...covered, instrumentUid: 'uid' }],
|
|
now,
|
|
).intervals[0].availability,
|
|
).toBe('missing');
|
|
});
|
|
|
|
it('loads a newly explicit tail and closes the previous month after rollover', () => {
|
|
const now = new Date(asOf.valueOf() + 1000);
|
|
expect(
|
|
planOperationRead([{ accountId: 'a', from, to: now }], [covered], now)
|
|
.load,
|
|
).toEqual([{ accountId: 'a', from: asOf, to: now }]);
|
|
const monthEnd = new Date('2026-10-01Z');
|
|
const rollover = new Date('2026-10-01T00:00:01Z');
|
|
const plan = planOperationRead(
|
|
[{ accountId: 'a', from, to: rollover }],
|
|
[covered],
|
|
rollover,
|
|
{ movingEnd: true },
|
|
);
|
|
expect(plan.load).toEqual([
|
|
{ accountId: 'a', from: asOf, to: monthEnd },
|
|
{ accountId: 'a', from: monthEnd, to: rollover },
|
|
]);
|
|
expect(
|
|
planOperationRead(
|
|
[{ accountId: 'a', from, to: monthEnd }],
|
|
[{ ...covered, to: monthEnd, asOf: monthEnd }],
|
|
rollover,
|
|
).load,
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('force refresh reloads only the shown target even when coverage is fresh', () => {
|
|
const target = { accountId: 'a', from: new Date('2026-09-20Z'), to: asOf };
|
|
expect(
|
|
planOperationRead([target], [covered], asOf, { force: true }).load,
|
|
).toEqual([target]);
|
|
});
|
|
});
|