Commit graph

1 commit

Author SHA1 Message Date
Souta
1e638471a4 test(plugin): Phase C.M7 — FakeFileExplorer test double for UI-reflect (0.4.45)
Pure-helper module; production code: untouched. Lays the foundation
for M8 (`assertSyncReflect`) and M9 (E2E matrix) — the slice of
Phase C that brings UI-reflect (the T5 wedge of the sync-latency
sequence) under automated assertion.

What ships:

- `plugin/tests/helpers/FakeFileExplorer.ts` (new)

  A test double that listens to the same `vault.trigger(...)` event
  stream Obsidian's File Explorer + MetadataCache + plugins
  subscribe to, and maintains a small synthetic "what does this
  vault look like after the events that have fired so far" model.
  M9 will use it as the **T5 observation point**: once
  `awaitReflect(path, event)` resolves, an Obsidian-faithful
  downstream consumer would also have observed the change.

  Surface:
    - `attach(vault: VaultLike) → dispose`: subscribes to all four
      vault events (create/modify/delete/rename) via `vault.on(...)`,
      returns a disposer that calls `vault.offref(...)` for each
      registration. `VaultLike` is a structural type
      (`Pick<Vault,'on'|'offref'>`) so per-test stubs only have to
      expose those two methods — no real `obsidian.Vault` runtime
      required.
    - `observe(event, ...args)`: direct event injection for tests
      that don't even want a Vault stub. Mirrors the
      `vault.trigger(event, ...args)` arg shape so a FakeVault's
      `trigger` can forward straight here.
    - `snapshot() → { paths[], mtimes }`: deterministic
      sorted-paths view of the current model. Folders are present
      in `paths` but absent from `mtimes` (mirroring the real
      vault, where TFolder has no `stat`).
    - `awaitReflect(path, event, timeoutMs?)`: resolves with
      `{ atMs }` (the `performance.now()` from when the underlying
      event fired — that's what M9 will feed into PerfAggregator's
      S.paint bucket; late-arriving awaits don't inflate latency).
      History-walks first so an event observed BEFORE
      `awaitReflect` was called still resolves it (eliminates a
      class of listener-race flakes); times out with a descriptive
      error including the recent history tail.
    - `reset()`: drop state and reject pending awaits — for
      `afterEach`-style hygiene.

  Folder semantics intentionally mirror VaultModelBuilder:
    - delete on a folder also drops descendants from the snapshot
      (vault.trigger fires once for the folder itself; descendants
      are silently orphaned in the production fileMap and we don't
      want the FE to lie about what File Explorer would show).
    - rename on a folder rewrites every descendant path under the
      new prefix.

  history is bounded (cap 1024 entries) so a long-running suite
  doesn't grow it unbounded; the cap matters in M9 where each test
  may observe dozens of events.

- `plugin/tests/FakeFileExplorer.test.ts` (new, 20 tests):

    observe / snapshot:
      - empty start
      - create adds path + records mtime
      - create on a folder records path with no mtime
      - modify updates mtime
      - delete removes path + mtime
      - folder delete removes descendants (depth ≥ 2)
      - rename moves path, preserves mtime when no new stat
      - rename uses the new file's stat when present
      - folder rename rewrites descendants

    awaitReflect:
      - resolves from history when event already fired
      - second await for same event doesn't double-consume
      - resolves a pending waiter when event arrives later
      - waiter matches its event even when others fire first
      - times out with descriptive error
      - timeout leaves no leaked waiter (later observe doesn't crash)

    attach():
      - subscribes to all four events (verified via FakeVault's
        listenerCount)
      - disposer detaches all four
      - post-dispose, vault events no longer reflected

    reset():
      - drops state
      - rejects pending waiters

  Inline `FakeVault` helper (~30 lines) covers `on`/`offref`/
  `trigger`/`listenerCount`. Not extracted — it's the only consumer
  in the unit suite; M9 will likely pull a similar shape into its
  own integration helper.

Verification:

- `npx vitest run` — 35 files / 474 tests green (was 454, +20 new)
- `npx tsc --noEmit -p tsconfig.json` — clean

manifest/package/versions bumped 0.4.44 → 0.4.45.

Roadmap context: M7 is the prerequisite for M8 (assertSyncReflect)
and M9 (E2E matrix). After M8 lands, the assertion vocabulary
is "writer mutation → reader notification → FakeFileExplorer
reflects path/event"; M9 fills out the case matrix
(create / modify / delete / rename / large / binary / nested /
conflict / disconnect-reconnect).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 07:44:26 +09:00