Files
bitvid/tests/replaceable-created-at.test.mjs
thePR0M3TH3ANandClaude Opus 4.8 2dc7397535 [hashtags] Monotonic created_at so a rapid second edit doesn't false-error (#57)
Adding a second hashtag interest/disinterest quickly showed "didn't work" yet the
change persisted after a refresh. Cause: the replaceable preferences event used
created_at = floor(now/1000), so two edits in the same wall-clock second produced
the SAME created_at. Relays reject the second as "not newer" (client shows an
error) while relays that break created_at ties by id still store it (so it appears
after a refresh) — an inconsistent false-negative.

Fix: nextReplaceableCreatedAt(lastCreatedAt) (js/nostr/replaceableCreatedAt.js) —
always advance to max(now, lastCreatedAt + 1), so each publish is strictly newer
than the last and every relay accepts it unambiguously. Reusable for the other
replaceable lists (subscriptions/blocks) that have the same latent collision.

Tests: tests/replaceable-created-at.test.mjs (5 — same-second bump, clock-ahead,
no-prior, rapid successive strictly-increasing, floors fractional). Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-01 19:41:00 -04:00

61 lines
2.4 KiB
JavaScript

// Replaceable events (hashtag prefs, subscriptions, blocks) must publish with a
// created_at STRICTLY greater than the last one, or relays reject the second of two
// quick edits as "not newer" — the "added a second disinterest, it errored but
// works after refresh" bug. nextReplaceableCreatedAt guarantees monotonicity.
//
// test_integrity_note:
// change_type: ["new_tests"]
// scenarios:
// - id: SCN-replaceable-created-at
// given: "the last-published created_at + the current clock"
// when: "nextReplaceableCreatedAt computes the next created_at"
// then: "it is strictly greater than the last, and >= now"
// observable_outcomes:
// - "same wall-clock second as the last -> last+1 (never equal)"
// - "clock ahead of last -> uses now"
// - "no prior event -> uses now"
// - "successive rapid calls strictly increase"
// determinism_controls:
// - "pure function with an injected clock"
// anti_cheat_rationale:
// prevents: ["hard-coded return value"]
// relaxation:
// did_relax_any_assertion: false
import test from "node:test";
import { strict as assert } from "node:assert";
import { nextReplaceableCreatedAt } from "../js/nostr/replaceableCreatedAt.js";
test("same wall-clock second as the last event -> strictly greater (last+1)", () => {
assert.equal(nextReplaceableCreatedAt(1000, 1000), 1001);
assert.equal(nextReplaceableCreatedAt(1000, 999), 1001, "even a laggy clock advances");
});
test("clock ahead of the last event -> uses the clock", () => {
assert.equal(nextReplaceableCreatedAt(1000, 2000), 2000);
});
test("no prior event -> uses the clock", () => {
assert.equal(nextReplaceableCreatedAt(0, 500), 500);
assert.equal(nextReplaceableCreatedAt(undefined, 500), 500);
assert.equal(nextReplaceableCreatedAt(null, 500), 500);
});
test("rapid successive edits in the same second strictly increase", () => {
const now = 1_700_000_000;
let last = 0;
const seen = [];
for (let i = 0; i < 5; i += 1) {
last = nextReplaceableCreatedAt(last, now); // clock frozen within the same second
seen.push(last);
}
for (let i = 1; i < seen.length; i += 1) {
assert.ok(seen[i] > seen[i - 1], `edit ${i} must be strictly newer than ${i - 1}`);
}
assert.deepEqual(seen, [now, now + 1, now + 2, now + 3, now + 4]);
});
test("floors fractional inputs (created_at is unix seconds)", () => {
assert.equal(nextReplaceableCreatedAt(1000.9, 1000.4), 1001);
});