// Editing a post. Run: node test/edit.test.mjs
//
// atproto ALLOWS this — putRecord is lexicon-agnostic — but bsky's AppView deliberately ignores post
// updates, so getRecord reflects an edit and getPostThread does not (atproto #3038). The edit is real
// on P2P and ActivityPub, partial on bsky. spec/record-vocabulary.md §10.5.
import { editPage, renderPost, withViewer } from '../lib/render.js'
let pass = 0
const ok = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log(' ok -', m); pass++ }
const has = (s, sub, m) => ok(String(s).includes(sub), m)
const not = (s, sub, m) => ok(!String(s).includes(sub), m)
const post = (over = {}) => ({
uri: 'at://did:plc:me/app.bsky.feed.post/r1',
did: 'did:plc:me', rkey: 'r1', createdAt: '2026-01-01T00:00:00Z',
record: { $type: 'app.bsky.feed.post', text: 'original', createdAt: '2026-01-01T00:00:00Z' },
...over
})
// ── the form ────────────────────────────────────────────────────────────────────────────────────
{
const h = editPage({ post: post(), returnTo: '/' })
has(h, '>original</textarea>', 'prefilled from the record')
has(h, 'method="POST" action="/api/edit"', 'a plain form — editing works with scripting off')
has(h, 'name="uri"', 'carries the at:// uri')
// THE HONESTY REQUIREMENT (§10.5.4). bsky's failure is worse than a refusal: getRecord reflects
// the edit and getPostThread does not, so the post reads edited in some bsky contexts and stale in
// others. The user cannot be told "it did not sync", because partly it did.
has(h, 'AppView ignores', 'says plainly that bsky will not show the edit')
has(h, 'fediverse', 'and that the fediverse WILL be updated')
}
{
const h = editPage({ post: post({ record: { text: '<script>x</script>' } }), returnTo: '/' })
has(h, '<script>', 'the prefilled text is escaped')
not(h, '<script>x', 'no raw tag reaches the form')
}
{
const h = editPage({ post: null, returnTo: '/' })
has(h, 'post not found', 'a missing post gets an empty state, not a blank form')
not(h, 'action="/api/edit"', 'and no form to submit into nothing')
}
{
const h = editPage({ post: post(), error: 'you can only edit your own posts' })
has(h, 'role="alert"', 'an error is announced, not just coloured')
has(h, 'you can only edit your own posts', 'and says what went wrong')
}
{
const withImg = post({ record: { text: 't', embed: { images: [{ alt: 'a' }] } } })
has(editPage({ post: withImg }), 'Images are preserved', 'says images survive an edit and cannot be changed here')
}
// ── the control ─────────────────────────────────────────────────────────────────────────────────
{
const mine = withViewer([post()], { did: 'did:plc:me' }, '/')
const h = renderPost(mine[0])
has(h, 'class="editlink"', 'own post offers an edit affordance')
has(h, '/edit/did%3Aplc%3Ame/r1', 'linking to the edit page for that rkey')
// Edit is reversible, delete is not — so edit is a plain link and delete keeps its confirm.
has(h, 'class="delw"', 'delete still guards itself behind a confirm')
}
{
const theirs = withViewer([post({ did: 'did:plc:other' })], { did: 'did:plc:me' }, '/')
not(renderPost(theirs[0]), 'class="editlink"', "someone else's post offers NO edit affordance")
}
{
const anon = withViewer([post()], null, '/')
not(renderPost(anon[0]), 'class="editlink"', 'signed out offers no edit affordance')
}
// ── the edited marker ───────────────────────────────────────────────────────────────────────────
// ★ DERIVED, not a record field. applyCommit reads the firehose op's action; `update` IS the log's
// own statement that this rkey was edited. atproto's $orig/$hist pattern exists because atproto has
// no history — this substrate has some, so edit history is a READ (spec §10.5.2).
{
const base = post()
not(renderPost(base), '(edited)', 'an unedited post says nothing')
const ed = renderPost({ ...base, editedAt: '2026-02-02T00:00:00Z' })
has(ed, '(edited)', 'an edited post is marked')
// The ORIGINAL time stays: an edit is not a new post, and moving the timestamp would reorder the
// timeline and misstate when the thing was said.
has(ed, '2026-01-01T00:00:00Z', 'and keeps the original createdAt, not the edit time')
has(ed, 'the original time is kept', 'the title explains why the two differ')
}
console.log(`\nEDIT OK: ${pass} assertions`)