See the best posts from any Bluesky account
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add tombstone test covering embed_json clearing

Verifies that tombstoneUserSnapshots inserts a tombstone row with
embed_json='' even when the original snapshot carried an ImagesEmbed,
so the ReplacingMergeTree merge cannot leak stale embed data through
a deleted post. Closes the coverage gap flagged in the post-embeds
spec's Testing section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+62
+62
tests/unit/clickhouse_store.spec.ts
··· 564 564 assert.equal(Number(rawCountRows[0].n), 6, 'should have 3 original + 3 tombstone rows') 565 565 }).skip(async () => !(await isClickHouseAvailable()), 'ClickHouse not available') 566 566 567 + test('tombstoneUserSnapshots leaves empty embed_json on surviving row', async ({ assert }) => { 568 + const author = 'did:plc:author_tombstone_embed' 569 + const postUri = `at://${author}/app.bsky.feed.post/withembed` 570 + const embed: ImagesEmbed = { 571 + type: 'images', 572 + items: [ 573 + { 574 + thumb: 'https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:x/cidA', 575 + fullsize: 'https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:x/cidA', 576 + alt: 'pre-tombstone image', 577 + aspectRatio: { width: 1200, height: 800 }, 578 + }, 579 + ], 580 + } 581 + 582 + // Insert the original snapshot with a non-empty embed 583 + await store.insertPostSnapshots([ 584 + aSnapshot({ postAuthorDid: author, postUri, snapshotLikes: 7, embed }), 585 + ]) 586 + 587 + // Bulk tombstone — inserts a second row with embed_json='' and is_deleted=1 588 + await store.tombstoneUserSnapshots(author) 589 + 590 + // The tombstone row must carry embed_json='' so that when the 591 + // ReplacingMergeTree merge picks the newer (is_deleted=1) row per 592 + // (post_author_did, post_uri), no stale ImagesEmbed JSON is preserved. 593 + // Inspect the raw table first (pre-merge: 1 original + 1 tombstone row). 594 + const rawRs = await store.client.query({ 595 + query: ` 596 + SELECT embed_json, is_deleted 597 + FROM post_snapshots 598 + WHERE post_author_did = {authorDid:String} 599 + AND post_uri = {postUri:String} 600 + ORDER BY is_deleted ASC 601 + `, 602 + query_params: { authorDid: author, postUri }, 603 + format: 'JSONEachRow', 604 + }) 605 + const rawRows = await rawRs.json<{ embed_json: string; is_deleted: number }>() 606 + assert.equal(rawRows.length, 2, 'expected original row + tombstone row pre-merge') 607 + assert.equal(Number(rawRows[0].is_deleted), 0, 'first row is the original') 608 + assert.notEqual(rawRows[0].embed_json, '', 'original row still carries its embed') 609 + assert.equal(Number(rawRows[1].is_deleted), 1, 'second row is the tombstone') 610 + assert.equal(rawRows[1].embed_json, '', 'tombstone row must have empty embed_json') 611 + 612 + // ReplacingMergeTree with an is_deleted column removes is_deleted=1 rows 613 + // entirely under FINAL — so FINAL must return zero rows for this post. 614 + // This confirms the deleted post (and its embed) cannot leak through. 615 + const finalRs = await store.client.query({ 616 + query: ` 617 + SELECT embed_json 618 + FROM post_snapshots FINAL 619 + WHERE post_author_did = {authorDid:String} 620 + AND post_uri = {postUri:String} 621 + `, 622 + query_params: { authorDid: author, postUri }, 623 + format: 'JSONEachRow', 624 + }) 625 + const finalRows = await finalRs.json<{ embed_json: string }>() 626 + assert.equal(finalRows.length, 0, 'FINAL must not surface any row for the tombstoned post') 627 + }).skip(async () => !(await isClickHouseAvailable()), 'ClickHouse not available') 628 + 567 629 // ------------------------------------------------------------------------- 568 630 // Test 14: tombstoneUserSnapshots wraps errors with cause 569 631 // -------------------------------------------------------------------------