Lands the executable half of the Phase 01-06 contracts: stdio MCP wiring with bundle policy enforced at call time, JWT/api_key upstream auth with single-flight renewal and 401 retry, strict argument decoding, allowlisted result projections with enum label twins, TV season expansion, settings read-modify-write under a revision lock, and a sanitized ToolError envelope that never forwards raw upstream bodies.
113 lines
2.8 KiB
Go
113 lines
2.8 KiB
Go
package tools
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
func TestRegistryHas31UniqueTools(t *testing.T) {
|
|
reg := Registry()
|
|
if len(reg) != 31 {
|
|
t.Fatalf("registry has %d tools, want 31", len(reg))
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, d := range reg {
|
|
if seen[d.Name] {
|
|
t.Errorf("duplicate tool %q", d.Name)
|
|
}
|
|
seen[d.Name] = true
|
|
if d.Handler == nil {
|
|
t.Errorf("tool %q has no handler", d.Name)
|
|
}
|
|
if d.Bundle != "core" && d.Bundle != "moderation" && d.Bundle != "administration" {
|
|
t.Errorf("tool %q has unknown bundle %q", d.Name, d.Bundle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBundleCounts(t *testing.T) {
|
|
counts := map[string]int{}
|
|
for _, d := range Registry() {
|
|
counts[d.Bundle]++
|
|
}
|
|
if counts["core"] != 18 || counts["moderation"] != 5 || counts["administration"] != 8 {
|
|
t.Errorf("bundle counts = %v, want core=18 moderation=5 administration=8", counts)
|
|
}
|
|
}
|
|
|
|
func TestEveryToolHasInputSchema(t *testing.T) {
|
|
for _, d := range Registry() {
|
|
s, err := d.InputSchema()
|
|
if err != nil {
|
|
t.Errorf("%s: %v", d.Name, err)
|
|
continue
|
|
}
|
|
m, ok := s.(map[string]any)
|
|
if !ok || m["type"] != "object" {
|
|
t.Errorf("%s: input schema is not a JSON object schema", d.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEveryToolHasComposedOutputSchema(t *testing.T) {
|
|
for _, d := range Registry() {
|
|
s, err := d.OutputSchema()
|
|
if err != nil {
|
|
t.Errorf("%s: %v", d.Name, err)
|
|
continue
|
|
}
|
|
m := s.(map[string]any)
|
|
props := m["properties"].(map[string]any)
|
|
if props["operation"].(map[string]any)["const"] != d.Name {
|
|
t.Errorf("%s: operation const mismatch", d.Name)
|
|
}
|
|
defs := m["$defs"].(map[string]any)
|
|
if _, ok := defs["error"]; !ok {
|
|
t.Errorf("%s: output schema lacks error def", d.Name)
|
|
}
|
|
for _, f := range d.Families {
|
|
if _, ok := defs[f]; !ok {
|
|
t.Errorf("%s: output schema lacks family def %q", d.Name, f)
|
|
}
|
|
}
|
|
// Every #/$defs ref inside the composed schema must resolve.
|
|
for _, ref := range findRefs(m) {
|
|
if _, ok := defs[ref]; !ok {
|
|
t.Errorf("%s: unresolved $defs ref %q", d.Name, ref)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnabledFiltering(t *testing.T) {
|
|
coreOnly := map[string]bool{"core": true}
|
|
n := 0
|
|
for _, d := range Registry() {
|
|
if d.Enabled(coreOnly) {
|
|
n++
|
|
}
|
|
}
|
|
if n != 18 {
|
|
t.Errorf("core-only registry = %d tools, want 18", n)
|
|
}
|
|
}
|
|
|
|
// TestToolResultEnvelopeJSON verifies the wire field names.
|
|
func TestToolResultEnvelopeJSON(t *testing.T) {
|
|
res := &ToolResult{OK: true, Operation: "read_search",
|
|
Warnings: []string{}, CorrelationID: "abc"}
|
|
raw, err := json.Marshal(res)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m map[string]any
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, k := range []string{"ok", "operation", "warnings", "truncated", "correlation_id"} {
|
|
if _, ok := m[k]; !ok {
|
|
t.Errorf("envelope missing key %q", k)
|
|
}
|
|
}
|
|
}
|