Files
gmtool/tests/con/schedule.test.js
T
slaguru666andClaude Opus 4.8 b57880d630 feat: Slice 5 — convention hub (live now / up next, deep-links)
Add the convention hub landing screen: a pure schedule engine
(analyzeSchedule) classifies each slot as live/next/done/upcoming from an
injected now; <con-hub> renders the slate + summary and emits open-scenario;
a scenario registry resolves a slot's scenarioId to its module; gm-shell
routes hub<->session (hiding/restoring the Rail) with a home rail chip.

Ships continuum-2026.js with the real slate (only AFTERIMAGE ported;
others scenarioId:null, start-times TBC) and a fully-timed example-con.js
template/fixture. 122 tests pass; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:35:47 +01:00

58 lines
2.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import { analyzeSchedule, slotStartMs } from '../../src/con/schedule.js';
const at = (iso) => Date.parse(iso);
const SLOTS = [
{ id: 's1', title: 'Morning', playMinutes: 60, startsAt: '2026-07-24T09:00:00Z' },
{ id: 's2', title: 'Midday', playMinutes: 60, startsAt: '2026-07-24T12:00:00Z' },
{ id: 's3', title: 'Afternoon', playMinutes: 60, startsAt: '2026-07-24T15:00:00Z' },
{ id: 's4', title: 'Untimed', playMinutes: 60 },
];
describe('analyzeSchedule', () => {
it('classifies done / live / next / upcoming around now', () => {
const r = analyzeSchedule(SLOTS, at('2026-07-24T12:30:00Z'));
const byId = Object.fromEntries(r.slots.map((s) => [s.id, s.status]));
expect(byId.s1).toBe('done'); // ended 10:00
expect(byId.s2).toBe('live'); // 12:00–13:00
expect(byId.s3).toBe('next'); // first future timed slot
expect(byId.s4).toBe('upcoming'); // untimed, never next
expect(r.liveNow.id).toBe('s2');
expect(r.upNext.id).toBe('s3');
});
it('before the con: nothing live, earliest is next', () => {
const r = analyzeSchedule(SLOTS, at('2026-07-24T06:00:00Z'));
expect(r.liveNow).toBe(null);
expect(r.upNext.id).toBe('s1');
expect(r.slots.find((s) => s.id === 's2').status).toBe('upcoming');
});
it('after the con: everything done except the untimed slot', () => {
const r = analyzeSchedule(SLOTS, at('2026-07-25T00:00:00Z'));
expect(r.liveNow).toBe(null);
expect(r.upNext).toBe(null);
expect(r.slots.find((s) => s.id === 's1').status).toBe('done');
expect(r.slots.find((s) => s.id === 's4').status).toBe('upcoming');
});
it('sorts slots by start time, untimed last', () => {
const r = analyzeSchedule(SLOTS, 0);
expect(r.slots.map((s) => s.id)).toEqual(['s1', 's2', 's3', 's4']);
});
it('treats an empty schedule as idle', () => {
const r = analyzeSchedule([], 0);
expect(r.slots).toEqual([]);
expect(r.liveNow).toBe(null);
expect(r.upNext).toBe(null);
});
it('slotStartMs falls back to Infinity when unscheduled', () => {
expect(slotStartMs({})).toBe(Infinity);
expect(slotStartMs({ startMs: 5 })).toBe(5);
expect(slotStartMs({ startsAt: '2026-07-24T09:00:00Z' })).toBe(at('2026-07-24T09:00:00Z'));
});
});