Files
bitvid/tests/nostr/utils.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 1b11cb1bc1 [nostr] make video edit/delete reliably replace prior versions
Two replaceable-event (NIP-33/kind 30078) lifecycle fixes found auditing
edit/delete/tombstone:

1. created_at monotonicity. Edits and delete tombstones used bare
   Date.now(), with no guarantee of being newer than the version they replace.
   NIP-01 keeps the highest created_at (ties -> lowest id), so a device clock
   that's behind — or a second edit within the same second — left relays
   serving the OLD version: edits silently didn't apply, and (worse) deletions
   didn't actually remove the video for other clients (the local in-memory
   tombstone masked the failure locally). The edit and revert/delete builders
   now use max(now, baseEvent.created_at + 1). created_at is threaded through
   revertVideo and _softDeleteVersions so the bulk-delete path keeps it too.

2. Legacy tombstone key mismatch. The deletion builder synthesizes
   videoRootId = "LEGACY:<pubkey>:<dTag>" for legacy videos, so its tombstone
   keyed as "ROOT:LEGACY:..." while the original legacy event (no videoRootId)
   keyed as "<pubkey>:<dTag>" — the tombstone never matched the zombie it was
   meant to suppress. getActiveKey now treats a "LEGACY:"-prefixed root as
   non-distinct and falls through to the pubkey:dTag identity, so both match.

Verified sound and unchanged: d-tag + videoRootId preservation on edit,
ownership/version checks, dual delete (NIP-09 kind-5 + per-d-tag soft-delete),
and the tombstone guard wired across all ingestion paths.

Tests: edit/tombstone created_at is bumped above a future-dated base
(clock-skew) and uses wall-clock for a past base; a legacy deletion shares the
original event's key. Both fixes mutation-verified.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 20:32:50 -04:00

85 lines
2.6 KiB
JavaScript

/**
* @file tests/nostr/utils.test.mjs
* @description Unit tests for js/nostr/utils.js
*/
import { test, describe } from 'node:test';
import assert from 'node:assert';
import { getActiveKey } from '../../js/nostr/utils.js';
describe('js/nostr/utils.js', () => {
describe('getActiveKey', () => {
test('returns ROOT key when videoRootId is present', () => {
const video = {
videoRootId: 'root-123',
id: 'note-456',
pubkey: 'pk1',
tags: [['d', 'd-tag-val']]
};
const key = getActiveKey(video);
assert.strictEqual(key, 'ROOT:root-123');
});
test('returns pubkey:d key when videoRootId is missing but d-tag exists', () => {
const video = {
id: 'note-456',
pubkey: 'pk1',
tags: [['d', 'd-tag-val']]
};
const key = getActiveKey(video);
assert.strictEqual(key, 'pk1:d-tag-val');
});
test('returns LEGACY key when videoRootId and d-tag are missing', () => {
const video = {
id: 'note-456',
pubkey: 'pk1',
tags: [['t', 'hashtag']]
};
const key = getActiveKey(video);
assert.strictEqual(key, 'LEGACY:note-456');
});
test('handles missing tags array gracefully (returns LEGACY)', () => {
const video = {
id: 'note-789',
pubkey: 'pk2'
};
const key = getActiveKey(video);
assert.strictEqual(key, 'LEGACY:note-789');
});
test('handles empty tags array gracefully (returns LEGACY)', () => {
const video = {
id: 'note-789',
pubkey: 'pk2',
tags: []
};
const key = getActiveKey(video);
assert.strictEqual(key, 'LEGACY:note-789');
});
test('a synthesized LEGACY: root keys the same as the bare legacy event', () => {
// The deletion builder synthesizes videoRootId = "LEGACY:<pubkey>:<dTag>"
// for legacy videos. Its tombstone must map to the SAME key as the
// original legacy event (which has no videoRootId), or the zombie is never
// suppressed.
const original = { id: 'note-1', pubkey: 'pk1', tags: [['d', 'd-val']] };
const deletion = {
id: 'note-2',
pubkey: 'pk1',
tags: [['d', 'd-val']],
videoRootId: 'LEGACY:pk1:d-val',
};
const originalKey = getActiveKey(original);
const deletionKey = getActiveKey(deletion);
assert.strictEqual(originalKey, 'pk1:d-val');
assert.strictEqual(
deletionKey,
originalKey,
'legacy deletion tombstone must share the original event key',
);
});
});
});