test: modernize test suite with t.Context() and t.Parallel()
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
This commit is contained in:
parent
38d1faf425
commit
906d11612d
120 changed files with 1938 additions and 1139 deletions
|
|
@ -39,6 +39,7 @@ func allFileTools(workspace string) []tools.Tool {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestToolRegistry_AllToolsHaveSchema(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
registry := tools.NewToolRegistry()
|
||||
|
|
@ -68,6 +69,7 @@ func TestToolRegistry_AllToolsHaveSchema(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolRegistry_SchemaPropertiesAreTyped(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
for _, tool := range allFileTools(workspace) {
|
||||
|
|
@ -91,10 +93,11 @@ func TestToolRegistry_SchemaPropertiesAreTyped(t *testing.T) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestToolExecution_ReadFile_NonExistent(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
readTool := tools.NewReadFileTool(workspace, true)
|
||||
result := readTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := readTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": "nonexistent_file_12345.txt",
|
||||
})
|
||||
|
||||
|
|
@ -103,10 +106,11 @@ func TestToolExecution_ReadFile_NonExistent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolExecution_WriteAndReadFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
writeTool := tools.NewWriteFileTool(workspace, true)
|
||||
writeResult := writeTool.Execute(context.Background(), map[string]interface{}{
|
||||
writeResult := writeTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": "eval_test.txt",
|
||||
"content": "hello from eval test",
|
||||
})
|
||||
|
|
@ -114,7 +118,7 @@ func TestToolExecution_WriteAndReadFile(t *testing.T) {
|
|||
assert.False(t, writeResult.IsError, "write should succeed: %s", writeResult.ForLLM)
|
||||
|
||||
readTool := tools.NewReadFileTool(workspace, true)
|
||||
readResult := readTool.Execute(context.Background(), map[string]interface{}{
|
||||
readResult := readTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": "eval_test.txt",
|
||||
})
|
||||
require.NotNil(t, readResult)
|
||||
|
|
@ -123,10 +127,11 @@ func TestToolExecution_WriteAndReadFile(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolExecution_ExecBlocking(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
execTool := tools.NewExecTool(workspace, false)
|
||||
result := execTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := execTool.Execute(t.Context(), map[string]interface{}{
|
||||
"command": "echo dragonscale-eval-test",
|
||||
})
|
||||
|
||||
|
|
@ -136,13 +141,14 @@ func TestToolExecution_ExecBlocking(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolExecution_ListDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
os.WriteFile(filepath.Join(workspace, "file_a.txt"), []byte("a"), 0644)
|
||||
os.WriteFile(filepath.Join(workspace, "file_b.txt"), []byte("b"), 0644)
|
||||
|
||||
listTool := tools.NewListDirTool(workspace, true)
|
||||
result := listTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := listTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": ".",
|
||||
})
|
||||
|
||||
|
|
@ -153,16 +159,17 @@ func TestToolExecution_ListDir(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolExecution_EditFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
writeTool := tools.NewWriteFileTool(workspace, true)
|
||||
writeTool.Execute(context.Background(), map[string]interface{}{
|
||||
writeTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": "edit_target.txt",
|
||||
"content": "hello world foo bar",
|
||||
})
|
||||
|
||||
editTool := tools.NewEditFileTool(workspace, true)
|
||||
result := editTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := editTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": "edit_target.txt",
|
||||
"old_text": "world",
|
||||
"new_text": "dragonscale",
|
||||
|
|
@ -171,7 +178,7 @@ func TestToolExecution_EditFile(t *testing.T) {
|
|||
assert.False(t, result.IsError, "edit should succeed: %s", result.ForLLM)
|
||||
|
||||
readTool := tools.NewReadFileTool(workspace, true)
|
||||
readResult := readTool.Execute(context.Background(), map[string]interface{}{
|
||||
readResult := readTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": "edit_target.txt",
|
||||
})
|
||||
assert.Contains(t, readResult.ForLLM, "dragonscale")
|
||||
|
|
@ -179,16 +186,17 @@ func TestToolExecution_EditFile(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolExecution_AppendFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
writeTool := tools.NewWriteFileTool(workspace, true)
|
||||
writeTool.Execute(context.Background(), map[string]interface{}{
|
||||
writeTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": "append_target.txt",
|
||||
"content": "line one\n",
|
||||
})
|
||||
|
||||
appendTool := tools.NewAppendFileTool(workspace, true)
|
||||
result := appendTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := appendTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": "append_target.txt",
|
||||
"content": "line two\n",
|
||||
})
|
||||
|
|
@ -196,7 +204,7 @@ func TestToolExecution_AppendFile(t *testing.T) {
|
|||
assert.False(t, result.IsError, "append should succeed: %s", result.ForLLM)
|
||||
|
||||
readTool := tools.NewReadFileTool(workspace, true)
|
||||
readResult := readTool.Execute(context.Background(), map[string]interface{}{
|
||||
readResult := readTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": "append_target.txt",
|
||||
})
|
||||
assert.Contains(t, readResult.ForLLM, "line one")
|
||||
|
|
@ -208,10 +216,11 @@ func TestToolExecution_AppendFile(t *testing.T) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestToolExecution_ReadFile_WorkspaceRestriction(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
readTool := tools.NewReadFileTool(workspace, true)
|
||||
result := readTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := readTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": "/etc/passwd",
|
||||
})
|
||||
require.NotNil(t, result)
|
||||
|
|
@ -219,10 +228,11 @@ func TestToolExecution_ReadFile_WorkspaceRestriction(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolExecution_WriteFile_WorkspaceRestriction(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
writeTool := tools.NewWriteFileTool(workspace, true)
|
||||
result := writeTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := writeTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": "/tmp/escape_test.txt",
|
||||
"content": "should not write",
|
||||
})
|
||||
|
|
@ -231,10 +241,11 @@ func TestToolExecution_WriteFile_WorkspaceRestriction(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolExecution_ReadFile_PathTraversal(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
readTool := tools.NewReadFileTool(workspace, true)
|
||||
result := readTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := readTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": "../../../../etc/hostname",
|
||||
})
|
||||
require.NotNil(t, result)
|
||||
|
|
@ -242,6 +253,7 @@ func TestToolExecution_ReadFile_PathTraversal(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolExecution_Unrestricted(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
tmpFile := filepath.Join(os.TempDir(), "dragonscale_unrestricted_test.txt")
|
||||
|
|
@ -249,7 +261,7 @@ func TestToolExecution_Unrestricted(t *testing.T) {
|
|||
defer os.Remove(tmpFile)
|
||||
|
||||
readTool := tools.NewReadFileTool(workspace, false)
|
||||
result := readTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := readTool.Execute(t.Context(), map[string]interface{}{
|
||||
"path": tmpFile,
|
||||
})
|
||||
require.NotNil(t, result)
|
||||
|
|
@ -262,6 +274,7 @@ func TestToolExecution_Unrestricted(t *testing.T) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestToolRegistry_ProgressiveDisclosure(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
registry := tools.NewToolRegistry()
|
||||
|
|
@ -290,6 +303,7 @@ func TestToolRegistry_ProgressiveDisclosure(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolSearch_FindsReadFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
registry := tools.NewToolRegistry()
|
||||
|
|
@ -299,7 +313,7 @@ func TestToolSearch_FindsReadFile(t *testing.T) {
|
|||
registry.RegisterMetaTools()
|
||||
|
||||
searchTool := tools.NewToolSearchTool(registry)
|
||||
result := searchTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := searchTool.Execute(t.Context(), map[string]interface{}{
|
||||
"query": "read file",
|
||||
})
|
||||
|
||||
|
|
@ -309,6 +323,7 @@ func TestToolSearch_FindsReadFile(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolSearch_ListsAll(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
registry := tools.NewToolRegistry()
|
||||
|
|
@ -317,7 +332,7 @@ func TestToolSearch_ListsAll(t *testing.T) {
|
|||
registry.RegisterMetaTools()
|
||||
|
||||
searchTool := tools.NewToolSearchTool(registry)
|
||||
result := searchTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := searchTool.Execute(t.Context(), map[string]interface{}{
|
||||
"query": "",
|
||||
})
|
||||
|
||||
|
|
@ -327,6 +342,7 @@ func TestToolSearch_ListsAll(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolSearch_NoResults(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
registry := tools.NewToolRegistry()
|
||||
|
|
@ -334,7 +350,7 @@ func TestToolSearch_NoResults(t *testing.T) {
|
|||
registry.RegisterMetaTools()
|
||||
|
||||
searchTool := tools.NewToolSearchTool(registry)
|
||||
result := searchTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := searchTool.Execute(t.Context(), map[string]interface{}{
|
||||
"query": "xyzzy_nonexistent_tool",
|
||||
})
|
||||
|
||||
|
|
@ -343,6 +359,7 @@ func TestToolSearch_NoResults(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCall_DispatchesCorrectly(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
os.WriteFile(filepath.Join(workspace, "dispatch_test.txt"), []byte("dispatch ok"), 0644)
|
||||
|
|
@ -352,7 +369,7 @@ func TestToolCall_DispatchesCorrectly(t *testing.T) {
|
|||
registry.RegisterMetaTools()
|
||||
|
||||
callTool := tools.NewToolCallTool(registry)
|
||||
result := callTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := callTool.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "read_file",
|
||||
"arguments": map[string]interface{}{
|
||||
"path": "dispatch_test.txt",
|
||||
|
|
@ -365,6 +382,7 @@ func TestToolCall_DispatchesCorrectly(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCall_RejectsRecursion(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
registry := tools.NewToolRegistry()
|
||||
|
|
@ -372,7 +390,7 @@ func TestToolCall_RejectsRecursion(t *testing.T) {
|
|||
registry.RegisterMetaTools()
|
||||
|
||||
callTool := tools.NewToolCallTool(registry)
|
||||
result := callTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := callTool.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "tool_call",
|
||||
})
|
||||
|
||||
|
|
@ -381,11 +399,12 @@ func TestToolCall_RejectsRecursion(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCall_RejectsUnknownTool(t *testing.T) {
|
||||
t.Parallel()
|
||||
registry := tools.NewToolRegistry()
|
||||
registry.RegisterMetaTools()
|
||||
|
||||
callTool := tools.NewToolCallTool(registry)
|
||||
result := callTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := callTool.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "nonexistent_tool_xyz",
|
||||
})
|
||||
|
||||
|
|
@ -394,17 +413,19 @@ func TestToolCall_RejectsUnknownTool(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCall_MissingToolName(t *testing.T) {
|
||||
t.Parallel()
|
||||
registry := tools.NewToolRegistry()
|
||||
registry.RegisterMetaTools()
|
||||
|
||||
callTool := tools.NewToolCallTool(registry)
|
||||
result := callTool.Execute(context.Background(), map[string]interface{}{})
|
||||
result := callTool.Execute(t.Context(), map[string]interface{}{})
|
||||
|
||||
require.NotNil(t, result)
|
||||
assert.True(t, result.IsError, "missing tool_name should be rejected")
|
||||
}
|
||||
|
||||
func TestToolCall_StringArguments(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
os.WriteFile(filepath.Join(workspace, "str_args.txt"), []byte("string args ok"), 0644)
|
||||
|
|
@ -414,7 +435,7 @@ func TestToolCall_StringArguments(t *testing.T) {
|
|||
registry.RegisterMetaTools()
|
||||
|
||||
callTool := tools.NewToolCallTool(registry)
|
||||
result := callTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := callTool.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "read_file",
|
||||
"arguments": `{"path": "str_args.txt"}`,
|
||||
})
|
||||
|
|
@ -429,6 +450,7 @@ func TestToolCall_StringArguments(t *testing.T) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestToolRegistry_GatewayMarking(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
registry := tools.NewToolRegistry()
|
||||
|
|
@ -458,6 +480,7 @@ func TestToolRegistry_GatewayMarking(t *testing.T) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestConfig_DefaultValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := config.DefaultConfig()
|
||||
|
||||
assert.True(t, cfg.Agents.Defaults.RestrictToSandbox, "restrict to sandbox should be on by default")
|
||||
|
|
@ -469,6 +492,7 @@ func TestConfig_DefaultValues(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestConfig_LoadEvalConfigs(t *testing.T) {
|
||||
t.Parallel()
|
||||
evalDir := filepath.Join("..", "..", "eval", "configs")
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -492,6 +516,7 @@ func TestConfig_LoadEvalConfigs(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestConfig_MissingFileReturnsDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg, err := config.LoadConfig("/nonexistent/path/config.json")
|
||||
require.NoError(t, err, "missing config should return defaults, not error")
|
||||
assert.Equal(t, 768, cfg.Memory.EmbeddingDims, "should have default embedding dims")
|
||||
|
|
@ -502,10 +527,11 @@ func TestConfig_MissingFileReturnsDefaults(t *testing.T) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestToolExecution_ExecTimeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
execTool := tools.NewExecTool(workspace, false)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 1)
|
||||
defer cancel()
|
||||
|
||||
result := execTool.Execute(ctx, map[string]interface{}{
|
||||
|
|
@ -516,10 +542,11 @@ func TestToolExecution_ExecTimeout(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolExecution_ExecEmptyCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
execTool := tools.NewExecTool(workspace, false)
|
||||
result := execTool.Execute(context.Background(), map[string]interface{}{
|
||||
result := execTool.Execute(t.Context(), map[string]interface{}{
|
||||
"command": "",
|
||||
})
|
||||
|
||||
|
|
@ -532,6 +559,7 @@ func TestToolExecution_ExecEmptyCommand(t *testing.T) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestToolSchema_JSONRoundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
workspace := testWorkspace(t)
|
||||
|
||||
for _, tool := range allFileTools(workspace) {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ package fantasy
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -111,7 +112,7 @@ func TestStreamingAgentCallbacks(t *testing.T) {
|
|||
// Create agent
|
||||
agent := NewAgent(mockModel)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Create streaming call with all callbacks
|
||||
streamCall := AgentStreamCall{
|
||||
|
|
@ -301,7 +302,7 @@ func TestStreamingAgentWithTools(t *testing.T) {
|
|||
WithTools(&EchoTool{}),
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Track callback invocations
|
||||
var toolInputStartCalled bool
|
||||
|
|
@ -399,7 +400,7 @@ func TestStreamingAgentTextDeltas(t *testing.T) {
|
|||
}
|
||||
|
||||
agent := NewAgent(mockModel)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Track text deltas
|
||||
var textDeltas []string
|
||||
|
|
@ -461,7 +462,7 @@ func TestStreamingAgentReasoning(t *testing.T) {
|
|||
}
|
||||
|
||||
agent := NewAgent(mockModel)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
var reasoningDeltas []string
|
||||
var textDeltas []string
|
||||
|
|
@ -502,7 +503,7 @@ func TestStreamingAgentError(t *testing.T) {
|
|||
}
|
||||
|
||||
agent := NewAgent(mockModel)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Track error callbacks
|
||||
var errorOccurred bool
|
||||
|
|
@ -568,7 +569,7 @@ func TestStreamingAgentSources(t *testing.T) {
|
|||
}
|
||||
|
||||
agent := NewAgent(mockModel)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
var sources []SourceContent
|
||||
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ func TestAgent_Generate_ResultContent_AllTypes(t *testing.T) {
|
|||
}
|
||||
|
||||
agent := NewAgent(model, WithTools(tool1))
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "prompt",
|
||||
})
|
||||
|
||||
|
|
@ -209,7 +209,7 @@ func TestAgent_Generate_ResultText(t *testing.T) {
|
|||
}
|
||||
|
||||
agent := NewAgent(model)
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "prompt",
|
||||
})
|
||||
|
||||
|
|
@ -279,7 +279,7 @@ func TestAgent_Generate_ResultToolCalls(t *testing.T) {
|
|||
}
|
||||
|
||||
agent := NewAgent(model, WithTools(tool1, tool2))
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test-input",
|
||||
})
|
||||
|
||||
|
|
@ -353,7 +353,7 @@ func TestAgent_Generate_ResultToolResults(t *testing.T) {
|
|||
}
|
||||
|
||||
agent := NewAgent(model, WithTools(tool1))
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test-input",
|
||||
})
|
||||
|
||||
|
|
@ -440,7 +440,7 @@ func TestAgent_Generate_MultipleSteps(t *testing.T) {
|
|||
}
|
||||
|
||||
agent := NewAgent(model, WithTools(tool1))
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test-input",
|
||||
})
|
||||
|
||||
|
|
@ -499,7 +499,7 @@ func TestAgent_Generate_BasicText(t *testing.T) {
|
|||
}
|
||||
|
||||
agent := NewAgent(model)
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
})
|
||||
|
||||
|
|
@ -531,7 +531,7 @@ func TestAgent_Generate_EmptyPrompt(t *testing.T) {
|
|||
model := &mockLanguageModel{}
|
||||
agent := NewAgent(model)
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "", // Empty prompt should cause error
|
||||
})
|
||||
|
||||
|
|
@ -570,7 +570,7 @@ func TestAgent_Generate_WithSystemPrompt(t *testing.T) {
|
|||
}
|
||||
|
||||
agent := NewAgent(model, WithSystemPrompt("You are a helpful assistant"))
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
})
|
||||
|
||||
|
|
@ -623,7 +623,7 @@ func TestAgent_Generate_OptionsActiveTools(t *testing.T) {
|
|||
}
|
||||
|
||||
agent := NewAgent(model, WithTools(tool1, tool2))
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test-input",
|
||||
ActiveTools: []string{"tool1"}, // Only tool1 should be active
|
||||
})
|
||||
|
|
@ -872,7 +872,7 @@ func TestStopConditions_Integration(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithStopConditions(StepCountIs(1)))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
})
|
||||
|
||||
|
|
@ -904,7 +904,7 @@ func TestStopConditions_Integration(t *testing.T) {
|
|||
FinishReasonIs(FinishReasonStop), // Or stop on finish reason
|
||||
))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
})
|
||||
|
||||
|
|
@ -952,7 +952,7 @@ func TestPrepareStep(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithSystemPrompt("Original system prompt"))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
PrepareStep: prepareStepFunc,
|
||||
})
|
||||
|
|
@ -989,7 +989,7 @@ func TestPrepareStep(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model)
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
PrepareStep: prepareStepFunc,
|
||||
})
|
||||
|
|
@ -1034,7 +1034,7 @@ func TestPrepareStep(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithTools(tool1, tool2, tool3))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
PrepareStep: prepareStepFunc,
|
||||
})
|
||||
|
|
@ -1073,7 +1073,7 @@ func TestPrepareStep(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithTools(tool1))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
PrepareStep: prepareStepFunc,
|
||||
})
|
||||
|
|
@ -1133,7 +1133,7 @@ func TestPrepareStep(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithSystemPrompt("Original system"), WithTools(tool1, tool2))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
PrepareStep: prepareStepFunc,
|
||||
})
|
||||
|
|
@ -1194,7 +1194,7 @@ func TestPrepareStep(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithSystemPrompt("Parent system"), WithTools(tool1))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
PrepareStep: prepareStepFunc,
|
||||
})
|
||||
|
|
@ -1240,7 +1240,7 @@ func TestPrepareStep(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithTools(tool1, tool2))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
PrepareStep: prepareStepFunc,
|
||||
})
|
||||
|
|
@ -1289,7 +1289,7 @@ func TestToolCallRepair(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithTools(tool), WithStopConditions(StepCountIs(2))) // Limit steps
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
})
|
||||
|
||||
|
|
@ -1333,7 +1333,7 @@ func TestToolCallRepair(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithTools(tool), WithStopConditions(StepCountIs(2))) // Limit steps
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
})
|
||||
|
||||
|
|
@ -1388,7 +1388,7 @@ func TestToolCallRepair(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithTools(tool), WithRepairToolCall(repairFunc), WithStopConditions(StepCountIs(2)))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
})
|
||||
|
||||
|
|
@ -1438,7 +1438,7 @@ func TestToolCallRepair(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithTools(tool), WithRepairToolCall(repairFunc), WithStopConditions(StepCountIs(2)))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
})
|
||||
|
||||
|
|
@ -1476,7 +1476,7 @@ func TestToolCallRepair(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithTools(tool), WithStopConditions(StepCountIs(2)))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
})
|
||||
|
||||
|
|
@ -1521,7 +1521,7 @@ func TestToolCallRepair(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithTools(tool), WithStopConditions(StepCountIs(2)))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "test prompt",
|
||||
})
|
||||
|
||||
|
|
@ -1582,7 +1582,7 @@ func TestAgent_MediaToolResponses(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithTools(imageTool), WithStopConditions(StepCountIs(3)))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "Generate an image",
|
||||
})
|
||||
|
||||
|
|
@ -1636,7 +1636,7 @@ func TestAgent_MediaToolResponses(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithTools(audioTool), WithStopConditions(StepCountIs(3)))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "Generate audio",
|
||||
})
|
||||
|
||||
|
|
@ -1690,7 +1690,7 @@ func TestAgent_MediaToolResponses(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithTools(imageTool), WithStopConditions(StepCountIs(3)))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "Take a screenshot",
|
||||
})
|
||||
|
||||
|
|
@ -1749,7 +1749,7 @@ func TestAgent_MediaToolResponses(t *testing.T) {
|
|||
|
||||
agent := NewAgent(model, WithTools(imageTool), WithStopConditions(StepCountIs(3)))
|
||||
|
||||
result, err := agent.Generate(context.Background(), AgentCall{
|
||||
result, err := agent.Generate(t.Context(), AgentCall{
|
||||
Prompt: "Generate image",
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ package fantasy
|
|||
|
||||
import (
|
||||
"errors"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestMessageJSONSerialization(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
message Message
|
||||
|
|
@ -199,44 +201,29 @@ func compareMessagePart(t *testing.T, index int, original, decoded MessagePart)
|
|||
case ContentTypeText:
|
||||
orig := original.(TextPart)
|
||||
dec := decoded.(TextPart)
|
||||
if orig.Text != dec.Text {
|
||||
t.Errorf("content[%d] text mismatch: got %q, want %q", index, dec.Text, orig.Text)
|
||||
if diff := cmp.Diff(orig, dec); diff != "" {
|
||||
t.Errorf("content[%d] TextPart mismatch (-want +got):\n%s", index, diff)
|
||||
}
|
||||
|
||||
case ContentTypeReasoning:
|
||||
orig := original.(ReasoningPart)
|
||||
dec := decoded.(ReasoningPart)
|
||||
if orig.Text != dec.Text {
|
||||
t.Errorf("content[%d] reasoning text mismatch: got %q, want %q", index, dec.Text, orig.Text)
|
||||
if diff := cmp.Diff(orig, dec); diff != "" {
|
||||
t.Errorf("content[%d] ReasoningPart mismatch (-want +got):\n%s", index, diff)
|
||||
}
|
||||
|
||||
case ContentTypeFile:
|
||||
orig := original.(FilePart)
|
||||
dec := decoded.(FilePart)
|
||||
if orig.Filename != dec.Filename {
|
||||
t.Errorf("content[%d] filename mismatch: got %q, want %q", index, dec.Filename, orig.Filename)
|
||||
}
|
||||
if orig.MediaType != dec.MediaType {
|
||||
t.Errorf("content[%d] media type mismatch: got %q, want %q", index, dec.MediaType, orig.MediaType)
|
||||
}
|
||||
if !reflect.DeepEqual(orig.Data, dec.Data) {
|
||||
t.Errorf("content[%d] file data mismatch", index)
|
||||
if diff := cmp.Diff(orig, dec); diff != "" {
|
||||
t.Errorf("content[%d] FilePart mismatch (-want +got):\n%s", index, diff)
|
||||
}
|
||||
|
||||
case ContentTypeToolCall:
|
||||
orig := original.(ToolCallPart)
|
||||
dec := decoded.(ToolCallPart)
|
||||
if orig.ToolCallID != dec.ToolCallID {
|
||||
t.Errorf("content[%d] tool call id mismatch: got %q, want %q", index, dec.ToolCallID, orig.ToolCallID)
|
||||
}
|
||||
if orig.ToolName != dec.ToolName {
|
||||
t.Errorf("content[%d] tool name mismatch: got %q, want %q", index, dec.ToolName, orig.ToolName)
|
||||
}
|
||||
if orig.Input != dec.Input {
|
||||
t.Errorf("content[%d] tool input mismatch: got %q, want %q", index, dec.Input, orig.Input)
|
||||
}
|
||||
if orig.ProviderExecuted != dec.ProviderExecuted {
|
||||
t.Errorf("content[%d] provider executed mismatch: got %v, want %v", index, dec.ProviderExecuted, orig.ProviderExecuted)
|
||||
if diff := cmp.Diff(orig, dec); diff != "" {
|
||||
t.Errorf("content[%d] ToolCallPart mismatch (-want +got):\n%s", index, diff)
|
||||
}
|
||||
|
||||
case ContentTypeToolResult:
|
||||
|
|
@ -259,30 +246,35 @@ func compareToolResultOutput(t *testing.T, index int, original, decoded ToolResu
|
|||
case ToolResultContentTypeText:
|
||||
orig := original.(ToolResultOutputContentText)
|
||||
dec := decoded.(ToolResultOutputContentText)
|
||||
if orig.Text != dec.Text {
|
||||
t.Errorf("content[%d] tool result text mismatch: got %q, want %q", index, dec.Text, orig.Text)
|
||||
if diff := cmp.Diff(orig, dec); diff != "" {
|
||||
t.Errorf("content[%d] ToolResultOutputContentText mismatch (-want +got):\n%s", index, diff)
|
||||
}
|
||||
|
||||
case ToolResultContentTypeError:
|
||||
orig := original.(ToolResultOutputContentError)
|
||||
dec := decoded.(ToolResultOutputContentError)
|
||||
if orig.Error.Error() != dec.Error.Error() {
|
||||
t.Errorf("content[%d] tool result error mismatch: got %q, want %q", index, dec.Error.Error(), orig.Error.Error())
|
||||
if orig.Error == nil && dec.Error == nil {
|
||||
return
|
||||
}
|
||||
if orig.Error == nil || dec.Error == nil {
|
||||
t.Errorf("content[%d] ToolResultOutputContentError mismatch (-want +got): %v != %v", index, orig.Error, dec.Error)
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(orig.Error.Error(), dec.Error.Error()); diff != "" {
|
||||
t.Errorf("content[%d] ToolResultOutputContentError mismatch (-want +got):\n%s", index, diff)
|
||||
}
|
||||
|
||||
case ToolResultContentTypeMedia:
|
||||
orig := original.(ToolResultOutputContentMedia)
|
||||
dec := decoded.(ToolResultOutputContentMedia)
|
||||
if orig.Data != dec.Data {
|
||||
t.Errorf("content[%d] tool result media data mismatch", index)
|
||||
}
|
||||
if orig.MediaType != dec.MediaType {
|
||||
t.Errorf("content[%d] tool result media type mismatch: got %q, want %q", index, dec.MediaType, orig.MediaType)
|
||||
if diff := cmp.Diff(orig, dec); diff != "" {
|
||||
t.Errorf("content[%d] ToolResultOutputContentMedia mismatch (-want +got):\n%s", index, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelperFunctions(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NewUserMessage - text only", func(t *testing.T) {
|
||||
msg := NewUserMessage("Hello")
|
||||
|
||||
|
|
@ -412,6 +404,7 @@ func TestHelperFunctions(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("empty text part", func(t *testing.T) {
|
||||
msg := Message{
|
||||
Role: MessageRoleUser,
|
||||
|
|
@ -520,6 +513,7 @@ func TestEdgeCases(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInvalidJSONHandling(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("unknown message part type", func(t *testing.T) {
|
||||
invalidJSON := `{
|
||||
"role": "user",
|
||||
|
|
@ -606,6 +600,7 @@ func (m *mockProviderData) UnmarshalJSON(data []byte) error {
|
|||
}
|
||||
|
||||
func TestPromptSerialization(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("serialize prompt (message slice)", func(t *testing.T) {
|
||||
prompt := Prompt{
|
||||
NewSystemMessage("You are helpful"),
|
||||
|
|
@ -647,6 +642,7 @@ func TestPromptSerialization(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStreamPartErrorSerialization(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("stream part with ProviderError containing OpenAI API error", func(t *testing.T) {
|
||||
// Create a mock OpenAI API error
|
||||
openaiErr := errors.New("invalid_api_key: Incorrect API key provided")
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
)
|
||||
|
||||
func TestRepairJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -79,6 +80,7 @@ func TestRepairJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepairJSONMultipleTopLevel(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -125,6 +127,7 @@ func TestRepairJSONMultipleTopLevel(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepairJSONEnsureASCII(t *testing.T) {
|
||||
t.Parallel()
|
||||
got, err := RepairJSON("{'test_中国人_ascii':'统一码'}", WithEnsureASCII(false))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
|
|
@ -136,6 +139,7 @@ func TestRepairJSONEnsureASCII(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepairJSONStreamStable(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -197,6 +201,7 @@ func TestRepairJSONStreamStable(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoads(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -273,6 +278,7 @@ func TestLoads(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepairJSONSkipJSONLoads(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -320,6 +326,7 @@ func TestRepairJSONSkipJSONLoads(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepairJSONWithLog(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -368,6 +375,7 @@ func TestRepairJSONWithLog(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepairJSONStrict(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -438,6 +446,7 @@ func TestRepairJSONStrict(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseArrayObjects(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -479,6 +488,7 @@ func TestParseArrayObjects(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseArrayEdgeCases(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -620,6 +630,7 @@ func TestParseArrayEdgeCases(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseArrayMissingQuotes(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -656,6 +667,7 @@ func TestParseArrayMissingQuotes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseComment(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -712,6 +724,7 @@ func TestParseComment(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseNumber(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -757,6 +770,7 @@ func TestParseNumber(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseNumberEdgeCases(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -863,6 +877,7 @@ func TestParseNumberEdgeCases(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseObjectObjects(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -912,6 +927,7 @@ func TestParseObjectObjects(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseObjectEdgeCases(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -1118,6 +1134,7 @@ func TestParseObjectEdgeCases(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseObjectMergeAtEnd(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -1179,6 +1196,7 @@ func TestParseObjectMergeAtEnd(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseStringBasics(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -1225,6 +1243,7 @@ func TestParseStringBasics(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMissingAndMixedQuotes(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -1346,6 +1365,7 @@ func TestMissingAndMixedQuotes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestEscaping(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -1416,6 +1436,7 @@ func TestEscaping(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMarkdown(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -1452,6 +1473,7 @@ func TestMarkdown(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLeadingTrailingCharacters(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -1493,6 +1515,7 @@ func TestLeadingTrailingCharacters(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStringJSONLLMBlock(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -1534,6 +1557,7 @@ func TestStringJSONLLMBlock(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseBooleanOrNull(t *testing.T) {
|
||||
t.Parallel()
|
||||
loadCases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
)
|
||||
|
||||
func TestParseAzureURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package openai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
|
@ -817,7 +816,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -850,7 +849,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -875,7 +874,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -916,7 +915,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -943,7 +942,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
LogProbs: fantasy.Opt(true),
|
||||
|
|
@ -978,7 +977,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -1003,7 +1002,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -1028,7 +1027,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -1061,7 +1060,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
LogitBias: map[string]int64{
|
||||
|
|
@ -1104,7 +1103,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "o1-mini")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(
|
||||
&ProviderOptions{
|
||||
|
|
@ -1145,7 +1144,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-4o")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
TextVerbosity: fantasy.Opt("low"),
|
||||
|
|
@ -1184,7 +1183,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
Tools: []fantasy.Tool{
|
||||
fantasy.FunctionTool{
|
||||
|
|
@ -1257,7 +1256,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
Tools: []fantasy.Tool{
|
||||
fantasy.FunctionTool{
|
||||
|
|
@ -1305,7 +1304,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
Tools: []fantasy.Tool{
|
||||
fantasy.FunctionTool{
|
||||
|
|
@ -1375,7 +1374,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -1418,7 +1417,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-4o-mini")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -1454,7 +1453,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-4o-mini")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -1483,7 +1482,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "o1-preview")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
Temperature: &[]float64{0.5}[0],
|
||||
TopP: &[]float64{0.7}[0],
|
||||
|
|
@ -1532,7 +1531,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "o1-preview")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
MaxOutputTokens: &[]int64{1000}[0],
|
||||
})
|
||||
|
|
@ -1577,7 +1576,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "o1-preview")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -1605,7 +1604,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "o1-preview")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
MaxCompletionTokens: fantasy.Opt(int64(255)),
|
||||
|
|
@ -1644,7 +1643,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
Prediction: map[string]any{
|
||||
|
|
@ -1689,7 +1688,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
Store: fantasy.Opt(true),
|
||||
|
|
@ -1728,7 +1727,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
Metadata: map[string]any{
|
||||
|
|
@ -1771,7 +1770,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
PromptCacheKey: fantasy.Opt("test-cache-key-123"),
|
||||
|
|
@ -1810,7 +1809,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
SafetyIdentifier: fantasy.Opt("test-safety-identifier-123"),
|
||||
|
|
@ -1847,7 +1846,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-4o-search-preview")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
Temperature: &[]float64{0.7}[0],
|
||||
})
|
||||
|
|
@ -1882,7 +1881,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "o3-mini")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
ServiceTier: fantasy.Opt("flex"),
|
||||
|
|
@ -1919,7 +1918,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-4o-mini")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
ServiceTier: fantasy.Opt("flex"),
|
||||
|
|
@ -1953,7 +1952,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-4o-mini")
|
||||
|
||||
_, err = model.Generate(context.Background(), fantasy.Call{
|
||||
_, err = model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
ServiceTier: fantasy.Opt("priority"),
|
||||
|
|
@ -1990,7 +1989,7 @@ func TestDoGenerate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
result, err := model.Generate(context.Background(), fantasy.Call{
|
||||
result, err := model.Generate(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
ServiceTier: fantasy.Opt("priority"),
|
||||
|
|
@ -2298,7 +2297,7 @@ func TestDoStream(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
stream, err := model.Stream(context.Background(), fantasy.Call{
|
||||
stream, err := model.Stream(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -2355,7 +2354,7 @@ func TestDoStream(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
stream, err := model.Stream(context.Background(), fantasy.Call{
|
||||
stream, err := model.Stream(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
Tools: []fantasy.Tool{
|
||||
fantasy.FunctionTool{
|
||||
|
|
@ -2442,7 +2441,7 @@ func TestDoStream(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
stream, err := model.Stream(context.Background(), fantasy.Call{
|
||||
stream, err := model.Stream(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -2482,7 +2481,7 @@ func TestDoStream(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
stream, err := model.Stream(context.Background(), fantasy.Call{
|
||||
stream, err := model.Stream(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -2524,7 +2523,7 @@ func TestDoStream(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
_, err = model.Stream(context.Background(), fantasy.Call{
|
||||
_, err = model.Stream(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -2573,7 +2572,7 @@ func TestDoStream(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
stream, err := model.Stream(context.Background(), fantasy.Call{
|
||||
stream, err := model.Stream(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -2624,7 +2623,7 @@ func TestDoStream(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
stream, err := model.Stream(context.Background(), fantasy.Call{
|
||||
stream, err := model.Stream(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -2668,7 +2667,7 @@ func TestDoStream(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
_, err = model.Stream(context.Background(), fantasy.Call{
|
||||
_, err = model.Stream(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
Store: fantasy.Opt(true),
|
||||
|
|
@ -2711,7 +2710,7 @@ func TestDoStream(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-3.5-turbo")
|
||||
|
||||
_, err = model.Stream(context.Background(), fantasy.Call{
|
||||
_, err = model.Stream(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
Metadata: map[string]any{
|
||||
|
|
@ -2758,7 +2757,7 @@ func TestDoStream(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "o3-mini")
|
||||
|
||||
_, err = model.Stream(context.Background(), fantasy.Call{
|
||||
_, err = model.Stream(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
ServiceTier: fantasy.Opt("flex"),
|
||||
|
|
@ -2801,7 +2800,7 @@ func TestDoStream(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "gpt-4o-mini")
|
||||
|
||||
_, err = model.Stream(context.Background(), fantasy.Call{
|
||||
_, err = model.Stream(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
ProviderOptions: NewProviderOptions(&ProviderOptions{
|
||||
ServiceTier: fantasy.Opt("priority"),
|
||||
|
|
@ -2845,7 +2844,7 @@ func TestDoStream(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "o1-preview")
|
||||
|
||||
stream, err := model.Stream(context.Background(), fantasy.Call{
|
||||
stream, err := model.Stream(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
@ -2892,7 +2891,7 @@ func TestDoStream(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
model, _ := provider.LanguageModel(t.Context(), "o1-preview")
|
||||
|
||||
stream, err := model.Stream(context.Background(), fantasy.Call{
|
||||
stream, err := model.Stream(t.Context(), fantasy.Call{
|
||||
Prompt: testPrompt,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ var anthropicTestModels = []testModel{
|
|||
}
|
||||
|
||||
func TestAnthropicCommon(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pairs []builderPair
|
||||
for _, m := range anthropicTestModels {
|
||||
pairs = append(pairs, builderPair{m.name, anthropicBuilder(m.model), nil, nil})
|
||||
|
|
@ -56,6 +57,7 @@ func addAnthropicCaching(ctx context.Context, options fantasy.PrepareStepFunctio
|
|||
}
|
||||
|
||||
func TestAnthropicCommonWithCacheControl(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pairs []builderPair
|
||||
for _, m := range anthropicTestModels {
|
||||
pairs = append(pairs, builderPair{m.name, anthropicBuilder(m.model), nil, addAnthropicCaching})
|
||||
|
|
@ -64,6 +66,7 @@ func TestAnthropicCommonWithCacheControl(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAnthropicThinking(t *testing.T) {
|
||||
t.Parallel()
|
||||
opts := fantasy.ProviderOptions{
|
||||
anthropic.Name: &anthropic.ProviderOptions{
|
||||
Thinking: &anthropic.ThinkingProviderOption{
|
||||
|
|
@ -82,6 +85,7 @@ func TestAnthropicThinking(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAnthropicThinkingWithCacheControl(t *testing.T) {
|
||||
t.Parallel()
|
||||
opts := fantasy.ProviderOptions{
|
||||
anthropic.Name: &anthropic.ProviderOptions{
|
||||
Thinking: &anthropic.ThinkingProviderOption{
|
||||
|
|
@ -100,6 +104,7 @@ func TestAnthropicThinkingWithCacheControl(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAnthropicObjectGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pairs []builderPair
|
||||
for _, m := range anthropicTestModels {
|
||||
pairs = append(pairs, builderPair{m.name, anthropicBuilder(m.model), nil, nil})
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
)
|
||||
|
||||
func TestAzureResponsesCommon(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pairs []builderPair
|
||||
models := []testModel{
|
||||
{"azure-gpt-5-mini", "gpt-5-mini", true},
|
||||
|
|
@ -41,6 +42,7 @@ func azureReasoningBuilder(model string) builderFunc {
|
|||
}
|
||||
|
||||
func TestAzureResponsesWithSummaryThinking(t *testing.T) {
|
||||
t.Parallel()
|
||||
opts := fantasy.ProviderOptions{
|
||||
openai.Name: &openai.ResponsesProviderOptions{
|
||||
Include: []openai.IncludeType{
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
const defaultBaseURL = "https://fantasy-playground-resource.openai.azure.com"
|
||||
|
||||
func TestAzureCommon(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCommon(t, []builderPair{
|
||||
{"azure-o4-mini", builderAzureO4Mini, nil, nil},
|
||||
{"azure-gpt-5-mini", builderAzureGpt5Mini, nil, nil},
|
||||
|
|
@ -24,6 +25,7 @@ func TestAzureCommon(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAzureThinking(t *testing.T) {
|
||||
t.Parallel()
|
||||
opts := fantasy.ProviderOptions{
|
||||
openai.Name: &openai.ProviderOptions{
|
||||
ReasoningEffort: openai.ReasoningEffortOption(openai.ReasoningEffortHigh),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func TestBedrockCommon(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCommon(t, []builderPair{
|
||||
{"bedrock-anthropic-claude-3-sonnet", builderBedrockClaude3Sonnet, nil, nil},
|
||||
{"bedrock-anthropic-claude-3-opus", builderBedrockClaude3Opus, nil, nil},
|
||||
|
|
@ -19,6 +20,7 @@ func TestBedrockCommon(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBedrockBasicAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
testSimple(t, builderPair{"bedrock-anthropic-claude-3-sonnet", buildersBedrockBasicAuth, nil, nil})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ var vertexTestModels = []testModel{
|
|||
}
|
||||
|
||||
func TestGoogleCommon(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pairs []builderPair
|
||||
for _, m := range geminiTestModels {
|
||||
pairs = append(pairs, builderPair{m.name, geminiBuilder(m.model), nil, nil})
|
||||
|
|
@ -37,6 +38,7 @@ func TestGoogleCommon(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGoogleThinking(t *testing.T) {
|
||||
t.Parallel()
|
||||
opts := fantasy.ProviderOptions{
|
||||
google.Name: &google.ProviderOptions{
|
||||
ThinkingConfig: &google.ThinkingConfig{
|
||||
|
|
@ -57,6 +59,7 @@ func TestGoogleThinking(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGoogleObjectGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pairs []builderPair
|
||||
for _, m := range geminiTestModels {
|
||||
pairs = append(pairs, builderPair{m.name, geminiBuilder(m.model), nil, nil})
|
||||
|
|
@ -65,6 +68,7 @@ func TestGoogleObjectGeneration(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGoogleVertexObjectGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pairs []builderPair
|
||||
for _, m := range vertexTestModels {
|
||||
pairs = append(pairs, builderPair{m.name, vertexBuilder(m.model), nil, nil})
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ func geminiImageBuilder(model string) builderFunc {
|
|||
}
|
||||
|
||||
func TestImageUploadAgent(t *testing.T) {
|
||||
t.Parallel()
|
||||
pairs := []builderPair{
|
||||
{
|
||||
name: "anthropic-claude-sonnet-4",
|
||||
|
|
@ -100,6 +101,7 @@ func TestImageUploadAgent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestImageUploadAgentStreaming(t *testing.T) {
|
||||
t.Parallel()
|
||||
pairs := []builderPair{
|
||||
{
|
||||
name: "anthropic-claude-sonnet-4",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
)
|
||||
|
||||
func TestOpenAIResponsesCommon(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pairs []builderPair
|
||||
for _, m := range openaiTestModels {
|
||||
pairs = append(pairs, builderPair{m.name, openAIReasoningBuilder(m.model), nil, nil})
|
||||
|
|
@ -34,6 +35,7 @@ func openAIReasoningBuilder(model string) builderFunc {
|
|||
}
|
||||
|
||||
func TestOpenAIResponsesWithSummaryThinking(t *testing.T) {
|
||||
t.Parallel()
|
||||
opts := fantasy.ProviderOptions{
|
||||
openai.Name: &openai.ResponsesProviderOptions{
|
||||
Include: []openai.IncludeType{
|
||||
|
|
@ -54,6 +56,7 @@ func TestOpenAIResponsesWithSummaryThinking(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOpenAIResponsesObjectGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pairs []builderPair
|
||||
for _, m := range openaiTestModels {
|
||||
pairs = append(pairs, builderPair{m.name, openAIReasoningBuilder(m.model), nil, nil})
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ var openaiTestModels = []testModel{
|
|||
}
|
||||
|
||||
func TestOpenAICommon(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pairs []builderPair
|
||||
for _, m := range openaiTestModels {
|
||||
pairs = append(pairs, builderPair{m.name, openAIBuilder(m.model), nil, nil})
|
||||
|
|
@ -26,6 +27,7 @@ func TestOpenAICommon(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOpenAIObjectGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pairs []builderPair
|
||||
for _, m := range openaiTestModels {
|
||||
pairs = append(pairs, builderPair{m.name, openAIBuilder(m.model), nil, nil})
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
)
|
||||
|
||||
func TestOpenAICompatibleCommon(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCommon(t, []builderPair{
|
||||
{"xai-grok-4-fast", builderXAIGrok4Fast, nil, nil},
|
||||
{"xai-grok-code-fast", builderXAIGrokCodeFast, nil, nil},
|
||||
|
|
@ -24,6 +25,7 @@ func TestOpenAICompatibleCommon(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOpenAICompatObjectGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
testObjectGeneration(t, []builderPair{
|
||||
{"xai-grok-4-fast", builderXAIGrok4Fast, nil, nil},
|
||||
{"xai-grok-code-fast", builderXAIGrokCodeFast, nil, nil},
|
||||
|
|
@ -32,6 +34,7 @@ func TestOpenAICompatObjectGeneration(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOpenAICompatibleThinking(t *testing.T) {
|
||||
t.Parallel()
|
||||
opts := fantasy.ProviderOptions{
|
||||
openaicompat.Name: &openaicompat.ProviderOptions{
|
||||
ReasoningEffort: openai.ReasoningEffortOption(openai.ReasoningEffortHigh),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ var openrouterTestModels = []testModel{
|
|||
}
|
||||
|
||||
func TestOpenRouterCommon(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pairs []builderPair
|
||||
for _, m := range openrouterTestModels {
|
||||
pairs = append(pairs, builderPair{m.name, openrouterBuilder(m.model), nil, nil})
|
||||
|
|
@ -34,12 +35,14 @@ func TestOpenRouterCommon(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOpenRouterCommonWithAnthropicCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCommon(t, []builderPair{
|
||||
{"claude-sonnet-4", openrouterBuilder("anthropic/claude-sonnet-4"), nil, addAnthropicCaching},
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpenRouterThinking(t *testing.T) {
|
||||
t.Parallel()
|
||||
opts := fantasy.ProviderOptions{
|
||||
openrouter.Name: &openrouter.ProviderOptions{
|
||||
Reasoning: &openrouter.ReasoningOptions{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package providertests
|
||||
|
||||
import (
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"charm.land/fantasy/providers/anthropic"
|
||||
"charm.land/fantasy/providers/google"
|
||||
|
|
@ -14,6 +15,7 @@ import (
|
|||
)
|
||||
|
||||
func TestProviderRegistry_Serialization_OpenAIOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
msg := fantasy.Message{
|
||||
Role: fantasy.MessageRoleUser,
|
||||
Content: []fantasy.MessagePart{
|
||||
|
|
@ -52,7 +54,10 @@ func TestProviderRegistry_Serialization_OpenAIOptions(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProviderRegistry_Serialization_OpenAIResponses(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Use ResponsesProviderOptions in provider options
|
||||
)
|
||||
|
||||
msg := fantasy.Message{
|
||||
Role: fantasy.MessageRoleUser,
|
||||
Content: []fantasy.MessagePart{
|
||||
|
|
@ -95,6 +100,7 @@ func TestProviderRegistry_Serialization_OpenAIResponses(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProviderRegistry_Serialization_OpenAIResponsesReasoningMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
resp := fantasy.Response{
|
||||
Content: []fantasy.Content{
|
||||
fantasy.TextContent{
|
||||
|
|
@ -144,6 +150,7 @@ func TestProviderRegistry_Serialization_OpenAIResponsesReasoningMetadata(t *test
|
|||
}
|
||||
|
||||
func TestProviderRegistry_Serialization_AnthropicOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
sendReasoning := true
|
||||
msg := fantasy.Message{
|
||||
Role: fantasy.MessageRoleUser,
|
||||
|
|
@ -172,6 +179,7 @@ func TestProviderRegistry_Serialization_AnthropicOptions(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProviderRegistry_Serialization_GoogleOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
msg := fantasy.Message{
|
||||
Role: fantasy.MessageRoleUser,
|
||||
Content: []fantasy.MessagePart{
|
||||
|
|
@ -200,6 +208,7 @@ func TestProviderRegistry_Serialization_GoogleOptions(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProviderRegistry_Serialization_OpenRouterOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
includeUsage := true
|
||||
msg := fantasy.Message{
|
||||
Role: fantasy.MessageRoleUser,
|
||||
|
|
@ -231,6 +240,7 @@ func TestProviderRegistry_Serialization_OpenRouterOptions(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProviderRegistry_Serialization_OpenAICompatOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
effort := openai.ReasoningEffortHigh
|
||||
msg := fantasy.Message{
|
||||
Role: fantasy.MessageRoleUser,
|
||||
|
|
@ -262,7 +272,10 @@ func TestProviderRegistry_Serialization_OpenAICompatOptions(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProviderRegistry_MultiProvider(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Test with multiple providers in one message
|
||||
)
|
||||
|
||||
sendReasoning := true
|
||||
msg := fantasy.Message{
|
||||
Role: fantasy.MessageRoleUser,
|
||||
|
|
@ -299,6 +312,7 @@ func TestProviderRegistry_MultiProvider(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProviderRegistry_ErrorHandling(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("unknown provider type", func(t *testing.T) {
|
||||
invalidJSON := `{
|
||||
"role": "user",
|
||||
|
|
@ -333,8 +347,11 @@ func TestProviderRegistry_ErrorHandling(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProviderRegistry_AllTypesRegistered(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Verify all expected provider types are registered
|
||||
// We test that unmarshaling with proper type IDs doesn't fail with "unknown provider data type"
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
providerName string
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ var vercelTestModels = []testModel{
|
|||
}
|
||||
|
||||
func TestVercelCommon(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pairs []builderPair
|
||||
for _, m := range vercelTestModels {
|
||||
pairs = append(pairs, builderPair{m.name, vercelBuilder(m.model), nil, nil})
|
||||
|
|
@ -28,12 +29,14 @@ func TestVercelCommon(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVercelCommonWithAnthropicCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCommon(t, []builderPair{
|
||||
{"claude-sonnet-4", vercelBuilder("anthropic/claude-sonnet-4"), nil, addAnthropicCaching},
|
||||
})
|
||||
}
|
||||
|
||||
func TestVercelThinking(t *testing.T) {
|
||||
t.Parallel()
|
||||
enabled := true
|
||||
opts := fantasy.ProviderOptions{
|
||||
vercel.Name: &vercel.ProviderOptions{
|
||||
|
|
|
|||
|
|
@ -47,9 +47,10 @@ func driveGeneratePath(ctx context.Context, f *reactFSM) {
|
|||
|
||||
// TestFSM_Start verifies the FSM transitions from Init to PrepareStep on Start.
|
||||
func TestFSM_Start(t *testing.T) {
|
||||
t.Parallel()
|
||||
obs := &captureObserver{}
|
||||
f, _ := newTestFSM(t, obs)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
f.Fire(ctx, ReActTriggerStart)
|
||||
|
||||
|
|
@ -63,9 +64,10 @@ func TestFSM_Start(t *testing.T) {
|
|||
// TestFSM_FullHappyPath drives one complete step through all states and
|
||||
// ends in Done via Finished.
|
||||
func TestFSM_FullHappyPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
obs := &captureObserver{}
|
||||
f, _ := newTestFSM(t, obs)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
f.Fire(ctx, ReActTriggerStart)
|
||||
driveGeneratePath(ctx, f)
|
||||
|
|
@ -81,9 +83,10 @@ func TestFSM_FullHappyPath(t *testing.T) {
|
|||
// TestFSM_Continue verifies that the loop can re-enter PrepareStep after a
|
||||
// tool-call step.
|
||||
func TestFSM_Continue(t *testing.T) {
|
||||
t.Parallel()
|
||||
obs := &captureObserver{}
|
||||
f, _ := newTestFSM(t, obs)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
f.Fire(ctx, ReActTriggerStart)
|
||||
driveGeneratePath(ctx, f)
|
||||
|
|
@ -101,9 +104,10 @@ func TestFSM_Continue(t *testing.T) {
|
|||
|
||||
// TestFSM_StopConditionMet verifies the alternative Done path.
|
||||
func TestFSM_StopConditionMet(t *testing.T) {
|
||||
t.Parallel()
|
||||
obs := &captureObserver{}
|
||||
f, _ := newTestFSM(t, obs)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
f.Fire(ctx, ReActTriggerStart)
|
||||
driveGeneratePath(ctx, f)
|
||||
|
|
@ -115,9 +119,10 @@ func TestFSM_StopConditionMet(t *testing.T) {
|
|||
|
||||
// TestFSM_ErrorTransition verifies the error state is reachable from any state.
|
||||
func TestFSM_ErrorTransition(t *testing.T) {
|
||||
t.Parallel()
|
||||
obs := &captureObserver{}
|
||||
f, _ := newTestFSM(t, obs)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
f.Fire(ctx, ReActTriggerStart)
|
||||
f.Fire(ctx, ReActTriggerPrepared)
|
||||
|
|
@ -130,9 +135,10 @@ func TestFSM_ErrorTransition(t *testing.T) {
|
|||
|
||||
// TestFSM_RecoveredContinue verifies the error → PrepareStep recovery path.
|
||||
func TestFSM_RecoveredContinue(t *testing.T) {
|
||||
t.Parallel()
|
||||
obs := &captureObserver{}
|
||||
f, _ := newTestFSM(t, obs)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
f.Fire(ctx, ReActTriggerStart)
|
||||
f.Fire(ctx, ReActTriggerErrored) // error before prepared
|
||||
|
|
@ -146,8 +152,9 @@ func TestFSM_RecoveredContinue(t *testing.T) {
|
|||
// TestFSM_UnhandledTriggerIsPermissive verifies that firing an invalid trigger
|
||||
// from a given state does NOT return an error (permissive design).
|
||||
func TestFSM_UnhandledTriggerIsPermissive(t *testing.T) {
|
||||
t.Parallel()
|
||||
f, _ := newTestFSM(t, nil)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// From Init, firing Finished is not a permitted transition.
|
||||
// The FSM must silently ignore it (no panic, no error).
|
||||
|
|
@ -159,8 +166,9 @@ func TestFSM_UnhandledTriggerIsPermissive(t *testing.T) {
|
|||
// TestFSM_TransitionLog verifies the log accumulates correctly and Snapshot
|
||||
// returns a copy.
|
||||
func TestFSM_TransitionLog(t *testing.T) {
|
||||
t.Parallel()
|
||||
f, _ := newTestFSM(t, nil)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
f.Fire(ctx, ReActTriggerStart)
|
||||
f.Fire(ctx, ReActTriggerPrepared)
|
||||
|
|
@ -177,10 +185,11 @@ func TestFSM_TransitionLog(t *testing.T) {
|
|||
// TestFSM_StepIndex verifies that the step index embedded in transitions
|
||||
// reflects the pointer value at emission time.
|
||||
func TestFSM_StepIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
idx := 0
|
||||
obs := &captureObserver{}
|
||||
f := newReActFSM(obs, &idx)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
f.Fire(ctx, ReActTriggerStart) // stepIndex = 0
|
||||
idx = 1
|
||||
|
|
@ -194,8 +203,9 @@ func TestFSM_StepIndex(t *testing.T) {
|
|||
|
||||
// TestFSM_NilObserverSafe verifies no panic when no observer is attached.
|
||||
func TestFSM_NilObserverSafe(t *testing.T) {
|
||||
t.Parallel()
|
||||
f, _ := newTestFSM(t, nil)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
f.Fire(ctx, ReActTriggerStart)
|
||||
|
|
@ -206,6 +216,7 @@ func TestFSM_NilObserverSafe(t *testing.T) {
|
|||
// TestReActTransitionLog_ConcurrentAppend verifies the log is safe under
|
||||
// concurrent writes.
|
||||
func TestReActTransitionLog_ConcurrentAppend(t *testing.T) {
|
||||
t.Parallel()
|
||||
log := NewReActTransitionLog()
|
||||
const n = 100
|
||||
var wg sync.WaitGroup
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ import (
|
|||
)
|
||||
|
||||
func TestEnumSupport(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Test enum via struct tags
|
||||
)
|
||||
|
||||
type WeatherInput struct {
|
||||
Location string `json:"location" description:"City name"`
|
||||
Units string `json:"units" enum:"celsius,fahrenheit,kelvin" description:"Temperature units"`
|
||||
|
|
@ -34,6 +37,7 @@ func TestEnumSupport(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSchemaToParameters(t *testing.T) {
|
||||
t.Parallel()
|
||||
testSchema := Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*Schema{
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func TestDAGToolRuntime_IndependentToolsRunConcurrently(t *testing.T) {
|
|||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
res, err = rt.Execute(context.Background(), []AgentTool{tool}, toolCalls, nil)
|
||||
res, err = rt.Execute(t.Context(), []AgentTool{tool}, toolCalls, nil)
|
||||
}()
|
||||
|
||||
// Both tools should start before we release.
|
||||
|
|
@ -100,7 +100,7 @@ func TestDAGToolRuntime_DependenciesWaitAndInputIsResolved(t *testing.T) {
|
|||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
res, err = rt.Execute(context.Background(), []AgentTool{toolA, toolB}, toolCalls, nil)
|
||||
res, err = rt.Execute(t.Context(), []AgentTool{toolA, toolB}, toolCalls, nil)
|
||||
}()
|
||||
|
||||
<-startA
|
||||
|
|
@ -138,7 +138,7 @@ func TestDAGToolRuntime_CycleDetected(t *testing.T) {
|
|||
{ToolCallID: "b", ToolName: "p", Input: `{"x":"$tool.a"}`},
|
||||
}
|
||||
|
||||
res, err := rt.Execute(context.Background(), []AgentTool{tool}, toolCalls, nil)
|
||||
res, err := rt.Execute(t.Context(), []AgentTool{tool}, toolCalls, nil)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, res)
|
||||
}
|
||||
|
|
@ -172,7 +172,7 @@ func TestDAGToolRuntime_OnToolResultSerialized(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
res, err := rt.Execute(context.Background(), []AgentTool{tool}, toolCalls, cb)
|
||||
res, err := rt.Execute(t.Context(), []AgentTool{tool}, toolCalls, cb)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 2)
|
||||
|
||||
|
|
@ -206,7 +206,7 @@ func TestDAGToolRuntime_MetricsAndLogHooks(t *testing.T) {
|
|||
toolCalls := []ToolCallContent{
|
||||
{ToolCallID: "a", ToolName: "p", Input: `{}`},
|
||||
}
|
||||
res, err := rt.Execute(context.Background(), []AgentTool{tool}, toolCalls, nil)
|
||||
res, err := rt.Execute(t.Context(), []AgentTool{tool}, toolCalls, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.True(t, metricsCalled)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ func TestParallelToolRuntime_OrderAndCallbackDeterminism(t *testing.T) {
|
|||
{ToolCallID: "c3", ToolName: "p", Input: `{"delay_ms":30,"value":"c"}`},
|
||||
}
|
||||
|
||||
results, err := runtime.Execute(context.Background(), []AgentTool{tool}, toolCalls, cb)
|
||||
results, err := runtime.Execute(t.Context(), []AgentTool{tool}, toolCalls, cb)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 3)
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ func TestParallelToolRuntime_BarrierForNonParallelTools(t *testing.T) {
|
|||
{ToolCallID: "p3", ToolName: "p", Input: `{}`},
|
||||
}
|
||||
|
||||
results, err := runtime.Execute(context.Background(), []AgentTool{parallel, seq}, toolCalls, cb)
|
||||
results, err := runtime.Execute(t.Context(), []AgentTool{parallel, seq}, toolCalls, cb)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 4)
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ func TestParallelToolRuntime_CriticalErrorPropagation(t *testing.T) {
|
|||
{ToolCallID: "bad", ToolName: "p", Input: `{}`},
|
||||
}
|
||||
|
||||
results, err := runtime.Execute(context.Background(), []AgentTool{tool}, toolCalls, nil)
|
||||
results, err := runtime.Execute(t.Context(), []AgentTool{tool}, toolCalls, nil)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, results)
|
||||
}
|
||||
|
|
@ -148,7 +148,7 @@ func TestParallelToolRuntime_MetricsAndLogHooks(t *testing.T) {
|
|||
toolCalls := []ToolCallContent{
|
||||
{ToolCallID: "a", ToolName: "p", Input: `{}`},
|
||||
}
|
||||
res, err := rt.Execute(context.Background(), []AgentTool{tool}, toolCalls, nil)
|
||||
res, err := rt.Execute(t.Context(), []AgentTool{tool}, toolCalls, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.True(t, metricsCalled)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -14,7 +15,8 @@ type CalculatorInput struct {
|
|||
}
|
||||
|
||||
func TestTypedToolFuncExample(t *testing.T) {
|
||||
// Create a typed tool using the function API
|
||||
t.Parallel()
|
||||
|
||||
tool := NewAgentTool(
|
||||
"calculator",
|
||||
"Evaluates simple mathematical expressions",
|
||||
|
|
@ -39,13 +41,14 @@ func TestTypedToolFuncExample(t *testing.T) {
|
|||
Input: `{"expression": "2+2"}`,
|
||||
}
|
||||
|
||||
result, err := tool.Run(context.Background(), call)
|
||||
result, err := tool.Run(t.Context(), call)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "4", result.Content)
|
||||
require.False(t, result.IsError)
|
||||
}
|
||||
|
||||
func TestEnumToolExample(t *testing.T) {
|
||||
t.Parallel()
|
||||
type WeatherInput struct {
|
||||
Location string `json:"location" description:"City name"`
|
||||
Units string `json:"units" enum:"celsius,fahrenheit" description:"Temperature units"`
|
||||
|
|
@ -78,34 +81,51 @@ func TestEnumToolExample(t *testing.T) {
|
|||
Input: `{"location": "San Francisco", "units": "fahrenheit"}`,
|
||||
}
|
||||
|
||||
result, err := tool.Run(context.Background(), call)
|
||||
result, err := tool.Run(t.Context(), call)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, result.Content, "San Francisco")
|
||||
require.Contains(t, result.Content, "72°F")
|
||||
}
|
||||
|
||||
func TestNewImageResponse(t *testing.T) {
|
||||
imageData := []byte{0x89, 0x50, 0x4E, 0x47} // PNG header bytes
|
||||
mediaType := "image/png"
|
||||
|
||||
resp := NewImageResponse(imageData, mediaType)
|
||||
|
||||
require.Equal(t, "image", resp.Type)
|
||||
require.Equal(t, imageData, resp.Data)
|
||||
require.Equal(t, mediaType, resp.MediaType)
|
||||
require.False(t, resp.IsError)
|
||||
require.Empty(t, resp.Content)
|
||||
}
|
||||
|
||||
func TestNewMediaResponse(t *testing.T) {
|
||||
audioData := []byte{0x52, 0x49, 0x46, 0x46} // RIFF header bytes
|
||||
mediaType := "audio/wav"
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
mediaType string
|
||||
data []byte
|
||||
wantType string
|
||||
}{
|
||||
{
|
||||
name: "image response",
|
||||
data: []byte{0x89, 0x50, 0x4E, 0x47},
|
||||
mediaType: "image/png",
|
||||
wantType: "image",
|
||||
},
|
||||
{
|
||||
name: "audio response",
|
||||
data: []byte{0x52, 0x49, 0x46, 0x46},
|
||||
mediaType: "audio/wav",
|
||||
wantType: "media",
|
||||
},
|
||||
}
|
||||
|
||||
resp := NewMediaResponse(audioData, mediaType)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var got ToolResponse
|
||||
if tt.wantType == "image" {
|
||||
got = NewImageResponse(tt.data, tt.mediaType)
|
||||
} else {
|
||||
got = NewMediaResponse(tt.data, tt.mediaType)
|
||||
}
|
||||
|
||||
require.Equal(t, "media", resp.Type)
|
||||
require.Equal(t, audioData, resp.Data)
|
||||
require.Equal(t, mediaType, resp.MediaType)
|
||||
require.False(t, resp.IsError)
|
||||
require.Empty(t, resp.Content)
|
||||
require.Empty(t, cmp.Diff(ToolResponse{
|
||||
Type: tt.wantType,
|
||||
Data: tt.data,
|
||||
MediaType: tt.mediaType,
|
||||
IsError: false,
|
||||
Content: "",
|
||||
}, got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ func (m *toolCallingModel) Generate(_ context.Context, call fantasy.Call) (*fant
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (m *toolCallingModel) Stream(_ context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
|
||||
resp, err := m.Generate(context.Background(), call)
|
||||
func (m *toolCallingModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
|
||||
resp, err := m.Generate(ctx, call)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -162,6 +162,7 @@ func (t *echoTool) Execute(_ context.Context, args map[string]interface{}) *tool
|
|||
// TestIntegration_FullAgentLoop_SimpleResponse tests the full agent loop
|
||||
// with a simple mock model that returns text directly (no tool calls).
|
||||
func TestIntegration_FullAgentLoop_SimpleResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-integration-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -183,7 +184,7 @@ func TestIntegration_FullAgentLoop_SimpleResponse(t *testing.T) {
|
|||
model := newMockLanguageModel("Hello from Fantasy agent")
|
||||
al := mustNewAgentLoop(t, cfg, msgBus, model)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
msg := bus.InboundMessage{
|
||||
|
|
@ -225,6 +226,7 @@ func TestIntegration_FullAgentLoop_SimpleResponse(t *testing.T) {
|
|||
// TestIntegration_FullAgentLoop_WithToolCalls tests the full agent loop
|
||||
// including tool call execution and response incorporation.
|
||||
func TestIntegration_FullAgentLoop_WithToolCalls(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-integration-tools-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -249,7 +251,7 @@ func TestIntegration_FullAgentLoop_WithToolCalls(t *testing.T) {
|
|||
// Register the echo tool
|
||||
al.RegisterTool(&echoTool{})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
msg := bus.InboundMessage{
|
||||
|
|
@ -279,6 +281,7 @@ func TestIntegration_FullAgentLoop_WithToolCalls(t *testing.T) {
|
|||
// TestIntegration_ProcessDirect tests the ProcessDirect method
|
||||
// which is used by CLI mode for one-shot message processing.
|
||||
func TestIntegration_ProcessDirect(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-integration-direct-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -300,7 +303,7 @@ func TestIntegration_ProcessDirect(t *testing.T) {
|
|||
model := newMockLanguageModel("Direct CLI response")
|
||||
al := mustNewAgentLoop(t, cfg, msgBus, model)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
response, err := al.ProcessDirect(ctx, "Direct message", "direct-session")
|
||||
|
|
@ -374,6 +377,7 @@ func (m *streamingModel) Model() string { return "streaming-mock" }
|
|||
// TestIntegration_Streaming_TextDeltas tests that the streaming agent loop
|
||||
// publishes text deltas to the bus and returns the complete text.
|
||||
func TestIntegration_Streaming_TextDeltas(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-integration-stream-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -395,7 +399,7 @@ func TestIntegration_Streaming_TextDeltas(t *testing.T) {
|
|||
model := newStreamingModel("Hello from streaming agent response")
|
||||
al := mustNewAgentLoop(t, cfg, msgBus, model)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Collect stream deltas in background
|
||||
|
|
@ -450,6 +454,7 @@ func TestIntegration_Streaming_TextDeltas(t *testing.T) {
|
|||
// TestIntegration_Streaming_WithToolCalls tests streaming with a model
|
||||
// that requests tool calls before producing a final streamed response.
|
||||
func TestIntegration_Streaming_WithToolCalls(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-integration-stream-tools-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -472,7 +477,7 @@ func TestIntegration_Streaming_WithToolCalls(t *testing.T) {
|
|||
al := mustNewAgentLoop(t, cfg, msgBus, model)
|
||||
al.RegisterTool(&echoTool{})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Collect deltas
|
||||
|
|
@ -512,6 +517,7 @@ func TestIntegration_Streaming_WithToolCalls(t *testing.T) {
|
|||
// TestIntegration_MultipleMessages tests sequential message processing
|
||||
// to verify session history accumulation.
|
||||
func TestIntegration_MultipleMessages(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-integration-multi-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -534,7 +540,7 @@ func TestIntegration_MultipleMessages(t *testing.T) {
|
|||
al := mustNewAgentLoop(t, cfg, msgBus, model)
|
||||
|
||||
sessionKey := "multi-msg-session"
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Send 3 messages
|
||||
for i := 0; i < 3; i++ {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package agent_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -17,8 +16,9 @@ func newDelegateKV(t *testing.T, agentID string) *agent.DelegateKV {
|
|||
}
|
||||
|
||||
func TestDelegateKV_PutAndGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newDelegateKV(t, "agent-1")
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
require.NoError(t, kv.Put(ctx, "key1", []byte("hello world")))
|
||||
|
||||
|
|
@ -28,8 +28,9 @@ func TestDelegateKV_PutAndGet(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDelegateKV_GetMissingKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newDelegateKV(t, "agent-1")
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
got, err := kv.Get(ctx, "nonexistent")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -37,8 +38,9 @@ func TestDelegateKV_GetMissingKey(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDelegateKV_PutOverwrite(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newDelegateKV(t, "agent-1")
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
require.NoError(t, kv.Put(ctx, "k", []byte("v1")))
|
||||
require.NoError(t, kv.Put(ctx, "k", []byte("v2")))
|
||||
|
|
@ -49,8 +51,9 @@ func TestDelegateKV_PutOverwrite(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDelegateKV_BinaryValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newDelegateKV(t, "agent-bin")
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Include bytes that need base64 encoding (null bytes, high bytes)
|
||||
data := []byte{0x00, 0xFF, 0x1F, 0x7E, 0x80, 0xAB}
|
||||
|
|
@ -62,8 +65,9 @@ func TestDelegateKV_BinaryValues(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDelegateKV_Scan(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newDelegateKV(t, "agent-scan")
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
keys := []string{"prefix/a", "prefix/b", "prefix/c", "other/x"}
|
||||
for _, k := range keys {
|
||||
|
|
@ -76,8 +80,9 @@ func TestDelegateKV_Scan(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDelegateKV_ScanEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newDelegateKV(t, "agent-scan-empty")
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
got, err := kv.Scan(ctx, "nothing/")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -85,8 +90,9 @@ func TestDelegateKV_ScanEmpty(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDelegateKV_ScanSorted(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newDelegateKV(t, "agent-sorted")
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Insert out of order
|
||||
for _, k := range []string{"z/c", "z/a", "z/b"} {
|
||||
|
|
@ -99,10 +105,11 @@ func TestDelegateKV_ScanSorted(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDelegateKV_AgentIsolation(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := newTestQueries(t)
|
||||
kv1 := agent.NewDelegateKV(db.delegate, "agent-A")
|
||||
kv2 := agent.NewDelegateKV(db.delegate, "agent-B")
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
require.NoError(t, kv1.Put(ctx, "shared-key", []byte("from-A")))
|
||||
require.NoError(t, kv2.Put(ctx, "shared-key", []byte("from-B")))
|
||||
|
|
@ -117,13 +124,15 @@ func TestDelegateKV_AgentIsolation(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDelegateKV_EmptyKey_PutErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newDelegateKV(t, "agent-1")
|
||||
err := kv.Put(context.Background(), "", []byte("val"))
|
||||
err := kv.Put(t.Context(), "", []byte("val"))
|
||||
assert.Error(t, err, "empty key should be rejected")
|
||||
}
|
||||
|
||||
func TestDelegateKV_EmptyKey_GetErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newDelegateKV(t, "agent-1")
|
||||
_, err := kv.Get(context.Background(), "")
|
||||
_, err := kv.Get(t.Context(), "")
|
||||
assert.Error(t, err, "empty key should be rejected")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func mustNewAgentLoop(t *testing.T, cfg *config.Config, msgBus *bus.MessageBus,
|
|||
if cfg != nil && cfg.Memory.DBPath == "" && strings.TrimSpace(cfg.Agents.Defaults.Workspace) != "" {
|
||||
cfg.Memory.DBPath = filepath.Join(cfg.Agents.Defaults.Workspace, "agent-loop-test.db")
|
||||
}
|
||||
al, err := NewAgentLoop(context.Background(), cfg, msgBus, model)
|
||||
al, err := NewAgentLoop(t.Context(), cfg, msgBus, model)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAgentLoop: %v", err)
|
||||
}
|
||||
|
|
@ -73,6 +73,7 @@ func (m *mockLanguageModel) Provider() string { return "mock" }
|
|||
func (m *mockLanguageModel) Model() string { return "mock-model" }
|
||||
|
||||
func TestContinuityKeepCount_UsesConfiguredPolicy(t *testing.T) {
|
||||
t.Parallel()
|
||||
buildHistory := func(n int, content string) []messages.Message {
|
||||
history := make([]messages.Message, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
|
|
@ -113,6 +114,7 @@ func TestContinuityKeepCount_UsesConfiguredPolicy(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPrepareRuntimeState_ConcurrentSameSessionUsesSingleConversation(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -133,7 +135,7 @@ func TestPrepareRuntimeState_ConcurrentSameSessionUsesSingleConversation(t *test
|
|||
msgBus := bus.NewMessageBus()
|
||||
model := newMockLanguageModel("")
|
||||
al := mustNewAgentLoop(t, cfg, msgBus, model)
|
||||
beforeConversations, err := al.queries.ListAgentConversations(context.Background(), memsqlc.ListAgentConversationsParams{
|
||||
beforeConversations, err := al.queries.ListAgentConversations(t.Context(), memsqlc.ListAgentConversationsParams{
|
||||
Limit: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -152,7 +154,7 @@ func TestPrepareRuntimeState_ConcurrentSameSessionUsesSingleConversation(t *test
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
<-start
|
||||
conversationID, _, prepareErr := al.prepareRuntimeState(context.Background(), "race-session")
|
||||
conversationID, _, prepareErr := al.prepareRuntimeState(t.Context(), "race-session")
|
||||
if prepareErr != nil {
|
||||
errorsCh <- prepareErr
|
||||
return
|
||||
|
|
@ -180,7 +182,7 @@ func TestPrepareRuntimeState_ConcurrentSameSessionUsesSingleConversation(t *test
|
|||
t.Fatalf("expected one conversation id, got %d (%v)", len(uniqueConversationIDs), uniqueConversationIDs)
|
||||
}
|
||||
|
||||
conversations, err := al.queries.ListAgentConversations(context.Background(), memsqlc.ListAgentConversationsParams{
|
||||
conversations, err := al.queries.ListAgentConversations(t.Context(), memsqlc.ListAgentConversationsParams{
|
||||
Limit: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -192,7 +194,10 @@ func TestPrepareRuntimeState_ConcurrentSameSessionUsesSingleConversation(t *test
|
|||
}
|
||||
|
||||
func TestRecordLastChannel(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Create temp workspace
|
||||
)
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -218,7 +223,7 @@ func TestRecordLastChannel(t *testing.T) {
|
|||
|
||||
// Test RecordLastChannel
|
||||
testChannel := "test-channel"
|
||||
err = al.RecordLastChannel(context.Background(), testChannel)
|
||||
err = al.RecordLastChannel(t.Context(), testChannel)
|
||||
if err != nil {
|
||||
t.Fatalf("RecordLastChannel failed: %v", err)
|
||||
}
|
||||
|
|
@ -237,7 +242,10 @@ func TestRecordLastChannel(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRecordLastChatID(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Create temp workspace
|
||||
)
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -263,7 +271,7 @@ func TestRecordLastChatID(t *testing.T) {
|
|||
|
||||
// Test RecordLastChatID
|
||||
testChatID := "test-chat-id-123"
|
||||
err = al.RecordLastChatID(context.Background(), testChatID)
|
||||
err = al.RecordLastChatID(t.Context(), testChatID)
|
||||
if err != nil {
|
||||
t.Fatalf("RecordLastChatID failed: %v", err)
|
||||
}
|
||||
|
|
@ -282,7 +290,10 @@ func TestRecordLastChatID(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewAgentLoop_StateInitialized(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Create temp workspace
|
||||
)
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -313,6 +324,7 @@ func TestNewAgentLoop_StateInitialized(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewAgentLoop_UnifiedKernelDependenciesInitialized(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -344,6 +356,7 @@ func TestNewAgentLoop_UnifiedKernelDependenciesInitialized(t *testing.T) {
|
|||
|
||||
// TestToolRegistry_ToolRegistration verifies tools can be registered and retrieved
|
||||
func TestToolRegistry_ToolRegistration(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -390,6 +403,7 @@ func TestToolRegistry_ToolRegistration(t *testing.T) {
|
|||
|
||||
// TestToolContext_Updates verifies tool context is updated with channel/chatID
|
||||
func TestToolContext_Updates(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -421,6 +435,7 @@ func TestToolContext_Updates(t *testing.T) {
|
|||
|
||||
// TestToolRegistry_GetDefinitions verifies tool definitions can be retrieved
|
||||
func TestToolRegistry_GetDefinitions(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -465,6 +480,7 @@ func TestToolRegistry_GetDefinitions(t *testing.T) {
|
|||
|
||||
// TestAgentLoop_GetStartupInfo verifies startup info contains tools
|
||||
func TestAgentLoop_GetStartupInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -512,6 +528,7 @@ func TestAgentLoop_GetStartupInfo(t *testing.T) {
|
|||
|
||||
// TestAgentLoop_Stop verifies Stop() sets running to false
|
||||
func TestAgentLoop_Stop(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -619,6 +636,7 @@ const responseTimeout = 3 * time.Second
|
|||
|
||||
// TestToolResult_SilentToolDoesNotSendUserMessage verifies silent tools don't trigger outbound
|
||||
func TestToolResult_SilentToolDoesNotSendUserMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -642,7 +660,7 @@ func TestToolResult_SilentToolDoesNotSendUserMessage(t *testing.T) {
|
|||
helper := testHelper{al: al}
|
||||
|
||||
// ReadFileTool returns SilentResult, which should not send user message
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
msg := bus.InboundMessage{
|
||||
Channel: "test",
|
||||
SenderID: "user1",
|
||||
|
|
@ -661,6 +679,7 @@ func TestToolResult_SilentToolDoesNotSendUserMessage(t *testing.T) {
|
|||
|
||||
// TestToolResult_UserFacingToolDoesSendMessage verifies user-facing tools trigger outbound
|
||||
func TestToolResult_UserFacingToolDoesSendMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -684,7 +703,7 @@ func TestToolResult_UserFacingToolDoesSendMessage(t *testing.T) {
|
|||
helper := testHelper{al: al}
|
||||
|
||||
// ExecTool returns UserResult, which should send user message
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
msg := bus.InboundMessage{
|
||||
Channel: "test",
|
||||
SenderID: "user1",
|
||||
|
|
@ -702,6 +721,7 @@ func TestToolResult_UserFacingToolDoesSendMessage(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResolveFinalContent_RecoversFromPriorStepText(t *testing.T) {
|
||||
t.Parallel()
|
||||
al := &AgentLoop{}
|
||||
steps := []fantasy.StepResult{
|
||||
{
|
||||
|
|
@ -730,6 +750,7 @@ func TestResolveFinalContent_RecoversFromPriorStepText(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResolveFinalContent_ErrorsWhenNoTextExists(t *testing.T) {
|
||||
t.Parallel()
|
||||
al := &AgentLoop{}
|
||||
steps := []fantasy.StepResult{
|
||||
{
|
||||
|
|
@ -748,6 +769,7 @@ func TestResolveFinalContent_ErrorsWhenNoTextExists(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResolveFinalContent_RecoversFromToolResultText(t *testing.T) {
|
||||
t.Parallel()
|
||||
al := &AgentLoop{}
|
||||
steps := []fantasy.StepResult{
|
||||
{
|
||||
|
|
@ -776,6 +798,7 @@ func TestResolveFinalContent_RecoversFromToolResultText(t *testing.T) {
|
|||
// TestForceCompression_PersistsProvenance verifies that emergency compression
|
||||
// cycles persist provenance metadata to the audit log for postmortem.
|
||||
func TestForceCompression_PersistsProvenance(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-provenance-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -827,7 +850,7 @@ func TestForceCompression_PersistsProvenance(t *testing.T) {
|
|||
t.Fatalf("test precondition failed: token_estimate=%d threshold=%d", tokenEstimate, criticalThreshold)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
al.forceCompression(ctx, sessionKey, "", "")
|
||||
|
||||
del := al.MemoryDelegate()
|
||||
|
|
@ -872,6 +895,7 @@ func TestForceCompression_PersistsProvenance(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPersistOversizedRecoveryRefs_CreatesRecoverableReferences(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "agent-recovery-ref-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -904,7 +928,7 @@ func TestPersistOversizedRecoveryRefs_CreatesRecoverableReferences(t *testing.T)
|
|||
},
|
||||
}
|
||||
|
||||
refs, err := al.persistOversizedRecoveryRefs(context.Background(), "recovery-session", omitted)
|
||||
refs, err := al.persistOversizedRecoveryRefs(t.Context(), "recovery-session", omitted)
|
||||
if err != nil {
|
||||
t.Fatalf("persistOversizedRecoveryRefs failed: %v", err)
|
||||
}
|
||||
|
|
@ -919,7 +943,7 @@ func TestPersistOversizedRecoveryRefs_CreatesRecoverableReferences(t *testing.T)
|
|||
return "recovery-session"
|
||||
},
|
||||
})
|
||||
res := dagTool.Execute(context.Background(), map[string]interface{}{
|
||||
res := dagTool.Execute(t.Context(), map[string]interface{}{
|
||||
"node_id": refs[0],
|
||||
"session_key": "recovery-session",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func makeOffloader(t *testing.T, base fantasy.ToolRuntime, threshold, chunkChars
|
|||
q := db.delegate.Queries()
|
||||
convID := newConversation(t, q)
|
||||
s := agent.NewStateStore(q)
|
||||
run, err := s.CreateRun(context.Background(), convID)
|
||||
run, err := s.CreateRun(t.Context(), convID)
|
||||
require.NoError(t, err)
|
||||
|
||||
kv := agent.NewDelegateKV(db.delegate, "offload-test")
|
||||
|
|
@ -76,8 +76,9 @@ func makeCalls(n int) []fantasy.ToolCallContent {
|
|||
// 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 := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
calls := makeCalls(1)
|
||||
results, err := r.Execute(ctx, nil, calls, nil)
|
||||
|
|
@ -95,6 +96,7 @@ func TestOffloading_SmallResult_KeptInline(t *testing.T) {
|
|||
// 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{
|
||||
|
|
@ -106,7 +108,7 @@ func TestOffloading_LargeResult_Truncated(t *testing.T) {
|
|||
}}
|
||||
|
||||
r, _, _, kv := makeOffloader(t, base, 50, 30)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
calls := makeCalls(1)
|
||||
calls[0].ToolCallID = "call-a"
|
||||
|
|
@ -149,8 +151,9 @@ func TestOffloading_LargeResult_Truncated(t *testing.T) {
|
|||
|
||||
// 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 := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
calls := makeCalls(2)
|
||||
_, err := r.Execute(ctx, nil, calls, nil)
|
||||
|
|
@ -165,11 +168,12 @@ func TestOffloading_DBMetadata_Inserted(t *testing.T) {
|
|||
|
||||
// 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(context.Background(), convID)
|
||||
run, err := s.CreateRun(t.Context(), convID)
|
||||
require.NoError(t, err)
|
||||
|
||||
r := &agent.OffloadingToolRuntime{
|
||||
|
|
@ -178,12 +182,13 @@ func TestOffloading_NilKV_Errors(t *testing.T) {
|
|||
RunID: run.ID,
|
||||
}
|
||||
|
||||
_, err = r.Execute(context.Background(), nil, makeCalls(1), nil)
|
||||
_, 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")
|
||||
|
||||
|
|
@ -193,15 +198,16 @@ func TestOffloading_NilQueries_Errors(t *testing.T) {
|
|||
RunID: ids.New(),
|
||||
}
|
||||
|
||||
_, err := r.Execute(context.Background(), nil, makeCalls(1), nil)
|
||||
_, 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(context.Background(), nil, nil, nil)
|
||||
results, err := r.Execute(t.Context(), nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, results, "no tool calls should produce no results")
|
||||
}
|
||||
|
|
@ -209,8 +215,9 @@ func TestOffloading_EmptyToolCalls_ReturnsNil(t *testing.T) {
|
|||
// 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 := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
calls := makeCalls(3)
|
||||
_, err := r.Execute(ctx, nil, calls, nil)
|
||||
|
|
@ -230,6 +237,7 @@ func TestOffloading_MultipleCalls_AllStoredInKV(t *testing.T) {
|
|||
|
||||
// TestChunkString verifies the internal chunking logic boundary conditions.
|
||||
func TestChunkString(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -268,10 +276,10 @@ func TestChunkString(t *testing.T) {
|
|||
|
||||
r, _, _, kv := makeOffloader(t, base, effectiveThreshold, tt.chunkSize)
|
||||
calls := []fantasy.ToolCallContent{{ToolCallID: "c", ToolName: "t"}}
|
||||
_, err := r.Execute(context.Background(), nil, calls, nil)
|
||||
_, err := r.Execute(t.Context(), nil, calls, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
keys, err := kv.Scan(context.Background(), "tool_results/")
|
||||
keys, err := kv.Scan(t.Context(), "tool_results/")
|
||||
require.NoError(t, err)
|
||||
|
||||
chunkCount := 0
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package agent_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
|
|
@ -19,7 +20,7 @@ func newTestQueries(t *testing.T) *testDB {
|
|||
t.Helper()
|
||||
d, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err, "NewLibSQLInMemory")
|
||||
require.NoError(t, d.Init(context.Background()), "Init")
|
||||
require.NoError(t, d.Init(t.Context()), "Init")
|
||||
t.Cleanup(func() { _ = d.Close() })
|
||||
return &testDB{delegate: d}
|
||||
}
|
||||
|
|
@ -36,7 +37,7 @@ func newConversation(t *testing.T, q *sqlc.Queries) ids.UUID {
|
|||
t.Helper()
|
||||
id := ids.New()
|
||||
title := "test-conv"
|
||||
_, err := q.CreateAgentConversation(context.Background(), sqlc.CreateAgentConversationParams{
|
||||
_, err := q.CreateAgentConversation(t.Context(), sqlc.CreateAgentConversationParams{
|
||||
ID: id,
|
||||
Title: &title,
|
||||
})
|
||||
|
|
@ -45,34 +46,39 @@ func newConversation(t *testing.T, q *sqlc.Queries) ids.UUID {
|
|||
}
|
||||
|
||||
func TestStateStore_CreateRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := newTestQueries(t)
|
||||
q := db.delegate.Queries()
|
||||
s := agent.NewStateStore(q)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
convID := newConversation(t, q)
|
||||
run, err := s.CreateRun(ctx, convID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, run.ID.IsZero(), "run ID must be set")
|
||||
assert.Equal(t, convID, run.ConversationID)
|
||||
assert.Equal(t, "running", run.Status)
|
||||
assert.Empty(t, cmp.Diff(sqlc.AgentRun{
|
||||
ConversationID: convID,
|
||||
Status: "running",
|
||||
}, run, cmpopts.IgnoreFields(sqlc.AgentRun{}, "ID", "MetadataJson", "CreatedAt", "UpdatedAt")))
|
||||
}
|
||||
|
||||
func TestStateStore_CreateRun_ZeroConversationID(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := newTestQueries(t)
|
||||
s := agent.NewStateStore(db.delegate.Queries())
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
_, err := s.CreateRun(ctx, ids.UUID{})
|
||||
assert.Error(t, err, "zero conversation id should be rejected")
|
||||
}
|
||||
|
||||
func TestStateStore_UpdateRunStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := newTestQueries(t)
|
||||
q := db.delegate.Queries()
|
||||
s := agent.NewStateStore(q)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
convID := newConversation(t, q)
|
||||
run, err := s.CreateRun(ctx, convID)
|
||||
|
|
@ -81,15 +87,18 @@ func TestStateStore_UpdateRunStatus(t *testing.T) {
|
|||
updated, err := s.UpdateRunStatus(ctx, run.ID, "completed", map[string]any{"steps": 3})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, run.ID, updated.ID)
|
||||
assert.Equal(t, "completed", updated.Status)
|
||||
assert.Empty(t, cmp.Diff(sqlc.AgentRun{
|
||||
ID: run.ID,
|
||||
Status: "completed",
|
||||
}, updated, cmpopts.IgnoreFields(sqlc.AgentRun{}, "ConversationID", "MetadataJson", "CreatedAt", "UpdatedAt")))
|
||||
}
|
||||
|
||||
func TestStateStore_UpdateRunStatus_EmptyStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := newTestQueries(t)
|
||||
q := db.delegate.Queries()
|
||||
s := agent.NewStateStore(q)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
convID := newConversation(t, q)
|
||||
run, err := s.CreateRun(ctx, convID)
|
||||
|
|
@ -100,10 +109,11 @@ func TestStateStore_UpdateRunStatus_EmptyStatus(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStateStore_AddRunState(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := newTestQueries(t)
|
||||
q := db.delegate.Queries()
|
||||
s := agent.NewStateStore(q)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
convID := newConversation(t, q)
|
||||
run, err := s.CreateRun(ctx, convID)
|
||||
|
|
@ -112,17 +122,19 @@ func TestStateStore_AddRunState(t *testing.T) {
|
|||
state, err := s.AddRunState(ctx, run.ID, 0, fantasy.ReActStateLLMCall, map[string]string{"model": "gpt-4o"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, state.ID.IsZero())
|
||||
assert.Equal(t, run.ID, state.RunID)
|
||||
assert.Equal(t, int64(0), state.StepIndex)
|
||||
assert.Equal(t, string(fantasy.ReActStateLLMCall), state.State)
|
||||
assert.Empty(t, cmp.Diff(sqlc.AgentRunState{
|
||||
RunID: run.ID,
|
||||
StepIndex: 0,
|
||||
State: string(fantasy.ReActStateLLMCall),
|
||||
}, state, cmpopts.IgnoreFields(sqlc.AgentRunState{}, "ID", "SnapshotJson", "CreatedAt", "UpdatedAt")))
|
||||
}
|
||||
|
||||
func TestStateStore_AddRunState_NegativeStep(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := newTestQueries(t)
|
||||
q := db.delegate.Queries()
|
||||
s := agent.NewStateStore(q)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
convID := newConversation(t, q)
|
||||
run, err := s.CreateRun(ctx, convID)
|
||||
|
|
@ -133,10 +145,11 @@ func TestStateStore_AddRunState_NegativeStep(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStateStore_AddTransition(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := newTestQueries(t)
|
||||
q := db.delegate.Queries()
|
||||
s := agent.NewStateStore(q)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
convID := newConversation(t, q)
|
||||
run, err := s.CreateRun(ctx, convID)
|
||||
|
|
@ -152,15 +165,19 @@ func TestStateStore_AddTransition(t *testing.T) {
|
|||
row, err := s.AddTransition(ctx, run.ID, tr)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, row.ID.IsZero())
|
||||
assert.Equal(t, string(fantasy.ReActStateInit), row.FromState)
|
||||
assert.Equal(t, string(fantasy.ReActStatePrepareStep), row.ToState)
|
||||
assert.Equal(t, string(fantasy.ReActTriggerStart), row.Trigger)
|
||||
assert.Empty(t, cmp.Diff(sqlc.AgentStateTransition{
|
||||
RunID: run.ID,
|
||||
StepIndex: 0,
|
||||
FromState: string(fantasy.ReActStateInit),
|
||||
ToState: string(fantasy.ReActStatePrepareStep),
|
||||
Trigger: string(fantasy.ReActTriggerStart),
|
||||
}, row, cmpopts.IgnoreFields(sqlc.AgentStateTransition{}, "ID", "At", "MetaJson", "Error", "CreatedAt", "UpdatedAt")))
|
||||
}
|
||||
|
||||
func TestStateStore_NilStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s *agent.StateStore
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
_, err := s.CreateRun(ctx, ids.New())
|
||||
assert.Error(t, err, "nil store should error")
|
||||
|
|
@ -169,11 +186,12 @@ func TestStateStore_NilStore(t *testing.T) {
|
|||
// --- CheckpointStore ---
|
||||
|
||||
func TestCheckpointStore_CreateAndList(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := newTestQueries(t)
|
||||
q := db.delegate.Queries()
|
||||
ss := agent.NewStateStore(q)
|
||||
cs := agent.NewCheckpointStore(q)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
convID := newConversation(t, q)
|
||||
run, err := ss.CreateRun(ctx, convID)
|
||||
|
|
@ -195,11 +213,12 @@ func TestCheckpointStore_CreateAndList(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCheckpointStore_GetByName(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := newTestQueries(t)
|
||||
q := db.delegate.Queries()
|
||||
ss := agent.NewStateStore(q)
|
||||
cs := agent.NewCheckpointStore(q)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
convID := newConversation(t, q)
|
||||
run, err := ss.CreateRun(ctx, convID)
|
||||
|
|
@ -217,10 +236,11 @@ func TestCheckpointStore_GetByName(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCheckpointStore_EmptyName(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := newTestQueries(t)
|
||||
q := db.delegate.Queries()
|
||||
cs := agent.NewCheckpointStore(q)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
convID := newConversation(t, q)
|
||||
_, err := cs.CreateCheckpoint(ctx, convID, " ", ids.New(), nil)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package agent_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -21,7 +21,7 @@ func setupSearchFixture(t *testing.T) (fantasy.AgentTool, agent.KVDelegate, stri
|
|||
q := db.delegate.Queries()
|
||||
convID := newConversation(t, q)
|
||||
s := agent.NewStateStore(q)
|
||||
run, err := s.CreateRun(context.Background(), convID)
|
||||
run, err := s.CreateRun(t.Context(), convID)
|
||||
require.NoError(t, err)
|
||||
|
||||
kv := agent.NewDelegateKV(db.delegate, "search-test")
|
||||
|
|
@ -41,7 +41,7 @@ func setupSearchFixture(t *testing.T) (fantasy.AgentTool, agent.KVDelegate, stri
|
|||
{ToolCallID: "c2", ToolName: "beta"},
|
||||
{ToolCallID: "c3", ToolName: "alpha"},
|
||||
}
|
||||
_, err = r.Execute(context.Background(), nil, calls, nil)
|
||||
_, err = r.Execute(t.Context(), nil, calls, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
tool := agent.NewToolResultSearchTool(q, kv)
|
||||
|
|
@ -51,7 +51,7 @@ func setupSearchFixture(t *testing.T) (fantasy.AgentTool, agent.KVDelegate, stri
|
|||
// invokeSearch calls the search tool with the given input and parses the JSON response.
|
||||
func invokeSearch(t *testing.T, tool fantasy.AgentTool, input agent.ToolResultSearchInput) map[string]any {
|
||||
t.Helper()
|
||||
resp, err := tool.Run(context.Background(), fantasy.ToolCall{
|
||||
resp, err := tool.Run(t.Context(), fantasy.ToolCall{
|
||||
Input: marshalInput(t, input),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -69,89 +69,102 @@ func marshalInput(t *testing.T, v any) string {
|
|||
return string(b)
|
||||
}
|
||||
|
||||
func TestToolResultSearch_ByConversationID(t *testing.T) {
|
||||
tool, _, convID, _ := setupSearchFixture(t)
|
||||
ctx := context.Background()
|
||||
_ = ctx
|
||||
func TestToolResultSearch_QueryFilters(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
out := invokeSearch(t, tool, agent.ToolResultSearchInput{
|
||||
ConversationID: convID,
|
||||
Limit: 10,
|
||||
})
|
||||
tests := []struct {
|
||||
name string
|
||||
build func(convID, runID string) agent.ToolResultSearchInput
|
||||
wantTotal float64
|
||||
wantTCID string
|
||||
}{
|
||||
{
|
||||
name: "by conversation ID",
|
||||
build: func(convID, _ string) agent.ToolResultSearchInput {
|
||||
return agent.ToolResultSearchInput{
|
||||
ConversationID: convID,
|
||||
Limit: 10,
|
||||
}
|
||||
},
|
||||
wantTotal: 3,
|
||||
},
|
||||
{
|
||||
name: "by run ID",
|
||||
build: func(_, runID string) agent.ToolResultSearchInput {
|
||||
return agent.ToolResultSearchInput{
|
||||
RunID: runID,
|
||||
Limit: 10,
|
||||
}
|
||||
},
|
||||
wantTotal: 3,
|
||||
},
|
||||
{
|
||||
name: "by run ID and tool_call_id",
|
||||
build: func(_, runID string) agent.ToolResultSearchInput {
|
||||
return agent.ToolResultSearchInput{
|
||||
RunID: runID,
|
||||
ToolCallID: "c2",
|
||||
}
|
||||
},
|
||||
wantTotal: 1,
|
||||
wantTCID: "c2",
|
||||
},
|
||||
{
|
||||
name: "filter by tool_name",
|
||||
build: func(convID, _ string) agent.ToolResultSearchInput {
|
||||
return agent.ToolResultSearchInput{
|
||||
ConversationID: convID,
|
||||
ToolName: "alpha",
|
||||
Limit: 10,
|
||||
}
|
||||
},
|
||||
wantTotal: 2,
|
||||
},
|
||||
{
|
||||
name: "filter by query",
|
||||
build: func(convID, _ string) agent.ToolResultSearchInput {
|
||||
return agent.ToolResultSearchInput{
|
||||
ConversationID: convID,
|
||||
Query: "beta",
|
||||
Limit: 10,
|
||||
}
|
||||
},
|
||||
wantTotal: 1,
|
||||
},
|
||||
{
|
||||
name: "default limit",
|
||||
build: func(convID, _ string) agent.ToolResultSearchInput {
|
||||
return agent.ToolResultSearchInput{
|
||||
ConversationID: convID,
|
||||
}
|
||||
},
|
||||
wantTotal: 3,
|
||||
},
|
||||
}
|
||||
|
||||
total, _ := out["total"].(float64)
|
||||
assert.Equal(t, float64(3), total, "should find all 3 results by conversation_id")
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tool, _, convID, runID := setupSearchFixture(t)
|
||||
input := tt.build(convID, runID)
|
||||
out := invokeSearch(t, tool, input)
|
||||
|
||||
func TestToolResultSearch_ByRunID(t *testing.T) {
|
||||
tool, _, _, runID := setupSearchFixture(t)
|
||||
total, _ := out["total"].(float64)
|
||||
assert.Equal(t, tt.wantTotal, total)
|
||||
|
||||
out := invokeSearch(t, tool, agent.ToolResultSearchInput{
|
||||
RunID: runID,
|
||||
Limit: 10,
|
||||
})
|
||||
|
||||
total, _ := out["total"].(float64)
|
||||
assert.Equal(t, float64(3), total)
|
||||
}
|
||||
|
||||
func TestToolResultSearch_ByRunID_AndToolCallID(t *testing.T) {
|
||||
tool, _, _, runID := setupSearchFixture(t)
|
||||
|
||||
out := invokeSearch(t, tool, agent.ToolResultSearchInput{
|
||||
RunID: runID,
|
||||
ToolCallID: "c2",
|
||||
})
|
||||
|
||||
total, _ := out["total"].(float64)
|
||||
assert.Equal(t, float64(1), total)
|
||||
items := out["items"].([]any)
|
||||
item := items[0].(map[string]any)
|
||||
assert.Equal(t, "c2", item["tool_call_id"])
|
||||
}
|
||||
|
||||
func TestToolResultSearch_FilterByToolName(t *testing.T) {
|
||||
tool, _, convID, _ := setupSearchFixture(t)
|
||||
|
||||
out := invokeSearch(t, tool, agent.ToolResultSearchInput{
|
||||
ConversationID: convID,
|
||||
ToolName: "alpha",
|
||||
Limit: 10,
|
||||
})
|
||||
|
||||
total, _ := out["total"].(float64)
|
||||
assert.Equal(t, float64(2), total, "filter by tool_name should return only 'alpha' results")
|
||||
}
|
||||
|
||||
func TestToolResultSearch_FilterByQuery(t *testing.T) {
|
||||
tool, _, convID, _ := setupSearchFixture(t)
|
||||
|
||||
out := invokeSearch(t, tool, agent.ToolResultSearchInput{
|
||||
ConversationID: convID,
|
||||
Query: "beta",
|
||||
Limit: 10,
|
||||
})
|
||||
|
||||
total, _ := out["total"].(float64)
|
||||
assert.Equal(t, float64(1), total)
|
||||
}
|
||||
|
||||
func TestToolResultSearch_DefaultLimit(t *testing.T) {
|
||||
tool, _, convID, _ := setupSearchFixture(t)
|
||||
|
||||
// No limit specified -- default is 5, but we only have 3 items
|
||||
out := invokeSearch(t, tool, agent.ToolResultSearchInput{
|
||||
ConversationID: convID,
|
||||
})
|
||||
|
||||
total, _ := out["total"].(float64)
|
||||
assert.Equal(t, float64(3), total)
|
||||
if tt.wantTCID != "" {
|
||||
items := out["items"].([]any)
|
||||
item := items[0].(map[string]any)
|
||||
assert.Equal(t, tt.wantTCID, item["tool_call_id"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolResultSearch_MissingConvAndRunID_ErrorResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool, _, _, _ := setupSearchFixture(t)
|
||||
|
||||
resp, err := tool.Run(context.Background(), fantasy.ToolCall{
|
||||
resp, err := tool.Run(t.Context(), fantasy.ToolCall{
|
||||
Input: marshalInput(t, agent.ToolResultSearchInput{}),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -163,11 +176,12 @@ func TestToolResultSearch_MissingConvAndRunID_ErrorResponse(t *testing.T) {
|
|||
// TestToolResultSearch_LineView verifies that the KV full-result is sliced
|
||||
// into the requested line range.
|
||||
func TestToolResultSearch_LineView(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := newTestQueries(t)
|
||||
q := db.delegate.Queries()
|
||||
convID := newConversation(t, q)
|
||||
s := agent.NewStateStore(q)
|
||||
run, err := s.CreateRun(context.Background(), convID)
|
||||
run, err := s.CreateRun(t.Context(), convID)
|
||||
require.NoError(t, err)
|
||||
|
||||
kv := agent.NewDelegateKV(db.delegate, "line-view-test")
|
||||
|
|
@ -189,7 +203,7 @@ func TestToolResultSearch_LineView(t *testing.T) {
|
|||
RunID: run.ID,
|
||||
ThresholdChars: 100000,
|
||||
}
|
||||
_, err = r.Execute(context.Background(), nil, []fantasy.ToolCallContent{{ToolCallID: "lv1", ToolName: "liner"}}, nil)
|
||||
_, err = r.Execute(t.Context(), nil, []fantasy.ToolCallContent{{ToolCallID: "lv1", ToolName: "liner"}}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
tool := agent.NewToolResultSearchTool(q, kv)
|
||||
|
|
@ -220,11 +234,12 @@ func TestToolResultSearch_LineView(t *testing.T) {
|
|||
|
||||
// TestToolResultSearch_ChunkView verifies chunk-range retrieval from KV.
|
||||
func TestToolResultSearch_ChunkView(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := newTestQueries(t)
|
||||
q := db.delegate.Queries()
|
||||
convID := newConversation(t, q)
|
||||
s := agent.NewStateStore(q)
|
||||
run, err := s.CreateRun(context.Background(), convID)
|
||||
run, err := s.CreateRun(t.Context(), convID)
|
||||
require.NoError(t, err)
|
||||
|
||||
kv := agent.NewDelegateKV(db.delegate, "chunk-view-test")
|
||||
|
|
@ -247,7 +262,7 @@ func TestToolResultSearch_ChunkView(t *testing.T) {
|
|||
ThresholdChars: 10,
|
||||
ChunkChars: 20,
|
||||
}
|
||||
_, err = r.Execute(context.Background(), nil, []fantasy.ToolCallContent{{ToolCallID: "cv1", ToolName: "chunker"}}, nil)
|
||||
_, err = r.Execute(t.Context(), nil, []fantasy.ToolCallContent{{ToolCallID: "cv1", ToolName: "chunker"}}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
tool := agent.NewToolResultSearchTool(q, kv)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ func makeJWTForClaims(t *testing.T, claims map[string]interface{}) string {
|
|||
}
|
||||
|
||||
func TestBuildAuthorizeURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := OAuthProviderConfig{
|
||||
Issuer: "https://auth.example.com",
|
||||
ClientID: "test-client-id",
|
||||
|
|
@ -68,6 +69,7 @@ func TestBuildAuthorizeURL(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBuildAuthorizeURLOpenAIExtras(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := OpenAIOAuthConfig()
|
||||
pkce := PKCECodes{CodeVerifier: "test-verifier", CodeChallenge: "test-challenge"}
|
||||
|
||||
|
|
@ -90,6 +92,7 @@ func TestBuildAuthorizeURLOpenAIExtras(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseTokenResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
resp := map[string]interface{}{
|
||||
"access_token": "test-access-token",
|
||||
"refresh_token": "test-refresh-token",
|
||||
|
|
@ -121,6 +124,7 @@ func TestParseTokenResponse(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseTokenResponseExtractsAccountIDFromIDToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
idToken := makeJWTForClaims(t, map[string]interface{}{"chatgpt_account_id": "acc-id-from-id-token"})
|
||||
resp := map[string]interface{}{
|
||||
"access_token": "opaque-access-token",
|
||||
|
|
@ -140,6 +144,7 @@ func TestParseTokenResponseExtractsAccountIDFromIDToken(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractAccountIDFromOrganizationsFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
token := makeJWTForClaims(t, map[string]interface{}{
|
||||
"organizations": []interface{}{
|
||||
map[string]interface{}{"id": "org_from_orgs"},
|
||||
|
|
@ -152,6 +157,7 @@ func TestExtractAccountIDFromOrganizationsFallback(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseTokenResponseNoAccessToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := []byte(`{"refresh_token": "test"}`)
|
||||
_, err := parseTokenResponse(body, "openai")
|
||||
if err == nil {
|
||||
|
|
@ -160,6 +166,7 @@ func TestParseTokenResponseNoAccessToken(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseTokenResponseAccountIDFromIDToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
idToken := makeJWTWithAccountID("acc-from-id")
|
||||
resp := map[string]interface{}{
|
||||
"access_token": "not-a-jwt",
|
||||
|
|
@ -186,6 +193,7 @@ func makeJWTWithAccountID(accountID string) string {
|
|||
}
|
||||
|
||||
func TestExchangeCodeForTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/oauth/token" {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
|
|
@ -229,6 +237,7 @@ func TestExchangeCodeForTokens(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRefreshAccessToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/oauth/token" {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
|
|
@ -276,6 +285,7 @@ func TestRefreshAccessToken(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRefreshAccessTokenNoRefreshToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := OpenAIOAuthConfig()
|
||||
cred := &AuthCredential{
|
||||
AccessToken: "old-token",
|
||||
|
|
@ -290,6 +300,7 @@ func TestRefreshAccessTokenNoRefreshToken(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRefreshAccessTokenPreservesRefreshAndAccountID(t *testing.T) {
|
||||
t.Parallel()
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := map[string]interface{}{
|
||||
"access_token": "new-access-token-only",
|
||||
|
|
@ -321,6 +332,7 @@ func TestRefreshAccessTokenPreservesRefreshAndAccountID(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOpenAIOAuthConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := OpenAIOAuthConfig()
|
||||
if cfg.Issuer != "https://auth.openai.com" {
|
||||
t.Errorf("Issuer = %q, want %q", cfg.Issuer, "https://auth.openai.com")
|
||||
|
|
@ -334,6 +346,7 @@ func TestOpenAIOAuthConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseDeviceCodeResponseIntervalAsNumber(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := []byte(`{"device_auth_id":"abc","user_code":"DEF-1234","interval":5}`)
|
||||
|
||||
resp, err := parseDeviceCodeResponse(body)
|
||||
|
|
@ -353,6 +366,7 @@ func TestParseDeviceCodeResponseIntervalAsNumber(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseDeviceCodeResponseIntervalAsString(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := []byte(`{"device_auth_id":"abc","user_code":"DEF-1234","interval":"5"}`)
|
||||
|
||||
resp, err := parseDeviceCodeResponse(body)
|
||||
|
|
@ -366,6 +380,7 @@ func TestParseDeviceCodeResponseIntervalAsString(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseDeviceCodeResponseInvalidInterval(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := []byte(`{"device_auth_id":"abc","user_code":"DEF-1234","interval":"abc"}`)
|
||||
|
||||
if _, err := parseDeviceCodeResponse(body); err == nil {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
)
|
||||
|
||||
func TestGeneratePKCE(t *testing.T) {
|
||||
t.Parallel()
|
||||
codes, err := GeneratePKCE()
|
||||
if err != nil {
|
||||
t.Fatalf("GeneratePKCE() error: %v", err)
|
||||
|
|
@ -35,6 +36,7 @@ func TestGeneratePKCE(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGeneratePKCEUniqueness(t *testing.T) {
|
||||
t.Parallel()
|
||||
codes1, err := GeneratePKCE()
|
||||
if err != nil {
|
||||
t.Fatalf("GeneratePKCE() error: %v", err)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func TestAuthCredentialIsExpired(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
expiresAt time.Time
|
||||
|
|
@ -29,6 +30,7 @@ func TestAuthCredentialIsExpired(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAuthCredentialNeedsRefresh(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
expiresAt time.Time
|
||||
|
|
@ -51,6 +53,7 @@ func TestAuthCredentialNeedsRefresh(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStoreRoundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
origHome := os.Getenv("HOME")
|
||||
t.Setenv("HOME", tmpDir)
|
||||
|
|
@ -114,6 +117,7 @@ func TestStoreFilePermissions(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStoreMultiProvider(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
origHome := os.Getenv("HOME")
|
||||
t.Setenv("HOME", tmpDir)
|
||||
|
|
@ -147,6 +151,7 @@ func TestStoreMultiProvider(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDeleteCredential(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
origHome := os.Getenv("HOME")
|
||||
t.Setenv("HOME", tmpDir)
|
||||
|
|
@ -171,6 +176,7 @@ func TestDeleteCredential(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoadStoreEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
origHome := os.Getenv("HOME")
|
||||
t.Setenv("HOME", tmpDir)
|
||||
|
|
|
|||
22
pkg/cache/lru_test.go
vendored
22
pkg/cache/lru_test.go
vendored
|
|
@ -33,6 +33,7 @@ func (c *mockClock) Advance(d time.Duration) {
|
|||
// --- Basic LRU Tests ---
|
||||
|
||||
func TestLRU_SetAndGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := New[string, int](Options[string, int]{MaxSize: 10})
|
||||
|
||||
c.Set("a", 1)
|
||||
|
|
@ -55,6 +56,7 @@ func TestLRU_SetAndGet(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_Update(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := New[string, string](Options[string, string]{MaxSize: 10})
|
||||
|
||||
c.Set("k", "v1")
|
||||
|
|
@ -71,6 +73,7 @@ func TestLRU_Update(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_EvictionOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
var evicted []string
|
||||
c := New[string, int](Options[string, int]{
|
||||
MaxSize: 3,
|
||||
|
|
@ -110,6 +113,7 @@ func TestLRU_EvictionOrder(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_MaxSizeZero_Unlimited(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := New[int, int](Options[int, int]{})
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
|
|
@ -122,6 +126,7 @@ func TestLRU_MaxSizeZero_Unlimited(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_Delete(t *testing.T) {
|
||||
t.Parallel()
|
||||
var evictCalled bool
|
||||
c := New[string, int](Options[string, int]{
|
||||
MaxSize: 10,
|
||||
|
|
@ -148,6 +153,7 @@ func TestLRU_Delete(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_Clear(t *testing.T) {
|
||||
t.Parallel()
|
||||
var evictCount int
|
||||
c := New[string, int](Options[string, int]{
|
||||
MaxSize: 10,
|
||||
|
|
@ -171,6 +177,7 @@ func TestLRU_Clear(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_Peek_DoesNotPromote(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := New[string, int](Options[string, int]{MaxSize: 3})
|
||||
|
||||
c.Set("a", 1)
|
||||
|
|
@ -196,6 +203,7 @@ func TestLRU_Peek_DoesNotPromote(t *testing.T) {
|
|||
// --- TTL Tests ---
|
||||
|
||||
func TestLRU_TTL_Expiry(t *testing.T) {
|
||||
t.Parallel()
|
||||
clock := newMockClock(time.Now())
|
||||
c := New[string, int](Options[string, int]{
|
||||
MaxSize: 10,
|
||||
|
|
@ -225,6 +233,7 @@ func TestLRU_TTL_Expiry(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_TTL_PerEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
clock := newMockClock(time.Now())
|
||||
c := New[string, int](Options[string, int]{
|
||||
MaxSize: 10,
|
||||
|
|
@ -251,6 +260,7 @@ func TestLRU_TTL_PerEntry(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_Peek_ExpiresEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
clock := newMockClock(time.Now())
|
||||
c := New[string, int](Options[string, int]{
|
||||
MaxSize: 10,
|
||||
|
|
@ -268,6 +278,7 @@ func TestLRU_Peek_ExpiresEntries(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_Purge(t *testing.T) {
|
||||
t.Parallel()
|
||||
clock := newMockClock(time.Now())
|
||||
c := New[string, int](Options[string, int]{
|
||||
MaxSize: 10,
|
||||
|
|
@ -300,6 +311,7 @@ func TestLRU_Purge(t *testing.T) {
|
|||
// --- Stale-While-Revalidate Tests ---
|
||||
|
||||
func TestLRU_SWR_ReturnsStaleAndRefreshes(t *testing.T) {
|
||||
t.Parallel()
|
||||
clock := newMockClock(time.Now())
|
||||
var fetchCalls atomic.Int32
|
||||
refreshDone := make(chan struct{}, 1)
|
||||
|
|
@ -347,6 +359,7 @@ func TestLRU_SWR_ReturnsStaleAndRefreshes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_SWR_HardExpiry(t *testing.T) {
|
||||
t.Parallel()
|
||||
clock := newMockClock(time.Now())
|
||||
c := New[string, int](Options[string, int]{
|
||||
MaxSize: 10,
|
||||
|
|
@ -367,6 +380,7 @@ func TestLRU_SWR_HardExpiry(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_SWR_NoFetchFunc_StaleStillReturned(t *testing.T) {
|
||||
t.Parallel()
|
||||
clock := newMockClock(time.Now())
|
||||
c := New[string, int](Options[string, int]{
|
||||
MaxSize: 10,
|
||||
|
|
@ -388,6 +402,7 @@ func TestLRU_SWR_NoFetchFunc_StaleStillReturned(t *testing.T) {
|
|||
// --- Tag-Based Invalidation Tests ---
|
||||
|
||||
func TestLRU_Tags_InvalidateByTag(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := New[string, int](Options[string, int]{MaxSize: 10})
|
||||
|
||||
c.SetWithTags("user:1", 1, []string{"users"})
|
||||
|
|
@ -423,6 +438,7 @@ func TestLRU_Tags_InvalidateByTag(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_Tags_InvalidateNonexistentTag(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := New[string, int](Options[string, int]{MaxSize: 10})
|
||||
c.Set("a", 1)
|
||||
|
||||
|
|
@ -435,6 +451,7 @@ func TestLRU_Tags_InvalidateNonexistentTag(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_Tags_UpdateRemovesOldTags(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := New[string, int](Options[string, int]{MaxSize: 10})
|
||||
|
||||
c.SetWithTags("k", 1, []string{"tag-a"})
|
||||
|
|
@ -458,6 +475,7 @@ func TestLRU_Tags_UpdateRemovesOldTags(t *testing.T) {
|
|||
// --- Concurrent Access Tests ---
|
||||
|
||||
func TestLRU_ConcurrentAccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := New[int, int](Options[int, int]{MaxSize: 100})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
|
@ -493,6 +511,7 @@ func TestLRU_ConcurrentAccess(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_ConcurrentTags(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := New[string, int](Options[string, int]{MaxSize: 100})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
|
@ -524,6 +543,7 @@ func TestLRU_ConcurrentTags(t *testing.T) {
|
|||
// --- Edge Cases ---
|
||||
|
||||
func TestLRU_ZeroTTL_NoExpiry(t *testing.T) {
|
||||
t.Parallel()
|
||||
clock := newMockClock(time.Now())
|
||||
c := New[string, int](Options[string, int]{
|
||||
MaxSize: 10,
|
||||
|
|
@ -540,6 +560,7 @@ func TestLRU_ZeroTTL_NoExpiry(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_MaxSizeOne(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := New[string, int](Options[string, int]{MaxSize: 1})
|
||||
|
||||
c.Set("a", 1)
|
||||
|
|
@ -557,6 +578,7 @@ func TestLRU_MaxSizeOne(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLRU_Keys_Order(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := New[string, int](Options[string, int]{MaxSize: 10})
|
||||
|
||||
c.Set("a", 1)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package channels
|
|||
import "testing"
|
||||
|
||||
func TestBaseChannelIsAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
allowList []string
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func TestParseSlackChatID(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
chatID string
|
||||
|
|
@ -54,6 +55,7 @@ func TestParseSlackChatID(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStripBotMention(t *testing.T) {
|
||||
t.Parallel()
|
||||
ch := &SlackChannel{botUserID: "U12345BOT"}
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -99,6 +101,7 @@ func TestStripBotMention(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewSlackChannel(t *testing.T) {
|
||||
t.Parallel()
|
||||
msgBus := bus.NewMessageBus()
|
||||
|
||||
t.Run("missing bot token", func(t *testing.T) {
|
||||
|
|
@ -143,6 +146,7 @@ func TestNewSlackChannel(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSlackChannelIsAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
msgBus := bus.NewMessageBus()
|
||||
|
||||
t.Run("empty allowlist allows all", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
// TestDefaultConfig_HeartbeatEnabled verifies heartbeat is enabled by default
|
||||
func TestDefaultConfig_HeartbeatEnabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if !cfg.Heartbeat.Enabled {
|
||||
|
|
@ -19,6 +20,7 @@ func TestDefaultConfig_HeartbeatEnabled(t *testing.T) {
|
|||
|
||||
// TestDefaultConfig_SandboxPath verifies sandbox path is resolvable
|
||||
func TestDefaultConfig_SandboxPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
path := cfg.SandboxPath()
|
||||
if path == "" {
|
||||
|
|
@ -28,6 +30,7 @@ func TestDefaultConfig_SandboxPath(t *testing.T) {
|
|||
|
||||
// TestDefaultConfig_Model verifies model is set
|
||||
func TestDefaultConfig_Model(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if cfg.Agents.Defaults.Model == "" {
|
||||
|
|
@ -37,6 +40,7 @@ func TestDefaultConfig_Model(t *testing.T) {
|
|||
|
||||
// TestDefaultConfig_MaxTokens verifies max tokens has default value
|
||||
func TestDefaultConfig_MaxTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if cfg.Agents.Defaults.MaxTokens == 0 {
|
||||
|
|
@ -46,6 +50,7 @@ func TestDefaultConfig_MaxTokens(t *testing.T) {
|
|||
|
||||
// TestDefaultConfig_MaxToolIterations verifies max tool iterations has default value
|
||||
func TestDefaultConfig_MaxToolIterations(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if cfg.Agents.Defaults.MaxToolIterations == 0 {
|
||||
|
|
@ -54,6 +59,7 @@ func TestDefaultConfig_MaxToolIterations(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDefaultConfig_ContinuityRetention(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if cfg.Agents.Defaults.ContinuityRetention.MinMessages <= 0 {
|
||||
|
|
@ -72,6 +78,7 @@ func TestDefaultConfig_ContinuityRetention(t *testing.T) {
|
|||
|
||||
// TestDefaultConfig_Temperature verifies temperature has default value
|
||||
func TestDefaultConfig_Temperature(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if cfg.Agents.Defaults.Temperature == 0 {
|
||||
|
|
@ -81,6 +88,7 @@ func TestDefaultConfig_Temperature(t *testing.T) {
|
|||
|
||||
// TestDefaultConfig_Gateway verifies gateway defaults
|
||||
func TestDefaultConfig_Gateway(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if cfg.Gateway.Host != "0.0.0.0" {
|
||||
|
|
@ -93,6 +101,7 @@ func TestDefaultConfig_Gateway(t *testing.T) {
|
|||
|
||||
// TestDefaultConfig_Providers verifies provider structure
|
||||
func TestDefaultConfig_Providers(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
|
||||
// Verify all providers are empty by default
|
||||
|
|
@ -121,6 +130,7 @@ func TestDefaultConfig_Providers(t *testing.T) {
|
|||
|
||||
// TestDefaultConfig_Channels verifies channels are disabled by default
|
||||
func TestDefaultConfig_Channels(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
|
||||
// Verify all channels are disabled by default
|
||||
|
|
@ -152,6 +162,7 @@ func TestDefaultConfig_Channels(t *testing.T) {
|
|||
|
||||
// TestDefaultConfig_WebTools verifies web tools config
|
||||
func TestDefaultConfig_WebTools(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
|
||||
// Verify web tools defaults
|
||||
|
|
@ -167,6 +178,7 @@ func TestDefaultConfig_WebTools(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSaveConfig_FilePermissions(t *testing.T) {
|
||||
t.Parallel()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("file permission bits are not enforced on Windows")
|
||||
}
|
||||
|
|
@ -192,6 +204,7 @@ func TestSaveConfig_FilePermissions(t *testing.T) {
|
|||
|
||||
// TestConfig_Complete verifies all config fields are set
|
||||
func TestConfig_Complete(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
|
||||
// Verify complete config structure
|
||||
|
|
@ -222,6 +235,7 @@ func TestConfig_Complete(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDefaultConfig_OpenAIWebSearchEnabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
if !cfg.Providers.OpenAI.WebSearch {
|
||||
t.Fatal("DefaultConfig().Providers.OpenAI.WebSearch should be true")
|
||||
|
|
@ -229,6 +243,7 @@ func TestDefaultConfig_OpenAIWebSearchEnabled(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "config.json")
|
||||
if err := os.WriteFile(configPath, []byte(`{"providers":{"openai":{"api_base":""}}}`), 0o600); err != nil {
|
||||
|
|
@ -245,6 +260,7 @@ func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidate_MemoryConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*Config)
|
||||
|
|
@ -318,6 +334,7 @@ func TestValidate_MemoryConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidate_ContinuityRetentionConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultConfig()
|
||||
cfg.Agents.Defaults.ContinuityRetention.MinMessages = 8
|
||||
cfg.Agents.Defaults.ContinuityRetention.MaxMessages = 4
|
||||
|
|
@ -342,6 +359,7 @@ func containsMemoryWarning(s string) bool {
|
|||
}
|
||||
|
||||
func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "config.json")
|
||||
if err := os.WriteFile(configPath, []byte(`{"providers":{"openai":{"web_search":false}}}`), 0o600); err != nil {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func TestSaveStore_FilePermissions(t *testing.T) {
|
||||
t.Parallel()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("file permission bits are not enforced on Windows")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ func (t *mockNilResultTool) Execute(_ context.Context, _ map[string]interface{})
|
|||
// --- PicoToolAdapter.Info() Tests ---
|
||||
|
||||
func TestAdapter_Info(t *testing.T) {
|
||||
t.Parallel()
|
||||
adapter := &PicoToolAdapter{inner: &mockDualChannelTool{}}
|
||||
info := adapter.Info()
|
||||
|
||||
|
|
@ -127,6 +128,7 @@ func TestAdapter_Info(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAdapter_Info_UnwrapsSchemaWithRequired(t *testing.T) {
|
||||
t.Parallel()
|
||||
mock := &mockToolWithRequired{}
|
||||
adapter := &PicoToolAdapter{inner: mock}
|
||||
info := adapter.Info()
|
||||
|
|
@ -159,6 +161,7 @@ func (t *mockToolWithRequired) Execute(_ context.Context, _ map[string]interface
|
|||
// --- PicoToolAdapter.Run() Tests ---
|
||||
|
||||
func TestAdapter_Run_SilentTool_NoPublish(t *testing.T) {
|
||||
t.Parallel()
|
||||
msgBus := bus.NewMessageBus()
|
||||
adapter := &PicoToolAdapter{
|
||||
inner: &mockSilentTool{},
|
||||
|
|
@ -173,7 +176,7 @@ func TestAdapter_Run_SilentTool_NoPublish(t *testing.T) {
|
|||
Input: "{}",
|
||||
}
|
||||
|
||||
resp, err := adapter.Run(context.Background(), call)
|
||||
resp, err := adapter.Run(t.Context(), call)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -187,6 +190,7 @@ func TestAdapter_Run_SilentTool_NoPublish(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAdapter_Run_DualChannel_PublishesForUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
msgBus := bus.NewMessageBus()
|
||||
adapter := &PicoToolAdapter{
|
||||
inner: &mockDualChannelTool{},
|
||||
|
|
@ -201,7 +205,7 @@ func TestAdapter_Run_DualChannel_PublishesForUser(t *testing.T) {
|
|||
Input: `{"input": "hello"}`,
|
||||
}
|
||||
|
||||
resp, err := adapter.Run(context.Background(), call)
|
||||
resp, err := adapter.Run(t.Context(), call)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -216,6 +220,7 @@ func TestAdapter_Run_DualChannel_PublishesForUser(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAdapter_Run_ErrorTool_ReturnsErrorResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
adapter := &PicoToolAdapter{
|
||||
inner: &mockErrorTool{},
|
||||
}
|
||||
|
|
@ -226,7 +231,7 @@ func TestAdapter_Run_ErrorTool_ReturnsErrorResponse(t *testing.T) {
|
|||
Input: "{}",
|
||||
}
|
||||
|
||||
resp, err := adapter.Run(context.Background(), call)
|
||||
resp, err := adapter.Run(t.Context(), call)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error (adapter should not return Go errors): %v", err)
|
||||
}
|
||||
|
|
@ -240,6 +245,7 @@ func TestAdapter_Run_ErrorTool_ReturnsErrorResponse(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAdapter_Run_NilResult_ReturnsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
adapter := &PicoToolAdapter{
|
||||
inner: &mockNilResultTool{},
|
||||
}
|
||||
|
|
@ -250,7 +256,7 @@ func TestAdapter_Run_NilResult_ReturnsError(t *testing.T) {
|
|||
Input: "{}",
|
||||
}
|
||||
|
||||
resp, err := adapter.Run(context.Background(), call)
|
||||
resp, err := adapter.Run(t.Context(), call)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected Go error: %v", err)
|
||||
}
|
||||
|
|
@ -264,6 +270,7 @@ func TestAdapter_Run_NilResult_ReturnsError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAdapter_Run_ContextualTool_SetsContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctxTool := &mockContextualTool{}
|
||||
adapter := &PicoToolAdapter{
|
||||
inner: ctxTool,
|
||||
|
|
@ -277,7 +284,7 @@ func TestAdapter_Run_ContextualTool_SetsContext(t *testing.T) {
|
|||
Input: "{}",
|
||||
}
|
||||
|
||||
resp, err := adapter.Run(context.Background(), call)
|
||||
resp, err := adapter.Run(t.Context(), call)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -289,6 +296,7 @@ func TestAdapter_Run_ContextualTool_SetsContext(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAdapter_Run_InvalidJSON_ReturnsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
adapter := &PicoToolAdapter{
|
||||
inner: &mockSilentTool{},
|
||||
}
|
||||
|
|
@ -299,7 +307,7 @@ func TestAdapter_Run_InvalidJSON_ReturnsError(t *testing.T) {
|
|||
Input: "not valid json{{{",
|
||||
}
|
||||
|
||||
resp, err := adapter.Run(context.Background(), call)
|
||||
resp, err := adapter.Run(t.Context(), call)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected Go error: %v", err)
|
||||
}
|
||||
|
|
@ -312,6 +320,7 @@ func TestAdapter_Run_InvalidJSON_ReturnsError(t *testing.T) {
|
|||
// --- BuildAdaptedTools Tests ---
|
||||
|
||||
func TestBuildAdaptedTools_NilRegistry(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := BuildAdaptedTools(nil, nil, "", "")
|
||||
if result != nil {
|
||||
t.Error("Expected nil for nil registry")
|
||||
|
|
@ -319,6 +328,7 @@ func TestBuildAdaptedTools_NilRegistry(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBuildAdaptedTools_WrapsAllTools(t *testing.T) {
|
||||
t.Parallel()
|
||||
registry := tools.NewToolRegistry()
|
||||
registry.Register(&mockSilentTool{})
|
||||
registry.Register(&mockDualChannelTool{})
|
||||
|
|
@ -348,6 +358,7 @@ func TestBuildAdaptedTools_WrapsAllTools(t *testing.T) {
|
|||
// --- parseToolArgs Tests ---
|
||||
|
||||
func TestParseToolArgs_EmptyInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
args, err := parseToolArgs("")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
|
|
@ -358,6 +369,7 @@ func TestParseToolArgs_EmptyInput(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseToolArgs_EmptyObject(t *testing.T) {
|
||||
t.Parallel()
|
||||
args, err := parseToolArgs("{}")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
|
|
@ -368,6 +380,7 @@ func TestParseToolArgs_EmptyObject(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseToolArgs_ValidJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
args, err := parseToolArgs(`{"key": "value", "num": 42}`)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
|
|
@ -378,6 +391,7 @@ func TestParseToolArgs_ValidJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseToolArgs_InvalidJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := parseToolArgs("not json")
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid JSON")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
// --- MessagesToFantasy Tests ---
|
||||
|
||||
func TestMessagesToFantasy_EmptySlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := MessagesToFantasy(nil)
|
||||
if len(result) != 0 {
|
||||
t.Errorf("Expected empty slice for nil input, got %d", len(result))
|
||||
|
|
@ -23,6 +24,7 @@ func TestMessagesToFantasy_EmptySlice(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMessageToFantasy_SimpleTextMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
msg := messages.Message{
|
||||
Role: "user",
|
||||
Content: "Hello, world",
|
||||
|
|
@ -47,6 +49,7 @@ func TestMessageToFantasy_SimpleTextMessage(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMessageToFantasy_AssistantWithMultipleToolCalls(t *testing.T) {
|
||||
t.Parallel()
|
||||
msg := messages.Message{
|
||||
Role: "assistant",
|
||||
Content: "Let me run both tools.",
|
||||
|
|
@ -113,6 +116,7 @@ func TestMessageToFantasy_AssistantWithMultipleToolCalls(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMessageToFantasy_ToolCallWithMapArgsFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
msg := messages.Message{
|
||||
Role: "assistant",
|
||||
ToolCalls: []messages.ToolCall{
|
||||
|
|
@ -145,6 +149,7 @@ func TestMessageToFantasy_ToolCallWithMapArgsFallback(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMessageToFantasy_ToolResultMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
msg := messages.Message{
|
||||
Role: "tool",
|
||||
Content: "file contents here",
|
||||
|
|
@ -177,6 +182,7 @@ func TestMessageToFantasy_ToolResultMessage(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMessageToFantasy_EmptyContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
msg := messages.Message{
|
||||
Role: "assistant",
|
||||
Content: "",
|
||||
|
|
@ -193,6 +199,7 @@ func TestMessageToFantasy_EmptyContent(t *testing.T) {
|
|||
// --- StepToMessages Tests ---
|
||||
|
||||
func TestStepToMessages_TextOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
step := fantasy.StepResult{
|
||||
Response: fantasy.Response{
|
||||
Content: fantasy.ResponseContent{
|
||||
|
|
@ -216,6 +223,7 @@ func TestStepToMessages_TextOnly(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStepToMessages_MultipleToolCalls(t *testing.T) {
|
||||
t.Parallel()
|
||||
step := fantasy.StepResult{
|
||||
Response: fantasy.Response{
|
||||
Content: fantasy.ResponseContent{
|
||||
|
|
@ -280,6 +288,7 @@ func TestStepToMessages_MultipleToolCalls(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStepToMessages_ErrorToolResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
testErr := errors.New("permission denied")
|
||||
step := fantasy.StepResult{
|
||||
Response: fantasy.Response{
|
||||
|
|
@ -315,6 +324,7 @@ func TestStepToMessages_ErrorToolResult(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStepToMessages_ErrorToolResult_NilError(t *testing.T) {
|
||||
t.Parallel()
|
||||
step := fantasy.StepResult{
|
||||
Response: fantasy.Response{
|
||||
Content: fantasy.ResponseContent{
|
||||
|
|
@ -339,6 +349,7 @@ func TestStepToMessages_ErrorToolResult_NilError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStepToMessages_ToolCallWithoutText(t *testing.T) {
|
||||
t.Parallel()
|
||||
step := fantasy.StepResult{
|
||||
Response: fantasy.Response{
|
||||
Content: fantasy.ResponseContent{
|
||||
|
|
@ -372,6 +383,7 @@ func TestStepToMessages_ToolCallWithoutText(t *testing.T) {
|
|||
// --- AgentResultToMessages Tests ---
|
||||
|
||||
func TestAgentResultToMessages_MultipleSteps(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := &fantasy.AgentResult{
|
||||
Steps: []fantasy.StepResult{
|
||||
{
|
||||
|
|
@ -417,7 +429,10 @@ func TestAgentResultToMessages_MultipleSteps(t *testing.T) {
|
|||
// --- Round-trip fidelity test ---
|
||||
|
||||
func TestRoundTrip_PicoClawToFantasyAndBack(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Start with DragonScale messages representing a typical conversation
|
||||
)
|
||||
|
||||
original := []messages.Message{
|
||||
{Role: "user", Content: "Read the file"},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func TestExecuteHeartbeat_Async(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "heartbeat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -49,6 +50,7 @@ func TestExecuteHeartbeat_Async(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExecuteHeartbeat_Error(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "heartbeat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -87,6 +89,7 @@ func TestExecuteHeartbeat_Error(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExecuteHeartbeat_Silent(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "heartbeat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -125,6 +128,7 @@ func TestExecuteHeartbeat_Silent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHeartbeatService_StartStop(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "heartbeat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -144,6 +148,7 @@ func TestHeartbeatService_StartStop(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHeartbeatService_Disabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "heartbeat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -161,6 +166,7 @@ func TestHeartbeatService_Disabled(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExecuteHeartbeat_NilResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "heartbeat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -183,6 +189,7 @@ func TestExecuteHeartbeat_NilResult(t *testing.T) {
|
|||
|
||||
// TestLogPath verifies heartbeat log is written to workspace directory
|
||||
func TestLogPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "heartbeat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -203,6 +210,7 @@ func TestLogPath(t *testing.T) {
|
|||
|
||||
// TestHeartbeatFilePath verifies HEARTBEAT.md is at workspace root
|
||||
func TestHeartbeatFilePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "heartbeat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -222,6 +230,7 @@ func TestHeartbeatFilePath(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExecuteHeartbeat_UsesDueContextWithoutHeartbeatFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "heartbeat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
)
|
||||
|
||||
func TestNew_IsV7(t *testing.T) {
|
||||
t.Parallel()
|
||||
u := New()
|
||||
|
||||
if u.IsZero() {
|
||||
|
|
@ -24,6 +25,7 @@ func TestNew_IsV7(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNew_Unique(t *testing.T) {
|
||||
t.Parallel()
|
||||
seen := make(map[UUID]bool, 1000)
|
||||
for i := 0; i < 1000; i++ {
|
||||
u := New()
|
||||
|
|
@ -35,6 +37,7 @@ func TestNew_Unique(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNew_Monotonic(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := New()
|
||||
b := New()
|
||||
// UUIDv7 embeds ms timestamp in first 6 bytes. b >= a in timestamp.
|
||||
|
|
@ -50,6 +53,7 @@ func TestNew_Monotonic(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParse_RoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
u := New()
|
||||
s := u.String()
|
||||
|
||||
|
|
@ -63,6 +67,7 @@ func TestParse_RoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParse_Errors(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []string{
|
||||
"",
|
||||
"not-a-uuid",
|
||||
|
|
@ -78,6 +83,7 @@ func TestParse_Errors(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIsZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
var zero UUID
|
||||
if !zero.IsZero() {
|
||||
t.Fatal("zero UUID should be zero")
|
||||
|
|
@ -89,6 +95,7 @@ func TestIsZero(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValue_BlobRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
u := New()
|
||||
v, err := u.Value()
|
||||
if err != nil {
|
||||
|
|
@ -112,6 +119,7 @@ func TestValue_BlobRoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValue_ZeroIsNil(t *testing.T) {
|
||||
t.Parallel()
|
||||
var zero UUID
|
||||
v, err := zero.Value()
|
||||
if err != nil {
|
||||
|
|
@ -123,6 +131,7 @@ func TestValue_ZeroIsNil(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestScan_NilLeavesZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
var u UUID
|
||||
if err := u.Scan(nil); err != nil {
|
||||
t.Fatalf("Scan(nil): %v", err)
|
||||
|
|
@ -133,6 +142,7 @@ func TestScan_NilLeavesZero(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestScan_String(t *testing.T) {
|
||||
t.Parallel()
|
||||
orig := New()
|
||||
var u UUID
|
||||
if err := u.Scan(orig.String()); err != nil {
|
||||
|
|
@ -144,6 +154,7 @@ func TestScan_String(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestScan_InvalidBlob(t *testing.T) {
|
||||
t.Parallel()
|
||||
var u UUID
|
||||
if err := u.Scan([]byte{1, 2, 3}); err == nil {
|
||||
t.Fatal("Scan(3-byte blob) should fail")
|
||||
|
|
@ -151,6 +162,7 @@ func TestScan_InvalidBlob(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestScan_InvalidType(t *testing.T) {
|
||||
t.Parallel()
|
||||
var u UUID
|
||||
if err := u.Scan(42); err == nil {
|
||||
t.Fatal("Scan(int) should fail")
|
||||
|
|
@ -158,6 +170,7 @@ func TestScan_InvalidType(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestJSON_RoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
u := New()
|
||||
|
||||
b, err := jsonv2.Marshal(u)
|
||||
|
|
@ -184,6 +197,7 @@ func TestJSON_RoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestJSON_ZeroUUID(t *testing.T) {
|
||||
t.Parallel()
|
||||
var zero UUID
|
||||
b, err := jsonv2.Marshal(zero)
|
||||
if err != nil {
|
||||
|
|
@ -196,6 +210,7 @@ func TestJSON_ZeroUUID(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestJSON_InStruct(t *testing.T) {
|
||||
t.Parallel()
|
||||
type record struct {
|
||||
ID UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
|
@ -217,6 +232,7 @@ func TestJSON_InStruct(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFromBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
u := New()
|
||||
b := u.Bytes()
|
||||
restored := FromBytes(b)
|
||||
|
|
@ -226,6 +242,7 @@ func TestFromBytes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMustParse_Panics(t *testing.T) {
|
||||
t.Parallel()
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatal("MustParse should panic on bad input")
|
||||
|
|
@ -235,6 +252,7 @@ func TestMustParse_Panics(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestString_Format(t *testing.T) {
|
||||
t.Parallel()
|
||||
u := New()
|
||||
s := u.String()
|
||||
if len(s) != 36 {
|
||||
|
|
@ -246,6 +264,7 @@ func TestString_Format(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUUIDComparable(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := New()
|
||||
b := a // copy
|
||||
if a != b {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
package itr
|
||||
|
||||
import (
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestToolRequestMarshalRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
req ToolRequest
|
||||
|
|
@ -68,16 +71,13 @@ func TestToolRequestMarshalRoundTrip(t *testing.T) {
|
|||
decoded, err := UnmarshalRequest(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.req.ID, decoded.ID)
|
||||
assert.Equal(t, tt.req.Type, decoded.Type)
|
||||
assert.Equal(t, tt.req.SessionKey, decoded.SessionKey)
|
||||
assert.Equal(t, tt.req.Depth, decoded.Depth)
|
||||
assert.Equal(t, tt.req.ToolCallID, decoded.ToolCallID)
|
||||
assert.Empty(t, cmp.Diff(tt.req, decoded))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolExecPayloadPreservation(t *testing.T) {
|
||||
t.Parallel()
|
||||
req := NewToolExecRequest("id-1", "s", "tc", "shell", `{"cmd":"ls -la"}`)
|
||||
data, err := req.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
|
@ -92,6 +92,7 @@ func TestToolExecPayloadPreservation(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGrepPayloadPreservation(t *testing.T) {
|
||||
t.Parallel()
|
||||
req := NewGrepRequest("g1", "s", 2, "error.*fatal", 25, true)
|
||||
data, err := req.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
|
@ -107,6 +108,7 @@ func TestGrepPayloadPreservation(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDAGPlanPayloadPreservation(t *testing.T) {
|
||||
t.Parallel()
|
||||
plan := DAGPlan{
|
||||
Nodes: []DAGNode{
|
||||
{ID: "a", Type: CmdToolSearch, Payload: ToolSearch{Query: "files", MaxResults: 5}, DependsOn: nil},
|
||||
|
|
@ -143,6 +145,7 @@ func TestDAGPlanPayloadPreservation(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolResponseMarshalRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
resp ToolResponse
|
||||
|
|
@ -169,17 +172,13 @@ func TestToolResponseMarshalRoundTrip(t *testing.T) {
|
|||
decoded, err := UnmarshalResponse(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.resp.ID, decoded.ID)
|
||||
assert.Equal(t, tt.resp.Result, decoded.Result)
|
||||
assert.Equal(t, tt.resp.IsError, decoded.IsError)
|
||||
assert.Equal(t, tt.resp.LeakDetected, decoded.LeakDetected)
|
||||
assert.Equal(t, tt.resp.CostTokens, decoded.CostTokens)
|
||||
assert.Equal(t, tt.resp.RedactedKeys, decoded.RedactedKeys)
|
||||
assert.Empty(t, cmp.Diff(tt.resp, decoded))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalRequestJSON_UnknownType(t *testing.T) {
|
||||
t.Parallel()
|
||||
data, _ := jsonv2.Marshal(map[string]interface{}{
|
||||
"id": "bad",
|
||||
"type": "nonexistent_command",
|
||||
|
|
@ -190,21 +189,25 @@ func TestUnmarshalRequestJSON_UnknownType(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUnmarshalRequestJSON_InvalidJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := UnmarshalRequestJSON([]byte(`{invalid`))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMarshalRequestFB_UnknownType(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := MarshalRequestFB(ToolRequest{ID: "bad", Type: CommandType("bogus")})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUnmarshalRequestFB_Garbage(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := UnmarshalRequestFB([]byte{0, 0, 0, 0})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRequestJSON_Roundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
orig := NewToolExecRequest("j1", "s", "tc", "shell", `{"cmd":"ls"}`)
|
||||
data, err := jsonv2.Marshal(orig)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -217,6 +220,7 @@ func TestRequestJSON_Roundtrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResponseJSON_Roundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
orig := NewSuccessResponse("j2", "ok", 10)
|
||||
data, err := jsonv2.Marshal(orig)
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ package dag_test
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/itr"
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/itr/dag"
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus"
|
||||
|
|
@ -51,6 +52,7 @@ func (s *staticTool) Execute(_ context.Context, args map[string]interface{}) *to
|
|||
}
|
||||
|
||||
func TestExecutor_LinearDependencyChain(t *testing.T) {
|
||||
t.Parallel()
|
||||
toolMap := map[string]tools.Tool{
|
||||
"step1": &staticTool{name: "step1", result: "r1"},
|
||||
"step2": &staticTool{name: "step2", result: "r2"},
|
||||
|
|
@ -67,13 +69,14 @@ func TestExecutor_LinearDependencyChain(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
result, err := executor.Execute(context.Background(), "test-sess", plan)
|
||||
result, err := executor.Execute(t.Context(), "test-sess", plan)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, result.NodeResults["a"], "r1")
|
||||
assert.Contains(t, result.NodeResults["b"], "r2")
|
||||
}
|
||||
|
||||
func TestExecutor_ParallelNodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
toolMap := map[string]tools.Tool{
|
||||
"alpha": &staticTool{name: "alpha", result: "a-result"},
|
||||
"beta": &staticTool{name: "beta", result: "b-result"},
|
||||
|
|
@ -90,13 +93,14 @@ func TestExecutor_ParallelNodes(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
result, err := executor.Execute(context.Background(), "test-sess", plan)
|
||||
result, err := executor.Execute(t.Context(), "test-sess", plan)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "a-result", result.NodeResults["n1"])
|
||||
assert.Equal(t, "b-result", result.NodeResults["n2"])
|
||||
}
|
||||
|
||||
func TestExecutor_CycleDetection(t *testing.T) {
|
||||
t.Parallel()
|
||||
bus := makeBus(t, map[string]tools.Tool{})
|
||||
defer bus.Close()
|
||||
|
||||
|
|
@ -109,12 +113,13 @@ func TestExecutor_CycleDetection(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
_, err := executor.Execute(context.Background(), "test-sess", plan)
|
||||
_, err := executor.Execute(t.Context(), "test-sess", plan)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cycle")
|
||||
}
|
||||
|
||||
func TestExecutor_WithJoiner(t *testing.T) {
|
||||
t.Parallel()
|
||||
toolMap := map[string]tools.Tool{
|
||||
"tool1": &staticTool{name: "tool1", result: "data-A"},
|
||||
"tool2": &staticTool{name: "tool2", result: "data-B"},
|
||||
|
|
@ -140,24 +145,26 @@ func TestExecutor_WithJoiner(t *testing.T) {
|
|||
JoinerQuery: "Combine the results into a summary",
|
||||
}
|
||||
|
||||
result, err := executor.Execute(context.Background(), "test-sess", plan)
|
||||
result, err := executor.Execute(t.Context(), "test-sess", plan)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, result.FinalAnswer, "synthesized:")
|
||||
assert.Equal(t, uint32(50), result.TotalTokens)
|
||||
}
|
||||
|
||||
func TestExecutor_EmptyPlan(t *testing.T) {
|
||||
t.Parallel()
|
||||
bus := makeBus(t, map[string]tools.Tool{})
|
||||
defer bus.Close()
|
||||
|
||||
executor := dag.NewExecutor(bus, nil)
|
||||
|
||||
result, err := executor.Execute(context.Background(), "test-sess", &itr.DAGPlan{})
|
||||
result, err := executor.Execute(t.Context(), "test-sess", &itr.DAGPlan{})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, result.NodeResults)
|
||||
}
|
||||
|
||||
func TestResolver_NodeRefSubstitution(t *testing.T) {
|
||||
t.Parallel()
|
||||
argsJSON := `{"query": "search for #nodeprev results"}`
|
||||
toolMap := map[string]tools.Tool{
|
||||
"search": &staticTool{name: "search", result: "found"},
|
||||
|
|
@ -175,31 +182,35 @@ func TestResolver_NodeRefSubstitution(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
result, err := executor.Execute(context.Background(), "test-sess", plan)
|
||||
result, err := executor.Execute(t.Context(), "test-sess", plan)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, result.NodeResults["prev"], "previous-output")
|
||||
assert.Contains(t, result.NodeResults["search"], "found")
|
||||
}
|
||||
|
||||
func TestRouter_SimpleQuerySelectsReAct(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := dag.DefaultRouterConfig()
|
||||
mode := dag.Route(dag.ModeAuto, "What is the weather?", cfg)
|
||||
assert.Equal(t, dag.ModeReAct, mode)
|
||||
}
|
||||
|
||||
func TestRouter_ComplexQuerySelectsDAG(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := dag.DefaultRouterConfig()
|
||||
mode := dag.Route(dag.ModeAuto, "Search for the latest news about AI, read the top 3 articles, and compare their viewpoints to create a summary report with aggregate statistics", cfg)
|
||||
assert.Equal(t, dag.ModeDAG, mode)
|
||||
}
|
||||
|
||||
func TestRouter_ExplicitModeOverridesAuto(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := dag.DefaultRouterConfig()
|
||||
mode := dag.Route(dag.ModeReAct, "Do many complex parallel things simultaneously", cfg)
|
||||
assert.Equal(t, dag.ModeReAct, mode)
|
||||
}
|
||||
|
||||
func TestPlanner_ValidatePlan(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
plan string
|
||||
|
|
@ -238,7 +249,7 @@ func TestPlanner_ValidatePlan(t *testing.T) {
|
|||
return tt.plan, 10, nil
|
||||
}
|
||||
planner := dag.NewPlanner(mockModel, nil, dag.DefaultPlannerConfig())
|
||||
_, _, planErr := planner.Plan(context.Background(), "test query", nil)
|
||||
_, _, planErr := planner.Plan(t.Context(), "test query", nil)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, planErr)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -2,35 +2,41 @@ package dag
|
|||
|
||||
import (
|
||||
"context"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/itr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractJSON_PlainJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := `{"nodes": [{"id": "n1"}]}`
|
||||
assert.Equal(t, input, extractJSON(input))
|
||||
}
|
||||
|
||||
func TestExtractJSON_MarkdownFenced(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := "Here is the plan:\n```json\n{\"nodes\": [{\"id\": \"n1\"}]}\n```\nDone."
|
||||
assert.Equal(t, `{"nodes": [{"id": "n1"}]}`, extractJSON(input))
|
||||
}
|
||||
|
||||
func TestExtractJSON_GenericFenced(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := "```\n{\"nodes\": []}\n```"
|
||||
assert.Equal(t, `{"nodes": []}`, extractJSON(input))
|
||||
}
|
||||
|
||||
func TestExtractJSON_LeadingText(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := "The plan is: {\"nodes\":[]}"
|
||||
assert.Equal(t, `{"nodes":[]}`, extractJSON(input))
|
||||
}
|
||||
|
||||
func TestFindIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, 0, findIndex("abc", "a"))
|
||||
assert.Equal(t, 2, findIndex("abc", "c"))
|
||||
assert.Equal(t, -1, findIndex("abc", "z"))
|
||||
|
|
@ -39,6 +45,7 @@ func TestFindIndex(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidatePlan_Valid(t *testing.T) {
|
||||
t.Parallel()
|
||||
plan := &itr.DAGPlan{
|
||||
Nodes: []itr.DAGNode{
|
||||
{ID: "a", Type: itr.CmdToolExec},
|
||||
|
|
@ -49,6 +56,7 @@ func TestValidatePlan_Valid(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidatePlan_Empty(t *testing.T) {
|
||||
t.Parallel()
|
||||
plan := &itr.DAGPlan{Nodes: nil}
|
||||
err := validatePlan(plan)
|
||||
require.Error(t, err)
|
||||
|
|
@ -56,6 +64,7 @@ func TestValidatePlan_Empty(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidatePlan_DuplicateID(t *testing.T) {
|
||||
t.Parallel()
|
||||
plan := &itr.DAGPlan{
|
||||
Nodes: []itr.DAGNode{
|
||||
{ID: "x", Type: itr.CmdToolExec},
|
||||
|
|
@ -68,6 +77,7 @@ func TestValidatePlan_DuplicateID(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidatePlan_EmptyID(t *testing.T) {
|
||||
t.Parallel()
|
||||
plan := &itr.DAGPlan{
|
||||
Nodes: []itr.DAGNode{{ID: "", Type: itr.CmdToolExec}},
|
||||
}
|
||||
|
|
@ -77,6 +87,7 @@ func TestValidatePlan_EmptyID(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidatePlan_UnknownDependency(t *testing.T) {
|
||||
t.Parallel()
|
||||
plan := &itr.DAGPlan{
|
||||
Nodes: []itr.DAGNode{
|
||||
{ID: "a", Type: itr.CmdToolExec, DependsOn: []string{"missing"}},
|
||||
|
|
@ -88,6 +99,7 @@ func TestValidatePlan_UnknownDependency(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidatePlan_SelfDependency(t *testing.T) {
|
||||
t.Parallel()
|
||||
plan := &itr.DAGPlan{
|
||||
Nodes: []itr.DAGNode{
|
||||
{ID: "a", Type: itr.CmdToolExec, DependsOn: []string{"a"}},
|
||||
|
|
@ -99,6 +111,7 @@ func TestValidatePlan_SelfDependency(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidatePlan_CyclicDependency(t *testing.T) {
|
||||
t.Parallel()
|
||||
plan := &itr.DAGPlan{
|
||||
Nodes: []itr.DAGNode{
|
||||
{ID: "a", Type: itr.CmdToolExec, DependsOn: []string{"b"}},
|
||||
|
|
@ -111,6 +124,7 @@ func TestValidatePlan_CyclicDependency(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParsePlanResponse_ValidJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := `{
|
||||
"nodes": [
|
||||
{"id": "n1", "type": "tool_exec", "payload": {"tool_name": "read_file", "args_json": "{\"path\":\"/tmp/a\"}"}}
|
||||
|
|
@ -131,6 +145,7 @@ func TestParsePlanResponse_ValidJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParsePlanResponse_WithMarkdownFence(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := "```json\n" + `{"nodes": [{"id": "x", "type": "tool_search", "payload": {"query": "files"}}]}` + "\n```"
|
||||
|
||||
plan, err := parsePlanResponse(input)
|
||||
|
|
@ -143,11 +158,13 @@ func TestParsePlanResponse_WithMarkdownFence(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParsePlanResponse_InvalidJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := parsePlanResponse("not json at all")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPlannerPlanE2E(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockLLM := func(ctx context.Context, systemPrompt, userQuery string) (string, uint32, error) {
|
||||
plan := itr.DAGPlan{
|
||||
Nodes: []itr.DAGNode{
|
||||
|
|
@ -164,7 +181,7 @@ func TestPlannerPlanE2E(t *testing.T) {
|
|||
}
|
||||
|
||||
planner := NewPlanner(mockLLM, nil, DefaultPlannerConfig())
|
||||
plan, tokens, err := planner.Plan(context.Background(), "search and read", nil)
|
||||
plan, tokens, err := planner.Plan(t.Context(), "search and read", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint32(100), tokens)
|
||||
require.Len(t, plan.Nodes, 2)
|
||||
|
|
@ -173,12 +190,13 @@ func TestPlannerPlanE2E(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPlannerPlanLLMError(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockLLM := func(ctx context.Context, systemPrompt, userQuery string) (string, uint32, error) {
|
||||
return "", 50, assert.AnError
|
||||
}
|
||||
|
||||
planner := NewPlanner(mockLLM, nil, DefaultPlannerConfig())
|
||||
_, tokens, err := planner.Plan(context.Background(), "anything", nil)
|
||||
_, tokens, err := planner.Plan(t.Context(), "anything", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, uint32(50), tokens)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
)
|
||||
|
||||
func TestNeedsReplan(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.True(t, needsReplan("The task is incomplete [NEEDS_MORE_STEPS]"))
|
||||
assert.True(t, needsReplan("[NEEDS_MORE_STEPS]"))
|
||||
assert.False(t, needsReplan("Task complete. Here is the answer."))
|
||||
|
|
@ -14,5 +15,6 @@ func TestNeedsReplan(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestReplanSentinelIsConsistent(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "[NEEDS_MORE_STEPS]", replanSentinel)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func TestTopologicalOrderLinear(t *testing.T) {
|
||||
t.Parallel()
|
||||
states := map[string]*nodeState{
|
||||
"a": newNodeState("a", nil),
|
||||
"b": newNodeState("b", []string{"a"}),
|
||||
|
|
@ -23,6 +24,7 @@ func TestTopologicalOrderLinear(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTopologicalOrderParallel(t *testing.T) {
|
||||
t.Parallel()
|
||||
states := map[string]*nodeState{
|
||||
"a": newNodeState("a", nil),
|
||||
"b": newNodeState("b", nil),
|
||||
|
|
@ -40,6 +42,7 @@ func TestTopologicalOrderParallel(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTopologicalOrderCycleDetection(t *testing.T) {
|
||||
t.Parallel()
|
||||
states := map[string]*nodeState{
|
||||
"a": newNodeState("a", []string{"c"}),
|
||||
"b": newNodeState("b", []string{"a"}),
|
||||
|
|
@ -52,6 +55,7 @@ func TestTopologicalOrderCycleDetection(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTopologicalOrderSingleNode(t *testing.T) {
|
||||
t.Parallel()
|
||||
states := map[string]*nodeState{
|
||||
"only": newNodeState("only", nil),
|
||||
}
|
||||
|
|
@ -63,6 +67,7 @@ func TestTopologicalOrderSingleNode(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResolveRefs(t *testing.T) {
|
||||
t.Parallel()
|
||||
states := map[string]*nodeState{
|
||||
"search": newNodeState("search", nil),
|
||||
}
|
||||
|
|
@ -75,6 +80,7 @@ func TestResolveRefs(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResolveRefsNoMatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
states := map[string]*nodeState{}
|
||||
input := `{"path":"#nodemissing"}`
|
||||
result := resolveRefs(input, states)
|
||||
|
|
@ -82,6 +88,7 @@ func TestResolveRefsNoMatch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResolveToolExecArgsNoRefs(t *testing.T) {
|
||||
t.Parallel()
|
||||
states := map[string]*nodeState{}
|
||||
input := `{"path":"/tmp/plain.txt"}`
|
||||
result := resolveToolExecArgs(input, states)
|
||||
|
|
@ -89,6 +96,7 @@ func TestResolveToolExecArgsNoRefs(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestEscapeForJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
|
|
@ -106,6 +114,7 @@ func TestEscapeForJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNodeStateSetAndGetResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
ns := newNodeState("test", nil)
|
||||
|
||||
go func() {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func TestRouteExplicitModes(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultRouterConfig()
|
||||
|
||||
assert.Equal(t, ModeReAct, Route(ModeReAct, "anything", cfg))
|
||||
|
|
@ -15,17 +16,20 @@ func TestRouteExplicitModes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRouteAutoSimpleQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultRouterConfig()
|
||||
assert.Equal(t, ModeReAct, Route(ModeAuto, "what is the weather?", cfg))
|
||||
}
|
||||
|
||||
func TestRouteAutoComplexQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultRouterConfig()
|
||||
longQuery := strings.Repeat("word ", 35)
|
||||
assert.Equal(t, ModeDAG, Route(ModeAuto, longQuery, cfg))
|
||||
}
|
||||
|
||||
func TestRouteAutoParallelKeywords(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultRouterConfig()
|
||||
|
||||
keywords := []string{
|
||||
|
|
@ -42,12 +46,14 @@ func TestRouteAutoParallelKeywords(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRouteAutoToolSignals(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultRouterConfig()
|
||||
q := "search the codebase, read the file, then execute the command"
|
||||
assert.Equal(t, ModeDAG, Route(ModeAuto, q, cfg))
|
||||
}
|
||||
|
||||
func TestToolLoopModeString(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "react", ModeReAct.String())
|
||||
assert.Equal(t, "dag", ModeDAG.String())
|
||||
assert.Equal(t, "auto", ModeAuto.String())
|
||||
|
|
@ -55,6 +61,7 @@ func TestToolLoopModeString(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestClassifyQueryDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultRouterConfig()
|
||||
assert.Equal(t, ModeReAct, classifyQuery("hello", cfg))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,281 +5,148 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/itr"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFBCodec_RequestRoundtrip_Peek(t *testing.T) {
|
||||
orig := itr.NewPeekRequest("req-1", "sess-A", 2, 1024, 4096)
|
||||
func TestFBCodec_RequestRoundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data, err := itr.MarshalRequestFB(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalRequestFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, orig.ID, got.ID)
|
||||
assert.Equal(t, orig.Type, got.Type)
|
||||
assert.Equal(t, orig.Depth, got.Depth)
|
||||
assert.Equal(t, orig.SessionKey, got.SessionKey)
|
||||
origP, ok := orig.Payload.(itr.Peek)
|
||||
require.True(t, ok, "orig payload should be Peek")
|
||||
gotP, ok := got.Payload.(itr.Peek)
|
||||
require.True(t, ok, "got payload should be Peek")
|
||||
assert.Equal(t, origP, gotP)
|
||||
}
|
||||
|
||||
func TestFBCodec_RequestRoundtrip_Grep(t *testing.T) {
|
||||
orig := itr.NewGrepRequest("req-2", "sess-B", 1, "error.*fatal", 25, true)
|
||||
|
||||
data, err := itr.MarshalRequestFB(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalRequestFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, orig.Type, got.Type)
|
||||
p, ok := got.Payload.(itr.Grep)
|
||||
require.True(t, ok, "payload should be Grep")
|
||||
assert.Equal(t, "error.*fatal", p.Pattern)
|
||||
assert.Equal(t, uint32(25), p.MaxMatches)
|
||||
assert.True(t, p.CaseInsensitive)
|
||||
}
|
||||
|
||||
func TestFBCodec_RequestRoundtrip_Partition(t *testing.T) {
|
||||
orig := itr.NewPartitionRequest("req-3", "sess-C", 0, 8, "semantic", 100, true)
|
||||
|
||||
data, err := itr.MarshalRequestFB(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalRequestFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
p, ok := got.Payload.(itr.Partition)
|
||||
require.True(t, ok, "payload should be Partition")
|
||||
assert.Equal(t, uint32(8), p.K)
|
||||
assert.Equal(t, "semantic", p.Method)
|
||||
assert.Equal(t, uint32(100), p.Overlap)
|
||||
assert.True(t, p.Semantic)
|
||||
}
|
||||
|
||||
func TestFBCodec_RequestRoundtrip_Recurse(t *testing.T) {
|
||||
orig := itr.NewRecurseRequest("req-4", "sess-D", 3, "summarize this", "ctx-key-7", 5)
|
||||
|
||||
data, err := itr.MarshalRequestFB(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalRequestFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
p, ok := got.Payload.(itr.Recurse)
|
||||
require.True(t, ok, "payload should be Recurse")
|
||||
assert.Equal(t, "summarize this", p.SubQuery)
|
||||
assert.Equal(t, "ctx-key-7", p.ContextKey)
|
||||
assert.Equal(t, uint8(5), p.DepthHint)
|
||||
}
|
||||
|
||||
func TestFBCodec_RequestRoundtrip_ToolExec(t *testing.T) {
|
||||
orig := itr.NewToolExecRequest("req-5", "sess-E", "tc-1", "read_file", `{"path":"/etc/hosts"}`)
|
||||
|
||||
data, err := itr.MarshalRequestFB(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalRequestFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
p, ok := got.Payload.(itr.ToolExec)
|
||||
require.True(t, ok, "payload should be ToolExec")
|
||||
assert.Equal(t, "read_file", p.ToolName)
|
||||
assert.Equal(t, `{"path":"/etc/hosts"}`, p.ArgsJSON)
|
||||
assert.Equal(t, "tc-1", got.ToolCallID)
|
||||
}
|
||||
|
||||
func TestFBCodec_RequestRoundtrip_ExecWasm(t *testing.T) {
|
||||
orig := itr.ToolRequest{
|
||||
ID: "req-6",
|
||||
Type: itr.CmdExecWasm,
|
||||
Payload: itr.ExecWasm{ModuleKey: "mod-1", Entry: "main", InputJSON: `{"x":1}`},
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
SessionKey: "sess-F",
|
||||
}
|
||||
|
||||
data, err := itr.MarshalRequestFB(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalRequestFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
p, ok := got.Payload.(itr.ExecWasm)
|
||||
require.True(t, ok, "payload should be ExecWasm")
|
||||
assert.Equal(t, "mod-1", p.ModuleKey)
|
||||
assert.Equal(t, "main", p.Entry)
|
||||
assert.Equal(t, `{"x":1}`, p.InputJSON)
|
||||
}
|
||||
|
||||
func TestFBCodec_RequestRoundtrip_Final(t *testing.T) {
|
||||
orig := itr.NewFinalRequest("req-7", "sess-G", 2, "The answer is 42", "ans_var")
|
||||
|
||||
data, err := itr.MarshalRequestFB(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalRequestFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
p, ok := got.Payload.(itr.Final)
|
||||
require.True(t, ok, "payload should be Final")
|
||||
assert.Equal(t, "The answer is 42", p.Answer)
|
||||
assert.Equal(t, "ans_var", p.VarName)
|
||||
}
|
||||
|
||||
func TestFBCodec_RequestRoundtrip_ToolSearch(t *testing.T) {
|
||||
orig := itr.NewToolSearchRequest("req-8", "sess-H", "find file tools", 5)
|
||||
|
||||
data, err := itr.MarshalRequestFB(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalRequestFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
p, ok := got.Payload.(itr.ToolSearch)
|
||||
require.True(t, ok, "payload should be ToolSearch")
|
||||
assert.Equal(t, "find file tools", p.Query)
|
||||
assert.Equal(t, uint8(5), p.MaxResults)
|
||||
}
|
||||
|
||||
func TestFBCodec_RequestRoundtrip_CodeExec(t *testing.T) {
|
||||
orig := itr.NewCodeExecRequest("req-9", "sess-I", "console.log('hi')", "javascript")
|
||||
|
||||
data, err := itr.MarshalRequestFB(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalRequestFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
p, ok := got.Payload.(itr.CodeExec)
|
||||
require.True(t, ok, "payload should be CodeExec")
|
||||
assert.Equal(t, "console.log('hi')", p.Code)
|
||||
assert.Equal(t, "javascript", p.Language)
|
||||
}
|
||||
|
||||
func TestFBCodec_RequestRoundtrip_DAGPlan(t *testing.T) {
|
||||
plan := itr.DAGPlan{
|
||||
Nodes: []itr.DAGNode{
|
||||
{
|
||||
ID: "a",
|
||||
Type: itr.CmdToolExec,
|
||||
Payload: itr.ToolExec{ToolName: "read_file", ArgsJSON: `{"path":"x.txt"}`},
|
||||
},
|
||||
{
|
||||
ID: "b",
|
||||
Type: itr.CmdToolSearch,
|
||||
Payload: itr.ToolSearch{Query: "search tools", MaxResults: 3},
|
||||
DependsOn: []string{"a"},
|
||||
tests := []struct {
|
||||
name string
|
||||
req itr.ToolRequest
|
||||
}{
|
||||
{
|
||||
name: "peek",
|
||||
req: itr.NewPeekRequest("req-1", "sess-A", 2, 1024, 4096),
|
||||
},
|
||||
{
|
||||
name: "grep",
|
||||
req: itr.NewGrepRequest("req-2", "sess-B", 1, "error.*fatal", 25, true),
|
||||
},
|
||||
{
|
||||
name: "partition",
|
||||
req: itr.NewPartitionRequest("req-3", "sess-C", 0, 8, "semantic", 100, true),
|
||||
},
|
||||
{
|
||||
name: "recurse",
|
||||
req: itr.NewRecurseRequest("req-4", "sess-D", 3, "summarize this", "ctx-key-7", 5),
|
||||
},
|
||||
{
|
||||
name: "tool exec",
|
||||
req: itr.NewToolExecRequest("req-5", "sess-E", "tc-1", "read_file", `{"path":"/etc/hosts"}`),
|
||||
},
|
||||
{
|
||||
name: "exec wasm",
|
||||
req: itr.ToolRequest{
|
||||
ID: "req-6",
|
||||
Type: itr.CmdExecWasm,
|
||||
Payload: itr.ExecWasm{ModuleKey: "mod-1", Entry: "main", InputJSON: `{"x":1}`},
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
SessionKey: "sess-F",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "final",
|
||||
req: itr.NewFinalRequest("req-7", "sess-G", 2, "The answer is 42", "ans_var"),
|
||||
},
|
||||
{
|
||||
name: "tool search",
|
||||
req: itr.NewToolSearchRequest("req-8", "sess-H", "find file tools", 5),
|
||||
},
|
||||
{
|
||||
name: "code exec",
|
||||
req: itr.NewCodeExecRequest("req-9", "sess-I", "console.log('hi')", "javascript"),
|
||||
},
|
||||
{
|
||||
name: "dag plan",
|
||||
req: itr.NewDAGPlanRequest("req-10", "sess-J", itr.DAGPlan{
|
||||
Nodes: []itr.DAGNode{
|
||||
{
|
||||
ID: "a",
|
||||
Type: itr.CmdToolExec,
|
||||
Payload: itr.ToolExec{ToolName: "read_file", ArgsJSON: `{"path":"x.txt"}`},
|
||||
},
|
||||
{
|
||||
ID: "b",
|
||||
Type: itr.CmdToolSearch,
|
||||
Payload: itr.ToolSearch{Query: "search tools", MaxResults: 3},
|
||||
DependsOn: []string{"a"},
|
||||
},
|
||||
},
|
||||
MaxParallel: 4,
|
||||
TokenBudget: 10000,
|
||||
JoinerQuery: "Summarize the results",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "timestamp preserved",
|
||||
req: itr.ToolRequest{
|
||||
ID: "ts-test",
|
||||
Type: itr.CmdPeek,
|
||||
Payload: itr.Peek{Start: 0, Length: 10},
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
},
|
||||
},
|
||||
MaxParallel: 4,
|
||||
TokenBudget: 10000,
|
||||
JoinerQuery: "Summarize the results",
|
||||
}
|
||||
orig := itr.NewDAGPlanRequest("req-10", "sess-J", plan)
|
||||
|
||||
data, err := itr.MarshalRequestFB(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalRequestFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, itr.CmdDAGPlan, got.Type)
|
||||
|
||||
gotPlan, ok := got.Payload.(itr.DAGPlan)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, uint8(4), gotPlan.MaxParallel)
|
||||
assert.Equal(t, uint32(10000), gotPlan.TokenBudget)
|
||||
assert.Equal(t, "Summarize the results", gotPlan.JoinerQuery)
|
||||
require.Len(t, gotPlan.Nodes, 2)
|
||||
|
||||
nodeA := gotPlan.Nodes[0]
|
||||
assert.Equal(t, "a", nodeA.ID)
|
||||
assert.Equal(t, itr.CmdToolExec, nodeA.Type)
|
||||
te, ok := nodeA.Payload.(itr.ToolExec)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "read_file", te.ToolName)
|
||||
assert.Equal(t, `{"path":"x.txt"}`, te.ArgsJSON)
|
||||
|
||||
nodeB := gotPlan.Nodes[1]
|
||||
assert.Equal(t, "b", nodeB.ID)
|
||||
assert.Equal(t, itr.CmdToolSearch, nodeB.Type)
|
||||
ts, ok := nodeB.Payload.(itr.ToolSearch)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "search tools", ts.Query)
|
||||
assert.Equal(t, uint8(3), ts.MaxResults)
|
||||
assert.Equal(t, []string{"a"}, nodeB.DependsOn)
|
||||
}
|
||||
|
||||
func TestFBCodec_ResponseRoundtrip_Success(t *testing.T) {
|
||||
orig := itr.NewSuccessResponse("resp-1", "file contents here", 150)
|
||||
|
||||
data, err := itr.MarshalResponseFB(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalResponseFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "resp-1", got.ID)
|
||||
assert.Equal(t, "file contents here", got.Result)
|
||||
assert.False(t, got.IsError)
|
||||
assert.Equal(t, uint32(150), got.CostTokens)
|
||||
}
|
||||
|
||||
func TestFBCodec_ResponseRoundtrip_Error(t *testing.T) {
|
||||
orig := itr.NewErrorResponse("resp-2", "tool not found")
|
||||
|
||||
data, err := itr.MarshalResponseFB(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalResponseFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, got.IsError)
|
||||
assert.Equal(t, "tool not found", got.Result)
|
||||
}
|
||||
|
||||
func TestFBCodec_ResponseRoundtrip_Leak(t *testing.T) {
|
||||
orig := itr.NewLeakResponse("resp-3", "redacted output", []string{"api_key", "password"})
|
||||
|
||||
data, err := itr.MarshalResponseFB(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalResponseFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, got.LeakDetected)
|
||||
assert.Equal(t, []string{"api_key", "password"}, got.RedactedKeys)
|
||||
}
|
||||
|
||||
func TestFBCodec_TimestampPreserved(t *testing.T) {
|
||||
ts := time.Now().UnixNano()
|
||||
orig := itr.ToolRequest{
|
||||
ID: "ts-test",
|
||||
Type: itr.CmdPeek,
|
||||
Payload: itr.Peek{Start: 0, Length: 10},
|
||||
Timestamp: ts,
|
||||
}
|
||||
|
||||
data, err := itr.MarshalRequestFB(orig)
|
||||
require.NoError(t, err)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := itr.UnmarshalRequestFB(data)
|
||||
require.NoError(t, err)
|
||||
data, err := itr.MarshalRequestFB(tt.req)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, ts, got.Timestamp)
|
||||
got, err := itr.UnmarshalRequestFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, cmp.Diff(tt.req, got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFBCodec_ResponseRoundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
res itr.ToolResponse
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
res: itr.NewSuccessResponse("resp-1", "file contents here", 150),
|
||||
},
|
||||
{
|
||||
name: "error",
|
||||
res: itr.NewErrorResponse("resp-2", "tool not found"),
|
||||
},
|
||||
{
|
||||
name: "leak",
|
||||
res: itr.NewLeakResponse("resp-3", "redacted output", []string{"api_key", "password"}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data, err := itr.MarshalResponseFB(tt.res)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := itr.UnmarshalResponseFB(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, cmp.Diff(tt.res, got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFBCodec_UnknownCommandType(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := itr.MarshalRequestFB(itr.ToolRequest{
|
||||
ID: "bad",
|
||||
Type: itr.CommandType("nonexistent"),
|
||||
})
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package wasm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -47,14 +46,16 @@ var minimalWASM = []byte{
|
|||
}
|
||||
|
||||
func TestNewRuntime(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
rt, err := NewRuntime(ctx, DefaultRuntimeConfig())
|
||||
require.NoError(t, err)
|
||||
defer rt.Close(ctx)
|
||||
}
|
||||
|
||||
func TestExecuteMinimalModule(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
rt, err := NewRuntime(ctx, DefaultRuntimeConfig())
|
||||
require.NoError(t, err)
|
||||
defer rt.Close(ctx)
|
||||
|
|
@ -67,7 +68,8 @@ func TestExecuteMinimalModule(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExecuteTimeout(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
cfg := DefaultRuntimeConfig()
|
||||
cfg.ExecTimeout = 1 * time.Millisecond
|
||||
rt, err := NewRuntime(ctx, cfg)
|
||||
|
|
@ -83,7 +85,8 @@ func TestExecuteTimeout(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExecuteInvalidWASM(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
rt, err := NewRuntime(ctx, DefaultRuntimeConfig())
|
||||
require.NoError(t, err)
|
||||
defer rt.Close(ctx)
|
||||
|
|
@ -94,7 +97,8 @@ func TestExecuteInvalidWASM(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRuntimeCloseIdempotent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
rt, err := NewRuntime(ctx, DefaultRuntimeConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -103,7 +107,8 @@ func TestRuntimeCloseIdempotent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExecuteAfterClose(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
rt, err := NewRuntime(ctx, DefaultRuntimeConfig())
|
||||
require.NoError(t, err)
|
||||
rt.Close(ctx)
|
||||
|
|
@ -114,6 +119,7 @@ func TestExecuteAfterClose(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLimitedBuffer(t *testing.T) {
|
||||
t.Parallel()
|
||||
lb := &limitedBuffer{max: 5}
|
||||
n, err := lb.Write([]byte("hello world"))
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -122,6 +128,7 @@ func TestLimitedBuffer(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLimitedBufferExactFit(t *testing.T) {
|
||||
t.Parallel()
|
||||
lb := &limitedBuffer{max: 5}
|
||||
n, err := lb.Write([]byte("hello"))
|
||||
assert.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ import (
|
|||
)
|
||||
|
||||
func TestTransportNonCodeExecForwarded(t *testing.T) {
|
||||
rt, err := NewRuntime(context.Background(), DefaultRuntimeConfig())
|
||||
t.Parallel()
|
||||
rt, err := NewRuntime(t.Context(), DefaultRuntimeConfig())
|
||||
require.NoError(t, err)
|
||||
defer rt.Close(context.Background())
|
||||
defer rt.Close(t.Context())
|
||||
|
||||
forwarded := false
|
||||
transport := NewTransport(rt, func(ctx context.Context, req itr.ToolRequest) (itr.ToolResponse, error) {
|
||||
|
|
@ -21,7 +22,7 @@ func TestTransportNonCodeExecForwarded(t *testing.T) {
|
|||
})
|
||||
|
||||
req := itr.NewToolExecRequest("id-1", "sess", "tc", "read_file", `{"path":"/tmp"}`)
|
||||
resp, err := transport.Send(context.Background(), req)
|
||||
resp, err := transport.Send(t.Context(), req)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, forwarded)
|
||||
assert.Equal(t, "forwarded", resp.Result)
|
||||
|
|
@ -29,35 +30,38 @@ func TestTransportNonCodeExecForwarded(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTransportNonCodeExecNoFallback(t *testing.T) {
|
||||
rt, err := NewRuntime(context.Background(), DefaultRuntimeConfig())
|
||||
t.Parallel()
|
||||
rt, err := NewRuntime(t.Context(), DefaultRuntimeConfig())
|
||||
require.NoError(t, err)
|
||||
defer rt.Close(context.Background())
|
||||
defer rt.Close(t.Context())
|
||||
|
||||
transport := NewTransport(rt, nil)
|
||||
|
||||
req := itr.NewToolExecRequest("id-1", "sess", "tc", "read_file", `{"path":"/tmp"}`)
|
||||
resp, err := transport.Send(context.Background(), req)
|
||||
resp, err := transport.Send(t.Context(), req)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, resp.IsError)
|
||||
assert.Contains(t, resp.Result, "unsupported command type")
|
||||
}
|
||||
|
||||
func TestTransportCodeExecEmptyCode(t *testing.T) {
|
||||
rt, err := NewRuntime(context.Background(), DefaultRuntimeConfig())
|
||||
t.Parallel()
|
||||
rt, err := NewRuntime(t.Context(), DefaultRuntimeConfig())
|
||||
require.NoError(t, err)
|
||||
defer rt.Close(context.Background())
|
||||
defer rt.Close(t.Context())
|
||||
|
||||
transport := NewTransport(rt, nil)
|
||||
|
||||
req := itr.NewCodeExecRequest("id-1", "sess", "", "javascript")
|
||||
resp, err := transport.Send(context.Background(), req)
|
||||
resp, err := transport.Send(t.Context(), req)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, resp.IsError)
|
||||
assert.Contains(t, resp.Result, "empty code")
|
||||
}
|
||||
|
||||
func TestTransportClose(t *testing.T) {
|
||||
rt, err := NewRuntime(context.Background(), DefaultRuntimeConfig())
|
||||
t.Parallel()
|
||||
rt, err := NewRuntime(t.Context(), DefaultRuntimeConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
transport := NewTransport(rt, nil)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
)
|
||||
|
||||
func TestLogLevelFiltering(t *testing.T) {
|
||||
t.Parallel()
|
||||
initialLevel := GetLevel()
|
||||
defer SetLevel(initialLevel)
|
||||
|
||||
|
|
@ -45,6 +46,7 @@ func TestLogLevelFiltering(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoggerWithComponent(t *testing.T) {
|
||||
t.Parallel()
|
||||
initialLevel := GetLevel()
|
||||
defer SetLevel(initialLevel)
|
||||
|
||||
|
|
@ -81,6 +83,7 @@ func TestLoggerWithComponent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLogLevels(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
level LogLevel
|
||||
|
|
@ -103,6 +106,7 @@ func TestLogLevels(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSetGetLevel(t *testing.T) {
|
||||
t.Parallel()
|
||||
initialLevel := GetLevel()
|
||||
defer SetLevel(initialLevel)
|
||||
|
||||
|
|
@ -117,6 +121,7 @@ func TestSetGetLevel(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoggerHelperFunctions(t *testing.T) {
|
||||
t.Parallel()
|
||||
initialLevel := GetLevel()
|
||||
defer SetLevel(initialLevel)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package dag_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -18,7 +17,8 @@ import (
|
|||
)
|
||||
|
||||
func TestBackfillMissingSessionDAGs_CreatesSnapshotsAndPersistsStatus(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
d, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, d.Init(ctx))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -24,6 +26,7 @@ func makeMessages(n int) []Message {
|
|||
}
|
||||
|
||||
func TestCompressor_EmptyInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := NewCompressor(DefaultCompressorConfig())
|
||||
d := c.Compress(nil)
|
||||
assert.Empty(t, d.Nodes)
|
||||
|
|
@ -31,6 +34,7 @@ func TestCompressor_EmptyInput(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCompressor_SmallInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := NewCompressor(DefaultCompressorConfig())
|
||||
msgs := []Message{
|
||||
{Role: "user", Content: "Hello, how are you?"},
|
||||
|
|
@ -48,6 +52,7 @@ func TestCompressor_SmallInput(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCompressor_ChunkSplitting(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultCompressorConfig()
|
||||
cfg.ChunkSize = 4
|
||||
c := NewCompressor(cfg)
|
||||
|
|
@ -67,6 +72,7 @@ func TestCompressor_ChunkSplitting(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCompressor_SectionBuilding(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultCompressorConfig()
|
||||
cfg.ChunkSize = 4
|
||||
cfg.SectionSize = 2
|
||||
|
|
@ -89,6 +95,7 @@ func TestCompressor_SectionBuilding(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCompressor_SessionSummary(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultCompressorConfig()
|
||||
cfg.ChunkSize = 4
|
||||
cfg.SectionSize = 2
|
||||
|
|
@ -109,6 +116,7 @@ func TestCompressor_SessionSummary(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractSentences(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
|
|
@ -133,6 +141,7 @@ func TestExtractSentences(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractSentences_Truncation(t *testing.T) {
|
||||
t.Parallel()
|
||||
long := strings.Repeat("This is a very long sentence with many words. ", 20)
|
||||
result := extractSentences(long, 5)
|
||||
assert.LessOrEqual(t, len([]rune(result)), 210)
|
||||
|
|
@ -140,6 +149,7 @@ func TestExtractSentences_Truncation(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNode_FormatForPrompt(t *testing.T) {
|
||||
t.Parallel()
|
||||
n := &Node{
|
||||
ID: "chunk-1",
|
||||
Level: LevelChunk,
|
||||
|
|
@ -153,6 +163,7 @@ func TestNode_FormatForPrompt(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDAG_FormatLevel(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultCompressorConfig()
|
||||
cfg.ChunkSize = 4
|
||||
c := NewCompressor(cfg)
|
||||
|
|
@ -167,6 +178,7 @@ func TestDAG_FormatLevel(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDAG_TotalTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultCompressorConfig()
|
||||
cfg.ChunkSize = 4
|
||||
c := NewCompressor(cfg)
|
||||
|
|
@ -179,25 +191,30 @@ func TestDAG_TotalTokens(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestComputeBudget(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultBudgetConfig()
|
||||
b := ComputeBudget(100000, cfg)
|
||||
|
||||
assert.Equal(t, 100000, b.Total)
|
||||
assert.Equal(t, 20000, b.SystemPrompt)
|
||||
assert.Equal(t, 10000, b.Observations)
|
||||
assert.Equal(t, 5000, b.Knowledge)
|
||||
assert.Equal(t, 25000, b.DAGSummaries)
|
||||
assert.Equal(t, 30000, b.RawTail)
|
||||
assert.Equal(t, 10000, b.ToolResults)
|
||||
assert.Empty(t, cmp.Diff(Budget{
|
||||
Total: 100000,
|
||||
SystemPrompt: 20000,
|
||||
Observations: 10000,
|
||||
Knowledge: 5000,
|
||||
DAGSummaries: 25000,
|
||||
RawTail: 30000,
|
||||
ToolResults: 10000,
|
||||
}, b))
|
||||
}
|
||||
|
||||
func TestBudget_Remaining(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := Budget{Total: 10000}
|
||||
assert.Equal(t, 7000, b.Remaining(1000, 500, 500, 500, 500, 0))
|
||||
assert.Equal(t, 0, b.Remaining(5000, 3000, 1000, 1000, 1000, 0))
|
||||
}
|
||||
|
||||
func TestSelectDAGLevel(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultCompressorConfig()
|
||||
cfg.ChunkSize = 4
|
||||
cfg.SectionSize = 2
|
||||
|
|
@ -219,6 +236,7 @@ func TestSelectDAGLevel(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTailMessageCount(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, 4, TailMessageCount(100)) // Minimum
|
||||
assert.Equal(t, 20, TailMessageCount(1000)) // 1000/50
|
||||
assert.Equal(t, 4, TailMessageCount(0)) // Zero budget
|
||||
|
|
@ -226,6 +244,7 @@ func TestTailMessageCount(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRenderDAGForBudget(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Empty(t, RenderDAGForBudget(nil, 1000))
|
||||
|
||||
cfg := DefaultCompressorConfig()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package delegate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
|
@ -12,6 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func TestNewFromConfig_LocalOnly_DefaultPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
defaultPath := filepath.Join(tmpDir, "test.db")
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ func TestNewFromConfig_LocalOnly_DefaultPath(t *testing.T) {
|
|||
}
|
||||
defer d.Close()
|
||||
|
||||
if err := d.Init(context.Background()); err != nil {
|
||||
if err := d.Init(t.Context()); err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -32,13 +32,14 @@ func TestNewFromConfig_LocalOnly_DefaultPath(t *testing.T) {
|
|||
}
|
||||
|
||||
// Verify it's functional
|
||||
err = d.UpsertWorkingContext(context.Background(), "agent", "sess", "test content")
|
||||
err = d.UpsertWorkingContext(t.Context(), "agent", "sess", "test content")
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertWorkingContext: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFromConfig_LocalOnly_CustomPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
customPath := filepath.Join(tmpDir, "custom.db")
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ func TestNewFromConfig_LocalOnly_CustomPath(t *testing.T) {
|
|||
}
|
||||
defer d.Close()
|
||||
|
||||
if err := d.Init(context.Background()); err != nil {
|
||||
if err := d.Init(t.Context()); err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +64,7 @@ func TestNewFromConfig_LocalOnly_CustomPath(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewFromConfig_CustomDims(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
|
||||
|
|
@ -82,6 +84,7 @@ func TestNewFromConfig_CustomDims(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewFromConfig_DefaultDims(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
|
||||
|
|
@ -99,6 +102,7 @@ func TestNewFromConfig_DefaultDims(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewFromConfig_ReplicaFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
|
||||
|
|
@ -121,12 +125,13 @@ func TestNewFromConfig_ReplicaFallback(t *testing.T) {
|
|||
}
|
||||
|
||||
// Should still be functional in local mode
|
||||
if err := d.Init(context.Background()); err != nil {
|
||||
if err := d.Init(t.Context()); err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFromConfig_LocalFullRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
dbPath := filepath.Join(tmpDir, "roundtrip.db")
|
||||
|
||||
|
|
@ -140,11 +145,11 @@ func TestNewFromConfig_LocalFullRoundTrip(t *testing.T) {
|
|||
}
|
||||
defer d.Close()
|
||||
|
||||
if err := d.Init(context.Background()); err != nil {
|
||||
if err := d.Init(t.Context()); err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Working context round-trip
|
||||
if err := d.UpsertWorkingContext(ctx, "a1", "s1", "hello"); err != nil {
|
||||
|
|
@ -180,6 +185,7 @@ func TestNewFromConfig_LocalFullRoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSyncConfig_Defaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := config.DefaultConfig()
|
||||
// Memory is always enabled -- no Enabled field to check.
|
||||
if cfg.Memory.EmbeddingDims != 768 {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ func makeAuditEntry(agentID, sessionKey, action, target string) *memory.AuditEnt
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_InsertAuditEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
entry *memory.AuditEntry
|
||||
|
|
@ -54,7 +55,7 @@ func TestLibSQLDelegate_InsertAuditEntry(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
require.NoError(t, d.InsertAuditEntry(ctx, tt.entry))
|
||||
|
||||
count, err := d.CountAuditEntries(ctx, tt.entry.AgentID)
|
||||
|
|
@ -65,6 +66,7 @@ func TestLibSQLDelegate_InsertAuditEntry(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_ListAuditEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, d *LibSQLDelegate, ctx context.Context)
|
||||
|
|
@ -114,7 +116,7 @@ func TestLibSQLDelegate_ListAuditEntries(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, d, ctx)
|
||||
}
|
||||
|
|
@ -126,6 +128,7 @@ func TestLibSQLDelegate_ListAuditEntries(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_ListAuditEntriesByAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, d *LibSQLDelegate, ctx context.Context)
|
||||
|
|
@ -160,7 +163,7 @@ func TestLibSQLDelegate_ListAuditEntriesByAction(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, d, ctx)
|
||||
}
|
||||
|
|
@ -177,6 +180,7 @@ func TestLibSQLDelegate_ListAuditEntriesByAction(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_ListAuditEntriesBySession(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, d *LibSQLDelegate, ctx context.Context)
|
||||
|
|
@ -209,7 +213,7 @@ func TestLibSQLDelegate_ListAuditEntriesBySession(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, d, ctx)
|
||||
}
|
||||
|
|
@ -221,8 +225,9 @@ func TestLibSQLDelegate_ListAuditEntriesBySession(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_PruneOldAuditEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
require.NoError(t, d.InsertAuditEntry(ctx, makeAuditEntry("a1", "s1", "tool_call", "t1")))
|
||||
require.NoError(t, d.InsertAuditEntry(ctx, makeAuditEntry("a1", "s1", "tool_call", "t2")))
|
||||
|
|
@ -240,6 +245,7 @@ func TestLibSQLDelegate_PruneOldAuditEntries(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_CountAuditEntriesByAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, d *LibSQLDelegate, ctx context.Context)
|
||||
|
|
@ -269,7 +275,7 @@ func TestLibSQLDelegate_CountAuditEntriesByAction(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, d, ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package delegate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/ids"
|
||||
|
|
@ -10,7 +9,7 @@ import (
|
|||
|
||||
func BenchmarkListRecallItems(b *testing.B) {
|
||||
d := newBenchDelegate(b)
|
||||
ctx := context.Background()
|
||||
ctx := b.Context()
|
||||
agent := "bench-agent"
|
||||
session := "bench-sess"
|
||||
|
||||
|
|
@ -36,7 +35,7 @@ func BenchmarkListRecallItems(b *testing.B) {
|
|||
|
||||
func BenchmarkGetWorkingContext(b *testing.B) {
|
||||
d := newBenchDelegate(b)
|
||||
ctx := context.Background()
|
||||
ctx := b.Context()
|
||||
agent := "bench-agent"
|
||||
session := "bench-sess"
|
||||
|
||||
|
|
@ -51,7 +50,7 @@ func BenchmarkGetWorkingContext(b *testing.B) {
|
|||
|
||||
func BenchmarkUpsertKV(b *testing.B) {
|
||||
d := newBenchDelegate(b)
|
||||
ctx := context.Background()
|
||||
ctx := b.Context()
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
|
@ -62,7 +61,7 @@ func BenchmarkUpsertKV(b *testing.B) {
|
|||
|
||||
func BenchmarkGetKV(b *testing.B) {
|
||||
d := newBenchDelegate(b)
|
||||
ctx := context.Background()
|
||||
ctx := b.Context()
|
||||
_ = d.UpsertKV(ctx, "bench-agent", "bench-key", "bench-value")
|
||||
|
||||
b.ReportAllocs()
|
||||
|
|
@ -74,7 +73,7 @@ func BenchmarkGetKV(b *testing.B) {
|
|||
|
||||
func BenchmarkInsertAuditEntry(b *testing.B) {
|
||||
d := newBenchDelegate(b)
|
||||
ctx := context.Background()
|
||||
ctx := b.Context()
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
|
@ -93,7 +92,7 @@ func BenchmarkInsertAuditEntry(b *testing.B) {
|
|||
// BenchmarkInsertRecallItems_Sequential measures sequential single-insert performance.
|
||||
func BenchmarkInsertRecallItems_Sequential(b *testing.B) {
|
||||
d := newBenchDelegate(b)
|
||||
ctx := context.Background()
|
||||
ctx := b.Context()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
|
|
@ -116,7 +115,7 @@ func BenchmarkInsertRecallItems_Sequential(b *testing.B) {
|
|||
// Compare with BenchmarkInsertRecallItems_Sequential to quantify WAL savings.
|
||||
func BenchmarkInsertRecallItems_Batch(b *testing.B) {
|
||||
d := newBenchDelegate(b)
|
||||
ctx := context.Background()
|
||||
ctx := b.Context()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
|
|
@ -140,7 +139,7 @@ func BenchmarkInsertRecallItems_Batch(b *testing.B) {
|
|||
// BenchmarkInsertArchivalChunks_Sequential measures sequential chunk inserts.
|
||||
func BenchmarkInsertArchivalChunks_Sequential(b *testing.B) {
|
||||
d := newBenchDelegate(b)
|
||||
ctx := context.Background()
|
||||
ctx := b.Context()
|
||||
recallID := ids.New()
|
||||
_ = d.InsertRecallItem(ctx, &memory.RecallItem{
|
||||
ID: recallID, AgentID: "bench-agent", SessionKey: "s",
|
||||
|
|
@ -162,7 +161,7 @@ func BenchmarkInsertArchivalChunks_Sequential(b *testing.B) {
|
|||
// BenchmarkInsertArchivalChunks_Batch measures batch-tx chunk inserts for 5 chunks.
|
||||
func BenchmarkInsertArchivalChunks_Batch(b *testing.B) {
|
||||
d := newBenchDelegate(b)
|
||||
ctx := context.Background()
|
||||
ctx := b.Context()
|
||||
recallID := ids.New()
|
||||
_ = d.InsertRecallItem(ctx, &memory.RecallItem{
|
||||
ID: recallID, AgentID: "bench-agent", SessionKey: "s",
|
||||
|
|
@ -185,11 +184,12 @@ func BenchmarkInsertArchivalChunks_Batch(b *testing.B) {
|
|||
|
||||
func newBenchDelegate(b *testing.B) *LibSQLDelegate {
|
||||
b.Helper()
|
||||
ctx := b.Context()
|
||||
d, err := NewLibSQLInMemory()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if err := d.Init(context.Background()); err != nil {
|
||||
if err := d.Init(ctx); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.Cleanup(func() { d.Close() })
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package delegate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/memory/dag"
|
||||
|
|
@ -10,8 +9,9 @@ import (
|
|||
)
|
||||
|
||||
func TestLibSQLDelegate_PersistDAG(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
compressor := dag.NewCompressor(dag.DefaultCompressorConfig())
|
||||
msgs := make([]dag.Message, 16)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ func makeDoc(agentID, name, category, content string) *memory.AgentDocument {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_GetDocument(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, d *LibSQLDelegate, ctx context.Context)
|
||||
|
|
@ -67,7 +68,7 @@ func TestLibSQLDelegate_GetDocument(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, d, ctx)
|
||||
}
|
||||
|
|
@ -86,6 +87,7 @@ func TestLibSQLDelegate_GetDocument(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_UpsertDocument(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
ops []*memory.AgentDocument
|
||||
|
|
@ -139,7 +141,7 @@ func TestLibSQLDelegate_UpsertDocument(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
for _, doc := range tt.ops {
|
||||
require.NoError(t, d.UpsertDocument(ctx, doc))
|
||||
}
|
||||
|
|
@ -152,6 +154,7 @@ func TestLibSQLDelegate_UpsertDocument(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_DeleteDocument(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, d *LibSQLDelegate, ctx context.Context)
|
||||
|
|
@ -185,7 +188,7 @@ func TestLibSQLDelegate_DeleteDocument(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, d, ctx)
|
||||
}
|
||||
|
|
@ -206,6 +209,7 @@ func TestLibSQLDelegate_DeleteDocument(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_ListDocumentsByCategory(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, d *LibSQLDelegate, ctx context.Context)
|
||||
|
|
@ -256,7 +260,7 @@ func TestLibSQLDelegate_ListDocumentsByCategory(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, d, ctx)
|
||||
}
|
||||
|
|
@ -271,6 +275,7 @@ func TestLibSQLDelegate_ListDocumentsByCategory(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_ListAllDocuments(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, d *LibSQLDelegate, ctx context.Context)
|
||||
|
|
@ -318,7 +323,7 @@ func TestLibSQLDelegate_ListAllDocuments(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, d, ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package delegate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
|
@ -14,8 +13,9 @@ import (
|
|||
)
|
||||
|
||||
func TestCronKVBackend_Roundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
agentID := pkg.NAME
|
||||
kvKey := "cron:store"
|
||||
|
||||
|
|
@ -61,8 +61,9 @@ func TestCronKVBackend_Roundtrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCronKVBackend_UpdatePreservesShape(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
agentID := pkg.NAME
|
||||
kvKey := "cron:store"
|
||||
|
||||
|
|
@ -78,8 +79,9 @@ func TestCronKVBackend_UpdatePreservesShape(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCronKVBackend_PrefixScan(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
agentID := pkg.NAME
|
||||
|
||||
require.NoError(t, d.UpsertKV(ctx, agentID, "cron:store", "{}"))
|
||||
|
|
@ -94,8 +96,9 @@ func TestCronKVBackend_PrefixScan(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestEndToEnd_SessionAndAuditFlow(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
agentID := "a1"
|
||||
sessionKey := "sess-integration"
|
||||
|
||||
|
|
@ -171,8 +174,9 @@ func TestEndToEnd_SessionAndAuditFlow(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestEndToEnd_WorkingContextAndRecallRoundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
agentID := "a1"
|
||||
sessionKey := "sess-wc"
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
)
|
||||
|
||||
func TestLibSQLDelegate_GetKV(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, d *LibSQLDelegate, ctx context.Context)
|
||||
|
|
@ -73,7 +74,7 @@ func TestLibSQLDelegate_GetKV(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, d, ctx)
|
||||
}
|
||||
|
|
@ -89,6 +90,7 @@ func TestLibSQLDelegate_GetKV(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_UpsertKV(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
ops []kvOp
|
||||
|
|
@ -147,7 +149,7 @@ func TestLibSQLDelegate_UpsertKV(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
for _, op := range tt.ops {
|
||||
require.NoError(t, d.UpsertKV(ctx, op.agent, op.key, op.val))
|
||||
}
|
||||
|
|
@ -159,6 +161,7 @@ func TestLibSQLDelegate_UpsertKV(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_DeleteKV(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, d *LibSQLDelegate, ctx context.Context)
|
||||
|
|
@ -193,7 +196,7 @@ func TestLibSQLDelegate_DeleteKV(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, d, ctx)
|
||||
}
|
||||
|
|
@ -218,6 +221,7 @@ func TestLibSQLDelegate_DeleteKV(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_ListKVByPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, d *LibSQLDelegate, ctx context.Context)
|
||||
|
|
@ -286,7 +290,7 @@ func TestLibSQLDelegate_ListKVByPrefix(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, d, ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ func makeRecallItem(agentID, sessionKey, role, content, tags string) *memory.Rec
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_InsertSessionMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
agentID string
|
||||
|
|
@ -70,7 +71,7 @@ func TestLibSQLDelegate_InsertSessionMessage(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
require.NoError(t, d.InsertSessionMessage(ctx, tt.agentID, tt.sessionKey, tt.role, tt.content))
|
||||
|
||||
|
|
@ -82,6 +83,7 @@ func TestLibSQLDelegate_InsertSessionMessage(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_ListSessionMessages(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, d *LibSQLDelegate, ctx context.Context)
|
||||
|
|
@ -182,7 +184,7 @@ func TestLibSQLDelegate_ListSessionMessages(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, d, ctx)
|
||||
}
|
||||
|
|
@ -206,6 +208,7 @@ func TestLibSQLDelegate_ListSessionMessages(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_CountSessionMessages(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, d *LibSQLDelegate, ctx context.Context)
|
||||
|
|
@ -257,7 +260,7 @@ func TestLibSQLDelegate_CountSessionMessages(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, d, ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package delegate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
|
|
@ -15,7 +14,7 @@ func newTestDelegate(t *testing.T) *LibSQLDelegate {
|
|||
if err != nil {
|
||||
t.Fatalf("NewLibSQLInMemory: %v", err)
|
||||
}
|
||||
if err := d.Init(context.Background()); err != nil {
|
||||
if err := d.Init(t.Context()); err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { d.Close() })
|
||||
|
|
@ -23,8 +22,9 @@ func newTestDelegate(t *testing.T) *LibSQLDelegate {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_WorkingContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Initially nil
|
||||
wc, err := d.GetWorkingContext(ctx, "agent-1", "sess-1")
|
||||
|
|
@ -63,8 +63,9 @@ func TestLibSQLDelegate_WorkingContext(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_RecallItemCRUD(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
item := &memory.RecallItem{
|
||||
ID: ids.New(),
|
||||
|
|
@ -156,8 +157,9 @@ func testEmbedding768(seed ...float32) []float32 {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_ArchivalChunkCRUD(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
parentRecall := &memory.RecallItem{
|
||||
ID: ids.New(), AgentID: "agent-1", SessionKey: "sess-1",
|
||||
|
|
@ -237,8 +239,9 @@ func TestLibSQLDelegate_ArchivalChunkCRUD(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_SummaryCRUD(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
summary := &memory.MemorySummary{
|
||||
ID: ids.New(),
|
||||
|
|
@ -266,8 +269,9 @@ func TestLibSQLDelegate_SummaryCRUD(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_KeywordSearch(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
items := []*memory.RecallItem{
|
||||
{ID: ids.New(), AgentID: "agent-1", SessionKey: "s1", Role: "user", Sector: memory.SectorSemantic, Importance: 0.9, Content: "Go programming language is fast"},
|
||||
|
|
@ -291,8 +295,9 @@ func TestLibSQLDelegate_KeywordSearch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_Counts(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Initial counts should be zero
|
||||
rc, err := d.CountRecallItems(ctx, "agent-1", "")
|
||||
|
|
@ -328,8 +333,9 @@ func TestLibSQLDelegate_Counts(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_FTSSearch(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Insert recall items with searchable content
|
||||
items := []*memory.RecallItem{
|
||||
|
|
@ -382,8 +388,9 @@ func TestLibSQLDelegate_FTSSearch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_VectorSearch(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Insert archival chunks with 768-dim embeddings (schema requires F32_BLOB(768))
|
||||
embData := [][]float32{
|
||||
|
|
@ -439,6 +446,7 @@ func TestLibSQLDelegate_VectorSearch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBuildFTSMatchExpr(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
|
|
@ -465,6 +473,7 @@ func TestBuildFTSMatchExpr(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVectorToString(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
input memory.Embedding
|
||||
expected string
|
||||
|
|
@ -485,7 +494,10 @@ func TestVectorToString(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractVector(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Round-trip test: Embedding.Value() -> blob -> extractVector
|
||||
)
|
||||
|
||||
original := memory.Embedding{0.1, -0.2, 0.3, 0.99, -0.01}
|
||||
dv, err := original.Value()
|
||||
if err != nil {
|
||||
|
|
@ -510,6 +522,7 @@ func TestExtractVector(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLibSQLDelegate_Capabilities(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
|
||||
// After Init(), capabilities should have been probed
|
||||
|
|
@ -526,13 +539,14 @@ func TestLibSQLDelegate_Capabilities(t *testing.T) {
|
|||
}
|
||||
|
||||
// Calling detect again should be a no-op (idempotent)
|
||||
d.detectCapabilities(context.Background())
|
||||
d.detectCapabilities(t.Context())
|
||||
if d.HasFTS() != hasFTS || d.HasVectorSearch() != hasVec {
|
||||
t.Error("detectCapabilities changed results on second call — not idempotent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddingValueScanRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
vectors := []memory.Embedding{
|
||||
{0.0, 1.0, -1.0, 0.5, -0.5},
|
||||
{3.4028235e+38, -3.4028235e+38}, // max float32
|
||||
|
|
@ -578,8 +592,9 @@ func TestEmbeddingValueScanRoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIntegration_FullStackNoDisk(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newTestDelegate(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
agentID := "integration-agent"
|
||||
|
||||
t.Run("KV_Store", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
package memory_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/ids"
|
||||
|
|
@ -23,7 +22,7 @@ func setupFullStack(t *testing.T) (*memstore.MemoryStore, *delegate.LibSQLDelega
|
|||
t.Helper()
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err, "create in-memory delegate")
|
||||
require.NoError(t, del.Init(context.Background()), "run goose migrations")
|
||||
require.NoError(t, del.Init(t.Context()), "run goose migrations")
|
||||
t.Cleanup(func() { del.Close() })
|
||||
|
||||
chunker := memstore.NewMarkdownChunker(memstore.DefaultMarkdownChunkerConfig())
|
||||
|
|
@ -33,21 +32,23 @@ func setupFullStack(t *testing.T) (*memstore.MemoryStore, *delegate.LibSQLDelega
|
|||
}
|
||||
|
||||
func TestIntegration_GooseMigrationIdempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
defer del.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
require.NoError(t, del.Init(ctx), "first migration up")
|
||||
require.NoError(t, del.Init(ctx), "idempotent re-Init")
|
||||
}
|
||||
|
||||
func TestIntegration_GooseMigration_DownUpRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
defer del.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
require.NoError(t, del.Init(ctx), "initial up")
|
||||
|
||||
|
|
@ -83,8 +84,9 @@ func TestIntegration_GooseMigration_DownUpRoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIntegration_BlobPK_RoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
store, _ := setupFullStack(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
item := &memory.RecallItem{
|
||||
AgentID: testAgent,
|
||||
|
|
@ -106,8 +108,9 @@ func TestIntegration_BlobPK_RoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIntegration_ArchivalChunking_EndToEnd(t *testing.T) {
|
||||
t.Parallel()
|
||||
store, del := setupFullStack(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
longContent := "# Architecture Notes\n\nThe system uses hexagonal architecture with ports and adapters.\n\n"
|
||||
longContent += "## Database Layer\n\nWe use libSQL with BLOB primary keys for storage efficiency.\n\n"
|
||||
|
|
@ -131,8 +134,9 @@ func TestIntegration_ArchivalChunking_EndToEnd(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIntegration_FTS5Search(t *testing.T) {
|
||||
t.Parallel()
|
||||
store, _ := setupFullStack(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
items := []struct {
|
||||
content string
|
||||
|
|
@ -172,8 +176,9 @@ func TestIntegration_FTS5Search(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIntegration_CascadeDelete(t *testing.T) {
|
||||
t.Parallel()
|
||||
store, del := setupFullStack(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
recallID, err := store.StoreArchival(ctx, "Archival content to be cascade deleted", testAgent, map[string]string{
|
||||
"agent_id": testAgent,
|
||||
|
|
@ -195,8 +200,9 @@ func TestIntegration_CascadeDelete(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIntegration_ToolOffload(t *testing.T) {
|
||||
t.Parallel()
|
||||
store, _ := setupFullStack(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
largeResult := ""
|
||||
for i := 0; i < 500; i++ {
|
||||
|
|
@ -211,8 +217,9 @@ func TestIntegration_ToolOffload(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIntegration_WorkingContext_Persistence(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, del := setupFullStack(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
require.NoError(t, del.UpsertWorkingContext(ctx, testAgent, testSession, "initial state"))
|
||||
|
||||
|
|
@ -228,8 +235,9 @@ func TestIntegration_WorkingContext_Persistence(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIntegration_Summary_CRUD(t *testing.T) {
|
||||
t.Parallel()
|
||||
store, del := setupFullStack(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
summary := &memory.MemorySummary{
|
||||
AgentID: testAgent,
|
||||
|
|
@ -249,8 +257,9 @@ func TestIntegration_Summary_CRUD(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIntegration_IDUniqueness_AcrossEntities(t *testing.T) {
|
||||
t.Parallel()
|
||||
store, _ := setupFullStack(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
seenIDs := make(map[ids.UUID]string)
|
||||
|
||||
|
|
@ -284,8 +293,9 @@ func TestIntegration_IDUniqueness_AcrossEntities(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIntegration_ContextPressure(t *testing.T) {
|
||||
t.Parallel()
|
||||
store, _ := setupFullStack(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
pressure, err := store.ContextUsage(ctx, testAgent, testSession)
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
)
|
||||
|
||||
func TestActiveContextProjection_TotalTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := &ActiveContextProjection{
|
||||
Segments: []ProjectionSegment{
|
||||
{Tokens: 120},
|
||||
|
|
@ -20,6 +21,7 @@ func TestActiveContextProjection_TotalTokens(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestActiveContextProjection_HasLosslessRefs(t *testing.T) {
|
||||
t.Parallel()
|
||||
now := time.Now().UTC()
|
||||
p := &ActiveContextProjection{
|
||||
Segments: []ProjectionSegment{
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ func writeSessionFile(t *testing.T, dir, name string, sess SessionFile) {
|
|||
}
|
||||
|
||||
func TestMigrateFileSessions_Basic(t *testing.T) {
|
||||
t.Parallel()
|
||||
sessDir := t.TempDir()
|
||||
del := newMockDelegate()
|
||||
|
||||
|
|
@ -182,6 +183,7 @@ func TestMigrateFileSessions_Basic(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMigrateFileSessions_Idempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
sessDir := t.TempDir()
|
||||
del := newMockDelegate()
|
||||
|
||||
|
|
@ -217,6 +219,7 @@ func TestMigrateFileSessions_Idempotent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMigrateFileSessions_EmptyDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
sessDir := t.TempDir()
|
||||
del := newMockDelegate()
|
||||
|
||||
|
|
@ -230,6 +233,7 @@ func TestMigrateFileSessions_EmptyDir(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMigrateFileSessions_NonexistentDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
del := newMockDelegate()
|
||||
|
||||
result, err := MigrateFileSessions(t.Context(), del, pkg.NAME, "/nonexistent/path")
|
||||
|
|
@ -242,6 +246,7 @@ func TestMigrateFileSessions_NonexistentDir(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMigrateFileSessions_SkipsEmptyMessages(t *testing.T) {
|
||||
t.Parallel()
|
||||
sessDir := t.TempDir()
|
||||
del := newMockDelegate()
|
||||
|
||||
|
|
@ -264,6 +269,7 @@ func TestMigrateFileSessions_SkipsEmptyMessages(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMigrateFileSessions_FallbackKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
sessDir := t.TempDir()
|
||||
del := newMockDelegate()
|
||||
|
||||
|
|
@ -288,6 +294,7 @@ func TestMigrateFileSessions_FallbackKey(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMigrateFileSessions_MalformedJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
sessDir := t.TempDir()
|
||||
del := newMockDelegate()
|
||||
|
||||
|
|
|
|||
|
|
@ -7,24 +7,30 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewObservation_ThreeDateModel(t *testing.T) {
|
||||
t.Parallel()
|
||||
ref := time.Date(2026, 2, 16, 10, 0, 0, 0, time.UTC)
|
||||
obs := time.Date(2026, 2, 18, 14, 30, 0, 0, time.UTC)
|
||||
|
||||
o := NewObservation("User prefers Go over Rust", PriorityNotable, ref, obs)
|
||||
|
||||
assert.Equal(t, "User prefers Go over Rust", o.Content)
|
||||
assert.Equal(t, PriorityNotable, o.Priority)
|
||||
assert.Equal(t, ref.Unix(), o.ReferencedAt)
|
||||
assert.Equal(t, obs.Unix(), o.ObservedAt)
|
||||
assert.Equal(t, "2 days ago", o.RelativeDate)
|
||||
assert.Empty(t, cmp.Diff(Observation{
|
||||
Content: "User prefers Go over Rust",
|
||||
Priority: PriorityNotable,
|
||||
ObservedAt: obs.Unix(),
|
||||
ReferencedAt: ref.Unix(),
|
||||
RelativeDate: "2 days ago",
|
||||
}, o))
|
||||
}
|
||||
|
||||
func TestRelativeDate(t *testing.T) {
|
||||
t.Parallel()
|
||||
now := time.Date(2026, 2, 18, 14, 0, 0, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -53,6 +59,7 @@ func TestRelativeDate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPriorityEmoji(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "🔴", PriorityCritical.Emoji())
|
||||
assert.Equal(t, "🟡", PriorityNotable.Emoji())
|
||||
assert.Equal(t, "🔵", PriorityInformational.Emoji())
|
||||
|
|
@ -60,6 +67,7 @@ func TestPriorityEmoji(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFormatBlock(t *testing.T) {
|
||||
t.Parallel()
|
||||
now := time.Date(2026, 2, 18, 14, 30, 0, 0, time.UTC)
|
||||
obs := []Observation{
|
||||
NewObservation("Decision: use SQLite", PriorityCritical, now, now),
|
||||
|
|
@ -75,11 +83,13 @@ func TestFormatBlock(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFormatBlock_Empty(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "", FormatBlock(nil))
|
||||
assert.Equal(t, "", FormatBlock([]Observation{}))
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshalRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
now := time.Now()
|
||||
obs := []Observation{
|
||||
NewObservation("Fact A", PriorityCritical, now, now),
|
||||
|
|
@ -93,23 +103,25 @@ func TestMarshalUnmarshalRoundTrip(t *testing.T) {
|
|||
parsed, err := UnmarshalObservations(data)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, parsed, 2)
|
||||
assert.Equal(t, "Fact A", parsed[0].Content)
|
||||
assert.Equal(t, PriorityCritical, parsed[0].Priority)
|
||||
assert.Empty(t, cmp.Diff(obs[0], parsed[0]))
|
||||
}
|
||||
|
||||
func TestUnmarshalObservations_Empty(t *testing.T) {
|
||||
t.Parallel()
|
||||
obs, err := UnmarshalObservations("")
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, obs)
|
||||
}
|
||||
|
||||
func TestEstimateTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
tokens := EstimateTokens("Hello, world!")
|
||||
assert.True(t, tokens > 0)
|
||||
assert.True(t, tokens < 20)
|
||||
}
|
||||
|
||||
func TestParseObservations(t *testing.T) {
|
||||
t.Parallel()
|
||||
now := time.Now()
|
||||
response := `critical|User decided to migrate to SQLite
|
||||
notable|Prefers hexagonal architecture
|
||||
|
|
@ -119,13 +131,31 @@ notable|`
|
|||
|
||||
obs := parseObservations(response, now)
|
||||
assert.Len(t, obs, 3)
|
||||
assert.Equal(t, PriorityCritical, obs[0].Priority)
|
||||
assert.Equal(t, "User decided to migrate to SQLite", obs[0].Content)
|
||||
assert.Equal(t, PriorityNotable, obs[1].Priority)
|
||||
assert.Equal(t, PriorityInformational, obs[2].Priority)
|
||||
assert.Empty(t, cmp.Diff(Observation{
|
||||
Content: "User decided to migrate to SQLite",
|
||||
Priority: PriorityCritical,
|
||||
ObservedAt: now.Unix(),
|
||||
ReferencedAt: now.Unix(),
|
||||
RelativeDate: relativeDate(now, now),
|
||||
}, obs[0]))
|
||||
assert.Empty(t, cmp.Diff(Observation{
|
||||
Content: "Prefers hexagonal architecture",
|
||||
Priority: PriorityNotable,
|
||||
ObservedAt: now.Unix(),
|
||||
ReferencedAt: now.Unix(),
|
||||
RelativeDate: relativeDate(now, now),
|
||||
}, obs[1]))
|
||||
assert.Empty(t, cmp.Diff(Observation{
|
||||
Content: "Uses VS Code as primary editor",
|
||||
Priority: PriorityInformational,
|
||||
ObservedAt: now.Unix(),
|
||||
ReferencedAt: now.Unix(),
|
||||
RelativeDate: relativeDate(now, now),
|
||||
}, obs[2]))
|
||||
}
|
||||
|
||||
func TestObserver_ShouldObserve(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockModel := func(_ context.Context, _ string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
|
@ -140,6 +170,7 @@ func TestObserver_ShouldObserve(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestObserver_Observe(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockModel := func(_ context.Context, prompt string) (string, error) {
|
||||
return "critical|Important decision made\nnotable|User preference noted", nil
|
||||
}
|
||||
|
|
@ -151,13 +182,14 @@ func TestObserver_Observe(t *testing.T) {
|
|||
{Role: "assistant", Content: "Good choice for embedded use cases"},
|
||||
}
|
||||
|
||||
obs, err := o.Observe(context.Background(), msgs, nil)
|
||||
obs, err := o.Observe(t.Context(), msgs, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, obs, 2)
|
||||
assert.Equal(t, PriorityCritical, obs[0].Priority)
|
||||
}
|
||||
|
||||
func TestReflector_ShouldReflect(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockModel := func(_ context.Context, _ string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
|
@ -175,6 +207,7 @@ func TestReflector_ShouldReflect(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestReflector_Reflect(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockModel := func(_ context.Context, prompt string) (string, error) {
|
||||
return "KEEP 0\nDROP 1\nKEEP 2", nil
|
||||
}
|
||||
|
|
@ -188,7 +221,7 @@ func TestReflector_Reflect(t *testing.T) {
|
|||
NewObservation("Notable thing", PriorityNotable, now, now),
|
||||
}
|
||||
|
||||
kept, err := r.Reflect(context.Background(), obs)
|
||||
kept, err := r.Reflect(t.Context(), obs)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, kept, 2)
|
||||
assert.Equal(t, "Critical fact", kept[0].Content)
|
||||
|
|
@ -196,6 +229,7 @@ func TestReflector_Reflect(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestReflector_ReflectFallbackKeepsCritical(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockModel := func(_ context.Context, _ string) (string, error) {
|
||||
return "garbage output", nil
|
||||
}
|
||||
|
|
@ -208,13 +242,14 @@ func TestReflector_ReflectFallbackKeepsCritical(t *testing.T) {
|
|||
NewObservation("Can drop", PriorityInformational, now, now),
|
||||
}
|
||||
|
||||
kept, err := r.Reflect(context.Background(), obs)
|
||||
kept, err := r.Reflect(t.Context(), obs)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, kept, 1)
|
||||
assert.Equal(t, "Must keep", kept[0].Content)
|
||||
}
|
||||
|
||||
func TestParsePriority(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, PriorityCritical, parsePriority("critical"))
|
||||
assert.Equal(t, PriorityCritical, parsePriority("CRITICAL"))
|
||||
assert.Equal(t, PriorityNotable, parsePriority("notable"))
|
||||
|
|
@ -223,6 +258,7 @@ func TestParsePriority(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseKeptIndices(t *testing.T) {
|
||||
t.Parallel()
|
||||
now := time.Now()
|
||||
obs := []Observation{
|
||||
NewObservation("A", PriorityCritical, now, now),
|
||||
|
|
|
|||
|
|
@ -42,9 +42,10 @@ func (e *countingEmbedder) Dimensions() int { return e.dims }
|
|||
func (e *countingEmbedder) Model() string { return "test-model" }
|
||||
|
||||
func TestCachedEmbedder_CachesIdenticalText(t *testing.T) {
|
||||
t.Parallel()
|
||||
inner := &countingEmbedder{dims: 8}
|
||||
cached := NewCachedEmbedder(inner, DefaultCachedEmbedderConfig())
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// First call — should hit inner
|
||||
vec1, err := cached.Embed(ctx, "hello world")
|
||||
|
|
@ -76,9 +77,10 @@ func TestCachedEmbedder_CachesIdenticalText(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCachedEmbedder_DifferentTextHitsInner(t *testing.T) {
|
||||
t.Parallel()
|
||||
inner := &countingEmbedder{dims: 4}
|
||||
cached := NewCachedEmbedder(inner, DefaultCachedEmbedderConfig())
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
cached.Embed(ctx, "text A")
|
||||
cached.Embed(ctx, "text B")
|
||||
|
|
@ -93,9 +95,10 @@ func TestCachedEmbedder_DifferentTextHitsInner(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCachedEmbedder_BatchPartialCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
inner := &countingEmbedder{dims: 4}
|
||||
cached := NewCachedEmbedder(inner, DefaultCachedEmbedderConfig())
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Pre-cache one text
|
||||
cached.Embed(ctx, "cached text")
|
||||
|
|
@ -126,9 +129,10 @@ func TestCachedEmbedder_BatchPartialCache(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCachedEmbedder_BatchAllCached(t *testing.T) {
|
||||
t.Parallel()
|
||||
inner := &countingEmbedder{dims: 4}
|
||||
cached := NewCachedEmbedder(inner, DefaultCachedEmbedderConfig())
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Pre-cache all texts
|
||||
cached.Embed(ctx, "A")
|
||||
|
|
@ -148,6 +152,7 @@ func TestCachedEmbedder_BatchAllCached(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCachedEmbedder_Dimensions(t *testing.T) {
|
||||
t.Parallel()
|
||||
inner := &countingEmbedder{dims: 768}
|
||||
cached := NewCachedEmbedder(inner, DefaultCachedEmbedderConfig())
|
||||
if cached.Dimensions() != 768 {
|
||||
|
|
@ -156,6 +161,7 @@ func TestCachedEmbedder_Dimensions(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCachedEmbedder_Model(t *testing.T) {
|
||||
t.Parallel()
|
||||
inner := &countingEmbedder{dims: 4}
|
||||
cached := NewCachedEmbedder(inner, DefaultCachedEmbedderConfig())
|
||||
if cached.Model() != "test-model" {
|
||||
|
|
@ -164,12 +170,13 @@ func TestCachedEmbedder_Model(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCachedEmbedder_MaxEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
inner := &countingEmbedder{dims: 4}
|
||||
cached := NewCachedEmbedder(inner, CachedEmbedderConfig{
|
||||
MaxEntries: 3,
|
||||
TTL: time.Hour,
|
||||
})
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Fill cache
|
||||
cached.Embed(ctx, "A")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
)
|
||||
|
||||
func TestMarkdownChunker_BasicSplit(t *testing.T) {
|
||||
t.Parallel()
|
||||
chunker := NewMarkdownChunker(MarkdownChunkerConfig{
|
||||
ChunkSize: 100,
|
||||
ChunkOverlap: 20,
|
||||
|
|
@ -26,6 +27,7 @@ func TestMarkdownChunker_BasicSplit(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMarkdownChunker_SmallContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
chunker := NewMarkdownChunker(DefaultMarkdownChunkerConfig())
|
||||
|
||||
chunks, err := chunker.Chunk("Short text.")
|
||||
|
|
@ -35,6 +37,7 @@ func TestMarkdownChunker_SmallContent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMarkdownChunker_PreservesMarkdownStructure(t *testing.T) {
|
||||
t.Parallel()
|
||||
chunker := NewMarkdownChunker(MarkdownChunkerConfig{
|
||||
ChunkSize: 200,
|
||||
ChunkOverlap: 40,
|
||||
|
|
@ -81,6 +84,7 @@ Even more content follows here with additional details and explanations that mak
|
|||
}
|
||||
|
||||
func TestMarkdownChunker_EmptyContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
chunker := NewMarkdownChunker(DefaultMarkdownChunkerConfig())
|
||||
chunks, err := chunker.Chunk("")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -88,6 +92,7 @@ func TestMarkdownChunker_EmptyContent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMarkdownChunker_DefaultConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultMarkdownChunkerConfig()
|
||||
assert.Equal(t, 1600, cfg.ChunkSize)
|
||||
assert.Equal(t, 320, cfg.ChunkOverlap)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/config"
|
||||
)
|
||||
|
||||
func TestNewEmbedderFromConfig_EmptyProvider(t *testing.T) {
|
||||
t.Parallel()
|
||||
emb, err := NewEmbedderFromConfig(config.EmbeddingConfig{}, config.ProvidersConfig{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
|
|
@ -20,6 +22,7 @@ func TestNewEmbedderFromConfig_EmptyProvider(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewEmbedderFromConfig_UnknownProvider(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := NewEmbedderFromConfig(config.EmbeddingConfig{Provider: "nonexistent"}, config.ProvidersConfig{})
|
||||
if err == nil {
|
||||
t.Error("expected error for unknown provider")
|
||||
|
|
@ -27,6 +30,7 @@ func TestNewEmbedderFromConfig_UnknownProvider(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewEmbedderFromConfig_OpenAI_NoKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := NewEmbedderFromConfig(
|
||||
config.EmbeddingConfig{Provider: "openai"},
|
||||
config.ProvidersConfig{},
|
||||
|
|
@ -37,6 +41,7 @@ func TestNewEmbedderFromConfig_OpenAI_NoKey(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewEmbedderFromConfig_OpenAI_FallbackKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
respData, _ := jsonv2.Marshal(map[string]interface{}{
|
||||
"data": []map[string]interface{}{
|
||||
|
|
@ -70,6 +75,7 @@ func TestNewEmbedderFromConfig_OpenAI_FallbackKey(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewEmbedderFromConfig_Ollama(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
respData, _ := jsonv2.Marshal(map[string]interface{}{
|
||||
"embeddings": [][]float32{{0.1, 0.2, 0.3}},
|
||||
|
|
@ -98,6 +104,7 @@ func TestNewEmbedderFromConfig_Ollama(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewEmbedderFromConfig_Ollama_FallbackBase(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
respData, _ := jsonv2.Marshal(map[string]interface{}{
|
||||
"embeddings": [][]float32{{0.5}},
|
||||
|
|
@ -121,6 +128,7 @@ func TestNewEmbedderFromConfig_Ollama_FallbackBase(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewEmbedderFromConfig_CaseInsensitive(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
respData, _ := jsonv2.Marshal(map[string]interface{}{
|
||||
"embeddings": [][]float32{{0.1}},
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
func TestOllamaEmbedder_Embed(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/embed" {
|
||||
t.Errorf("expected /api/embed, got %s", r.URL.Path)
|
||||
|
|
@ -35,7 +36,7 @@ func TestOllamaEmbedder_Embed(t *testing.T) {
|
|||
Model: "test-model",
|
||||
})
|
||||
|
||||
vec, err := e.Embed(context.Background(), "hello world")
|
||||
vec, err := e.Embed(t.Context(), "hello world")
|
||||
if err != nil {
|
||||
t.Fatalf("Embed: %v", err)
|
||||
}
|
||||
|
|
@ -53,6 +54,7 @@ func TestOllamaEmbedder_Embed(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOllamaEmbedder_EmbedBatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
callCount := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
callCount++
|
||||
|
|
@ -65,7 +67,7 @@ func TestOllamaEmbedder_EmbedBatch(t *testing.T) {
|
|||
|
||||
e := NewOllamaEmbedder(OllamaEmbedderConfig{Base: srv.URL})
|
||||
|
||||
vecs, err := e.EmbedBatch(context.Background(), []string{"a", "b", "c"})
|
||||
vecs, err := e.EmbedBatch(t.Context(), []string{"a", "b", "c"})
|
||||
if err != nil {
|
||||
t.Fatalf("EmbedBatch: %v", err)
|
||||
}
|
||||
|
|
@ -78,6 +80,7 @@ func TestOllamaEmbedder_EmbedBatch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOllamaEmbedder_ServerError(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("model not found"))
|
||||
|
|
@ -85,13 +88,14 @@ func TestOllamaEmbedder_ServerError(t *testing.T) {
|
|||
defer srv.Close()
|
||||
|
||||
e := NewOllamaEmbedder(OllamaEmbedderConfig{Base: srv.URL})
|
||||
_, err := e.Embed(context.Background(), "test")
|
||||
_, err := e.Embed(t.Context(), "test")
|
||||
if err == nil {
|
||||
t.Error("expected error for server error response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOllamaEmbedder_EmptyResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
respData, _ := jsonv2.Marshal(ollamaEmbedResponse{Embeddings: [][]float32{}})
|
||||
w.Write(respData)
|
||||
|
|
@ -99,13 +103,14 @@ func TestOllamaEmbedder_EmptyResponse(t *testing.T) {
|
|||
defer srv.Close()
|
||||
|
||||
e := NewOllamaEmbedder(OllamaEmbedderConfig{Base: srv.URL})
|
||||
_, err := e.Embed(context.Background(), "test")
|
||||
_, err := e.Embed(t.Context(), "test")
|
||||
if err == nil {
|
||||
t.Error("expected error for empty embeddings")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOllamaEmbedder_DefaultModel(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := NewOllamaEmbedder(OllamaEmbedderConfig{})
|
||||
if e.Model() != defaultOllamaModel {
|
||||
t.Errorf("expected %q, got %q", defaultOllamaModel, e.Model())
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
func TestOpenAIEmbedder_Embed(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/embeddings" {
|
||||
t.Errorf("expected /embeddings, got %s", r.URL.Path)
|
||||
|
|
@ -40,7 +41,7 @@ func TestOpenAIEmbedder_Embed(t *testing.T) {
|
|||
APIKey: "test-key",
|
||||
})
|
||||
|
||||
vec, err := e.Embed(context.Background(), "hello")
|
||||
vec, err := e.Embed(t.Context(), "hello")
|
||||
if err != nil {
|
||||
t.Fatalf("Embed: %v", err)
|
||||
}
|
||||
|
|
@ -56,6 +57,7 @@ func TestOpenAIEmbedder_Embed(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOpenAIEmbedder_EmbedBatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req openAIEmbedRequest
|
||||
jsonv2.UnmarshalRead(r.Body, &req)
|
||||
|
|
@ -84,7 +86,7 @@ func TestOpenAIEmbedder_EmbedBatch(t *testing.T) {
|
|||
APIKey: "k",
|
||||
})
|
||||
|
||||
vecs, err := e.EmbedBatch(context.Background(), []string{"a", "b", "c"})
|
||||
vecs, err := e.EmbedBatch(t.Context(), []string{"a", "b", "c"})
|
||||
if err != nil {
|
||||
t.Fatalf("EmbedBatch: %v", err)
|
||||
}
|
||||
|
|
@ -94,6 +96,7 @@ func TestOpenAIEmbedder_EmbedBatch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOpenAIEmbedder_SingleTextNotArray(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req openAIEmbedRequest
|
||||
jsonv2.UnmarshalRead(r.Body, &req)
|
||||
|
|
@ -111,13 +114,14 @@ func TestOpenAIEmbedder_SingleTextNotArray(t *testing.T) {
|
|||
defer srv.Close()
|
||||
|
||||
e := NewOpenAIEmbedder(OpenAIEmbedderConfig{Base: srv.URL, APIKey: "k"})
|
||||
_, err := e.Embed(context.Background(), "single")
|
||||
_, err := e.Embed(t.Context(), "single")
|
||||
if err != nil {
|
||||
t.Fatalf("Embed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIEmbedder_ServerError(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`{"error":{"message":"invalid api key"}}`))
|
||||
|
|
@ -125,13 +129,14 @@ func TestOpenAIEmbedder_ServerError(t *testing.T) {
|
|||
defer srv.Close()
|
||||
|
||||
e := NewOpenAIEmbedder(OpenAIEmbedderConfig{Base: srv.URL, APIKey: "bad"})
|
||||
_, err := e.Embed(context.Background(), "test")
|
||||
_, err := e.Embed(t.Context(), "test")
|
||||
if err == nil {
|
||||
t.Error("expected error for 401 response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIEmbedder_DefaultModel(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := NewOpenAIEmbedder(OpenAIEmbedderConfig{APIKey: "k"})
|
||||
if e.Model() != defaultOpenAIModel {
|
||||
t.Errorf("expected %q, got %q", defaultOpenAIModel, e.Model())
|
||||
|
|
@ -139,6 +144,7 @@ func TestOpenAIEmbedder_DefaultModel(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOpenAIEmbedder_NoAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "" {
|
||||
t.Error("expected no auth header when key is empty")
|
||||
|
|
@ -151,7 +157,7 @@ func TestOpenAIEmbedder_NoAuth(t *testing.T) {
|
|||
defer srv.Close()
|
||||
|
||||
e := NewOpenAIEmbedder(OpenAIEmbedderConfig{Base: srv.URL})
|
||||
_, err := e.Embed(context.Background(), "test")
|
||||
_, err := e.Embed(t.Context(), "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Embed: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ func deterministicVec(text string, dim int) memory.Embedding {
|
|||
|
||||
func newTestStore(t *testing.T, withEmbedder bool) *MemoryStore {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
|
|
@ -95,7 +95,8 @@ func newTestStore(t *testing.T, withEmbedder bool) *MemoryStore {
|
|||
}
|
||||
|
||||
func TestWorkingContext_SetAndGet(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, false)
|
||||
|
||||
// Initially empty
|
||||
|
|
@ -122,7 +123,8 @@ func TestWorkingContext_SetAndGet(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestWorkingContext_IsolatedBySessions(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, false)
|
||||
|
||||
err := store.SetWorkingContext(ctx, "agent-1", "session-a", "Context A")
|
||||
|
|
@ -140,7 +142,8 @@ func TestWorkingContext_IsolatedBySessions(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRecall_CRUD(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, false)
|
||||
|
||||
item := &memory.RecallItem{
|
||||
|
|
@ -187,7 +190,8 @@ func TestRecall_CRUD(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestArchival_StoreAndRetrieve(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, true)
|
||||
|
||||
// Store a multi-chunk document
|
||||
|
|
@ -209,7 +213,8 @@ func TestArchival_StoreAndRetrieve(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestArchival_WithoutEmbedder(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, false) // no embedder
|
||||
|
||||
content := "Short archival content for testing without embeddings."
|
||||
|
|
@ -226,7 +231,8 @@ func TestArchival_WithoutEmbedder(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSearch_KeywordOnly(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, false)
|
||||
|
||||
// Seed some recall items
|
||||
|
|
@ -251,7 +257,8 @@ func TestSearch_KeywordOnly(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSearch_HybridWithEmbeddings(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, true)
|
||||
|
||||
// Seed recall items
|
||||
|
|
@ -283,7 +290,8 @@ func TestSearch_HybridWithEmbeddings(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSearch_ShadowModeUsesBaselineAndTracksParity(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, false)
|
||||
|
||||
// Baseline recall hit.
|
||||
|
|
@ -319,7 +327,8 @@ func TestSearch_ShadowModeUsesBaselineAndTracksParity(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSearch_DoesNotPromoteWithoutAugmentedSignals(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, false)
|
||||
|
||||
gates := retrievalPromotionGates{
|
||||
|
|
@ -361,7 +370,8 @@ func TestSearch_DoesNotPromoteWithoutAugmentedSignals(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSearch_PromoteOnlyOnGateWin(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, false)
|
||||
|
||||
gates := retrievalPromotionGates{
|
||||
|
|
@ -406,7 +416,8 @@ func TestSearch_PromoteOnlyOnGateWin(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSearch_FastRollbackPreservesBaselinePath(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, false)
|
||||
|
||||
del, ok := store.delegate.(*delegate.LibSQLDelegate)
|
||||
|
|
@ -492,7 +503,8 @@ func TestSearch_FastRollbackPreservesBaselinePath(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUpdateRetrievalPolicy_PersistFailuresDoNotBlockTransitions(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, true)
|
||||
|
||||
baseDelegate := store.delegate
|
||||
|
|
@ -530,7 +542,8 @@ func TestUpdateRetrievalPolicy_PersistFailuresDoNotBlockTransitions(t *testing.T
|
|||
}
|
||||
|
||||
func TestSearch_ConcurrentRetrievalPolicyUpdates(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, true)
|
||||
|
||||
require.NoError(t, store.StoreRecall(ctx, &memory.RecallItem{
|
||||
|
|
@ -576,7 +589,8 @@ func TestSearch_ConcurrentRetrievalPolicyUpdates(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestContextUsage(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, false)
|
||||
|
||||
// Empty system — should be normal pressure
|
||||
|
|
@ -597,7 +611,8 @@ func TestContextUsage(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestContextUsage_RecallTokenEstimateUsesContent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, false)
|
||||
|
||||
contentA := strings.Repeat("schedule follow-up reminder ", 80)
|
||||
|
|
@ -631,7 +646,8 @@ func TestContextUsage_RecallTokenEstimateUsesContent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestContextUsage_PressureLevels(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
|
|
@ -656,6 +672,7 @@ func TestContextUsage_PressureLevels(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestShouldOffload(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := &MemoryStore{cfg: Config{OffloadThresholdTokens: 100}}
|
||||
|
||||
assert.False(t, store.ShouldOffload("short"))
|
||||
|
|
@ -663,7 +680,8 @@ func TestShouldOffload(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOffloadToolResult(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, false)
|
||||
|
||||
largeContent := strings.Repeat("This is a large tool result that should be offloaded. ", 20)
|
||||
|
|
@ -682,7 +700,8 @@ func TestOffloadToolResult(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStoreSummary(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, false)
|
||||
|
||||
summary := &memory.MemorySummary{
|
||||
|
|
@ -699,7 +718,8 @@ func TestStoreSummary(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDeleteRecall_CascadesArchival(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
store := newTestStore(t, true)
|
||||
|
||||
// Store archival content (creates recall item + archival chunks)
|
||||
|
|
@ -726,6 +746,7 @@ func TestDeleteRecall_CascadesArchival(t *testing.T) {
|
|||
// --- Retrieval pipeline unit tests ---
|
||||
|
||||
func TestCosineSimilarity(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b memory.Embedding
|
||||
|
|
@ -747,6 +768,7 @@ func TestCosineSimilarity(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRRF_MergesTwoSets(t *testing.T) {
|
||||
t.Parallel()
|
||||
idA, idB, idC := ids.New(), ids.New(), ids.New()
|
||||
set1 := []memory.SearchResult{
|
||||
{ID: idA, Content: "a", Score: 1.0},
|
||||
|
|
@ -764,7 +786,10 @@ func TestRRF_MergesTwoSets(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRecencyDecay(t *testing.T) {
|
||||
t.Parallel(
|
||||
// 0 hours age → decay = 1.0
|
||||
)
|
||||
|
||||
assert.InDelta(t, 1.0, RecencyDecay(0, 168), 0.001)
|
||||
|
||||
// 168 hours (1 half-life) → decay = 0.5
|
||||
|
|
@ -775,6 +800,7 @@ func TestRecencyDecay(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestApplyRecencyDecay_ReordersByAge(t *testing.T) {
|
||||
t.Parallel()
|
||||
now := time.Now()
|
||||
idOld, idNew := ids.New(), ids.New()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -17,7 +17,7 @@ func newTestMemoryTool(t *testing.T) *MemoryTool {
|
|||
|
||||
func executeAndParse(t *testing.T, tool *MemoryTool, input string) *MemoryToolResponse {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
raw, err := tool.Execute(ctx, input)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -27,6 +27,7 @@ func executeAndParse(t *testing.T, tool *MemoryTool, input string) *MemoryToolRe
|
|||
}
|
||||
|
||||
func TestMemoryTool_WriteAndRead(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := newTestMemoryTool(t)
|
||||
|
||||
// Write
|
||||
|
|
@ -52,6 +53,7 @@ func TestMemoryTool_WriteAndRead(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMemoryTool_WriteArchival(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := newTestMemoryTool(t)
|
||||
|
||||
resp := executeAndParse(t, tool, `{"action":"write","content":"Large document content for archival.","tier":"archival","source":"test"}`)
|
||||
|
|
@ -60,6 +62,7 @@ func TestMemoryTool_WriteArchival(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMemoryTool_Search(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := newTestMemoryTool(t)
|
||||
|
||||
// Seed data
|
||||
|
|
@ -74,6 +77,7 @@ func TestMemoryTool_Search(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMemoryTool_Update(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := newTestMemoryTool(t)
|
||||
|
||||
// Write
|
||||
|
|
@ -91,6 +95,7 @@ func TestMemoryTool_Update(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMemoryTool_Delete(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := newTestMemoryTool(t)
|
||||
|
||||
// Write
|
||||
|
|
@ -108,6 +113,7 @@ func TestMemoryTool_Delete(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMemoryTool_Status(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := newTestMemoryTool(t)
|
||||
|
||||
resp := executeAndParse(t, tool, `{"action":"status"}`)
|
||||
|
|
@ -118,6 +124,7 @@ func TestMemoryTool_Status(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMemoryTool_InvalidAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := newTestMemoryTool(t)
|
||||
|
||||
resp := executeAndParse(t, tool, `{"action":"explode"}`)
|
||||
|
|
@ -126,6 +133,7 @@ func TestMemoryTool_InvalidAction(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMemoryTool_InvalidJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := newTestMemoryTool(t)
|
||||
|
||||
resp := executeAndParse(t, tool, `not json`)
|
||||
|
|
@ -134,6 +142,7 @@ func TestMemoryTool_InvalidJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMemoryTool_MissingRequiredFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := newTestMemoryTool(t)
|
||||
|
||||
tests := []struct {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -13,7 +12,7 @@ import (
|
|||
|
||||
func newTestQueueManager(t *testing.T, contextWindow int) (*QueueManager, *MemoryStore) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
|
|
@ -32,7 +31,8 @@ func newTestQueueManager(t *testing.T, contextWindow int) (*QueueManager, *Memor
|
|||
}
|
||||
|
||||
func TestQueueManager_NormalPressure(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
qm, _ := newTestQueueManager(t, 100000)
|
||||
|
||||
decision, err := qm.Evaluate(ctx, "agent-1", "session-1")
|
||||
|
|
@ -42,7 +42,8 @@ func TestQueueManager_NormalPressure(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestQueueManager_WarnPressure(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
qm, store := newTestQueueManager(t, 100) // tiny window
|
||||
|
||||
// Fill working context to ~75% of context window.
|
||||
|
|
@ -56,7 +57,8 @@ func TestQueueManager_WarnPressure(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestQueueManager_OffloadPressure(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
qm, store := newTestQueueManager(t, 100) // tiny window
|
||||
|
||||
// Fill to ~82% of context window.
|
||||
|
|
@ -70,7 +72,8 @@ func TestQueueManager_OffloadPressure(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestQueueManager_FlushPressure(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
qm, store := newTestQueueManager(t, 100) // tiny window
|
||||
|
||||
// Fill to ~88% of context window.
|
||||
|
|
@ -84,7 +87,8 @@ func TestQueueManager_FlushPressure(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestQueueManager_EvictOldest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
qm, store := newTestQueueManager(t, 100)
|
||||
|
||||
// Seed recall items
|
||||
|
|
@ -108,7 +112,8 @@ func TestQueueManager_EvictOldest(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestQueueManager_EvictEmpty(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
qm, _ := newTestQueueManager(t, 100)
|
||||
|
||||
evicted, summary, err := qm.EvictOldest(ctx, "agent-1", "empty-session")
|
||||
|
|
@ -118,6 +123,7 @@ func TestQueueManager_EvictEmpty(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDefaultQueueManagerConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := DefaultQueueManagerConfig()
|
||||
assert.InDelta(t, 0.70, cfg.WarnThreshold, 0.001)
|
||||
assert.InDelta(t, 0.80, cfg.OffloadThreshold, 0.001)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/memory"
|
||||
|
|
@ -10,8 +9,9 @@ import (
|
|||
)
|
||||
|
||||
func TestHeuristicScorer_BasicScoring(t *testing.T) {
|
||||
t.Parallel()
|
||||
scorer := NewHeuristicScorer()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -38,8 +38,9 @@ func TestHeuristicScorer_BasicScoring(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHeuristicScorer_SectorClassification(t *testing.T) {
|
||||
t.Parallel()
|
||||
scorer := NewHeuristicScorer()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -78,6 +79,7 @@ func TestHeuristicScorer_SectorClassification(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseScoringResponse_ValidJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := `{"importance": 0.85, "salience": 0.6, "sector": "semantic"}`
|
||||
result, err := parseScoringResponse(input)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -87,6 +89,7 @@ func TestParseScoringResponse_ValidJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseScoringResponse_WithCodeFences(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := "```json\n{\"importance\": 0.9, \"salience\": 0.3, \"sector\": \"procedural\"}\n```"
|
||||
result, err := parseScoringResponse(input)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -95,6 +98,7 @@ func TestParseScoringResponse_WithCodeFences(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseScoringResponse_ClampsValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := `{"importance": 1.5, "salience": -0.3, "sector": "episodic"}`
|
||||
result, err := parseScoringResponse(input)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -103,6 +107,7 @@ func TestParseScoringResponse_ClampsValues(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseScoringResponse_UnknownSector(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := `{"importance": 0.5, "salience": 0.5, "sector": "unknown_sector"}`
|
||||
result, err := parseScoringResponse(input)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -110,6 +115,7 @@ func TestParseScoringResponse_UnknownSector(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNormalizeSector(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
input string
|
||||
expected memory.Sector
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func TestCamelToSnake(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -41,6 +42,7 @@ func TestCamelToSnake(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestConvertKeysToSnake(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := map[string]interface{}{
|
||||
"apiKey": "test-key",
|
||||
"apiBase": "https://example.com",
|
||||
|
|
@ -87,6 +89,7 @@ func TestConvertKeysToSnake(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoadOpenClawConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
|
||||
|
|
@ -144,6 +147,7 @@ func TestLoadOpenClawConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestConvertConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("providers mapping", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"providers": map[string]interface{}{
|
||||
|
|
@ -301,6 +305,7 @@ func TestConvertConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMergeConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("fills empty fields", func(t *testing.T) {
|
||||
existing := config.DefaultConfig()
|
||||
incoming := config.DefaultConfig()
|
||||
|
|
@ -365,6 +370,7 @@ func TestMergeConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPlanWorkspaceMigration(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("copies available files", func(t *testing.T) {
|
||||
srcDir := t.TempDir()
|
||||
dstDir := t.TempDir()
|
||||
|
|
@ -495,6 +501,7 @@ func TestPlanWorkspaceMigration(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFindOpenClawConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("finds openclaw.json", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
|
|
@ -549,6 +556,7 @@ func TestFindOpenClawConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRewriteWorkspacePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -569,6 +577,7 @@ func TestRewriteWorkspacePath(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRunDryRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
openclawHome := t.TempDir()
|
||||
picoClawHome := t.TempDir()
|
||||
|
||||
|
|
@ -610,6 +619,7 @@ func TestRunDryRun(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRunFullMigration(t *testing.T) {
|
||||
t.Parallel()
|
||||
openclawHome := t.TempDir()
|
||||
picoClawHome := t.TempDir()
|
||||
|
||||
|
|
@ -708,6 +718,7 @@ func TestRunFullMigration(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRunOpenClawNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
opts := Options{
|
||||
OpenClawHome: "/nonexistent/path/to/openclaw",
|
||||
PicoClawHome: t.TempDir(),
|
||||
|
|
@ -720,6 +731,7 @@ func TestRunOpenClawNotFound(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRunMutuallyExclusiveFlags(t *testing.T) {
|
||||
t.Parallel()
|
||||
opts := Options{
|
||||
ConfigOnly: true,
|
||||
WorkspaceOnly: true,
|
||||
|
|
@ -732,6 +744,7 @@ func TestRunMutuallyExclusiveFlags(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBackupFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
filePath := filepath.Join(tmpDir, "test.md")
|
||||
os.WriteFile(filePath, []byte("original content"), 0644)
|
||||
|
|
@ -751,6 +764,7 @@ func TestBackupFile(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCopyFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
srcPath := filepath.Join(tmpDir, "src.md")
|
||||
dstPath := filepath.Join(tmpDir, "dst.md")
|
||||
|
|
@ -771,6 +785,7 @@ func TestCopyFile(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRunConfigOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
openclawHome := t.TempDir()
|
||||
picoClawHome := t.TempDir()
|
||||
|
||||
|
|
@ -811,6 +826,7 @@ func TestRunConfigOnly(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRunWorkspaceOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
openclawHome := t.TempDir()
|
||||
picoClawHome := t.TempDir()
|
||||
|
||||
|
|
|
|||
|
|
@ -27,18 +27,22 @@ func errorModel(_ context.Context, _, _ string) (string, uint32, error) {
|
|||
}
|
||||
|
||||
func TestEngine_SmallContext_AnswerDirectly(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := rlm.DefaultEngineConfig()
|
||||
cfg.Strategy.DirectThreshold = 10000 // larger than our test context
|
||||
engine := rlm.NewEngine(cfg, nil, echoModel)
|
||||
|
||||
answer, tokens, err := engine.Answer(context.Background(), "sess", "what?", "short context")
|
||||
answer, tokens, err := engine.Answer(t.Context(), "sess", "what?", "short context")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, answer)
|
||||
assert.Greater(t, tokens, uint32(0))
|
||||
}
|
||||
|
||||
func TestEngine_LargeContext_Partitions(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Force partitioning by setting DirectThreshold very low.
|
||||
)
|
||||
|
||||
cfg := rlm.DefaultEngineConfig()
|
||||
cfg.Strategy.DirectThreshold = 10
|
||||
cfg.Strategy.DefaultPartitionK = 2
|
||||
|
|
@ -48,13 +52,14 @@ func TestEngine_LargeContext_Partitions(t *testing.T) {
|
|||
largeCtx := strings.Repeat("hello world ", 100) // ~1200 bytes
|
||||
engine := rlm.NewEngine(cfg, nil, echoModel)
|
||||
|
||||
answer, tokens, err := engine.Answer(context.Background(), "sess", "summarise", largeCtx)
|
||||
answer, tokens, err := engine.Answer(t.Context(), "sess", "summarise", largeCtx)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, answer)
|
||||
assert.Greater(t, tokens, uint32(0))
|
||||
}
|
||||
|
||||
func TestEngine_MaxDepthTerminates(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := rlm.DefaultEngineConfig()
|
||||
cfg.Strategy.DirectThreshold = 0 // always partition
|
||||
cfg.Strategy.MaxDepth = 3
|
||||
|
|
@ -64,20 +69,22 @@ func TestEngine_MaxDepthTerminates(t *testing.T) {
|
|||
engine := rlm.NewEngine(cfg, nil, echoModel)
|
||||
|
||||
// Should terminate without stack overflow.
|
||||
_, _, err := engine.Answer(context.Background(), "sess", "any", content)
|
||||
_, _, err := engine.Answer(t.Context(), "sess", "any", content)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEngine_ModelError_Propagates(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := rlm.DefaultEngineConfig()
|
||||
cfg.Strategy.DirectThreshold = 10000
|
||||
engine := rlm.NewEngine(cfg, nil, errorModel)
|
||||
|
||||
_, _, err := engine.Answer(context.Background(), "sess", "q", "ctx")
|
||||
_, _, err := engine.Answer(t.Context(), "sess", "q", "ctx")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEngine_GrepQuery_NarrowsContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := rlm.DefaultEngineConfig()
|
||||
cfg.Strategy.DirectThreshold = 10
|
||||
cfg.Strategy.DefaultPartitionK = 2
|
||||
|
|
@ -88,14 +95,15 @@ func TestEngine_GrepQuery_NarrowsContext(t *testing.T) {
|
|||
content := "foo\nfunc myFunction() {}\nbar\nbaz"
|
||||
engine := rlm.NewEngine(cfg, nil, echoModel)
|
||||
|
||||
answer, _, err := engine.Answer(context.Background(), "sess", "find func definition", content)
|
||||
answer, _, err := engine.Answer(t.Context(), "sess", "find func definition", content)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, answer)
|
||||
}
|
||||
|
||||
func TestFanOut_AllPartitions_Processed(t *testing.T) {
|
||||
t.Parallel()
|
||||
partitions := []string{"A", "B", "C", "D"}
|
||||
results := rlm.FanOut(context.Background(), partitions, 2,
|
||||
results := rlm.FanOut(t.Context(), partitions, 2,
|
||||
func(_ context.Context, idx int, key, part string) rlm.PartitionResult {
|
||||
return rlm.PartitionResult{PartitionIdx: idx, ContextKey: key, Answer: "ans-" + part, Tokens: 1}
|
||||
})
|
||||
|
|
@ -108,11 +116,12 @@ func TestFanOut_AllPartitions_Processed(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFanOut_UnboundedConcurrency(t *testing.T) {
|
||||
t.Parallel()
|
||||
parts := make([]string, 20)
|
||||
for i := range parts {
|
||||
parts[i] = fmt.Sprintf("part-%d", i)
|
||||
}
|
||||
results := rlm.FanOut(context.Background(), parts, 0,
|
||||
results := rlm.FanOut(t.Context(), parts, 0,
|
||||
func(_ context.Context, idx int, key, part string) rlm.PartitionResult {
|
||||
return rlm.PartitionResult{PartitionIdx: idx, Answer: part, Tokens: 2}
|
||||
})
|
||||
|
|
@ -121,7 +130,8 @@ func TestFanOut_UnboundedConcurrency(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFanOut_Empty(t *testing.T) {
|
||||
results := rlm.FanOut(context.Background(), nil, 4,
|
||||
t.Parallel()
|
||||
results := rlm.FanOut(t.Context(), nil, 4,
|
||||
func(_ context.Context, _ int, _, _ string) rlm.PartitionResult {
|
||||
return rlm.PartitionResult{}
|
||||
})
|
||||
|
|
@ -129,6 +139,7 @@ func TestFanOut_Empty(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMergeResults_Deduplication(t *testing.T) {
|
||||
t.Parallel()
|
||||
results := []rlm.PartitionResult{
|
||||
{Answer: "alpha"},
|
||||
{Answer: "beta"},
|
||||
|
|
@ -140,6 +151,7 @@ func TestMergeResults_Deduplication(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMergeResults_WithErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
results := []rlm.PartitionResult{
|
||||
{Answer: "good"},
|
||||
{Err: fmt.Errorf("failed"), Answer: "should be skipped"},
|
||||
|
|
@ -149,6 +161,7 @@ func TestMergeResults_WithErrors(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStrategyPlanner_SmallContext_OpFinal(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := rlm.DefaultStrategyConfig()
|
||||
cfg.DirectThreshold = 1000
|
||||
planner := rlm.NewStrategyPlanner(cfg)
|
||||
|
|
@ -158,6 +171,7 @@ func TestStrategyPlanner_SmallContext_OpFinal(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStrategyPlanner_MaxDepth_OpFinal(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := rlm.DefaultStrategyConfig()
|
||||
cfg.MaxDepth = 3
|
||||
planner := rlm.NewStrategyPlanner(cfg)
|
||||
|
|
@ -167,6 +181,7 @@ func TestStrategyPlanner_MaxDepth_OpFinal(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStrategyPlanner_KeywordQuery_OpGrep(t *testing.T) {
|
||||
t.Parallel()
|
||||
planner := rlm.NewStrategyPlanner(rlm.DefaultStrategyConfig())
|
||||
|
||||
// "find" prefix should trigger grep.
|
||||
|
|
@ -176,6 +191,7 @@ func TestStrategyPlanner_KeywordQuery_OpGrep(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStrategyPlanner_LargeContext_OpPartition(t *testing.T) {
|
||||
t.Parallel()
|
||||
planner := rlm.NewStrategyPlanner(rlm.DefaultStrategyConfig())
|
||||
|
||||
op := planner.PlanNext(100000, "summarise everything", 0)
|
||||
|
|
|
|||
|
|
@ -12,14 +12,16 @@ import (
|
|||
)
|
||||
|
||||
func TestFanOutEmpty(t *testing.T) {
|
||||
results := FanOut(context.Background(), nil, 0, nil)
|
||||
t.Parallel()
|
||||
results := FanOut(t.Context(), nil, 0, nil)
|
||||
assert.Nil(t, results)
|
||||
}
|
||||
|
||||
func TestFanOutUnbounded(t *testing.T) {
|
||||
t.Parallel()
|
||||
partitions := []string{"part-0", "part-1", "part-2"}
|
||||
|
||||
results := FanOut(context.Background(), partitions, 0, func(ctx context.Context, idx int, key, partition string) PartitionResult {
|
||||
results := FanOut(t.Context(), partitions, 0, func(ctx context.Context, idx int, key, partition string) PartitionResult {
|
||||
return PartitionResult{
|
||||
PartitionIdx: idx,
|
||||
ContextKey: key,
|
||||
|
|
@ -37,6 +39,7 @@ func TestFanOutUnbounded(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFanOutBounded(t *testing.T) {
|
||||
t.Parallel()
|
||||
partitions := make([]string, 10)
|
||||
for i := range partitions {
|
||||
partitions[i] = fmt.Sprintf("chunk-%d", i)
|
||||
|
|
@ -45,7 +48,7 @@ func TestFanOutBounded(t *testing.T) {
|
|||
var maxConcurrent int64
|
||||
var current int64
|
||||
|
||||
results := FanOut(context.Background(), partitions, 3, func(ctx context.Context, idx int, key, partition string) PartitionResult {
|
||||
results := FanOut(t.Context(), partitions, 3, func(ctx context.Context, idx int, key, partition string) PartitionResult {
|
||||
c := atomic.AddInt64(¤t, 1)
|
||||
for {
|
||||
old := atomic.LoadInt64(&maxConcurrent)
|
||||
|
|
@ -71,9 +74,10 @@ func TestFanOutBounded(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFanOutPreservesOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
partitions := []string{"A", "B", "C", "D"}
|
||||
|
||||
results := FanOut(context.Background(), partitions, 2, func(ctx context.Context, idx int, key, partition string) PartitionResult {
|
||||
results := FanOut(t.Context(), partitions, 2, func(ctx context.Context, idx int, key, partition string) PartitionResult {
|
||||
return PartitionResult{
|
||||
PartitionIdx: idx,
|
||||
Answer: partition,
|
||||
|
|
@ -88,6 +92,7 @@ func TestFanOutPreservesOrder(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMergeResultsDeduplication(t *testing.T) {
|
||||
t.Parallel()
|
||||
results := []PartitionResult{
|
||||
{Answer: " answer one "},
|
||||
{Answer: "answer one"},
|
||||
|
|
@ -101,6 +106,7 @@ func TestMergeResultsDeduplication(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMergeResultsAllErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
results := []PartitionResult{
|
||||
{Err: fmt.Errorf("e1")},
|
||||
{Err: fmt.Errorf("e2")},
|
||||
|
|
@ -109,6 +115,7 @@ func TestMergeResultsAllErrors(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMergeResultsAllEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
results := []PartitionResult{
|
||||
{Answer: ""},
|
||||
{Answer: " "},
|
||||
|
|
@ -117,6 +124,7 @@ func TestMergeResultsAllEmpty(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTotalTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
results := []PartitionResult{
|
||||
{Tokens: 100},
|
||||
{Tokens: 250},
|
||||
|
|
@ -126,5 +134,6 @@ func TestTotalTokens(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTotalTokensEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, uint32(0), TotalTokens(nil))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ import (
|
|||
)
|
||||
|
||||
func TestRope_EmptyRope(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("")
|
||||
assert.Equal(t, 0, r.Len())
|
||||
assert.Equal(t, "", r.String())
|
||||
}
|
||||
|
||||
func TestRope_BasicAppendAndString(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("hello")
|
||||
r.Append(" world")
|
||||
assert.Equal(t, 11, r.Len())
|
||||
|
|
@ -23,6 +25,7 @@ func TestRope_BasicAppendAndString(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRope_LargeContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
content := strings.Repeat("abcdefghij", 1000) // 10000 bytes
|
||||
r := rlm.NewRope(content)
|
||||
assert.Equal(t, 10000, r.Len())
|
||||
|
|
@ -30,6 +33,7 @@ func TestRope_LargeContent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRope_Slice_ValidRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("hello world")
|
||||
s, err := r.Slice(6, 11)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -37,6 +41,7 @@ func TestRope_Slice_ValidRange(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRope_Slice_ZeroLength(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("hello")
|
||||
s, err := r.Slice(2, 2)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -44,12 +49,14 @@ func TestRope_Slice_ZeroLength(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRope_Slice_OutOfRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("hello")
|
||||
_, err := r.Slice(3, 10)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRope_Slice_AcrossAppendBoundary(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("hello")
|
||||
r.Append(" world")
|
||||
s, err := r.Slice(3, 8)
|
||||
|
|
@ -58,12 +65,14 @@ func TestRope_Slice_AcrossAppendBoundary(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRope_Lines(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("line1\nline2\nline3")
|
||||
lines := r.Lines()
|
||||
assert.Equal(t, []string{"line1", "line2", "line3"}, lines)
|
||||
}
|
||||
|
||||
func TestRope_GrepLines_CaseSensitive(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("apple\nBanana\napricot\ncherry")
|
||||
matches := r.GrepLines("ap", 0, false)
|
||||
require.Len(t, matches, 2)
|
||||
|
|
@ -72,6 +81,7 @@ func TestRope_GrepLines_CaseSensitive(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRope_GrepLines_CaseInsensitive(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("Apple\nbanana\nAPRICOT")
|
||||
matches := r.GrepLines("apple", 0, true)
|
||||
require.Len(t, matches, 1)
|
||||
|
|
@ -79,18 +89,21 @@ func TestRope_GrepLines_CaseInsensitive(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRope_GrepLines_MaxMatches(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("aa\naa\naa\naa\naa")
|
||||
matches := r.GrepLines("aa", 3, false)
|
||||
assert.Len(t, matches, 3)
|
||||
}
|
||||
|
||||
func TestRope_GrepLines_NoMatches(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("hello world")
|
||||
matches := r.GrepLines("xyz", 0, false)
|
||||
assert.Empty(t, matches)
|
||||
}
|
||||
|
||||
func TestRope_Partition_Even(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("12345678")
|
||||
parts := r.Partition(4)
|
||||
assert.Len(t, parts, 4)
|
||||
|
|
@ -98,6 +111,7 @@ func TestRope_Partition_Even(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRope_Partition_MoreThanContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("hi")
|
||||
parts := r.Partition(10)
|
||||
assert.Len(t, parts, 10)
|
||||
|
|
@ -107,6 +121,7 @@ func TestRope_Partition_MoreThanContent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRope_Partition_Empty(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rlm.NewRope("")
|
||||
parts := r.Partition(4)
|
||||
assert.Len(t, parts, 4)
|
||||
|
|
@ -116,7 +131,10 @@ func TestRope_Partition_Empty(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRope_RuneLen(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Multi-byte Unicode characters.
|
||||
)
|
||||
|
||||
r := rlm.NewRope("héllo") // 'é' is 2 bytes
|
||||
assert.Equal(t, 5, r.RuneLen())
|
||||
assert.Equal(t, 6, r.Len()) // bytes
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
)
|
||||
|
||||
func TestStrategyPlanNextFinalAtMaxDepth(t *testing.T) {
|
||||
t.Parallel()
|
||||
sp := NewStrategyPlanner(StrategyConfig{
|
||||
DirectThreshold: 8192,
|
||||
DefaultPartitionK: 4,
|
||||
|
|
@ -18,6 +19,7 @@ func TestStrategyPlanNextFinalAtMaxDepth(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStrategyPlanNextFinalSmallContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
sp := NewStrategyPlanner(DefaultStrategyConfig())
|
||||
|
||||
op := sp.PlanNext(1000, "any query", 0)
|
||||
|
|
@ -25,6 +27,7 @@ func TestStrategyPlanNextFinalSmallContext(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStrategyPlanNextGrepForKeywordQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
sp := NewStrategyPlanner(DefaultStrategyConfig())
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -47,6 +50,7 @@ func TestStrategyPlanNextGrepForKeywordQuery(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStrategyPlanNextPartitionDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
sp := NewStrategyPlanner(DefaultStrategyConfig())
|
||||
op := sp.PlanNext(100_000, "summarize this document", 0)
|
||||
assert.Equal(t, OpPartition, op.Type)
|
||||
|
|
@ -54,6 +58,7 @@ func TestStrategyPlanNextPartitionDefault(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStrategyPlanNextPartitionLargeContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
sp := NewStrategyPlanner(DefaultStrategyConfig())
|
||||
op := sp.PlanNext(5_000_000, "summarize this corpus", 0)
|
||||
assert.Equal(t, OpPartition, op.Type)
|
||||
|
|
@ -61,18 +66,22 @@ func TestStrategyPlanNextPartitionLargeContext(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractKeywordQuoted(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "handleRequest", extractKeyword(`find "handleRequest" in the codebase`))
|
||||
}
|
||||
|
||||
func TestExtractKeywordNoQuotes(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "find", extractKeyword("find the main function"))
|
||||
}
|
||||
|
||||
func TestExtractKeywordEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "", extractKeyword(""))
|
||||
}
|
||||
|
||||
func TestLooksLikeKeywordQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.True(t, looksLikeKeywordQuery(`find "something"`))
|
||||
assert.True(t, looksLikeKeywordQuery("error: something broke"))
|
||||
assert.True(t, looksLikeKeywordQuery("func processData"))
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ func TestResolveBaseConfigPath_PrefersXDGOverLegacy(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResolveBaseConfigPath_FallsBackToLegacyWhenXDGMissing(t *testing.T) {
|
||||
t.Parallel()
|
||||
home := t.TempDir()
|
||||
xdg := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
|
|
@ -58,6 +59,7 @@ func TestResolveBaseConfigPath_FallsBackToLegacyWhenXDGMissing(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoadResolvedConfig_AppliesOverlayAndKeepsBaseValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
basePath := filepath.Join(dir, "base.json")
|
||||
overlayPath := filepath.Join(dir, "overlay.json")
|
||||
|
|
@ -82,6 +84,7 @@ func TestLoadResolvedConfig_AppliesOverlayAndKeepsBaseValues(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestEnsureMinProviderTimeout_SetsFloor(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
basePath := filepath.Join(dir, "base.json")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte(`{"providers":{"openai":{"timeout":0}}}`), 0o644))
|
||||
|
|
@ -95,10 +98,11 @@ func TestEnsureMinProviderTimeout_SetsFloor(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStartOutbound_DropAndConsumeDoNotBlockPublishers(t *testing.T) {
|
||||
t.Parallel()
|
||||
modes := []OutboundMode{OutboundModeDrop, OutboundModeConsume}
|
||||
for _, mode := range modes {
|
||||
t.Run(string(mode), func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
|
|
@ -127,7 +131,8 @@ func TestStartOutbound_DropAndConsumeDoNotBlockPublishers(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStartOutbound_CallbackReceivesMessages(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
|
|
@ -158,6 +163,7 @@ func TestStartOutbound_CallbackReceivesMessages(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidateKernelInvariants(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("nil loop", func(t *testing.T) {
|
||||
err := validateKernelInvariants(nil)
|
||||
require.Error(t, err)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
)
|
||||
|
||||
func TestExtractJSON_RawJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -30,6 +31,7 @@ func TestExtractJSON_RawJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractJSON_CodeFence(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -58,6 +60,7 @@ func TestExtractJSON_CodeFence(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractJSON_EmbeddedInProse(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := `Based on my analysis, the result is {"importance": 0.9, "sector": "semantic"} which indicates high relevance.`
|
||||
var result struct {
|
||||
Importance float64 `json:"importance"`
|
||||
|
|
@ -70,6 +73,7 @@ func TestExtractJSON_EmbeddedInProse(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractJSON_NestedBracesInStrings(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := `{"content": "function() { return {}; }", "count": 1}`
|
||||
var result map[string]interface{}
|
||||
err := ExtractJSON(input, &result, nil)
|
||||
|
|
@ -79,6 +83,7 @@ func TestExtractJSON_NestedBracesInStrings(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractJSON_InjectionAttempts(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -131,12 +136,14 @@ func TestExtractJSON_InjectionAttempts(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractJSON_EmptyInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
var result map[string]interface{}
|
||||
err := ExtractJSON("", &result, nil)
|
||||
assert.ErrorIs(t, err, ErrNoJSON)
|
||||
}
|
||||
|
||||
func TestExtractJSON_MultipleFences_TakesFirst(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := "```json\n{\"first\": true}\n```\nmore text\n```json\n{\"second\": true}\n```"
|
||||
var result map[string]interface{}
|
||||
err := ExtractJSON(input, &result, nil)
|
||||
|
|
@ -147,6 +154,7 @@ func TestExtractJSON_MultipleFences_TakesFirst(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSanitizeToolArgs_ValidInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
schema := map[string]ArgSpec{
|
||||
"path": {Type: ArgString, Required: true, MaxLength: 256},
|
||||
"content": {Type: ArgString, Required: true},
|
||||
|
|
@ -166,6 +174,7 @@ func TestSanitizeToolArgs_ValidInput(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSanitizeToolArgs_MissingRequired(t *testing.T) {
|
||||
t.Parallel()
|
||||
schema := map[string]ArgSpec{
|
||||
"path": {Type: ArgString, Required: true},
|
||||
}
|
||||
|
|
@ -176,6 +185,7 @@ func TestSanitizeToolArgs_MissingRequired(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSanitizeToolArgs_ExceedsMaxLength(t *testing.T) {
|
||||
t.Parallel()
|
||||
schema := map[string]ArgSpec{
|
||||
"cmd": {Type: ArgString, Required: true, MaxLength: 10},
|
||||
}
|
||||
|
|
@ -186,6 +196,7 @@ func TestSanitizeToolArgs_ExceedsMaxLength(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSanitizeToolArgs_TypeCoercion(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
argType ArgType
|
||||
|
|
@ -220,12 +231,14 @@ func TestSanitizeToolArgs_TypeCoercion(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractFirstBraced_EscapedQuotes(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := `{"msg": "say \"hello\" world"}`
|
||||
result := extractFirstBraced(input)
|
||||
assert.Equal(t, input, result)
|
||||
}
|
||||
|
||||
func TestExtractFirstBraced_UnbalancedBraces(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := `text { not closed`
|
||||
result := extractFirstBraced(input)
|
||||
assert.Empty(t, result)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
)
|
||||
|
||||
func TestRedactor_APIKeys(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewRedactor()
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -30,6 +31,7 @@ func TestRedactor_APIKeys(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRedactor_Bearer(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewRedactor()
|
||||
input := "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxx"
|
||||
out := r.Redact(input)
|
||||
|
|
@ -37,6 +39,7 @@ func TestRedactor_Bearer(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRedactor_JWT(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewRedactor()
|
||||
jwt := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
||||
out := r.Redact(jwt)
|
||||
|
|
@ -44,6 +47,7 @@ func TestRedactor_JWT(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRedactor_PII(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewRedactor()
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -64,6 +68,7 @@ func TestRedactor_PII(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRedactor_SecretValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewRedactor()
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -84,6 +89,7 @@ func TestRedactor_SecretValues(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRedactor_SafeText(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewRedactor()
|
||||
safe := "This is a normal log message about processing 42 items."
|
||||
assert.Equal(t, safe, r.Redact(safe))
|
||||
|
|
@ -91,6 +97,7 @@ func TestRedactor_SafeText(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRedactor_RedactMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewRedactor()
|
||||
m := map[string]interface{}{
|
||||
"command": "curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxxxxxxxxxxxxxxxxx.yyyyyyyy'",
|
||||
|
|
@ -109,6 +116,7 @@ func TestRedactor_RedactMap(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMaskKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func TestAuditLogAppendAndRetrieve(t *testing.T) {
|
||||
t.Parallel()
|
||||
al := NewAuditLog()
|
||||
|
||||
event := AuditEvent{
|
||||
|
|
@ -32,6 +33,7 @@ func TestAuditLogAppendAndRetrieve(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAuditLogConcurrentAppend(t *testing.T) {
|
||||
t.Parallel()
|
||||
al := NewAuditLog()
|
||||
n := 100
|
||||
|
||||
|
|
@ -52,6 +54,7 @@ func TestAuditLogConcurrentAppend(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAuditLogFilterBySession(t *testing.T) {
|
||||
t.Parallel()
|
||||
al := NewAuditLog()
|
||||
|
||||
_ = al.Append(AuditEvent{RequestID: "r1", SessionKey: "sess-A"})
|
||||
|
|
@ -69,6 +72,7 @@ func TestAuditLogFilterBySession(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAuditLogLeakEvents(t *testing.T) {
|
||||
t.Parallel()
|
||||
al := NewAuditLog()
|
||||
|
||||
_ = al.Append(AuditEvent{RequestID: "r1", LeakDetected: false})
|
||||
|
|
@ -98,6 +102,7 @@ func (ms *mockSink) Write(event AuditEvent) error {
|
|||
}
|
||||
|
||||
func TestAuditLogSinkIntegration(t *testing.T) {
|
||||
t.Parallel()
|
||||
sink := &mockSink{}
|
||||
al := NewAuditLog(sink)
|
||||
|
||||
|
|
@ -109,6 +114,7 @@ func TestAuditLogSinkIntegration(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAuditLogSinkError(t *testing.T) {
|
||||
t.Parallel()
|
||||
sink := &mockSink{failAt: 1}
|
||||
al := NewAuditLog(sink)
|
||||
|
||||
|
|
@ -121,6 +127,7 @@ func TestAuditLogSinkError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAuditLogEventsImmutable(t *testing.T) {
|
||||
t.Parallel()
|
||||
al := NewAuditLog()
|
||||
_ = al.Append(AuditEvent{RequestID: "r1"})
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ package securebus_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/itr"
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/security"
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus"
|
||||
|
|
@ -76,12 +77,13 @@ func makeBus(t *testing.T, toolMap map[string]tools.Tool, secrets *security.Secr
|
|||
// ── tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestBus_SuccessfulToolExec(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := &staticTool{name: "greet", result: "hello world"}
|
||||
bus := makeBus(t, map[string]tools.Tool{"greet": tool}, nil)
|
||||
defer bus.Close()
|
||||
|
||||
req := itr.NewToolExecRequest("req-1", "sess", "tc-1", "greet", makeArgsJSON(nil))
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
resp := bus.Execute(t.Context(), req)
|
||||
|
||||
assert.False(t, resp.IsError)
|
||||
assert.Equal(t, "hello world", resp.Result)
|
||||
|
|
@ -89,22 +91,24 @@ func TestBus_SuccessfulToolExec(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBus_UnknownTool(t *testing.T) {
|
||||
t.Parallel()
|
||||
bus := makeBus(t, map[string]tools.Tool{}, nil)
|
||||
defer bus.Close()
|
||||
|
||||
req := itr.NewToolExecRequest("req-2", "sess", "tc-2", "nonexistent", makeArgsJSON(nil))
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
resp := bus.Execute(t.Context(), req)
|
||||
|
||||
assert.True(t, resp.IsError)
|
||||
}
|
||||
|
||||
func TestBus_ToolReturnsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := &staticTool{name: "fail", result: "something broke", isErr: true}
|
||||
bus := makeBus(t, map[string]tools.Tool{"fail": tool}, nil)
|
||||
defer bus.Close()
|
||||
|
||||
req := itr.NewToolExecRequest("req-3", "sess", "tc-3", "fail", makeArgsJSON(nil))
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
resp := bus.Execute(t.Context(), req)
|
||||
|
||||
assert.True(t, resp.IsError)
|
||||
assert.Equal(t, 1, bus.AuditLog().Len())
|
||||
|
|
@ -113,14 +117,17 @@ func TestBus_ToolReturnsError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBus_LeakDetection(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Tool output contains an API key — should be redacted.
|
||||
)
|
||||
|
||||
apiKey := "AKIAIOSFODNN7EXAMPLE" // fake AWS key matching redactor pattern
|
||||
tool := &staticTool{name: "leaky", result: "result: " + apiKey}
|
||||
bus := makeBus(t, map[string]tools.Tool{"leaky": tool}, nil)
|
||||
defer bus.Close()
|
||||
|
||||
req := itr.NewToolExecRequest("req-4", "sess", "tc-4", "leaky", makeArgsJSON(nil))
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
resp := bus.Execute(t.Context(), req)
|
||||
|
||||
assert.True(t, resp.LeakDetected, "should detect API key in output")
|
||||
assert.NotContains(t, resp.Result, apiKey, "raw API key must not appear in response")
|
||||
|
|
@ -130,7 +137,10 @@ func TestBus_LeakDetection(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBus_SecretInjection_ArgVariant(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Tool reads injected "token" arg from args map.
|
||||
)
|
||||
|
||||
echoT := &echoTool{}
|
||||
|
||||
// Give echo tool a capability that declares a secret injected as arg:input.
|
||||
|
|
@ -174,7 +184,7 @@ func TestBus_SecretInjection_ArgVariant(t *testing.T) {
|
|||
defer bus.Close()
|
||||
|
||||
req := itr.NewToolExecRequest("req-5", "sess", "tc-5", "echo", makeArgsJSON(nil))
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
resp := bus.Execute(t.Context(), req)
|
||||
|
||||
assert.False(t, resp.IsError)
|
||||
assert.Equal(t, "supersecret", resp.Result, "injected secret should appear in tool output")
|
||||
|
|
@ -185,6 +195,7 @@ func TestBus_SecretInjection_ArgVariant(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBus_PolicyViolation_RecursionDepth(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := &staticTool{name: "ok", result: "fine"}
|
||||
bus := makeBus(t, map[string]tools.Tool{"ok": tool}, nil)
|
||||
defer bus.Close()
|
||||
|
|
@ -192,7 +203,7 @@ func TestBus_PolicyViolation_RecursionDepth(t *testing.T) {
|
|||
req := itr.NewToolExecRequest("req-6", "sess", "tc-6", "ok", makeArgsJSON(nil))
|
||||
req.Depth = 255 // far exceeds MaxRecursionDepth=10
|
||||
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
resp := bus.Execute(t.Context(), req)
|
||||
|
||||
assert.True(t, resp.IsError, "depth violation should produce an error response")
|
||||
events := bus.AuditLog().Events()
|
||||
|
|
@ -201,13 +212,14 @@ func TestBus_PolicyViolation_RecursionDepth(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBus_AuditLog_FilterBySession(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := &staticTool{name: "t", result: "ok"}
|
||||
bus := makeBus(t, map[string]tools.Tool{"t": tool}, nil)
|
||||
defer bus.Close()
|
||||
|
||||
for _, sk := range []string{"session-A", "session-A", "session-B"} {
|
||||
req := itr.NewToolExecRequest("req-audit-"+sk, sk, "tc", "t", makeArgsJSON(nil))
|
||||
bus.Execute(context.Background(), req)
|
||||
bus.Execute(t.Context(), req)
|
||||
}
|
||||
|
||||
assert.Equal(t, 3, bus.AuditLog().Len())
|
||||
|
|
@ -216,40 +228,44 @@ func TestBus_AuditLog_FilterBySession(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBus_Transport_Send(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := &staticTool{name: "ping", result: "pong"}
|
||||
bus := makeBus(t, map[string]tools.Tool{"ping": tool}, nil)
|
||||
defer bus.Close()
|
||||
|
||||
req := itr.NewToolExecRequest("req-tr", "sess", "tc", "ping", makeArgsJSON(nil))
|
||||
resp, err := bus.Transport().Send(context.Background(), req)
|
||||
resp, err := bus.Transport().Send(t.Context(), req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "pong", resp.Result)
|
||||
}
|
||||
|
||||
func TestBus_InvalidArgsJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
tool := &staticTool{name: "ok", result: "ok"}
|
||||
bus := makeBus(t, map[string]tools.Tool{"ok": tool}, nil)
|
||||
defer bus.Close()
|
||||
|
||||
req := itr.NewToolExecRequest("req-bad", "sess", "tc", "ok", "{invalid json")
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
resp := bus.Execute(t.Context(), req)
|
||||
|
||||
assert.True(t, resp.IsError)
|
||||
}
|
||||
|
||||
func TestBus_RLMFinalCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
bus := makeBus(t, nil, nil)
|
||||
defer bus.Close()
|
||||
|
||||
req := itr.NewFinalRequest("req-final", "sess", 0, "the answer", "")
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
resp := bus.Execute(t.Context(), req)
|
||||
|
||||
assert.False(t, resp.IsError)
|
||||
assert.Equal(t, "the answer", resp.Result)
|
||||
}
|
||||
|
||||
func TestBus_CloseIdempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
bus := makeBus(t, nil, nil)
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
|
|
@ -260,6 +276,7 @@ func TestBus_CloseIdempotent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBus_ToolSearch(t *testing.T) {
|
||||
t.Parallel()
|
||||
bus := makeBus(t, nil, nil)
|
||||
defer bus.Close()
|
||||
|
||||
|
|
@ -268,24 +285,26 @@ func TestBus_ToolSearch(t *testing.T) {
|
|||
})
|
||||
|
||||
req := itr.NewToolSearchRequest("req-search", "sess", "file operations", 5)
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
resp := bus.Execute(t.Context(), req)
|
||||
|
||||
assert.False(t, resp.IsError)
|
||||
assert.Contains(t, resp.Result, "read_file")
|
||||
}
|
||||
|
||||
func TestBus_ToolSearchNotConfigured(t *testing.T) {
|
||||
t.Parallel()
|
||||
bus := makeBus(t, nil, nil)
|
||||
defer bus.Close()
|
||||
|
||||
req := itr.NewToolSearchRequest("req-search2", "sess", "anything", 5)
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
resp := bus.Execute(t.Context(), req)
|
||||
|
||||
assert.True(t, resp.IsError)
|
||||
assert.Contains(t, resp.Result, "not configured")
|
||||
}
|
||||
|
||||
func TestBus_NilToolResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
executor := func(ctx context.Context, name string, args map[string]interface{}) *tools.ToolResult {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -296,7 +315,7 @@ func TestBus_NilToolResult(t *testing.T) {
|
|||
defer bus.Close()
|
||||
|
||||
req := itr.NewToolExecRequest("req-nil", "sess", "tc", "something", "{}")
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
resp := bus.Execute(t.Context(), req)
|
||||
|
||||
assert.False(t, resp.IsError)
|
||||
assert.Empty(t, resp.Result)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func TestPolicyValidateRecursionDepth(t *testing.T) {
|
||||
t.Parallel()
|
||||
pe := NewPolicyEngine(PolicyConfig{MaxRecursionDepth: 5})
|
||||
|
||||
req := itr.ToolRequest{Depth: 3}
|
||||
|
|
@ -22,6 +23,7 @@ func TestPolicyValidateRecursionDepth(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPolicyValidateNoDepthLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
pe := NewPolicyEngine(PolicyConfig{MaxRecursionDepth: 0})
|
||||
|
||||
req := itr.ToolRequest{Depth: 255}
|
||||
|
|
@ -29,6 +31,7 @@ func TestPolicyValidateNoDepthLimit(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPolicyValidateNetworkSSRFBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
pe := NewPolicyEngine(DefaultPolicyConfig())
|
||||
|
||||
ssrfURLs := []string{
|
||||
|
|
@ -53,6 +56,7 @@ func TestPolicyValidateNetworkSSRFBlocked(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPolicyValidateNetworkAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
pe := NewPolicyEngine(DefaultPolicyConfig())
|
||||
rules := []tools.EndpointRule{{Pattern: "https://api.github.com/*"}}
|
||||
|
||||
|
|
@ -60,6 +64,7 @@ func TestPolicyValidateNetworkAllowed(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPolicyValidateNetworkNoRules(t *testing.T) {
|
||||
t.Parallel()
|
||||
pe := NewPolicyEngine(DefaultPolicyConfig())
|
||||
err := pe.ValidateNetwork("https://example.com", nil)
|
||||
require.Error(t, err)
|
||||
|
|
@ -67,6 +72,7 @@ func TestPolicyValidateNetworkNoRules(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPolicyValidateNetworkNoMatchingRule(t *testing.T) {
|
||||
t.Parallel()
|
||||
pe := NewPolicyEngine(DefaultPolicyConfig())
|
||||
rules := []tools.EndpointRule{{Pattern: "https://api.github.com/*"}}
|
||||
|
||||
|
|
@ -76,6 +82,7 @@ func TestPolicyValidateNetworkNoMatchingRule(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPolicyValidateFilesystemAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
pe := NewPolicyEngine(PolicyConfig{AllowedWorkspace: "/workspace"})
|
||||
rules := []tools.PathRule{{Pattern: "src/*", Mode: "rw"}}
|
||||
|
||||
|
|
@ -85,6 +92,7 @@ func TestPolicyValidateFilesystemAllowed(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPolicyValidateFilesystemNoRules(t *testing.T) {
|
||||
t.Parallel()
|
||||
pe := NewPolicyEngine(PolicyConfig{})
|
||||
err := pe.ValidateFilesystem("/etc/passwd", "r", nil)
|
||||
require.Error(t, err)
|
||||
|
|
@ -92,6 +100,7 @@ func TestPolicyValidateFilesystemNoRules(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPolicyValidateFilesystemModeMismatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
pe := NewPolicyEngine(PolicyConfig{AllowedWorkspace: "/workspace"})
|
||||
rules := []tools.PathRule{{Pattern: "data/*", Mode: "r"}}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
)
|
||||
|
||||
func TestSocketTransportRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
sockPath := filepath.Join(t.TempDir(), "test.sock")
|
||||
|
||||
server, err := NewSocketTransportServer(sockPath)
|
||||
|
|
@ -47,7 +48,7 @@ func TestSocketTransportRoundTrip(t *testing.T) {
|
|||
Timestamp: time.Now().UnixNano(),
|
||||
}
|
||||
|
||||
resp, err := client.Send(context.Background(), req)
|
||||
resp, err := client.Send(t.Context(), req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "req-001", resp.ID)
|
||||
assert.Contains(t, resp.Result, "req-001")
|
||||
|
|
@ -57,6 +58,7 @@ func TestSocketTransportRoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSocketTransportMultipleRequests(t *testing.T) {
|
||||
t.Parallel()
|
||||
sockPath := filepath.Join(t.TempDir(), "multi.sock")
|
||||
|
||||
server, err := NewSocketTransportServer(sockPath)
|
||||
|
|
@ -82,13 +84,14 @@ func TestSocketTransportMultipleRequests(t *testing.T) {
|
|||
"echo",
|
||||
`{}`,
|
||||
)
|
||||
resp, err := client.Send(context.Background(), req)
|
||||
resp, err := client.Send(t.Context(), req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, req.ID, resp.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocketTransportCleanup(t *testing.T) {
|
||||
t.Parallel()
|
||||
sockPath := filepath.Join(t.TempDir(), "cleanup.sock")
|
||||
|
||||
server, err := NewSocketTransportServer(sockPath)
|
||||
|
|
@ -104,6 +107,7 @@ func TestSocketTransportCleanup(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSocketTransportClientSendOnClosed(t *testing.T) {
|
||||
t.Parallel()
|
||||
sockPath := filepath.Join(t.TempDir(), "closed.sock")
|
||||
|
||||
server, err := NewSocketTransportServer(sockPath)
|
||||
|
|
@ -120,7 +124,7 @@ func TestSocketTransportClientSendOnClosed(t *testing.T) {
|
|||
|
||||
client.Close()
|
||||
|
||||
_, err = client.Send(context.Background(), itr.ToolRequest{ID: "fail"})
|
||||
_, err = client.Send(t.Context(), itr.ToolRequest{ID: "fail"})
|
||||
assert.Error(t, err)
|
||||
|
||||
server.Close()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func TestValidateURL_AllowedURLs(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []string{
|
||||
"https://example.com",
|
||||
"https://api.openai.com/v1/chat",
|
||||
|
|
@ -23,6 +24,7 @@ func TestValidateURL_AllowedURLs(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidateURL_BlockedSchemes(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []string{
|
||||
"file:///etc/passwd",
|
||||
"ftp://internal.server/data",
|
||||
|
|
@ -38,6 +40,7 @@ func TestValidateURL_BlockedSchemes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidateURL_BlockedHosts(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
|
|
@ -57,6 +60,7 @@ func TestValidateURL_BlockedHosts(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidateURL_BlockedIPs(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
|
|
@ -78,6 +82,7 @@ func TestValidateURL_BlockedIPs(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidateURL_EmptyAndInvalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
|
|
@ -95,6 +100,7 @@ func TestValidateURL_EmptyAndInvalid(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIsBlockedIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
ip string
|
||||
blocked bool
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func TestVault_RoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
key, err := GenerateKey()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, key, 32)
|
||||
|
|
@ -35,6 +36,7 @@ func TestVault_RoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVault_DifferentCiphertexts(t *testing.T) {
|
||||
t.Parallel()
|
||||
key, _ := GenerateKey()
|
||||
v, _ := NewVault(key)
|
||||
|
||||
|
|
@ -44,6 +46,7 @@ func TestVault_DifferentCiphertexts(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVault_WrongKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
key1, _ := GenerateKey()
|
||||
key2, _ := GenerateKey()
|
||||
|
||||
|
|
@ -58,6 +61,7 @@ func TestVault_WrongKey(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVault_TamperedCiphertext(t *testing.T) {
|
||||
t.Parallel()
|
||||
key, _ := GenerateKey()
|
||||
v, _ := NewVault(key)
|
||||
|
||||
|
|
@ -70,6 +74,7 @@ func TestVault_TamperedCiphertext(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVault_InvalidKeyLength(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := NewVault([]byte("too-short"))
|
||||
assert.ErrorIs(t, err, ErrKeyLength)
|
||||
|
||||
|
|
@ -78,6 +83,7 @@ func TestVault_InvalidKeyLength(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVault_EmptyInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
key, _ := GenerateKey()
|
||||
v, _ := NewVault(key)
|
||||
|
||||
|
|
@ -90,6 +96,7 @@ func TestVault_EmptyInput(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVault_BinaryData(t *testing.T) {
|
||||
t.Parallel()
|
||||
key, _ := GenerateKey()
|
||||
v, _ := NewVault(key)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
)
|
||||
|
||||
func TestSchnorrKeypair(t *testing.T) {
|
||||
t.Parallel()
|
||||
key := make([]byte, 32)
|
||||
for i := range key {
|
||||
key[i] = byte(i + 1)
|
||||
|
|
@ -22,11 +23,13 @@ func TestSchnorrKeypair(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSchnorrKeypairRejectsBadLength(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := SchnorrKeypair([]byte("short"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSchnorrFullHandshake(t *testing.T) {
|
||||
t.Parallel()
|
||||
masterKey := make([]byte, 32)
|
||||
for i := range masterKey {
|
||||
masterKey[i] = byte(i + 42)
|
||||
|
|
@ -49,6 +52,7 @@ func TestSchnorrFullHandshake(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSchnorrRejectsWrongKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
masterKey1 := make([]byte, 32)
|
||||
masterKey2 := make([]byte, 32)
|
||||
for i := range masterKey1 {
|
||||
|
|
@ -73,6 +77,7 @@ func TestSchnorrRejectsWrongKey(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestZKPSessionManagerIssueAndValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
masterKey := make([]byte, 32)
|
||||
for i := range masterKey {
|
||||
masterKey[i] = byte(i + 7)
|
||||
|
|
@ -96,6 +101,7 @@ func TestZKPSessionManagerIssueAndValidate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestZKPSessionManagerRejectsInvalidProof(t *testing.T) {
|
||||
t.Parallel()
|
||||
masterKey := make([]byte, 32)
|
||||
for i := range masterKey {
|
||||
masterKey[i] = byte(i)
|
||||
|
|
@ -109,6 +115,7 @@ func TestZKPSessionManagerRejectsInvalidProof(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestZKPSessionManagerExpiry(t *testing.T) {
|
||||
t.Parallel()
|
||||
masterKey := make([]byte, 32)
|
||||
for i := range masterKey {
|
||||
masterKey[i] = byte(i + 3)
|
||||
|
|
@ -128,6 +135,7 @@ func TestZKPSessionManagerExpiry(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestZKPSessionManagerRevoke(t *testing.T) {
|
||||
t.Parallel()
|
||||
masterKey := make([]byte, 32)
|
||||
for i := range masterKey {
|
||||
masterKey[i] = byte(i + 5)
|
||||
|
|
@ -148,6 +156,7 @@ func TestZKPSessionManagerRevoke(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHandshakePayloadBinaryRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
hp := HandshakePayload{Phase: 3}
|
||||
for i := 0; i < 32; i++ {
|
||||
hp.RX[i] = byte(i)
|
||||
|
|
@ -165,6 +174,7 @@ func TestHandshakePayloadBinaryRoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHandshakeResultBinaryRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
hr := HandshakeResult{ExpiresUnix: time.Now().Unix()}
|
||||
for i := 0; i < 32; i++ {
|
||||
hr.SessionToken[i] = byte(i + 200)
|
||||
|
|
@ -179,6 +189,7 @@ func TestHandshakeResultBinaryRoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestZKPSessionManagerCleanup(t *testing.T) {
|
||||
t.Parallel()
|
||||
masterKey := make([]byte, 32)
|
||||
for i := range masterKey {
|
||||
masterKey[i] = byte(i + 11)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -9,6 +8,8 @@ import (
|
|||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ import (
|
|||
)
|
||||
|
||||
func TestSanitizeFilename(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
|
|
@ -40,6 +42,7 @@ func TestSanitizeFilename(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSave_WithColonInKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
sm := NewSessionManager(tmpDir)
|
||||
|
||||
|
|
@ -71,6 +74,7 @@ func TestSave_WithColonInKey(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSave_RejectsPathTraversal(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir := t.TempDir()
|
||||
sm := NewSessionManager(tmpDir)
|
||||
|
||||
|
|
@ -84,6 +88,7 @@ func TestSave_RejectsPathTraversal(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTruncateHistory_ToolCallAware(t *testing.T) {
|
||||
t.Parallel()
|
||||
sm := NewSessionManager("") // in-memory only
|
||||
|
||||
key := "test-tool-truncation"
|
||||
|
|
@ -129,6 +134,7 @@ func TestTruncateHistory_ToolCallAware(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTruncateHistory_NoToolCalls(t *testing.T) {
|
||||
t.Parallel()
|
||||
sm := NewSessionManager("")
|
||||
|
||||
key := "test-no-tools"
|
||||
|
|
@ -151,6 +157,7 @@ func TestTruncateHistory_NoToolCalls(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAddFullMessage_NoHardCapTruncation(t *testing.T) {
|
||||
t.Parallel()
|
||||
sm := NewSessionManager("")
|
||||
|
||||
key := "test-hard-cap"
|
||||
|
|
@ -167,6 +174,7 @@ func TestAddFullMessage_NoHardCapTruncation(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCleanupStale(t *testing.T) {
|
||||
t.Parallel()
|
||||
sm := NewSessionManager("")
|
||||
|
||||
sm.AddFullMessage("active", messages.Message{Role: "user", Content: "hi"})
|
||||
|
|
@ -195,11 +203,12 @@ func TestCleanupStale(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSessionManager_DelegatePersistence(t *testing.T) {
|
||||
t.Parallel()
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
if err != nil {
|
||||
t.Fatalf("NewLibSQLInMemory: %v", err)
|
||||
}
|
||||
if err := del.Init(context.Background()); err != nil {
|
||||
if err := del.Init(t.Context()); err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
defer del.Close()
|
||||
|
|
@ -215,7 +224,7 @@ func TestSessionManager_DelegatePersistence(t *testing.T) {
|
|||
t.Fatalf("expected 2 messages in-memory, got %d", len(history))
|
||||
}
|
||||
|
||||
items, err := del.ListRecallItems(context.Background(), "test-agent", key, 100, 0)
|
||||
items, err := del.ListRecallItems(t.Context(), "test-agent", key, 100, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("ListRecallItems: %v", err)
|
||||
}
|
||||
|
|
@ -228,11 +237,12 @@ func TestSessionManager_DelegatePersistence(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSessionManager_DelegateSaveIsNoop(t *testing.T) {
|
||||
t.Parallel()
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
if err != nil {
|
||||
t.Fatalf("NewLibSQLInMemory: %v", err)
|
||||
}
|
||||
if err := del.Init(context.Background()); err != nil {
|
||||
if err := del.Init(t.Context()); err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
defer del.Close()
|
||||
|
|
@ -256,11 +266,12 @@ func TestSessionManager_DelegateSaveIsNoop(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSessionManager_DelegateBootstrapPaginationAndOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
if err != nil {
|
||||
t.Fatalf("NewLibSQLInMemory: %v", err)
|
||||
}
|
||||
if err := del.Init(context.Background()); err != nil {
|
||||
if err := del.Init(t.Context()); err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
defer del.Close()
|
||||
|
|
@ -291,9 +302,10 @@ func TestSessionManager_DelegateBootstrapPaginationAndOrder(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSessionManager_ProjectionPointerPersistedAndRestored(t *testing.T) {
|
||||
t.Parallel()
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, del.Init(context.Background()))
|
||||
require.NoError(t, del.Init(t.Context()))
|
||||
defer del.Close()
|
||||
|
||||
sessionKey := "ptr-persist"
|
||||
|
|
@ -303,35 +315,34 @@ func TestSessionManager_ProjectionPointerPersistedAndRestored(t *testing.T) {
|
|||
sm.AddMessage(sessionKey, "user", "third")
|
||||
|
||||
// Read persisted pointer from KV
|
||||
raw, err := del.GetKV(context.Background(), "test-agent", projectionPointerKey(sessionKey))
|
||||
raw, err := del.GetKV(t.Context(), "test-agent", projectionPointerKey(sessionKey))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, raw)
|
||||
var ptr ProjectionPointer
|
||||
require.NoError(t, jsonv2.Unmarshal([]byte(raw), &ptr))
|
||||
assert.Equal(t, 3, ptr.Count)
|
||||
assert.False(t, ptr.FirstMessageID.IsZero())
|
||||
assert.False(t, ptr.LastMessageID.IsZero())
|
||||
assert.NotZero(t, ptr.FirstMessageID)
|
||||
assert.NotZero(t, ptr.LastMessageID)
|
||||
assert.False(t, ptr.FirstCreatedAt.IsZero())
|
||||
assert.False(t, ptr.LastCreatedAt.IsZero())
|
||||
assert.Equal(t, 3, ptr.Count)
|
||||
|
||||
// New manager restores; pointer is re-persisted (same values)
|
||||
sm2 := NewSessionManager("", WithSessionDelegate(del, "test-agent"))
|
||||
history := sm2.GetHistory(sessionKey)
|
||||
require.Len(t, history, 3)
|
||||
raw2, err := del.GetKV(context.Background(), "test-agent", projectionPointerKey(sessionKey))
|
||||
raw2, err := del.GetKV(t.Context(), "test-agent", projectionPointerKey(sessionKey))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, raw2)
|
||||
var ptr2 ProjectionPointer
|
||||
require.NoError(t, jsonv2.Unmarshal([]byte(raw2), &ptr2))
|
||||
assert.Equal(t, ptr.Count, ptr2.Count)
|
||||
assert.Equal(t, ptr.FirstMessageID, ptr2.FirstMessageID)
|
||||
assert.Equal(t, ptr.LastMessageID, ptr2.LastMessageID)
|
||||
assert.Empty(t, cmp.Diff(ptr, ptr2, cmpopts.IgnoreFields(ProjectionPointer{}, "FirstCreatedAt", "LastCreatedAt")))
|
||||
}
|
||||
|
||||
func TestSessionManager_ProjectionPointerUpdatedOnAppend(t *testing.T) {
|
||||
t.Parallel()
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, del.Init(context.Background()))
|
||||
require.NoError(t, del.Init(t.Context()))
|
||||
defer del.Close()
|
||||
|
||||
sessionKey := "ptr-append"
|
||||
|
|
@ -339,7 +350,7 @@ func TestSessionManager_ProjectionPointerUpdatedOnAppend(t *testing.T) {
|
|||
|
||||
for i := 0; i < 4; i++ {
|
||||
sm.AddMessage(sessionKey, "user", fmt.Sprintf("msg-%d", i))
|
||||
raw, err := del.GetKV(context.Background(), "test-agent", projectionPointerKey(sessionKey))
|
||||
raw, err := del.GetKV(t.Context(), "test-agent", projectionPointerKey(sessionKey))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, raw)
|
||||
var ptr ProjectionPointer
|
||||
|
|
@ -349,9 +360,10 @@ func TestSessionManager_ProjectionPointerUpdatedOnAppend(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSessionManager_IntegrityMismatchRestoreStillSucceeds(t *testing.T) {
|
||||
t.Parallel()
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, del.Init(context.Background()))
|
||||
require.NoError(t, del.Init(t.Context()))
|
||||
defer del.Close()
|
||||
|
||||
sessionKey := "integrity-mismatch"
|
||||
|
|
@ -362,7 +374,7 @@ func TestSessionManager_IntegrityMismatchRestoreStillSucceeds(t *testing.T) {
|
|||
// Corrupt the stored pointer to simulate prior state mismatch
|
||||
corrupt := ProjectionPointer{Count: 0}
|
||||
data, _ := jsonv2.Marshal(corrupt)
|
||||
require.NoError(t, del.UpsertKV(context.Background(), "test-agent", projectionPointerKey(sessionKey), string(data)))
|
||||
require.NoError(t, del.UpsertKV(t.Context(), "test-agent", projectionPointerKey(sessionKey), string(data)))
|
||||
|
||||
// New manager restores; should succeed (lossless) despite mismatch
|
||||
sm2 := NewSessionManager("", WithSessionDelegate(del, "test-agent"))
|
||||
|
|
@ -372,7 +384,7 @@ func TestSessionManager_IntegrityMismatchRestoreStillSucceeds(t *testing.T) {
|
|||
assert.Equal(t, "b", history[1].Content)
|
||||
|
||||
// Pointer should now reflect restored state
|
||||
raw, err := del.GetKV(context.Background(), "test-agent", projectionPointerKey(sessionKey))
|
||||
raw, err := del.GetKV(t.Context(), "test-agent", projectionPointerKey(sessionKey))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, raw)
|
||||
var ptr ProjectionPointer
|
||||
|
|
@ -381,9 +393,10 @@ func TestSessionManager_IntegrityMismatchRestoreStillSucceeds(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSessionManager_ProjectionBackfillStatusPersistedOnBootstrap(t *testing.T) {
|
||||
t.Parallel()
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, del.Init(context.Background()))
|
||||
require.NoError(t, del.Init(t.Context()))
|
||||
defer del.Close()
|
||||
|
||||
writer := NewSessionManager("", WithSessionDelegate(del, "test-agent"))
|
||||
|
|
@ -393,7 +406,7 @@ func TestSessionManager_ProjectionBackfillStatusPersistedOnBootstrap(t *testing.
|
|||
|
||||
_ = NewSessionManager("", WithSessionDelegate(del, "test-agent"))
|
||||
|
||||
raw, err := del.GetKV(context.Background(), "test-agent", projectionBackfillStatusKey())
|
||||
raw, err := del.GetKV(t.Context(), "test-agent", projectionBackfillStatusKey())
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, raw)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func TestParseWikilinks(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
|
|
@ -66,6 +67,7 @@ func TestParseWikilinks(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMergeUnique(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b []string
|
||||
|
|
@ -94,6 +96,7 @@ func writeSkill(t *testing.T, dir, name, content string) {
|
|||
}
|
||||
|
||||
func TestBuildGraph_Basic(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := t.TempDir()
|
||||
|
||||
writeSkill(t, tmp, "risk-management", `---
|
||||
|
|
@ -152,6 +155,7 @@ No wikilinks here.
|
|||
}
|
||||
|
||||
func TestBuildGraph_FrontmatterLinksAndWikilinks(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := t.TempDir()
|
||||
|
||||
writeSkill(t, tmp, "alpha", `---
|
||||
|
|
@ -186,6 +190,7 @@ No links.
|
|||
}
|
||||
|
||||
func TestTraverseFrom(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := t.TempDir()
|
||||
|
||||
writeSkill(t, tmp, "a", `---
|
||||
|
|
@ -233,6 +238,7 @@ No outgoing links.
|
|||
}
|
||||
|
||||
func TestSearchSkills(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := t.TempDir()
|
||||
|
||||
writeSkill(t, tmp, "risk-management", `---
|
||||
|
|
@ -278,6 +284,7 @@ Content.
|
|||
}
|
||||
|
||||
func TestListMOCs(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := t.TempDir()
|
||||
|
||||
writeSkill(t, tmp, "trading-moc", `---
|
||||
|
|
@ -305,6 +312,7 @@ Content.
|
|||
}
|
||||
|
||||
func TestGetIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := t.TempDir()
|
||||
|
||||
writeSkill(t, tmp, "index", `---
|
||||
|
|
@ -332,6 +340,7 @@ Content.
|
|||
}
|
||||
|
||||
func TestGetIndex_None(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := t.TempDir()
|
||||
writeSkill(t, tmp, "some-skill", `---
|
||||
name: some-skill
|
||||
|
|
@ -346,6 +355,7 @@ Content.
|
|||
}
|
||||
|
||||
func TestExtendedFrontmatter_JSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := t.TempDir()
|
||||
|
||||
writeSkill(t, tmp, "json-skill", `---
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
)
|
||||
|
||||
func TestSkillsInfoValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
testcases := []struct {
|
||||
name string
|
||||
skillName string
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func TestAvailableTemplates(t *testing.T) {
|
||||
t.Parallel()
|
||||
templates := AvailableTemplates()
|
||||
assert.Contains(t, templates, "trading")
|
||||
assert.Contains(t, templates, "legal")
|
||||
|
|
@ -17,6 +18,7 @@ func TestAvailableTemplates(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInstallTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := t.TempDir()
|
||||
|
||||
err := InstallTemplate("trading", tmp)
|
||||
|
|
@ -35,6 +37,7 @@ func TestInstallTemplate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInstallTemplate_BuildsValidGraph(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := t.TempDir()
|
||||
require.NoError(t, InstallTemplate("trading", tmp))
|
||||
|
||||
|
|
@ -56,6 +59,7 @@ func TestInstallTemplate_BuildsValidGraph(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInstallTemplate_NotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := t.TempDir()
|
||||
err := InstallTemplate("nonexistent", tmp)
|
||||
assert.Error(t, err)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -11,7 +10,10 @@ import (
|
|||
)
|
||||
|
||||
func TestAtomicSave(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Create temp workspace
|
||||
)
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "state-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -21,7 +23,7 @@ func TestAtomicSave(t *testing.T) {
|
|||
sm := NewManager(tmpDir)
|
||||
|
||||
// Test SetLastChannel
|
||||
err = sm.SetLastChannel(context.Background(), "test-channel")
|
||||
err = sm.SetLastChannel(t.Context(), "test-channel")
|
||||
if err != nil {
|
||||
t.Fatalf("SetLastChannel failed: %v", err)
|
||||
}
|
||||
|
|
@ -51,6 +53,7 @@ func TestAtomicSave(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSetLastChatID(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "state-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -60,7 +63,7 @@ func TestSetLastChatID(t *testing.T) {
|
|||
sm := NewManager(tmpDir)
|
||||
|
||||
// Test SetLastChatID
|
||||
err = sm.SetLastChatID(context.Background(), "test-chat-id")
|
||||
err = sm.SetLastChatID(t.Context(), "test-chat-id")
|
||||
if err != nil {
|
||||
t.Fatalf("SetLastChatID failed: %v", err)
|
||||
}
|
||||
|
|
@ -84,6 +87,7 @@ func TestSetLastChatID(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAtomicity_NoCorruptionOnInterrupt(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "state-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -93,7 +97,7 @@ func TestAtomicity_NoCorruptionOnInterrupt(t *testing.T) {
|
|||
sm := NewManager(tmpDir)
|
||||
|
||||
// Write initial state
|
||||
err = sm.SetLastChannel(context.Background(), "initial-channel")
|
||||
err = sm.SetLastChannel(t.Context(), "initial-channel")
|
||||
if err != nil {
|
||||
t.Fatalf("SetLastChannel failed: %v", err)
|
||||
}
|
||||
|
|
@ -115,7 +119,7 @@ func TestAtomicity_NoCorruptionOnInterrupt(t *testing.T) {
|
|||
os.Remove(tempFile)
|
||||
|
||||
// Now do a proper save
|
||||
err = sm.SetLastChannel(context.Background(), "new-channel")
|
||||
err = sm.SetLastChannel(t.Context(), "new-channel")
|
||||
if err != nil {
|
||||
t.Fatalf("SetLastChannel failed: %v", err)
|
||||
}
|
||||
|
|
@ -127,6 +131,7 @@ func TestAtomicity_NoCorruptionOnInterrupt(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestConcurrentAccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "state-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -140,7 +145,7 @@ func TestConcurrentAccess(t *testing.T) {
|
|||
for i := 0; i < 10; i++ {
|
||||
go func(idx int) {
|
||||
channel := fmt.Sprintf("channel-%d", idx)
|
||||
sm.SetLastChannel(context.Background(), channel)
|
||||
sm.SetLastChannel(t.Context(), channel)
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
|
@ -170,6 +175,7 @@ func TestConcurrentAccess(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewManager_ExistingState(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "state-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
@ -178,8 +184,8 @@ func TestNewManager_ExistingState(t *testing.T) {
|
|||
|
||||
// Create initial state
|
||||
sm1 := NewManager(tmpDir)
|
||||
sm1.SetLastChannel(context.Background(), "existing-channel")
|
||||
sm1.SetLastChatID(context.Background(), "existing-chat-id")
|
||||
sm1.SetLastChannel(t.Context(), "existing-channel")
|
||||
sm1.SetLastChatID(t.Context(), "existing-chat-id")
|
||||
|
||||
// Create new manager with same workspace
|
||||
sm2 := NewManager(tmpDir)
|
||||
|
|
@ -195,6 +201,7 @@ func TestNewManager_ExistingState(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewManager_EmptyWorkspace(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, err := os.MkdirTemp("", "state-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ func setupIdentityDir(t *testing.T, files map[string]string) string {
|
|||
}
|
||||
|
||||
func TestSyncAll_InsertsNewFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := setupIdentityDir(t, map[string]string{
|
||||
"AGENT.md": "# Agent\nYou are helpful.",
|
||||
"SOUL.md": "# Soul\nCurious and kind.",
|
||||
|
|
@ -95,7 +96,7 @@ func TestSyncAll_InsertsNewFiles(t *testing.T) {
|
|||
store := newMockStore()
|
||||
s := New(dir, "agent-1", store)
|
||||
|
||||
require.NoError(t, s.SyncAll(context.Background()))
|
||||
require.NoError(t, s.SyncAll(t.Context()))
|
||||
|
||||
for _, name := range IdentityFiles {
|
||||
doc := store.getDoc("agent-1", name)
|
||||
|
|
@ -109,6 +110,7 @@ func TestSyncAll_InsertsNewFiles(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSyncAll_SkipsUnchangedFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := setupIdentityDir(t, map[string]string{
|
||||
"AGENT.md": "# Agent\nSame content.",
|
||||
})
|
||||
|
|
@ -116,29 +118,30 @@ func TestSyncAll_SkipsUnchangedFiles(t *testing.T) {
|
|||
store := newMockStore()
|
||||
s := New(dir, "agent-1", store)
|
||||
|
||||
require.NoError(t, s.SyncAll(context.Background()))
|
||||
require.NoError(t, s.SyncAll(t.Context()))
|
||||
firstDoc := store.getDoc("agent-1", "AGENT.md")
|
||||
require.NotNil(t, firstDoc)
|
||||
firstID := firstDoc.ID
|
||||
|
||||
require.NoError(t, s.SyncAll(context.Background()))
|
||||
require.NoError(t, s.SyncAll(t.Context()))
|
||||
secondDoc := store.getDoc("agent-1", "AGENT.md")
|
||||
assert.Equal(t, firstID, secondDoc.ID, "unchanged file should not be re-upserted")
|
||||
}
|
||||
|
||||
func TestSyncAll_UpsertsModifiedFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := setupIdentityDir(t, map[string]string{
|
||||
"AGENT.md": "# Agent\nVersion 1",
|
||||
})
|
||||
|
||||
store := newMockStore()
|
||||
s := New(dir, "agent-1", store)
|
||||
require.NoError(t, s.SyncAll(context.Background()))
|
||||
require.NoError(t, s.SyncAll(t.Context()))
|
||||
|
||||
v1Hash := store.getHash("agent-1", "AGENT.md")
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "AGENT.md"), []byte("# Agent\nVersion 2"), 0644))
|
||||
require.NoError(t, s.SyncAll(context.Background()))
|
||||
require.NoError(t, s.SyncAll(t.Context()))
|
||||
|
||||
v2Hash := store.getHash("agent-1", "AGENT.md")
|
||||
assert.NotEqual(t, v1Hash, v2Hash, "hash should change after file modification")
|
||||
|
|
@ -148,6 +151,7 @@ func TestSyncAll_UpsertsModifiedFiles(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSyncAll_SkipsMissingFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := setupIdentityDir(t, map[string]string{
|
||||
"AGENT.md": "# Agent only",
|
||||
})
|
||||
|
|
@ -155,7 +159,7 @@ func TestSyncAll_SkipsMissingFiles(t *testing.T) {
|
|||
store := newMockStore()
|
||||
s := New(dir, "agent-1", store)
|
||||
|
||||
require.NoError(t, s.SyncAll(context.Background()))
|
||||
require.NoError(t, s.SyncAll(t.Context()))
|
||||
|
||||
assert.NotNil(t, store.getDoc("agent-1", "AGENT.md"))
|
||||
assert.Nil(t, store.getDoc("agent-1", "SOUL.md"), "missing file should not create a doc")
|
||||
|
|
@ -164,6 +168,7 @@ func TestSyncAll_SkipsMissingFiles(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSyncAll_SkipsEmptyFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := setupIdentityDir(t, map[string]string{
|
||||
"AGENT.md": " \n\t\n ",
|
||||
})
|
||||
|
|
@ -171,11 +176,12 @@ func TestSyncAll_SkipsEmptyFiles(t *testing.T) {
|
|||
store := newMockStore()
|
||||
s := New(dir, "agent-1", store)
|
||||
|
||||
require.NoError(t, s.SyncAll(context.Background()))
|
||||
require.NoError(t, s.SyncAll(t.Context()))
|
||||
assert.Nil(t, store.getDoc("agent-1", "AGENT.md"), "empty/whitespace-only file should be skipped")
|
||||
}
|
||||
|
||||
func TestSyncAll_IsolatesAgents(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := setupIdentityDir(t, map[string]string{
|
||||
"AGENT.md": "# Shared agent file",
|
||||
})
|
||||
|
|
@ -184,8 +190,8 @@ func TestSyncAll_IsolatesAgents(t *testing.T) {
|
|||
s1 := New(dir, "agent-a", store)
|
||||
s2 := New(dir, "agent-b", store)
|
||||
|
||||
require.NoError(t, s1.SyncAll(context.Background()))
|
||||
require.NoError(t, s2.SyncAll(context.Background()))
|
||||
require.NoError(t, s1.SyncAll(t.Context()))
|
||||
require.NoError(t, s2.SyncAll(t.Context()))
|
||||
|
||||
docA := store.getDoc("agent-a", "AGENT.md")
|
||||
docB := store.getDoc("agent-b", "AGENT.md")
|
||||
|
|
@ -195,51 +201,54 @@ func TestSyncAll_IsolatesAgents(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCheckAndSync_DetectsModifiedFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := setupIdentityDir(t, map[string]string{
|
||||
"AGENT.md": "# Original",
|
||||
})
|
||||
|
||||
store := newMockStore()
|
||||
s := New(dir, "agent-1", store)
|
||||
require.NoError(t, s.SyncAll(context.Background()))
|
||||
require.NoError(t, s.SyncAll(t.Context()))
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "AGENT.md"), []byte("# Modified"), 0644))
|
||||
|
||||
require.NoError(t, s.CheckAndSync(context.Background()))
|
||||
require.NoError(t, s.CheckAndSync(t.Context()))
|
||||
|
||||
doc := store.getDoc("agent-1", "AGENT.md")
|
||||
assert.Contains(t, doc.Content, "Modified")
|
||||
}
|
||||
|
||||
func TestCheckAndSync_SkipsUntouchedFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := setupIdentityDir(t, map[string]string{
|
||||
"AGENT.md": "# Stable",
|
||||
})
|
||||
|
||||
store := newMockStore()
|
||||
s := New(dir, "agent-1", store)
|
||||
require.NoError(t, s.SyncAll(context.Background()))
|
||||
require.NoError(t, s.SyncAll(t.Context()))
|
||||
|
||||
hash1 := store.getHash("agent-1", "AGENT.md")
|
||||
|
||||
s.lastSync.Store(time.Now().Add(time.Second).UnixNano())
|
||||
require.NoError(t, s.CheckAndSync(context.Background()))
|
||||
require.NoError(t, s.CheckAndSync(t.Context()))
|
||||
|
||||
hash2 := store.getHash("agent-1", "AGENT.md")
|
||||
assert.Equal(t, hash1, hash2, "untouched file should not trigger re-sync")
|
||||
}
|
||||
|
||||
func TestWatch_DetectsFileChange(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := setupIdentityDir(t, map[string]string{
|
||||
"AGENT.md": "# Initial",
|
||||
})
|
||||
|
||||
store := newMockStore()
|
||||
s := New(dir, "agent-1", store)
|
||||
require.NoError(t, s.SyncAll(context.Background()))
|
||||
require.NoError(t, s.SyncAll(t.Context()))
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
require.NoError(t, s.Watch(ctx))
|
||||
defer s.Close()
|
||||
|
||||
|
|
@ -252,15 +261,16 @@ func TestWatch_DetectsFileChange(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestWatch_IgnoresNonIdentityFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := setupIdentityDir(t, map[string]string{
|
||||
"AGENT.md": "# Agent",
|
||||
})
|
||||
|
||||
store := newMockStore()
|
||||
s := New(dir, "agent-1", store)
|
||||
require.NoError(t, s.SyncAll(context.Background()))
|
||||
require.NoError(t, s.SyncAll(t.Context()))
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
require.NoError(t, s.Watch(ctx))
|
||||
defer s.Close()
|
||||
|
||||
|
|
@ -271,6 +281,7 @@ func TestWatch_IgnoresNonIdentityFiles(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestContentHash_Deterministic(t *testing.T) {
|
||||
t.Parallel()
|
||||
data := []byte("hello world")
|
||||
h1 := contentHash(data)
|
||||
h2 := contentHash(data)
|
||||
|
|
@ -279,12 +290,14 @@ func TestContentHash_Deterministic(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestContentHash_DifferentForDifferentContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
h1 := contentHash([]byte("version 1"))
|
||||
h2 := contentHash([]byte("version 2"))
|
||||
assert.NotEqual(t, h1, h2)
|
||||
}
|
||||
|
||||
func TestIsIdentityFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
want bool
|
||||
|
|
@ -306,6 +319,7 @@ func TestIsIdentityFile(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNew_SetsFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := newMockStore()
|
||||
s := New("/tmp/identity", "test-agent", store)
|
||||
assert.Equal(t, "/tmp/identity", s.identityDir)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
)
|
||||
|
||||
func TestAgenticMapTool_Execute_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
provider := &MockLanguageModel{}
|
||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", bus.NewMessageBus())
|
||||
manager.SetRunLoop(func(_ context.Context, _ ToolLoopConfig, _, userPrompt, _, _ string) (*ToolLoopResult, error) {
|
||||
|
|
@ -20,7 +21,7 @@ func TestAgenticMapTool_Execute_Success(t *testing.T) {
|
|||
})
|
||||
|
||||
tool := NewAgenticMapTool(manager)
|
||||
result := tool.Execute(context.Background(), map[string]interface{}{
|
||||
result := tool.Execute(t.Context(), map[string]interface{}{
|
||||
"items": []interface{}{
|
||||
map[string]interface{}{"name": "a"},
|
||||
map[string]interface{}{"name": "b"},
|
||||
|
|
@ -46,6 +47,7 @@ func TestAgenticMapTool_Execute_Success(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAgenticMapTool_Execute_RetriesFailedItems(t *testing.T) {
|
||||
t.Parallel()
|
||||
provider := &MockLanguageModel{}
|
||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", bus.NewMessageBus())
|
||||
callCount := 0
|
||||
|
|
@ -58,7 +60,7 @@ func TestAgenticMapTool_Execute_RetriesFailedItems(t *testing.T) {
|
|||
})
|
||||
|
||||
tool := NewAgenticMapTool(manager)
|
||||
result := tool.Execute(context.Background(), map[string]interface{}{
|
||||
result := tool.Execute(t.Context(), map[string]interface{}{
|
||||
"items": []interface{}{map[string]interface{}{"name": "retry-me"}},
|
||||
"task_template": "Retry item {{index}} => {{item_json}}",
|
||||
"max_retries": float64(2),
|
||||
|
|
@ -80,11 +82,12 @@ func TestAgenticMapTool_Execute_RetriesFailedItems(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAgenticMapTool_Execute_RequiresPlaceholders(t *testing.T) {
|
||||
t.Parallel()
|
||||
provider := &MockLanguageModel{}
|
||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", bus.NewMessageBus())
|
||||
tool := NewAgenticMapTool(manager)
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]interface{}{
|
||||
result := tool.Execute(t.Context(), map[string]interface{}{
|
||||
"items": []interface{}{map[string]interface{}{"name": "x"}},
|
||||
"task_template": "Handle item without placeholders",
|
||||
})
|
||||
|
|
@ -95,11 +98,12 @@ func TestAgenticMapTool_Execute_RequiresPlaceholders(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAgenticMapTool_Execute_ContextCancelled(t *testing.T) {
|
||||
t.Parallel()
|
||||
provider := &MockLanguageModel{}
|
||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", bus.NewMessageBus())
|
||||
tool := NewAgenticMapTool(manager)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := tool.Execute(ctx, map[string]interface{}{
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
)
|
||||
|
||||
func TestToolCallTool_Name(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
tc := NewToolCallTool(r)
|
||||
if tc.Name() != "tool_call" {
|
||||
|
|
@ -15,6 +16,7 @@ func TestToolCallTool_Name(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_Description(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
tc := NewToolCallTool(r)
|
||||
if tc.Description() == "" {
|
||||
|
|
@ -23,6 +25,7 @@ func TestToolCallTool_Description(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_Parameters(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
tc := NewToolCallTool(r)
|
||||
params := tc.Parameters()
|
||||
|
|
@ -40,9 +43,10 @@ func TestToolCallTool_Parameters(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_MissingToolName(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
tc := NewToolCallTool(r)
|
||||
result := tc.Execute(context.Background(), map[string]interface{}{})
|
||||
result := tc.Execute(t.Context(), map[string]interface{}{})
|
||||
|
||||
if !result.IsError {
|
||||
t.Error("expected error for missing tool_name")
|
||||
|
|
@ -50,11 +54,12 @@ func TestToolCallTool_MissingToolName(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_DispatchesToTool(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
r.Register(&stubTool{name: "read_file", desc: "Read a file"})
|
||||
tc := NewToolCallTool(r)
|
||||
|
||||
result := tc.Execute(context.Background(), map[string]interface{}{
|
||||
result := tc.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "read_file",
|
||||
"arguments": map[string]interface{}{"path": "/tmp/test.txt"},
|
||||
})
|
||||
|
|
@ -68,11 +73,12 @@ func TestToolCallTool_DispatchesToTool(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_PreventRecursion_ToolCall(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
r.RegisterMetaTools()
|
||||
|
||||
tc, _ := r.Get("tool_call")
|
||||
result := tc.Execute(context.Background(), map[string]interface{}{
|
||||
result := tc.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "tool_call",
|
||||
})
|
||||
|
||||
|
|
@ -82,11 +88,12 @@ func TestToolCallTool_PreventRecursion_ToolCall(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_PreventRecursion_ToolSearch(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
r.RegisterMetaTools()
|
||||
|
||||
tc, _ := r.Get("tool_call")
|
||||
result := tc.Execute(context.Background(), map[string]interface{}{
|
||||
result := tc.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "tool_search",
|
||||
})
|
||||
|
||||
|
|
@ -96,11 +103,12 @@ func TestToolCallTool_PreventRecursion_ToolSearch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_JSONStringArguments(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
r.Register(&echoTool{})
|
||||
tc := NewToolCallTool(r)
|
||||
|
||||
result := tc.Execute(context.Background(), map[string]interface{}{
|
||||
result := tc.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "echo",
|
||||
"arguments": `{"msg":"hello"}`,
|
||||
})
|
||||
|
|
@ -114,11 +122,12 @@ func TestToolCallTool_JSONStringArguments(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_NilArguments(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
r.Register(&stubTool{name: "no_args", desc: "Tool that needs no args"})
|
||||
tc := NewToolCallTool(r)
|
||||
|
||||
result := tc.Execute(context.Background(), map[string]interface{}{
|
||||
result := tc.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "no_args",
|
||||
})
|
||||
|
||||
|
|
@ -128,11 +137,12 @@ func TestToolCallTool_NilArguments(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_InvalidJSONArguments(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
r.Register(&stubToolWithSchema{name: "read_file", desc: "Read a file"})
|
||||
tc := NewToolCallTool(r)
|
||||
|
||||
result := tc.Execute(context.Background(), map[string]interface{}{
|
||||
result := tc.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "read_file",
|
||||
"arguments": "not-json",
|
||||
})
|
||||
|
|
@ -147,11 +157,12 @@ func TestToolCallTool_InvalidJSONArguments(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_MissingRequiredArgs_IncludesSchemaHint(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
r.Register(&stubToolWithSchema{name: "read_file", desc: "Read a file"})
|
||||
tc := NewToolCallTool(r)
|
||||
|
||||
result := tc.Execute(context.Background(), map[string]interface{}{
|
||||
result := tc.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "read_file",
|
||||
"arguments": map[string]interface{}{},
|
||||
})
|
||||
|
|
@ -168,10 +179,11 @@ func TestToolCallTool_MissingRequiredArgs_IncludesSchemaHint(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_ToolNotFoundSuggestsSearch(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
tc := NewToolCallTool(r)
|
||||
|
||||
result := tc.Execute(context.Background(), map[string]interface{}{
|
||||
result := tc.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "nonexistent",
|
||||
})
|
||||
|
||||
|
|
@ -184,6 +196,7 @@ func TestToolCallTool_ToolNotFoundSuggestsSearch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_ContextPropagation(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
ct := &contextCaptureTool{}
|
||||
r.Register(ct)
|
||||
|
|
@ -191,7 +204,7 @@ func TestToolCallTool_ContextPropagation(t *testing.T) {
|
|||
tc := NewToolCallTool(r)
|
||||
tc.SetContext("test-channel", "test-chat")
|
||||
|
||||
tc.Execute(context.Background(), map[string]interface{}{
|
||||
tc.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "capture",
|
||||
"arguments": map[string]interface{}{},
|
||||
})
|
||||
|
|
@ -203,6 +216,7 @@ func TestToolCallTool_ContextPropagation(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_ResourceProvider_LoadsResources(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
rt := &resourceAwareTool{
|
||||
resources: map[string]string{
|
||||
|
|
@ -212,7 +226,7 @@ func TestToolCallTool_ResourceProvider_LoadsResources(t *testing.T) {
|
|||
r.Register(rt)
|
||||
tc := NewToolCallTool(r)
|
||||
|
||||
result := tc.Execute(context.Background(), map[string]interface{}{
|
||||
result := tc.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "resource_tool",
|
||||
"arguments": map[string]interface{}{},
|
||||
})
|
||||
|
|
@ -231,12 +245,13 @@ func TestToolCallTool_ResourceProvider_LoadsResources(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_NormalizesCommaSeparatedToolName(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
r.Register(&stubTool{name: "write_file", desc: "write"})
|
||||
r.Register(&stubTool{name: "read_file", desc: "read"})
|
||||
tc := NewToolCallTool(r)
|
||||
|
||||
result := tc.Execute(context.Background(), map[string]interface{}{
|
||||
result := tc.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "write_file, read_file",
|
||||
"arguments": map[string]interface{}{"path": "x.txt"},
|
||||
})
|
||||
|
|
@ -250,11 +265,12 @@ func TestToolCallTool_NormalizesCommaSeparatedToolName(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_NormalizesEmbeddedToolName(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
r.Register(&stubTool{name: "exec", desc: "exec"})
|
||||
tc := NewToolCallTool(r)
|
||||
|
||||
result := tc.Execute(context.Background(), map[string]interface{}{
|
||||
result := tc.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "exec_tool_search_query_exec_run_shell_command_return_output_caution",
|
||||
"arguments": map[string]interface{}{"command": "echo hi"},
|
||||
})
|
||||
|
|
@ -268,11 +284,12 @@ func TestToolCallTool_NormalizesEmbeddedToolName(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToolCallTool_NoResourceProvider_StillWorks(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := NewToolRegistry()
|
||||
r.Register(&stubTool{name: "plain", desc: "No resources"})
|
||||
tc := NewToolCallTool(r)
|
||||
|
||||
result := tc.Execute(context.Background(), map[string]interface{}{
|
||||
result := tc.Execute(t.Context(), map[string]interface{}{
|
||||
"tool_name": "plain",
|
||||
"arguments": map[string]interface{}{},
|
||||
})
|
||||
|
|
@ -283,7 +300,8 @@ func TestToolCallTool_NoResourceProvider_StillWorks(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResourcesFromContext_Empty(t *testing.T) {
|
||||
res := ResourcesFromContext(context.Background())
|
||||
t.Parallel()
|
||||
res := ResourcesFromContext(t.Context())
|
||||
if res != nil {
|
||||
t.Error("expected nil for empty context")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
|
@ -16,7 +15,7 @@ import (
|
|||
func setupDAGTools(t *testing.T) (DAGToolDeps, *delegate.LibSQLDelegate) {
|
||||
d, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, d.Init(context.Background()))
|
||||
require.NoError(t, d.Init(t.Context()))
|
||||
|
||||
// Insert session messages so dag_expand/dag_grep have data
|
||||
agentID, sessionKey := "test-agent", "test-session"
|
||||
|
|
@ -26,7 +25,7 @@ func setupDAGTools(t *testing.T) (DAGToolDeps, *delegate.LibSQLDelegate) {
|
|||
role = "assistant"
|
||||
}
|
||||
content := "Message number " + strconv.Itoa(i)
|
||||
require.NoError(t, d.InsertSessionMessage(context.Background(), agentID, sessionKey, role, content))
|
||||
require.NoError(t, d.InsertSessionMessage(t.Context(), agentID, sessionKey, role, content))
|
||||
}
|
||||
|
||||
// Persist a DAG snapshot
|
||||
|
|
@ -40,7 +39,7 @@ func setupDAGTools(t *testing.T) (DAGToolDeps, *delegate.LibSQLDelegate) {
|
|||
msgs[i] = dag.Message{Role: role, Content: "Message " + string(rune('A'+i%26))}
|
||||
}
|
||||
dagOut := compressor.Compress(msgs)
|
||||
require.NoError(t, d.PersistDAG(context.Background(), agentID, sessionKey, &dag.PersistSnapshot{
|
||||
require.NoError(t, d.PersistDAG(t.Context(), agentID, sessionKey, &dag.PersistSnapshot{
|
||||
FromMsgIdx: 0,
|
||||
ToMsgIdx: 16,
|
||||
MsgCount: 16,
|
||||
|
|
@ -66,11 +65,12 @@ func setupDAGTools(t *testing.T) (DAGToolDeps, *delegate.LibSQLDelegate) {
|
|||
}
|
||||
|
||||
func TestDagDescribeTool(t *testing.T) {
|
||||
t.Parallel()
|
||||
deps, del := setupDAGTools(t)
|
||||
defer del.Close()
|
||||
|
||||
tool := NewDagDescribeTool(deps)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Describe an existing node (chunk-1 from default config with 16 msgs)
|
||||
res := tool.Execute(ctx, map[string]interface{}{"node_id": "chunk-1", "session_key": "test-session"})
|
||||
|
|
@ -80,10 +80,11 @@ func TestDagDescribeTool(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDagDescribeTool_NoSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
d, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
defer d.Close()
|
||||
require.NoError(t, d.Init(context.Background()))
|
||||
require.NoError(t, d.Init(t.Context()))
|
||||
|
||||
tool := NewDagDescribeTool(DAGToolDeps{
|
||||
Queries: d.Queries(),
|
||||
|
|
@ -91,17 +92,18 @@ func TestDagDescribeTool_NoSnapshot(t *testing.T) {
|
|||
AgentID: "x",
|
||||
SessionFn: func() string { return "nonexistent" },
|
||||
})
|
||||
res := tool.Execute(context.Background(), map[string]interface{}{"node_id": "chunk-1"})
|
||||
res := tool.Execute(t.Context(), map[string]interface{}{"node_id": "chunk-1"})
|
||||
assert.True(t, res.IsError)
|
||||
assert.Contains(t, res.ForLLM, "no DAG snapshot")
|
||||
}
|
||||
|
||||
func TestDagGrepTool(t *testing.T) {
|
||||
t.Parallel()
|
||||
deps, del := setupDAGTools(t)
|
||||
defer del.Close()
|
||||
|
||||
tool := NewDagGrepTool(deps)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
res := tool.Execute(ctx, map[string]interface{}{"query": "Message", "session_key": "test-session"})
|
||||
assert.False(t, res.IsError)
|
||||
|
|
@ -110,11 +112,12 @@ func TestDagGrepTool(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDagGrepTool_ScopedByNode(t *testing.T) {
|
||||
t.Parallel()
|
||||
deps, del := setupDAGTools(t)
|
||||
defer del.Close()
|
||||
|
||||
tool := NewDagGrepTool(deps)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
res := tool.Execute(ctx, map[string]interface{}{
|
||||
"query": "number",
|
||||
|
|
@ -126,11 +129,12 @@ func TestDagGrepTool_ScopedByNode(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDagExpandTool(t *testing.T) {
|
||||
t.Parallel()
|
||||
deps, del := setupDAGTools(t)
|
||||
defer del.Close()
|
||||
|
||||
tool := NewDagExpandTool(deps)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Expand chunk-1; needs Lister to return messages
|
||||
res := tool.Execute(ctx, map[string]interface{}{"node_id": "chunk-1", "session_key": "test-session"})
|
||||
|
|
@ -143,16 +147,17 @@ func TestDagExpandTool(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDagExpandTool_NoReader(t *testing.T) {
|
||||
t.Parallel()
|
||||
d, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
defer d.Close()
|
||||
require.NoError(t, d.Init(context.Background()))
|
||||
require.NoError(t, d.Init(t.Context()))
|
||||
|
||||
// Persist minimal DAG
|
||||
compressor := dag.NewCompressor(dag.DefaultCompressorConfig())
|
||||
msgs := []dag.Message{{Role: "user", Content: "x"}}
|
||||
dagOut := compressor.Compress(msgs)
|
||||
require.NoError(t, d.PersistDAG(context.Background(), "a", "s", &dag.PersistSnapshot{
|
||||
require.NoError(t, d.PersistDAG(t.Context(), "a", "s", &dag.PersistSnapshot{
|
||||
FromMsgIdx: 0, ToMsgIdx: 1, MsgCount: 1, DAG: dagOut,
|
||||
}))
|
||||
|
||||
|
|
@ -162,16 +167,17 @@ func TestDagExpandTool_NoReader(t *testing.T) {
|
|||
AgentID: "a",
|
||||
SessionFn: func() string { return "s" },
|
||||
})
|
||||
res := tool.Execute(context.Background(), map[string]interface{}{"node_id": "chunk-1", "session_key": "s"})
|
||||
res := tool.Execute(t.Context(), map[string]interface{}{"node_id": "chunk-1", "session_key": "s"})
|
||||
assert.True(t, res.IsError)
|
||||
assert.Contains(t, res.ForLLM, "dag query store is not configured")
|
||||
}
|
||||
|
||||
func TestDagExpandTool_RecoveryReference(t *testing.T) {
|
||||
t.Parallel()
|
||||
deps, del := setupDAGTools(t)
|
||||
defer del.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
record := DAGRecoveryRecord{
|
||||
NodeID: DAGRecoveryNodePrefix + "test-recovery",
|
||||
SessionKey: "test-session",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue