package adapter // 本文件证明出站 id 出口(outbound.go)的三条硬性质:**向后兼容**(老适配器一行不改、 // 探测失败即如实报「不知道」)、**三态可分辨**、**取不到就是空 / 绝不编造**。 import ( "context" "errors" "testing" ) // receiptAdapter 是同时实现了 PlatformAdapter 与 OutboundReceiptAdapter 的桩: // 它记录两条路径各被调用了几次,用来证明「实现了 seam 的适配器**只**走凭据路径」 // (绝不出现一条消息发两遍)。 type receiptAdapter struct { mockAdapter receiptCalls int receipts []SendReceipt } func (m *receiptAdapter) SendMessageWithReceipt(_ context.Context, resp UnifiedResponse) (SendOutcome, error) { m.receiptCalls++ m.sent = resp.Messages return SendOutcome{Receipts: m.receipts}, nil } var ( _ PlatformAdapter = (*receiptAdapter)(nil) _ OutboundReceiptAdapter = (*receiptAdapter)(nil) ) // failAdapter 的投递恒失败,用来验证错误原样穿过 SendWithReceipt。 type failAdapter struct { mockAdapter err error } func (f *failAdapter) SendMessage(context.Context, UnifiedResponse) error { return f.err } func TestNewSendReceiptNeverReportsEmptyID(t *testing.T) { t.Parallel() absent := NewSendReceipt("") if absent.MessageID != "" || absent.IDStatus != SendIDAbsent || absent.Reported() { t.Errorf("NewSendReceipt(\"\") = %+v, want absent with empty id", absent) } reported := NewSendReceipt("1700000012345") if reported.MessageID != "1700000012345" || !reported.Reported() { t.Errorf("NewSendReceipt(id) = %+v, want reported id", reported) } } // TestSendReceiptZeroValueIsUnknown 钉住 fail-closed 的零值口径:一个只写了 // `SendReceipt{}` 的实现表示「不知道」,绝不能被读成「平台给了 id」。 func TestSendReceiptZeroValueIsUnknown(t *testing.T) { t.Parallel() var zero SendReceipt if zero.IDStatus != SendIDUnreported { t.Errorf("zero IDStatus = %q, want %q", zero.IDStatus, SendIDUnreported) } if zero.MessageID != "" || zero.Reported() { t.Errorf("zero receipt = %+v, want empty and not reported", zero) } } // TestPlainAdapterIsNotAReceiptAdapter 是**向后兼容**的运行时证据:一个只实现 // PlatformAdapter 的适配器(等价于 kook / telegram / weixin-ilink / webhook)**不是** // OutboundReceiptAdapter —— 契约没被破坏,它们一行都不用改。 func TestPlainAdapterIsNotAReceiptAdapter(t *testing.T) { t.Parallel() var a PlatformAdapter = &mockAdapter{name: "telegram"} if _, ok := a.(OutboundReceiptAdapter); ok { t.Fatal("plain adapter must not satisfy OutboundReceiptAdapter") } } // TestSendWithReceiptFallsBackToSendMessage 覆盖未接线渠道:投递照旧走 SendMessage, // 凭据如实报「未取到」——**逐条**与 Messages 等长,且 id 全空(绝不编造)。 func TestSendWithReceiptFallsBackToSendMessage(t *testing.T) { t.Parallel() ad := &mockAdapter{name: "telegram"} resp := UnifiedResponse{Messages: []UnifiedMessage{ {ChannelType: ChannelDirect, ContentType: ContentText, Content: "一"}, {ChannelType: ChannelDirect, ContentType: ContentText, Content: "二"}, }} out, err := SendWithReceipt(context.Background(), ad, resp) if err != nil { t.Fatalf("SendWithReceipt() error = %v", err) } if len(ad.sent) != 2 { t.Fatalf("fallback did not deliver through SendMessage: sent = %+v", ad.sent) } if len(out.Receipts) != 2 { t.Fatalf("Receipts len = %d, want 2 (one per message)", len(out.Receipts)) } for i, r := range out.Receipts { if r.MessageID != "" || r.IDStatus != SendIDUnreported || r.Reported() { t.Errorf("receipt[%d] = %+v, want empty id with unreported status", i, r) } } } // TestSendWithReceiptUsesSeamWhenImplemented 证明实现了 seam 的适配器只走凭据路径 // (不会两条路径各发一遍)。 func TestSendWithReceiptUsesSeamWhenImplemented(t *testing.T) { t.Parallel() ad := &receiptAdapter{receipts: []SendReceipt{NewSendReceipt("9001")}} ad.name = "qq-obv11" out, err := SendWithReceipt(context.Background(), ad, UnifiedResponse{Messages: []UnifiedMessage{ {ChannelType: ChannelGroup, GroupID: "123", ContentType: ContentText, Content: "pong"}, }}) if err != nil { t.Fatalf("SendWithReceipt() error = %v", err) } if ad.receiptCalls != 1 { t.Errorf("receipt path calls = %d, want 1", ad.receiptCalls) } if len(out.Receipts) != 1 || out.Receipts[0].MessageID != "9001" { t.Errorf("Receipts = %+v, want one reported 9001", out.Receipts) } } func TestSendWithReceiptPropagatesError(t *testing.T) { t.Parallel() want := errors.New("boom") ad := &failAdapter{err: want} _, err := SendWithReceipt(context.Background(), ad, UnifiedResponse{Messages: []UnifiedMessage{ {ChannelType: ChannelDirect, ContentType: ContentText, Content: "x"}, }}) if !errors.Is(err, want) { t.Fatalf("SendWithReceipt() error = %v, want %v", err, want) } } func TestSendWithReceiptNilAdapter(t *testing.T) { t.Parallel() if _, err := SendWithReceipt(context.Background(), nil, UnifiedResponse{}); !errors.Is(err, ErrNotConnected) { t.Fatalf("SendWithReceipt(nil) error = %v, want ErrNotConnected", err) } }