From 2c386b2f499870fc6147c640501fb2ec7f949e4a Mon Sep 17 00:00:00 2001 From: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) Date: Fri, 31 Jul 2026 20:36:05 +0000 Subject: [PATCH] Report a blessing by what the bundle carries, and unbreak CI Two fixes from review and one from the pipeline. A comment can carry more than one blessing — "Bless it again" on a withdrawn row writes a second one, and two members can bless the same words independently — and `blessedRows()` concatenated the live list before the retracted one into a Map keyed by comment URI, so a stale tombstone overwrote the live blessing sitting beside it. The exact workflow the section offers therefore reported "blessing withdrawn" and offered to bless a third time, while the fold and every turn bundle still carried the live one: the screen inverted the single fact the section exists to report. It now folds to one row per COMMENT and a live blessing always outranks a tombstone on a different one, so the row says what the bundle actually holds and `Withdraw blessing` names the record that is in it. Turning the space switch off hid the whole section, blessings included — prose that was still in every turn bundle on the goal, with its only screen and its only withdrawal control gone. The switch governs solicitation, not revocation, so it now stops the backlink query and takes away the guest composer while blessed rows and the button that withdraws them stay. The rule is `showsCommunity()`, named so it can be tested rather than inlined in a template; `Community` takes a `discover` prop and says plainly that it did not look, rather than "none found". And CI, which was failing on this branch and is not transient. `Community.svelte` was the only component in the app carrying a scoped ` diff --git a/packages/ui/src/lib/components/Community.svelte.test.ts b/packages/ui/src/lib/components/Community.svelte.test.ts --- a/packages/ui/src/lib/components/Community.svelte.test.ts +++ b/packages/ui/src/lib/components/Community.svelte.test.ts @@ -28,7 +28,7 @@ const GUEST = 'did:plc:guest' const HOSTILE = ' **not bold**' -const bless = (rkey: string, body: string, comment: string) => ({ +const bless = (rkey: string, body: string, comment: string, createdAt = '2026-01-01T00:05:00Z') => ({ did: 'did:plc:member', uri: `at://did:plc:member/${COLLECTIONS.blessComment}/${rkey}`, cid: `cid-${rkey}`, @@ -40,7 +40,7 @@ goal: { uri: GOAL, cid: 'cid-goal' }, comment: { uri: `at://${GUEST}/${COLLECTIONS.message}/${comment}`, cid: `cid-${comment}` }, body, - createdAt: '2026-01-01T00:05:00Z', + createdAt, }, }) @@ -78,7 +78,12 @@ host.remove() }) -const render = (props: { view: GoalView; space: Space; reader?: ForeignReader }): void => { +const render = (props: { + view: GoalView + space: Space + discover?: boolean + reader?: ForeignReader +}): void => { component = mount(Community, { target: host, props }) as Record flushSync() } @@ -110,6 +115,54 @@ it('says nothing has been found rather than showing an empty list', () => { render({ view: view([]), space: space() }) expect(host.textContent).toContain('No comments from outside this space') + }) + + it('offers withdrawal, not a third blessing, once a withdrawn comment is blessed again', () => { + render({ + view: view( + [bless('b2', 'Blessed again', 'c1', '2026-01-01T00:09:00Z')], + [bless('b1', 'First blessing', 'c1', '2026-01-01T00:05:00Z')], + ), + space: space(), + }) + const labels = [...host.querySelectorAll('button')].map((entry) => entry.textContent?.trim()) + expect(labels).toContain('Withdraw blessing') + expect(labels).not.toContain('Bless it again') + expect(host.textContent).toContain('blessed') + expect(host.textContent).not.toContain('blessing withdrawn') + }) + + // The switch governs solicitation. Prose a member signed for is in the bundle of every turn on + // this goal until it is withdrawn, so it stays on screen with the control that removes it — + // hiding the section would leave agents reading text nobody could see or retract. + it('keeps a blessed comment and its withdrawal control when discovery is off', () => { + render({ + view: view([bless('b1', 'Still in every bundle', 'c1')]), + space: space(), + discover: false, + }) + expect(host.textContent).toContain('Still in every bundle') + expect( + [...host.querySelectorAll('button')].map((entry) => entry.textContent?.trim()), + ).toContain('Withdraw blessing') + // …and it does not claim a lookup happened. + expect(host.textContent).not.toContain('found through a public backlink index') + expect(host.textContent).toContain('no longer asking for comments') + }) + + it('does not query the index at all when discovery is off', () => { + const fetched = vi.fn(async () => new Response('{}', { status: 200 })) + const read = vi.fn() + vi.stubGlobal('fetch', fetched) + render({ + view: view([bless('b1', 'Blessed', 'c1')]), + space: space(), + discover: false, + reader: { getForeignRecord: read as unknown as ForeignReader['getForeignRecord'] }, + }) + expect(fetched).not.toHaveBeenCalled() + expect(read).not.toHaveBeenCalled() + vi.unstubAllGlobals() }) it('makes blessing a two-step, and writes only on the second press', async () => { diff --git a/packages/ui/src/routes/g/[did]/[rkey]/+page.svelte b/packages/ui/src/routes/g/[did]/[rkey]/+page.svelte --- a/packages/ui/src/routes/g/[did]/[rkey]/+page.svelte +++ b/packages/ui/src/routes/g/[did]/[rkey]/+page.svelte @@ -14,6 +14,7 @@ import UriChip from '$lib/components/UriChip.svelte' import Who from '$lib/components/Who.svelte' import { shortDate } from '$lib/format.js' + import { showsCommunity } from '$lib/guests.js' import { currentSpace } from '$lib/session.svelte.js' import type { ThreadMessage } from '$lib/replies.js' import { goalByRoute, unitsOf } from '$lib/space.js' @@ -34,6 +35,8 @@ const ending = $derived(goal ? endingBadge(goal) : undefined) /** Whether this space asks non-members for comments — an admin's `setGuestComments` overlay. */ const community = $derived(space.index.guestCommentsEnabled) + /** …and whether the section is drawn at all, which the switch alone does not decide. */ + const section = $derived(goal ? showsCommunity(goal, community) : false) /** The message the composer is answering, when Reply pointed it at one. */ let replyTo = $state(undefined) @@ -123,13 +126,14 @@ same record with one more field on it. --> (replyTo = message)} /> (replyTo = undefined)} /> - + + {#if section} + + {/if} {#if community} - {/if}