WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto

fix(appview): add \$type discriminator to forumRef and boardRef PDS writes (ATB-41) (#70)

Adds missing \$type fields to forum and board ref objects written to the PDS,
consistent with the replyRef fix in PR #61. Without \$type, the AT Protocol
runtime guards Post.isForumRef()/isBoardRef() return false, which would silently
break any future indexer refactor that adopts typed guards over optional chaining.

Also adds Post.isForumRef()/isBoardRef() assertions to the corresponding tests,
following the same contract-based pattern established for replyRef.

authored by

Malpercio and committed by
GitHub
a7b9c634 cfe76a95

+15
+4
apps/appview/src/routes/__tests__/posts.test.ts
··· 140 140 expect(Post.isReplyRef(putCall.record.reply)).toBe(true); 141 141 expect(putCall.record.reply.root.uri).toMatch(/^at:\/\/did:plc:.*\/space\.atbb\.post\//); 142 142 expect(putCall.record.reply.parent.uri).toMatch(/^at:\/\/did:plc:.*\/space\.atbb\.post\//); 143 + 144 + // Verify the forum ref also has its $type discriminator, consistent with 145 + // replyRef and boardRef — all three use the same lexicon ref pattern. 146 + expect(Post.isForumRef(putCall.record.forum)).toBe(true); 143 147 }); 144 148 145 149 it("creates reply to reply", async () => {
+8
apps/appview/src/routes/__tests__/topics.test.ts
··· 5 5 import { forums, categories, boards, users, posts, modActions } from "@atbb/db"; 6 6 import { eq } from "drizzle-orm"; 7 7 import { TID } from "@atproto/common-web"; 8 + import { SpaceAtbbPost as Post } from "@atbb/lexicon"; 8 9 9 10 // Mock auth and permission middleware at the module level 10 11 let mockPutRecord: ReturnType<typeof vi.fn>; ··· 204 205 expect(data.uri).toMatch(/^at:\/\/did:plc:test-user\/space\.atbb\.post\/3/); 205 206 expect(data.cid).toBeTruthy(); 206 207 expect(data.rkey).toBeTruthy(); 208 + 209 + // Verify forum and board refs written to PDS have $type discriminators — 210 + // same contract as replyRef (PR #61). Without $type, isForumRef()/isBoardRef() 211 + // return false and any future typed-guard refactor in the indexer silently breaks. 212 + const putCall = mockPutRecord.mock.calls[0][0]; 213 + expect(Post.isForumRef(putCall.record.forum)).toBe(true); 214 + expect(Post.isBoardRef(putCall.record.board)).toBe(true); 207 215 }); 208 216 209 217 it("returns 400 when title is missing", async () => {
+1
apps/appview/src/routes/posts.ts
··· 112 112 $type: "space.atbb.post", 113 113 text: validation.trimmed!, 114 114 forum: { 115 + $type: "space.atbb.post#forumRef", 115 116 forum: { uri: root.forumUri, cid: root.cid }, 116 117 }, 117 118 reply: {
+2
apps/appview/src/routes/topics.ts
··· 292 292 title: titleValidation.trimmed!, 293 293 text: validation.trimmed!, 294 294 forum: { 295 + $type: "space.atbb.post#forumRef", 295 296 forum: { uri: forumUri, cid: forum!.cid }, 296 297 }, 297 298 board: { 299 + $type: "space.atbb.post#boardRef", 298 300 board: { uri: boardUri, cid: board!.cid }, 299 301 }, 300 302 createdAt: new Date().toISOString(),