mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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]>
61 lines
2.4 KiB
JavaScript
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);
|
|
});
|