1import { describe, expect, it } from "vitest";2import {3 describeSyncOutcome,4 isManualSyncPending,5 syncIndicator,6 syncIndicatorLabel,7 type SyncOutcome,8} from "./sync-outcome";910describe("syncIndicator", () => {11 it("keeps a manual waiter pending until its inbound session reports an outcome", () => {12 expect(isManualSyncPending({ status: "syncing" })).toBe(true);13 expect(isManualSyncPending({ status: "idle", outcome: "waiting_for_peer" })).toBe(true);14 expect(isManualSyncPending({ status: "idle", outcome: "completed_no_changes" })).toBe(false);15 });16 it("pulses green only while a sync is actually running", () => {17 expect(syncIndicator({ status: "syncing" }, 1)).toBe("busy");18 19 expect(syncIndicator({ status: "syncing", outcome: "relay_unreachable" }, 1)).toBe("busy");20 });2122 it("is solid green for a completed sync, with or without changes", () => {23 expect(syncIndicator({ status: "idle", outcome: "completed_changes" }, 1)).toBe("ok");24 expect(syncIndicator({ status: "idle", outcome: "completed_no_changes" }, 1)).toBe("ok");25 });2627 it("never shows green for a paired-but-unsyncable device", () => {28 29 30 expect(syncIndicator({ status: "error", outcome: "no_eligible_host" }, 2)).toBe("error");31 expect(syncIndicator({ status: "error", outcome: "runner_unavailable" }, 2)).toBe("error");32 });3334 it("shows listener-only mesh role as healthy while waiting for its peer", () => {35 const summary = {36 status: "idle" as const,37 outcome: "waiting_for_peer" as const,38 detail: "Android [type=android, trust=direct]",39 };40 expect(syncIndicator(summary, 1)).toBe("ok");41 expect(describeSyncOutcome(summary)).toContain("Waiting for the paired device to connect");42 });4344 it("shows yellow for network-class misses — relay/handshake/timeout are warn, not error", () => {45 46 47 48 expect(syncIndicator({ status: "idle", outcome: "relay_unreachable" }, 1)).toBe("warn");49 expect(syncIndicator({ status: "idle", outcome: "handshake_failed" }, 1)).toBe("warn");50 expect(syncIndicator({ status: "idle", outcome: "sync_timeout" }, 1)).toBe("warn");51 });5253 it("keeps red on genuine configuration / identity problems that the alarm cannot fix", () => {54 expect(syncIndicator({ status: "error", outcome: "vault_id_mismatch" }, 1)).toBe("error");55 expect(syncIndicator({ status: "error", outcome: "private_relay_unset" }, 1)).toBe("error");56 expect(syncIndicator({ status: "error", outcome: "no_eligible_host" }, 1)).toBe("error");57 expect(syncIndicator({ status: "error", outcome: "runner_unavailable" }, 1)).toBe("error");58 expect(syncIndicator({ status: "error", outcome: "run_failed" }, 1)).toBe("error");59 });6061 it("treats vault_locked as warn (yellow) — the user hasn't unlocked yet, not a misconfig", () => {62 63 64 65 66 expect(syncIndicator({ status: "error", outcome: "vault_locked" }, 1)).toBe("warn");67 expect(syncIndicator({ status: "idle", outcome: "vault_locked" }, 1)).toBe("warn");68 });6970 it("returns to green after a successful dial even when the prior attempt was warn", () => {71 72 const prior: { status: "idle"; outcome: "completed_changes" } = {73 status: "idle",74 outcome: "completed_changes",75 };76 expect(syncIndicator(prior, 1)).toBe("ok");77 });7879 it("stays unlit when there is nothing paired and nothing attempted", () => {80 expect(syncIndicator({}, 0)).toBe("idle");81 expect(syncIndicator({ status: "idle", outcome: "no_paired_devices" }, 0)).toBe("idle");82 });8384 it("reads a healthy pair as green before the first attempt of this session", () => {85 expect(syncIndicator({ status: "idle" }, 1)).toBe("ok");86 });8788 it("maps every outcome to its explicit indicator state", () => {89 const expected: Record<SyncOutcome, ReturnType<typeof syncIndicator>> = {90 completed_changes: "ok",91 completed_no_changes: "ok",92 no_paired_devices: "idle",93 no_eligible_host: "error",94 waiting_for_peer: "ok",95 runner_unavailable: "error",96 sync_disabled: "idle",97 private_relay_unset: "error",98 vault_locked: "warn",99 relay_unreachable: "warn",100 handshake_failed: "warn",101 vault_id_mismatch: "error",102 peer_protocol_outdated: "error",103 104 reset_required: "error",105 sync_generation_unknown: "error",106 sync_timeout: "warn",107 connection_lost: "warn",108 trust_data_corrupted: "error",109 run_failed: "error",110 };111 for (const [outcome, indicator] of Object.entries(expected)) {112 expect(syncIndicator({ status: "idle", outcome: outcome as SyncOutcome }, 1)).toBe(indicator);113 }114 });115});116117describe("describeSyncOutcome", () => {118 it("names what moved on a successful sync", () => {119 expect(120 describeSyncOutcome({ outcome: "completed_changes", entriesSent: 2, entriesReceived: 1 }),121 ).toBe("Synced — 2 sent, 1 received");122 });123124 it("distinguishes a successful no-op from a failure in plain English", () => {125 expect(describeSyncOutcome({ outcome: "completed_no_changes" }))126 .toBe("Up to date — nothing to send or receive");127 expect(describeSyncOutcome({ outcome: "no_eligible_host" }))128 .not.toContain("Up to date");129 });130131 it("keeps the raw diagnostic detail visible after the plain-English reason", () => {132 133 const line = describeSyncOutcome({134 outcome: "no_eligible_host",135 detail: "Pixel [type=phone, trust=direct]",136 });137 expect(line).toContain("no device here can accept a sync");138 expect(line).toContain("Pixel [type=phone, trust=direct]");139 });140141 it("says nothing has happened yet rather than implying success", () => {142 expect(describeSyncOutcome({})).toBe("Not synced yet");143 });144});145146describe("syncIndicatorLabel", () => {147 it("states the colour's claim in words", () => {148 expect(syncIndicatorLabel("error", { outcome: "vault_id_mismatch" }))149 .toBe("Sync problem — That device holds a different vault");150 });151152 it("states the warn colour's claim in words — the alarm retries", () => {153 expect(syncIndicatorLabel("warn", { outcome: "relay_unreachable" }))154 .toBe("Last sync attempt missed — will retry — Couldn't reach the relay");155 });156});