Files
obsidian-livesync/src/serviceFeatures/syncGuard/SyncGuardState.unit.spec.ts
T

149 lines
6.1 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { SyncGuardState, type SyncGuardPresentation } from "./SyncGuardState";
describe("SyncGuardState", () => {
function setup() {
const presentations: SyncGuardPresentation[] = [];
const onChange = vi.fn((value: SyncGuardPresentation) => presentations.push(value));
const guard = new SyncGuardState(onChange, 2000);
return { guard, onChange, presentations };
}
it("never locks for a small incoming change batch", () => {
const { guard } = setup();
guard.resume(true, 20);
guard.catchUpActivityChanged(true, 0, 0);
guard.receivedDocuments(["f:one", "f:two"]);
guard.catchUpSucceeded();
guard.catchUpActivityChanged(false, 0, 10);
expect(guard.presentation.phase).toBe("idle");
});
it("counts distinct Metadata rather than repeated changes and waits for local application", () => {
const { guard } = setup();
guard.resume(true, 3);
guard.catchUpActivityChanged(true, 0, 0);
guard.receivedDocuments(["f:one", "f:one", "f:two"]);
expect(guard.presentation.phase).toBe("observing");
guard.receivedDocuments(["f:three"]);
expect(guard.presentation).toEqual({ phase: "transferring", receivedFiles: 3 });
guard.receivedDocuments(["f:four", "f:four"]);
expect(guard.presentation.receivedFiles).toBe(4);
guard.catchUpSucceeded();
guard.catchUpActivityChanged(false, 2, 10);
expect(guard.presentation.phase).toBe("applying");
guard.tick(0, 20);
guard.tick(1, 2000);
guard.tick(0, 2100);
guard.tick(0, 4099);
expect(guard.presentation.phase).toBe("applying");
guard.tick(0, 4100);
expect(guard.presentation.phase).toBe("idle");
});
it("keeps the lock with an interrupted transfer until the user bypasses it", () => {
const { guard } = setup();
guard.resume(true, 2);
guard.catchUpActivityChanged(true, 0, 0);
guard.receivedDocuments(["f:one", "f:two"]);
guard.catchUpActivityChanged(false, 0, 10);
expect(guard.presentation.phase).toBe("interrupted");
guard.bypass();
expect(guard.presentation.phase).toBe("idle");
guard.receivedDocuments(["f:other"]);
expect(guard.presentation.phase).toBe("idle");
});
it("does not unlock when a new pull starts while applying an earlier completed transfer", () => {
const { guard } = setup();
guard.resume(true, 1);
guard.catchUpActivityChanged(true, 0, 0);
guard.receivedDocuments(["f:first"]);
guard.catchUpSucceeded();
guard.catchUpActivityChanged(false, 0, 1);
expect(guard.presentation.phase).toBe("applying");
guard.catchUpActivityChanged(true, 0, 100);
expect(guard.presentation.phase).toBe("transferring");
guard.tick(0, 10_000);
expect(guard.presentation.phase).toBe("transferring");
guard.catchUpActivityChanged(false, 0, 10_001);
expect(guard.presentation.phase).toBe("interrupted");
});
it("can recover an interrupted catch-up when a new pull succeeds", () => {
const { guard } = setup();
guard.resume(true, 1);
guard.catchUpActivityChanged(true, 0, 0);
guard.receivedDocuments(["f:first"]);
guard.catchUpActivityChanged(false, 0, 10);
expect(guard.presentation.phase).toBe("interrupted");
guard.catchUpActivityChanged(true, 0, 20);
expect(guard.presentation.phase).toBe("transferring");
guard.catchUpSucceeded();
guard.catchUpActivityChanged(false, 0, 30);
guard.tick(0, 2030);
expect(guard.presentation.phase).toBe("idle");
});
it("does not declare the Vault ready after a failed received-file reflection", () => {
const { guard } = setup();
guard.resume(true, 1);
guard.catchUpActivityChanged(true, 0, 0);
guard.receivedDocuments(["f:first"]);
guard.reflectionFailed();
guard.catchUpSucceeded();
guard.catchUpActivityChanged(false, 0, 10);
guard.tick(0, 10_000);
expect(guard.presentation.phase).toBe("interrupted");
guard.bypass();
expect(guard.presentation.phase).toBe("idle");
});
it("does not treat a stale completion as success for a new catch-up", () => {
const { guard } = setup();
guard.resume(true, 1);
guard.catchUpSucceeded();
guard.catchUpActivityChanged(true, 0, 0);
guard.receivedDocuments(["f:one"]);
guard.catchUpActivityChanged(false, 0, 1);
expect(guard.presentation.phase).toBe("interrupted");
});
it("resets after resume and is opt-in", () => {
const { guard } = setup();
guard.resume(false, 1);
guard.catchUpActivityChanged(true, 0, 0);
guard.receivedDocuments(["f:one"]);
expect(guard.presentation.phase).toBe("idle");
guard.resume(true, 1);
guard.catchUpActivityChanged(true, 0, 10);
guard.receivedDocuments(["f:one"]);
expect(guard.presentation.phase).toBe("transferring");
guard.disable();
expect(guard.presentation.phase).toBe("idle");
});
it("handles NaN and negative thresholds conservatively", () => {
const { guard } = setup();
guard.resume(true, NaN);
guard.catchUpActivityChanged(true, 0, 0);
guard.receivedDocuments(["f:one"]);
expect(guard.presentation.phase).toBe("observing");
guard.resume(true, -2);
guard.catchUpActivityChanged(true, 0, 10);
guard.receivedDocuments(["f:one"]);
expect(guard.presentation.phase).toBe("transferring");
});
it("applies a lowered threshold while observing without unlocking an already protected Vault", () => {
const { guard } = setup();
guard.resume(true, 20);
guard.catchUpActivityChanged(true, 0, 0);
guard.receivedDocuments(["f:one", "f:two", "f:three"]);
guard.updateThreshold(3);
expect(guard.presentation.phase).toBe("transferring");
guard.updateThreshold(200);
expect(guard.presentation.phase).toBe("transferring");
});
});