import test from "node:test"; import assert from "node:assert/strict"; import { usageDateRange, usageMinutes, usageConversationContext, usageConversationURL } from "../src/components/usage-model.ts"; test("usage range begins at Beijing midnight independent of browser timezone", () => { const previous = process.env.TZ; try { for (const zone of ["Etc/UTC", "America/Los_Angeles", "Asia/Tokyo"]) { process.env.TZ = zone; assert.deepEqual(usageDateRange(30, new Date("2026-10-05T16:30:00Z")), { from: "2026-09-06T16:00:00.000Z", to: "2026-10-05T16:30:00.000Z", timezone: "Asia/Shanghai", }); } } finally { if (previous === undefined) delete process.env.TZ; else process.env.TZ = previous; } }); test("usage range includes the current local day and handles year rollover", () => { assert.equal(usageDateRange(1, new Date("2026-01-01T00:05:00+08:00")).from, "2025-12-31T16:00:00.000Z"); assert.equal(usageDateRange(7, new Date("2026-01-01T00:05:00+08:00")).from, "2025-12-25T16:00:00.000Z"); for (const days of [0, -1, 367, NaN, 1.5]) assert.throws(() => usageDateRange(days), RangeError); }); test('30/90/365 days are inclusive Beijing calendar days across midnight, with an exact request-time end', () => { const cases = [ ['2026-10-05T15:59:59.999Z', {30:'2026-09-05T16:00:00.000Z',90:'2026-07-07T16:00:00.000Z',365:'2025-10-05T16:00:00.000Z'}], ['2026-10-05T16:00:00.000Z', {30:'2026-09-06T16:00:00.000Z',90:'2026-07-08T16:00:00.000Z',365:'2025-10-06T16:00:00.000Z'}], ['2026-10-05T16:00:00.001Z', {30:'2026-09-06T16:00:00.000Z',90:'2026-07-08T16:00:00.000Z',365:'2025-10-06T16:00:00.000Z'}], ]; for(const [at,starts] of cases) for(const days of [30,90,365]) { assert.deepEqual(usageDateRange(days,new Date(at)), {from:starts[days],to:at,timezone:'Asia/Shanghai'}); } assert.equal(usageDateRange(365,new Date('2028-03-01T00:00:00+08:00')).from,'2027-03-02T16:00:00.000Z', 'the year option means 365 inclusive days, rather than the same date twelve months earlier'); }); test("short completed chats do not display zero minutes", () => { assert.equal(usageMinutes(0), "0"); assert.equal(usageMinutes(1), "<1"); assert.equal(usageMinutes(29000), "<1"); assert.equal(usageMinutes(60000), "1"); assert.equal(usageMinutes(98000), "1.6"); }); const roster = [{ id: 'a-first', household_id: 'a' }, { id: 'a-second', household_id: 'a' }, { id: 'b-first', household_id: 'b' }]; const fleet = [{ id: 'a-device', household_id: 'a', subject_id: 'a-second' }, { id: 'b-device', household_id: 'b', subject_id: 'b-first' }, { id: 'unassigned', household_id: 'a' }, { id: 'stale-owner', household_id: 'a', subject_id: 'b-first' }]; test('a selected device selects its verified elder even when another elder is already current', () => { for (const current of [undefined, 'a-first', 'a-second']) assert.deepEqual(usageConversationContext('a', fleet, roster, current, 'a-device'), { householdID: 'a', deviceID: 'a-device', subjectID: 'a-second' }); }); test('all devices use the current valid elder or the existing family default', () => { assert.equal(usageConversationContext('a', fleet, roster, 'a-second').subjectID, 'a-second'); assert.deepEqual(usageConversationContext('a', fleet, roster, 'b-first'), { householdID: 'a', deviceID: undefined, subjectID: 'a-first' }); }); test('all devices with a single verified owner select that elder instead of an unrelated default', () => { const singleOwnerFleet = [{ id: 'one', household_id: 'a', subject_id: 'a-second' }, { id: 'two', household_id: 'a', subject_id: 'a-second' }, fleet[1]]; assert.equal(usageConversationContext('a', singleOwnerFleet, roster, 'a-first').subjectID, 'a-second'); assert.equal(usageConversationContext('a', [...singleOwnerFleet, fleet[2]], roster, 'a-first').subjectID, 'a-first', 'incomplete ownership uses the existing default'); }); test('foreign or removed devices and invalid owners never cross the household boundary', () => { for (const device of ['b-device', 'removed']) assert.deepEqual(usageConversationContext('a', fleet, roster, 'b-first', device), { householdID: 'a', deviceID: undefined, subjectID: 'a-first' }); for (const device of ['unassigned', 'stale-owner']) assert.equal(usageConversationContext('a', fleet, roster, 'b-first', device).subjectID, 'a-first'); assert.equal(usageConversationContext('empty', fleet, roster, 'a-first', 'a-device').subjectID, undefined); }); test('the all-records URL carries verified context without promising date or device filtering', () => { const context = usageConversationContext('a', fleet, roster, 'a-first', 'a-device'); const url = new URL(usageConversationURL(context), 'https://synthetic.invalid'); assert.deepEqual(Object.fromEntries(url.searchParams), { section: 'conversations', household_id: 'a', usage_device_id: 'a-device', subject_id: 'a-second' }); assert(!url.searchParams.has('device_id') && !url.searchParams.has('from') && !url.searchParams.has('to')); });