Files
gnoma/internal/plugin/loader_test.go
T
vikingowl ec9433d783 chore(lint): clear remaining errcheck and staticcheck findings
Brings the project to a clean `make lint` baseline (0 issues).

Mechanical:
- Wrap deferred resp.Body.Close() in closures (router/discovery.go,
  router/probe.go) so the unchecked return surfaces as `_ = ...`.
- Apply `_ = ...` (single or multi-return blank) to test-file calls
  that intentionally ignore errors: os.MkdirAll / os.WriteFile / os.Chdir
  in setup paths, Close / Shutdown in teardown, Submit / Spawn / Send /
  LoadDir in tests that assert on side effects.

Structural:
- engine.handleRequestTooLarge drops the unused req parameter and
  rebuilds the request from compacted history (SA4009 — argument was
  overwritten before first use).
- provider.ClassifyHTTPStatus and google.applyCapabilityOverrides switch
  to tagged switches over the discriminator (QF1002).
- tui.app.go MouseWheel + inputMode and cmd/gnoma main slm-status use
  tagged switches in place of equality chains (QF1003).
- cmd/gnoma main.go merges a var decl with its immediate assignment
  (S1021).
- Three empty-branch sites (dispatcher_test, loader_test,
  coordinator_test) become real assertions or get the dead `if` removed
  (SA9003).
2026-05-19 17:53:42 +02:00

340 lines
9.9 KiB
Go

