Files
executus/run/palette_test.go
steve 0c80679719
executus CI / test (pull_request) Failing after 1m31s
executus CI / test (push) Failing after 1m31s
C0: address verified gadfly findings (trivial fixes)
From the PR #8 review (all graded in the gadfly MCP):
- skip empty palette names + dedupe by final tool name, instead of producing a
  "skill__" tool or an opaque box.Add duplicate error.
- delegationResult: no trailing blank line when a non-ok child produced no output.
- delegationErr: fold a child's partial output into the hard-failure error so it
  isn't silently dropped.

Deferred to C0b (design-level, not trivial): route delegation through the
tool.Registry gate/audit wrappers; expose the skill's real input schema to the
LLM instead of a generic inputs map. typed-nil PaletteSource is left as a caller
contract (the == nil guard catches the untyped-nil interface).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 09:53:11 -04:00

126 lines
4.4 KiB
Go

package run_test
import (
"context"
"encoding/json"
"testing"
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
"gitea.stevedudenhoeffer.com/steve/majordomo/provider/fake"
"gitea.stevedudenhoeffer.com/steve/executus/run"
"gitea.stevedudenhoeffer.com/steve/executus/tool"
)
// recordingPalette captures the delegation call it received.
type recordingPalette struct {
gotName, gotCaller, gotParent string
gotInputs map[string]any
}
func (p *recordingPalette) ResolveSkill(context.Context, string, string) (string, error) {
return "", nil
}
func (p *recordingPalette) InvokeSkill(_ context.Context, callerID, _, name string, inputs map[string]any, parentRunID string) (string, string, string, error) {
p.gotName, p.gotCaller, p.gotParent, p.gotInputs = name, callerID, parentRunID, inputs
return "the skill output", "child-run-1", "ok", nil
}
func (p *recordingPalette) ResolveAgent(context.Context, string, string) (string, error) {
return "", nil
}
func (p *recordingPalette) InvokeAgent(context.Context, string, string, string, string, string, string, string, []string, func(context.Context, string, string)) (string, string, string, error) {
return "", "", "ok", nil
}
// TestPaletteDelegation: an agent with a SkillPalette gets a skill__<name> tool;
// the model calls it, the executor routes it through run.Ports.Palette as a
// child of the current run, and the result flows back into the loop.
func TestPaletteDelegation(t *testing.T) {
pal := &recordingPalette{}
fp := fake.New("fake")
fp.Enqueue("m",
fake.ReplyWith(llm.Response{ToolCalls: []llm.ToolCall{{
ID: "c1",
Name: "skill__helper",
Arguments: json.RawMessage(`{"inputs":{"q":"hi"}}`),
}}}),
fake.Reply("delegated and done"),
)
m, err := fp.Model("m")
if err != nil {
t.Fatal(err)
}
ex := run.New(run.Config{
Registry: tool.NewRegistry(),
Models: func(ctx context.Context, _ string) (context.Context, llm.Model, error) { return ctx, m, nil },
Ports: run.Ports{Palette: pal},
})
res := ex.Run(context.Background(),
run.RunnableAgent{ID: "a1", Name: "boss", ModelTier: "m", SkillPalette: []string{"helper"}},
tool.Invocation{RunID: "parent-run", CallerID: "caller-7", ChannelID: "chan"},
"delegate please")
if res.Err != nil {
t.Fatalf("run error: %v", res.Err)
}
if res.Output != "delegated and done" {
t.Errorf("output = %q", res.Output)
}
if pal.gotName != "helper" {
t.Errorf("InvokeSkill name = %q, want helper", pal.gotName)
}
if pal.gotCaller != "caller-7" {
t.Errorf("InvokeSkill caller = %q, want caller-7", pal.gotCaller)
}
if pal.gotParent != "parent-run" {
t.Errorf("InvokeSkill parentRunID = %q, want parent-run (child of the current run)", pal.gotParent)
}
if pal.gotInputs["q"] != "hi" {
t.Errorf("InvokeSkill inputs = %+v, want q=hi", pal.gotInputs)
}
}
// TestNoPaletteNoDelegationTools: nil PaletteSource → no delegation tools, run
// still works (the agent just has no skill__/agent__ tools).
func TestNoPaletteNoDelegationTools(t *testing.T) {
fp := fake.New("fake")
fp.Enqueue("m", fake.Reply("ok"))
m, _ := fp.Model("m")
ex := run.New(run.Config{
Registry: tool.NewRegistry(),
Models: func(ctx context.Context, _ string) (context.Context, llm.Model, error) { return ctx, m, nil },
})
res := ex.Run(context.Background(),
run.RunnableAgent{Name: "x", ModelTier: "m", SkillPalette: []string{"helper"}},
tool.Invocation{RunID: "r"}, "hi")
if res.Err != nil || res.Output != "ok" {
t.Fatalf("nil-palette run failed: %v / %q", res.Err, res.Output)
}
}
// TestDelegationDedupeAndEmptySkip: empty + duplicate palette names are skipped,
// not turned into "skill__"/duplicate tools that error at box.Add (gadfly C0).
func TestDelegationDedupeAndEmptySkip(t *testing.T) {
pal := &recordingPalette{}
fp := fake.New("fake")
fp.Enqueue("m", fake.Reply("ok"))
m, _ := fp.Model("m")
ex := run.New(run.Config{
Registry: tool.NewRegistry(),
Models: func(ctx context.Context, _ string) (context.Context, llm.Model, error) { return ctx, m, nil },
Ports: run.Ports{Palette: pal},
})
// "" (empty) and a duplicate "helper" must not break the build.
res := ex.Run(context.Background(),
run.RunnableAgent{Name: "x", ModelTier: "m", SkillPalette: []string{"helper", "", "helper"}},
tool.Invocation{RunID: "r"}, "hi")
if res.Err != nil {
t.Fatalf("empty/duplicate palette names should be skipped, not error: %v", res.Err)
}
if res.Output != "ok" {
t.Fatalf("output = %q", res.Output)
}
}