feat(01-02): chat mutations mutate demo state instead of acking (FED-04)
Demo images / Build & push demo images (push) Successful in 4m21s
Demo images / Build & push demo images (push) Successful in 4m21s
Reactions, replies, read-receipts, edits, deletes, forwards and channel sends
shared one bare `{ ok: true, sent: true }` case, so none of them rendered on
the demo — the UI derives reaction chips and reply quotes from the message
store, and there was nothing in it to derive from.
Each now mirrors its daemon counterpart. Reactions/replies/receipts push typed
messages carrying the { sender_pubkey, sender_seq } target key Mesh.vue's
reactionIndex and replyTargetPreview read. Edits rewrite the text and set
edited_at; deletes tombstone IN PLACE (plaintext, typed_payload.deleted,
message_type 'delete') because that is what mesh/mod.rs apply_local_delete
does — it does not remove the row.
Edits and deletes go through a per-session overrides overlay keyed by
sender_seq, because mesh.messages rebuilds its seed array on every read, so
in-place mutation would only ever work for messages sent this session.
mesh.refresh and mesh.reboot-radio stay acknowledgements on purpose — the
daemon's handlers have no message-store effect either — with a comment saying
so, so a later reader does not "fix" them into divergence.
Also completes the phase bookkeeping for 01-02/03/11/12/13/14/15 and lands the
orphaned 01-12/01-14 SUMMARYs.
Verified: parity harness 17/17 live assertions; full frontend suite 102 files
/ 822 tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9c2ec82bdb
commit
90884e6259
@@ -214,7 +214,69 @@ try {
|
||||
else fail('cancelled request still listed as pending')
|
||||
}
|
||||
|
||||
// 6. federation.notify-did-change reports per-peer results.
|
||||
// 6. Chat mutations are observable on the next mesh.messages read — the
|
||||
// whole point of FED-04's "not a bare ok acknowledgement" requirement.
|
||||
const sent = await rpc('mesh.send-content-inline', {
|
||||
contact_id: 1,
|
||||
mime: 'text/plain',
|
||||
filename: 'mutate.txt',
|
||||
bytes_b64: Buffer.from('mutate me').toString('base64'),
|
||||
})
|
||||
const targetSeq = sent.message_id
|
||||
|
||||
await rpc('mesh.send-reaction', {
|
||||
contact_id: 1,
|
||||
target_pubkey: 'demo02abababababababababababababababababababababababababababab',
|
||||
target_seq: targetSeq,
|
||||
emoji: '🔥',
|
||||
})
|
||||
let msgs = (await rpc('mesh.messages', { limit: 500 })).messages
|
||||
const reaction = msgs.find(
|
||||
(m) => m.message_type === 'reaction' && m.typed_payload?.target?.sender_seq === targetSeq,
|
||||
)
|
||||
if (reaction?.typed_payload?.emoji === '🔥') pass('mesh.send-reaction is visible as a reaction message with a target key')
|
||||
else fail('reaction did not appear in mesh.messages with the UI-expected shape')
|
||||
|
||||
await rpc('mesh.send-reply', {
|
||||
contact_id: 1,
|
||||
target_pubkey: 'demo02abababababababababababababababababababababababababababab',
|
||||
target_seq: targetSeq,
|
||||
text: 'replying to that',
|
||||
})
|
||||
msgs = (await rpc('mesh.messages', { limit: 500 })).messages
|
||||
const reply = msgs.find((m) => m.message_type === 'reply' && m.typed_payload?.target?.sender_seq === targetSeq)
|
||||
if (reply?.plaintext === 'replying to that') pass('mesh.send-reply is visible as a reply carrying its target')
|
||||
else fail('reply did not appear with a target key')
|
||||
|
||||
await rpc('mesh.edit-message', { contact_id: 1, target_seq: targetSeq, new_text: 'edited text' })
|
||||
msgs = (await rpc('mesh.messages', { limit: 500 })).messages
|
||||
const edited = msgs.find((m) => m.sender_seq === targetSeq && m.direction === 'sent')
|
||||
if (edited?.plaintext === 'edited text' && edited?.typed_payload?.edited_at)
|
||||
pass('mesh.edit-message rewrote the text and set the edited marker')
|
||||
else fail(`edit not applied: ${JSON.stringify(edited?.plaintext)}`)
|
||||
|
||||
await rpc('mesh.delete-message', { contact_id: 1, target_seq: targetSeq })
|
||||
msgs = (await rpc('mesh.messages', { limit: 500 })).messages
|
||||
const deleted = msgs.find((m) => m.sender_seq === targetSeq && m.direction === 'sent')
|
||||
// The daemon tombstones in place rather than removing the row.
|
||||
if (deleted && deleted.message_type === 'delete' && deleted.typed_payload?.deleted === true)
|
||||
pass('mesh.delete-message tombstoned in place, as apply_local_delete does')
|
||||
else fail(`delete representation wrong: ${JSON.stringify(deleted)}`)
|
||||
|
||||
const beforeForward = (await rpc('mesh.messages', { limit: 500 })).count
|
||||
await rpc('mesh.forward-message', { contact_id: 2, source_message_id: targetSeq })
|
||||
msgs = (await rpc('mesh.messages', { limit: 500 })).messages
|
||||
const forwarded = msgs.filter((m) => m.peer_contact_id === 2 && m.direction === 'sent')
|
||||
if (msgs.length > beforeForward && forwarded.length > 0) pass('mesh.forward-message pushed a copy for the destination peer')
|
||||
else fail('forward did not create a message for the destination peer')
|
||||
|
||||
await rpc('mesh.send-channel', { channel: 3, message: 'channel broadcast' })
|
||||
msgs = (await rpc('mesh.messages', { limit: 500 })).messages
|
||||
if (msgs.some((m) => m.channel === 3 && m.plaintext === 'channel broadcast'))
|
||||
pass('mesh.send-channel pushed a channel-addressed message')
|
||||
else fail('channel message not visible in mesh.messages')
|
||||
|
||||
// 7. federation.notify-did-change reports per-peer results.
|
||||
const notified = await rpc('federation.notify-did-change', {
|
||||
old_did: 'did:key:zOld',
|
||||
new_did: 'did:key:zNew',
|
||||
|
||||
Reference in New Issue
Block a user