feat(chat): reduce on-chain Control memos into per-conversation hide state
ingest() now routes Control memos (self-addressed, so processed ahead of the "skip our own memos" guard) into a per-cid reducer that keeps the latest op by (compose-time, txid) — monotonic, so a replayed older op never flips state. A conversation whose latest op is "del" is hidden: its messages are dropped from ingest and erased from the store view (even rows loaded from the DB before the signal was reduced), on every device and on a fresh seed import, until a later "res" restores it. The DB rows are left intact so restore (or a rescan) brings the thread back. State is re-derived from chain each session; no local persistence. Adds conversationHiddenOnChain() + onChainDeletedConversations() (the restore picker's source), fully reducer-tested. Phase A, step 3 (reducer) of on-chain chat delete/restore. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15075,6 +15075,66 @@ void testHushChatControlMemo()
|
||||
== ChatComposeStatus::BadRequestText);
|
||||
}
|
||||
|
||||
// Control reducer: a self-addressed delete hides a conversation (dropped from ingest + erased from the
|
||||
// store view); a later restore un-hides it; the reduction is monotonic by (timestamp, txid).
|
||||
void testHushChatControlReducer()
|
||||
{
|
||||
using namespace dragonx::chat;
|
||||
ChatKeyPair me, peer;
|
||||
ChatIdentityResult rm = deriveChatIdentityFromSecret("ctrl-reduce", me, true);
|
||||
ChatIdentityResult rp = deriveChatIdentityFromSecret("ctrl-peer", peer, true);
|
||||
EXPECT_TRUE(rm.status == ChatIdentityStatus::Ready && rp.status == ChatIdentityStatus::Ready);
|
||||
ChatService svc; svc.setIdentity(me);
|
||||
const std::string cid = "conv-x";
|
||||
const std::int64_t now = static_cast<std::int64_t>(std::time(nullptr));
|
||||
|
||||
auto feed = [&](const OutgoingChatMemos& m, const std::string& txid, std::int64_t ts) {
|
||||
HushChatTransactionInput tx; tx.txid = txid;
|
||||
tx.outputs.push_back({0, m.headerMemo});
|
||||
tx.outputs.push_back({1, m.payloadMemo});
|
||||
auto ex = extractHushChatTransactionMetadata(tx, true);
|
||||
return svc.ingest(ex.metadata, {{txid, ts}}, ts);
|
||||
};
|
||||
|
||||
// A peer -> me message is stored and not hidden.
|
||||
OutgoingChatMemos pm;
|
||||
EXPECT_TRUE(buildOutgoingMessage(peer, rp.public_key_hex, "zs-peer", rm.public_key_hex, "zs-me", cid,
|
||||
"hi", pm) == ChatComposeStatus::Ok);
|
||||
EXPECT_EQ(feed(pm, "m1", now), 1);
|
||||
EXPECT_EQ((int)svc.store().conversation(cid).size(), 1);
|
||||
EXPECT_TRUE(!svc.conversationHiddenOnChain(cid));
|
||||
|
||||
// A self-addressed delete hides it and erases it from the store view (adds no message).
|
||||
OutgoingChatMemos dm;
|
||||
EXPECT_TRUE(buildOutgoingControl(rm.public_key_hex, "zs-me", cid, "del", "Peer", dm)
|
||||
== ChatComposeStatus::Ok);
|
||||
EXPECT_EQ(feed(dm, "d1", now + 10), 0);
|
||||
EXPECT_TRUE(svc.conversationHiddenOnChain(cid));
|
||||
EXPECT_EQ((int)svc.store().conversation(cid).size(), 0);
|
||||
auto del = svc.onChainDeletedConversations();
|
||||
EXPECT_EQ((int)del.size(), 1);
|
||||
EXPECT_EQ(del[0].first, cid);
|
||||
EXPECT_EQ(del[0].second, std::string("Peer"));
|
||||
|
||||
// Re-ingesting the peer message while hidden does not resurface it.
|
||||
EXPECT_EQ(feed(pm, "m1", now), 0);
|
||||
EXPECT_EQ((int)svc.store().conversation(cid).size(), 0);
|
||||
|
||||
// A later restore un-hides it; a rescan then re-imports the message.
|
||||
OutgoingChatMemos rmemo;
|
||||
EXPECT_TRUE(buildOutgoingControl(rm.public_key_hex, "zs-me", cid, "res", "Peer", rmemo)
|
||||
== ChatComposeStatus::Ok);
|
||||
feed(rmemo, "r1", now + 20);
|
||||
EXPECT_TRUE(!svc.conversationHiddenOnChain(cid));
|
||||
EXPECT_EQ(feed(pm, "m1", now), 1);
|
||||
EXPECT_EQ((int)svc.store().conversation(cid).size(), 1);
|
||||
|
||||
// Monotonic: replaying the older delete does not re-hide.
|
||||
feed(dm, "d1", now + 10);
|
||||
EXPECT_TRUE(!svc.conversationHiddenOnChain(cid));
|
||||
EXPECT_EQ((int)svc.store().conversation(cid).size(), 1);
|
||||
}
|
||||
|
||||
// ChatService: metadata batch -> decrypt -> threaded, deduped in-memory store.
|
||||
void testHushChatService()
|
||||
{
|
||||
@@ -20026,6 +20086,7 @@ int main()
|
||||
testHushChatCrypto();
|
||||
testHushChatReceivePath();
|
||||
testHushChatControlMemo();
|
||||
testHushChatControlReducer();
|
||||
testHushChatService();
|
||||
testHushChatDatabase();
|
||||
testHushChatDeleteConversation();
|
||||
|
||||
Reference in New Issue
Block a user