package plugin
import (
"log/slog"
"os"
"path/filepath"
"testing"
)
func testLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
}
// writePlugin creates a plugin directory with a plugin.json manifest.
func writePlugin(t *testing.T, dir, name, version string, caps *Capabilities) {
t.Helper()
pluginDir := filepath.Join(dir, name)
if err := os.MkdirAll(pluginDir, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
m := Manifest{Name: name, Version: version}
if caps != nil {
m.Capabilities = *caps
}
data, _ := marshalManifest(m)
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.json"), data, 0o644); err != nil {
t.Fatalf("write manifest: %v", err)
}
}
// writePluginWithSkill creates a plugin with a skill file.
func writePluginWithSkill(t *testing.T, dir, pluginName, skillName, skillContent string) {
t.Helper()
pluginDir := filepath.Join(dir, pluginName)
skillsDir := filepath.Join(pluginDir, "skills")
_ = os.MkdirAll(skillsDir, 0o755)
m := Manifest{
Name: pluginName,
Version: "1.0.0",
Capabilities: Capabilities{
Skills: []string{"skills/*.md"},
},
}
data, _ := marshalManifest(m)
_ = os.WriteFile(filepath.Join(pluginDir, "plugin.json"), data, 0o644)
_ = os.WriteFile(filepath.Join(skillsDir, skillName+".md"), []byte(skillContent), 0o644)
}
func marshalManifest(m Manifest) ([]byte, error) {
return marshalJSON(m)
}
func TestLoader_Discover_Empty(t *testing.T) {
dir := t.TempDir()
loader := NewLoader(testLogger())
plugins, err := loader.Discover(filepath.Join(dir, "global"), filepath.Join(dir, "project"))
if err != nil {
t.Fatalf("Discover: %v", err)
}
if len(plugins) != 0 {
t.Errorf("expected 0 plugins, got %d", len(plugins))
}
}
func TestLoader_Discover_GlobalPlugin(t *testing.T) {
dir := t.TempDir()
globalDir := filepath.Join(dir, "global")
writePlugin(t, globalDir, "git-tools", "1.0.0", nil)
loader := NewLoader(testLogger())
plugins, err := loader.Discover(globalDir, filepath.Join(dir, "project"))
if err != nil {
t.Fatalf("Discover: %v", err)
}
if len(plugins) != 1 {
t.Fatalf("expected 1 plugin, got %d", len(plugins))
}
if plugins[0].Manifest.Name != "git-tools" {
t.Errorf("Name = %q, want %q", plugins[0].Manifest.Name, "git-tools")
}
if plugins[0].Scope != "user" {
t.Errorf("Scope = %q, want %q", plugins[0].Scope, "user")
}
}
func TestLoader_Discover_ProjectOverridesGlobal(t *testing.T) {
dir := t.TempDir()
globalDir := filepath.Join(dir, "global")
projectDir := filepath.Join(dir, "project")
writePlugin(t, globalDir, "shared", "1.0.0", nil)
writePlugin(t, projectDir, "shared", "2.0.0", nil)
loader := NewLoader(testLogger())
plugins, err := loader.Discover(globalDir, projectDir)
if err != nil {
t.Fatalf("Discover: %v", err)
}
if len(plugins) != 1 {
t.Fatalf("expected 1 plugin (deduplicated), got %d", len(plugins))
}
if plugins[0].Manifest.Version != "2.0.0" {
t.Errorf("Version = %q, want %q (project should override global)", plugins[0].Manifest.Version, "2.0.0")
}
if plugins[0].Scope != "project" {
t.Errorf("Scope = %q, want %q", plugins[0].Scope, "project")
}
}
func TestLoader_Discover_SkipsInvalidManifest(t *testing.T) {
dir := t.TempDir()
globalDir := filepath.Join(dir, "global")
// Write a valid plugin.
writePlugin(t, globalDir, "good", "1.0.0", nil)
// Write an invalid plugin (bad JSON).
badDir := filepath.Join(globalDir, "bad")
_ = os.MkdirAll(badDir, 0o755)
_ = os.WriteFile(filepath.Join(badDir, "plugin.json"), []byte(`{invalid`), 0o644)
loader := NewLoader(testLogger())
plugins, err := loader.Discover(globalDir, filepath.Join(dir, "project"))
if err != nil {
t.Fatalf("Discover: %v", err)
}
if len(plugins) != 1 {
t.Fatalf("expected 1 plugin (skipping invalid), got %d", len(plugins))
}
if plugins[0].Manifest.Name != "good" {
t.Errorf("Name = %q, want %q", plugins[0].Manifest.Name, "good")
}
}
func TestLoader_Load_AllEnabled(t *testing.T) {
dir := t.TempDir()
globalDir := filepath.Join(dir, "global")
writePluginWithSkill(t, globalDir, "test-plugin", "my-skill", "---\nname: my-skill\n---\nHello")
loader := NewLoader(testLogger())
plugins, _ := loader.Discover(globalDir, filepath.Join(dir, "project"))
enabledSet := map[string]bool{"test-plugin": true}
result, err := loader.Load(plugins, enabledSet, nil)
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(result.Skills) != 1 {
t.Fatalf("expected 1 skill source, got %d", len(result.Skills))
}
if result.Skills[0].Source != "plugin:test-plugin" {
t.Errorf("Source = %q, want %q", result.Skills[0].Source, "plugin:test-plugin")
}
}
func TestLoader_Load_DisabledPlugin(t *testing.T) {
dir := t.TempDir()
globalDir := filepath.Join(dir, "global")
writePluginWithSkill(t, globalDir, "disabled-plugin", "skill", "---\nname: skill\n---\nHi")
loader := NewLoader(testLogger())
plugins, _ := loader.Discover(globalDir, filepath.Join(dir, "project"))
// Plugin not in enabled set.
result, err := loader.Load(plugins, map[string]bool{}, nil)
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(result.Skills) != 0 {
t.Errorf("expected 0 skills for disabled plugin, got %d", len(result.Skills))
}
}
func TestLoader_Load_HooksConverted(t *testing.T) {
dir := t.TempDir()
globalDir := filepath.Join(dir, "global")
caps := &Capabilities{
Hooks: []HookSpec{
{
Name: "check",
Event: "pre_tool_use",
Type: "command",
Exec: "scripts/check.sh",
ToolPattern: "bash*",
},
},
}
writePlugin(t, globalDir, "hook-plugin", "1.0.0", caps)
loader := NewLoader(testLogger())
plugins, _ := loader.Discover(globalDir, filepath.Join(dir, "project"))
result, err := loader.Load(plugins, map[string]bool{"hook-plugin": true}, nil)
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(result.Hooks) != 1 {
t.Fatalf("expected 1 hook, got %d", len(result.Hooks))
}
h := result.Hooks[0]
if h.Name != "check" {
t.Errorf("Hook name = %q", h.Name)
}
if h.Event != "pre_tool_use" {
t.Errorf("Hook event = %q", h.Event)
}
// Exec should be resolved to absolute path under plugin dir.
pluginDir := filepath.Join(globalDir, "hook-plugin")
wantExec := filepath.Join(pluginDir, "scripts/check.sh")
if h.Exec != wantExec {
t.Errorf("Hook exec = %q, want %q", h.Exec, wantExec)
}
}
func TestLoader_Load_MCPServersConverted(t *testing.T) {
dir := t.TempDir()
globalDir := filepath.Join(dir, "global")
caps := &Capabilities{
MCPServers: []MCPServerSpec{
{
Name: "git",
Command: "bin/mcp-git",
Args: []string{"--verbose"},
},
},
}
writePlugin(t, globalDir, "mcp-plugin", "1.0.0", caps)
loader := NewLoader(testLogger())
plugins, _ := loader.Discover(globalDir, filepath.Join(dir, "project"))
result, err := loader.Load(plugins, map[string]bool{"mcp-plugin": true}, nil)
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(result.MCPServers) != 1 {
t.Fatalf("expected 1 MCP server, got %d", len(result.MCPServers))
}
s := result.MCPServers[0]
if s.Name != "git" {
t.Errorf("Name = %q", s.Name)
}
// Command should be absolute path.
pluginDir := filepath.Join(globalDir, "mcp-plugin")
wantCmd := filepath.Join(pluginDir, "bin/mcp-git")
if s.Command != wantCmd {
t.Errorf("Command = %q, want %q", s.Command, wantCmd)
}
}
func TestLoader_Load_TOFU_RecordsPinOnFirstLoad(t *testing.T) {
dir := t.TempDir()
globalDir := filepath.Join(dir, "global")
writePlugin(t, globalDir, "newbie", "1.0.0", nil)
loader := NewLoader(testLogger())
plugins, _ := loader.Discover(globalDir, filepath.Join(dir, "project"))
pins, _ := NewFilePinStore(filepath.Join(dir, "pins.toml"))
result, err := loader.Load(plugins, map[string]bool{"newbie": true}, pins)
if err != nil {
t.Fatalf("Load: %v", err)
}
// Plugin declares no capabilities, but TOFU must still record a pin.
if n := len(result.Skills) + len(result.Hooks) + len(result.MCPServers); n != 0 {
t.Errorf("plugin with no capabilities produced %d entries", n)
}
if _, ok := pins.Get("newbie"); !ok {
t.Error("TOFU did not record a pin for the new plugin")
}
}
func TestLoader_Load_MatchingPin_LoadsSilently(t *testing.T) {
dir := t.TempDir()
globalDir := filepath.Join(dir, "global")
caps := &Capabilities{Hooks: []HookSpec{{Name: "h", Event: "pre_tool_use", Type: "command", Exec: "h.sh"}}}
writePlugin(t, globalDir, "trusted", "1.0.0", caps)
loader := NewLoader(testLogger())
plugins, _ := loader.Discover(globalDir, filepath.Join(dir, "project"))
// First load enrols.
pins, _ := NewFilePinStore(filepath.Join(dir, "pins.toml"))
if _, err := loader.Load(plugins, map[string]bool{"trusted": true}, pins); err != nil {
t.Fatal(err)
}
// Second load with the same manifest must produce the same capability set.
result, err := loader.Load(plugins, map[string]bool{"trusted": true}, pins)
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(result.Hooks) != 1 {
t.Fatalf("expected hook to be loaded on matching pin, got %d", len(result.Hooks))
}
}
func TestLoader_Load_PinMismatch_RefusesPlugin(t *testing.T) {
dir := t.TempDir()
globalDir := filepath.Join(dir, "global")
caps := &Capabilities{Hooks: []HookSpec{{Name: "h", Event: "pre_tool_use", Type: "command", Exec: "h.sh"}}}
writePlugin(t, globalDir, "drifted", "1.0.0", caps)
loader := NewLoader(testLogger())
plugins, _ := loader.Discover(globalDir, filepath.Join(dir, "project"))
// Seed a pin store with the WRONG hash to simulate manifest drift.
pins, _ := NewFilePinStore(filepath.Join(dir, "pins.toml"))
if err := pins.Set("drifted", "deadbeef"); err != nil {
t.Fatal(err)
}
result, err := loader.Load(plugins, map[string]bool{"drifted": true}, pins)
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(result.Hooks) != 0 {
t.Errorf("plugin with mismatched pin must not contribute hooks, got %d", len(result.Hooks))
}
// The bad pin must be left in place so the user can review and decide.
if h, _ := pins.Get("drifted"); h != "deadbeef" {
t.Errorf("loader silently overwrote a mismatched pin: got %q", h)
}
}