mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
The previous commit stops list_task_credentials from building its schema
with obj(map[string]any{}, ""). Two gaps remained:
- obj still emitted whatever it was given, so the same typo on another tool
would again produce "required": [""]. obj now drops names that are blank
after strings.TrimSpace and omits the "required" key when nothing is left.
- Tool schemas are cached in the tools table and SeedTool is insert-only, so
the code fix only reached fresh installs. Existing databases kept the
malformed row and strict providers kept rejecting every worker run.
healListTaskCredentialsSchema runs at startup next to the other tool
seeds and removes the "required" key from that one row, only when it is
exactly [""]:
UPDATE tools SET schema = schema - 'required', updated_at=now()
WHERE key='list_task_credentials' AND schema->'required' = '[""]'::jsonb
It is idempotent (a healed row no longer matches), needs no settings flag,
and leaves the description, bindings, enabled flag and every other tool
untouched. tool_schema_refresh_v7 is not bumped.
Tests: TestObjDropsBlankRequired and
TestListTaskCredentialsSchemaHasNoBlankRequired (agent),
TestHealListTaskCredentialsSchema (server, Postgres).
126 lines
4.0 KiB
Go
126 lines
4.0 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
actool "github.com/Autumn-27/norma/tool"
|
|
)
|
|
|
|
// TestBuiltinToolSeeds ensures the catalog builds from a nil-store ToolSet without
|
|
// panicking, has stable keys, unions agent bindings, and carries real descriptions.
|
|
func TestBuiltinToolSeeds(t *testing.T) {
|
|
seeds := BuiltinToolSeeds()
|
|
if len(seeds) == 0 {
|
|
t.Fatal("no seeds")
|
|
}
|
|
byKey := map[string]ToolSeed{}
|
|
for _, s := range seeds {
|
|
if s.Key == "" || s.Desc == "" {
|
|
t.Errorf("seed %q missing key/desc", s.Key)
|
|
}
|
|
byKey[s.Key] = s
|
|
}
|
|
// record_fact is bound to worker (also mainagent, which can log confirmed facts).
|
|
rf, ok := byKey["record_fact"]
|
|
hasWorker := false
|
|
for _, a := range rf.Agents {
|
|
if a == "worker" {
|
|
hasWorker = true
|
|
}
|
|
}
|
|
if !ok || !hasWorker {
|
|
t.Errorf("record_fact agents = %v, want to include worker", rf.Agents)
|
|
}
|
|
// add_task_scope is bound to the planner (deliberate scope widening).
|
|
if ts, ok := byKey["add_task_scope"]; !ok || len(ts.Agents) == 0 {
|
|
t.Errorf("add_task_scope not seeded / has no agent binding: %v", ts.Agents)
|
|
}
|
|
// SDK generic tools (incl. sleep, now part of DefaultTools) are deliberately NOT
|
|
// seeded — every agent owns them; they flow through ToolResolve untouched.
|
|
for _, k := range []string{"Bash", "Read", "Write", "Edit", "Grep", "sleep"} {
|
|
if _, ok := byKey[k]; ok {
|
|
t.Errorf("SDK tool %q should not be seeded", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestObjDropsBlankRequired: obj never emits a blank required name, and omits the
|
|
// "required" key entirely when no real name is left.
|
|
func TestObjDropsBlankRequired(t *testing.T) {
|
|
if got, ok := obj(map[string]any{}, "")["required"]; ok {
|
|
t.Errorf(`obj(props, "") required = %v, want no required key`, got)
|
|
}
|
|
got, _ := obj(map[string]any{}, " ", "x")["required"].([]string)
|
|
if len(got) != 1 || got[0] != "x" {
|
|
t.Errorf(`obj(props, " ", "x") required = %v, want [x]`, got)
|
|
}
|
|
}
|
|
|
|
// TestListTaskCredentialsSchemaHasNoBlankRequired guards the seeded schema of the
|
|
// parameterless list_task_credentials tool: "required": [""] is rejected by strict
|
|
// providers, which fails every worker run.
|
|
func TestListTaskCredentialsSchemaHasNoBlankRequired(t *testing.T) {
|
|
for _, s := range BuiltinToolSeeds() {
|
|
if s.Key != "list_task_credentials" {
|
|
continue
|
|
}
|
|
raw, err := json.Marshal(s.Schema)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var schema struct {
|
|
Required []string `json:"required"`
|
|
}
|
|
if err := json.Unmarshal(raw, &schema); err != nil {
|
|
t.Fatalf("schema %s: %v", raw, err)
|
|
}
|
|
for _, r := range schema.Required {
|
|
if strings.TrimSpace(r) == "" {
|
|
t.Errorf("blank required entry in schema %s", raw)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
t.Fatal("list_task_credentials not seeded")
|
|
}
|
|
|
|
// TestDecorateToolInjectsDefaults verifies a schema "default" fills a missing param
|
|
// before the underlying handler runs, and an explicitly-provided value is kept.
|
|
func TestDecorateToolInjectsDefaults(t *testing.T) {
|
|
var seen map[string]any
|
|
base := actool.Build(actool.Spec{
|
|
Name: "probe", Description: "orig",
|
|
Run: func(_ context.Context, in json.RawMessage, _ *actool.ToolContext) (actool.Result, error) {
|
|
_ = json.Unmarshal(in, &seen)
|
|
return actool.Text("ok"), nil
|
|
},
|
|
})
|
|
schema := map[string]any{"type": "object", "properties": map[string]any{
|
|
"limit": map[string]any{"type": "integer", "description": "n", "default": float64(3)},
|
|
"q": map[string]any{"type": "string", "description": "query"},
|
|
}}
|
|
dec := DecorateTool(base, "new desc", schema)
|
|
if dec.Description() != "new desc" {
|
|
t.Errorf("description = %q", dec.Description())
|
|
}
|
|
|
|
// limit omitted → default 3 injected; q kept.
|
|
if _, err := dec.Call(context.Background(), json.RawMessage(`{"q":"x"}`), nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if seen["limit"] != float64(3) || seen["q"] != "x" {
|
|
t.Errorf("injected = %v, want limit=3 q=x", seen)
|
|
}
|
|
|
|
// limit provided → default does NOT override.
|
|
if _, err := dec.Call(context.Background(), json.RawMessage(`{"limit":9}`), nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if seen["limit"] != float64(9) {
|
|
t.Errorf("limit = %v, want 9 (no override)", seen["limit"])
|
|
}
|
|
}
|