ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
110 lines
3.8 KiB
Go
110 lines
3.8 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
actool "github.com/Autumn-27/norma/tool"
|
|
"github.com/Autumn-27/artex/agent"
|
|
"github.com/Autumn-27/artex/db"
|
|
)
|
|
|
|
func names(tools []actool.CoreTool) map[string]actool.CoreTool {
|
|
m := map[string]actool.CoreTool{}
|
|
for _, t := range tools {
|
|
m[t.Name()] = t
|
|
}
|
|
return m
|
|
}
|
|
|
|
// TestWireTools verifies end-to-end against the live dev PG: wireTools seeds the
|
|
// catalog, ToolResolve keeps record_fact for worker but drops it for planner (not
|
|
// bound), and an edited description + injected default flow through.
|
|
func TestWireTools(t *testing.T) {
|
|
dsn, _, err := db.DSN()
|
|
if err != nil {
|
|
t.Skipf("no database config (%v) — skipping", err)
|
|
}
|
|
pg, err := db.Open(dsn)
|
|
if err != nil {
|
|
t.Skipf("postgres unavailable (%v) — skipping", err)
|
|
}
|
|
defer pg.Close()
|
|
|
|
wireTools(pg, nil) // nil domainReg: test only covers filter/decoration, not injection
|
|
t.Cleanup(func() { agent.ToolResolve = nil })
|
|
// Prevent interactive-shell Bash decoration: if the worker agent has
|
|
// interactive_shell=true in the DB, ToolResolve would append a note to
|
|
// Bash's description. Disable that for this test so the undecorated
|
|
// pass-through assertion holds regardless of DB state.
|
|
t.Setenv("AGENT_CORE_DISABLE_INTERACTIVE_SHELL", "1")
|
|
|
|
// Seeding populated the catalog.
|
|
rf, err := pg.GetTool("record_fact")
|
|
if err != nil || rf == nil {
|
|
t.Fatalf("record_fact not seeded: %v", err)
|
|
}
|
|
|
|
// Build a base as the worker does: its domain tools + defaults, resolved for
|
|
// "worker" then for "planner".
|
|
ts := agent.NewToolSet(nil, "")
|
|
base := append(ts.WorkerTools(), actool.DefaultTools()...)
|
|
ctx := context.Background()
|
|
|
|
worker := names(agent.ToolResolve(ctx, "worker", base))
|
|
if _, ok := worker["record_fact"]; !ok {
|
|
t.Error("worker lost record_fact")
|
|
}
|
|
// SDK generic tools aren't seeded → ToolResolve passes them through unchanged
|
|
// (same object, original description — not decorated). No startup prune touches
|
|
// non-catalog rows, so future user-defined custom tools survive too.
|
|
if bash, ok := worker["Bash"]; !ok {
|
|
t.Error("worker lost Bash (should pass through)")
|
|
} else if bash.Description() != actool.NewBash().Description() {
|
|
t.Error("Bash should pass through undecorated, but description changed")
|
|
}
|
|
// planner is not bound to record_fact → resolving a base that contains it drops it.
|
|
planner := names(agent.ToolResolve(ctx, "planner", base))
|
|
if _, ok := planner["record_fact"]; ok {
|
|
t.Error("planner should not get record_fact (not bound)")
|
|
}
|
|
|
|
// Edit description + add a default to a scalar param, then confirm the resolved
|
|
// worker tool reflects both. Restore from code default on cleanup.
|
|
t.Cleanup(func() { _ = pg.UpsertToolForce(rf.Key, rf.Description, rf.Schema, mustJSON(rf.Agents)) })
|
|
var schema map[string]any
|
|
_ = json.Unmarshal(rf.Schema, &schema)
|
|
if props, ok := schema["properties"].(map[string]any); ok {
|
|
if conf, ok := props["confidence"].(map[string]any); ok {
|
|
conf["default"] = "inferred"
|
|
}
|
|
}
|
|
edited := mustJSON(schema)
|
|
if err := pg.UpdateTool(rf.Key, "EDITED DESC", edited, mustJSON([]string{"worker"}), true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
worker2 := names(agent.ToolResolve(ctx, "worker", base))
|
|
got := worker2["record_fact"]
|
|
if got == nil {
|
|
t.Fatal("record_fact missing after edit")
|
|
}
|
|
if got.Description() != "EDITED DESC" {
|
|
t.Errorf("description = %q, want EDITED DESC", got.Description())
|
|
}
|
|
// Default injection: omit confidence → handler input should carry the default.
|
|
// We can't run the real handler (nil store), but InputSchema must show the default.
|
|
sc := got.InputSchema()
|
|
props := sc["properties"].(map[string]any)
|
|
conf := props["confidence"].(map[string]any)
|
|
if conf["default"] != "inferred" {
|
|
t.Errorf("confidence.default = %v, want inferred", conf["default"])
|
|
}
|
|
}
|
|
|
|
func mustJSON(v any) json.RawMessage {
|
|
b, _ := json.Marshal(v)
|
|
return b
|
|
}
|