// ⛔ A Mastodon Update WITHOUT an `updated` timestamp is silently discarded — 2xx, no edit, no
// media re-fetch. Recorded in the ap-bridge skill, and the profile Update did not set one: note
// Updates do (publisher.js:234), the profile path did not. So every profile change ever sent from
// this bridge was accepted and ignored, which is why a stale display name never corrected itself.
import test from 'brittle'
import { Publisher } from '../lib/publisher.js'
import { MemoryState } from '../lib/state.js'
import { MemoryFollowerStore } from '../lib/followers.js'
test('★★ a profile Update carries an `updated` timestamp', async (t) => {
const sent = []
const p = new Publisher({
records: {
async getProfile () { return { name: 'ashes' } },
async listNotes () { return { notes: [], cursor: null } },
async listInteractions () { return { interactions: [] } }
},
followers: new MemoryFollowerStore(['https://prsm.space/users/fleeky']),
deliver: async (d) => { sent.push(d) },
actorId: 'https://ap.7rnx.net/ap/users/ashatron/actor',
newId: () => 'id1',
log: () => {},
state: new MemoryState([], true),
buildActorDoc: async () => ({ id: 'https://ap.7rnx.net/ap/users/ashatron/actor', type: 'Person', name: 'ashes' })
})
await p.refreshProfile()
t.is(sent.length, 1, 'delivered to the follower')
t.is(sent[0].activity.type, 'Update')
t.ok(sent[0].activity.object.updated, '⛔ without this Mastodon accepts it and changes nothing')
t.ok(!isNaN(Date.parse(sent[0].activity.object.updated)), 'and it is a real timestamp')
})
// ⛔ THE POISONED FINGERPRINT. refreshProfile stored the fingerprint BEFORE checking recipients, so
// a run with no followers recorded "already sent this" — and every later run short-circuited. The
// profile was then permanently un-sendable, and the only symptom was silence.
test('⛔⛔ a run with no followers does not mark the profile as sent', async (t) => {
const sent = []
const followers = new MemoryFollowerStore([])
const state = new MemoryState([], true)
const mk = () => new Publisher({
records: { async getProfile () { return { name: 'ashes' } }, async listNotes () { return { notes: [], cursor: null } } },
followers,
deliver: async (d) => { sent.push(d) },
actorId: 'https://ap.7rnx.net/ap/users/ashatron/actor',
newId: () => 'id1',
log: () => {},
state,
buildActorDoc: async () => ({ id: 'https://ap.7rnx.net/ap/users/ashatron/actor', type: 'Person', name: 'ashes' })
})
await mk().refreshProfile()
t.is(sent.length, 0, 'nobody to send to')
await followers.add('https://prsm.space/users/fleeky')
await mk().refreshProfile()
t.is(sent.length, 1, 'and when a follower arrives, it IS sent — not swallowed by a stale fingerprint')
})
// ⛔⛔ THE SAME DEFECT ONE STEP LATER. The no-followers guard above fixed "recorded as told when
// nobody was told". But the fingerprint is still stored BEFORE the deliveries, and every delivery
// failure is caught and logged rather than propagated — so a run where every recipient failed also
// records "already sent this", and the profile becomes un-sendable without `force`.
//
// This is the shape ashatron's un-refreshed avatar has: the actor advertises an icon that serves,
// and no follower ever re-fetched it.
test('⛔⛔ a run where every delivery FAILS does not mark the profile as sent', async (t) => {
let attempts = 0
const followers = new MemoryFollowerStore(['https://prsm.space/users/fleeky'])
const state = new MemoryState([], true)
const mk = (fail) => new Publisher({
records: { async getProfile () { return { name: 'ashes' } }, async listNotes () { return { notes: [], cursor: null } } },
followers,
deliver: async () => { attempts++; if (fail) throw new Error('connect ECONNREFUSED') },
actorId: 'https://ap.7rnx.net/ap/users/ashatron/actor',
newId: () => 'id1',
log: () => {},
state,
buildActorDoc: async () => ({ id: 'https://ap.7rnx.net/ap/users/ashatron/actor', type: 'Person', name: 'ashes' })
})
await mk(true).refreshProfile()
t.is(attempts, 1, 'it tried')
t.absent(await state.getProfileFingerprint(), '⛔ nothing landed, so nothing may be recorded as sent')
// The retry is the point: a profile that failed to reach anybody must still be sendable.
await mk(false).refreshProfile()
t.is(attempts, 2, 'a later run tries again rather than short-circuiting')
t.ok(await state.getProfileFingerprint(), 'and records it once somebody was actually told')
})