- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
205 lines
5.2 KiB
Go
205 lines
5.2 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"testing"
|
|
|
|
"switchboard-core/store"
|
|
)
|
|
|
|
// TestMain registers late-registered tools (notes) with nil deps so
|
|
// that Definition()-level tests work. Execute() tests need real deps.
|
|
func TestMain(m *testing.M) {
|
|
RegisterNoteTools(store.Stores{}, nil)
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
// ── Registry Tests ──────────────────────────
|
|
|
|
func TestRegistryHasTools(t *testing.T) {
|
|
// TestMain registers note tools via RegisterNoteTools
|
|
if !HasTools() {
|
|
t.Fatal("Expected HasTools() == true after init registration")
|
|
}
|
|
}
|
|
|
|
func TestRegistryAllDefinitions(t *testing.T) {
|
|
defs := AllDefinitions()
|
|
if len(defs) < 4 {
|
|
t.Errorf("Expected at least 4 tool definitions, got %d", len(defs))
|
|
}
|
|
|
|
expected := map[string]bool{
|
|
"note_create": false,
|
|
"note_search": false,
|
|
"note_update": false,
|
|
"note_list": false,
|
|
}
|
|
for _, d := range defs {
|
|
if _, ok := expected[d.Name]; ok {
|
|
expected[d.Name] = true
|
|
}
|
|
// Every tool must have a description and parameters
|
|
if d.Description == "" {
|
|
t.Errorf("Tool %q has empty description", d.Name)
|
|
}
|
|
if len(d.Parameters) == 0 {
|
|
t.Errorf("Tool %q has empty parameters", d.Name)
|
|
}
|
|
}
|
|
for name, found := range expected {
|
|
if !found {
|
|
t.Errorf("Expected tool %q not found in registry", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegistryGetKnown(t *testing.T) {
|
|
tool := Get("note_create")
|
|
if tool == nil {
|
|
t.Fatal("Get(note_create) returned nil")
|
|
}
|
|
def := tool.Definition()
|
|
if def.Name != "note_create" {
|
|
t.Errorf("Expected name 'note_create', got %q", def.Name)
|
|
}
|
|
}
|
|
|
|
func TestRegistryGetUnknown(t *testing.T) {
|
|
tool := Get("nonexistent_tool")
|
|
if tool != nil {
|
|
t.Error("Get(nonexistent_tool) should return nil")
|
|
}
|
|
}
|
|
|
|
func TestExecuteCallUnknownTool(t *testing.T) {
|
|
ctx := context.Background()
|
|
execCtx := ExecutionContext{UserID: "test", ChannelID: "test"}
|
|
call := ToolCall{ID: "call_1", Name: "nonexistent", Arguments: "{}"}
|
|
|
|
result := ExecuteCall(ctx, execCtx, call)
|
|
|
|
if !result.IsError {
|
|
t.Error("Expected is_error=true for unknown tool")
|
|
}
|
|
if result.ToolCallID != "call_1" {
|
|
t.Errorf("Expected tool_call_id='call_1', got %q", result.ToolCallID)
|
|
}
|
|
}
|
|
|
|
func TestExecuteAll(t *testing.T) {
|
|
ctx := context.Background()
|
|
execCtx := ExecutionContext{UserID: "test", ChannelID: "test"}
|
|
calls := []ToolCall{
|
|
{ID: "call_1", Name: "nonexistent_a", Arguments: "{}"},
|
|
{ID: "call_2", Name: "nonexistent_b", Arguments: "{}"},
|
|
}
|
|
|
|
results := ExecuteAll(ctx, execCtx, calls)
|
|
|
|
if len(results) != 2 {
|
|
t.Fatalf("Expected 2 results, got %d", len(results))
|
|
}
|
|
for i, r := range results {
|
|
if !r.IsError {
|
|
t.Errorf("Result %d: expected is_error=true", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Tool Definition Schema Tests ────────────
|
|
|
|
func TestToolDefinitionsHaveValidJSON(t *testing.T) {
|
|
for _, def := range AllDefinitions() {
|
|
var schema map[string]interface{}
|
|
if err := json.Unmarshal(def.Parameters, &schema); err != nil {
|
|
t.Errorf("Tool %q has invalid JSON schema: %v", def.Name, err)
|
|
}
|
|
// Should be a JSON Schema object with "type" and "properties"
|
|
if schema["type"] != "object" {
|
|
t.Errorf("Tool %q schema type should be 'object', got %v", def.Name, schema["type"])
|
|
}
|
|
if _, ok := schema["properties"]; !ok {
|
|
t.Errorf("Tool %q schema missing 'properties'", def.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── JSON Schema Helper Tests ────────────────
|
|
|
|
func TestPropString(t *testing.T) {
|
|
p := Prop("string", "a description")
|
|
|
|
if p["type"] != "string" {
|
|
t.Errorf("Expected type=string, got %v", p["type"])
|
|
}
|
|
if p["description"] != "a description" {
|
|
t.Errorf("Expected description, got %v", p["description"])
|
|
}
|
|
}
|
|
|
|
func TestPropEnum(t *testing.T) {
|
|
p := PropEnum("content mode", "replace", "append", "prepend")
|
|
|
|
if p["type"] != "string" {
|
|
t.Error("Expected type=string")
|
|
}
|
|
enums := p["enum"].([]string)
|
|
if len(enums) != 3 {
|
|
t.Errorf("Expected 3 enum values, got %d", len(enums))
|
|
}
|
|
}
|
|
|
|
func TestPropArray(t *testing.T) {
|
|
p := PropArray("list of tags")
|
|
|
|
if p["type"] != "array" {
|
|
t.Error("Expected type=array")
|
|
}
|
|
items := p["items"].(map[string]string)
|
|
if items["type"] != "string" {
|
|
t.Error("Expected items.type=string")
|
|
}
|
|
}
|
|
|
|
func TestJSONSchema(t *testing.T) {
|
|
props := map[string]interface{}{
|
|
"title": Prop("string", "note title"),
|
|
"content": Prop("string", "note body"),
|
|
}
|
|
schema := JSONSchema(props, []string{"title"})
|
|
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal(schema, &m); err != nil {
|
|
t.Fatalf("Invalid JSON: %v", err)
|
|
}
|
|
|
|
if m["type"] != "object" {
|
|
t.Error("Expected type=object")
|
|
}
|
|
required := m["required"].([]interface{})
|
|
if len(required) != 1 || required[0] != "title" {
|
|
t.Errorf("Expected required=[title], got %v", required)
|
|
}
|
|
mProps := m["properties"].(map[string]interface{})
|
|
if _, ok := mProps["title"]; !ok {
|
|
t.Error("Missing property 'title'")
|
|
}
|
|
if _, ok := mProps["content"]; !ok {
|
|
t.Error("Missing property 'content'")
|
|
}
|
|
}
|
|
|
|
func TestMustJSON(t *testing.T) {
|
|
result := MustJSON(map[string]string{"key": "value"})
|
|
var m map[string]string
|
|
if err := json.Unmarshal(result, &m); err != nil {
|
|
t.Fatalf("MustJSON produced invalid JSON: %v", err)
|
|
}
|
|
if m["key"] != "value" {
|
|
t.Errorf("Expected value, got %q", m["key"])
|
|
}
|
|
}
|