sync-outcome.test.ts

6.0 KB · raw

1import { describe, expect, it } from "vitest";2import {3  describeSyncOutcome,4  isSyncFailure,5  isSyncWarning,6  syncIndicator,7  syncIndicatorLabel,8  type SyncOutcome,9} from "./sync-outcome";1011describe("syncIndicator", () => {12  it("pulses green only while a sync is actually running", () => {13    expect(syncIndicator({ status: "syncing" }, 1)).toBe("busy");14    // A running sync outranks a stale failure — the light describes now.15    expect(syncIndicator({ status: "syncing", outcome: "relay_unreachable" }, 1)).toBe("busy");16  });1718  it("is solid green for a completed sync, with or without changes", () => {19    expect(syncIndicator({ status: "idle", outcome: "completed_changes" }, 1)).toBe("ok");20    expect(syncIndicator({ status: "idle", outcome: "completed_no_changes" }, 1)).toBe("ok");21  });2223  it("never shows green for a paired-but-unsyncable device", () => {24    // The exact case that used to be a silent return: a device is paired, so25    // "you have a pair" is true, but nothing can sync. Green here would lie.26    expect(syncIndicator({ status: "error", outcome: "no_eligible_host" }, 2)).toBe("error");27    expect(syncIndicator({ status: "error", outcome: "runner_unavailable" }, 2)).toBe("error");28  });2930  it("shows yellow for network-class misses — relay/handshake/timeout are warn, not error", () => {31    // The alarm fires every 10 minutes and dials. If the peer doesn't answer,32    // the next alarm tick will try again. That is the network, not the app —33    // red would be alarm fatigue. These must be warn (yellow), not error.34    expect(syncIndicator({ status: "idle", outcome: "relay_unreachable" }, 1)).toBe("warn");35    expect(syncIndicator({ status: "idle", outcome: "handshake_failed" }, 1)).toBe("warn");36    expect(syncIndicator({ status: "idle", outcome: "sync_timeout" }, 1)).toBe("warn");37  });3839  it("keeps red on genuine configuration / identity problems that the alarm cannot fix", () => {40    expect(syncIndicator({ status: "error", outcome: "vault_id_mismatch" }, 1)).toBe("error");41    expect(syncIndicator({ status: "error", outcome: "private_relay_unset" }, 1)).toBe("error");42    expect(syncIndicator({ status: "error", outcome: "no_eligible_host" }, 1)).toBe("error");43    expect(syncIndicator({ status: "error", outcome: "runner_unavailable" }, 1)).toBe("error");44    expect(syncIndicator({ status: "error", outcome: "run_failed" }, 1)).toBe("error");45  });4647  it("treats vault_locked as warn (yellow) — the user hasn't unlocked yet, not a misconfig", () => {48    // vault_locked is the reader's vault being closed (e.g. SW restarted after49    // the popup was last dismissed). The next unlock + retry will succeed; the50    // alarm keeps trying. Same yellow semantics as a missed dial — a red pip51    // here would be misleading alarm fatigue.52    expect(syncIndicator({ status: "error", outcome: "vault_locked" }, 1)).toBe("warn");53    expect(syncIndicator({ status: "idle", outcome: "vault_locked" }, 1)).toBe("warn");54  });5556  it("returns to green after a successful dial even when the prior attempt was warn", () => {57    // The state machine must not get stuck on a warn that already cleared.58    const prior: { status: "idle"; outcome: "completed_changes" } = {59      status: "idle",60      outcome: "completed_changes",61    };62    expect(syncIndicator(prior, 1)).toBe("ok");63  });6465  it("stays unlit when there is nothing paired and nothing attempted", () => {66    expect(syncIndicator({}, 0)).toBe("idle");67    expect(syncIndicator({ status: "idle", outcome: "no_paired_devices" }, 0)).toBe("idle");68  });6970  it("reads a healthy pair as green before the first attempt of this session", () => {71    expect(syncIndicator({ status: "idle" }, 1)).toBe("ok");72  });7374  it("resolves every outcome to exactly one indicator, none falling through to ok", () => {75    const outcomes: SyncOutcome[] = [76      "completed_changes", "completed_no_changes", "no_paired_devices",77      "no_eligible_host", "runner_unavailable", "sync_disabled",78      "private_relay_unset", "vault_locked", "relay_unreachable",79      "handshake_failed", "vault_id_mismatch", "sync_timeout", "run_failed",80    ];81    for (const outcome of outcomes) {82      const light = syncIndicator({ status: "idle", outcome }, 1);83      expect(isSyncFailure(outcome) ? light : "not-error").not.toBe("ok");84      if (isSyncFailure(outcome)) expect(light).toBe("error");85      if (isSyncWarning(outcome)) expect(light).toBe("warn");86    }87  });88});8990describe("describeSyncOutcome", () => {91  it("names what moved on a successful sync", () => {92    expect(93      describeSyncOutcome({ outcome: "completed_changes", entriesSent: 2, entriesReceived: 1 }),94    ).toBe("Synced — 2 sent, 1 received");95  });9697  it("distinguishes a successful no-op from a failure in plain English", () => {98    expect(describeSyncOutcome({ outcome: "completed_no_changes" }))99      .toBe("Up to date — nothing to send or receive");100    expect(describeSyncOutcome({ outcome: "no_eligible_host" }))101      .not.toContain("Up to date");102  });103104  it("keeps the raw diagnostic detail visible after the plain-English reason", () => {105    // On a real device there is no devtools; the inventory is the whole clue.106    const line = describeSyncOutcome({107      outcome: "no_eligible_host",108      detail: "Pixel [type=phone, trust=direct]",109    });110    expect(line).toContain("no device here can accept a sync");111    expect(line).toContain("Pixel [type=phone, trust=direct]");112  });113114  it("says nothing has happened yet rather than implying success", () => {115    expect(describeSyncOutcome({})).toBe("Not synced yet");116  });117});118119describe("syncIndicatorLabel", () => {120  it("states the colour's claim in words", () => {121    expect(syncIndicatorLabel("error", { outcome: "vault_id_mismatch" }))122      .toBe("Sync problem — That device holds a different vault");123  });124125  it("states the warn colour's claim in words — the alarm retries", () => {126    expect(syncIndicatorLabel("warn", { outcome: "relay_unreachable" }))127      .toBe("Last sync attempt missed — will retry — Couldn't reach the relay");128  });129});