diff --git a/channels/shangwutong/db/generated/outbound.sql.go b/channels/shangwutong/db/generated/outbound.sql.go index f4bf11e8..2b95f10a 100644 --- a/channels/shangwutong/db/generated/outbound.sql.go +++ b/channels/shangwutong/db/generated/outbound.sql.go @@ -10,6 +10,37 @@ import ( "time" ) +const awaitOutboundEcho = `-- name: AwaitOutboundEcho :exec +UPDATE outbound_messages SET + delivery_status = 'uncertain', + external_id = NULL, + external_error_code = NULL, + status_sync_status = 'not_required', + claimed_at = NULL, + last_error = NULL, + updated_at = CURRENT_TIMESTAMP +WHERE id = ? AND delivery_status = 'delivering' +` + +func (q *Queries) AwaitOutboundEcho(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, awaitOutboundEcho, id) + return err +} + +const awaitOutboundPartEcho = `-- name: AwaitOutboundPartEcho :exec +UPDATE outbound_parts SET + delivery_status = 'uncertain', + external_id = NULL, + last_error = NULL, + updated_at = CURRENT_TIMESTAMP +WHERE id = ? AND delivery_status = 'pending' +` + +func (q *Queries) AwaitOutboundPartEcho(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, awaitOutboundPartEcho, id) + return err +} + const claimOutboundMessage = `-- name: ClaimOutboundMessage :one UPDATE outbound_messages SET delivery_status = 'delivering', @@ -199,7 +230,7 @@ UPDATE outbound_messages SET SELECT 1 FROM outbound_parts WHERE outbound_message_id = outbound_messages.id AND delivery_status <> 'delivered' - ) THEN 'pending' + ) THEN 'uncertain' ELSE 'delivered' END, result_version = result_version + 1, @@ -214,10 +245,11 @@ UPDATE outbound_messages SET ELSE 'pending' END, status_sync_next_at = NULL, + claimed_at = NULL, last_error = NULL, updated_at = CURRENT_TIMESTAMP WHERE outbound_messages.id = ?2 - AND delivery_status IN ('delivered', 'uncertain', 'failed') + AND delivery_status IN ('delivering', 'delivered', 'uncertain', 'failed') AND (delivery_status <> 'failed' OR external_error_code = 'uncertain_timeout') ` @@ -242,7 +274,7 @@ UPDATE outbound_parts SET updated_at = CURRENT_TIMESTAMP WHERE id = ? AND external_id IS NULL - AND delivery_status IN ('delivered', 'uncertain') + AND delivery_status IN ('pending', 'delivered', 'uncertain') ` type ConfirmOutboundPartEchoParams struct { @@ -476,13 +508,13 @@ const listOutboundEchoCandidates = `-- name: ListOutboundEchoCandidates :many SELECT id, account_id, swt_sid, event_id, occurred_at, gochat_message_id, retry_version, message_type, content, payload, delivery_status, result_version, external_id, external_error_code, claimed_at, attempts, next_attempt_at, status_sync_status, status_sync_attempts, status_sync_next_at, status_reported_at, last_error, created_at, updated_at FROM outbound_messages WHERE account_id = ? AND swt_sid = ? - AND delivery_status IN ('delivered', 'uncertain', 'failed') + AND delivery_status IN ('delivering', 'delivered', 'uncertain', 'failed') AND (delivery_status <> 'failed' OR external_error_code = 'uncertain_timeout') AND EXISTS ( SELECT 1 FROM outbound_parts WHERE outbound_message_id = outbound_messages.id AND external_id IS NULL - AND delivery_status IN ('delivered', 'uncertain') + AND delivery_status IN ('pending', 'delivered', 'uncertain') ) ORDER BY id LIMIT 20 diff --git a/channels/shangwutong/db/queries/outbound.sql b/channels/shangwutong/db/queries/outbound.sql index b024e28d..293f0245 100644 --- a/channels/shangwutong/db/queries/outbound.sql +++ b/channels/shangwutong/db/queries/outbound.sql @@ -29,6 +29,14 @@ UPDATE outbound_parts SET updated_at = CURRENT_TIMESTAMP WHERE id = ? AND delivery_status = 'pending'; +-- name: AwaitOutboundPartEcho :exec +UPDATE outbound_parts SET + delivery_status = 'uncertain', + external_id = NULL, + last_error = NULL, + updated_at = CURRENT_TIMESTAMP +WHERE id = ? AND delivery_status = 'pending'; + -- name: MarkOutboundPartUncertain :exec UPDATE outbound_parts SET delivery_status = 'uncertain', @@ -110,6 +118,17 @@ UPDATE outbound_messages SET updated_at = CURRENT_TIMESTAMP WHERE id = ? AND delivery_status = 'delivering'; +-- name: AwaitOutboundEcho :exec +UPDATE outbound_messages SET + delivery_status = 'uncertain', + external_id = NULL, + external_error_code = NULL, + status_sync_status = 'not_required', + claimed_at = NULL, + last_error = NULL, + updated_at = CURRENT_TIMESTAMP +WHERE id = ? AND delivery_status = 'delivering'; + -- name: MarkOutboundUncertain :exec UPDATE outbound_messages SET delivery_status = 'uncertain', @@ -230,13 +249,13 @@ WHERE id = ? AND status_sync_status = 'syncing'; SELECT * FROM outbound_messages WHERE account_id = ? AND swt_sid = ? - AND delivery_status IN ('delivered', 'uncertain', 'failed') + AND delivery_status IN ('delivering', 'delivered', 'uncertain', 'failed') AND (delivery_status <> 'failed' OR external_error_code = 'uncertain_timeout') AND EXISTS ( SELECT 1 FROM outbound_parts WHERE outbound_message_id = outbound_messages.id AND external_id IS NULL - AND delivery_status IN ('delivered', 'uncertain') + AND delivery_status IN ('pending', 'delivered', 'uncertain') ) ORDER BY id LIMIT 20; @@ -248,7 +267,7 @@ UPDATE outbound_messages SET SELECT 1 FROM outbound_parts WHERE outbound_message_id = outbound_messages.id AND delivery_status <> 'delivered' - ) THEN 'pending' + ) THEN 'uncertain' ELSE 'delivered' END, result_version = result_version + 1, @@ -263,10 +282,11 @@ UPDATE outbound_messages SET ELSE 'pending' END, status_sync_next_at = NULL, + claimed_at = NULL, last_error = NULL, updated_at = CURRENT_TIMESTAMP WHERE outbound_messages.id = sqlc.arg(id) - AND delivery_status IN ('delivered', 'uncertain', 'failed') + AND delivery_status IN ('delivering', 'delivered', 'uncertain', 'failed') AND (delivery_status <> 'failed' OR external_error_code = 'uncertain_timeout'); -- name: ConfirmOutboundPartEcho :execrows @@ -277,7 +297,7 @@ UPDATE outbound_parts SET updated_at = CURRENT_TIMESTAMP WHERE id = ? AND external_id IS NULL - AND delivery_status IN ('delivered', 'uncertain'); + AND delivery_status IN ('pending', 'delivered', 'uncertain'); -- name: FailExpiredUncertainMessages :execrows UPDATE outbound_messages SET diff --git a/channels/shangwutong/internal/delivery/inbound.go b/channels/shangwutong/internal/delivery/inbound.go index eea1f637..610a44f6 100644 --- a/channels/shangwutong/internal/delivery/inbound.go +++ b/channels/shangwutong/internal/delivery/inbound.go @@ -362,7 +362,7 @@ func (i *Inbound) confirmOutboundEcho(ctx context.Context, account *dbgen.Accoun } func outboundPartMatchesEcho(part *dbgen.OutboundPart, mapped mappedEvent, eventTime time.Time) bool { - if part == nil || part.ExternalID != nil || (part.DeliveryStatus != "delivered" && part.DeliveryStatus != "uncertain") { + if part == nil || part.ExternalID != nil || (part.DeliveryStatus != "pending" && part.DeliveryStatus != "delivered" && part.DeliveryStatus != "uncertain") { return false } if delta := eventTime.Sub(part.UpdatedAt); delta < -echoMatchWindow || delta > echoMatchWindow { diff --git a/channels/shangwutong/internal/delivery/inbound_test.go b/channels/shangwutong/internal/delivery/inbound_test.go index c589eda0..81c56d74 100644 --- a/channels/shangwutong/internal/delivery/inbound_test.go +++ b/channels/shangwutong/internal/delivery/inbound_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "strings" "sync" "testing" @@ -280,6 +281,48 @@ func TestInboundKind3ConfirmsUniqueOutboundEcho(t *testing.T) { } } +func TestAcceptedOutboundWaitsForEastEightKind3EchoBeforeSent(t *testing.T) { + ctx := context.Background() + database, account := deliveryDatabase(t, ctx) + if _, _, err := database.EnqueueOutbound(ctx, store.OutboundInput{ + AccountID: account.ID, SWTSessionID: "visitor", EventID: "message:77:created", OccurredAt: time.Now(), GoChatMessageID: 77, + MessageType: "text", Content: stringPointer("hello"), Payload: deliveryPayload(t, nil), + }, false); err != nil { + t.Fatal(err) + } + results := &resultRecorder{} + outbound, _ := NewOutbound(database, sessionStub{}, senderStub{}, results, nil, 1) + if worked, err := outbound.processOutbound(ctx); err != nil || !worked { + t.Fatalf("accepted delivery = %v, %v", worked, err) + } + queued, err := database.Reader().GetOutboundByGoChatMessageID(ctx, 77) + if err != nil || queued.DeliveryStatus != "uncertain" || queued.StatusSyncStatus != "not_required" { + t.Fatalf("accepted outbound = %#v, %v", queued, err) + } + if worked, err := outbound.processStatus(ctx); err != nil || worked || results.result.Status != "" { + t.Fatalf("accepted send must remain progress: worked=%v err=%v result=%#v", worked, err, results.result) + } + + const dotNetUnixEpochTicks int64 = 621355968000000000 + localNow := time.Now().UTC().Add(8 * time.Hour) + ticks := dotNetUnixEpochTicks + localNow.Unix()*10_000_000 + int64(localNow.Nanosecond()/100) + persistInboundEvent(t, database, account, swt.HeartbeatEvent{ + SessionID: "visitor", Kind: 3, OpName: "agent", Text: "hello", SeqID: 51, + Timestamp: strconv.FormatInt(ticks, 10), RawLine: "visitor 3 agent|hello 51 ticks", + }) + inbound, _ := NewInbound(database, &inboundRecorder{}, nil, 1) + if worked, err := inbound.processInbound(ctx); err != nil || !worked { + t.Fatalf("process echo = %v, %v", worked, err) + } + queued, err = database.Reader().GetOutboundByGoChatMessageID(ctx, 77) + if err != nil || queued.DeliveryStatus != "delivered" || value(queued.ExternalID) != "51" || queued.StatusSyncStatus != "pending" { + t.Fatalf("confirmed outbound = %#v, %v", queued, err) + } + if worked, err := outbound.processStatus(ctx); err != nil || !worked || results.result.Status != "sent" || value(results.result.ExternalID) != "51" { + t.Fatalf("confirmed status = worked:%v err:%v result:%#v", worked, err, results.result) + } +} + func TestInboundKind3ConfirmsMultipleOutboundPartsIndependently(t *testing.T) { ctx := context.Background() database, account := deliveryDatabase(t, ctx) diff --git a/channels/shangwutong/internal/delivery/mapping.go b/channels/shangwutong/internal/delivery/mapping.go index 0c63bce8..120c8592 100644 --- a/channels/shangwutong/internal/delivery/mapping.go +++ b/channels/shangwutong/internal/delivery/mapping.go @@ -751,10 +751,13 @@ func parseSWTTime(value string, fallback time.Time) time.Time { } } if numeric, err := strconv.ParseInt(value, 10, 64); err == nil { - const dotNetUnixEpochTicks int64 = 621355968000000000 + const ( + dotNetUnixEpochTicks int64 = 621355968000000000 + swtUTCOffset = 8 * time.Hour + ) if numeric >= dotNetUnixEpochTicks { delta := numeric - dotNetUnixEpochTicks - return time.Unix(delta/10_000_000, (delta%10_000_000)*100).UTC() + return time.Unix(delta/10_000_000, (delta%10_000_000)*100).Add(-swtUTCOffset).UTC() } if numeric > 1_000_000_000_000 { return time.UnixMilli(numeric).UTC() diff --git a/channels/shangwutong/internal/delivery/mapping_test.go b/channels/shangwutong/internal/delivery/mapping_test.go index 215a116a..c3c3fdd9 100644 --- a/channels/shangwutong/internal/delivery/mapping_test.go +++ b/channels/shangwutong/internal/delivery/mapping_test.go @@ -1,6 +1,7 @@ package delivery import ( + "strconv" "strings" "testing" "time" @@ -260,9 +261,13 @@ func TestShangwutongLocationAttributesDefaultToChina(t *testing.T) { } func TestParseSWTTimeSupportsDotNetTicks(t *testing.T) { - got := parseSWTTime("639183484614616556", time.Time{}) - if got.Year() != 2026 || got.Month() != time.June || got.Location() != time.UTC { - t.Fatalf("parsed time = %s", got) + const dotNetUnixEpochTicks int64 = 621355968000000000 + localWallTime := time.Date(2026, time.August, 16, 12, 30, 0, 0, time.UTC) + ticks := dotNetUnixEpochTicks + localWallTime.Unix()*10_000_000 + got := parseSWTTime(strconv.FormatInt(ticks, 10), time.Time{}) + want := time.Date(2026, time.August, 16, 4, 30, 0, 0, time.UTC) + if !got.Equal(want) { + t.Fatalf("parsed time = %s, want %s", got, want) } } diff --git a/channels/shangwutong/internal/delivery/outbound.go b/channels/shangwutong/internal/delivery/outbound.go index a25ae3a0..b720dd98 100644 --- a/channels/shangwutong/internal/delivery/outbound.go +++ b/channels/shangwutong/internal/delivery/outbound.go @@ -201,13 +201,13 @@ func (o *Outbound) processOutbound(ctx context.Context) (bool, error) { if err := o.sendPart(ctx, message, part); err != nil { return true, o.handlePartError(message, part, err) } - if err := o.store.Writer().CompleteOutboundPart(ctx, dbgen.CompleteOutboundPartParams{ID: part.ID}); err != nil { + if err := o.store.Writer().AwaitOutboundPartEcho(ctx, part.ID); err != nil { return true, err } } - err = o.store.Writer().CompleteOutboundMessage(ctx, dbgen.CompleteOutboundMessageParams{ExternalID: nil, ID: message.ID}) + err = o.store.Writer().AwaitOutboundEcho(ctx, message.ID) if err == nil { - o.metrics.Delivery("outbound", "delivered") + o.metrics.Delivery("outbound", "accepted") } return true, err } diff --git a/channels/shangwutong/internal/delivery/outbound_test.go b/channels/shangwutong/internal/delivery/outbound_test.go index 930c8aa6..d5fd5e8f 100644 --- a/channels/shangwutong/internal/delivery/outbound_test.go +++ b/channels/shangwutong/internal/delivery/outbound_test.go @@ -15,7 +15,7 @@ import ( "github.com/gochat/gochat/channels/shangwutong/internal/swt" ) -func TestOutboundSuccessIsPersistedBeforeStatusSync(t *testing.T) { +func TestAcceptedOutboundTimesOutThroughExistingFailurePath(t *testing.T) { ctx := context.Background() database, account := deliveryDatabase(t, ctx) payload := deliveryPayload(t, nil) @@ -35,16 +35,52 @@ func TestOutboundSuccessIsPersistedBeforeStatusSync(t *testing.T) { t.Fatalf("delivery = %v, %v", worked, err) } queued, err := database.Writer().GetOutboundByGoChatMessageID(ctx, 77) - if err != nil || queued.DeliveryStatus != "delivered" || queued.StatusSyncStatus != "pending" { + if err != nil || queued.DeliveryStatus != "uncertain" || queued.StatusSyncStatus != "not_required" { t.Fatalf("queued = %#v, %v", queued, err) } worked, err = worker.processStatus(ctx) - if err != nil || !worked || results.result.Status != "sent" || results.inboxID != account.GochatInboxID { + if err != nil || worked || results.result.Status != "" { t.Fatalf("status sync = %v, %v, %#v", worked, err, results) } - queued, _ = database.Writer().GetOutboundByGoChatMessageID(ctx, 77) - if queued.StatusSyncStatus != "synced" { - t.Fatalf("status sync state = %s", queued.StatusSyncStatus) + if messages, _, err := worker.expireUncertain(ctx, time.Now().Add(uncertainObservationWindow+time.Second)); err != nil || messages != 1 { + t.Fatalf("expiry = %d, %v", messages, err) + } + if worked, err = worker.processStatus(ctx); err != nil || !worked || results.result.Status != "failed" || results.inboxID != account.GochatInboxID { + t.Fatalf("failed status sync = %v, %v, %#v", worked, err, results) + } +} + +func TestOutboundEchoBeforeSendReturnsConfirmsDelivery(t *testing.T) { + ctx := context.Background() + database, account := deliveryDatabase(t, ctx) + if _, _, err := database.EnqueueOutbound(ctx, store.OutboundInput{ + AccountID: account.ID, SWTSessionID: "visitor", EventID: "message:77:created", OccurredAt: time.Now(), GoChatMessageID: 77, + MessageType: "text", Content: stringPointer("hello"), Payload: deliveryPayload(t, nil), + }, false); err != nil { + t.Fatal(err) + } + client := &inboundRecorder{} + inbound, _ := NewInbound(database, client, nil, 1) + sender := senderStub{beforeReturn: func() { + persistInboundEvent(t, database, account, swt.HeartbeatEvent{ + SessionID: "visitor", Kind: 3, OpName: "agent", Text: "hello", SeqID: 51, + Timestamp: time.Now().Format(time.RFC3339Nano), RawLine: "visitor 3 agent|hello 51 timestamp", + }) + if worked, err := inbound.processInbound(ctx); err != nil || !worked { + t.Fatalf("process echo during send = %v, %v", worked, err) + } + }} + results := &resultRecorder{} + outbound, _ := NewOutbound(database, sessionStub{}, sender, results, nil, 1) + if worked, err := outbound.processOutbound(ctx); err != nil || !worked { + t.Fatalf("delivery = %v, %v", worked, err) + } + queued, err := database.Reader().GetOutboundByGoChatMessageID(ctx, 77) + if err != nil || queued.DeliveryStatus != "delivered" || value(queued.ExternalID) != "51" || len(client.imports) != 0 { + t.Fatalf("queued=%#v imports=%#v err=%v", queued, client.imports, err) + } + if worked, err := outbound.processStatus(ctx); err != nil || !worked || results.result.Status != "sent" { + t.Fatalf("status = %v, %v, %#v", worked, err, results.result) } } @@ -257,7 +293,7 @@ func TestAcceptedTransferUpdatesOwnershipAndKeepsOutboundSendable(t *testing.T) } } -func TestOutboundRetrySkipsAlreadyDeliveredParts(t *testing.T) { +func TestOutboundRetryResendsPartsWithoutEcho(t *testing.T) { ctx := context.Background() database, account := deliveryDatabase(t, ctx) content, dataURL, fileName, fileSize := "hello", "https://gochat.test/image.png", "image.png", int64(9) @@ -280,7 +316,7 @@ func TestOutboundRetrySkipsAlreadyDeliveredParts(t *testing.T) { } queued, _ := database.Reader().GetOutboundByGoChatMessageID(ctx, 77) parts, _ := database.Reader().ListOutboundParts(ctx, queued.ID) - if queued.DeliveryStatus != "failed" || parts[0].DeliveryStatus != "delivered" || parts[1].DeliveryStatus != "failed" { + if queued.DeliveryStatus != "failed" || parts[0].DeliveryStatus != "uncertain" || parts[1].DeliveryStatus != "failed" { t.Fatalf("queued=%#v parts=%#v", queued, parts) } @@ -294,7 +330,7 @@ func TestOutboundRetrySkipsAlreadyDeliveredParts(t *testing.T) { } queued, _ = database.Reader().GetOutboundByGoChatMessageID(ctx, 77) parts, _ = database.Reader().ListOutboundParts(ctx, queued.ID) - if sender.textCalls != 1 || sender.imageCalls != 2 || queued.DeliveryStatus != "delivered" || parts[0].DeliveryStatus != "delivered" || parts[1].DeliveryStatus != "delivered" { + if sender.textCalls != 2 || sender.imageCalls != 2 || queued.DeliveryStatus != "uncertain" || parts[0].DeliveryStatus != "uncertain" || parts[1].DeliveryStatus != "uncertain" { t.Fatalf("sender=%#v queued=%#v parts=%#v", sender, queued, parts) } } @@ -398,9 +434,15 @@ func (s *sessionRecorder) InvalidateSession(context.Context, int64) error { return nil } -type senderStub struct{ err error } +type senderStub struct { + err error + beforeReturn func() +} func (s senderStub) SendText(context.Context, swt.Session, string, string) (swt.SendResult, error) { + if s.beforeReturn != nil { + s.beforeReturn() + } return swt.SendResult{Status: "ok"}, s.err } diff --git a/channels/shangwutong/internal/observability/metrics.go b/channels/shangwutong/internal/observability/metrics.go index 20ba6503..b8949a98 100644 --- a/channels/shangwutong/internal/observability/metrics.go +++ b/channels/shangwutong/internal/observability/metrics.go @@ -52,7 +52,7 @@ func (m *Metrics) Presence(result string) { func (m *Metrics) Delivery(direction, result string) { m.inc("swt_connector_delivery_total", labels( "direction", bounded(direction, "inbound", "outbound"), - "result", bounded(result, "delivered", "retry", "uncertain", "failed", "ambiguous"), + "result", bounded(result, "accepted", "delivered", "retry", "uncertain", "failed", "ambiguous"), )) } diff --git a/channels/shangwutong/internal/observability/metrics_test.go b/channels/shangwutong/internal/observability/metrics_test.go index af569e7f..d7f579b0 100644 --- a/channels/shangwutong/internal/observability/metrics_test.go +++ b/channels/shangwutong/internal/observability/metrics_test.go @@ -13,6 +13,7 @@ func TestMetricsRenderProductionSeriesWithoutHighCardinalityLabels(t *testing.T) metrics.Login("success") metrics.Presence("retryable_error") metrics.Delivery("inbound", "delivered") + metrics.Delivery("outbound", "accepted") metrics.StatusSync("success", "sent") metrics.Mapping(2, "native_message", "delivered") metrics.Mapping(24, "contact_attributes", "delivered") @@ -49,6 +50,9 @@ func TestMetricsRenderProductionSeriesWithoutHighCardinalityLabels(t *testing.T) if !strings.Contains(payload, `swt_connector_event_mapping_total{kind="24",strategy="contact_attributes",result="delivered"} 1`) { t.Fatalf("redacted kind=24 mapping diagnostic missing from:\n%s", payload) } + if !strings.Contains(payload, `swt_connector_delivery_total{direction="outbound",result="accepted"} 1`) { + t.Fatalf("accepted outbound delivery metric missing from:\n%s", payload) + } } func TestMetricKindKeepsEveryDocumentedKindBounded(t *testing.T) { diff --git a/channels/shangwutong/internal/swt/client.go b/channels/shangwutong/internal/swt/client.go index 5b66e1d9..db59b374 100644 --- a/channels/shangwutong/internal/swt/client.go +++ b/channels/shangwutong/internal/swt/client.go @@ -179,7 +179,7 @@ type SendResult struct { } func (c *Client) SendText(ctx context.Context, session Session, sid, text string) (SendResult, error) { - return c.sendHTML(ctx, session, sid, wrapHTML(text), "send_text") + return c.sendHTML(ctx, session, sid, textHTML(text), "send_text") } func (c *Client) sendHTML(ctx context.Context, session Session, sid, content, operation string) (SendResult, error) { @@ -257,13 +257,15 @@ func sessionAuthForm(session Session) url.Values { } } -func wrapHTML(text string) string { +func textHTML(text string) string { text = html.EscapeString(text) text = strings.ReplaceAll(text, "\r\n", "\n") text = strings.ReplaceAll(text, "\r", "\n") - return "

" + strings.ReplaceAll(text, "\n", "
") + "

" + return strings.ReplaceAll(text, "\n", "
") } +func wrapHTML(text string) string { return "

" + textHTML(text) + "

" } + func (c *Client) postForm(ctx context.Context, baseURL, endpoint string, form url.Values) (string, *http.Response, error) { body, response, _, err := c.postFormTracked(ctx, baseURL, endpoint, form) return body, response, err diff --git a/channels/shangwutong/internal/swt/client_test.go b/channels/shangwutong/internal/swt/client_test.go index 1d78def7..cf1a8b44 100644 --- a/channels/shangwutong/internal/swt/client_test.go +++ b/channels/shangwutong/internal/swt/client_test.go @@ -56,7 +56,7 @@ func TestClientSendTextEscapesHTML(t *testing.T) { if err := request.ParseForm(); err != nil { t.Fatal(err) } - if got := request.Form.Get("html"); got != "

<b>x</b>
next

" { + if got := request.Form.Get("html"); got != "<b>x</b>
next" { t.Fatalf("html = %q", got) } response.Header().Set("r", "ok")