Files
Maven/internal/tool/tool_test.go
T
kami 6b80fd0c0f tools: add scope column for capability model
Add a 'scope' TEXT column (default 'homelab') to the tools table so tools
can be namespaced by scope (e.g. "homelab:restart", "datacenter:reboot").
Backward-compat: bare name defaults to "homelab" scope.

Changes:
- Migration #1: ALTER TABLE tools ADD COLUMN scope
- store.Tool: add Scope field, update all SQL and scanTool()
- ipc.Tool DTO and request types: add Scope field
- CoreAPI interface: pass scope in ProposeTool/EnableTool
- storeAPI adapters: forward scope
- cmd/mavend/voice: pass scope (empty → homelab)
- cmd/mavweb/tools: show scope column in UI tables, hidden fields
- All tests updated for scope field
- Migration test made dynamic (startVer = len(migrations))
2026-07-05 11:40:15 +04:00

88 lines
3.0 KiB
Go

package tool
import (
"context"
"errors"
"reflect"
"testing"
"time"
"github.com/kami/maven/internal/ipc"
)
// fakeAPI — an in-memory tool store for the executor/matcher tests.
type fakeAPI struct{ tools map[string]ipc.Tool }
func (f fakeAPI) LookupTool(_ context.Context, name string) (ipc.Tool, error) {
t, ok := f.tools[name]
if !ok {
return ipc.Tool{}, ipc.ErrToolNotFound
}
return t, nil
}
func (f fakeAPI) ListTools(_ context.Context, status string) ([]ipc.Tool, error) {
var out []ipc.Tool
for _, t := range f.tools {
if status == "" || t.Status == status {
out = append(out, t)
}
}
return out, nil
}
func (f fakeAPI) ProposeTool(_ context.Context, _, _, _ string, _ time.Time) (bool, error) {
return true, nil
}
// TestExec covers the allowlist boundary: enabled runs, unknown/proposed refuse,
// destructive needs confirm, and args land as argv (no shell) after the prefix.
func TestExec(t *testing.T) {
api := fakeAPI{tools: map[string]ipc.Tool{
"restart": {Name: "restart", Scope: "homelab", Cmd: []string{"systemctl", "restart"}, Status: "enabled"},
"drop": {Name: "drop", Scope: "homelab", Cmd: []string{"dropdb"}, Destructive: true, Status: "enabled"},
"draft": {Name: "draft", Scope: "homelab", Cmd: []string{"x"}, Status: "proposed"},
}}
var gotArgv []string
e := NewExecutor(api, 0)
e.run = func(_ context.Context, argv []string) (string, error) { gotArgv = argv; return "ok", nil }
// enabled → runs, args appended to the fixed prefix as argv.
out, err := e.Exec(context.Background(), "restart", []string{"nginx; rm -rf /"}, false)
if err != nil || out != "ok" {
t.Fatalf("enabled: out=%q err=%v", out, err)
}
want := []string{"systemctl", "restart", "nginx; rm -rf /"}
if !reflect.DeepEqual(gotArgv, want) {
t.Fatalf("argv=%v want %v (injection must stay one argv element)", gotArgv, want)
}
// unknown → refuse.
if _, err := e.Exec(context.Background(), "nope", nil, false); !errors.Is(err, ErrNotEnabled) {
t.Fatalf("unknown: err=%v want ErrNotEnabled", err)
}
// proposed (not enabled) → refuse.
if _, err := e.Exec(context.Background(), "draft", nil, false); !errors.Is(err, ErrNotEnabled) {
t.Fatalf("proposed: err=%v want ErrNotEnabled", err)
}
// destructive unconfirmed → needs confirm; confirmed → runs.
if _, err := e.Exec(context.Background(), "drop", nil, false); !errors.Is(err, ErrNeedsConfirm) {
t.Fatalf("destructive: err=%v want ErrNeedsConfirm", err)
}
if _, err := e.Exec(context.Background(), "drop", []string{"db"}, true); err != nil {
t.Fatalf("destructive confirmed: err=%v", err)
}
if want := []string{"dropdb", "db"}; !reflect.DeepEqual(gotArgv, want) {
t.Fatalf("confirmed argv=%v want %v", gotArgv, want)
}
// matcher allowlist = enabled names only (proposed excluded).
m := NewMatcher(api)
fn, args, ok := m.Match("restart nginx")
if !ok || fn != "restart" || !reflect.DeepEqual(args, []string{"nginx"}) {
t.Fatalf("match: fn=%q args=%v ok=%v", fn, args, ok)
}
if _, _, ok := m.Match("draft something"); ok {
t.Fatal("proposed tool must not match (not enabled)")
}
}