chat / client / test / log-effects.test.mjs
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
 * lib/log's update chain — ordering, once-per-hash, and the return types.
 *
 * rk-log.json pins the projection and says nothing about WHEN anything runs,
 * and log's six `_` seams were never driven at all: `logScript` destructures
 * `priv` and then does `void priv;`. Everything that makes this file correct is
 * a property of the chain, so a re-decomposition could keep every existing
 * vector green and still change what the app sees.
 *
 * helpers/log-effects.mjs states the three properties and why each matters.
 */
import test from "node:test";
import assert from "node:assert/strict";
import { installDom } from "./harness/dom.mjs";
import { mods, readVector } from "./helpers/load.mjs";
import { fakeStack } from "./helpers/fakes.mjs";

installDom();
const M = await mods();
const S = await import("./helpers/log-effects.mjs");

const V = readVector("log-effects");
const env = {
  RoomLog: M.RoomLog,
  RoomCrypto: M.RoomCrypto,
  makeLogEncryption: M.makeLogEncryption,
  fakeStack,
};
const isEntry = (e) => e[0] === "onEntry";
const isSweep = (e) => e[0] === "db.iterator";

test("onEntry fires exactly once per hash, oldest first, and sweeps only when needed", async () => {
  const got = await S.updateOrdering(env);
  assert.deepEqual(got.transcript, V.updateOrdering.transcript);

  // sweep-unseen collects the oplog iterator's newest→oldest and REVERSES it,
  // so the app sees the log in log order
  assert.deepEqual(got.emittedHashes, ["e1", "e2", "e3", "e4", "e5", "e6", "e7"]);
  // exactly once each: no hash appears twice, and the count equals the set size
  assert.equal(got.emittedHashes.length, new Set(got.emittedHashes).size);
  assert.equal(got.transcript.filter(isEntry).length, 7);

  // exactly ONE sweep in the whole run — the first update, whose `next` named
  // an unseen hash. Every later update found its links already seen.
  assert.equal(got.transcript.filter(isSweep).length, 1);
  assert.equal(got.transcript.findIndex(isSweep), 1, "and it happened before the first emit");

  // `_seen` is read at RUN time, not at SCHEDULE time: e6 and e7 were queued in
  // the same turn, e7 links to e6, and at queue time e6 was unseen. Hoisting
  // that check out of the `.then` — precisely what a `p/let` rewrite does —
  // would have swept for e7.
  assert.equal(got.sweepsForThePair, 0);

  assert.equal(got.onUpdateReturn, "undefined");
});

test("on-update returns undefined; emit-unseen returns the chain mailbox awaits", async () => {
  const got = await S.returnTypes(env);
  assert.deepEqual(got.transcript, V.returnTypes.transcript);

  // lib/mailbox's join-phase does `js-await [_ (.emitUnseen log)]` and only then
  // reports how many entries joined. If emit-unseen returned undefined the
  // await would resolve immediately, mailbox would report and flush before the
  // entries had surfaced, and nothing in either file's tests would notice.
  assert.equal(got.emitUnseen.isThenable, true);
  assert.equal(got.emitUnseen.isTheChain, true, "the returned value IS _updateChain, not a copy");
  assert.equal(got.emitUnseen.emittedBeforeAwait, 0);
  assert.equal(got.emitUnseen.emittedAfterAwait, 2, "awaiting it means the sweep has finished");

  // and the other half of the pair: on-update is fire-and-forget
  assert.equal(got.onUpdateReturn, "undefined");
});

test("the log-effects vectors are not vacuous", () => {
  const clone = (x) => JSON.parse(JSON.stringify(x));
  const t = V.updateOrdering.transcript;

  // emission order reversed — sweep-unseen without its .reverse
  const reversed = clone(t);
  const idx = reversed.map((e, i) => [e, i]).filter(([e]) => isEntry(e)).map(([, i]) => i);
  const rows = idx.map((i) => reversed[i]).reverse();
  idx.forEach((i, k) => { reversed[i] = rows[k]; });
  assert.notDeepEqual(reversed, t);

  // a hash emitted twice — `_seen` not marked, or marked too late
  const twice = clone(t);
  twice.splice(twice.findIndex(isEntry) + 1, 0, ["onEntry", "e1", 1]);
  assert.notDeepEqual(twice, t);

  // a second sweep — the schedule-time check
  const swept = clone(t);
  swept.push(["db.iterator", 500]);
  assert.notDeepEqual(swept, t);

  // a dropped emit
  const dropped = clone(t).filter((e) => !(isEntry(e) && e[1] === "e5"));
  assert.notEqual(dropped.length, t.length);
  assert.notDeepEqual(dropped, t);

  // the return-type record, perturbed both ways
  const rt = clone(V.returnTypes);
  rt.emitUnseen.isThenable = false;
  assert.notDeepEqual(rt.emitUnseen, V.returnTypes.emitUnseen);
  const rt2 = clone(V.returnTypes);
  rt2.emitUnseen.emittedAfterAwait = 0;
  assert.notDeepEqual(rt2.emitUnseen, V.returnTypes.emitUnseen);

  // ...and the recorded transcripts are real
  assert.equal(t.filter(isEntry).length, 7);
  assert.equal(t.filter(isSweep).length, 1);
  assert.equal(V.returnTypes.transcript.filter(isEntry).length, 3);
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/chat.git