feat: add fake send connector contract

This commit is contained in:
zhaoyilun
2026-07-10 20:22:02 +08:00
parent 9eb7f382cc
commit dadf42817c
6 changed files with 343 additions and 18 deletions

View File

@@ -146,6 +146,153 @@ func TestISphereSendMessageProductionIsBlockedWithoutSideEffects(t *testing.T) {
}
}
func TestISphereSendMessageFakeConnectorAcceptedAudit(t *testing.T) {
audit := &fakeSendMessageAuditSink{}
idempotency := newFakeSendMessageIdempotencyStore()
connector := &fakeSendMessageConnector{
result: SendMessageConnectorResult{
Accepted: true,
Status: "accepted",
AckRef: "fake-ack-1",
ConnectorMode: "fake",
},
}
session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) {
RegisterISphereSendMessageToolWithStateAndConnector(server, audit, idempotency, connector)
})
defer cleanup()
content := "fake connector accepted"
contentHash := sha256HexForSendMessageTest(content)
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: ToolNameSendMessage,
Arguments: map[string]any{
"target_type": "direct",
"target_id": "alice@imopenfire1-lanzhou",
"content_text": content,
"content_sha256": contentHash,
"idempotency_key": "idem-fake-accepted-1",
"execution_mode": "production",
},
})
if err != nil {
t.Fatalf("call %s fake connector accepted: %v", ToolNameSendMessage, err)
}
if callResult.IsError {
t.Fatalf("fake connector accepted result should be structured, got error: %+v", callResult)
}
var decoded struct {
OK bool `json:"ok"`
SendStatus string `json:"send_status"`
ConnectorMode string `json:"connector_mode"`
ProductionSendEnabled bool `json:"production_send_enabled"`
SideEffects map[string]any `json:"side_effects"`
Audit map[string]any `json:"audit"`
}
payload, _ := json.Marshal(callResult.StructuredContent)
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode fake accepted payload %s: %v", payload, err)
}
if !decoded.OK || decoded.SendStatus != "accepted" || decoded.ConnectorMode != "fake" {
t.Fatalf("unexpected fake accepted response: %s", payload)
}
if decoded.ProductionSendEnabled {
t.Fatalf("fake connector must not flip default production_send_enabled: %s", payload)
}
if decoded.SideEffects["sent_message"] != false || decoded.SideEffects["clicked_ui"] != false || decoded.SideEffects["captured_network"] != false {
t.Fatalf("fake connector should not report real side effects: %#v", decoded.SideEffects)
}
if decoded.Audit["ack_ref"] != "fake-ack-1" || decoded.Audit["result"] != "accepted" {
t.Fatalf("unexpected fake accepted audit: %#v", decoded.Audit)
}
if strings.Contains(string(payload), content) || strings.Contains(string(payload), "idem-fake-accepted-1") {
t.Fatalf("fake accepted payload leaked raw content or idempotency key: %s", payload)
}
if connector.calls != 1 {
t.Fatalf("fake connector calls = %d, want 1", connector.calls)
}
if len(audit.events) != 1 {
t.Fatalf("audit events = %+v, want one", audit.events)
}
event := audit.events[0]
if event.Result != "accepted" || event.AckRef != "fake-ack-1" || event.ConnectorMode != "fake" {
t.Fatalf("unexpected audit event: %+v", event)
}
if event.ContentText != "" || event.IdempotencyKey != "" {
t.Fatalf("audit event leaked raw content or idempotency key: %+v", event)
}
}
func TestISphereSendMessageFakeConnectorFailureAudit(t *testing.T) {
audit := &fakeSendMessageAuditSink{}
idempotency := newFakeSendMessageIdempotencyStore()
connector := &fakeSendMessageConnector{
result: SendMessageConnectorResult{
Accepted: false,
Status: "failed",
ErrorCode: "fake_rejected",
ErrorMessage: "fake connector rejected the request",
ConnectorMode: "fake",
},
}
session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) {
RegisterISphereSendMessageToolWithStateAndConnector(server, audit, idempotency, connector)
})
defer cleanup()
content := "fake connector failed"
contentHash := sha256HexForSendMessageTest(content)
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: ToolNameSendMessage,
Arguments: map[string]any{
"target_type": "group",
"target_id": "project-room@conference.imopenfire1-lanzhou",
"content_text": content,
"content_sha256": contentHash,
"idempotency_key": "idem-fake-failed-1",
"execution_mode": "production",
},
})
if err != nil {
t.Fatalf("call %s fake connector failure: %v", ToolNameSendMessage, err)
}
if callResult.IsError {
t.Fatalf("fake connector failure should be structured, got error: %+v", callResult)
}
var decoded struct {
OK bool `json:"ok"`
SendStatus string `json:"send_status"`
ErrorCode string `json:"error_code"`
SideEffects map[string]any `json:"side_effects"`
Audit map[string]any `json:"audit"`
}
payload, _ := json.Marshal(callResult.StructuredContent)
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode fake failure payload %s: %v", payload, err)
}
if decoded.OK || decoded.SendStatus != "failed" || decoded.ErrorCode != "fake_rejected" {
t.Fatalf("unexpected fake failure response: %s", payload)
}
if decoded.SideEffects["sent_message"] != false || decoded.SideEffects["typed_text"] != false {
t.Fatalf("fake failure should not report real side effects: %#v", decoded.SideEffects)
}
if decoded.Audit["error_code"] != "fake_rejected" || decoded.Audit["result"] != "failed" {
t.Fatalf("unexpected fake failure audit: %#v", decoded.Audit)
}
if strings.Contains(string(payload), content) || strings.Contains(string(payload), "idem-fake-failed-1") {
t.Fatalf("fake failure payload leaked raw content or idempotency key: %s", payload)
}
if len(audit.events) != 1 {
t.Fatalf("audit events = %+v, want one", audit.events)
}
event := audit.events[0]
if event.Result != "failed" || event.ErrorCode != "fake_rejected" || event.ConnectorMode != "fake" {
t.Fatalf("unexpected audit event: %+v", event)
}
}
func TestISphereSendMessageRejectsContentHashMismatch(t *testing.T) {
session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) {
RegisterISphereSendMessageToolWithState(server, &fakeSendMessageAuditSink{}, newFakeSendMessageIdempotencyStore())
@@ -370,6 +517,17 @@ func (f *fakeSendMessageIdempotencyStore) ReserveSendMessageIdempotency(_ contex
return SendMessageIdempotencyDecision{}, nil
}
type fakeSendMessageConnector struct {
result SendMessageConnectorResult
err error
calls int
}
func (f *fakeSendMessageConnector) ExecuteSendMessage(_ context.Context, _ SendMessageConnectorRequest) (SendMessageConnectorResult, error) {
f.calls++
return f.result, f.err
}
func sha256HexForSendMessageTest(value string) string {
sum := sha256.Sum256([]byte(value))
return hex.EncodeToString(sum[:])