package controlplane import ( "context" "testing" "time" ) func TestContactSnapshotsReplaceOnlyAfterCompleteSync(t *testing.T) { ctx := context.Background() manager, err := OpenAccountStoreManager(t.TempDir()) if err != nil { t.Fatal(err) } defer manager.Close() store, err := manager.RegisterAccount(ctx, AccountRegistration{ AccountID: "account-contacts", StableIdentity: "wechat-contacts", SourceNodeID: "node-contacts", SourceGeneration: "generation-contacts", Verified: true, AuthorizationVersion: 1, ReportingScopes: []ReportingScope{{ChatID: "*", DataType: "*", ConfigVersion: 1}}, }) if err != nil { t.Fatal(err) } defer func() { _ = store.Close() }() // Simulate an existing v1 shard so opening it exercises the contacts-table migration. if _, err := store.db.ExecContext(ctx, `DROP TABLE contacts`); err != nil { t.Fatal(err) } if _, err := store.db.ExecContext(ctx, `UPDATE schema_meta SET value = '1' WHERE key = 'schema_version'`); err != nil { t.Fatal(err) } if err := store.Close(); err != nil { t.Fatal(err) } store, err = manager.OpenAccount(ctx, "account-contacts") if err != nil { t.Fatal(err) } now := time.Now().UTC() private := ContactRecord{ChatID: "wxid-contact", ChatType: string(ChatPrivate), DisplayName: "Contact", Remark: "Local", ObservedAt: now} group := ContactRecord{ChatID: "group@chatroom", ChatType: string(ChatGroup), DisplayName: "Group", ObservedAt: now} apply := func(sequence int64, snapshotID, coverage string, contacts ...ContactRecord) error { _, err := store.ApplyBatch(ctx, IngestBatch{ BatchID: "contacts-" + snapshotID, SourceGeneration: "generation-contacts", StreamKey: "contacts", Sequence: sequence, CursorStart: "0", CursorEnd: "0", PayloadHash: "hash-" + snapshotID, CoverageState: coverage, ContactsSnapshotID: snapshotID, Contacts: contacts, }) return err } if err := apply(1, "snapshot-1", "complete", private, group); err != nil { t.Fatal(err) } if items, _, err := store.QueryContacts(ctx, "", true, 10, 0); err != nil || len(items) != 1 || items[0].ChatID != group.ChatID { t.Fatalf("unexpected cached groups: %v %+v", err, items) } if err := apply(2, "snapshot-2", "partial", private); err != nil { t.Fatal(err) } if items, _, err := store.QueryContacts(ctx, "", true, 10, 0); err != nil || len(items) != 1 { t.Fatalf("partial snapshot removed prior contacts: %v %+v", err, items) } if err := apply(3, "snapshot-3", "complete", private); err != nil { t.Fatal(err) } if items, _, err := store.QueryContacts(ctx, "", true, 10, 0); err != nil || len(items) != 0 { t.Fatalf("complete snapshot did not remove stale contacts: %v %+v", err, items) } }