dc28b63ad8
executus CI / test (push) Successful in 36s
The tool registry core (registry, permission model, Invocation, gated-tool wrapper, ssrf guard, hmac, encryption, argcoerce, helpers, rootrun, session_tools, webhook_rate_limit) had zero mort coupling — it imports only majordomo/llm + x/crypto/hkdf — so it moves verbatim with a package rename (skilltools -> tool). All same-package tests came along and pass; the SSRF, gated-wrapper, encryption and output-pattern invariants are re-anchored here. majordomo re-enters the module graph (now pinned to the latest, incl. the front-loaded-output fix). model/ + llmmeta + structured follow next. Docs: CLAUDE.md now requires README/examples to stay in sync with changes in the same commit; CI skips docs/example-only pushes via paths-ignore. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
2.8 KiB
Go
84 lines
2.8 KiB
Go
package tool
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// coerceParams mirrors the field kinds legacy gollm's coercion supported.
|
|
type coerceParams struct {
|
|
Count int `json:"count"`
|
|
Ratio float64 `json:"ratio"`
|
|
Flag bool `json:"flag"`
|
|
Limit *int `json:"limit"`
|
|
Tags []int `json:"tags"`
|
|
Nested coerceIn `json:"nested"`
|
|
Verbose string `json:"verbose"`
|
|
}
|
|
|
|
type coerceIn struct {
|
|
Depth uint `json:"depth"`
|
|
}
|
|
|
|
// TestGatedTool_LenientArgCoercion anchors the legacy gollm-era tolerance the
|
|
// conversion preserved: numeric and boolean fields supplied as strings
|
|
// by the model ("3", "true") decode into the typed Args, recursing into
|
|
// pointers, slices, and nested structs. Models emit this shape
|
|
// constantly; losing the tolerance would break live tool traffic.
|
|
func TestGatedTool_LenientArgCoercion(t *testing.T) {
|
|
var seen coerceParams
|
|
tool := NewGatedTool[coerceParams](
|
|
"coerce_tool", "coercion test",
|
|
Permission{AuthoringRequirement: RequirementAnyone, SafeForShare: true},
|
|
func(ctx context.Context, inv Invocation, args coerceParams) (string, error) {
|
|
seen = args
|
|
return "ok", nil
|
|
},
|
|
)
|
|
|
|
out, err := buildAndExecute(t, tool, Invocation{SkillName: "x"}, VisibilityPrivate, nil,
|
|
`{"count":"3","ratio":" 2.5 ","flag":"true","limit":"7","tags":["1","2"],"nested":{"depth":"4"},"verbose":"yes"}`)
|
|
if err != nil || out != "ok" {
|
|
t.Fatalf("execute: out=%q err=%v", out, err)
|
|
}
|
|
if seen.Count != 3 || seen.Ratio != 2.5 || seen.Flag != true {
|
|
t.Fatalf("scalar coercion failed: %+v", seen)
|
|
}
|
|
if seen.Limit == nil || *seen.Limit != 7 {
|
|
t.Fatalf("pointer coercion failed: %+v", seen.Limit)
|
|
}
|
|
if len(seen.Tags) != 2 || seen.Tags[0] != 1 || seen.Tags[1] != 2 {
|
|
t.Fatalf("slice coercion failed: %+v", seen.Tags)
|
|
}
|
|
if seen.Nested.Depth != 4 {
|
|
t.Fatalf("nested coercion failed: %+v", seen.Nested)
|
|
}
|
|
if seen.Verbose != "yes" {
|
|
t.Fatalf("string field mangled: %q", seen.Verbose)
|
|
}
|
|
}
|
|
|
|
// TestGatedTool_StrictPathUnaffected confirms well-typed args take the
|
|
// zero-cost strict path and uncoercible strings still fail loudly.
|
|
func TestGatedTool_StrictPathUnaffected(t *testing.T) {
|
|
tool := NewGatedTool[coerceParams](
|
|
"coerce_strict_tool", "coercion test",
|
|
Permission{AuthoringRequirement: RequirementAnyone, SafeForShare: true},
|
|
func(ctx context.Context, inv Invocation, args coerceParams) (string, error) {
|
|
return "ok", nil
|
|
},
|
|
)
|
|
|
|
if out, err := buildAndExecute(t, tool, Invocation{SkillName: "x"}, VisibilityPrivate, nil,
|
|
`{"count":3,"ratio":2.5,"flag":true}`); err != nil || out != "ok" {
|
|
t.Fatalf("strict path: out=%q err=%v", out, err)
|
|
}
|
|
|
|
_, err := buildAndExecute(t, tool, Invocation{SkillName: "x"}, VisibilityPrivate, nil,
|
|
`{"count":"not-a-number"}`)
|
|
if err == nil || !strings.Contains(err.Error(), "invalid arguments") {
|
|
t.Fatalf("expected invalid-arguments error for uncoercible string, got %v", err)
|
|
}
|
|
}
|