src.nth.io/

summaryrefslogtreecommitdiff
path: root/test/sync.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'test/sync.test.ts')
-rw-r--r--test/sync.test.ts133
1 files changed, 133 insertions, 0 deletions
diff --git a/test/sync.test.ts b/test/sync.test.ts
new file mode 100644
index 0000000..d48f286
--- /dev/null
+++ b/test/sync.test.ts
@@ -0,0 +1,133 @@
+import { describe, expect, it } from "vitest";
+import {
+ MATTER_EPOCH_UNIX_SECONDS,
+ compareDeviceClock,
+ currentMatterUtcMicroseconds,
+ formatDurationMicros,
+ formatMatterMicros,
+ matterMicrosToUnixMillis,
+ unixMillisToMatterMicros,
+} from "../src/sync.js";
+
+describe("Matter epoch conversion", () => {
+ it("maps 2000-01-01T00:00:00Z to 0 microseconds", () => {
+ const unixMs = BigInt(Date.parse("2000-01-01T00:00:00Z"));
+ expect(unixMillisToMatterMicros(unixMs)).toBe(0n);
+ });
+
+ it("maps 2000-01-01T00:00:01Z to 1,000,000 microseconds", () => {
+ const unixMs = BigInt(Date.parse("2000-01-01T00:00:01Z"));
+ expect(unixMillisToMatterMicros(unixMs)).toBe(1_000_000n);
+ });
+
+ it("keeps the Unix epoch conversion exact", () => {
+ expect(unixMillisToMatterMicros(0n)).toBe(-MATTER_EPOCH_UNIX_SECONDS * 1_000_000n);
+ expect(unixMillisToMatterMicros(0n)).toBe(-946_684_800_000_000n);
+ });
+
+ it("keeps current dates exact through bigint round-trips", () => {
+ const unixMs = BigInt(Date.parse("2026-07-26T20:00:00.123Z"));
+ const micros = unixMillisToMatterMicros(unixMs);
+ expect(micros).toBe((unixMs - 946_684_800_000n) * 1_000n);
+ expect(matterMicrosToUnixMillis(micros)).toBe(unixMs);
+ });
+
+ it("stays exact beyond JavaScript number precision", () => {
+ // Microsecond timestamps pass Number.MAX_SAFE_INTEGER around year 2285;
+ // conversion must not degrade there, proving no number arithmetic occurs.
+ const unixMs = BigInt(Date.parse("2300-01-01T00:00:00.001Z"));
+ const micros = unixMillisToMatterMicros(unixMs);
+ expect(micros > BigInt(Number.MAX_SAFE_INTEGER)).toBe(true);
+ expect(matterMicrosToUnixMillis(micros)).toBe(unixMs);
+ });
+
+ it("computes the current time via the injected clock", () => {
+ const nowMs = Date.parse("2026-07-26T20:00:00Z");
+ const micros = currentMatterUtcMicroseconds(() => nowMs);
+ expect(micros).toBe(unixMillisToMatterMicros(BigInt(nowMs)));
+ });
+
+ it("formats Matter microseconds as an ISO instant", () => {
+ expect(formatMatterMicros(0n)).toBe("2000-01-01T00:00:00.000Z");
+ expect(formatMatterMicros(1_000_000n)).toBe("2000-01-01T00:00:01.000Z");
+ });
+});
+
+describe("formatDurationMicros", () => {
+ it("scales units with magnitude", () => {
+ expect(formatDurationMicros(0n)).toBe("0us");
+ expect(formatDurationMicros(999n)).toBe("999us");
+ expect(formatDurationMicros(412_000n)).toBe("412ms");
+ expect(formatDurationMicros(3_200_000n)).toBe("3.2s");
+ expect(formatDurationMicros(59_000_000n)).toBe("59s");
+ expect(formatDurationMicros(125_000_000n)).toBe("2m 5s");
+ expect(formatDurationMicros(3_840_000_000n)).toBe("1h 4m");
+ // 90,061 seconds = 1d 1h 1m 1s; seconds are dropped once days appear.
+ expect(formatDurationMicros(90_061_000_000n)).toBe("1d 1h 1m");
+ });
+
+ it("rejects negative durations", () => {
+ expect(() => formatDurationMicros(-1n)).toThrow();
+ });
+});
+
+describe("compareDeviceClock", () => {
+ // compareDeviceClock operates on Unix-epoch microseconds (matter.js convention).
+ const host = BigInt(Date.parse("2026-07-26T20:00:00.000Z")) * 1_000n;
+
+ it("reports an unset device clock", () => {
+ const comparison = compareDeviceClock(null, host);
+ expect(comparison.deviceTime).toBeNull();
+ expect(comparison.deltaMicroseconds).toBeNull();
+ expect(comparison.hostTime).toBe("2026-07-26T20:00:00.000Z");
+ expect(comparison.description).toBe("device clock was unset");
+ });
+
+ it("reports a device clock behind the host", () => {
+ const comparison = compareDeviceClock(host - 83_000_000n, host);
+ expect(comparison.deviceTime).toBe("2026-07-26T19:58:37.000Z");
+ expect(comparison.deltaMicroseconds).toBe(-83_000_000n);
+ expect(comparison.description).toBe("device clock was 1m 23s behind");
+ });
+
+ it("reports a device clock ahead of the host", () => {
+ const comparison = compareDeviceClock(host + 5_500_000n, host);
+ expect(comparison.deltaMicroseconds).toBe(5_500_000n);
+ expect(comparison.description).toBe("device clock was 5.5s ahead");
+ });
+
+ it("treats sub-second deltas as in sync but still reports them", () => {
+ const comparison = compareDeviceClock(host - 412_000n, host);
+ expect(comparison.description).toBe("device clock was within 1s of host time (412ms behind)");
+ });
+
+ it("detects a device that encodes Unix-epoch time on the wire (wrong epoch bug)", () => {
+ // Such firmware decodes to exactly 946,684,800s ahead at the matter.js
+ // API boundary. The raw delta stays exact; the effective delta folds
+ // the shift out.
+ const wrong = host + 946_684_800n * 1_000_000n + 1_400_000n;
+ const comparison = compareDeviceClock(wrong, host);
+ expect(comparison.epochShifted).toBe(true);
+ expect(comparison.deltaMicroseconds).toBe(946_684_800_000_000n + 1_400_000n);
+ expect(comparison.effectiveDeltaMicroseconds).toBe(1_400_000n);
+ expect(comparison.deviceTime).toBe("2026-07-26T20:00:01.400Z");
+ expect(comparison.description).toBe(
+ "device encodes Unix-epoch time on the wire (off-spec); corrected, its clock was 1.4s ahead",
+ );
+ });
+
+ it("does not fold out large errors that are not the epoch shift", () => {
+ // 30 days past the shift window: a genuinely broken clock, not epoch bug.
+ const wrong = host + 946_684_800n * 1_000_000n + 30n * 86_400n * 1_000_000n;
+ const comparison = compareDeviceClock(wrong, host);
+ expect(comparison.epochShifted).toBe(false);
+ expect(comparison.effectiveDeltaMicroseconds).toBe(comparison.deltaMicroseconds);
+ expect(comparison.description).toContain("ahead");
+ });
+
+ it("keeps spec-compliant comparisons unshifted", () => {
+ const comparison = compareDeviceClock(host - 83_000_000n, host);
+ expect(comparison.epochShifted).toBe(false);
+ expect(comparison.effectiveDeltaMicroseconds).toBe(-83_000_000n);
+ });
+});