1import { describe, expect, it } from "vitest";2import {3 describeSyncOutcome,4 syncIndicator,5 syncIndicatorLabel,6 type SyncOutcome,7} from "./sync-outcome";89describe("syncIndicator", () => {10 it("pulses green only while a sync is actually running", () => {11 expect(syncIndicator({ status: "syncing" }, 1)).toBe("busy");12 13 expect(syncIndicator({ status: "syncing", outcome: "relay_unreachable" }, 1)).toBe("busy");14 });1516 it("is solid green for a completed sync, with or without changes", () => {17 expect(syncIndicator({ status: "idle", outcome: "completed_changes" }, 1)).toBe("ok");18 expect(syncIndicator({ status: "idle", outcome: "completed_no_changes" }, 1)).toBe("ok");19 });2021 it("never shows green for a paired-but-unsyncable device", () => {22 23 24 expect(syncIndicator({ status: "error", outcome: "no_eligible_host" }, 2)).toBe("error");25 expect(syncIndicator({ status: "error", outcome: "runner_unavailable" }, 2)).toBe("error");26 });2728 it("shows listener-only mesh role as healthy while waiting for its peer", () => {29 const summary = {30 status: "idle" as const,31 outcome: "waiting_for_peer" as const,32 detail: "Android [type=android, trust=direct]",33 };34 expect(syncIndicator(summary, 1)).toBe("ok");35 expect(describeSyncOutcome(summary)).toContain("Waiting for the paired device to connect");36 });3738 it("shows yellow for network-class misses — relay/handshake/timeout are warn, not error", () => {39 40 41 42 expect(syncIndicator({ status: "idle", outcome: "relay_unreachable" }, 1)).toBe("warn");43 expect(syncIndicator({ status: "idle", outcome: "handshake_failed" }, 1)).toBe("warn");44 expect(syncIndicator({ status: "idle", outcome: "sync_timeout" }, 1)).toBe("warn");45 });4647 it("keeps red on genuine configuration / identity problems that the alarm cannot fix", () => {48 expect(syncIndicator({ status: "error", outcome: "vault_id_mismatch" }, 1)).toBe("error");49 expect(syncIndicator({ status: "error", outcome: "private_relay_unset" }, 1)).toBe("error");50 expect(syncIndicator({ status: "error", outcome: "no_eligible_host" }, 1)).toBe("error");51 expect(syncIndicator({ status: "error", outcome: "runner_unavailable" }, 1)).toBe("error");52 expect(syncIndicator({ status: "error", outcome: "run_failed" }, 1)).toBe("error");53 });5455 it("treats vault_locked as warn (yellow) — the user hasn't unlocked yet, not a misconfig", () => {56 57 58 59 60 expect(syncIndicator({ status: "error", outcome: "vault_locked" }, 1)).toBe("warn");61 expect(syncIndicator({ status: "idle", outcome: "vault_locked" }, 1)).toBe("warn");62 });6364 it("returns to green after a successful dial even when the prior attempt was warn", () => {65 66 const prior: { status: "idle"; outcome: "completed_changes" } = {67 status: "idle",68 outcome: "completed_changes",69 };70 expect(syncIndicator(prior, 1)).toBe("ok");71 });7273 it("stays unlit when there is nothing paired and nothing attempted", () => {74 expect(syncIndicator({}, 0)).toBe("idle");75 expect(syncIndicator({ status: "idle", outcome: "no_paired_devices" }, 0)).toBe("idle");76 });7778 it("reads a healthy pair as green before the first attempt of this session", () => {79 expect(syncIndicator({ status: "idle" }, 1)).toBe("ok");80 });8182 it("maps every outcome to its explicit indicator state", () => {83 const expected: Record<SyncOutcome, ReturnType<typeof syncIndicator>> = {84 completed_changes: "ok",85 completed_no_changes: "ok",86 no_paired_devices: "idle",87 no_eligible_host: "error",88 waiting_for_peer: "ok",89 runner_unavailable: "error",90 sync_disabled: "idle",91 private_relay_unset: "error",92 vault_locked: "warn",93 relay_unreachable: "warn",94 handshake_failed: "warn",95 vault_id_mismatch: "error",96 sync_timeout: "warn",97 connection_lost: "warn",98 run_failed: "error",99 };100 for (const [outcome, indicator] of Object.entries(expected)) {101 expect(syncIndicator({ status: "idle", outcome: outcome as SyncOutcome }, 1)).toBe(indicator);102 }103 });104});105106describe("describeSyncOutcome", () => {107 it("names what moved on a successful sync", () => {108 expect(109 describeSyncOutcome({ outcome: "completed_changes", entriesSent: 2, entriesReceived: 1 }),110 ).toBe("Synced — 2 sent, 1 received");111 });112113 it("distinguishes a successful no-op from a failure in plain English", () => {114 expect(describeSyncOutcome({ outcome: "completed_no_changes" }))115 .toBe("Up to date — nothing to send or receive");116 expect(describeSyncOutcome({ outcome: "no_eligible_host" }))117 .not.toContain("Up to date");118 });119120 it("keeps the raw diagnostic detail visible after the plain-English reason", () => {121 122 const line = describeSyncOutcome({123 outcome: "no_eligible_host",124 detail: "Pixel [type=phone, trust=direct]",125 });126 expect(line).toContain("no device here can accept a sync");127 expect(line).toContain("Pixel [type=phone, trust=direct]");128 });129130 it("says nothing has happened yet rather than implying success", () => {131 expect(describeSyncOutcome({})).toBe("Not synced yet");132 });133});134135describe("syncIndicatorLabel", () => {136 it("states the colour's claim in words", () => {137 expect(syncIndicatorLabel("error", { outcome: "vault_id_mismatch" }))138 .toBe("Sync problem — That device holds a different vault");139 });140141 it("states the warn colour's claim in words — the alarm retries", () => {142 expect(syncIndicatorLabel("warn", { outcome: "relay_unreachable" }))143 .toBe("Last sync attempt missed — will retry — Couldn't reach the relay");144 });145});