Files

71 lines
4.9 KiB
JavaScript
Raw Permalink Normal View History

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'));
});