P1 (part 1): move skilltools core -> tool/ (clean, verbatim)
executus CI / test (push) Successful in 36s
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>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user