mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
The server module named github.com/hanzoai/team/server/v8 — a path that resolves to the retired workspace's repository, not this one — while server/public still published github.com/mattermost/mattermost/server/public. Both are github.com/hanzoteam/server/... now, which is where this code actually lives. That second half is what made the binary unbootable. Upstream's public path stayed claimed by upstream, mattermost-plugin-ai imported it, and the build linked public/model twice; public/plugin's gob.Register then ran twice under one name and panicked at init, before main. Reverting the rename traded the brand for a working binary. Rebranding the plugin alongside it needs no such trade: the agents fork is github.com/hanzoteam/agents/v2 against our public module, so no module named mattermost/mattermost is in the graph at all. Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and it is ours.
226 lines
7.3 KiB
Go
226 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
"github.com/hanzoteam/server/server/public/shared/mlog"
|
|
)
|
|
|
|
func (tr *TestRunner) runPostTests(ctx context.Context) error {
|
|
tr.logger.Info("=== Post Sync Tests ===")
|
|
|
|
channelA, channelB, err := tr.setupSharedChannel(ctx, "post-test")
|
|
if err != nil {
|
|
tr.fail("post/setup", err.Error())
|
|
return err
|
|
}
|
|
|
|
// Create a user and add to channel
|
|
userID, err := tr.createTestUser(ctx, "postuser1")
|
|
if err != nil {
|
|
tr.fail("post/create-user", err.Error())
|
|
return err
|
|
}
|
|
_, _, err = tr.clientA.AddChannelMember(ctx, channelA, userID)
|
|
if err != nil {
|
|
tr.fail("post/add-user-to-channel", err.Error())
|
|
return err
|
|
}
|
|
|
|
// Wait for user to sync before posting
|
|
if err := tr.waitFor(ctx, 15*time.Second, func() bool {
|
|
return tr.verifyMemberOnB(ctx, channelB, "postuser1")
|
|
}); err != nil {
|
|
tr.fail("post/user-sync", "postuser1 did not sync to Server B before posting")
|
|
return err
|
|
}
|
|
|
|
// ── Test: Create post and verify sync ───────────────────
|
|
tr.logger.Info("Creating post on Server A...")
|
|
postMessage := "Hello from Server A! " + model.NewId()[:8]
|
|
post, _, err := tr.clientA.CreatePost(ctx, &model.Post{
|
|
ChannelId: channelA,
|
|
Message: postMessage,
|
|
})
|
|
if err != nil {
|
|
tr.fail("post/create", err.Error())
|
|
return err
|
|
}
|
|
|
|
testName := "post/sync-create"
|
|
err = tr.waitFor(ctx, 30*time.Second, func() bool {
|
|
return tr.findPostOnB(ctx, channelB, postMessage) != ""
|
|
})
|
|
if err != nil {
|
|
tr.fail(testName, "post did not sync to Server B")
|
|
} else {
|
|
tr.pass(testName)
|
|
}
|
|
|
|
// ── Test: Edit post and verify sync ─────────────────────
|
|
tr.logger.Info("Editing post on Server A...")
|
|
editedMessage := "Edited: " + postMessage
|
|
post.Message = editedMessage
|
|
_, _, err = tr.clientA.UpdatePost(ctx, post.Id, post)
|
|
if err != nil {
|
|
tr.fail("post/edit", err.Error())
|
|
} else {
|
|
testName := "post/sync-edit"
|
|
err := tr.waitFor(ctx, 30*time.Second, func() bool {
|
|
return tr.findPostOnB(ctx, channelB, editedMessage) != ""
|
|
})
|
|
if err != nil {
|
|
tr.fail(testName, "post edit did not sync to Server B")
|
|
} else {
|
|
tr.pass(testName)
|
|
}
|
|
}
|
|
|
|
// ── Test: Delete post and verify sync ───────────────────
|
|
tr.logger.Info("Deleting post on Server A...")
|
|
_, err = tr.clientA.DeletePost(ctx, post.Id)
|
|
if err != nil {
|
|
tr.fail("post/delete", err.Error())
|
|
} else {
|
|
testName := "post/sync-delete"
|
|
err := tr.waitFor(ctx, 30*time.Second, func() bool {
|
|
return tr.findPostOnB(ctx, channelB, editedMessage) == ""
|
|
})
|
|
if err != nil {
|
|
tr.fail(testName, "post delete did not sync to Server B")
|
|
} else {
|
|
tr.pass(testName)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// runOfflineRecoveryTests verifies that messages posted while a remote is briefly
|
|
// offline (for less than the 5-minute IsOnline() threshold) are eventually synced
|
|
// once the remote comes back online — even though no new organic message triggers a
|
|
// sync and no connection-state flip occurs via the IsOnline() path.
|
|
//
|
|
// This exercises the recovery path added for MM-69792: a failed sync sets a flag in
|
|
// the remote cluster service, and the next successful ping fires a targeted resync.
|
|
//
|
|
// The test requires managing the Server B lifecycle, so it is skipped in unmanaged mode.
|
|
func (tr *TestRunner) runOfflineRecoveryTests(ctx context.Context) error {
|
|
tr.logger.Info("=== Offline Recovery Tests ===")
|
|
|
|
if !tr.cfg.Manage || tr.mgr == nil {
|
|
tr.logger.Info("Skipping offline-recovery test (requires managed mode to stop/restart Server B)")
|
|
return nil
|
|
}
|
|
|
|
channelA, channelB, err := tr.setupSharedChannel(ctx, "offline-recovery-test")
|
|
if err != nil {
|
|
tr.fail("post/offline-recovery-setup", err.Error())
|
|
return err
|
|
}
|
|
|
|
// Create a user and add to the channel so posts have an author that syncs.
|
|
userID, err := tr.createTestUser(ctx, "offlineuser1")
|
|
if err != nil {
|
|
tr.fail("post/offline-recovery-create-user", err.Error())
|
|
return err
|
|
}
|
|
if _, _, err = tr.clientA.AddChannelMember(ctx, channelA, userID); err != nil {
|
|
tr.fail("post/offline-recovery-add-user", err.Error())
|
|
return err
|
|
}
|
|
if err := tr.waitFor(ctx, 15*time.Second, func() bool {
|
|
return tr.verifyMemberOnB(ctx, channelB, "offlineuser1")
|
|
}); err != nil {
|
|
tr.fail("post/offline-recovery-user-sync", "offlineuser1 did not sync to Server B before posting")
|
|
return err
|
|
}
|
|
|
|
testName := "post/offline-recovery"
|
|
|
|
// Step 1: baseline sync works.
|
|
m1 := "M1 baseline " + model.NewId()[:8]
|
|
tr.logger.Info("Posting M1 (baseline) on Server A...")
|
|
if _, _, err := tr.clientA.CreatePost(ctx, &model.Post{ChannelId: channelA, Message: m1}); err != nil {
|
|
tr.fail(testName, "failed to create baseline post M1: "+err.Error())
|
|
return nil
|
|
}
|
|
if err := tr.waitFor(ctx, 30*time.Second, func() bool {
|
|
return tr.findPostOnB(ctx, channelB, m1) != ""
|
|
}); err != nil {
|
|
tr.fail(testName, "baseline post M1 did not sync to Server B")
|
|
return nil
|
|
}
|
|
tr.logger.Info("Baseline post M1 synced to Server B")
|
|
|
|
// Step 2: stop Server B.
|
|
tr.mgr.StopServerB()
|
|
|
|
// Step 3: post M2 and M3 while Server B is offline.
|
|
m2 := "M2 during-outage " + model.NewId()[:8]
|
|
m3 := "M3 during-outage " + model.NewId()[:8]
|
|
tr.logger.Info("Posting M2 and M3 on Server A while Server B is offline...")
|
|
for _, msg := range []string{m2, m3} {
|
|
if _, _, err := tr.clientA.CreatePost(ctx, &model.Post{ChannelId: channelA, Message: msg}); err != nil {
|
|
tr.fail(testName, "failed to create post during outage: "+err.Error())
|
|
// Best effort: bring Server B back before returning so later tests can run.
|
|
if startErr := tr.mgr.StartServerB(ctx); startErr != nil {
|
|
tr.logger.Error("Failed to restart Server B", mlog.Err(startErr))
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Step 4: wait long enough for send retries to exhaust, but short enough that
|
|
// IsOnline() has not flipped (LastPingAt stays within the 5-minute window).
|
|
tr.logger.Info("Waiting 90s for send retries to exhaust while Server B stays offline...")
|
|
if err := interruptibleSleep(ctx, 90*time.Second); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Step 5: restart Server B.
|
|
if err := tr.mgr.StartServerB(ctx); err != nil {
|
|
tr.fail(testName, "failed to restart Server B: "+err.Error())
|
|
return err
|
|
}
|
|
|
|
// Step 6: M2 and M3 must eventually appear on Server B via the recovery path.
|
|
tr.logger.Info("Waiting up to 120s for M2 and M3 to sync to Server B after recovery...")
|
|
if err := tr.waitFor(ctx, 120*time.Second, func() bool {
|
|
return tr.findPostOnB(ctx, channelB, m2) != "" && tr.findPostOnB(ctx, channelB, m3) != ""
|
|
}); err != nil {
|
|
tr.fail(testName, "M2 and/or M3 did not sync to Server B after recovery")
|
|
return nil
|
|
}
|
|
|
|
tr.pass(testName)
|
|
return nil
|
|
}
|
|
|
|
// interruptibleSleep sleeps for d, returning early with an error if ctx is cancelled.
|
|
func interruptibleSleep(ctx context.Context, d time.Duration) error {
|
|
select {
|
|
case <-time.After(d):
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
// findPostOnB searches for a post with the given message in a channel on Server B.
|
|
// Returns the post ID if found, empty string otherwise.
|
|
func (tr *TestRunner) findPostOnB(ctx context.Context, channelB, message string) string {
|
|
postList, _, err := tr.clientB.GetPostsForChannel(ctx, channelB, 0, 100, "", false, false)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
for _, post := range postList.Posts {
|
|
if post.Message == message {
|
|
return post.Id
|
|
}
|
|
}
|
|
return ""
|
|
}
|