package updater import ( "archive/zip" "bytes" "context" "encoding/json" "net/http/httptest" "os" "path/filepath" "testing" "time" "scout/internal/httpapi" "scout/internal/registry" "scout/pkg/client" "scout/pkg/scoutpkg" ) func TestPrepareActivateAndConfirmHealthy(t *testing.T) { api := newTestClient(t) packageData := createScoutPackage(t, "1.1.0") if _, err := api.Upload(context.Background(), client.UploadRequest{ FileName: "demo.scout", File: bytes.NewReader(packageData), }); err != nil { t.Fatal(err) } root := t.TempDir() manager, err := New(Config{ Client: api, PackageName: "demo-core", CurrentVersion: "1.0.0", RootDir: root, Platform: "linux", Architecture: "amd64", KeepReleases: 2, }) if err != nil { t.Fatal(err) } prepared, err := manager.Prepare(context.Background(), "") if err != nil { t.Fatal(err) } if prepared.Version != "1.1.0" { t.Fatalf("\u51c6\u5907\u7248\u672c\u9519\u8bef: %s", prepared.Version) } if _, err := os.Stat(filepath.Join(root, "current.json")); !os.IsNotExist(err) { t.Fatal("Prepare \u4e0d\u5e94\u4fee\u6539\u5f53\u524d\u7248\u672c\u6307\u9488") } content, err := os.ReadFile(filepath.Join(prepared.Path, "demo")) if err != nil || string(content) != "new binary" { t.Fatalf("\u89e3\u538b\u7ed3\u679c\u9519\u8bef: %q, %v", content, err) } activated, err := manager.Activate() if err != nil { t.Fatal(err) } if activated.Version != prepared.Version { t.Fatalf("\u6fc0\u6d3b\u7248\u672c\u9519\u8bef: %s", activated.Version) } if err := manager.ConfirmHealthy(activated.Version); err != nil { t.Fatal(err) } status, err := manager.Status() if err != nil { t.Fatal(err) } if status.Current == nil || status.Current.Version != "1.1.0" || !status.State.Healthy { t.Fatalf("\u5065\u5eb7\u72b6\u6001\u9519\u8bef: %+v", status) } if status.Pending != nil { t.Fatal("\u5065\u5eb7\u786e\u8ba4\u540e\u5e94\u6e05\u9664\u5f85\u6fc0\u6d3b\u6307\u9488") } } func TestActivationPreservesOldReleaseAndStaleActivationRollsBack(t *testing.T) { api := newTestClient(t) if _, err := api.Upload(context.Background(), client.UploadRequest{ FileName: "demo.scout", File: bytes.NewReader(createScoutPackage(t, "2.0.0")), }); err != nil { t.Fatal(err) } root := t.TempDir() manager, err := New(Config{ Client: api, PackageName: "demo-core", RootDir: root, Platform: "linux", Architecture: "amd64", HealthTimeout: 10 * time.Millisecond, KeepReleases: 2, }) if err != nil { t.Fatal(err) } oldDir := filepath.Join(root, "releases", "1.0.0") if err := os.MkdirAll(oldDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(oldDir, "demo"), []byte("old binary"), 0o755); err != nil { t.Fatal(err) } oldPointer, err := manager.RegisterCurrent("1.0.0", oldDir, "demo") if err != nil { t.Fatal(err) } if _, err := manager.Prepare(context.Background(), "2.0.0"); err != nil { t.Fatal(err) } if _, err := manager.Activate(); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(oldPointer.Path, oldPointer.Entrypoint)); err != nil { t.Fatalf("\u6fc0\u6d3b\u540e\u65e7\u7248\u672c\u4e0d\u5e94\u88ab\u5220\u9664: %v", err) } recovered, err := manager.RecoverStaleActivation(time.Now().Add(time.Second)) if err != nil || !recovered { t.Fatalf("\u8d85\u65f6\u56de\u6eda\u5931\u8d25: %t, %v", recovered, err) } status, err := manager.Status() if err != nil { t.Fatal(err) } if status.Current == nil || status.Current.Version != "1.0.0" || status.State.Stage != "rolled_back" { t.Fatalf("\u56de\u6eda\u72b6\u6001\u9519\u8bef: %+v", status) } } func TestHealthyCleanupUsesConfiguredRetention(t *testing.T) { api := newTestClient(t) if _, err := api.Upload(context.Background(), client.UploadRequest{ FileName: "demo.scout", File: bytes.NewReader(createScoutPackage(t, "1.1.0")), }); err != nil { t.Fatal(err) } root := t.TempDir() manager, err := New(Config{ Client: api, PackageName: "demo-core", RootDir: root, Platform: "linux", Architecture: "amd64", KeepReleases: 1, }) if err != nil { t.Fatal(err) } oldDir := filepath.Join(root, "releases", "1.0.0") if err := os.MkdirAll(oldDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(oldDir, "demo"), []byte("old binary"), 0o755); err != nil { t.Fatal(err) } if _, err := manager.RegisterCurrent("1.0.0", oldDir, "demo"); err != nil { t.Fatal(err) } prepared, err := manager.Prepare(context.Background(), "") if err != nil { t.Fatal(err) } if _, err := manager.Activate(); err != nil { t.Fatal(err) } if _, err := os.Stat(oldDir); err != nil { t.Fatalf("\u5065\u5eb7\u786e\u8ba4\u524d\u5fc5\u987b\u4fdd\u7559\u65e7\u7248\u672c: %v", err) } if err := manager.ConfirmHealthy(prepared.Version); err != nil { t.Fatal(err) } if _, err := os.Stat(oldDir); !os.IsNotExist(err) { t.Fatalf("\u5065\u5eb7\u786e\u8ba4\u540e\u672a\u6309\u4fdd\u7559\u7b56\u7565\u6e05\u7406\u65e7\u7248\u672c: %v", err) } } func TestPrepareCleansFailedStagingAndCanRetry(t *testing.T) { api := newTestClient(t) invalidManifest := scoutpkg.Manifest{ SchemaVersion: 1, Name: "demo-core", Version: "1.1.0", Channel: "stable", PackageType: "full", Targets: []scoutpkg.Target{{ Platform: "linux", Architecture: "amd64", Path: "targets/linux/amd64", Entrypoint: "targets/linux/amd64/missing", }}, } if _, err := api.Upload(context.Background(), client.UploadRequest{ FileName: "invalid.scout", File: bytes.NewReader(createScoutArchive(t, invalidManifest, map[string][]byte{ "targets/linux/amd64/payload": []byte("payload"), })), }); err != nil { t.Fatal(err) } root := t.TempDir() manager, err := New(Config{ Client: api, PackageName: "demo-core", RootDir: root, Platform: "linux", Architecture: "amd64", HealthTimeout: 10 * time.Millisecond, }) if err != nil { t.Fatal(err) } if _, err := manager.Prepare(context.Background(), ""); err == nil { t.Fatal("入口不存在的更新包应该准备失败") } staging, err := filepath.Glob(filepath.Join(root, "releases", ".staging-*")) if err != nil { t.Fatal(err) } if len(staging) != 0 { t.Fatalf("失败后不应保留暂存目录: %v", staging) } if _, err := api.Upload(context.Background(), client.UploadRequest{ FileName: "valid.scout", File: bytes.NewReader(createScoutPackage(t, "1.2.0")), }); err != nil { t.Fatal(err) } prepared, err := manager.Prepare(context.Background(), "") if err != nil { t.Fatalf("清理失败暂存目录后应允许重试: %v", err) } if prepared.Version != "1.2.0" { t.Fatalf("重试准备的版本错误: %s", prepared.Version) } } func newTestClient(t *testing.T) *client.Client { t.Helper() store, err := registry.New(t.TempDir()) if err != nil { t.Fatal(err) } server := httptest.NewServer((&httpapi.Server{Registry: store, DeveloperToken: "test-token"}).Handler()) t.Cleanup(server.Close) api, err := client.New(server.URL, client.WithDeveloperToken("test-token")) if err != nil { t.Fatal(err) } return api } func createScoutPackage(t *testing.T, version string) []byte { t.Helper() manifest := scoutpkg.Manifest{ SchemaVersion: 1, Name: "demo-core", Version: version, Channel: "stable", PackageType: "full", Targets: []scoutpkg.Target{{ Platform: "linux", Architecture: "amd64", Path: "targets/linux/amd64", Entrypoint: "targets/linux/amd64/demo", }}, } return createScoutArchive(t, manifest, map[string][]byte{ "targets/linux/amd64/demo": []byte("new binary"), }) } func createScoutArchive(t *testing.T, manifest scoutpkg.Manifest, files map[string][]byte) []byte { t.Helper() buffer := bytes.NewBuffer(nil) writer := zip.NewWriter(buffer) manifestData, err := json.Marshal(manifest) if err != nil { t.Fatal(err) } entry, err := writer.Create(scoutpkg.ManifestName) if err != nil { t.Fatal(err) } if _, err := entry.Write(manifestData); err != nil { t.Fatal(err) } for name, content := range files { entry, err := writer.Create(name) if err != nil { t.Fatal(err) } if _, err := entry.Write(content); err != nil { t.Fatal(err) } } if err := writer.Close(); err != nil { t.Fatal(err) } return buffer.Bytes() }