| … | … | @@ -15,8 +15,10 @@ import { tidFor } from '../lib/tid.js' |
| 15 | 15 | const at = '2026-09-07T23:12:04.600Z' |
| 16 | 16 | const row = (name, body) => ({ name, body, createdAt: at }) |
| 17 | 17 | |
| 18 | | function fakePeer ({ networks = ['bsky'], rows = [], profile = null, unknown = false } = {}) { |
| 18 | function fakePeer ({ networks = ['bsky'], rows = [], profile = null, unknown = false, deleted = null } = {}) { |
| 19 | 19 | return { |
| 20 | // Only when given: a peer adapter without it (the desktop CLI's socket client) must still work. |
| 21 | ...(deleted ? { ownDeleted: async () => deleted } : {}), |
| 20 | 22 | reach: async () => (unknown ? { networks: [], credential: null, unknown: true } : { networks, credential: null }), |
| 21 | 23 | ownPosts: async () => ({ feed: 'f'.repeat(64), rows }), |
| 22 | 24 | call: async (cmd) => { |
| … | … | @@ -214,3 +216,64 @@ test('⛔ no claim without bsky consent, on a dry run, or when the repo cannot b |
| 214 | 216 | await projectCycle({ peer: fakePeer(), bsky: blind.sink, send: true }) |
| 215 | 217 | t.absent(blind.written.some((w) => w.collection === FEED_CLAIM), 'could not read is not absent') |
| 216 | 218 | }) |
| 219 | |
| 220 | // ── deletes ────────────────────────────────────────────────────────────────────────────────────── |
| 221 | // |
| 222 | // ⛔ A deleted favela post is retracted from bsky ONLY on positive evidence — a tombstone the peer |
| 223 | // reports — and only where the repo still holds it. A name merely missing from ownPosts() is never |
| 224 | // grounds to delete anything: "not replicated yet" looks identical. |
| 225 | |
| 226 | const deletes = (written) => written.filter((w) => w.action === 'delete') |
| 227 | |
| 228 | test('★★★ a deleted post that bsky holds is deleted there, at its TID rkey', async (t) => { |
| 229 | const name = '1788822724600' |
| 230 | const { sink, written } = fakeSink({ has: [tidFor(name)] }) |
| 231 | const r = await projectCycle({ peer: fakePeer({ deleted: { post: [name], reply: [] } }), bsky: sink, send: true }) |
| 232 | t.alike(deletes(written), [{ action: 'delete', collection: 'app.bsky.feed.post', rkey: tidFor(name) }], 'exactly that record') |
| 233 | t.is(r.deleted, 1) |
| 234 | }) |
| 235 | |
| 236 | test('★★ a deleted reply is retracted the same way — replies are app.bsky.feed.post too', async (t) => { |
| 237 | const name = '1788822724601' |
| 238 | const { sink, written } = fakeSink({ has: [tidFor(name)] }) |
| 239 | await projectCycle({ peer: fakePeer({ deleted: { post: [], reply: [name] } }), bsky: sink, send: true }) |
| 240 | t.is(deletes(written).length, 1) |
| 241 | t.is(deletes(written)[0].rkey, tidFor(name)) |
| 242 | }) |
| 243 | |
| 244 | test('★★ a deleted post bsky never had is not touched', async (t) => { |
| 245 | const { sink, written } = fakeSink({ has: [] }) |
| 246 | const r = await projectCycle({ peer: fakePeer({ deleted: { post: ['1788822724600'], reply: [] } }), bsky: sink, send: true }) |
| 247 | t.is(deletes(written).length, 0, 'nothing to retract') |
| 248 | t.is(r.deleted, 0) |
| 249 | }) |
| 250 | |
| 251 | test('★★ a dry run deletes nothing', async (t) => { |
| 252 | const name = '1788822724600' |
| 253 | const { sink, written } = fakeSink({ has: [tidFor(name)] }) |
| 254 | const said = [] |
| 255 | await projectCycle({ peer: fakePeer({ deleted: { post: [name], reply: [] } }), bsky: sink, send: false, log: (l) => said.push(l) }) |
| 256 | t.is(deletes(written).length, 0) |
| 257 | t.ok(said.some((l) => /would DELETE/.test(l)), 'but says it would') |
| 258 | }) |
| 259 | |
| 260 | test('★★ retraction is not gated on consent — switching bsky off must not strand a deleted post there', async (t) => { |
| 261 | const name = '1788822724600' |
| 262 | const { sink, written } = fakeSink({ has: [tidFor(name)] }) |
| 263 | await projectCycle({ peer: fakePeer({ networks: [], deleted: { post: [name], reply: [] } }), bsky: sink, send: true }) |
| 264 | t.is(deletes(written).length, 1, 'deleting publishes nothing, so it does not need consent to publish') |
| 265 | }) |
| 266 | |
| 267 | test('★★★ an unreadable repo deletes nothing either', async (t) => { |
| 268 | const { sink, written } = fakeSink({ haveNull: true }) |
| 269 | await projectCycle({ peer: fakePeer({ deleted: { post: ['1788822724600'], reply: [] } }), bsky: sink, send: true }) |
| 270 | t.is(deletes(written).length, 0) |
| 271 | }) |
| 272 | |
| 273 | test('a peer with no ownDeleted (the desktop socket client) projects as before', async (t) => { |
| 274 | const { sink, written } = fakeSink({ has: [] }) |
| 275 | const r = await projectCycle({ peer: fakePeer({ rows: [row('1788822724600', 'hello')] }), bsky: sink, send: true }) |
| 276 | t.is(r.sent, 1) |
| 277 | t.is(deletes(written).length, 0) |
| 278 | t.is(r.deleted, 0) |
| 279 | }) |