package repo import ( "context" "errors" "testing" "github.com/agentmail/gateway/internal/db" ) // RecipientDeliverable 是「邮件会不会进黑洞」的唯一判据。 // // 事故背景:发信路径原来只校验地址语法、调用权限与会话别名,从不问 // 「这个名字存在吗」。实测发给已彻底删除的 remotebot 返回 200,邮件入库、 // 分配了 20 个来回预算、建好会话,而那一端永远不会有人读。发件人看到 200 // 和一个 session_id,以为送出去了 —— 静默丢件比报错严重,报错能立刻改, // 静默丢件要等对方追问才发现。 func TestRecipientDeliverable_Human(t *testing.T) { setupTestDB(t) ctx := context.Background() if _, err := db.DB.ExecContext(ctx, `INSERT INTO users (username, password_hash) VALUES ('alice', 'x')`); err != nil { t.Fatalf("seed user: %v", err) } if err := RecipientDeliverable(ctx, "alice"); err != nil { t.Fatalf("人类用户应当可达,得到: %v", err) } } func TestRecipientDeliverable_OnlineAgent(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) if err := RecipientDeliverable(ctx, "pi"); err != nil { t.Fatalf("在册且未停用的 Agent 应当可达,得到: %v", err) } } func TestRecipientDeliverable_UnknownName(t *testing.T) { setupTestDB(t) ctx := context.Background() err := RecipientDeliverable(ctx, "ghost") if !errors.Is(err, ErrRecipientUnknown) { t.Fatalf("不存在的收件人应当返回 ErrRecipientUnknown,得到: %v", err) } } // 删除后立刻不可达 —— 这正是本次事故的场景。 func TestRecipientDeliverable_AfterDelete(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "remotebot", 20) if err := RecipientDeliverable(ctx, "remotebot"); err != nil { t.Fatalf("删除前应当可达,得到: %v", err) } if _, err := DeleteAgent(ctx, "remotebot"); err != nil { t.Fatalf("delete agent: %v", err) } err := RecipientDeliverable(ctx, "remotebot") if !errors.Is(err, ErrRecipientUnknown) { t.Fatalf("删除后必须不可达,得到: %v", err) } } // 停用选择「当场拒收」而不是「入库等恢复后补投」:停用的语义就是这个 // Agent 现在不干活,让发件人以为信已送达更坏 —— 它会照常等回信。 func TestRecipientDeliverable_Disabled(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "opencode", 20) if _, err := SetAgentDisabled(ctx, "opencode", true); err != nil { t.Fatalf("disable: %v", err) } err := RecipientDeliverable(ctx, "opencode") if !errors.Is(err, ErrRecipientDisabled) { t.Fatalf("已停用的 Agent 应当返回 ErrRecipientDisabled,得到: %v", err) } } // 恢复后重新可达,否则停用就成了不可逆操作。 func TestRecipientDeliverable_ReenabledAgain(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "opencode", 20) if _, err := SetAgentDisabled(ctx, "opencode", true); err != nil { t.Fatalf("disable: %v", err) } if _, err := SetAgentDisabled(ctx, "opencode", false); err != nil { t.Fatalf("re-enable: %v", err) } if err := RecipientDeliverable(ctx, "opencode"); err != nil { t.Fatalf("恢复后应当重新可达,得到: %v", err) } } // 空名交给上层的地址解析处理,这里放行 —— 不然人类给自己发信 // (to 位省略 name)会被这道检查误伤。 func TestRecipientDeliverable_EmptyName(t *testing.T) { setupTestDB(t) if err := RecipientDeliverable(context.Background(), ""); err != nil { t.Fatalf("空名应当放行,得到: %v", err) } } // 人类用户名与 Agent 名共用命名空间。一个名字同时是人类用户时按人类算 —— // 人的收件箱一直在,不受 Agent 停用影响。 func TestRecipientDeliverable_HumanWinsOverDisabledAgent(t *testing.T) { setupTestDB(t) ctx := context.Background() if _, err := db.DB.ExecContext(ctx, `INSERT INTO users (username, password_hash) VALUES ('dual', 'x')`); err != nil { t.Fatalf("seed user: %v", err) } seedAgent(t, "dual", 20) if _, err := SetAgentDisabled(ctx, "dual", true); err != nil { t.Fatalf("disable: %v", err) } if err := RecipientDeliverable(ctx, "dual"); err != nil { t.Fatalf("同名人类用户应当优先放行,得到: %v", err) } }