mirror of
https://github.com/lukaszraczylo/llm-testbench.git
synced 2026-10-12 00:45:19 +00:00
470 lines
16 KiB
Go
470 lines
16 KiB
Go
package tests
|
|
|
|
import (
|
|
"github.com/lukaszraczylo/llm-testbench/internal/eval"
|
|
"github.com/lukaszraczylo/llm-testbench/internal/testkit"
|
|
)
|
|
|
|
// registerHardTests registers every programming/hard test. This subcategory
|
|
// stresses harder daily-programming accuracy - concurrency/async reasoning,
|
|
// subtle language traps, algorithm output tracing, and API-contract edge
|
|
// cases - each forced to a short, machine-checkable answer.
|
|
func registerHardTests(r *testkit.Registry) {
|
|
r.Register(hardFloatBinaddTest())
|
|
r.Register(hardNullishOverOrTest())
|
|
r.Register(hardGoroutineRaceTest())
|
|
r.Register(hardDeferNamedReturnTest())
|
|
r.Register(hardIntDivisionTest())
|
|
r.Register(hardRegexGreedyTest())
|
|
r.Register(hardPromiseAllSettledTest())
|
|
r.Register(hardContextTimeoutTest())
|
|
r.Register(hardJSONUnmarshalNullTest())
|
|
r.Register(hardVarShadowTest())
|
|
r.Register(hardPyVersionSortTest())
|
|
r.Register(hardGoParenBalanceTest())
|
|
}
|
|
|
|
// hardFloatBinaddTest: judge whether 0.1 + 0.2 === 0.3 holds in
|
|
// JavaScript / IEEE-754 doubles.
|
|
//
|
|
// ground truth: neither 0.1 nor 0.2 has an exact binary representation,
|
|
// so 0.1 + 0.2 computes to the nearest double to the mathematical sum,
|
|
// which is 0.30000000000000004, not the double for the literal 0.3.
|
|
// Therefore the strict equality comparison evaluates to false. This is a
|
|
// canonical, deterministic fact of IEEE-754 double arithmetic, independent
|
|
// of any runtime; verified with node during authoring.
|
|
func hardFloatBinaddTest() testkit.Test {
|
|
prompt := `In JavaScript, evaluate this comparison:
|
|
|
|
0.1 + 0.2 === 0.3
|
|
|
|
Does the strict-equality comparison evaluate to true or to false?
|
|
Respond with only a JSON object:
|
|
{"equal": true}
|
|
or
|
|
{"equal": false}`
|
|
|
|
return testkit.Test{
|
|
ID: "hard-float-binadd",
|
|
Category: "programming",
|
|
Subcategory: "hard",
|
|
Description: "Judge IEEE-754 double equality for 0.1 + 0.2 === 0.3 in JavaScript (false).",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONField("equal", false),
|
|
}
|
|
}
|
|
|
|
// hardNullishOverOrTest: trace the value of a nullish-coalescing
|
|
// expression over a falsy (but non-null) operand.
|
|
//
|
|
// ground truth: ?? returns the right-hand operand only when the left-hand
|
|
// operand is null or undefined. 0 is neither null nor undefined, so
|
|
// n ?? 42 evaluates to 0 - unlike ||, which would have returned 42 for
|
|
// the falsy 0. This is the core distinction between nullish coalescing
|
|
// and logical OR. Verified with node during authoring.
|
|
func hardNullishOverOrTest() testkit.Test {
|
|
prompt := `Consider this TypeScript code:
|
|
|
|
const n = 0;
|
|
const v = n ?? 42;
|
|
|
|
What is the value of v?
|
|
Respond with only a JSON object:
|
|
{"value": ...}
|
|
|
|
Give the numeric value only.`
|
|
|
|
return testkit.Test{
|
|
ID: "hard-nullish-over-or",
|
|
Category: "programming",
|
|
Subcategory: "hard",
|
|
Description: "Trace nullish-coalescing over a falsy-but-non-null operand: 0 ?? 42 is 0, not 42.",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONField("value", 0),
|
|
}
|
|
}
|
|
|
|
// hardGoroutineRaceTest: judge whether an unsynchronized shared counter
|
|
// is guaranteed to reach N*M exactly.
|
|
//
|
|
// ground truth: incrementing a shared int from multiple goroutines with no
|
|
// synchronization is a data race (Go's race detector would flag it). A
|
|
// read-modify-write of a shared variable is not atomic; two goroutines can
|
|
// read the same value before either writes back, so increments are lost.
|
|
// The final value is some value strictly between M (at best serialized,
|
|
// one goroutine's work) and N*M, but it is NOT guaranteed to equal
|
|
// N*M. This claim follows from the Go memory model / data-race semantics
|
|
// and cannot be recomputed by running (the outcome is nondeterministic).
|
|
func hardGoroutineRaceTest() testkit.Test {
|
|
prompt := `A Go program starts 8 goroutines, each of which performs:
|
|
counter++
|
|
1000 times, where counter is a shared int with NO synchronization
|
|
(no mutex, no atomic) between the goroutines. The program then waits for
|
|
all 8 goroutines and prints counter.
|
|
|
|
Is counter's final value guaranteed to be exactly 8000?
|
|
Respond with only a JSON object:
|
|
{"guaranteed": true}
|
|
or
|
|
{"guaranteed": false}`
|
|
|
|
return testkit.Test{
|
|
ID: "hard-goroutine-race",
|
|
Category: "programming",
|
|
Subcategory: "hard",
|
|
Description: "Judge whether an unsynchronized shared counter is guaranteed to reach N*M (false: data race).",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONField("guaranteed", false),
|
|
}
|
|
}
|
|
|
|
// hardDeferNamedReturnTest: trace the return value of a function whose
|
|
// deferred closure writes its named return during a normal (non-panic)
|
|
// return.
|
|
//
|
|
// ground truth: return 7 first assigns the named result r = 7; then,
|
|
// still before the function returns to its caller, the deferred closure
|
|
// runs and overwrites r = 99. The caller observes the named return after
|
|
// all defers have run, so f() returns 99 - not 7 and not 5. This is the
|
|
// classic named-return-plus-defer trap. Verified with go run during
|
|
// authoring.
|
|
func hardDeferNamedReturnTest() testkit.Test {
|
|
prompt := `Consider this Go code:
|
|
|
|
func f() (r int) {
|
|
defer func() { r = 99 }()
|
|
r = 5
|
|
return 7
|
|
}
|
|
|
|
What value does f() return?
|
|
Respond with only the number, no explanation.`
|
|
|
|
return testkit.Test{
|
|
ID: "hard-defer-named-return",
|
|
Category: "programming",
|
|
Subcategory: "hard",
|
|
Description: "Trace a deferred closure overwriting a named return value during a normal return (99).",
|
|
Prompt: prompt,
|
|
Eval: eval.Numeric(eval.ExtractLastNumber[int], hardDeferNamedReturnWant, 0),
|
|
}
|
|
}
|
|
|
|
// hardDeferNamedReturnWant is f()'s return value.
|
|
//
|
|
// ground truth: the deferred closure runs during the function's epilogue,
|
|
// after the explicit `return 7` has set the named result r = 7, and
|
|
// overwrites it with 99; the caller therefore receives 99 (see the full
|
|
// derivation on hardDeferNamedReturnTest). Recomputed by running the
|
|
// identical snippet with go run during authoring.
|
|
const hardDeferNamedReturnWant = 99
|
|
|
|
// hardIntDivisionTest: judge the result of integer division being
|
|
// assigned to a floating-point variable.
|
|
//
|
|
// ground truth: a / b with two int operands performs integer division,
|
|
// which truncates toward zero: 7 / 2 = 3 (not 3.5). That integral
|
|
// result is then implicitly converted to double, so d = 3.0. The common
|
|
// error is assuming floating division because the target is a double.
|
|
// Verified with go run during authoring.
|
|
func hardIntDivisionTest() testkit.Test {
|
|
prompt := `Consider this C code:
|
|
|
|
int a = 7;
|
|
int b = 2;
|
|
double d = a / b;
|
|
|
|
What is the value of d?
|
|
Respond with only the number, no explanation.`
|
|
|
|
return testkit.Test{
|
|
ID: "hard-int-division",
|
|
Category: "programming",
|
|
Subcategory: "hard",
|
|
Description: "Trace integer-division-then-widen: 7 / 2 is 3 (stored as 3.0), not 3.5.",
|
|
Prompt: prompt,
|
|
Eval: eval.Numeric(eval.ExtractLastNumber[float64], hardIntDivisionWant, 0),
|
|
}
|
|
}
|
|
|
|
// hardIntDivisionWant is d's value.
|
|
//
|
|
// ground truth: integers divide as integers (truncating), giving 7 / 2 =
|
|
// 3, converted to double 3.0; the float value is 3, so Numeric want is
|
|
// 3.0. Recomputed by running the identical arithmetic during authoring.
|
|
const hardIntDivisionWant = 3.0
|
|
|
|
// hardRegexGreedyTest: trace which substrings two greedy capture groups
|
|
// of an anchored pattern match.
|
|
//
|
|
// ground truth: quantifiers are greedy by default - each takes as much as
|
|
// it can while still allowing the rest of the pattern to match. In
|
|
// /^(\d+)(\d+)$/, the first \d+ grabs the longest prefix that leaves at
|
|
// least one digit for the second \d+ (which needs at least one match
|
|
// because + is one-or-more). That longest prefix of "12345" is "1234",
|
|
// leaving "5" for group 2. A naive symmetric split like ["12","345"]
|
|
// fails because group 1 would then still be able to grow. Verified with
|
|
// node during authoring.
|
|
func hardRegexGreedyTest() testkit.Test {
|
|
prompt := `Consider this regular expression applied to the string "12345":
|
|
|
|
^(\d+)(\d+)$
|
|
|
|
(^ and $ anchor to the whole string, and \d+ means one or more digits.)
|
|
|
|
What are the two captured groups? Respond with only a JSON array of the
|
|
two captured strings, in group order, for example ["a","b"]`
|
|
|
|
return testkit.Test{
|
|
ID: "hard-regex-greedy",
|
|
Category: "programming",
|
|
Subcategory: "hard",
|
|
Description: "Trace greedy capture-group backtracking: ^(one-or-more digits)(one-or-more digits)$ on 12345 yields 1234 and 5.",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONStringArrayEquals([]string{"1234", "5"}),
|
|
}
|
|
}
|
|
|
|
// hardPromiseAllSettledTest: trace the per-result status values of
|
|
// Promise.allSettled over a mixed resolve/reject input.
|
|
//
|
|
// ground truth: Promise.allSettled always resolves, and each element of
|
|
// its result array is an object whose status is the literal "fulfilled"
|
|
// for a resolved input and "rejected" for a rejected input, preserving
|
|
// input order. Promise.resolve(42) yields {status:"fulfilled", value:42};
|
|
// Promise.reject(new Error("boom")) yields {status:"rejected",
|
|
// reason:...}. So the two status values, in order, are "fulfilled" then
|
|
// "rejected". Verified with node during authoring.
|
|
func hardPromiseAllSettledTest() testkit.Test {
|
|
prompt := `Consider this TypeScript code:
|
|
|
|
const results = await Promise.allSettled([
|
|
Promise.resolve(42),
|
|
Promise.reject(new Error("boom")),
|
|
]);
|
|
|
|
Each element of results has a "status" field.
|
|
|
|
List the two status values, in array order. Respond with only a JSON
|
|
array of the two status strings, for example ["a","b"]`
|
|
|
|
return testkit.Test{
|
|
ID: "hard-promise-allsettled",
|
|
Category: "programming",
|
|
Subcategory: "hard",
|
|
Description: "Trace Promise.allSettled result statuses over a fulfilled+rejected input (fulfilled, rejected).",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONStringArrayEquals([]string{"fulfilled", "rejected"}),
|
|
}
|
|
}
|
|
|
|
// hardContextTimeoutTest: identify the exact error string a timed-out
|
|
// context's Err() returns.
|
|
//
|
|
// ground truth: when a context created by context.WithTimeout fires, its
|
|
// Done channel closes and Err() returns the sentinel
|
|
// context.DeadlineExceeded, whose Error() string (per the context
|
|
// package) is exactly "context deadline exceeded" - which is distinct
|
|
// from "context canceled", the string of context.Canceled returned when a
|
|
// cancel function is called. Verified with go run during authoring.
|
|
func hardContextTimeoutTest() testkit.Test {
|
|
prompt := `In Go, a context created with context.WithTimeout fires after its
|
|
deadline. At that point:
|
|
|
|
ctx.Err()
|
|
|
|
returns a non-nil error. What is the exact string of that error?
|
|
Respond with only a JSON object:
|
|
{"err": "..."}
|
|
|
|
with the exact string value.`
|
|
|
|
return testkit.Test{
|
|
ID: "hard-context-timeout",
|
|
Category: "programming",
|
|
Subcategory: "hard",
|
|
Description: "Identify ctx.Err() after a context.WithTimeout deadline fires (context deadline exceeded).",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONField("err", "context deadline exceeded"),
|
|
}
|
|
}
|
|
|
|
// hardJSONUnmarshalNullTest: judge the behavior of json.Unmarshal on a
|
|
// literal JSON null into an already-populated struct.
|
|
//
|
|
// ground truth: per encoding/json, a JSON null into a non-nil pointer
|
|
// target is a no-op: Unmarshal returns nil (no error) and leaves the
|
|
// target's existing value unchanged. It does not zero the struct and does
|
|
// not error. Verified with go run during authoring (a struct holding
|
|
// Items: [1] stays [1] with nil error).
|
|
func hardJSONUnmarshalNullTest() testkit.Test {
|
|
prompt := `In Go:
|
|
|
|
var s Struct // s is a populated struct value (some fields set)
|
|
err := json.Unmarshal([]byte("null"), &s)
|
|
|
|
Does Unmarshal return an error, and is s left unchanged (no fields
|
|
cleared)?
|
|
Respond with only a JSON object:
|
|
{"returns_error": true}
|
|
or
|
|
{"returns_error": false}`
|
|
|
|
return testkit.Test{
|
|
ID: "hard-json-unmarshal-null",
|
|
Category: "programming",
|
|
Subcategory: "hard",
|
|
Description: "Judge json.Unmarshal of a literal null into a populated struct (no error, unchanged).",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONField("returns_error", false),
|
|
}
|
|
}
|
|
|
|
// hardVarShadowTest: judge whether an inner := declaration shadows an
|
|
// outer variable without changing it.
|
|
//
|
|
// ground truth: `count := 5` inside the if block declares a NEW, inner
|
|
// variable that shadows the outer count for the duration of that block
|
|
// (Go's := always creates a new variable when at least one variable on
|
|
// the left is new in the current scope). Incrementing the inner count
|
|
// leaves the outer count untouched, so after the block outer count is
|
|
// still 0. Verified with go run during authoring.
|
|
func hardVarShadowTest() testkit.Test {
|
|
prompt := `Consider this Go code:
|
|
|
|
count := 0
|
|
if true {
|
|
count := 5
|
|
count++
|
|
}
|
|
// count here?
|
|
|
|
Does the outer count remain unchanged (equal to 0) after the if block?
|
|
Respond with only a JSON object:
|
|
{"outer_unchanged": true}
|
|
or
|
|
{"outer_unchanged": false}`
|
|
|
|
return testkit.Test{
|
|
ID: "hard-var-shadow",
|
|
Category: "programming",
|
|
Subcategory: "hard",
|
|
Description: "Trace Go variable shadowing: an inner := leaves the outer variable unchanged (true).",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONField("outer_unchanged", true),
|
|
}
|
|
}
|
|
|
|
// hardPyVersionVersions is the inline version list for
|
|
// hardPyVersionSortTest.
|
|
const hardPyVersionVersions = `versions = [
|
|
"1.9.0",
|
|
"1.10.0",
|
|
"1.2.0",
|
|
"2.0.0",
|
|
"0.9.9",
|
|
]`
|
|
|
|
// hardPyVersionSortWant is the expected stdout of a correct script: the
|
|
// second-highest version under numeric component-by-component ordering.
|
|
//
|
|
// ground truth: ordering versions by numeric components puts 0.9.9 <
|
|
// 1.2.0 < 1.9.0 < 1.10.0 < 2.0.0 (1.10.0 is newer than 1.9.0 because
|
|
// its second component 10 > 9), so the second-highest is 1.10.0. A
|
|
// plain lexicographic string sort (the trap) would instead report 1.9.0,
|
|
// which is exactly the failure the test discriminates. Recomputed by
|
|
// running the correct script with python3 during authoring.
|
|
const hardPyVersionSortWant = "1.10.0"
|
|
|
|
// hardPyVersionSortTest: write a python3 script that prints the
|
|
// second-highest version under numeric component ordering.
|
|
//
|
|
// ground truth: see hardPyVersionSortWant; correctness is determined by
|
|
// running the script (version ordering is behavior, not a text match).
|
|
func hardPyVersionSortTest() testkit.Test {
|
|
prompt := `Here is an inline list of software version strings:
|
|
|
|
` + "```python" + `
|
|
` + hardPyVersionVersions + `
|
|
` + "```" + `
|
|
|
|
Write a python3 program that sorts the versions numerically, component by
|
|
component (natural version ordering: "1.10.0" is newer than "1.9.0"
|
|
because the second component 10 > 9), and prints only the second-highest
|
|
version.`
|
|
|
|
return testkit.Test{
|
|
ID: "hard-py-version-sort",
|
|
Category: "programming",
|
|
Subcategory: "hard",
|
|
Description: "Write a python3 script printing the second-highest version by numeric component ordering.",
|
|
System: terseCodeOnly,
|
|
Prompt: prompt,
|
|
Eval: eval.PyRun(eval.PassthroughHarness, hardPyVersionSortWant),
|
|
}
|
|
}
|
|
|
|
// hardGoParenBalanceHarness is a complete, independent driver file (its
|
|
// own package clause and imports) that eval.GoRun writes to harness.go
|
|
// alongside the model's IsBalanced in solution.go. It drives IsBalanced
|
|
// across balanced, nested, mismatched, unclosed, and wrongly-ordered
|
|
// cases, printing PASS only if every case matches its expected result.
|
|
const hardGoParenBalanceHarness = `package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
func main() {
|
|
cases := []struct {
|
|
in string
|
|
want bool
|
|
}{
|
|
{"", true},
|
|
{"()", true},
|
|
{"()[]{}", true},
|
|
{"([{}])", true},
|
|
{"(", false},
|
|
{"(]", false},
|
|
{"([)]", false},
|
|
{"((", false},
|
|
{")(", false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := IsBalanced(c.in); got != c.want {
|
|
fmt.Printf("FAIL %q: got %v want %v\n", c.in, got, c.want)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
fmt.Println("PASS")
|
|
}`
|
|
|
|
// hardGoParenBalanceTest: implement a stack-based balanced-parentheses
|
|
// checker.
|
|
//
|
|
// ground truth: the harness above is the oracle - it exercises empty,
|
|
// balanced, nested, mismatched-closer, trailing-opener, and wrong-order
|
|
// inputs, so a correct stack-based implementation prints PASS. There is no
|
|
// separate hardcoded constant beyond the PASS sentinel.
|
|
func hardGoParenBalanceTest() testkit.Test {
|
|
prompt := `Implement a Go function with exactly this signature:
|
|
|
|
func IsBalanced(s string) bool
|
|
|
|
It must report whether s's parentheses are balanced and correctly nested:
|
|
'(' '[' '{' are openers, ')' ']' '}' are the matching closers. An empty
|
|
string is balanced (return true). Return false for any unclosed opener,
|
|
any closer with no matching open, or any wrong-nesting (e.g. "([)]").`
|
|
|
|
return testkit.Test{
|
|
ID: "hard-go-paren-balance",
|
|
Category: "programming",
|
|
Subcategory: "hard",
|
|
Description: "Implement a stack-based balanced-parentheses checker for (), [], and {}.",
|
|
System: terseCodeOnly,
|
|
Prompt: prompt,
|
|
Eval: eval.GoRun(hardGoParenBalanceHarness, "PASS"),
|
|
}
|
|
}
|