feat(chat): add an independent "sensitive" axis to the control reducer
Routes control ops to one of two independent per-conversation reductions: the hide axis (del/res) and a new sensitive axis (sensitive/unsensitive), so a conversation can be deleted AND sensitive without the two states clobbering each other. Unknown future ops are ignored (not stored in either axis). Adds conversationSensitiveOnChain() + onChainSensitiveConversations(); the reduction stays monotonic by (compose-time, txid). Reducer-tested, including axis independence and that marking sensitive neither hides nor erases a thread. Phase B, step 1 (reducer) of per-conversation local-key encryption (policy level). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15133,6 +15133,50 @@ void testHushChatControlReducer()
|
||||
feed(dm, "d1", now + 10);
|
||||
EXPECT_TRUE(!svc.conversationHiddenOnChain(cid));
|
||||
EXPECT_EQ((int)svc.store().conversation(cid).size(), 1);
|
||||
|
||||
// ---- Sensitive axis: independent of the hide axis. Build control metadata directly with explicit
|
||||
// timestamps so the monotonic ordering is deterministic (not subject to compose-time ties). ----
|
||||
auto ctl = [&](const std::string& op, const std::string& c, std::int64_t ts, const std::string& txid) {
|
||||
std::vector<HushChatTransactionMetadata> md(1);
|
||||
md[0].txid = txid;
|
||||
md[0].type = HushChatHeaderType::Control;
|
||||
md[0].op = op;
|
||||
md[0].conversation_id = c;
|
||||
md[0].reply_zaddr = "zs-me";
|
||||
md[0].sender_public_key_hex = rm.public_key_hex;
|
||||
md[0].payload_memo = "Peer";
|
||||
md[0].sent_at = ts;
|
||||
return svc.ingest(md, {}, ts);
|
||||
};
|
||||
|
||||
EXPECT_TRUE(!svc.conversationSensitiveOnChain(cid));
|
||||
EXPECT_EQ(ctl("sensitive", cid, now + 100, "s1"), 0); // a control adds no message
|
||||
EXPECT_TRUE(svc.conversationSensitiveOnChain(cid)); // marked sensitive
|
||||
EXPECT_TRUE(!svc.conversationHiddenOnChain(cid)); // but NOT hidden (independent axis)
|
||||
EXPECT_EQ((int)svc.store().conversation(cid).size(), 1); // and the thread is not erased
|
||||
{
|
||||
auto sens = svc.onChainSensitiveConversations();
|
||||
EXPECT_EQ((int)sens.size(), 1);
|
||||
EXPECT_EQ(sens[0].first, cid);
|
||||
}
|
||||
|
||||
// A conversation can be hidden AND sensitive at the same time.
|
||||
ctl("del", cid, now + 200, "d2");
|
||||
EXPECT_TRUE(svc.conversationHiddenOnChain(cid));
|
||||
EXPECT_TRUE(svc.conversationSensitiveOnChain(cid));
|
||||
EXPECT_EQ((int)svc.store().conversation(cid).size(), 0); // the hide erased the view; sensitive persists
|
||||
|
||||
// unsensitive (later) clears sensitive and leaves the hide axis untouched; an older replay can't re-set.
|
||||
ctl("unsensitive", cid, now + 300, "u1");
|
||||
EXPECT_TRUE(!svc.conversationSensitiveOnChain(cid));
|
||||
EXPECT_TRUE(svc.conversationHiddenOnChain(cid));
|
||||
ctl("sensitive", cid, now + 100, "s1"); // replay the older sensitive
|
||||
EXPECT_TRUE(!svc.conversationSensitiveOnChain(cid));
|
||||
|
||||
// An unknown future op touches neither axis.
|
||||
ctl("futurecustomop", "conv-y", now + 400, "x1");
|
||||
EXPECT_TRUE(!svc.conversationHiddenOnChain("conv-y"));
|
||||
EXPECT_TRUE(!svc.conversationSensitiveOnChain("conv-y"));
|
||||
}
|
||||
|
||||
// ChatService: metadata batch -> decrypt -> threaded, deduped in-memory store.
|
||||
|
||||
Reference in New Issue
Block a user