import { describe, expect, it } from "vitest"; import { describeSyncOutcome, syncIndicator, syncIndicatorLabel, type SyncOutcome, } from "./sync-outcome"; describe("syncIndicator", () => { it("pulses green only while a sync is actually running", () => { expect(syncIndicator({ status: "syncing" }, 1)).toBe("busy"); // A running sync outranks a stale failure — the light describes now. expect(syncIndicator({ status: "syncing", outcome: "relay_unreachable" }, 1)).toBe("busy"); }); it("is solid green for a completed sync, with or without changes", () => { expect(syncIndicator({ status: "idle", outcome: "completed_changes" }, 1)).toBe("ok"); expect(syncIndicator({ status: "idle", outcome: "completed_no_changes" }, 1)).toBe("ok"); }); it("never shows green for a paired-but-unsyncable device", () => { // The exact case that used to be a silent return: a device is paired, so // "you have a pair" is true, but nothing can sync. Green here would lie. expect(syncIndicator({ status: "error", outcome: "no_eligible_host" }, 2)).toBe("error"); expect(syncIndicator({ status: "error", outcome: "runner_unavailable" }, 2)).toBe("error"); }); it("shows listener-only mesh role as healthy while waiting for its peer", () => { const summary = { status: "idle" as const, outcome: "waiting_for_peer" as const, detail: "Android [type=android, trust=direct]", }; expect(syncIndicator(summary, 1)).toBe("ok"); expect(describeSyncOutcome(summary)).toContain("Waiting for the paired device to connect"); }); it("shows yellow for network-class misses — relay/handshake/timeout are warn, not error", () => { // The alarm fires every 10 minutes and dials. If the peer doesn't answer, // the next alarm tick will try again. That is the network, not the app — // red would be alarm fatigue. These must be warn (yellow), not error. expect(syncIndicator({ status: "idle", outcome: "relay_unreachable" }, 1)).toBe("warn"); expect(syncIndicator({ status: "idle", outcome: "handshake_failed" }, 1)).toBe("warn"); expect(syncIndicator({ status: "idle", outcome: "sync_timeout" }, 1)).toBe("warn"); }); it("keeps red on genuine configuration / identity problems that the alarm cannot fix", () => { expect(syncIndicator({ status: "error", outcome: "vault_id_mismatch" }, 1)).toBe("error"); expect(syncIndicator({ status: "error", outcome: "private_relay_unset" }, 1)).toBe("error"); expect(syncIndicator({ status: "error", outcome: "no_eligible_host" }, 1)).toBe("error"); expect(syncIndicator({ status: "error", outcome: "runner_unavailable" }, 1)).toBe("error"); expect(syncIndicator({ status: "error", outcome: "run_failed" }, 1)).toBe("error"); }); it("treats vault_locked as warn (yellow) — the user hasn't unlocked yet, not a misconfig", () => { // vault_locked is the reader's vault being closed (e.g. SW restarted after // the popup was last dismissed). The next unlock + retry will succeed; the // alarm keeps trying. Same yellow semantics as a missed dial — a red pip // here would be misleading alarm fatigue. expect(syncIndicator({ status: "error", outcome: "vault_locked" }, 1)).toBe("warn"); expect(syncIndicator({ status: "idle", outcome: "vault_locked" }, 1)).toBe("warn"); }); it("returns to green after a successful dial even when the prior attempt was warn", () => { // The state machine must not get stuck on a warn that already cleared. const prior: { status: "idle"; outcome: "completed_changes" } = { status: "idle", outcome: "completed_changes", }; expect(syncIndicator(prior, 1)).toBe("ok"); }); it("stays unlit when there is nothing paired and nothing attempted", () => { expect(syncIndicator({}, 0)).toBe("idle"); expect(syncIndicator({ status: "idle", outcome: "no_paired_devices" }, 0)).toBe("idle"); }); it("reads a healthy pair as green before the first attempt of this session", () => { expect(syncIndicator({ status: "idle" }, 1)).toBe("ok"); }); it("maps every outcome to its explicit indicator state", () => { const expected: Record> = { completed_changes: "ok", completed_no_changes: "ok", no_paired_devices: "idle", no_eligible_host: "error", waiting_for_peer: "ok", runner_unavailable: "error", sync_disabled: "idle", private_relay_unset: "error", vault_locked: "warn", relay_unreachable: "warn", handshake_failed: "warn", vault_id_mismatch: "error", peer_protocol_outdated: "error", sync_timeout: "warn", connection_lost: "warn", run_failed: "error", }; for (const [outcome, indicator] of Object.entries(expected)) { expect(syncIndicator({ status: "idle", outcome: outcome as SyncOutcome }, 1)).toBe(indicator); } }); }); describe("describeSyncOutcome", () => { it("names what moved on a successful sync", () => { expect( describeSyncOutcome({ outcome: "completed_changes", entriesSent: 2, entriesReceived: 1 }), ).toBe("Synced — 2 sent, 1 received"); }); it("distinguishes a successful no-op from a failure in plain English", () => { expect(describeSyncOutcome({ outcome: "completed_no_changes" })) .toBe("Up to date — nothing to send or receive"); expect(describeSyncOutcome({ outcome: "no_eligible_host" })) .not.toContain("Up to date"); }); it("keeps the raw diagnostic detail visible after the plain-English reason", () => { // On a real device there is no devtools; the inventory is the whole clue. const line = describeSyncOutcome({ outcome: "no_eligible_host", detail: "Pixel [type=phone, trust=direct]", }); expect(line).toContain("no device here can accept a sync"); expect(line).toContain("Pixel [type=phone, trust=direct]"); }); it("says nothing has happened yet rather than implying success", () => { expect(describeSyncOutcome({})).toBe("Not synced yet"); }); }); describe("syncIndicatorLabel", () => { it("states the colour's claim in words", () => { expect(syncIndicatorLabel("error", { outcome: "vault_id_mismatch" })) .toBe("Sync problem — That device holds a different vault"); }); it("states the warn colour's claim in words — the alarm retries", () => { expect(syncIndicatorLabel("warn", { outcome: "relay_unreachable" })) .toBe("Last sync attempt missed — will retry — Couldn't reach the relay"); }); });