OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / test / appview-ingest.test.mjs

105 lines · javascript ref:
// favela appview ingest (#1 fix) — Bare-native, offline. Proves the delete/reconcile/incremental paths:
//   - putPost is UPDATE-SAFE (an edit that moves createdAt clears the stale reverse-chron key)
//   - delPost removes from timeline + author
//   - applyCommit(create) reads the record from the frame's OWN CAR; applyCommit(delete) removes it
//     (the exact bug the old snapshot re-fold missed — deletes)
//   - indexPeer is a RECONCILE (a record gone from the repo is removed from the index, not orphaned)
// applyCommit takes NORMALIZED ops [{action, collection, rkey, record?}] (DidPeer.commitOps decodes the
// frame CAR; that CAR-extraction path is validated in atproto-did-peer's own test). Here we test the
// appview's create/update/delete + reconcile logic directly with plain ops.
// Run: `bare test/appview-ingest.test.mjs`
import '../lib/bare-globals.js'
import { storeDir } from './_store-dir.mjs'
import Corestore from 'corestore'
import { Appview } from '../lib/appview.js'
import { mkGetRecordStore } from './mk-record-store.mjs'

const assert = (cond, msg) => { if (!cond) throw new Error('FAIL: ' + msg); console.log('  ok -', msg) }

const dir = storeDir('appview-ingest-test')
const store = new Corestore(dir)
const av = await new Appview(store, { getRecordStore: mkGetRecordStore(dir + '/rec') }).ready()
const DID = 'did:plc:ingest'

// --- putPost update-safe (edit that MOVES createdAt must not orphan the old key) ---
await av.putPost({ did: DID, rkey: 'r1', record: { text: 'v1', createdAt: '2026-07-01T00:00:00.000Z' } })
let tl = await av.getTimeline({ limit: 10 })
assert(tl.posts.length === 1 && tl.posts[0].record.text === 'v1', 'putPost indexed one post')
await av.putPost({ did: DID, rkey: 'r1', record: { text: 'v2', createdAt: '2026-07-09T00:00:00.000Z' } })
tl = await av.getTimeline({ limit: 10 })
assert(tl.posts.length === 1, 'update with moved createdAt did NOT orphan a stale timeline key (still 1)')
assert(tl.posts[0].record.text === 'v2', 'update reflects new content')
let feed = await av.getAuthorFeed(DID, { limit: 10 })
assert(feed.posts.length === 1, 'author feed also has exactly 1 (no orphan)')

// --- delPost ---
assert(await av.delPost({ did: DID, rkey: 'r1' }) === true, 'delPost reports it removed something')
tl = await av.getTimeline({ limit: 10 })
feed = await av.getAuthorFeed(DID, { limit: 10 })
assert(tl.posts.length === 0 && feed.posts.length === 0, 'delPost removed from BOTH timeline and author feed')
assert(await av.delPost({ did: DID, rkey: 'r1' }) === false, 'delPost of an absent post is a no-op')

// --- applyCommit: create then delete (normalized ops) ---
const rec = { $type: 'app.bsky.feed.post', text: 'hello', createdAt: '2026-07-05T00:00:00.000Z' }
const nCreate = await av.applyCommit(DID, [{ action: 'create', collection: 'app.bsky.feed.post', rkey: 'c1', record: rec }])
assert(nCreate === 1, 'applyCommit create applied 1 op')
tl = await av.getTimeline({ limit: 10 })
assert(tl.posts.some(p => p.record.text === 'hello'), 'applyCommit create indexed the record')
const nDel = await av.applyCommit(DID, [{ action: 'delete', collection: 'app.bsky.feed.post', rkey: 'c1' }])
assert(nDel === 1, 'applyCommit delete applied 1 op')
tl = await av.getTimeline({ limit: 10 })
assert(!tl.posts.some(p => p.record.text === 'hello'), 'applyCommit delete REMOVED the record (the bug the snapshot re-fold missed)')

