H-28: harden Shangwutong CID sync (#5)

* fix(shangwutong): close contact sync review gaps

* fix(shangwutong): harden CID sync boundaries

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-13 01:26:56 +08:00
committed by GitHub
co-authored by rogee
parent 0e9bf8dc14
commit d0995798f4
7 changed files with 133 additions and 41 deletions
@@ -47,6 +47,29 @@ func TestInboundVisitorMessageCreatesResourcesAndPersistsMappings(t *testing.T)
}
}
func TestInboundKind52PersistsCIDWithoutCreatingResources(t *testing.T) {
ctx := context.Background()
database, account := deliveryDatabase(t, ctx)
persistInboundEvent(t, database, account, swt.HeartbeatEvent{
SessionID: "visitor", Kind: 52, Text: "a|cid%2F123|x|y|z|name", SeqID: 42,
Timestamp: time.Now().Format(time.RFC3339Nano), RawLine: "visitor 52 history 42 timestamp",
})
client := &inboundRecorder{}
worker, err := NewInbound(database, client, nil, 1)
if err != nil {
t.Fatal(err)
}
if worked, err := worker.processInbound(ctx); err != nil || !worked {
t.Fatalf("process kind=52 = %v, %v", worked, err)
}
if client.metadataUpdates != 1 || client.lastCID != "cid/123" || client.lastSourceID != "visitor" || client.lastInboxID != account.GochatInboxID {
t.Fatalf("metadata updates = %#v", client)
}
if len(client.imports) != 0 {
t.Fatalf("kind=52 must not import a message: %#v", client.imports)
}
}
func TestInboundKind3ConfirmsUniqueOutboundEcho(t *testing.T) {
ctx := context.Background()
database, account := deliveryDatabase(t, ctx)
@@ -407,6 +430,10 @@ type inboundRecorder struct {
importErr error
retractions []int64
attachmentCount int
metadataUpdates int
lastInboxID int64
lastSourceID string
lastCID string
}
func (r *inboundRecorder) EnsureContact(_ context.Context, _, _ string, request gochat.ContactRequest) (gochat.Contact, error) {
@@ -417,7 +444,9 @@ func (r *inboundRecorder) UpdateContact(_ context.Context, _, _, sourceID string
return gochat.Contact{ID: 456, SourceID: sourceID, Name: request.Name}, nil
}
func (*inboundRecorder) UpdateContactChannelMetadata(context.Context, int64, string, string) error {
func (r *inboundRecorder) UpdateContactChannelMetadata(_ context.Context, inboxID int64, sourceID, cid string) error {
r.metadataUpdates++
r.lastInboxID, r.lastSourceID, r.lastCID = inboxID, sourceID, cid
return nil
}
@@ -195,8 +195,8 @@ func mapInboundEvent(kind int64, seqID int64, text, operator, rawTimestamp, sour
func parseHistoryCID(text string) string {
for _, record := range strings.Split(text, "#") {
fields := strings.Split(record, "|")
if len(fields) > 5 {
if cid, err := url.QueryUnescape(fields[1]); err == nil && strings.TrimSpace(cid) != "" {
if len(fields) > 5 && strings.TrimSpace(fields[1]) != "" {
if cid, err := url.QueryUnescape(strings.TrimSpace(fields[1])); err == nil && strings.TrimSpace(cid) != "" {
return strings.TrimSpace(cid)
}
}
@@ -37,7 +37,7 @@ func (c *Client) UpdateContactChannelMetadata(ctx context.Context, inboxID int64
if inboxID <= 0 || strings.TrimSpace(sourceID) == "" || strings.TrimSpace(cid) == "" {
return errors.New("contact channel metadata identifiers are required")
}
path := fmt.Sprintf("/api/v1/connector/shangwutong/inboxes/%d/contacts/%s", inboxID, url.PathEscape(sourceID))
path := fmt.Sprintf("/api/v1/connector/shangwutong/inboxes/%d/contacts/%s", inboxID, url.PathEscape(strings.TrimSpace(sourceID)))
body := map[string]any{"cid": strings.TrimSpace(cid)}
return c.doJSON(ctx, http.MethodPatch, path, body, nil, fmt.Sprintf("swt-contact-metadata:%d:%s:%s", inboxID, sourceID, cid))
}
@@ -132,6 +132,36 @@ func TestRollbackMigrationRefusesRenameOperations(t *testing.T) {
}
}
func TestRollbackMigrationKeepsOldOperationsWhenRenameIsAbsent(t *testing.T) {
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "rollback.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
if _, err := db.Exec(`CREATE TABLE outbound_operations (
id INTEGER PRIMARY KEY AUTOINCREMENT, account_id INTEGER NOT NULL, swt_sid TEXT NOT NULL,
event_id TEXT NOT NULL UNIQUE, operation TEXT NOT NULL, payload TEXT NOT NULL,
occurred_at DATETIME NOT NULL, delivery_status TEXT NOT NULL, claimed_at DATETIME,
attempts INTEGER NOT NULL, next_attempt_at DATETIME, last_error TEXT,
created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL)`); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`INSERT INTO outbound_operations(account_id,swt_sid,event_id,operation,payload,occurred_at,delivery_status,attempts,created_at,updated_at) VALUES (1,'sid','event','end_conversation','{}',CURRENT_TIMESTAMP,'pending',0,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`); err != nil {
t.Fatal(err)
}
rollback, err := os.ReadFile(filepath.Join("..", "..", "db", "migrations", "002_add_rename_operation.down.sql"))
if err != nil {
t.Fatal(err)
}
if _, err := db.Exec(string(rollback)); err != nil {
t.Fatal(err)
}
var operation string
if err := db.QueryRow("SELECT operation FROM outbound_operations").Scan(&operation); err != nil || operation != "end_conversation" {
t.Fatalf("operation = %q, err=%v", operation, err)
}
}
func TestOutboundClaimStopsBehindUncertainMessage(t *testing.T) {
ctx := context.Background()
store, err := Open(ctx, filepath.Join(t.TempDir(), "connector.db"))