Files
obsidian-foundry-sync/tests/schema-version.test.ts
Kaysser Kayyali 8fd56a22d9 feat(E0): shared sync primitives — per-uuid lock, ccHash, schema versions
Foundation for the live-sync epic plan (E0). Frozen-contract primitives that
gate E1a/E1b/E2, grounded in the real code shapes.

- synclock.ts: per-UUID bidirectional lock (push/pull/baseline) replacing the
  per-relPath inflight Set. acquire/release/withLock/isHeld/heldOps, skip|queue
  policy, reentrant-NO, relPath: fallback. Queue path retries until acquired or
  throws LockAcquireTimeout (no silent drop); fairness — acquire defers to
  queued waiters. SYNC_LOCK_ENABLED flag (default on; off = byte-identical
  legacy inflight).
- cchash.ts: ccHash(entry, inverse) + ccHashFromGet, frozen CC_HASH_CONTRACT.
  Contract corrected from epic prose: the body spans data.description +
  data.notes (CcData object, not a string), with ## Secrets re-inserted.
  Throws CcHashError on missing description (not coerced to ""). Three E1a-gate
  assumptions documented (sidebar exclusion, Secrets heading/case, section order).
- schema-version.ts: FLAGS_SCHEMA_VERSION / SYNC_STATE_SCHEMA_VERSION branded
  constants + parseSchemaVersion (branded discriminated union).
- server.ts: AutoSyncController refactored to use the lock (single read; uuid
  resolved from the same read used to gate — no stale-uuid gap; uuidCache powers
  the debounce pre-check without a file read). AutoSyncController/State exported,
  lock public for E2's poll path.
- 43 tests across 4 files (synclock, cchash, schema-version, server-lock
  integration incl. cross-direction block + flag-off byte-identical). tsc clean.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-22 22:01:57 +00:00

43 lines
2.3 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
FLAGS_SCHEMA_VERSION,
SYNC_STATE_SCHEMA_VERSION,
parseSchemaVersion,
} from "../src/schema-version.js";
import type { FlagsSchemaVersion, SyncStateSchemaVersion } from "../src/schema-version.js";
describe("schema-version constants (E0.3)", () => {
it("are unequal strings with distinct prefixes", () => {
expect(FLAGS_SCHEMA_VERSION).not.toBe(SYNC_STATE_SCHEMA_VERSION);
expect(FLAGS_SCHEMA_VERSION.startsWith("flags-")).toBe(true);
expect(SYNC_STATE_SCHEMA_VERSION.startsWith("sync-")).toBe(true);
expect(typeof FLAGS_SCHEMA_VERSION).toBe("string");
expect(typeof SYNC_STATE_SCHEMA_VERSION).toBe("string");
});
it("parseSchemaVersion branches on prefix and returns null for unknown", () => {
expect(parseSchemaVersion(FLAGS_SCHEMA_VERSION)).toEqual({ kind: "flags", version: FLAGS_SCHEMA_VERSION });
expect(parseSchemaVersion(SYNC_STATE_SCHEMA_VERSION)).toEqual({ kind: "sync-state", version: SYNC_STATE_SCHEMA_VERSION });
expect(parseSchemaVersion("flags-campaign-codex/v2")).toEqual({ kind: "flags", version: "flags-campaign-codex/v2" });
expect(parseSchemaVersion("sync-state/v2")).toEqual({ kind: "sync-state", version: "sync-state/v2" });
expect(parseSchemaVersion("unknown/v1")).toBeNull();
expect(parseSchemaVersion("")).toBeNull();
});
it("branded types are not assignable to each other (compile-time guard)", () => {
// Nominal brands via distinct property names (__flagsBrand / __syncBrand).
// If a brand ever stops preventing cross-assignment, one of these conditional
// types flips to `true` and the matching `= false as const` line errors at
// compile time (true not assignable to false) — the guard is enforced by
// tsc, not just asserted in prose.
type FlagsExtendsSync = [FlagsSchemaVersion] extends [SyncStateSchemaVersion] ? true : false;
type SyncExtendsFlags = [SyncStateSchemaVersion] extends [FlagsSchemaVersion] ? true : false;
const flagsExtendsSync: FlagsExtendsSync = false as const;
const syncExtendsFlags: SyncExtendsFlags = false as const;
expect(flagsExtendsSync).toBe(false);
expect(syncExtendsFlags).toBe(false);
// Runtime sanity: the brands are still distinct string values at runtime.
expect(FLAGS_SCHEMA_VERSION).not.toBe(SYNC_STATE_SCHEMA_VERSION);
});
});