OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / test / tag-feed.test.mjs

50 lines · javascript ref:
// favela topic/tag feeds (#3) — Bare-native, offline. extractTags + per-tag reverse-chron feed +
// delete/edit tag reconciliation + no prefix collision. Run: `bare test/tag-feed.test.mjs`
import '../lib/bare-globals.js'
import { storeDir } from './_store-dir.mjs'
import Corestore from 'corestore'
import { Appview, extractTags } 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 sortEq = (a, b) => JSON.stringify([...a].sort()) === JSON.stringify([...b].sort())

// --- extractTags ---
assert(sortEq(extractTags({ text: 'hello #Cat and #dog' }), ['cat', 'dog']), 'extract #hashtags from text (lowercased)')
assert(extractTags({ text: 'no tags here' }).length === 0, 'no tags -> empty')
assert(sortEq(extractTags({ tags: ['News', 'news'] }), ['news']), 'structured tags deduped + lowercased')
assert(extractTags({ facets: [{ features: [{ $type: 'app.bsky.richtext.facet#tag', tag: 'p2p' }] }] })[0] === 'p2p', 'facet #tag feature extracted')
assert(extractTags({ text: 'reach me a@b.com not#atag' }).length === 0, 'mid-word # is not a tag (needs start/space)')

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

await av.putPost({ did: DID, rkey: 'p1', record: { text: 'about #cat stuff', createdAt: '2026-07-01T00:00:00.000Z' } })
await av.putPost({ did: DID, rkey: 'p2', record: { text: 'more #cat and #cats', createdAt: '2026-07-05T00:00:00.000Z' } })
await av.putPost({ did: DID, rkey: 'p3', record: { text: 'a #dog post', createdAt: '2026-07-03T00:00:00.000Z' } })

let cat = await av.getTagFeed('cat', { limit: 10 })
assert(cat.posts.length === 2, '#cat feed has the 2 cat posts')
assert(cat.posts[0].record.text.includes('more'), 'tag feed newest-first')
const cats = await av.getTagFeed('cats', { limit: 10 })
assert(cats.posts.length === 1, '#cats (a prefix-neighbour of #cat) is a SEPARATE feed (no prefix collision)')
const dog0 = await av.getTagFeed('#DOG', { limit: 10 })
assert(dog0.posts.length === 1, 'getTagFeed normalizes the query (strips # + lowercases)')

// --- delete removes from the tag feed ---
await av.delPost({ did: DID, rkey: 'p1' })
cat = await av.getTagFeed('cat', { limit: 10 })
assert(cat.posts.length === 1 && cat.posts[0].record.text.includes('more'), 'delete removed p1 from the #cat feed')

// --- edit that CHANGES tags: #dog -> #fish ---
await av.putPost({ did: DID, rkey: 'p3', record: { text: 'now a #fish post', createdAt: '2026-07-03T00:00:00.000Z' } })
assert((await av.getTagFeed('dog', { limit: 10 })).posts.length === 0, 'edit removed the OLD tag (#dog) from the feed')
assert((await av.getTagFeed('fish', { limit: 10 })).posts.length === 1, 'edit added the NEW tag (#fish) to the feed')

console.log('\nTAG-FEED OK: extractTags + per-tag feed + delete/edit reconciliation + no prefix collision')
await av.close()
try { await store.close() } catch {}