|
| 1 | +import { test } from 'node:test'; |
| 2 | +import assert from 'node:assert/strict'; |
| 3 | +import { getNodeEligibility, formatEligibilityTime } from './nodeEligibility.ts'; |
| 4 | + |
| 5 | +// Fake clock: every case injects `now` instead of reading Date.now(), so no real timers run. |
| 6 | +const NOW_MS = Date.UTC(2026, 0, 15, 12, 0, 0); // 2026-01-15T12:00:00Z |
| 7 | + |
| 8 | +test('future delayed CREATED node is scheduled for its stored eligibility time', () => { |
| 9 | + const at = NOW_MS + 90 * 60 * 1000; |
| 10 | + const result = getNodeEligibility({ status: 'CREATED', enqueue_after: at }, NOW_MS); |
| 11 | + assert.deepEqual(result, { kind: 'scheduled', at: new Date(at) }); |
| 12 | +}); |
| 13 | + |
| 14 | +test('past-due CREATED node is reported as eligible and waiting, not as scheduled', () => { |
| 15 | + const at = NOW_MS - 5 * 60 * 1000; |
| 16 | + const result = getNodeEligibility({ status: 'CREATED', enqueue_after: at }, NOW_MS); |
| 17 | + assert.deepEqual(result, { kind: 'waiting', at: new Date(at) }); |
| 18 | +}); |
| 19 | + |
| 20 | +test('an eligibility time equal to now counts as eligible (matches the server-side <= pick-up rule)', () => { |
| 21 | + const result = getNodeEligibility({ status: 'CREATED', enqueue_after: NOW_MS }, NOW_MS); |
| 22 | + assert.equal(result?.kind, 'waiting'); |
| 23 | +}); |
| 24 | + |
| 25 | +test('old record without the field shows nothing (no Invalid Date, no epoch zero)', () => { |
| 26 | + assert.equal(getNodeEligibility({ status: 'CREATED' }, NOW_MS), null); |
| 27 | + assert.equal(getNodeEligibility({ status: 'CREATED', enqueue_after: undefined }, NOW_MS), null); |
| 28 | + assert.equal(getNodeEligibility({ status: 'CREATED', enqueue_after: null }, NOW_MS), null); |
| 29 | + assert.equal(getNodeEligibility({ status: 'CREATED', enqueue_after: 0 }, NOW_MS), null); |
| 30 | + assert.equal(getNodeEligibility({ status: 'CREATED', enqueue_after: Number.NaN }, NOW_MS), null); |
| 31 | +}); |
| 32 | + |
| 33 | +test('already queued, running or finished nodes carry no waiting message', () => { |
| 34 | + const at = NOW_MS + 60 * 1000; |
| 35 | + for (const status of ['QUEUED', 'EXECUTED', 'SUCCESS', 'ERRORED', 'TIMEDOUT', 'CANCELLED', 'PRUNED', 'NEXT_CREATED_ERROR'] as const) { |
| 36 | + assert.equal(getNodeEligibility({ status, enqueue_after: at }, NOW_MS), null, status); |
| 37 | + } |
| 38 | +}); |
| 39 | + |
| 40 | +test('missing details yield nothing', () => { |
| 41 | + assert.equal(getNodeEligibility(null, NOW_MS), null); |
| 42 | + assert.equal(getNodeEligibility(undefined, NOW_MS), null); |
| 43 | +}); |
| 44 | + |
| 45 | +test('the same instant renders in the viewer timezone with a zone label, never re-persisted', () => { |
| 46 | + const at = new Date(Date.UTC(2026, 0, 15, 23, 30, 0)); // near a day boundary |
| 47 | + const utc = formatEligibilityTime(at, 'en-US', 'UTC'); |
| 48 | + const tokyo = formatEligibilityTime(at, 'en-US', 'Asia/Tokyo'); |
| 49 | + const la = formatEligibilityTime(at, 'en-US', 'America/Los_Angeles'); |
| 50 | + assert.match(utc, /UTC/); |
| 51 | + assert.match(tokyo, /GMT\+9|JST/); |
| 52 | + assert.match(la, /PST|GMT-8/); |
| 53 | + // Different calendar days for the same instant; the instant itself is unchanged. |
| 54 | + assert.match(tokyo, /1\/16\/2026/); |
| 55 | + assert.match(la, /1\/15\/2026/); |
| 56 | + assert.equal(at.getTime(), Date.UTC(2026, 0, 15, 23, 30, 0)); |
| 57 | +}); |
0 commit comments