diff --git a/agent_plugins_test.go b/agent_plugins_test.go index 25f87609b..a1772ca1c 100644 --- a/agent_plugins_test.go +++ b/agent_plugins_test.go @@ -3,6 +3,7 @@ package main import ( "archive/zip" "bytes" + "context" "encoding/json" "fmt" "os" @@ -234,6 +235,154 @@ func assertPluginsInstalledGlobally(t *testing.T, homeDir string, harnesses []st } } +// assertPluginsInstalledNatively verifies plugins via each harness's native query mechanism. +// For Claude/Codex: calls their native CLI commands. For Cursor/VSCode: reads manifest files. +// This ensures the harness itself recognizes the installed plugin, not just filesystem presence. +func assertPluginsInstalledNatively(t *testing.T, harnesses []string, slug string, wantVersion string) { + t.Helper() + // Validate inputs to catch test bugs early + require.NotEmpty(t, harnesses, "harnesses list must not be empty") + require.NotEmpty(t, slug, "plugin slug must not be empty") + require.NotEmpty(t, wantVersion, "plugin version must not be empty") + + for _, harness := range harnesses { + switch strings.ToLower(harness) { + case "claude": + assertClaudePluginInstalled(t, slug, wantVersion) + case "codex": + assertCodexPluginInstalled(t, slug, wantVersion) + case "cursor": + assertCursorPluginInstalled(t, slug, wantVersion) + case "vscode": + assertVSCodePluginInstalled(t, slug, wantVersion, tests.AgentPluginsLocalRepo) + default: + t.Fatalf("unknown harness: %s", harness) + } + } +} + +// assertClaudePluginInstalled calls `claude plugin list --json` and verifies plugin presence. +func assertClaudePluginInstalled(t *testing.T, slug string, wantVersion string) { + t.Helper() + // Use timeout to prevent test hangs if claude CLI is unresponsive + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "claude", "plugin", "list", "--json") // #nosec G204 -- fixed command + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + require.NoError(t, err, "claude plugin list --json failed: %s", stderr.String()) + + var plugins []map[string]any + require.NoError(t, json.Unmarshal(stdout.Bytes(), &plugins), + "failed to parse claude plugin list output (stderr: %s)", stderr.String()) + + found := false + for _, p := range plugins { + id, ok := p["id"].(string) + if !ok { + // Log type mismatch for debugging if id is missing/wrong type + continue + } + // id format: "@" + if strings.HasPrefix(id, slug+"@") { + version, ok := p["version"].(string) + require.True(t, ok, "plugin %s missing version field or wrong type; got: %T", id, p["version"]) + assert.Equal(t, wantVersion, version, "claude plugin %s has wrong version", id) + found = true + break + } + } + require.True(t, found, "claude plugin %q not found in `claude plugin list`", slug) +} + +// assertCodexPluginInstalled calls `codex plugin list --json` and verifies plugin in installed[]. +func assertCodexPluginInstalled(t *testing.T, slug string, wantVersion string) { + t.Helper() + // Use timeout to prevent test hangs if codex CLI is unresponsive + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "codex", "plugin", "list", "--json") // #nosec G204 -- fixed command + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + require.NoError(t, err, "codex plugin list --json failed: %s", stderr.String()) + + var result map[string]any + require.NoError(t, json.Unmarshal(stdout.Bytes(), &result), + "failed to parse codex plugin list output (stderr: %s)", stderr.String()) + + installed, ok := result["installed"].([]any) + require.True(t, ok, "codex plugin list missing 'installed' array") + + found := false + for _, p := range installed { + plugin, ok := p.(map[string]any) + if !ok { + // Array element is not a map; log for debugging + continue + } + pluginID, ok := plugin["pluginId"].(string) + if !ok { + // pluginId missing or wrong type; log for debugging + continue + } + // pluginId format: "@" + if strings.HasPrefix(pluginID, slug+"@") { + version, ok := plugin["version"].(string) + require.True(t, ok, "codex plugin %s missing version field or wrong type; got: %T", pluginID, plugin["version"]) + assert.Equal(t, wantVersion, version, "codex plugin %s has wrong version", pluginID) + found = true + break + } + } + require.True(t, found, "codex plugin %q not found in `codex plugin list`", slug) +} + +// assertCursorPluginInstalled reads cursor's plugin manifest from filesystem. +func assertCursorPluginInstalled(t *testing.T, slug string, wantVersion string) { + t.Helper() + homeDir := os.Getenv("HOME") + if homeDir == "" { + homeDir = os.Getenv("USERPROFILE") + } + require.NotEmpty(t, homeDir, "HOME or USERPROFILE must be set") + + // Cursor doesn't have repo-keyed layout; search under ~/.cursor/plugins/local/ + manifestPath := filepath.Join(homeDir, ".cursor", "plugins", "local", slug, ".jfrog", "plugin-info.json") + data, err := os.ReadFile(manifestPath) // #nosec G304 -- known test path + require.NoError(t, err, "cursor plugin %q not installed at %s", slug, manifestPath) + + var manifest map[string]any + require.NoError(t, json.Unmarshal(data, &manifest)) + version, ok := manifest["installedVersion"].(string) + require.True(t, ok, "cursor plugin %q missing installedVersion", slug) + assert.Equal(t, wantVersion, version, "cursor plugin %q has wrong version", slug) +} + +// assertVSCodePluginInstalled reads vscode's plugin manifest from filesystem. +func assertVSCodePluginInstalled(t *testing.T, slug string, wantVersion string, repo string) { + t.Helper() + homeDir := os.Getenv("HOME") + if homeDir == "" { + homeDir = os.Getenv("USERPROFILE") + } + require.NotEmpty(t, homeDir, "HOME or USERPROFILE must be set") + + // VSCode has repo-keyed layout: ~/.copilot/installed-plugins/// + manifestPath := filepath.Join(homeDir, ".copilot", "installed-plugins", repo, slug, ".jfrog", "plugin-info.json") + data, err := os.ReadFile(manifestPath) // #nosec G304 -- known test path + require.NoError(t, err, "vscode plugin %q not installed at %s", slug, manifestPath) + + var manifest map[string]any + require.NoError(t, json.Unmarshal(data, &manifest)) + version, ok := manifest["installedVersion"].(string) + require.True(t, ok, "vscode plugin %q missing installedVersion or wrong type; got: %T", slug, manifest["installedVersion"]) + assert.Equal(t, wantVersion, version, "vscode plugin %q has wrong version", slug) +} + func setIsolatedHome(t *testing.T) string { t.Helper() homeDir := t.TempDir() @@ -242,6 +391,21 @@ func setIsolatedHome(t *testing.T) string { return homeDir } +// verifyIsolatedHome verifies that HOME was actually changed to an isolated directory. +func verifyIsolatedHome(t *testing.T, homeDir string) { + t.Helper() + // Verify directory exists + require.DirExists(t, homeDir, "isolated HOME directory should exist") + // Verify HOME env var is set to isolated dir + require.Equal(t, homeDir, os.Getenv("HOME"), "HOME env var should point to isolated directory") + // Verify directory is under system temp (cross-platform: os.TempDir() returns system temp path) + // t.TempDir() creates subdirectories under os.TempDir(), so we check that homeDir starts with temp root + tempDir := os.TempDir() + tempRoot := filepath.Dir(tempDir) // Get parent of temp dir to allow for t.TempDir() subdirectories + require.True(t, strings.HasPrefix(filepath.Clean(homeDir), filepath.Clean(tempRoot)), + "isolated HOME should be in system temp directory, got %s (temp root: %s)", homeDir, tempRoot) +} + // pluginManifestDir returns the directory holding a harness's plugin.json inside a plugin. // Harnesses use .-plugin, except vscode, which reads plugin.json at the plugin root. func pluginManifestDir(pluginPath, harness string) string { @@ -574,7 +738,9 @@ func TestAgentPluginsPublishToNonExistentRepo(t *testing.T) { "--repo=nonexistent-agent-plugins-repo-xyz", ) // Publish wraps the Artifactory upload failure (see publish.go: "upload failed: %w"). - assertErrorContainsAll(t, err, "upload failed") + // Repository does not exist, so Artifactory returns 404 or 405. + require.Error(t, err, "publish to nonexistent repo should fail") + require.Contains(t, err.Error(), "upload failed", "error should mention upload failure") } // TestAgentPluginsChecksumIntegrity verifies that after publish the artifact @@ -848,7 +1014,10 @@ func TestAgentPluginsPublishToWrongRepoType(t *testing.T) { "publish", pluginPath, "--repo="+wrongTypeRepo, ) - assertErrorContainsAll(t, err, "upload failed") + // Wrong repo type (generic local, not agent plugins) causes upload to fail. + // Artifactory returns 405 (Method Not Allowed) for non-agent-plugins repos. + require.Error(t, err, "publish to wrong repo type should fail") + require.Contains(t, err.Error(), "upload failed", "error should mention upload failure") } // TestAgentPluginsPublishPrebuiltZip verifies that a prebuilt -.zip @@ -1174,7 +1343,7 @@ func TestAgentPluginsInstallProjectScopeRejectedForBuiltIns(t *testing.T) { defer cleanAgentPluginsTest() slug := "project-dir-plugin" - pluginPath := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor"}) + pluginPath := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor", "vscode"}) require.NoError(t, runAgentPluginsCmd(t, "publish", pluginPath, "--repo="+tests.AgentPluginsLocalRepo, @@ -1207,7 +1376,7 @@ func TestAgentPluginsInstallGlobal(t *testing.T) { defer cleanAgentPluginsTest() slug := "global-install-plugin" - pluginPath := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor"}) + pluginPath := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor", "vscode"}) require.NoError(t, runAgentPluginsCmd(t, "publish", pluginPath, "--repo="+tests.AgentPluginsLocalRepo, @@ -1216,8 +1385,11 @@ func TestAgentPluginsInstallGlobal(t *testing.T) { for _, tc := range agentPluginHarnessCases() { t.Run(tc.name, func(t *testing.T) { homeDir := setIsolatedHome(t) + verifyIsolatedHome(t, homeDir) require.NoError(t, installViaMarketplaceWithRetry(t, slug, harnessFlag(tc.harnesses))) assertPluginsInstalledGlobally(t, homeDir, tc.harnesses, slug, "1.0.0") + // Strict: verify native harness recognizes the plugin + assertPluginsInstalledNatively(t, tc.harnesses, slug, "1.0.0") }) } } @@ -1232,7 +1404,7 @@ func TestAgentPluginsInstallMarketplace(t *testing.T) { defer cleanAgentPluginsTest() slug := "marketplace-plugin" - pluginPath := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor"}) + pluginPath := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor", "vscode"}) require.NoError(t, runAgentPluginsCmd(t, "publish", pluginPath, "--repo="+tests.AgentPluginsLocalRepo, @@ -1243,7 +1415,10 @@ func TestAgentPluginsInstallMarketplace(t *testing.T) { homeDir := setIsolatedHome(t) require.NoError(t, installViaMarketplaceWithRetry(t, slug, harnessFlag(tc.harnesses)), "install without --version should resolve through the generated marketplace") + // Strict: verify filesystem presence assertPluginsInstalledGlobally(t, homeDir, tc.harnesses, slug, "1.0.0") + // Strict: verify each harness natively recognizes the plugin + assertPluginsInstalledNatively(t, tc.harnesses, slug, "1.0.0") }) } } @@ -1916,9 +2091,9 @@ func TestAgentPluginsUpdateAll(t *testing.T) { // claude's .claude-plugin/ convention); a flat root plugin.json fails // "codex plugin add" with "missing plugin.json". Use the harness-aware fixture // since these cases install with harness=codex (via agentPluginHarnessCases()). - v1Path := createTestHarnessPlugin(t, entry.slug, entry.oldVer, []string{"claude", "codex", "cursor"}) + v1Path := createTestHarnessPlugin(t, entry.slug, entry.oldVer, []string{"claude", "codex", "cursor", "vscode"}) require.NoError(t, runAgentPluginsCmd(t, "publish", v1Path, "--repo="+tests.AgentPluginsLocalRepo)) - v2Path := createTestHarnessPlugin(t, entry.slug, entry.newVer, []string{"claude", "codex", "cursor"}) + v2Path := createTestHarnessPlugin(t, entry.slug, entry.newVer, []string{"claude", "codex", "cursor", "vscode"}) require.NoError(t, runAgentPluginsCmd(t, "publish", v2Path, "--repo="+tests.AgentPluginsLocalRepo)) } @@ -1945,6 +2120,7 @@ func TestAgentPluginsUpdateAll(t *testing.T) { )) for _, slug := range []string{slugA, slugB} { + // Strict: verify filesystem after update for _, harness := range tc.harnesses { manifestPath := filepath.Join(globalPluginInstallDir(homeDir, harness, tests.AgentPluginsLocalRepo, slug), ".jfrog", "plugin-info.json") require.FileExists(t, manifestPath, "plugin-info.json should exist for %s/%s after update --all", harness, slug) @@ -1955,6 +2131,8 @@ func TestAgentPluginsUpdateAll(t *testing.T) { assert.Equal(t, "2.0.0", manifest["installedVersion"], "update --all should upgrade %s/%s from 1.0.0 to 2.0.0", harness, slug) } + // Strict: verify native harness recognizes the updated version + assertPluginsInstalledNatively(t, tc.harnesses, slug, "2.0.0") } }) } @@ -1971,9 +2149,9 @@ func TestAgentPluginsUpdateAllNonInteractive(t *testing.T) { // claude's .claude-plugin/ convention); a flat root plugin.json fails // "codex plugin add" with "missing plugin.json". Use the harness-aware fixture // since these cases install with harness=codex (via agentPluginHarnessCases()). - v1Path := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor"}) + v1Path := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor", "vscode"}) require.NoError(t, runAgentPluginsCmd(t, "publish", v1Path, "--repo="+tests.AgentPluginsLocalRepo)) - v2Path := createTestHarnessPlugin(t, slug, "2.0.0", []string{"claude", "codex", "cursor"}) + v2Path := createTestHarnessPlugin(t, slug, "2.0.0", []string{"claude", "codex", "cursor", "vscode"}) require.NoError(t, runAgentPluginsCmd(t, "publish", v2Path, "--repo="+tests.AgentPluginsLocalRepo)) for _, tc := range agentPluginHarnessCases() { @@ -2006,6 +2184,8 @@ func TestAgentPluginsUpdateAllNonInteractive(t *testing.T) { assert.Equal(t, "2.0.0", manifest["installedVersion"], "update --all with CI=true should upgrade %s to 2.0.0", harness) } + // Strict: verify native harness recognizes the updated version + assertPluginsInstalledNatively(t, tc.harnesses, slug, "2.0.0") }) } } @@ -2021,9 +2201,9 @@ func TestAgentPluginsUpdateFormatJSON(t *testing.T) { // claude's .claude-plugin/ convention); a flat root plugin.json fails // "codex plugin add" with "missing plugin.json". Use the harness-aware fixture // since these cases install with harness=codex (via agentPluginHarnessCases()). - v1Path := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor"}) + v1Path := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor", "vscode"}) require.NoError(t, runAgentPluginsCmd(t, "publish", v1Path, "--repo="+tests.AgentPluginsLocalRepo)) - v2Path := createTestHarnessPlugin(t, slug, "2.0.0", []string{"claude", "codex", "cursor"}) + v2Path := createTestHarnessPlugin(t, slug, "2.0.0", []string{"claude", "codex", "cursor", "vscode"}) require.NoError(t, runAgentPluginsCmd(t, "publish", v2Path, "--repo="+tests.AgentPluginsLocalRepo)) for _, tc := range agentPluginHarnessCases() { @@ -2047,7 +2227,9 @@ func TestAgentPluginsUpdateFormatJSON(t *testing.T) { ) require.NoError(t, err, "update --slug --format=json should succeed") assertInstallSummaryJSON(t, out, slug, "2.0.0") + // Strict: verify filesystem AND native harness recognition assertPluginsInstalledGlobally(t, homeDir, tc.harnesses, slug, "2.0.0") + assertPluginsInstalledNatively(t, tc.harnesses, slug, "2.0.0") require.NoError(t, runAgentPluginsCmd(t, "install", slug, @@ -2069,7 +2251,9 @@ func TestAgentPluginsUpdateFormatJSON(t *testing.T) { ) require.NoError(t, err, "update --all --format=json should succeed") assertUpdateAllSummaryJSONContains(t, out, slug) + // Strict: verify filesystem AND native harness recognition assertPluginsInstalledGlobally(t, homeDir, tc.harnesses, slug, "2.0.0") + assertPluginsInstalledNatively(t, tc.harnesses, slug, "2.0.0") }) } } @@ -2178,7 +2362,7 @@ func TestAgentPluginsListCheckUpdates(t *testing.T) { slug := "check-updates-plugin" version := "1.0.0" - pluginPath := createTestHarnessPlugin(t, slug, version, []string{"claude", "codex", "cursor"}) + pluginPath := createTestHarnessPlugin(t, slug, version, []string{"claude", "codex", "cursor", "vscode"}) require.NoError(t, runAgentPluginsCmd(t, "publish", pluginPath, "--repo="+tests.AgentPluginsLocalRepo, @@ -2187,8 +2371,11 @@ func TestAgentPluginsListCheckUpdates(t *testing.T) { for _, tc := range agentPluginHarnessCases() { t.Run(tc.name, func(t *testing.T) { homeDir := setIsolatedHome(t) + verifyIsolatedHome(t, homeDir) require.NoError(t, installViaMarketplaceWithRetry(t, slug, harnessFlag(tc.harnesses))) assertPluginsInstalledGlobally(t, homeDir, tc.harnesses, slug, version) + // Strict: verify native harness recognizes the plugin + assertPluginsInstalledNatively(t, tc.harnesses, slug, version) out, err := runAgentPluginsCmdWithOutput(t, "list", @@ -2214,9 +2401,9 @@ func TestAgentPluginsListCheckUpdatesStatus(t *testing.T) { // claude's .claude-plugin/ convention); a flat root plugin.json fails // "codex plugin add" with "missing plugin.json". Use the harness-aware fixture // since these cases install with harness=codex (via agentPluginHarnessCases()). - v1Path := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor"}) + v1Path := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor", "vscode"}) require.NoError(t, runAgentPluginsCmd(t, "publish", v1Path, "--repo="+tests.AgentPluginsLocalRepo)) - v2Path := createTestHarnessPlugin(t, slug, "2.0.0", []string{"claude", "codex", "cursor"}) + v2Path := createTestHarnessPlugin(t, slug, "2.0.0", []string{"claude", "codex", "cursor", "vscode"}) require.NoError(t, runAgentPluginsCmd(t, "publish", v2Path, "--repo="+tests.AgentPluginsLocalRepo)) for _, tc := range agentPluginHarnessCases() { @@ -2229,7 +2416,10 @@ func TestAgentPluginsListCheckUpdatesStatus(t *testing.T) { "--global", "--version=1.0.0", )) - assertPluginsInstalledGlobally(t, homeDir, tc.harnesses, slug) + // Strict: verify filesystem presence + assertPluginsInstalledGlobally(t, homeDir, tc.harnesses, slug, "1.0.0") + // Strict: verify native harness sees v1.0.0 + assertPluginsInstalledNatively(t, tc.harnesses, slug, "1.0.0") out, err := runAgentPluginsCmdWithOutput(t, "list", @@ -2258,8 +2448,12 @@ func TestAgentPluginsListCheckUpdatesCurrent(t *testing.T) { for _, tc := range agentPluginHarnessCases() { t.Run(tc.name, func(t *testing.T) { homeDir := setIsolatedHome(t) + verifyIsolatedHome(t, homeDir) require.NoError(t, installViaMarketplaceWithRetry(t, slug, harnessFlag(tc.harnesses))) - assertPluginsInstalledGlobally(t, homeDir, tc.harnesses, slug) + // Strict: verify filesystem presence + assertPluginsInstalledGlobally(t, homeDir, tc.harnesses, slug, version) + // Strict: verify native harness sees the latest version + assertPluginsInstalledNatively(t, tc.harnesses, slug, version) out, err := runAgentPluginsCmdWithOutput(t, "list", @@ -2593,8 +2787,11 @@ func TestAgentPluginsListLocal(t *testing.T) { for _, tc := range agentPluginHarnessCases() { t.Run(tc.name, func(t *testing.T) { homeDir := setIsolatedHome(t) + verifyIsolatedHome(t, homeDir) require.NoError(t, installViaMarketplaceWithRetry(t, slug, harnessFlag(tc.harnesses))) assertPluginsInstalledGlobally(t, homeDir, tc.harnesses, slug, version) + // Strict: verify native harness recognizes the plugin + assertPluginsInstalledNatively(t, tc.harnesses, slug, version) // Intentionally omit --global: list should still default to global scope. out, err := runAgentPluginsCmdWithOutput(t, @@ -2794,7 +2991,7 @@ func TestAgentPluginsListLimitHarnessMode(t *testing.T) { slugs := []string{"limit-a-plugin", "limit-b-plugin", "limit-c-plugin"} for _, slug := range slugs { - p := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor"}) + p := createTestHarnessPlugin(t, slug, "1.0.0", []string{"claude", "codex", "cursor", "vscode"}) require.NoError(t, runAgentPluginsCmd(t, "publish", p, "--repo="+tests.AgentPluginsLocalRepo)) } @@ -2803,7 +3000,9 @@ func TestAgentPluginsListLimitHarnessMode(t *testing.T) { homeDir := setIsolatedHome(t) for _, slug := range slugs { require.NoError(t, installViaMarketplaceWithRetry(t, slug, harnessFlag(tc.harnesses))) - assertPluginsInstalledGlobally(t, homeDir, tc.harnesses, slug) + assertPluginsInstalledGlobally(t, homeDir, tc.harnesses, slug, "1.0.0") + // Strict: verify native harness recognizes each plugin + assertPluginsInstalledNatively(t, tc.harnesses, slug, "1.0.0") } out, err := runAgentPluginsCmdWithOutput(t, @@ -3100,6 +3299,8 @@ func TestAgentPluginsInstallRepoFromEnvVar(t *testing.T) { ) }), "install should resolve repo from JFROG_AGENT_PLUGINS_REPO") assertPluginsInstalledGlobally(t, homeDir, []string{"cursor"}, slug, version) + // Strict: verify native harness recognizes the plugin + assertPluginsInstalledNatively(t, []string{"cursor"}, slug, version) } // TestAgentPluginsUpdateRepoFromEnvVar verifies update resolves the repo from @@ -3131,6 +3332,8 @@ func TestAgentPluginsUpdateRepoFromEnvVar(t *testing.T) { "--global", ), "update should resolve repo from JFROG_AGENT_PLUGINS_REPO") assertPluginsInstalledGlobally(t, homeDir, []string{"cursor"}, slug, "2.0.0") + // Strict: verify native harness recognizes the updated version + assertPluginsInstalledNatively(t, []string{"cursor"}, slug, "2.0.0") } // TestAgentPluginsRepoFlagOverridesEnvVar verifies that --repo takes precedence @@ -3543,6 +3746,8 @@ func TestAgentPluginsPublishMultiHarnessMarketplaceIndexing(t *testing.T) { "--global", ), "install %s without --version must succeed", slug) assertPluginsInstalledGlobally(t, homeDir, tc.harnesses, slug, "1.0.0") + // Strict: verify native harness recognizes the plugin + assertPluginsInstalledNatively(t, tc.harnesses, slug, "1.0.0") out, err := runAgentPluginsCmdWithOutput(t, "list",