fix(channel): message_sent 回声升 T2 落行,出站消息 id 不再被丢
塔转 M12 登记的缺口(塔裁定「归你」,文件都在本片 scope 内):她自己发出消息 的回声(message_sent)被当成「帧级链路遥测」一并豁免 self 升档 ⇒ 停在 T1, 只进计数、不落行,`message_id` 被丢,「她最近发了哪些消息」在库里查不到。 - events.go: 把 MessageSent 从 isFrameTelemetry 的豁免里摘出(它不是帧级遥测: 一帧 = 她真发出的一条消息,回声带的是那条出站消息的 id)。 心跳 / 生命周期的豁免**保持不变**(其登记的迁移债注释原样保留)。 - events_mapping_test.go: 两条 MessageSent 断言同步更新 (:52 自身回声命中自己 ⇒ 由 T1 改 T2;:33 无 self 信号仍走基础档 T1,标签改精确)。 - events_test.go: 新增端到端用例(真 SQLite):self 命中 ⇒ 落 1 行、tier=t2、 MessageID=out-msg-42 留在库里、不进上下文、不驱动 loop。
This commit is contained in:
1 parent
537a8b7988
commit
1d49d266f0
3 files changed
+56
-9
No files matched your search
@@ -259,10 +259,16 @@ func kindTier(kind adapter.NoticeKind) events.Tier {
|
||||
}
|
||||
}
|
||||
|
||||
// isFrameTelemetry 报告该类别是否属于**帧级链路遥测**:心跳(~15s 一帧)、生命周期、
|
||||
// 她自己发出消息的回声。这三类的 self 命中是**恒定**的(帧帧都命中 self_id),所以
|
||||
// 按 §2.2 升档等于「每帧写一行 channel_events」—— 而 §4.3 明令「心跳 / 在线状态逐帧
|
||||
// 写库是纯浪费」(计数表存在的理由),§3.1 给这三类的档位也正是 T1。
|
||||
// isFrameTelemetry 报告该类别是否属于**帧级链路遥测**:心跳(~15s 一帧)与生命周期。
|
||||
// 这两类的 self 命中是**恒定**的(帧帧都命中 self_id),所以按 §2.2 升档等于「每帧写一行
|
||||
// channel_events」—— 而 §4.3 明令「心跳 / 在线状态逐帧写库是纯浪费」(计数表存在的理由),
|
||||
// §3.1 给这两类的档位也正是 T1。
|
||||
//
|
||||
// 🔴 **`message_sent`(她自己发出消息的回声)不在此列**(切 M13;原 M12 登记的缺口):
|
||||
// 它**不是**帧级遥测 —— 一帧对应她真发出的一条消息,回声带的是那条出站消息的 id
|
||||
// (`RefMessage`)。此前把它一并豁免,会让 self 命中**不升档** ⇒ 整条回声只进计数,
|
||||
// `message_id` 被丢,「她自己最近发了哪些消息」在库里查不到(M12 的 A 层出站 id 因此
|
||||
// 断在这里)。摘出后它按 §2.2 正常 T1 → T2 逐条落行。
|
||||
//
|
||||
// ⚠️ **由此登记的债**(design/26 §2.6「她自己账号的在线 / 离线」= T2 高价值):
|
||||
// 那是**状态迁移**(在线→离线 / 离线→在线),不是每一帧。判定它需要迁移信号
|
||||
@@ -272,8 +278,7 @@ func kindTier(kind adapter.NoticeKind) events.Tier {
|
||||
// 迁移事件(T2)留待后续切片;已在切片报告「未做 / 存疑」中列明。
|
||||
func isFrameTelemetry(cat adapter.NoticeCategory) bool {
|
||||
switch cat {
|
||||
case adapter.NoticeCategoryHeartbeat, adapter.NoticeCategoryLifecycle,
|
||||
adapter.NoticeCategoryMessageSent:
|
||||
case adapter.NoticeCategoryHeartbeat, adapter.NoticeCategoryLifecycle:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestEventTier_两轴映射_正常(t *testing.T) {
|
||||
}{
|
||||
{"心跳是 T1", adapter.SystemNotice{Category: adapter.NoticeCategoryHeartbeat}, events.TierT1},
|
||||
{"生命周期是 T1", adapter.SystemNotice{Category: adapter.NoticeCategoryLifecycle}, events.TierT1},
|
||||
{"自身回声是 T1", adapter.SystemNotice{Category: adapter.NoticeCategoryMessageSent}, events.TierT1},
|
||||
{"自身回声(无 self 信号)是 T1", adapter.SystemNotice{Category: adapter.NoticeCategoryMessageSent}, events.TierT1},
|
||||
{"他人在线状态是 T1", adapter.SystemNotice{Category: adapter.NoticeCategoryClientStatus}, events.TierT1},
|
||||
{"未知类型是 T1", adapter.SystemNotice{Category: adapter.NoticeCategoryUnknown}, events.TierT1},
|
||||
{"解析失败是 T1", adapter.SystemNotice{Category: adapter.NoticeCategoryDecodeError}, events.TierT1},
|
||||
@@ -48,8 +48,8 @@ func TestEventTier_两轴映射_正常(t *testing.T) {
|
||||
adapter.SystemNotice{Category: adapter.NoticeCategoryHeartbeat, SelfRelated: true}, events.TierT1},
|
||||
{"生命周期命中自己 ⇒ 不升档(帧级遥测)",
|
||||
adapter.SystemNotice{Category: adapter.NoticeCategoryLifecycle, SelfRelated: true}, events.TierT1},
|
||||
{"自身回声命中自己 ⇒ 不升档(帧级遥测)",
|
||||
adapter.SystemNotice{Category: adapter.NoticeCategoryMessageSent, SelfRelated: true}, events.TierT1},
|
||||
{"自身回声命中自己 ⇒ 升档 T2(出站 id 回声须落行)",
|
||||
adapter.SystemNotice{Category: adapter.NoticeCategoryMessageSent, SelfRelated: true}, events.TierT2},
|
||||
{"类别缺失按 Kind 兜底:join ⇒ T2",
|
||||
adapter.SystemNotice{Kind: adapter.NoticeJoin}, events.TierT2},
|
||||
{"类别缺失按 Kind 兜底:request ⇒ T2",
|
||||
|
||||
@@ -205,6 +205,48 @@ func TestRecordEvent_unknown不进上下文但落行_正常(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRecordEvent_自身回声升T2落行带出站id_正常 钉住切 M13 修掉的缺口(原 M12 登记):
|
||||
// 她自己发出消息的回声(message_sent)**不是帧级遥测** —— self 命中必须把它从 T1 升到 T2
|
||||
// 逐条落行,`message_id`(出站消息 id)随之留在库里;此前它被帧级豁免挡回 T1,id 被丢。
|
||||
func TestRecordEvent_自身回声升T2落行带出站id_正常(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
store := newTestEventStore(t)
|
||||
|
||||
ad := eventAdapter("qq-obv11:conf0", adapter.SystemNotice{
|
||||
Kind: adapter.NoticeOther,
|
||||
Category: adapter.NoticeCategoryMessageSent,
|
||||
PlatformType: "message_sent",
|
||||
Ignorable: true,
|
||||
SelfRelated: true,
|
||||
RefMessage: "out-msg-42",
|
||||
})
|
||||
b, chat, hist, _ := eventBridgeFixture(t, ad, store)
|
||||
|
||||
b.processFrom(ctx, groupNoticeMessage(), ad)
|
||||
|
||||
if chat.sessionID != "" {
|
||||
t.Fatalf("出站回声驱动了 loop:session=%q", chat.sessionID)
|
||||
}
|
||||
if recs := hist.records(); len(recs) != 0 {
|
||||
t.Fatalf("出站回声被登记进会话历史:%+v", recs)
|
||||
}
|
||||
rows, err := store.List(ctx, events.Filter{})
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("出站回声落行数 = %d, want 1(self 命中 ⇒ T2 逐条落行)", len(rows))
|
||||
}
|
||||
ev := rows[0]
|
||||
if ev.Tier != events.TierT2 || ev.Category != events.Category(adapter.NoticeCategoryMessageSent) {
|
||||
t.Fatalf("落行档位/类别 = (%s,%s), want (t2,message_sent)", ev.Tier, ev.Category)
|
||||
}
|
||||
if ev.MessageID != "out-msg-42" {
|
||||
t.Fatalf("出站消息 id = %q, want out-msg-42(id 必须留在库里)", ev.MessageID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRecordEvent_未接线时入站照常_异常:`Options.EventStore == nil` 时降级为
|
||||
// 「内存计数 + 首次一条 DEBUG」,**绝不**中断入站 —— 高价值事件的升级真会话行为必须
|
||||
// 逐字不变(fail-visible 而非 fail-crash,§4.3)。
|
||||
|
||||
Reference in new issue
Block a user