Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 0 additions & 146 deletions pkg/tools/repair_test.go

This file was deleted.

89 changes: 89 additions & 0 deletions pkg/tools/tools_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package tools

import (
"bytes"
"context"
"log/slog"
"testing"

"github.com/docker/aijson"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -79,6 +82,92 @@ func TestNewHandler_InvalidArguments(t *testing.T) {
require.Error(t, err)
}

// The next three tests pin the docker-agent-specific behavior of NewHandler.
// Repair semantics themselves are covered by github.com/docker/aijson; what
// is local to this package is (a) that NewHandler actually delegates to
// aijson, and (b) that the OnRepair hook fans out to the tool_input_repaired
// slog event with the expected fields (and stays quiet on the hot path).

// TestNewHandler_DelegatesToAijson is the wiring canary: if someone swaps
// aijson.Unmarshal back to encoding/json.Unmarshal, this test catches it.
func TestNewHandler_DelegatesToAijson(t *testing.T) {
type args struct {
Paths []string `json:"paths"`
}
var got args
handler := NewHandler(func(_ context.Context, a args) (*ToolCallResult, error) {
got = a
return ResultSuccess("ok"), nil
})

_, err := handler(t.Context(), ToolCall{
Type: "function",
Function: FunctionCall{
Name: "read_multiple_files",
Arguments: `{"paths":"only.txt"}`,
},
})
require.NoError(t, err)
assert.Equal(t, []string{"only.txt"}, got.Paths)
}

func TestNewHandler_EmitsRepairTelemetry(t *testing.T) {
var buf bytes.Buffer
prev := slog.Default()
slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})))
Comment thread
trungutt marked this conversation as resolved.
t.Cleanup(func() { slog.SetDefault(prev) })

type args struct {
Paths []string `json:"paths"`
}
handler := NewHandler(func(_ context.Context, _ args) (*ToolCallResult, error) {
return ResultSuccess("ok"), nil
})

_, err := handler(t.Context(), ToolCall{
Type: "function",
Function: FunctionCall{
Name: "read_multiple_files",
Arguments: `{"paths":"only.txt"}`,
},
})
require.NoError(t, err)

out := buf.String()
assert.Contains(t, out, "tool_input_repaired")
assert.Contains(t, out, "tool=read_multiple_files")
// Track the exported aijson constant rather than the underlying string
// so the assertion follows the library if it ever renames the value.
assert.Contains(t, out, string(aijson.KindWrapInArray))
}

// TestNewHandler_NoTelemetryOnValidInput pins the hot-path contract: a
// well-formed tool call must NOT emit tool_input_repaired. Without this,
// a regression in aijson's strict-first ordering would silently flood logs.
func TestNewHandler_NoTelemetryOnValidInput(t *testing.T) {
var buf bytes.Buffer
prev := slog.Default()
slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})))
t.Cleanup(func() { slog.SetDefault(prev) })

type args struct {
Paths []string `json:"paths"`
}
handler := NewHandler(func(_ context.Context, _ args) (*ToolCallResult, error) {
return ResultSuccess("ok"), nil
})

_, err := handler(t.Context(), ToolCall{
Type: "function",
Function: FunctionCall{
Name: "read_multiple_files",
Arguments: `{"paths":["a.txt","b.txt"]}`,
},
})
require.NoError(t, err)
assert.NotContains(t, buf.String(), "tool_input_repaired")
}

func TestToolCallResultWithoutPayload(t *testing.T) {
result := &ToolCallResult{
Output: "large output",
Expand Down
Loading