// --- applyCommit ignores non-post collections (e.g. a graph.list op) ---
const nIgnore = await av.applyCommit(DID, [{ action: 'create', collection: 'app.bsky.graph.list', rkey: 'l1', record: { name: 'x' } }])
assert(nIgnore === 0, 'applyCommit ignores non-post collections')

// --- indexPeer reconcile removes stragglers (delete-correctness for the snapshot baseline) ---
const mk = (coll, rkey, text, createdAt) => ({ uri: `${DID}/${coll}/${rkey}`, value: { $type: coll, text, createdAt } })
const fakePeer = {
  did: DID,
  _recs: {
    'app.bsky.feed.post': [mk('app.bsky.feed.post', 'p1', 'post-1', '2026-07-02T00:00:00.000Z'), mk('app.bsky.feed.post', 'p2', 'post-2', '2026-07-03T00:00:00.000Z')],
    'net.favela.local.post': []
  },
  listRecords (c) { return this._recs[c] || [] }
}
assert(await av.indexPeer(fakePeer) === 2, 'indexPeer baseline indexed 2')
tl = await av.getTimeline({ limit: 10 })
assert(tl.posts.filter(p => (p.record.text || '').startsWith('post-')).length === 2, 'both present after baseline')
// p2 is now gone from the repo (listRecords returns only p1) — a reconcile must drop it
fakePeer._recs['app.bsky.feed.post'] = [mk('app.bsky.feed.post', 'p1', 'post-1', '2026-07-02T00:00:00.000Z')]
assert(await av.indexPeer(fakePeer) === 1, 'indexPeer reconcile now sees 1')
tl = await av.getTimeline({ limit: 10 })
assert(tl.posts.some(p => p.record.text === 'post-1'), 'p1 still present')
assert(!tl.posts.some(p => p.record.text === 'post-2'), 'p2 straggler REMOVED by reconcile (delete-correctness)')

// --- the edited marker: DERIVED FROM THE FIREHOSE, never from a record field ---
// applyCommit reads the op's action; `update` is the LOG's own statement that this rkey was edited
// (spec/record-vocabulary.md 10.5.2 -- atproto's $orig/$hist pattern exists because atproto has no
// history, and this substrate has some, so edit history is a READ over existing state).
const EDID = 'did:plc:edited'
const erec = (t) => ({ $type: 'app.bsky.feed.post', text: t, createdAt: '2026-07-20T00:00:00.000Z' })
const findE = async (rkey) => (await av.getAuthorFeed(EDID, { limit: 20 })).posts.find((p) => p.rkey === rkey)

await av.applyCommit(EDID, [{ action: 'create', collection: 'app.bsky.feed.post', rkey: 'e1', record: erec('one') }])
assert(!(await findE('e1')).editedAt, 'a CREATE op leaves no edited marker')

await av.applyCommit(EDID, [{ action: 'update', collection: 'app.bsky.feed.post', rkey: 'e1', record: erec('two') }])
const firstStamp = (await findE('e1')).editedAt
assert(!!firstStamp, 'an UPDATE op marks the post edited')

// A re-index must neither ERASE nor INVENT. indexPeer re-puts every record on a snapshot reconcile,
// so inferring "edited" from "a prior entry exists" would mark an ENTIRE REPO edited on any
// re-index -- which is exactly why the signal comes from the op action rather than the index.
await av.putPost({ did: EDID, collection: 'app.bsky.feed.post', rkey: 'e1', record: erec('two') })
assert((await findE('e1')).editedAt === firstStamp, 'a plain re-put KEEPS the original stamp (first edit wins)')

await av.putPost({ did: EDID, collection: 'app.bsky.feed.post', rkey: 'e2', record: erec('fresh') })
assert(!(await findE('e2')).editedAt, 'and a plain re-put NEVER invents an edit')

assert((await findE('e1')).createdAt === '2026-07-20T00:00:00.000Z', 'an edit keeps the ORIGINAL createdAt')

console.log('\nAPPVIEW-INGEST OK: putPost update-safe + delPost + applyCommit(create/delete via CAR) + indexPeer reconcile')
await av.close()
try { await store.close() } catch {}