修主人报的硬故障「后台思考的主动推送一个都没有推出来」(根因两条,缺一不可): - 后端 gateway/proactive.go 只广播 proactive_message 帧、**从不写会话历史** ⇒ 刷新补不回来 - 前端**零消费点** ⇒ 帧被静默丢弃(既无气泡也无通知) 后端:新增 proactive_persist.go(**先落库、再发帧**;web/assistant/mode=chat;未接线时空操作且不谎称已落库) 载荷新增 id/createdAt(落库行权威 id);PushResult.Detail 三态如实分开报告「投递到几个连接」与「是否落库」 前端:handlers/proactive.ts(气泡**恒落** + 按落库 id 去重 + **非前台才补通知** + 拒绝授权/畸形载荷可见降级) + lib/notify/proactive.ts(纯逻辑 isBackground/decideProactiveNotice/有界去重表) + handlers/task.ts(另三个被丢的帧 task_done/automation_result/subagent_result) + ws/resync.ts + useWsResync.ts(**重连重跑本页引导读取**,首次连接不跑) 双份核查(带三层证据):现状不双份 —— 路由分叉「一次 Push 至多一个 sink 成功」,且 IM 路由本来就把主动消息镜像回主会话恰好一次 契约 v0.72;变异实测:只发帧不落库 / 写成 user 角色 / 去重失效 / 前台也弹 / IM 成功再打活跃 ⇒ 全部打红
124 lines
3.9 KiB
Go
124 lines
3.9 KiB
Go
package gateway
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||
)
|
||
|
||
// TestHub_SendProactive_OwnerAndAdmin 验证主动推送恰好抵达
|
||
// owner/admin 连接并返回投递计数;viewer 或未认证
|
||
// (nil claims)连接绝不收到它。
|
||
func TestHub_SendProactive_OwnerAndAdmin(t *testing.T) {
|
||
t.Parallel()
|
||
hb := newTestHub(nil, nil)
|
||
owner := newFakeConn(hb, 8)
|
||
owner.claims = &auth.Claims{Role: auth.RoleOwner}
|
||
admin := newFakeConn(hb, 8)
|
||
admin.claims = &auth.Claims{Role: auth.RoleAdmin}
|
||
viewer := newFakeConn(hb, 8)
|
||
viewer.claims = &auth.Claims{Role: auth.RoleViewer}
|
||
bare := newFakeConn(hb, 8) // claims 为 nil:未认证
|
||
for _, c := range []*wsConn{owner, admin, viewer, bare} {
|
||
hb.register(c)
|
||
}
|
||
|
||
msg := adapter.UnifiedMessage{
|
||
Platform: "cyrene-backthink",
|
||
ChannelType: adapter.ChannelDirect,
|
||
Content: "记得喝水哦",
|
||
ContentType: adapter.ContentText,
|
||
Sender: adapter.Actor{Role: adapter.RoleSystem, DisplayName: "Cyrene"},
|
||
ChannelID: "kook:123",
|
||
SenderID: "uid-999",
|
||
MessageID: "m-42",
|
||
}
|
||
n, err := hb.SendProactive(context.Background(), msg)
|
||
if err != nil {
|
||
t.Fatalf("SendProactive err = %v", err)
|
||
}
|
||
if n.Connections != 2 {
|
||
t.Fatalf("count = %d, want 2 (owner+admin; viewer and bare excluded)", n.Connections)
|
||
}
|
||
|
||
for _, c := range []*wsConn{owner, admin} {
|
||
ev := readFakeFrame(t, c)
|
||
if ev.Type != "proactive_message" {
|
||
t.Fatalf("frame type = %q, want proactive_message (frame=%+v)", ev.Type, ev)
|
||
}
|
||
data, ok := ev.Data.(map[string]any)
|
||
if !ok {
|
||
t.Fatalf("frame data type = %T, want map[string]any", ev.Data)
|
||
}
|
||
if data["content"] != "记得喝水哦" {
|
||
t.Fatalf("frame content = %q, want the proactive text", data["content"])
|
||
}
|
||
if data["sender"] != "Cyrene" {
|
||
t.Fatalf("frame sender = %q, want Cyrene", data["sender"])
|
||
}
|
||
if data["platform"] != "cyrene-backthink" {
|
||
t.Fatalf("frame platform = %q, want cyrene-backthink", data["platform"])
|
||
}
|
||
}
|
||
for _, c := range []*wsConn{viewer, bare} {
|
||
if err := connHasNoFrame(c); err != nil {
|
||
t.Fatalf("unexpected frame for a non-owner/admin conn: %v", err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestHub_SendProactive_NoOnlineOwner 验证没有存活 owner/admin 连接时
|
||
// 计数为零(无错误),使调用方回退到 IM bridge。
|
||
func TestHub_SendProactive_NoOnlineOwner(t *testing.T) {
|
||
t.Parallel()
|
||
hb := newTestHub(nil, nil)
|
||
viewer := newFakeConn(hb, 8)
|
||
viewer.claims = &auth.Claims{Role: auth.RoleViewer}
|
||
hb.register(viewer)
|
||
|
||
n, err := hb.SendProactive(context.Background(), adapter.UnifiedMessage{Content: "hi"})
|
||
if err != nil {
|
||
t.Fatalf("SendProactive err = %v", err)
|
||
}
|
||
if n.Connections != 0 {
|
||
t.Fatalf("count = %d, want 0", n.Connections)
|
||
}
|
||
}
|
||
|
||
// TestHub_SendProactive_CancelledContext 验证被取消的 context 拒绝该
|
||
// 推送(fail closed)并返回被 wrap 的错误。
|
||
func TestHub_SendProactive_CancelledContext(t *testing.T) {
|
||
t.Parallel()
|
||
hb := newTestHub(nil, nil)
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
cancel()
|
||
|
||
if _, err := hb.SendProactive(ctx, adapter.UnifiedMessage{Content: "hi"}); err == nil {
|
||
t.Fatalf("expected a context error, got nil")
|
||
}
|
||
}
|
||
|
||
// TestServer_SendProactive_DelegatesToHub 验证 Server.SendProactive 镜像
|
||
// hub,使组合根能把网关自身交给汇聚点设置器。
|
||
func TestServer_SendProactive_DelegatesToHub(t *testing.T) {
|
||
t.Parallel()
|
||
env := newEnv(t, nil, false)
|
||
c := newFakeConn(env.srv.hub, 8)
|
||
c.claims = &auth.Claims{Role: auth.RoleOwner}
|
||
env.srv.hub.register(c)
|
||
|
||
n, err := env.srv.SendProactive(context.Background(), adapter.UnifiedMessage{Content: "hi"})
|
||
if err != nil {
|
||
t.Fatalf("SendProactive err = %v", err)
|
||
}
|
||
if n.Connections != 1 {
|
||
t.Fatalf("count = %d, want 1", n.Connections)
|
||
}
|
||
ev := readFakeFrame(t, c)
|
||
if ev.Type != "proactive_message" {
|
||
t.Fatalf("frame type = %q, want proactive_message", ev.Type)
|
||
}
|
||
}
|