package tools import ( "context" "testing" ) // ── Predicate Tests ───────────────────────── func TestAlwaysAvailable(t *testing.T) { cases := []ToolContext{ {}, {ChannelType: "direct"}, {IsVisitor: true}, {WorkspaceID: "ws-1", WorkflowID: "wf-1", TeamID: "t-1"}, } for i, tc := range cases { if !AlwaysAvailable(tc) { t.Errorf("case %d: AlwaysAvailable returned false", i) } } } func TestRequireWorkspace(t *testing.T) { if RequireWorkspace(ToolContext{}) { t.Error("Expected false when WorkspaceID is empty") } if !RequireWorkspace(ToolContext{WorkspaceID: "ws-123"}) { t.Error("Expected true when WorkspaceID is set") } } func TestRequireWorkflow(t *testing.T) { if RequireWorkflow(ToolContext{}) { t.Error("Expected false when WorkflowID is empty") } if !RequireWorkflow(ToolContext{WorkflowID: "wf-456"}) { t.Error("Expected true when WorkflowID is set") } } func TestRequireTeam(t *testing.T) { if RequireTeam(ToolContext{}) { t.Error("Expected false when TeamID is empty") } if !RequireTeam(ToolContext{TeamID: "team-1"}) { t.Error("Expected true when TeamID is set") } } func TestDenyVisitor(t *testing.T) { if !DenyVisitor(ToolContext{IsVisitor: false}) { t.Error("Expected true for authenticated user") } if DenyVisitor(ToolContext{IsVisitor: true}) { t.Error("Expected false for visitor") } } func TestAll(t *testing.T) { wsAndNonVisitor := All(RequireWorkspace, DenyVisitor) cases := []struct { name string tc ToolContext expect bool }{ {"both satisfied", ToolContext{WorkspaceID: "ws-1", IsVisitor: false}, true}, {"no workspace", ToolContext{WorkspaceID: "", IsVisitor: false}, false}, {"is visitor", ToolContext{WorkspaceID: "ws-1", IsVisitor: true}, false}, {"neither satisfied", ToolContext{WorkspaceID: "", IsVisitor: true}, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := wsAndNonVisitor(tc.tc); got != tc.expect { t.Errorf("got %v, want %v", got, tc.expect) } }) } } func TestAllEmpty(t *testing.T) { // All() with no predicates should pass (vacuous truth) pred := All() if !pred(ToolContext{}) { t.Error("All() with no predicates should return true") } } func TestAny(t *testing.T) { wsOrWorkflow := Any(RequireWorkspace, RequireWorkflow) cases := []struct { name string tc ToolContext expect bool }{ {"workspace only", ToolContext{WorkspaceID: "ws-1"}, true}, {"workflow only", ToolContext{WorkflowID: "wf-1"}, true}, {"both", ToolContext{WorkspaceID: "ws-1", WorkflowID: "wf-1"}, true}, {"neither", ToolContext{}, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := wsOrWorkflow(tc.tc); got != tc.expect { t.Errorf("got %v, want %v", got, tc.expect) } }) } } func TestAnyEmpty(t *testing.T) { // Any() with no predicates should fail (no match) pred := Any() if pred(ToolContext{}) { t.Error("Any() with no predicates should return false") } } func TestNot(t *testing.T) { notVisitor := Not(func(tc ToolContext) bool { return tc.IsVisitor }) if !notVisitor(ToolContext{IsVisitor: false}) { t.Error("Not(visitor) should return true for non-visitor") } if notVisitor(ToolContext{IsVisitor: true}) { t.Error("Not(visitor) should return false for visitor") } } func TestComposedPredicates(t *testing.T) { // workspace_create: available when NO workspace AND not visitor workspaceCreate := All(Not(RequireWorkspace), DenyVisitor) cases := []struct { name string tc ToolContext expect bool }{ {"no ws, authenticated", ToolContext{}, true}, {"has ws, authenticated", ToolContext{WorkspaceID: "ws-1"}, false}, {"no ws, visitor", ToolContext{IsVisitor: true}, false}, {"has ws, visitor", ToolContext{WorkspaceID: "ws-1", IsVisitor: true}, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := workspaceCreate(tc.tc); got != tc.expect { t.Errorf("got %v, want %v", got, tc.expect) } }) } } // ── BaseTool Tests ────────────────────────── type testToolWithBase struct { BaseTool } func (t *testToolWithBase) Definition() ToolDef { return ToolDef{ Name: "always_tool", Description: "test tool with BaseTool embed", Parameters: MustJSON(map[string]interface{}{"type": "object", "properties": map[string]interface{}{}}), } } func (t *testToolWithBase) Execute(_ context.Context, _ ExecutionContext, _ string) (string, error) { return `{"ok":true}`, nil } func TestBaseToolSatisfiesContextualTool(t *testing.T) { tool := &testToolWithBase{} // Must satisfy ContextualTool ct, ok := interface{}(tool).(ContextualTool) if !ok { t.Fatal("testToolWithBase should satisfy ContextualTool interface") } // BaseTool.Availability() should return AlwaysAvailable if !ct.Availability()(ToolContext{IsVisitor: true}) { t.Error("BaseTool.Availability() should be AlwaysAvailable") } } // ── AvailableFor Tests ────────────────────── // testContextTool overrides availability with a custom predicate. type testContextTool struct { BaseTool name string req Require } func (t *testContextTool) Definition() ToolDef { return ToolDef{ Name: t.name, Description: "test contextual tool: " + t.name, Parameters: MustJSON(map[string]interface{}{"type": "object", "properties": map[string]interface{}{}}), } } func (t *testContextTool) Execute(_ context.Context, _ ExecutionContext, _ string) (string, error) { return `{"ok":true}`, nil } func (t *testContextTool) Availability() Require { return t.req } // testPlainTool is a tool that does NOT implement ContextualTool — // only the base Tool interface. It should always be included. type testPlainTool struct { name string } func (t *testPlainTool) Definition() ToolDef { return ToolDef{ Name: t.name, Description: "plain tool: " + t.name, Parameters: MustJSON(map[string]interface{}{"type": "object", "properties": map[string]interface{}{}}), } } func (t *testPlainTool) Execute(_ context.Context, _ ExecutionContext, _ string) (string, error) { return `{"ok":true}`, nil } func TestAvailableFor(t *testing.T) { // Save and restore global registry origRegistry := registry defer func() { registry = origRegistry }() registry = map[string]Tool{ "always_tool": &testToolWithBase{}, "ws_tool": &testContextTool{name: "ws_tool", req: RequireWorkspace}, "visitor_denied": &testContextTool{name: "visitor_denied", req: DenyVisitor}, "ws_no_visitor": &testContextTool{name: "ws_no_visitor", req: All(RequireWorkspace, DenyVisitor)}, "plain_tool": &testPlainTool{name: "plain_tool"}, } cases := []struct { name string tctx ToolContext disabled map[string]bool expect []string // expected tool names (sorted doesn't matter, just set membership) }{ { name: "empty context — only always + plain", tctx: ToolContext{}, expect: []string{"always_tool", "visitor_denied", "plain_tool"}, }, { name: "workspace context — ws tools available", tctx: ToolContext{WorkspaceID: "ws-1"}, expect: []string{"always_tool", "ws_tool", "visitor_denied", "ws_no_visitor", "plain_tool"}, }, { name: "visitor with workspace — visitor_denied and ws_no_visitor excluded", tctx: ToolContext{WorkspaceID: "ws-1", IsVisitor: true}, expect: []string{"always_tool", "ws_tool", "plain_tool"}, }, { name: "disabled tool excluded", tctx: ToolContext{WorkspaceID: "ws-1"}, disabled: map[string]bool{"ws_tool": true}, expect: []string{"always_tool", "visitor_denied", "ws_no_visitor", "plain_tool"}, }, { name: "visitor no workspace — minimal set", tctx: ToolContext{IsVisitor: true}, expect: []string{"always_tool", "plain_tool"}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { disabled := tc.disabled if disabled == nil { disabled = map[string]bool{} } defs := AvailableFor(tc.tctx, disabled) got := make(map[string]bool, len(defs)) for _, d := range defs { got[d.Name] = true } // Check expected tools are present for _, name := range tc.expect { if !got[name] { t.Errorf("expected tool %q not found in result", name) } } // Check no unexpected tools expect := make(map[string]bool, len(tc.expect)) for _, name := range tc.expect { expect[name] = true } for name := range got { if !expect[name] { t.Errorf("unexpected tool %q in result", name) } } }) } } func TestAvailableForBackwardCompat(t *testing.T) { // AvailableFor with empty ToolContext should behave like // the old AllDefinitionsFiltered for plain tools origRegistry := registry defer func() { registry = origRegistry }() registry = map[string]Tool{ "tool_a": &testPlainTool{name: "tool_a"}, "tool_b": &testPlainTool{name: "tool_b"}, "tool_c": &testPlainTool{name: "tool_c"}, } // No disabled, empty context — all plain tools should appear defs := AvailableFor(ToolContext{}, map[string]bool{}) if len(defs) != 3 { t.Errorf("Expected 3 tools, got %d", len(defs)) } // Disable one defs = AvailableFor(ToolContext{}, map[string]bool{"tool_b": true}) if len(defs) != 2 { t.Errorf("Expected 2 tools, got %d", len(defs)) } for _, d := range defs { if d.Name == "tool_b" { t.Error("tool_b should be filtered out") } } } // ── ToolContext from ExecutionContext ──────── func TestToolContextFromExecContext(t *testing.T) { // Verify the fields map correctly between the two structs. // This is a compile-time/structural test — when ToolContext is // built from ExecutionContext in the completion handler, these // fields must align. exec := ExecutionContext{ UserID: "u-1", ChannelID: "ch-1", PersonaID: "p-1", WorkspaceID: "ws-1", WorkflowID: "wf-1", TeamID: "t-1", } // Simulate what the completion handler will do tc := ToolContext{ WorkspaceID: exec.WorkspaceID, WorkflowID: exec.WorkflowID, TeamID: exec.TeamID, PersonaID: exec.PersonaID, // ChannelType and IsVisitor come from channel record / gin context } if tc.WorkspaceID != "ws-1" { t.Errorf("WorkspaceID mismatch: %q", tc.WorkspaceID) } if tc.WorkflowID != "wf-1" { t.Errorf("WorkflowID mismatch: %q", tc.WorkflowID) } if tc.TeamID != "t-1" { t.Errorf("TeamID mismatch: %q", tc.TeamID) } if tc.PersonaID != "p-1" { t.Errorf("PersonaID mismatch: %q", tc.PersonaID) } }