package eval import ( "strings" "testing" ) func caseOf(t *testing.T, yamlCase string) *Case { t.Helper() s, err := Parse("t.yaml", []byte("cases:\n - prompt: q\n"+indent(yamlCase))) if err != nil { t.Fatal(err) } return &s.Cases[0] } func indent(s string) string { var b strings.Builder for _, l := range strings.Split(strings.TrimSpace(s), "\n") { b.WriteString(" " + l + "\n") } return b.String() } func TestEachRuleScoresTheAnswersItShouldAndRefusesTheRest(t *testing.T) { for _, tc := range []struct { rule string answer string want float64 }{ {`exact: "42"`, "42", 1}, {`exact: "42"`, " 42\n", 1}, {`exact: "42"`, "42.", 0}, {"exact: Paris\nignore_case: true", "PARIS", 1}, {`contains: Paris`, "The capital is Paris.", 1}, {`contains: [Paris, France]`, "Paris is in France", 1}, {`contains: [Paris, France]`, "Paris", 0}, {"contains: paris\nignore_case: true", "PARIS", 1}, {`regex: '^\s*\d{4}-\d{2}-\d{2}\s*$'`, "2026-10-04", 1}, {`regex: '^\d+$'`, "about 12", 0}, {"regex: '^yes'\nignore_case: true", "Yes, it is", 1}, {`number: 42`, "17 + 25 = 42", 1}, {`number: 42`, "The answer is 42.", 1}, {`number: 42`, "It is 41", 0}, {`number: 1234567`, "1,234,567", 1}, {"number: 3.14\ntolerance: 0.01", "pi is about 3.1416", 1}, {"number: 3.14\ntolerance: 0.001", "pi is about 3.1416", 0}, {`number: -5`, "3 - 8 = -5", 1}, // A date's last part is a number, not a negative one. {`number: 4`, "from 2026-10-04", 1}, {`number: 1`, "no number here", 0}, } { c := caseOf(t, tc.rule) v := scoreRule(c, cleanAnswer(tc.answer)) if v.score != tc.want { t.Errorf("%s against %q scored %v (%s), wanted %v", strings.ReplaceAll(tc.rule, "\n", "; "), tc.answer, v.score, v.detail, tc.want) } } } func TestAnAnswerIsReadAsJSONAndHeldToItsSchema(t *testing.T) { c := caseOf(t, ` json_schema: type: object required: [name, age, tags] additionalProperties: false properties: name: {type: string, minLength: 1} age: {type: integer, minimum: 0, maximum: 150} tags: {type: array, items: {type: string}, maxItems: 3} kind: {enum: [cat, dog]}`) for _, tc := range []struct { answer string pass bool why string }{ {`{"name": "Ada", "age": 36, "tags": ["math"]}`, true, ""}, {"Here it is:\n```json\n{\"name\": \"Ada\", \"age\": 36, \"tags\": []}\n```", true, ""}, {`Sure! {"name": "Ada", "age": 36, "tags": []} Hope that helps.`, true, ""}, {`{"name": "Ada", "age": 36.5, "tags": []}`, false, "age is a number, not integer"}, {`{"name": "Ada", "tags": []}`, false, "has no age"}, {`{"name": "Ada", "age": 36, "tags": [], "extra": 1}`, false, "extra is not allowed"}, {`{"name": "Ada", "age": 200, "tags": []}`, false, "above 150"}, {`{"name": "Ada", "age": 3, "tags": [1]}`, false, "tags[0] is an integer, not string"}, {`{"name": "Ada", "age": 3, "tags": [], "kind": "fish"}`, false, "not one of"}, {`{"name": "", "age": 3, "tags": []}`, false, "fewer than 1"}, {`not json at all`, false, "not JSON"}, } { v := scoreRule(c, tc.answer) if (v.score == 1) != tc.pass || (tc.why != "" && !strings.Contains(v.detail, tc.why)) { t.Errorf("%q: %v %q, wanted pass=%v saying %q", tc.answer, v.score, v.detail, tc.pass, tc.why) } } } func TestASchemaUsingWhatIsNotCheckedIsRefusedWhenTheSuiteIsRead(t *testing.T) { _, err := Parse("t.yaml", []byte("cases:\n - prompt: q\n json_schema: {oneOf: [{type: string}]}\n")) if err == nil || !strings.Contains(err.Error(), "oneOf is not supported") { t.Fatalf("a schema with oneOf was accepted: %v", err) } } func TestAJudgesReplyIsReadAsAScore(t *testing.T) { for _, tc := range []struct { reply string want float64 bad bool }{ {`{"score": 0.8, "reason": "mostly right"}`, 0.8, false}, {"hmm\n```json\n{\"score\": 1, \"reason\": \"ok\"}\n```", 1, false}, {`{"score": "0.25"}`, 0.25, false}, {`{"pass": false, "reason": "wrong city"}`, 0, false}, {`{"score": 7}`, 1, false}, {`0.6`, 0.6, false}, {`PASS`, 1, false}, {`Fail: it named the wrong city.`, 0, false}, {`I cannot say.`, 0, true}, } { v, err := readJudgement(tc.reply) if (err != nil) != tc.bad || v.score != tc.want { t.Errorf("%q: %v %v, wanted %v (unreadable: %v)", tc.reply, v.score, err, tc.want, tc.bad) } } } func TestAReasoningModelsThinkingIsNotPartOfItsAnswer(t *testing.T) { if got := cleanAnswer("\n17+25... carry the one\n\n\n42"); got != "42" { t.Errorf("%q", got) } if got := cleanAnswer("still thinking when it was cut off"); got != "" { t.Errorf("%q", got) } }