Files
MealQuest/internal/ai/harness/guard_test.go
T
dev_agentandClaude Opus 5.5 d03068ace5
CI / ci (push) Successful in 1m34s
CI / ci (pull_request) Successful in 1m37s
Add concurrency guards to harness.Run: single-flight, semaphore and timeouts
harness.Run now takes a scope key. Concurrent runs of one skill with one
scope share a single in-flight run and its result, errors included, so
only one Generate call is made; different scopes run independently. A
process-wide Guard (NewGuard, DefaultMaxCalls = 2) bounds concurrent LLM
calls with a context-aware semaphore. Each skill declares Timeout()
(DefaultTimeout = 60 s), enforced via context over the whole run; a run
past it fails with an error matching llm.ErrTimeout. A caller whose
context ends gets the context error; the shared run is cancelled, and
its slot freed, once no caller waits for it.

llm.Fake results can now block (Release) and signal entry (Started), so
tests hold calls in flight.

Maestro task: 8f533721-32c9-4f73-af9f-9a9f0e94abf8

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:00:20 -04:00

306 lines
9.3 KiB
Go

package harness
import (
"context"
"errors"
"strings"
"testing"
"time"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ai/llm"
)
// timeoutSkill is echoSkill with its own timeout.
type timeoutSkill struct {
echoSkill
timeout time.Duration
}
func (s timeoutSkill) Timeout() time.Duration { return s.timeout }
// blocked is one scripted Generate call that blocks until released.
type blocked struct {
started chan struct{}
release chan struct{}
}
// queueBlocked queues r on f as a call that blocks until released.
func queueBlocked(f *llm.Fake, r llm.GenerateResult) blocked {
b := blocked{started: make(chan struct{}), release: make(chan struct{})}
r.Started, r.Release = b.started, b.release
f.QueueGenerate(r)
return b
}
// result is what one Run returned.
type result struct {
out echoOutput
err error
}
// goRun starts Run of s with scope on cfg and returns where its result
// arrives.
func goRun(ctx context.Context, cfg Config, s Skill[echoInput, echoOutput], scope string) <-chan result {
ch := make(chan result, 1)
go func() {
out, err := Run(ctx, cfg, s, scope, echoInput{Word: "taco"})
ch <- result{out, err}
}()
return ch
}
// await returns what arrives on ch, failing the test after a second.
func await[T any](t *testing.T, ch <-chan T, what string) T {
t.Helper()
select {
case v := <-ch:
return v
case <-time.After(time.Second):
t.Fatalf("timed out waiting for %s", what)
panic("unreachable")
}
}
// waitStarted waits for b's Generate call to start.
func waitStarted(t *testing.T, b blocked, what string) {
t.Helper()
select {
case <-b.started:
case <-time.After(time.Second):
t.Fatalf("timed out waiting for %s to call Generate", what)
}
}
// assertNotStarted checks that b's Generate call has not started after a
// grace period.
func assertNotStarted(t *testing.T, b blocked, what string) {
t.Helper()
select {
case <-b.started:
t.Fatalf("%s called Generate while no slot was free", what)
case <-time.After(50 * time.Millisecond):
}
}
// waitWaiters waits until n callers wait for the run of skill with scope.
func waitWaiters(t *testing.T, g *Guard, skill, scope string, n int) {
t.Helper()
deadline := time.Now().Add(time.Second)
for {
g.mu.Lock()
got := 0
if f := g.flights[flightKey{skill: skill, scope: scope}]; f != nil {
got = f.waiters
}
g.mu.Unlock()
if got == n {
return
}
if time.Now().After(deadline) {
t.Fatalf("%d callers wait for %s/%s, want %d", got, skill, scope, n)
}
time.Sleep(time.Millisecond)
}
}
func TestRunSameScopeSharesOneExecution(t *testing.T) {
for name, script := range map[string]llm.GenerateResult{
"output": reply(`{"echo":"taco"}`),
"error": {Err: llm.ErrAuth},
} {
t.Run(name, func(t *testing.T) {
var f llm.Fake
cfg := testConfig(t, &f)
b := queueBlocked(&f, script)
first := goRun(context.Background(), cfg, echoSkill{}, "week-1")
waitStarted(t, b, "the first run")
second := goRun(context.Background(), cfg, echoSkill{}, "week-1")
waitWaiters(t, cfg.Guard, "echo", "week-1", 2)
close(b.release)
r1, r2 := await(t, first, "the first run"), await(t, second, "the second run")
if r1 != r2 {
t.Fatalf("results differ: %+v and %+v", r1, r2)
}
if name == "output" && (r1.err != nil || r1.out.Echo != "taco") {
t.Fatalf("result = %+v, want taco", r1)
}
if name == "error" && !errors.Is(r1.err, llm.ErrAuth) {
t.Fatalf("err = %v, want ErrAuth", r1.err)
}
assertCalls(t, &f, 1)
if rows := runRows(t, cfg.Store); len(rows) != 1 {
t.Fatalf("%d ai_runs rows, want 1 for the shared run", len(rows))
}
})
}
}
func TestRunDifferentScopesRunIndependently(t *testing.T) {
var f llm.Fake
cfg := testConfig(t, &f)
b1 := queueBlocked(&f, reply(`{"echo":"taco"}`))
b2 := queueBlocked(&f, reply(`{"echo":"taco"}`))
week1 := goRun(context.Background(), cfg, echoSkill{}, "week-1")
week2 := goRun(context.Background(), cfg, echoSkill{}, "week-2")
// Both calls are in Generate at once: neither waits for the other.
waitStarted(t, b1, "a run")
waitStarted(t, b2, "the other run")
close(b1.release)
close(b2.release)
for _, ch := range []<-chan result{week1, week2} {
if r := await(t, ch, "a run"); r.err != nil || r.out.Echo != "taco" {
t.Fatalf("result = %+v", r)
}
}
assertCalls(t, &f, 2)
}
func TestRunThirdCallWaitsForSlot(t *testing.T) {
var f llm.Fake
cfg := testConfig(t, &f)
b1 := queueBlocked(&f, reply(`{"echo":"taco"}`))
b2 := queueBlocked(&f, reply(`{"echo":"taco"}`))
b3 := queueBlocked(&f, reply(`{"echo":"taco"}`))
first := goRun(context.Background(), cfg, echoSkill{}, "week-1")
waitStarted(t, b1, "the first run")
second := goRun(context.Background(), cfg, echoSkill{}, "week-2")
waitStarted(t, b2, "the second run")
third := goRun(context.Background(), cfg, echoSkill{}, "week-3")
assertNotStarted(t, b3, "the third run")
if n := len(f.Requests()); n != 2 {
t.Fatalf("Generate called %d times while both slots were taken, want 2", n)
}
close(b1.release)
waitStarted(t, b3, "the third run")
close(b2.release)
close(b3.release)
for _, ch := range []<-chan result{first, second, third} {
if r := await(t, ch, "a run"); r.err != nil || r.out.Echo != "taco" {
t.Fatalf("result = %+v", r)
}
}
assertCalls(t, &f, 3)
}
func TestRunCancelledContextReleasesSlot(t *testing.T) {
var f llm.Fake
cfg := testConfig(t, &f)
b1 := queueBlocked(&f, reply(`{"echo":"taco"}`))
b2 := queueBlocked(&f, reply(`{"echo":"taco"}`))
b3 := queueBlocked(&f, reply(`{"echo":"taco"}`))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
first := goRun(ctx, cfg, echoSkill{}, "week-1")
waitStarted(t, b1, "the first run")
second := goRun(context.Background(), cfg, echoSkill{}, "week-2")
waitStarted(t, b2, "the second run")
third := goRun(context.Background(), cfg, echoSkill{}, "week-3")
assertNotStarted(t, b3, "the third run")
cancel()
if r := await(t, first, "the cancelled run"); !errors.Is(r.err, context.Canceled) {
t.Fatalf("cancelled run err = %v, want context.Canceled", r.err)
}
// The cancelled run's slot goes to the third run; the first call's
// release is never closed.
waitStarted(t, b3, "the third run")
close(b2.release)
close(b3.release)
for _, ch := range []<-chan result{second, third} {
if r := await(t, ch, "a run"); r.err != nil || r.out.Echo != "taco" {
t.Fatalf("result = %+v", r)
}
}
assertCalls(t, &f, 3)
var statuses []string
for _, r := range runRows(t, cfg.Store) {
statuses = append(statuses, r.Status)
}
if strings.Join(statuses, ",") != "provider_error,ok,ok" {
t.Fatalf("statuses = %v, want the cancelled run recorded as provider_error", statuses)
}
}
func TestRunCallerGivingUpLeavesSharedRun(t *testing.T) {
var f llm.Fake
cfg := testConfig(t, &f)
b := queueBlocked(&f, reply(`{"echo":"taco"}`))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
leader := goRun(ctx, cfg, echoSkill{}, "week-1")
waitStarted(t, b, "the first run")
follower := goRun(context.Background(), cfg, echoSkill{}, "week-1")
waitWaiters(t, cfg.Guard, "echo", "week-1", 2)
cancel()
if r := await(t, leader, "the cancelled caller"); !errors.Is(r.err, context.Canceled) {
t.Fatalf("cancelled caller err = %v, want context.Canceled", r.err)
}
close(b.release)
if r := await(t, follower, "the remaining caller"); r.err != nil || r.out.Echo != "taco" {
t.Fatalf("remaining caller = %+v, want the shared run's output", r)
}
assertCalls(t, &f, 1)
}
func TestRunTimeout(t *testing.T) {
t.Run("in Generate", func(t *testing.T) {
var f llm.Fake
cfg := testConfig(t, &f)
queueBlocked(&f, reply(`{"echo":"taco"}`))
_, err := Run(context.Background(), cfg, timeoutSkill{timeout: 20 * time.Millisecond}, "week-1", echoInput{Word: "taco"})
if !errors.Is(err, llm.ErrTimeout) {
t.Fatalf("err = %v, want ErrTimeout", err)
}
assertCalls(t, &f, 1)
rows := runRows(t, cfg.Store)
if len(rows) != 1 || rows[0].Status != string(StatusProviderError) || !strings.Contains(rows[0].Error.String, llm.ErrTimeout.Error()) {
t.Fatalf("rows = %+v, want one provider_error row naming the timeout", rows)
}
})
t.Run("waiting for a slot", func(t *testing.T) {
var f llm.Fake
cfg := testConfig(t, &f)
cfg.Guard = testGuard(t, 1)
b := queueBlocked(&f, reply(`{"echo":"taco"}`))
holder := goRun(context.Background(), cfg, echoSkill{}, "week-1")
waitStarted(t, b, "the slot holder")
_, err := Run(context.Background(), cfg, timeoutSkill{timeout: 20 * time.Millisecond}, "week-2", echoInput{Word: "taco"})
if !errors.Is(err, llm.ErrTimeout) {
t.Fatalf("err = %v, want ErrTimeout", err)
}
close(b.release)
if r := await(t, holder, "the slot holder"); r.err != nil {
t.Fatal(r.err)
}
assertCalls(t, &f, 1)
})
}
func TestRunRefusesBadGuardInput(t *testing.T) {
if _, err := NewGuard(0); err == nil {
t.Fatal("NewGuard(0) succeeded")
}
var f llm.Fake
cfg := testConfig(t, &f)
if _, err := Run(context.Background(), cfg, echoSkill{}, "", echoInput{Word: "taco"}); err == nil || !strings.Contains(err.Error(), "no scope") {
t.Fatalf("empty scope err = %v", err)
}
if _, err := Run(context.Background(), cfg, timeoutSkill{}, "week-1", echoInput{Word: "taco"}); err == nil || !strings.Contains(err.Error(), "not positive") {
t.Fatalf("zero timeout err = %v", err)
}
assertCalls(t, &f, 0)
if rows := runRows(t, cfg.Store); len(rows) != 0 {
t.Fatalf("refused runs wrote %d ai_runs rows, want none", len(rows))
}
}