Quality feedback integration: TestQualityTracker_InfluencesArmSelection verifies that 5 successes vs 5 failures tips Router.Select() to the high-quality arm once EMA has enough observations. Companion test confirms heuristic fallback below minObservations. Coordinator tests expanded from 2 → 5: added guidance content check (parallel/serial/synthesize present), false-positive table extended with 7 cases including the reordered keywords from the previous fix. Agent tool suite: tool interface contracts for all four tools (Name, Description, Parameters validity, IsReadOnly). Extracted duplicated 2000-char truncation into truncateOutput() helper (format.go), removing the inline copies in agent.go and batch.go. Four boundary tests cover empty, short, exact-max, and over-max cases.
160 lines
4.5 KiB
Go
160 lines
4.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"somegit.dev/Owlibou/gnoma/internal/router"
|
|
)
|
|
|
|
func TestParseTaskType_ExplicitHintTakesPrecedence(t *testing.T) {
|
|
// Explicit hints should override prompt classification
|
|
tests := []struct {
|
|
hint string
|
|
prompt string
|
|
want router.TaskType
|
|
}{
|
|
{"review", "fix the bug", router.TaskReview},
|
|
{"refactor", "write tests", router.TaskRefactor},
|
|
{"debug", "plan the architecture", router.TaskDebug},
|
|
{"explain", "implement the feature", router.TaskExplain},
|
|
{"planning", "debug the crash", router.TaskPlanning},
|
|
{"generation", "review the code", router.TaskGeneration},
|
|
}
|
|
for _, tt := range tests {
|
|
got := parseTaskType(tt.hint, tt.prompt)
|
|
if got != tt.want {
|
|
t.Errorf("parseTaskType(%q, %q) = %s, want %s", tt.hint, tt.prompt, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseTaskType_AutoClassifiesWhenNoHint(t *testing.T) {
|
|
// No hint → classify from prompt instead of defaulting to TaskGeneration
|
|
tests := []struct {
|
|
prompt string
|
|
want router.TaskType
|
|
}{
|
|
{"review this pull request", router.TaskReview},
|
|
{"fix the failing test", router.TaskDebug},
|
|
{"refactor the auth module", router.TaskRefactor},
|
|
{"write unit tests for handler", router.TaskUnitTest},
|
|
{"explain how the router works", router.TaskExplain},
|
|
{"audit security of the API", router.TaskSecurityReview},
|
|
{"plan the migration strategy", router.TaskPlanning},
|
|
{"scaffold a new service", router.TaskBoilerplate},
|
|
}
|
|
for _, tt := range tests {
|
|
got := parseTaskType("", tt.prompt)
|
|
if got != tt.want {
|
|
t.Errorf("parseTaskType(%q) = %s, want %s (auto-classified)", tt.prompt, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Tool interface tests ---
|
|
|
|
func TestAgentTool_Interface(t *testing.T) {
|
|
tool := New(nil, nil)
|
|
|
|
if tool.Name() != "agent" {
|
|
t.Errorf("Name() = %q, want %q", tool.Name(), "agent")
|
|
}
|
|
if tool.Description() == "" {
|
|
t.Error("Description() must be non-empty")
|
|
}
|
|
if !json.Valid(tool.Parameters()) {
|
|
t.Error("Parameters() must be valid JSON")
|
|
}
|
|
if !tool.IsReadOnly() {
|
|
t.Error("IsReadOnly() must be true — agent spawning is non-destructive to parent context")
|
|
}
|
|
if tool.IsDestructive() {
|
|
t.Error("IsDestructive() must be false")
|
|
}
|
|
}
|
|
|
|
func TestBatchTool_Interface(t *testing.T) {
|
|
tool := NewBatch(nil, nil)
|
|
|
|
if tool.Name() != "spawn_elfs" {
|
|
t.Errorf("Name() = %q, want %q", tool.Name(), "spawn_elfs")
|
|
}
|
|
if tool.Description() == "" {
|
|
t.Error("Description() must be non-empty")
|
|
}
|
|
if !json.Valid(tool.Parameters()) {
|
|
t.Error("Parameters() must be valid JSON")
|
|
}
|
|
if !tool.IsReadOnly() {
|
|
t.Error("IsReadOnly() must be true")
|
|
}
|
|
if tool.IsDestructive() {
|
|
t.Error("IsDestructive() must be false")
|
|
}
|
|
}
|
|
|
|
func TestListResultsTool_Interface(t *testing.T) {
|
|
tool := NewListResultsTool(nil)
|
|
|
|
if tool.Name() != "list_results" {
|
|
t.Errorf("Name() = %q, want %q", tool.Name(), "list_results")
|
|
}
|
|
if !json.Valid(tool.Parameters()) {
|
|
t.Error("Parameters() must be valid JSON")
|
|
}
|
|
if !tool.IsReadOnly() {
|
|
t.Error("IsReadOnly() must be true")
|
|
}
|
|
}
|
|
|
|
func TestReadResultTool_Interface(t *testing.T) {
|
|
tool := NewReadResultTool(nil)
|
|
|
|
if tool.Name() != "read_result" {
|
|
t.Errorf("Name() = %q, want %q", tool.Name(), "read_result")
|
|
}
|
|
if !json.Valid(tool.Parameters()) {
|
|
t.Error("Parameters() must be valid JSON")
|
|
}
|
|
if !tool.IsReadOnly() {
|
|
t.Error("IsReadOnly() must be true")
|
|
}
|
|
}
|
|
|
|
// --- Truncation tests ---
|
|
|
|
func TestTruncateOutput(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
max int
|
|
truncated bool // true = expect truncation; false = expect unchanged
|
|
want string // only checked when !truncated
|
|
}{
|
|
{name: "short text unchanged", input: "hello", max: 2000, want: "hello"},
|
|
{name: "empty string unchanged", input: "", max: 2000, want: ""},
|
|
{name: "exact max unchanged", input: strings.Repeat("x", 2000), max: 2000, want: strings.Repeat("x", 2000)},
|
|
{name: "over max is truncated", input: strings.Repeat("y", 5000), max: 2000, truncated: true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := truncateOutput(tt.input, tt.max)
|
|
if !tt.truncated {
|
|
if got != tt.want {
|
|
t.Errorf("got %q, want %q", got, tt.want)
|
|
}
|
|
return
|
|
}
|
|
// Truncated: result must start with first max chars and include notice.
|
|
if !strings.HasPrefix(got, strings.Repeat("y", tt.max)) {
|
|
t.Error("truncated result should start with first max chars of input")
|
|
}
|
|
if !strings.Contains(got, "[truncated") {
|
|
t.Error("truncated result must contain '[truncated' notice")
|
|
}
|
|
})
|
|
}
|
|
}
|