Apply two idiomatic Go test improvements across all 113 test files in the codebase: 1. Replace context.Background() with t.Context() in test functions. t.Context() (added in Go 1.21) returns a context that is automatically cancelled when the test finishes, preventing goroutine leaks and making test teardown deterministic without manual cancel calls. 2. Add t.Parallel() to independent test functions. Parallel tests run concurrently within a package, significantly reducing total test suite wall-clock time and surfacing data races that sequential execution would hide. Also fix TestStateStore_AddTransition: add "UpdatedAt" to the cmpopts.IgnoreFields list — the DB now populates UpdatedAt on insert, causing the zero-value comparison to fail. Packages affected (113 files): - eval/go_evals - internal/fantasy (agent, json, jsonrepair, providers/*, providertests/*, schema, tool_runtime_*) - pkg/agent (integration, kv_delegate, loop, offloading_runtime, state_store, tool_result_search) - pkg/auth (oauth, pkce, store) - pkg/cache, pkg/channels, pkg/config, pkg/cron - pkg/fantasy (adapter, convert) - pkg/heartbeat, pkg/ids - pkg/itr (commands, dag/*, fb_codec, wasm/*) - pkg/logger - pkg/memory (dag/*, delegate/*, integration, kernel_contract, migrate_sessions, observation, store/*) - pkg/migrate - pkg/rlm (engine, fanout, rope, strategy) - pkg/runtime - pkg/security (jsonextract, redact, securebus/*, urlguard, vault, zkp) - pkg/session, pkg/skills, pkg/state, pkg/sync - pkg/tools (agentic_map, call, dag, edit, filesystem, focus, llm_map, map_*, message, obligation, registry_progressive, result, retrieval, search, shell, skills, spawn, subagent_*, toolloop, web) - pkg/worker
297 lines
8.6 KiB
Go
297 lines
8.6 KiB
Go
package agent_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"charm.land/fantasy"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/ZanzyTHEbar/dragonscale/pkg/agent"
|
|
"github.com/ZanzyTHEbar/dragonscale/pkg/ids"
|
|
)
|
|
|
|
// staticToolRuntime is a test ToolRuntime that returns pre-defined results.
|
|
type staticToolRuntime struct {
|
|
results []fantasy.ToolResultContent
|
|
}
|
|
|
|
func (s staticToolRuntime) Execute(_ context.Context, _ []fantasy.AgentTool, calls []fantasy.ToolCallContent, _ func(fantasy.ToolResultContent) error) ([]fantasy.ToolResultContent, error) {
|
|
if len(s.results) > 0 {
|
|
return s.results, nil
|
|
}
|
|
// Echo: return one result per call.
|
|
out := make([]fantasy.ToolResultContent, len(calls))
|
|
for i, c := range calls {
|
|
out[i] = fantasy.ToolResultContent{
|
|
ToolCallID: c.ToolCallID,
|
|
ToolName: c.ToolName,
|
|
Result: fantasy.ToolResultOutputContentText{Text: "echo:" + c.ToolCallID},
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func makeOffloader(t *testing.T, base fantasy.ToolRuntime, threshold, chunkChars int) (*agent.OffloadingToolRuntime, *ids.UUID, *ids.UUID, agent.KVDelegate) {
|
|
t.Helper()
|
|
db := newTestQueries(t)
|
|
q := db.delegate.Queries()
|
|
convID := newConversation(t, q)
|
|
s := agent.NewStateStore(q)
|
|
run, err := s.CreateRun(t.Context(), convID)
|
|
require.NoError(t, err)
|
|
|
|
kv := agent.NewDelegateKV(db.delegate, "offload-test")
|
|
|
|
// Default to the echo static runtime when no base is supplied.
|
|
if base == nil {
|
|
base = staticToolRuntime{}
|
|
}
|
|
|
|
r := &agent.OffloadingToolRuntime{
|
|
Base: base,
|
|
KV: kv,
|
|
Queries: q,
|
|
ConversationID: convID,
|
|
RunID: run.ID,
|
|
ThresholdChars: threshold,
|
|
ChunkChars: chunkChars,
|
|
}
|
|
return r, &convID, &run.ID, kv
|
|
}
|
|
|
|
func makeCalls(n int) []fantasy.ToolCallContent {
|
|
out := make([]fantasy.ToolCallContent, n)
|
|
for i := range n {
|
|
out[i] = fantasy.ToolCallContent{
|
|
ToolCallID: "call-" + string(rune('a'+i)),
|
|
ToolName: "test_tool",
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// TestOffloading_SmallResult_KeptInline verifies that results below the
|
|
// threshold are stored in KV but the inline value is unchanged.
|
|
func TestOffloading_SmallResult_KeptInline(t *testing.T) {
|
|
t.Parallel()
|
|
r, _, _, kv := makeOffloader(t, nil, 1000, 500)
|
|
ctx := t.Context()
|
|
|
|
calls := makeCalls(1)
|
|
results, err := r.Execute(ctx, nil, calls, nil)
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 1)
|
|
|
|
assert.Equal(t, "echo:call-a", results[0].Result.(fantasy.ToolResultOutputContentText).Text)
|
|
|
|
// KV should still have the full result stored
|
|
keys, err := kv.Scan(ctx, "tool_results/")
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, keys, "full result should be stored in KV")
|
|
}
|
|
|
|
// TestOffloading_LargeResult_Truncated verifies that results above the
|
|
// threshold are truncated inline and chunked in KV.
|
|
func TestOffloading_LargeResult_Truncated(t *testing.T) {
|
|
t.Parallel()
|
|
longText := strings.Repeat("x", 200)
|
|
|
|
base := staticToolRuntime{results: []fantasy.ToolResultContent{
|
|
{
|
|
ToolCallID: "call-a",
|
|
ToolName: "big_tool",
|
|
Result: fantasy.ToolResultOutputContentText{Text: longText},
|
|
},
|
|
}}
|
|
|
|
r, _, _, kv := makeOffloader(t, base, 50, 30)
|
|
ctx := t.Context()
|
|
|
|
calls := makeCalls(1)
|
|
calls[0].ToolCallID = "call-a"
|
|
calls[0].ToolName = "big_tool"
|
|
|
|
results, err := r.Execute(ctx, nil, calls, nil)
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 1)
|
|
|
|
inline := results[0].Result.(fantasy.ToolResultOutputContentText).Text
|
|
assert.Contains(t, inline, "[TRUNCATED]", "inline text must contain truncation notice")
|
|
assert.Contains(t, inline, "chunk_count:", "inline text must contain chunk_count")
|
|
// The inline text portion before the notice should be <= threshold + small
|
|
// overhead (the "…" ellipsis appended by truncateRunes).
|
|
inlineLines := strings.SplitN(inline, "[TRUNCATED]", 2)
|
|
prefix := strings.TrimSpace(inlineLines[0])
|
|
// 52 = threshold(50) + ellipsis rune(1) + one newline that TrimSpace may strip
|
|
assert.LessOrEqual(t, len([]rune(prefix)), 52, "prefix before [TRUNCATED] must be within threshold + ellipsis overhead")
|
|
|
|
// Chunks must be stored in KV
|
|
keys, err := kv.Scan(ctx, "tool_results/")
|
|
require.NoError(t, err)
|
|
chunkKeys := []string{}
|
|
for _, k := range keys {
|
|
if strings.Contains(k, "/chunks/") {
|
|
chunkKeys = append(chunkKeys, k)
|
|
}
|
|
}
|
|
assert.NotEmpty(t, chunkKeys, "chunks must be written to KV for large results")
|
|
|
|
// Reassemble chunks and verify content
|
|
var full strings.Builder
|
|
for _, k := range chunkKeys {
|
|
part, err := kv.Get(ctx, k)
|
|
require.NoError(t, err)
|
|
full.Write(part)
|
|
}
|
|
assert.Equal(t, longText, full.String(), "reassembled chunks must equal original text")
|
|
}
|
|
|
|
// TestOffloading_DBMetadata_Inserted verifies that the DB record is created.
|
|
func TestOffloading_DBMetadata_Inserted(t *testing.T) {
|
|
t.Parallel()
|
|
r, convIDPtr, runIDPtr, _ := makeOffloader(t, nil, 1000, 500)
|
|
ctx := t.Context()
|
|
|
|
calls := makeCalls(2)
|
|
_, err := r.Execute(ctx, nil, calls, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Verification: the KV scan is the best cross-check here because the
|
|
// OffloadingToolRuntime owns its own db handle (via makeOffloader).
|
|
// We verify indirectly: 2 calls → 2 full.json entries in KV.
|
|
_ = convIDPtr
|
|
_ = runIDPtr
|
|
}
|
|
|
|
// TestOffloading_NilKV_Errors verifies that a nil KVDelegate returns an error.
|
|
func TestOffloading_NilKV_Errors(t *testing.T) {
|
|
t.Parallel()
|
|
db := newTestQueries(t)
|
|
q := db.delegate.Queries()
|
|
convID := newConversation(t, q)
|
|
s := agent.NewStateStore(q)
|
|
run, err := s.CreateRun(t.Context(), convID)
|
|
require.NoError(t, err)
|
|
|
|
r := &agent.OffloadingToolRuntime{
|
|
Queries: q,
|
|
ConversationID: convID,
|
|
RunID: run.ID,
|
|
}
|
|
|
|
_, err = r.Execute(t.Context(), nil, makeCalls(1), nil)
|
|
assert.Error(t, err, "nil KV should fail")
|
|
}
|
|
|
|
// TestOffloading_NilQueries_Errors verifies that a nil Queries returns an error.
|
|
func TestOffloading_NilQueries_Errors(t *testing.T) {
|
|
t.Parallel()
|
|
db := newTestQueries(t)
|
|
kv := agent.NewDelegateKV(db.delegate, "a")
|
|
|
|
r := &agent.OffloadingToolRuntime{
|
|
KV: kv,
|
|
ConversationID: ids.New(),
|
|
RunID: ids.New(),
|
|
}
|
|
|
|
_, err := r.Execute(t.Context(), nil, makeCalls(1), nil)
|
|
assert.Error(t, err, "nil queries should fail")
|
|
}
|
|
|
|
// TestOffloading_EmptyToolCalls_ReturnsNil verifies no work is done when
|
|
// no tool calls are provided.
|
|
func TestOffloading_EmptyToolCalls_ReturnsNil(t *testing.T) {
|
|
t.Parallel()
|
|
r, _, _, _ := makeOffloader(t, nil, 1000, 500)
|
|
results, err := r.Execute(t.Context(), nil, nil, nil)
|
|
require.NoError(t, err)
|
|
assert.Nil(t, results, "no tool calls should produce no results")
|
|
}
|
|
|
|
// TestOffloading_MultipleCalls_AllStoredInKV verifies that each call gets
|
|
// its own full-result KV entry.
|
|
func TestOffloading_MultipleCalls_AllStoredInKV(t *testing.T) {
|
|
t.Parallel()
|
|
r, _, _, kv := makeOffloader(t, nil, 1000, 500)
|
|
ctx := t.Context()
|
|
|
|
calls := makeCalls(3)
|
|
_, err := r.Execute(ctx, nil, calls, nil)
|
|
require.NoError(t, err)
|
|
|
|
keys, err := kv.Scan(ctx, "tool_results/")
|
|
require.NoError(t, err)
|
|
|
|
fullKeys := []string{}
|
|
for _, k := range keys {
|
|
if strings.HasSuffix(k, "/full.json") {
|
|
fullKeys = append(fullKeys, k)
|
|
}
|
|
}
|
|
assert.Len(t, fullKeys, 3, "each tool call must produce a full.json entry")
|
|
}
|
|
|
|
// TestChunkString verifies the internal chunking logic boundary conditions.
|
|
func TestChunkString(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
chunkSize int
|
|
wantLen int
|
|
}{
|
|
{"empty string", "", 10, 1},
|
|
{"exact multiple", "abcdef", 3, 2},
|
|
{"with remainder", "abcde", 3, 2},
|
|
{"smaller than chunk", "abc", 10, 1},
|
|
{"zero chunk size returns one chunk", "hello", 0, 1},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Access via the offloading runtime's internal chunkString helper.
|
|
// We expose it indirectly through the large-result path.
|
|
// Just count chunks seen in KV after an execution.
|
|
if tt.input == "" {
|
|
return
|
|
}
|
|
|
|
base := staticToolRuntime{results: []fantasy.ToolResultContent{
|
|
{
|
|
ToolCallID: "c",
|
|
ToolName: "t",
|
|
Result: fantasy.ToolResultOutputContentText{Text: tt.input},
|
|
},
|
|
}}
|
|
var effectiveThreshold int
|
|
if tt.chunkSize > 0 {
|
|
effectiveThreshold = 1
|
|
} else {
|
|
effectiveThreshold = 1000
|
|
}
|
|
|
|
r, _, _, kv := makeOffloader(t, base, effectiveThreshold, tt.chunkSize)
|
|
calls := []fantasy.ToolCallContent{{ToolCallID: "c", ToolName: "t"}}
|
|
_, err := r.Execute(t.Context(), nil, calls, nil)
|
|
require.NoError(t, err)
|
|
|
|
keys, err := kv.Scan(t.Context(), "tool_results/")
|
|
require.NoError(t, err)
|
|
|
|
chunkCount := 0
|
|
for _, k := range keys {
|
|
if strings.Contains(k, "/chunks/") {
|
|
chunkCount++
|
|
}
|
|
}
|
|
|
|
if tt.chunkSize > 0 && len(tt.input) > effectiveThreshold {
|
|
assert.Equal(t, tt.wantLen, chunkCount)
|
|
}
|
|
})
|
|
}
|
|
}
|