Files
docker-agent/pkg/evaluation/assertions_test.go
T
melmennaoui 54e78f0868 feat: wire assertion runner into runSingleEval execution pipeline
Add a code-based assertion evaluator and integrate it into the eval
pipeline so assertions declared in the eval JSON are executed after the
agent run, alongside the existing size/tool-call/relevance checks.

New files:
- assertions.go: runAssertions() evaluates 8 assertion types
  (contains, not_contains, equals, starts_with, ends_with, regex,
  cost_threshold, tool_called) against the agent response, cost,
  and tool-call list.
- assertions_test.go: comprehensive tests for all assertion types.

Modified files:
- types.go: AssertionResults/AssertionsPassed/AssertionsTotal fields
  on Result; checkResults() reports assertion successes and failures.
- eval.go: runSingleEval() runs assertions after relevance checks.
- save.go: populateEvalResult() populates AssertionsCheck on the
  session's EvalResult.
- progress.go: tracks and displays assertion failure counts in the
  live progress bar.
- eval_test.go: checkResults tests for assertion pass/fail/zero.

Closes #4086
2026-09-01 16:48:52 +02:00

204 lines
4.9 KiB
Go

package evaluation
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/docker/docker-agent/pkg/session"
)
func TestRunAssertions(t *testing.T) {
t.Parallel()
tests := []struct {
name string
assertions []session.Assertion
response string
cost float64
toolCalls []string
wantPass []bool
}{
{
name: "contains pass",
assertions: []session.Assertion{
{Name: "has hello", Type: "contains", Value: "hello"},
},
response: "hello world",
wantPass: []bool{true},
},
{
name: "contains fail",
assertions: []session.Assertion{
{Name: "has goodbye", Type: "contains", Value: "goodbye"},
},
response: "hello world",
wantPass: []bool{false},
},
{
name: "not_contains pass",
assertions: []session.Assertion{
{Name: "no error", Type: "not_contains", Value: "error"},
},
response: "all good",
wantPass: []bool{true},
},
{
name: "not_contains fail",
assertions: []session.Assertion{
{Name: "no error", Type: "not_contains", Value: "error"},
},
response: "there was an error",
wantPass: []bool{false},
},
{
name: "equals pass with whitespace trimming",
assertions: []session.Assertion{
{Name: "exact", Type: "equals", Value: "hello"},
},
response: " hello ",
wantPass: []bool{true},
},
{
name: "equals fail",
assertions: []session.Assertion{
{Name: "exact", Type: "equals", Value: "hello"},
},
response: "hello world",
wantPass: []bool{false},
},
{
name: "starts_with pass",
assertions: []session.Assertion{
{Name: "prefix", Type: "starts_with", Value: "hello"},
},
response: "hello world",
wantPass: []bool{true},
},
{
name: "starts_with fail",
assertions: []session.Assertion{
{Name: "prefix", Type: "starts_with", Value: "world"},
},
response: "hello world",
wantPass: []bool{false},
},
{
name: "ends_with pass",
assertions: []session.Assertion{
{Name: "suffix", Type: "ends_with", Value: "world"},
},
response: "hello world",
wantPass: []bool{true},
},
{
name: "ends_with fail",
assertions: []session.Assertion{
{Name: "suffix", Type: "ends_with", Value: "hello"},
},
response: "hello world",
wantPass: []bool{false},
},
{
name: "regex pass",
assertions: []session.Assertion{
{Name: "pattern", Type: "regex", Value: `\d+ files`},
},
response: "found 42 files",
wantPass: []bool{true},
},
{
name: "regex fail",
assertions: []session.Assertion{
{Name: "pattern", Type: "regex", Value: `\d+ files`},
},
response: "no files found",
wantPass: []bool{false},
},
{
name: "regex invalid pattern",
assertions: []session.Assertion{
{Name: "bad regex", Type: "regex", Value: `[invalid`},
},
response: "anything",
wantPass: []bool{false},
},
{
name: "cost_threshold pass",
assertions: []session.Assertion{
{Name: "cheap", Type: "cost_threshold", Value: "0.10"},
},
cost: 0.05,
wantPass: []bool{true},
},
{
name: "cost_threshold fail",
assertions: []session.Assertion{
{Name: "cheap", Type: "cost_threshold", Value: "0.01"},
},
cost: 0.05,
wantPass: []bool{false},
},
{
name: "cost_threshold invalid value",
assertions: []session.Assertion{
{Name: "bad cost", Type: "cost_threshold", Value: "not-a-number"},
},
cost: 0.05,
wantPass: []bool{false},
},
{
name: "tool_called pass",
assertions: []session.Assertion{
{Name: "used search", Type: "tool_called", Value: "search"},
},
toolCalls: []string{"read_file", "search", "write_file"},
wantPass: []bool{true},
},
{
name: "tool_called fail",
assertions: []session.Assertion{
{Name: "used search", Type: "tool_called", Value: "search"},
},
toolCalls: []string{"read_file", "write_file"},
wantPass: []bool{false},
},
{
name: "unknown type fails",
assertions: []session.Assertion{
{Name: "unknown", Type: "magic", Value: "xyz"},
},
wantPass: []bool{false},
},
{
name: "multiple assertions mixed results",
assertions: []session.Assertion{
{Name: "has hello", Type: "contains", Value: "hello"},
{Name: "no error", Type: "not_contains", Value: "error"},
{Name: "used search", Type: "tool_called", Value: "search"},
},
response: "hello world",
toolCalls: []string{"read_file"},
wantPass: []bool{true, true, false},
},
{
name: "empty assertions",
assertions: nil,
wantPass: []bool{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
results := runAssertions(tt.assertions, tt.response, tt.cost, tt.toolCalls)
assert.Len(t, results, len(tt.wantPass))
for i, want := range tt.wantPass {
assert.Equal(t, want, results[i].Passed,
"assertion %d (%s): expected pass=%v, got pass=%v, reason=%s",
i, results[i].Name, want, results[i].Passed, results[i].Reason)
}
})
}
}