Files
executus/tool/argcoerce_test.go
steve dc28b63ad8
executus CI / test (push) Successful in 36s
P1 (part 1): move skilltools core -> tool/ (clean, verbatim)
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>
2026-06-26 19:31:47 -04:00

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)
}
}