ReviewOS

also looking at this

stacks/postline

UI polish, real queue + scheduling, Bluesky threads, live analytics, blog target

#8
Merged glennmichael123 wants to merge feat/ui-polish into main
54 files +2036 -589

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

tests/unit/social-publish.test.tsadded+97-0
Changes to tests/unit/social-publish.test.ts
@@ -0,0 +1,97 @@
1import { afterEach, describe, expect, test } from 'bun:test'
2import { BlueskyPublishingDriver } from '../../storage/framework/core/socials/src/drivers/bluesky'
3
4const realFetch = globalThis.fetch
5
6afterEach(() => {
7 globalThis.fetch = realFetch
8})
9
10interface CapturedRequest {
11 url: string
12 body: any
13}
14
15function mockBlueskyApi(captured: CapturedRequest[]): void {
16 globalThis.fetch = (async (input: any, init?: any) => {
17 const url = String(input)
18 const body = init?.body ? JSON.parse(String(init.body)) : undefined
19 captured.push({ url, body })
20 return new Response(JSON.stringify({
21 uri: `at://did:plc:test/app.bsky.feed.post/rec${captured.length}`,
22 cid: `cid${captured.length}`,
23 }), { status: 200, headers: { 'content-type': 'application/json' } })
24 }) as typeof fetch
25}
26
27const identity = {
28 handle: 'tester.bsky.social',
29 did: 'did:plc:test',
30 accessToken: 'jwt-token',
31}
32
33describe('BlueskyPublishingDriver.publish', () => {
34 test('creates a plain post record without reply refs', async () => {
35 const captured: CapturedRequest[] = []
36 mockBlueskyApi(captured)
37
38 const driver = new BlueskyPublishingDriver()
39 const result = await driver.publish(identity, { text: 'hello world' })
40
41 expect(captured).toHaveLength(1)
42 expect(captured[0].url).toContain('/xrpc/com.atproto.repo.createRecord')
43 expect(captured[0].body.record.text).toBe('hello world')
44 expect(captured[0].body.record.reply).toBeUndefined()
45 expect(result.uri).toBe('at://did:plc:test/app.bsky.feed.post/rec1')
46 expect(result.cid).toBe('cid1')
47 })
48
49 test('threads a reply via root/parent refs on the record', async () => {
50 const captured: CapturedRequest[] = []
51 mockBlueskyApi(captured)
52
53 const reply = {
54 root: { uri: 'at://did:plc:test/app.bsky.feed.post/root', cid: 'cid-root' },
55 parent: { uri: 'at://did:plc:test/app.bsky.feed.post/parent', cid: 'cid-parent' },
56 }
57
58 const driver = new BlueskyPublishingDriver()
59 await driver.publish(identity, { text: 'second segment', reply })
60
61 expect(captured[0].body.record.reply).toEqual(reply)
62 })
63
64 test('attaches an external link card when provided', async () => {
65 const captured: CapturedRequest[] = []
66 mockBlueskyApi(captured)
67
68 const driver = new BlueskyPublishingDriver()
69 await driver.publish(identity, {
70 text: 'with card',
71 external: { uri: 'https://example.com', title: 'Example', description: 'Desc' },
72 })
73
74 expect(captured[0].body.record.embed).toEqual({
75 $type: 'app.bsky.embed.external',
76 external: { uri: 'https://example.com', title: 'Example', description: 'Desc' },
77 })
78 })
79
80 test('rejects posts over the character limit without hitting the API', async () => {
81 const captured: CapturedRequest[] = []
82 mockBlueskyApi(captured)
83
84 const driver = new BlueskyPublishingDriver()
85 await expect(driver.publish(identity, { text: 'x'.repeat(301) })).rejects.toThrow(/300 characters/)
86 expect(captured).toHaveLength(0)
87 })
88
89 test('rejects when the identity has no access token', async () => {
90 const captured: CapturedRequest[] = []
91 mockBlueskyApi(captured)
92
93 const driver = new BlueskyPublishingDriver()
94 await expect(driver.publish({ handle: 'tester.bsky.social' }, { text: 'hi' })).rejects.toThrow(/access token/)
95 expect(captured).toHaveLength(0)
96 })
97})