sync-outcome.test.ts

3.6 KB · raw

1import { describe, expect, it } from "vitest";2import {3  describeSyncOutcome,4  isSyncFailure,5  syncIndicator,6  syncIndicatorLabel,7  type SyncOutcome,8} from "./sync-outcome";910describe("syncIndicator", () => {11  it("pulses green only while a sync is actually running", () => {12    expect(syncIndicator({ status: "syncing" }, 1)).toBe("busy");13    // A running sync outranks a stale failure — the light describes now.14    expect(syncIndicator({ status: "syncing", outcome: "relay_unreachable" }, 1)).toBe("busy");15  });1617  it("is solid green for a completed sync, with or without changes", () => {18    expect(syncIndicator({ status: "idle", outcome: "completed_changes" }, 1)).toBe("ok");19    expect(syncIndicator({ status: "idle", outcome: "completed_no_changes" }, 1)).toBe("ok");20  });2122  it("never shows green for a paired-but-unsyncable device", () => {23    // The exact case that used to be a silent return: a device is paired, so24    // "you have a pair" is true, but nothing can sync. Green here would lie.25    expect(syncIndicator({ status: "error", outcome: "no_eligible_host" }, 2)).toBe("error");26    expect(syncIndicator({ status: "error", outcome: "runner_unavailable" }, 2)).toBe("error");27  });2829  it("stays unlit when there is nothing paired and nothing attempted", () => {30    expect(syncIndicator({}, 0)).toBe("idle");31    expect(syncIndicator({ status: "idle", outcome: "no_paired_devices" }, 0)).toBe("idle");32  });3334  it("reads a healthy pair as green before the first attempt of this session", () => {35    expect(syncIndicator({ status: "idle" }, 1)).toBe("ok");36  });3738  it("resolves every outcome to exactly one indicator, none falling through to ok", () => {39    const outcomes: SyncOutcome[] = [40      "completed_changes", "completed_no_changes", "no_paired_devices",41      "no_eligible_host", "runner_unavailable", "sync_disabled",42      "private_relay_unset", "vault_locked", "relay_unreachable",43      "handshake_failed", "vault_id_mismatch", "sync_timeout", "run_failed",44    ];45    for (const outcome of outcomes) {46      const light = syncIndicator({ status: "idle", outcome }, 1);47      expect(isSyncFailure(outcome) ? light : "not-error").not.toBe("ok");48      if (isSyncFailure(outcome)) expect(light).toBe("error");49    }50  });51});5253describe("describeSyncOutcome", () => {54  it("names what moved on a successful sync", () => {55    expect(56      describeSyncOutcome({ outcome: "completed_changes", entriesSent: 2, entriesReceived: 1 }),57    ).toBe("Synced — 2 sent, 1 received");58  });5960  it("distinguishes a successful no-op from a failure in plain English", () => {61    expect(describeSyncOutcome({ outcome: "completed_no_changes" }))62      .toBe("Up to date — nothing to send or receive");63    expect(describeSyncOutcome({ outcome: "no_eligible_host" }))64      .not.toContain("Up to date");65  });6667  it("keeps the raw diagnostic detail visible after the plain-English reason", () => {68    // On a real device there is no devtools; the inventory is the whole clue.69    const line = describeSyncOutcome({70      outcome: "no_eligible_host",71      detail: "Pixel [type=phone, trust=direct]",72    });73    expect(line).toContain("no device here can accept a sync");74    expect(line).toContain("Pixel [type=phone, trust=direct]");75  });7677  it("says nothing has happened yet rather than implying success", () => {78    expect(describeSyncOutcome({})).toBe("Not synced yet");79  });80});8182describe("syncIndicatorLabel", () => {83  it("states the colour's claim in words", () => {84    expect(syncIndicatorLabel("error", { outcome: "vault_id_mismatch" }))85      .toBe("Sync problem — That device holds a different vault");86  });87});