milestone 29: activity — history, live and policy simulation on one surface
Gates / test (push) Successful in 1m40s
Gates / package (push) Successful in 3m58s
Gates / container (push) Successful in 14s
CI / gates (push) Successful in 12m51s
Gates / frontend (push) Successful in 1m18s
Gates / test-aarch64 (push) Successful in 6m57s
Gates / test (push) Successful in 1m40s
Gates / package (push) Successful in 3m58s
Gates / container (push) Successful in 14s
CI / gates (push) Successful in 12m51s
Gates / frontend (push) Successful in 1m18s
Gates / test-aarch64 (push) Successful in 6m57s
query log, live and lookup merge into /activity. history filters live in the url, so a pasted link or back/forward reproduces the exact view; the result column separates servfail and nxdomain from success in the list. live is follow-by-default with freeze, and a streamed row opens its in-memory provenance detail — no correlation invented for rows sqlite has not written. lookup survives as the current policy simulation under /activity/test. investigation links carry absolute bounds, and the diagnostics page now honors since/until instead of ignoring them. the old routes are gone without aliases.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
datetimeField,
|
||||
datetimeLocalToUnix,
|
||||
editDatetimeField,
|
||||
resolveDatetimeField,
|
||||
unixToDatetimeLocal,
|
||||
} from "./datetime";
|
||||
|
||||
/**
|
||||
* Every assertion here is about local time, so the zone has to be pinned. New
|
||||
* York is the zone the DST cases are written for: the fold is 2024-11-03 01:30
|
||||
* and the gap is 2024-03-10 02:30.
|
||||
*/
|
||||
// The app never reads `process`, so `src` is typed without node's globals; the
|
||||
// test host is node, where assigning `TZ` re-reads the zone for `Date`.
|
||||
declare const process: { env: Record<string, string | undefined> };
|
||||
|
||||
const originalTz = process.env["TZ"];
|
||||
beforeAll(() => {
|
||||
process.env["TZ"] = "America/New_York";
|
||||
});
|
||||
afterAll(() => {
|
||||
process.env["TZ"] = originalTz;
|
||||
});
|
||||
|
||||
/** 2024-06-01T12:34:56 EDT. */
|
||||
const SUMMER = 1_717_259_696;
|
||||
|
||||
test("a non-zero-second instant round trips", () => {
|
||||
expect(unixToDatetimeLocal(SUMMER)).toBe("2024-06-01T12:34:56");
|
||||
expect(datetimeLocalToUnix("2024-06-01T12:34:56")).toBe(SUMMER);
|
||||
});
|
||||
|
||||
test("text without seconds parses as :00", () => {
|
||||
expect(datetimeLocalToUnix("2024-06-01T12:34")).toBe(datetimeLocalToUnix("2024-06-01T12:34:00"));
|
||||
});
|
||||
|
||||
test("text that is not a datetime-local value names no instant", () => {
|
||||
expect(datetimeLocalToUnix("")).toBeUndefined();
|
||||
expect(datetimeLocalToUnix("yesterday")).toBeUndefined();
|
||||
expect(datetimeLocalToUnix("2024-06-01")).toBeUndefined();
|
||||
expect(datetimeLocalToUnix("2024-13-01T00:00:00")).toBeUndefined();
|
||||
});
|
||||
|
||||
/** 2024-11-03 01:30 EDT and 01:30 EST: two instants, one wall clock. */
|
||||
const FOLD_FIRST = 1_730_611_800;
|
||||
const FOLD_SECOND = 1_730_615_400;
|
||||
|
||||
test("the fall-back fold gives two instants the same text", () => {
|
||||
expect(unixToDatetimeLocal(FOLD_FIRST)).toBe("2024-11-03T01:30:00");
|
||||
expect(unixToDatetimeLocal(FOLD_SECOND)).toBe("2024-11-03T01:30:00");
|
||||
expect(datetimeLocalToUnix("2024-11-03T01:30:00")).toBe(FOLD_FIRST);
|
||||
});
|
||||
|
||||
test("an untouched fold bound applies the instant it was seeded with, not a re-parse of its text", () => {
|
||||
const field = datetimeField(FOLD_SECOND);
|
||||
expect(field.text).toBe("2024-11-03T01:30:00");
|
||||
expect(resolveDatetimeField(field)).toEqual({ ok: true, value: FOLD_SECOND });
|
||||
});
|
||||
|
||||
test("an edited fold bound resolves to the first of the two instants, which is what its text says", () => {
|
||||
const field = editDatetimeField(datetimeField(FOLD_SECOND), "2024-11-03T01:30:00");
|
||||
expect(resolveDatetimeField(field)).toEqual({ ok: true, value: FOLD_FIRST });
|
||||
});
|
||||
|
||||
test("a spring-forward time that exists on no clock is rejected rather than slid forward an hour", () => {
|
||||
const field = editDatetimeField(datetimeField(undefined), "2024-03-10T02:30:00");
|
||||
expect(datetimeLocalToUnix("2024-03-10T02:30:00")).toBe(1_710_055_800);
|
||||
expect(unixToDatetimeLocal(1_710_055_800)).toBe("2024-03-10T03:30:00");
|
||||
expect(resolveDatetimeField(field)).toEqual({ ok: false, reason: "nonexistent" });
|
||||
});
|
||||
|
||||
test("an edited bound round trips with non-zero seconds", () => {
|
||||
const field = editDatetimeField(datetimeField(undefined), "2024-06-01T12:34:56");
|
||||
expect(resolveDatetimeField(field)).toEqual({ ok: true, value: SUMMER });
|
||||
});
|
||||
|
||||
test("clearing an edited bound drops the filter", () => {
|
||||
const field = editDatetimeField(datetimeField(SUMMER), "");
|
||||
expect(resolveDatetimeField(field)).toEqual({ ok: true, value: undefined });
|
||||
});
|
||||
|
||||
test("an unparseable edit is reported, never silently dropped", () => {
|
||||
const field = editDatetimeField(datetimeField(undefined), "2024-06-32T99:99");
|
||||
expect(resolveDatetimeField(field)).toEqual({ ok: false, reason: "unparseable" });
|
||||
});
|
||||
|
||||
test("an unset bound seeds an empty field that stays unset", () => {
|
||||
const field = datetimeField(undefined);
|
||||
expect(field.text).toBe("");
|
||||
expect(resolveDatetimeField(field)).toEqual({ ok: true, value: undefined });
|
||||
});
|
||||
|
||||
test("a fractional part on the seconds is parsed and dropped, not rejected", () => {
|
||||
// jsdom, and any engine that sanitizes to the full grammar, hands the input
|
||||
// back with milliseconds attached; a bound is a whole second either way.
|
||||
const field = editDatetimeField(datetimeField(undefined), "2023-11-14T23:13:37.000");
|
||||
const resolved = resolveDatetimeField(field);
|
||||
expect(resolved).toEqual({ ok: true, value: datetimeLocalToUnix("2023-11-14T23:13:37") });
|
||||
});
|
||||
Reference in New Issue
Block a user