package homeplugins import ( "archive/zip" "bytes" "context" "crypto/sha256" "encoding/hex" "errors" "io" "net/http" "net/http/httptest" "os" "path/filepath" "runtime" "strings" "testing" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" sdkpluginstore "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginstore" "gopkg.in/yaml.v3") type fakePluginRuntime struct { busy bool unloaded []string} type fakePluginLoadInspector map[string]bool func (r *fakePluginRuntime) PluginBusy(id string) bool { return r.busy} func (r *fakePluginRuntime) UnloadPlugin(id string) bool { r.unloaded = append(r.unloaded, id) r.busy = false return true} func (i fakePluginLoadInspector) PluginRegistered(id string) bool { return i[id]} type contextPluginRuntime struct { fakePluginRuntime unloadContext context.Context} func (r *contextPluginRuntime) UnloadPluginContext(ctx context.Context, id string) bool { r.unloadContext = ctx return r.UnloadPlugin(id)} func TestSyncPlatformInstallsManifestArtifact(t *testing.T) { root := t.TempDir() archiveData := makeZip(t, map[string]string{"sample.dll": "library-data"}) archiveName := "sample_0.2.0_windows_amd64.zip" checksum := sha256.Sum256(archiveData) httpClient := mapHTTPDoer{ "https://api.github.com/repos/owner/sample-plugin/releases/tags/v0.2.0": []byte(`{ "tag_name": "v0.2.0", "assets": [ {"name": "` + archiveName + `", "browser_download_url": "https://downloads.example/` + archiveName + `"}, {"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"} ] }`), "https://downloads.example/" + archiveName: archiveData, "https://downloads.example/checksums.txt": []byte(hex.EncodeToString(checksum[:]) + " " + archiveName + "\n"), } restore := replacePluginStoreClientForTest(httpClient) defer restore() if errSync := SyncPlatform(context.Background(), syncTestConfig(t, root), nil, Platform{GOOS: "windows", GOARCH: "amd64"}); errSync != nil { t.Fatalf("SyncPlatform() error = %v", errSync) } target := pluginTestPath(root, "windows", "amd64", "sample", "0.2.0") got, errRead := os.ReadFile(target) if errRead != nil { t.Fatalf("read target: %v", errRead) } if string(got) != "library-data" { t.Fatalf("target data = %q, want library-data", string(got)) }} func TestSyncResolvedWithReportUsesTemporaryAuthAndClearsIt(t *testing.T) { root := t.TempDir() libraryName := "sample" + pluginExtension(runtime.GOOS) archiveData := makeZip(t, map[string]string{libraryName: "library-data"}) checksum := sha256.Sum256(archiveData) var authenticated bool server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer temporary-token" { http.Error(w, "unauthorized", http.StatusUnauthorized) return } authenticated = true _, _ = w.Write(archiveData) })) t.Cleanup(server.Close) response, errUnauthenticated := server.Client().Get(server.URL + "/private/sample.zip") if errUnauthenticated != nil { t.Fatalf("unauthenticated GET error = %v", errUnauthenticated) } _ = response.Body.Close() if response.StatusCode != http.StatusUnauthorized { t.Fatalf("unauthenticated status = %d, want 401", response.StatusCode) } originalClient := newResolvedPluginStoreClient newResolvedPluginStoreClient = func(_ *config.Config, auth []sdkpluginstore.ResolvedAuthConfig, expiresAt time.Time) sdkpluginstore.Client { return sdkpluginstore.NewClientWithResolvedAuthExpiry(server.Client(), "", auth, expiresAt) } defer func() { newResolvedPluginStoreClient = originalClient }() token := sdkpluginstore.Secret("temporary-token") backing := token items := []sdkpluginstore.PluginSyncItem{{ Manifest: sdkpluginstore.Manifest{ SchemaVersion: sdkpluginstore.SchemaVersionV2, ID: "sample", Version: "1.0.0", Install: sdkpluginstore.InstallPlan{Type: sdkpluginstore.InstallTypeDirect, Artifacts: []sdkpluginstore.Artifact{{ GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, URL: server.URL + "/private/sample.zip", SHA256: hex.EncodeToString(checksum[:]), Size: int64(len(archiveData)), }}}, }, Auth: []sdkpluginstore.ResolvedAuthConfig{{ Match: server.URL + "/private/", ApplyTo: []string{sdkpluginstore.RequestKindArtifact}, Type: sdkpluginstore.AuthTypeBearer, Token: token, }}, }} enabled := true cfg := &config.Config{ Home: config.HomeConfig{Enabled: true}, Plugins: config.PluginsConfig{Enabled: true, Dir: root, Configs: map[string]config.PluginInstanceConfig{"sample": {Enabled: &enabled}}}, } report, errSync := SyncResolvedWithReport(context.Background(), cfg, items, time.Now().UTC().Add(time.Minute), map[string]string{"sample": "0.9.0"}, nil) if errSync != nil { t.Fatalf("SyncResolvedWithReport() error = %v", errSync) } if !authenticated || !report.OK || len(report.Plugins) != 1 || report.Plugins[0].Version != "1.0.0" { t.Fatalf("authenticated=%v report=%+v, want successful authenticated install", authenticated, report) } for index, value := range backing { if value != 0 { t.Fatalf("token byte %d = %d, want zero after sync", index, value) } } if items[0].Auth != nil { t.Fatalf("sync item retained auth references: %#v", items[0].Auth) } target := pluginTestPath(root, runtime.GOOS, runtime.GOARCH, "sample", "1.0.0") if got, errRead := os.ReadFile(target); errRead != nil || string(got) != "library-data" { t.Fatalf("installed plugin = %q, error = %v", got, errRead) }} func TestSyncResolvedWithReportIncludesUnchangedInstalledPlugins(t *testing.T) { root := t.TempDir() target := pluginTestPath(root, runtime.GOOS, runtime.GOARCH, "sample", "1.0.0") if errMkdir := os.MkdirAll(filepath.Dir(target), 0o755); errMkdir != nil { t.Fatalf("MkdirAll() error = %v", errMkdir) } if errWrite := os.WriteFile(target, []byte("plugin"), 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } cfg := &config.Config{ Home: config.HomeConfig{Enabled: true}, Plugins: config.PluginsConfig{ Enabled: true, Dir: root, Configs: map[string]config.PluginInstanceConfig{ "sample": pluginConfigFromYAML(t, `enabled: truestore: id: sample name: Sample description: Adds sample support. author: owner version: 1.0.0 release-tag: v1.0.0 repository: https://github.com/owner/sample-plugin`), }, }, } report, errSync := SyncResolvedWithReport( context.Background(), cfg, nil, time.Now().UTC().Add(time.Minute), map[string]string{"sample": "1.0.0"}, nil, ) if errSync != nil { t.Fatalf("SyncResolvedWithReport() error = %v", errSync) } if len(report.Plugins) != 1 || report.Plugins[0].ID != "sample" || report.Plugins[0].InstallStatus != pluginInstallStatusSkipped { t.Fatalf("report plugins = %+v, want unchanged installed sample", report.Plugins) } status := report.Plugins[0] if status.Path != target || status.ReleaseTag != "v1.0.0" || status.Repository != "https://github.com/owner/sample-plugin" || status.InstallType != sdkpluginstore.InstallTypeGitHubRelease { t.Fatalf("unchanged plugin status = %+v, want preserved path and manifest metadata", status) } if errLoad := MarkLoadResults(&report, fakePluginLoadInspector{}); errLoad == nil { t.Fatal("MarkLoadResults() error = nil, want installed plugin load failure") } if report.Plugins[0].LoadStatus != pluginLoadStatusFailed { t.Fatalf("load status = %q, want failed", report.Plugins[0].LoadStatus) }} func TestSyncResolvedWithReportDoesNotMixInstalledAndConfiguredMetadata(t *testing.T) { root := t.TempDir() target := pluginTestPath(root, runtime.GOOS, runtime.GOARCH, "sample", "1.0.0") if errMkdir := os.MkdirAll(filepath.Dir(target), 0o755); errMkdir != nil { t.Fatalf("MkdirAll() error = %v", errMkdir) } if errWrite := os.WriteFile(target, []byte("plugin"), 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } cfg := &config.Config{ Home: config.HomeConfig{Enabled: true}, Plugins: config.PluginsConfig{ Enabled: true, Dir: root, Configs: map[string]config.PluginInstanceConfig{ "sample": pluginConfigFromYAML(t, `enabled: truestore: id: sample name: Sample description: Adds sample support. author: owner version: 2.0.0 release-tag: v2.0.0 repository: https://github.com/owner/sample-plugin-v2`), }, }, } report, errSync := SyncResolvedWithReport( context.Background(), cfg, nil, time.Now().UTC().Add(time.Minute), map[string]string{"sample": "1.0.0"}, nil, ) if errSync != nil { t.Fatalf("SyncResolvedWithReport() error = %v", errSync) } if len(report.Plugins) != 1 { t.Fatalf("report plugins = %+v, want one installed sample", report.Plugins) } status := report.Plugins[0] if status.Version != "1.0.0" || status.Path != target { t.Fatalf("installed plugin status = %+v, want version 1.0.0 at %s", status, target) } if status.ReleaseTag != "" || status.Repository != "" || status.InstallType != "" { t.Fatalf("installed plugin status = %+v, want no metadata from configured version 2.0.0", status) }} func TestInstalledVersionsUsesPluginFilesOnDisk(t *testing.T) { root := t.TempDir() target := pluginTestPath(root, runtime.GOOS, runtime.GOARCH, "sample", "2.3.4") if errMkdir := os.MkdirAll(filepath.Dir(target), 0o755); errMkdir != nil { t.Fatalf("MkdirAll() error = %v", errMkdir) } if errWrite := os.WriteFile(target, []byte("plugin"), 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } cfg := &config.Config{Plugins: config.PluginsConfig{Dir: root, Configs: map[string]config.PluginInstanceConfig{"sample": {}}}} versions, errVersions := InstalledVersions(cfg) if errVersions != nil { t.Fatalf("InstalledVersions() error = %v", errVersions) } if versions["sample"] != "2.3.4" { t.Fatalf("InstalledVersions() = %#v, want sample 2.3.4", versions) }} func TestSyncPlatformWithReportRecordsSuccessfulInstall(t *testing.T) { root := t.TempDir() archiveData := makeZip(t, map[string]string{"sample.dll": "library-data"}) archiveName := "sample_0.2.0_windows_amd64.zip" checksum := sha256.Sum256(archiveData) httpClient := mapHTTPDoer{ "https://api.github.com/repos/owner/sample-plugin/releases/tags/v0.2.0": []byte(`{ "tag_name": "v0.2.0", "assets": [ {"name": "` + archiveName + `", "browser_download_url": "https://downloads.example/` + archiveName + `"}, {"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"} ] }`), "https://downloads.example/" + archiveName: archiveData, "https://downloads.example/checksums.txt": []byte(hex.EncodeToString(checksum[:]) + " " + archiveName + "\n"), } restore := replacePluginStoreClientForTest(httpClient) defer restore() report, errSync := SyncPlatformWithReport(context.Background(), syncTestConfig(t, root), nil, Platform{GOOS: "windows", GOARCH: "amd64"}) if errSync != nil { t.Fatalf("SyncPlatformWithReport() error = %v", errSync) } if !report.OK || report.Status != pluginTaskStatusOK || report.Phase != pluginTaskPhaseInstall { t.Fatalf("report status = %+v, want successful install phase", report) } if len(report.Plugins) != 1 { t.Fatalf("report plugins len = %d, want 1", len(report.Plugins)) } plugin := report.Plugins[0] if plugin.ID != "sample" || plugin.InstallStatus != pluginInstallStatusInstalled || plugin.Version != "0.2.0" { t.Fatalf("plugin report = %+v, want installed sample 0.2.0", plugin) } if wantPath := pluginTestPath(root, "windows", "amd64", "sample", "0.2.0"); plugin.Path != wantPath { t.Fatalf("plugin path = %q, want %q", plugin.Path, wantPath) }} func TestSyncPlatformWithReportRecordsSkippedIdenticalArtifact(t *testing.T) { root := t.TempDir() targetDir := filepath.Join(root, "windows", "amd64") if errMkdir := os.MkdirAll(targetDir, 0o755); errMkdir != nil { t.Fatalf("MkdirAll() error = %v", errMkdir) } target := filepath.Join(targetDir, "sample-v0.2.0.dll") if errWrite := os.WriteFile(target, []byte("library-data"), 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } archiveData := makeZip(t, map[string]string{"sample.dll": "library-data"}) archiveName := "sample_0.2.0_windows_amd64.zip" checksum := sha256.Sum256(archiveData) httpClient := mapHTTPDoer{ "https://api.github.com/repos/owner/sample-plugin/releases/tags/v0.2.0": []byte(`{ "tag_name": "v0.2.0", "assets": [ {"name": "` + archiveName + `", "browser_download_url": "https://downloads.example/` + archiveName + `"}, {"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"} ] }`), "https://downloads.example/" + archiveName: archiveData, "https://downloads.example/checksums.txt": []byte(hex.EncodeToString(checksum[:]) + " " + archiveName + "\n"), } restore := replacePluginStoreClientForTest(httpClient) defer restore() report, errSync := SyncPlatformWithReport(context.Background(), syncTestConfig(t, root), nil, Platform{GOOS: "windows", GOARCH: "amd64"}) if errSync != nil { t.Fatalf("SyncPlatformWithReport() error = %v", errSync) } if !report.OK || len(report.Plugins) != 1 { t.Fatalf("report = %+v, want one successful skipped plugin", report) } plugin := report.Plugins[0] if plugin.ID != "sample" || plugin.InstallStatus != pluginInstallStatusSkipped || !plugin.Skipped { t.Fatalf("plugin report = %+v, want skipped identical sample", plugin) } if plugin.Path != target { t.Fatalf("plugin path = %q, want %q", plugin.Path, target) }} func TestSyncPlatformSkipsIdenticalBusyPlugin(t *testing.T) { root := t.TempDir() targetDir := filepath.Join(root, "windows", "amd64") if errMkdir := os.MkdirAll(targetDir, 0o755); errMkdir != nil { t.Fatalf("MkdirAll() error = %v", errMkdir) } target := filepath.Join(targetDir, "sample-v0.2.0.dll") if errWrite := os.WriteFile(target, []byte("library-data"), 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } archiveData := makeZip(t, map[string]string{"sample.dll": "library-data"}) archiveName := "sample_0.2.0_windows_amd64.zip" checksum := sha256.Sum256(archiveData) httpClient := mapHTTPDoer{ "https://api.github.com/repos/owner/sample-plugin/releases/tags/v0.2.0": []byte(`{ "tag_name": "v0.2.0", "assets": [ {"name": "` + archiveName + `", "browser_download_url": "https://downloads.example/` + archiveName + `"}, {"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"} ] }`), "https://downloads.example/" + archiveName: archiveData, "https://downloads.example/checksums.txt": []byte(hex.EncodeToString(checksum[:]) + " " + archiveName + "\n"), } restore := replacePluginStoreClientForTest(httpClient) defer restore() runtime := &fakePluginRuntime{busy: true} if errSync := SyncPlatform(context.Background(), syncTestConfig(t, root), runtime, Platform{GOOS: "windows", GOARCH: "amd64"}); errSync != nil { t.Fatalf("SyncPlatform() error = %v", errSync) } if len(runtime.unloaded) != 0 { t.Fatalf("UnloadPlugin() calls = %v, want none", runtime.unloaded) } got, errRead := os.ReadFile(target) if errRead != nil { t.Fatalf("read target: %v", errRead) } if string(got) != "library-data" { t.Fatalf("target data = %q, want library-data", string(got)) }} func TestSyncPlatformSkipsConfigWithoutManifest(t *testing.T) { restore := replacePluginStoreClientForTest(mapHTTPDoer{}) defer restore() cfg := &config.Config{ Home: config.HomeConfig{Enabled: true}, Plugins: config.PluginsConfig{ Enabled: true, Dir: t.TempDir(), Configs: map[string]config.PluginInstanceConfig{ "sample": pluginConfigFromYAML(t, `enabled: true`), }, }, } if errSync := SyncPlatform(context.Background(), cfg, nil, Platform{GOOS: "linux", GOARCH: "amd64"}); errSync != nil { t.Fatalf("SyncPlatform() error = %v", errSync) }} func TestSyncPlatformRejectsInvalidManifest(t *testing.T) { cfg := &config.Config{ Home: config.HomeConfig{Enabled: true}, Plugins: config.PluginsConfig{ Enabled: true, Dir: t.TempDir(), Configs: map[string]config.PluginInstanceConfig{ "sample": pluginConfigFromYAML(t, `enabled: truestore: id: sample`), }, }, } if errSync := SyncPlatform(context.Background(), cfg, nil, Platform{GOOS: "linux", GOARCH: "amd64"}); errSync == nil { t.Fatal("SyncPlatform() error = nil, want invalid manifest") }} func TestSyncPlatformWithReportRecordsInvalidManifest(t *testing.T) { cfg := &config.Config{ Home: config.HomeConfig{Enabled: true}, Plugins: config.PluginsConfig{ Enabled: true, Dir: t.TempDir(), Configs: map[string]config.PluginInstanceConfig{ "sample": pluginConfigFromYAML(t, `enabled: truestore: id: sample`), }, }, } report, errSync := SyncPlatformWithReport(context.Background(), cfg, nil, Platform{GOOS: "linux", GOARCH: "amd64"}) if errSync == nil { t.Fatal("SyncPlatformWithReport() error = nil, want invalid manifest") } if report.OK || report.Status != pluginTaskStatusError || len(report.Plugins) != 1 { t.Fatalf("report = %+v, want one failed plugin", report) } if report.Plugins[0].ID != "sample" || report.Plugins[0].InstallStatus != pluginInstallStatusFailed || !strings.Contains(report.Plugins[0].Error, "invalid store manifest") { t.Fatalf("plugin report = %+v, want invalid manifest failure", report.Plugins[0]) }} func TestMarkLoadResultsFailsWhenInstalledPluginDidNotLoad(t *testing.T) { report := SyncReport{ Status: pluginTaskStatusOK, OK: true, Phase: pluginTaskPhaseInstall, Plugins: []PluginInstallStatus{{ID: "sample", InstallStatus: pluginInstallStatusInstalled}}, } errLoad := MarkLoadResults(&report, fakePluginLoadInspector{}) if errLoad == nil { t.Fatal("MarkLoadResults() error = nil, want load failure") } if report.OK || report.Status != pluginTaskStatusError || report.Phase != pluginTaskPhaseLoad { t.Fatalf("report = %+v, want failed load phase", report) } if report.Plugins[0].LoadStatus != pluginLoadStatusFailed || !strings.Contains(report.Plugins[0].Error, "installed but not loaded") { t.Fatalf("plugin report = %+v, want load failure", report.Plugins[0]) }} func TestMarkLoadResultsPreservesInstallFailure(t *testing.T) { report := SyncReport{Lines 1–500 of 814. Load more
package homeplugins import ( "archive/zip" "bytes" "context" "crypto/sha256" "encoding/hex" "errors" "io" "net/http" "net/http/httptest" "os" "path/filepath" "runtime" "strings" "testing" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" sdkpluginstore "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginstore" "gopkg.in/yaml.v3" ) type fakePluginRuntime struct { busy bool unloaded []string } type fakePluginLoadInspector map[string]bool func (r *fakePluginRuntime) PluginBusy(id string) bool { return r.busy } func (r *fakePluginRuntime) UnloadPlugin(id string) bool { r.unloaded = append(r.unloaded, id) r.busy = false return true } func (i fakePluginLoadInspector) PluginRegistered(id string) bool { return i[id] } type contextPluginRuntime struct { fakePluginRuntime unloadContext context.Context } func (r *contextPluginRuntime) UnloadPluginContext(ctx context.Context, id string) bool { r.unloadContext = ctx return r.UnloadPlugin(id) } func TestSyncPlatformInstallsManifestArtifact(t *testing.T) { root := t.TempDir() archiveData := makeZip(t, map[string]string{"sample.dll": "library-data"}) archiveName := "sample_0.2.0_windows_amd64.zip" checksum := sha256.Sum256(archiveData) httpClient := mapHTTPDoer{ "https://api.github.com/repos/owner/sample-plugin/releases/tags/v0.2.0": []byte(`{ "tag_name": "v0.2.0", "assets": [ {"name": "` + archiveName + `", "browser_download_url": "https://downloads.example/` + archiveName + `"}, {"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"} ] }`), "https://downloads.example/" + archiveName: archiveData, "https://downloads.example/checksums.txt": []byte(hex.EncodeToString(checksum[:]) + " " + archiveName + "\n"), } restore := replacePluginStoreClientForTest(httpClient) defer restore() if errSync := SyncPlatform(context.Background(), syncTestConfig(t, root), nil, Platform{GOOS: "windows", GOARCH: "amd64"}); errSync != nil { t.Fatalf("SyncPlatform() error = %v", errSync) } target := pluginTestPath(root, "windows", "amd64", "sample", "0.2.0") got, errRead := os.ReadFile(target) if errRead != nil { t.Fatalf("read target: %v", errRead) } if string(got) != "library-data" { t.Fatalf("target data = %q, want library-data", string(got)) } } func TestSyncResolvedWithReportUsesTemporaryAuthAndClearsIt(t *testing.T) { root := t.TempDir() libraryName := "sample" + pluginExtension(runtime.GOOS) archiveData := makeZip(t, map[string]string{libraryName: "library-data"}) checksum := sha256.Sum256(archiveData) var authenticated bool server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer temporary-token" { http.Error(w, "unauthorized", http.StatusUnauthorized) return } authenticated = true _, _ = w.Write(archiveData) })) t.Cleanup(server.Close) response, errUnauthenticated := server.Client().Get(server.URL + "/private/sample.zip") if errUnauthenticated != nil { t.Fatalf("unauthenticated GET error = %v", errUnauthenticated) } _ = response.Body.Close() if response.StatusCode != http.StatusUnauthorized { t.Fatalf("unauthenticated status = %d, want 401", response.StatusCode) } originalClient := newResolvedPluginStoreClient newResolvedPluginStoreClient = func(_ *config.Config, auth []sdkpluginstore.ResolvedAuthConfig, expiresAt time.Time) sdkpluginstore.Client { return sdkpluginstore.NewClientWithResolvedAuthExpiry(server.Client(), "", auth, expiresAt) } defer func() { newResolvedPluginStoreClient = originalClient }() token := sdkpluginstore.Secret("temporary-token") backing := token items := []sdkpluginstore.PluginSyncItem{{ Manifest: sdkpluginstore.Manifest{ SchemaVersion: sdkpluginstore.SchemaVersionV2, ID: "sample", Version: "1.0.0", Install: sdkpluginstore.InstallPlan{Type: sdkpluginstore.InstallTypeDirect, Artifacts: []sdkpluginstore.Artifact{{ GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, URL: server.URL + "/private/sample.zip", SHA256: hex.EncodeToString(checksum[:]), Size: int64(len(archiveData)), }}}, }, Auth: []sdkpluginstore.ResolvedAuthConfig{{ Match: server.URL + "/private/", ApplyTo: []string{sdkpluginstore.RequestKindArtifact}, Type: sdkpluginstore.AuthTypeBearer, Token: token, }}, }} enabled := true cfg := &config.Config{ Home: config.HomeConfig{Enabled: true}, Plugins: config.PluginsConfig{Enabled: true, Dir: root, Configs: map[string]config.PluginInstanceConfig{"sample": {Enabled: &enabled}}}, } report, errSync := SyncResolvedWithReport(context.Background(), cfg, items, time.Now().UTC().Add(time.Minute), map[string]string{"sample": "0.9.0"}, nil) if errSync != nil { t.Fatalf("SyncResolvedWithReport() error = %v", errSync) } if !authenticated || !report.OK || len(report.Plugins) != 1 || report.Plugins[0].Version != "1.0.0" { t.Fatalf("authenticated=%v report=%+v, want successful authenticated install", authenticated, report) } for index, value := range backing { if value != 0 { t.Fatalf("token byte %d = %d, want zero after sync", index, value) } } if items[0].Auth != nil { t.Fatalf("sync item retained auth references: %#v", items[0].Auth) } target := pluginTestPath(root, runtime.GOOS, runtime.GOARCH, "sample", "1.0.0") if got, errRead := os.ReadFile(target); errRead != nil || string(got) != "library-data" { t.Fatalf("installed plugin = %q, error = %v", got, errRead) } } func TestSyncResolvedWithReportIncludesUnchangedInstalledPlugins(t *testing.T) { root := t.TempDir() target := pluginTestPath(root, runtime.GOOS, runtime.GOARCH, "sample", "1.0.0") if errMkdir := os.MkdirAll(filepath.Dir(target), 0o755); errMkdir != nil { t.Fatalf("MkdirAll() error = %v", errMkdir) } if errWrite := os.WriteFile(target, []byte("plugin"), 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } cfg := &config.Config{ Home: config.HomeConfig{Enabled: true}, Plugins: config.PluginsConfig{ Enabled: true, Dir: root, Configs: map[string]config.PluginInstanceConfig{ "sample": pluginConfigFromYAML(t, ` enabled: true store: id: sample name: Sample description: Adds sample support. author: owner version: 1.0.0 release-tag: v1.0.0 repository: https://github.com/owner/sample-plugin `), }, }, } report, errSync := SyncResolvedWithReport( context.Background(), cfg, nil, time.Now().UTC().Add(time.Minute), map[string]string{"sample": "1.0.0"}, nil, ) if errSync != nil { t.Fatalf("SyncResolvedWithReport() error = %v", errSync) } if len(report.Plugins) != 1 || report.Plugins[0].ID != "sample" || report.Plugins[0].InstallStatus != pluginInstallStatusSkipped { t.Fatalf("report plugins = %+v, want unchanged installed sample", report.Plugins) } status := report.Plugins[0] if status.Path != target || status.ReleaseTag != "v1.0.0" || status.Repository != "https://github.com/owner/sample-plugin" || status.InstallType != sdkpluginstore.InstallTypeGitHubRelease { t.Fatalf("unchanged plugin status = %+v, want preserved path and manifest metadata", status) } if errLoad := MarkLoadResults(&report, fakePluginLoadInspector{}); errLoad == nil { t.Fatal("MarkLoadResults() error = nil, want installed plugin load failure") } if report.Plugins[0].LoadStatus != pluginLoadStatusFailed { t.Fatalf("load status = %q, want failed", report.Plugins[0].LoadStatus) } } func TestSyncResolvedWithReportDoesNotMixInstalledAndConfiguredMetadata(t *testing.T) { root := t.TempDir() target := pluginTestPath(root, runtime.GOOS, runtime.GOARCH, "sample", "1.0.0") if errMkdir := os.MkdirAll(filepath.Dir(target), 0o755); errMkdir != nil { t.Fatalf("MkdirAll() error = %v", errMkdir) } if errWrite := os.WriteFile(target, []byte("plugin"), 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } cfg := &config.Config{ Home: config.HomeConfig{Enabled: true}, Plugins: config.PluginsConfig{ Enabled: true, Dir: root, Configs: map[string]config.PluginInstanceConfig{ "sample": pluginConfigFromYAML(t, ` enabled: true store: id: sample name: Sample description: Adds sample support. author: owner version: 2.0.0 release-tag: v2.0.0 repository: https://github.com/owner/sample-plugin-v2 `), }, }, } report, errSync := SyncResolvedWithReport( context.Background(), cfg, nil, time.Now().UTC().Add(time.Minute), map[string]string{"sample": "1.0.0"}, nil, ) if errSync != nil { t.Fatalf("SyncResolvedWithReport() error = %v", errSync) } if len(report.Plugins) != 1 { t.Fatalf("report plugins = %+v, want one installed sample", report.Plugins) } status := report.Plugins[0] if status.Version != "1.0.0" || status.Path != target { t.Fatalf("installed plugin status = %+v, want version 1.0.0 at %s", status, target) } if status.ReleaseTag != "" || status.Repository != "" || status.InstallType != "" { t.Fatalf("installed plugin status = %+v, want no metadata from configured version 2.0.0", status) } } func TestInstalledVersionsUsesPluginFilesOnDisk(t *testing.T) { root := t.TempDir() target := pluginTestPath(root, runtime.GOOS, runtime.GOARCH, "sample", "2.3.4") if errMkdir := os.MkdirAll(filepath.Dir(target), 0o755); errMkdir != nil { t.Fatalf("MkdirAll() error = %v", errMkdir) } if errWrite := os.WriteFile(target, []byte("plugin"), 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } cfg := &config.Config{Plugins: config.PluginsConfig{Dir: root, Configs: map[string]config.PluginInstanceConfig{"sample": {}}}} versions, errVersions := InstalledVersions(cfg) if errVersions != nil { t.Fatalf("InstalledVersions() error = %v", errVersions) } if versions["sample"] != "2.3.4" { t.Fatalf("InstalledVersions() = %#v, want sample 2.3.4", versions) } } func TestSyncPlatformWithReportRecordsSuccessfulInstall(t *testing.T) { root := t.TempDir() archiveData := makeZip(t, map[string]string{"sample.dll": "library-data"}) archiveName := "sample_0.2.0_windows_amd64.zip" checksum := sha256.Sum256(archiveData) httpClient := mapHTTPDoer{ "https://api.github.com/repos/owner/sample-plugin/releases/tags/v0.2.0": []byte(`{ "tag_name": "v0.2.0", "assets": [ {"name": "` + archiveName + `", "browser_download_url": "https://downloads.example/` + archiveName + `"}, {"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"} ] }`), "https://downloads.example/" + archiveName: archiveData, "https://downloads.example/checksums.txt": []byte(hex.EncodeToString(checksum[:]) + " " + archiveName + "\n"), } restore := replacePluginStoreClientForTest(httpClient) defer restore() report, errSync := SyncPlatformWithReport(context.Background(), syncTestConfig(t, root), nil, Platform{GOOS: "windows", GOARCH: "amd64"}) if errSync != nil { t.Fatalf("SyncPlatformWithReport() error = %v", errSync) } if !report.OK || report.Status != pluginTaskStatusOK || report.Phase != pluginTaskPhaseInstall { t.Fatalf("report status = %+v, want successful install phase", report) } if len(report.Plugins) != 1 { t.Fatalf("report plugins len = %d, want 1", len(report.Plugins)) } plugin := report.Plugins[0] if plugin.ID != "sample" || plugin.InstallStatus != pluginInstallStatusInstalled || plugin.Version != "0.2.0" { t.Fatalf("plugin report = %+v, want installed sample 0.2.0", plugin) } if wantPath := pluginTestPath(root, "windows", "amd64", "sample", "0.2.0"); plugin.Path != wantPath { t.Fatalf("plugin path = %q, want %q", plugin.Path, wantPath) } } func TestSyncPlatformWithReportRecordsSkippedIdenticalArtifact(t *testing.T) { root := t.TempDir() targetDir := filepath.Join(root, "windows", "amd64") if errMkdir := os.MkdirAll(targetDir, 0o755); errMkdir != nil { t.Fatalf("MkdirAll() error = %v", errMkdir) } target := filepath.Join(targetDir, "sample-v0.2.0.dll") if errWrite := os.WriteFile(target, []byte("library-data"), 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } archiveData := makeZip(t, map[string]string{"sample.dll": "library-data"}) archiveName := "sample_0.2.0_windows_amd64.zip" checksum := sha256.Sum256(archiveData) httpClient := mapHTTPDoer{ "https://api.github.com/repos/owner/sample-plugin/releases/tags/v0.2.0": []byte(`{ "tag_name": "v0.2.0", "assets": [ {"name": "` + archiveName + `", "browser_download_url": "https://downloads.example/` + archiveName + `"}, {"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"} ] }`), "https://downloads.example/" + archiveName: archiveData, "https://downloads.example/checksums.txt": []byte(hex.EncodeToString(checksum[:]) + " " + archiveName + "\n"), } restore := replacePluginStoreClientForTest(httpClient) defer restore() report, errSync := SyncPlatformWithReport(context.Background(), syncTestConfig(t, root), nil, Platform{GOOS: "windows", GOARCH: "amd64"}) if errSync != nil { t.Fatalf("SyncPlatformWithReport() error = %v", errSync) } if !report.OK || len(report.Plugins) != 1 { t.Fatalf("report = %+v, want one successful skipped plugin", report) } plugin := report.Plugins[0] if plugin.ID != "sample" || plugin.InstallStatus != pluginInstallStatusSkipped || !plugin.Skipped { t.Fatalf("plugin report = %+v, want skipped identical sample", plugin) } if plugin.Path != target { t.Fatalf("plugin path = %q, want %q", plugin.Path, target) } } func TestSyncPlatformSkipsIdenticalBusyPlugin(t *testing.T) { root := t.TempDir() targetDir := filepath.Join(root, "windows", "amd64") if errMkdir := os.MkdirAll(targetDir, 0o755); errMkdir != nil { t.Fatalf("MkdirAll() error = %v", errMkdir) } target := filepath.Join(targetDir, "sample-v0.2.0.dll") if errWrite := os.WriteFile(target, []byte("library-data"), 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } archiveData := makeZip(t, map[string]string{"sample.dll": "library-data"}) archiveName := "sample_0.2.0_windows_amd64.zip" checksum := sha256.Sum256(archiveData) httpClient := mapHTTPDoer{ "https://api.github.com/repos/owner/sample-plugin/releases/tags/v0.2.0": []byte(`{ "tag_name": "v0.2.0", "assets": [ {"name": "` + archiveName + `", "browser_download_url": "https://downloads.example/` + archiveName + `"}, {"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"} ] }`), "https://downloads.example/" + archiveName: archiveData, "https://downloads.example/checksums.txt": []byte(hex.EncodeToString(checksum[:]) + " " + archiveName + "\n"), } restore := replacePluginStoreClientForTest(httpClient) defer restore() runtime := &fakePluginRuntime{busy: true} if errSync := SyncPlatform(context.Background(), syncTestConfig(t, root), runtime, Platform{GOOS: "windows", GOARCH: "amd64"}); errSync != nil { t.Fatalf("SyncPlatform() error = %v", errSync) } if len(runtime.unloaded) != 0 { t.Fatalf("UnloadPlugin() calls = %v, want none", runtime.unloaded) } got, errRead := os.ReadFile(target) if errRead != nil { t.Fatalf("read target: %v", errRead) } if string(got) != "library-data" { t.Fatalf("target data = %q, want library-data", string(got)) } } func TestSyncPlatformSkipsConfigWithoutManifest(t *testing.T) { restore := replacePluginStoreClientForTest(mapHTTPDoer{}) defer restore() cfg := &config.Config{ Home: config.HomeConfig{Enabled: true}, Plugins: config.PluginsConfig{ Enabled: true, Dir: t.TempDir(), Configs: map[string]config.PluginInstanceConfig{ "sample": pluginConfigFromYAML(t, `enabled: true`), }, }, } if errSync := SyncPlatform(context.Background(), cfg, nil, Platform{GOOS: "linux", GOARCH: "amd64"}); errSync != nil { t.Fatalf("SyncPlatform() error = %v", errSync) } } func TestSyncPlatformRejectsInvalidManifest(t *testing.T) { cfg := &config.Config{ Home: config.HomeConfig{Enabled: true}, Plugins: config.PluginsConfig{ Enabled: true, Dir: t.TempDir(), Configs: map[string]config.PluginInstanceConfig{ "sample": pluginConfigFromYAML(t, ` enabled: true store: id: sample `), }, }, } if errSync := SyncPlatform(context.Background(), cfg, nil, Platform{GOOS: "linux", GOARCH: "amd64"}); errSync == nil { t.Fatal("SyncPlatform() error = nil, want invalid manifest") } } func TestSyncPlatformWithReportRecordsInvalidManifest(t *testing.T) { cfg := &config.Config{ Home: config.HomeConfig{Enabled: true}, Plugins: config.PluginsConfig{ Enabled: true, Dir: t.TempDir(), Configs: map[string]config.PluginInstanceConfig{ "sample": pluginConfigFromYAML(t, ` enabled: true store: id: sample `), }, }, } report, errSync := SyncPlatformWithReport(context.Background(), cfg, nil, Platform{GOOS: "linux", GOARCH: "amd64"}) if errSync == nil { t.Fatal("SyncPlatformWithReport() error = nil, want invalid manifest") } if report.OK || report.Status != pluginTaskStatusError || len(report.Plugins) != 1 { t.Fatalf("report = %+v, want one failed plugin", report) } if report.Plugins[0].ID != "sample" || report.Plugins[0].InstallStatus != pluginInstallStatusFailed || !strings.Contains(report.Plugins[0].Error, "invalid store manifest") { t.Fatalf("plugin report = %+v, want invalid manifest failure", report.Plugins[0]) } } func TestMarkLoadResultsFailsWhenInstalledPluginDidNotLoad(t *testing.T) { report := SyncReport{ Status: pluginTaskStatusOK, OK: true, Phase: pluginTaskPhaseInstall, Plugins: []PluginInstallStatus{{ID: "sample", InstallStatus: pluginInstallStatusInstalled}}, } errLoad := MarkLoadResults(&report, fakePluginLoadInspector{}) if errLoad == nil { t.Fatal("MarkLoadResults() error = nil, want load failure") } if report.OK || report.Status != pluginTaskStatusError || report.Phase != pluginTaskPhaseLoad { t.Fatalf("report = %+v, want failed load phase", report) } if report.Plugins[0].LoadStatus != pluginLoadStatusFailed || !strings.Contains(report.Plugins[0].Error, "installed but not loaded") { t.Fatalf("plugin report = %+v, want load failure", report.Plugins[0]) } } func TestMarkLoadResultsPreservesInstallFailure(t *testing.T) { report := SyncReport{