package agent_test import ( "strings" "testing" "github.com/smallnest/pigo/agent" ) // hermetic points provider/skill/plugin discovery at throwaway dirs and supplies // a dummy key so New resolves fully without ever contacting a network. None of // these tests call Prompt/Stream, so no real request is made. func hermetic(t *testing.T) { t.Helper() t.Setenv("OPENROUTER_API_KEY", "test-key") t.Setenv("PIGO_HOME", t.TempDir()) } func contains(set []string, name string) bool { for _, n := range set { if n == name { return true } } return false } // TestNewDefaults is the zero-config path: the full built-in tool set is // advertised and the model id resolves to the openrouter provider. func TestNewDefaults(t *testing.T) { hermetic(t) sess, err := agent.New(agent.WithModel("openrouter/free")) if err != nil { t.Fatalf("New: %v", err) } defer sess.Close() if got := sess.Model(); got != "openrouter/free" { t.Errorf("Model() = %q, want %q", got, "openrouter/free") } if got := sess.Provider(); got != "openrouter" { t.Errorf("Provider() = %q, want %q", got, "openrouter") } for _, want := range []string{"read", "write", "edit", "grep", "find", "bash", "task"} { if !contains(sess.ToolNames(), want) { t.Errorf("default tool set missing %q: %q", want, sess.ToolNames()) } } } // TestWithToolsAllowlist confirms an allowlist narrows the set to exactly the // named tools, in order. func TestWithToolsAllowlist(t *testing.T) { hermetic(t) sess, err := agent.New( agent.WithModel("openrouter/free"), agent.WithTools("read", "grep"), ) if err != nil { t.Fatalf("New: %v", err) } defer sess.Close() if got := strings.Join(sess.ToolNames(), ","); got != "read,grep" { t.Errorf("ToolNames() = %q, want %q", got, "read,grep") } } // TestDenyWinsOverAllow is the fail-closed guarantee at the SDK layer: a tool on // both lists is removed. func TestDenyWinsOverAllow(t *testing.T) { hermetic(t) sess, err := agent.New( agent.WithModel("openrouter/free"), agent.WithTools("read", "bash"), agent.WithDisallowedTools("bash"), ) if err != nil { t.Fatalf("New: %v", err) } defer sess.Close() if contains(sess.ToolNames(), "bash") { t.Errorf("bash was on both lists and must be removed: %q", sess.ToolNames()) } if !contains(sess.ToolNames(), "read") { t.Errorf("read was allowed and not denied, so must survive: %q", sess.ToolNames()) } } // TestWithoutTools yields an empty set — a pure text completion. func TestWithoutTools(t *testing.T) { hermetic(t) sess, err := agent.New( agent.WithModel("openrouter/free"), agent.WithoutTools(), ) if err != nil { t.Fatalf("New: %v", err) } defer sess.Close() if len(sess.ToolNames()) != 0 { t.Errorf("WithoutTools must leave no tools, got %q", sess.ToolNames()) } } // TestUnknownToolIsError confirms a misspelled tool name fails construction // rather than silently dropping the boundary. func TestUnknownToolIsError(t *testing.T) { hermetic(t) _, err := agent.New( agent.WithModel("openrouter/free"), agent.WithTools("raed"), ) if err == nil { t.Fatal("New = nil error, want a failure for the misspelled tool name") } } // TestInvalidThinkingLevelIsError confirms the level is validated up front. func TestInvalidThinkingLevelIsError(t *testing.T) { hermetic(t) _, err := agent.New( agent.WithModel("openrouter/free"), agent.WithThinkingLevel("supersonic"), ) if err == nil { t.Fatal("New = nil error, want a failure for the invalid thinking level") } } // TestValidThinkingLevels accepts every documented level. func TestValidThinkingLevels(t *testing.T) { hermetic(t) for _, level := range []string{"off", "minimal", "low", "medium", "high", "xhigh", "max"} { sess, err := agent.New( agent.WithModel("openrouter/free"), agent.WithThinkingLevel(level), ) if err != nil { t.Errorf("WithThinkingLevel(%q): %v", level, err) continue } sess.Close() } } // TestCloseHermetic confirms Close is a no-op (nil) when the session holds no // plugin manager or memory store, and is safe to call. func TestCloseHermetic(t *testing.T) { hermetic(t) sess, err := agent.New(agent.WithModel("openrouter/free"), agent.WithoutTools()) if err != nil { t.Fatalf("New: %v", err) } if err := sess.Close(); err != nil { t.Errorf("Close() = %v, want nil", err) } }