Files
krow_backend/go-api/internal/runtime/store_test.go
Suriyakumarvijayanayagam 34fa58a6b9
Some checks failed
CI / test (push) Failing after 4m38s
CI / fixture (push) Failing after 7s
Remove the Anthropic path; the gateway speaks one wire protocol
The platform now runs on Groq by default, through the OpenAI-compatible
chat-completions shape. That shape is not one vendor — Gemini, OpenRouter,
Together, vLLM and a local Ollama serve it too — so moving again stays
configuration rather than code.

Two things in the deleted file were not Anthropic's and would have gone
with it silently:

  withRetry / MaxAttempts / retryBackoff were defined in anthropic.go and
  CALLED BY openai.go. Deleting the file wholesale would have removed the
  retry policy of the provider that survived, and nothing in openai.go
  mentions it, so the loss would have been invisible until the next 429.
  The policy is a property of this platform's runs, not of a vendor's API;
  it now lives in retry.go where no provider can carry it off.

  StreamComplete had the same problem and moves to gateway.go, beside the
  Streamer interface whose comment already referenced it.

Three stale-configuration failures are now refused at startup instead of
being ignored. Each was verified firing through the real config.Load():

  MODEL_PROVIDER=anthropic — named separately from every other wrong value
  because it used to be correct. Ignoring it gives a stack that believes it
  is on Claude while every run goes to Groq and is billed there.

  ANTHROPIC_API_KEY set while MODEL_API_KEY is empty. Ignoring a key an
  operator did set is the worst version of this: they fail every run on a
  missing credential they are looking straight at.

  A leftover claude-* model id, naming the tier that carries it. This is
  the check the previous commit's error-detail work was diagnosing: such an
  id is accepted by this process, rejected by the provider, and 400s on
  EVERY run. "A model is wrong" does not say which of three lines to edit.

Defaults ship as a matched pair. defaultBaseURL and the three tier ids are
one decision, not four: an id is only meaningful against the service that
serves it, and a Groq id on an OpenAI base URL is the same failure from the
other side. The tiers also stop being one model — a tier whose cost does
not differ is a distinction that buys nothing.

Verified end to end against a stub of the wire, driving the real wiring
(config.Load in production mode, gateway.New, StreamComplete): streamed
deltas, tool-call decoding, the loopback credential exemption, and usage
totalling 150 rather than 190 — the cached-prefix subtraction still holds.

gofmt clean, go vet clean, 14/14 non-DB packages pass. httpserver still
needs a reachable database.

NOT verified: the I7 planted-injection eval. Removing this path removed the
only model whose refusal behaviour had been measured against it, so the new
default is unproven there until `make eval-live` runs with a real key. The
Groq model ids should also be confirmed against Groq's current lineup.
Flagged in CLAUDE.md §12 and docs/handover.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
2026-09-05 11:52:21 +05:30

165 lines
5.2 KiB
Go

package runtime_test
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/krow/krow-backend/go-api/internal/runtime"
"github.com/krow/krow-backend/go-api/internal/testutil"
)
// trajectory builds a saveable run for the given harness org.
func trajectory(orgID, runID string) *runtime.Trajectory {
started := time.Now().Add(-2 * time.Second).UTC()
return &runtime.Trajectory{
RunID: runID,
OrgID: orgID,
AgentID: "activity-agent",
AgentVersion: 3,
Tier: "balanced",
Model: "llama-3.3-70b-versatile",
StartedAt: started,
EndedAt: started.Add(1200 * time.Millisecond),
Termination: runtime.TerminationCompleted,
Entries: []runtime.Entry{
{Seq: 1, At: started, Kind: runtime.EntryMessage, Role: "user", Text: "what happened?"},
{Seq: 2, At: started, Kind: runtime.EntryBudget, Budget: &runtime.Snapshot{StepsLeft: 8, TokensLeft: 120000}},
{Seq: 3, At: started, Kind: runtime.EntryMessage, Role: "assistant", Text: "Twelve events."},
},
Usage: runtime.RunUsage{InputTokens: 900, OutputTokens: 120, TotalTokens: 1020, ModelCalls: 1},
}
}
func TestPostgresSinkSavesAndReadsBack(t *testing.T) {
h := testutil.New(t)
ctx := context.Background()
sink := runtime.NewPostgresSink(h.Pool)
traj := trajectory(h.OrgID, "run_store_basic")
if err := sink.Save(ctx, traj); err != nil {
t.Fatalf("save: %v", err)
}
var (
agentID, tier, model, termination string
version, modelCalls int
total int64
entries []byte
)
err := h.Pool.QueryRow(ctx, `
SELECT agent_id, agent_version, tier, model, termination, total_tokens, model_calls, entries
FROM agent_runs WHERE run_id = $1`, traj.RunID,
).Scan(&agentID, &version, &tier, &model, &termination, &total, &modelCalls, &entries)
if err != nil {
t.Fatalf("read back: %v", err)
}
if agentID != "activity-agent" || version != 3 {
t.Errorf("stored %s v%d, want activity-agent v3", agentID, version)
}
if termination != string(runtime.TerminationCompleted) {
t.Errorf("termination = %q, want Completed", termination)
}
// Both the tier asked for and the model that answered, so a trajectory read
// a year later does not require knowing that week's routing.
if tier != "balanced" || model != "llama-3.3-70b-versatile" {
t.Errorf("tier/model = %q/%q, want balanced/llama-3.3-70b-versatile", tier, model)
}
if total != 1020 || modelCalls != 1 {
t.Errorf("usage = %d tokens over %d calls, want 1020/1", total, modelCalls)
}
var round []runtime.Entry
if err := json.Unmarshal(entries, &round); err != nil {
t.Fatalf("entries did not round-trip: %v", err)
}
if len(round) != 3 || round[0].Role != "user" || round[2].Text != "Twelve events." {
t.Errorf("entries round-tripped as %+v", round)
}
}
func TestPostgresSinkIsIdempotentPerRun(t *testing.T) {
// A run id is generated once and written once. A second save is a retry of
// one that already landed — failing it would turn a harmless duplicate
// into a reported error on a run that succeeded.
h := testutil.New(t)
ctx := context.Background()
sink := runtime.NewPostgresSink(h.Pool)
traj := trajectory(h.OrgID, "run_store_twice")
if err := sink.Save(ctx, traj); err != nil {
t.Fatalf("first save: %v", err)
}
if err := sink.Save(ctx, traj); err != nil {
t.Fatalf("second save should be a no-op, got: %v", err)
}
var n int
if err := h.Pool.QueryRow(ctx,
`SELECT count(*) FROM agent_runs WHERE run_id = $1`, traj.RunID).Scan(&n); err != nil {
t.Fatalf("count: %v", err)
}
if n != 1 {
t.Errorf("%d rows for one run id, want 1", n)
}
}
func TestPostgresSinkRefusesRunsItCannotAttribute(t *testing.T) {
h := testutil.New(t)
ctx := context.Background()
sink := runtime.NewPostgresSink(h.Pool)
cases := map[string]*runtime.Trajectory{
"no run id": func() *runtime.Trajectory {
tr := trajectory(h.OrgID, "")
return tr
}(),
// I5: tenancy is not optional. Caught here so the failure names the
// cause rather than surfacing as a NOT NULL violation.
"no org": func() *runtime.Trajectory {
tr := trajectory("", "run_no_org")
return tr
}(),
// An invented termination must never reach the column that evals and
// dashboards group by.
"invented termination": func() *runtime.Trajectory {
tr := trajectory(h.OrgID, "run_bad_term")
tr.Termination = "Finished"
return tr
}(),
}
for name, tr := range cases {
if err := sink.Save(ctx, tr); err == nil {
t.Errorf("%s: save should have been refused", name)
}
}
}
func TestPostgresSinkStoresAnEmptyTrajectory(t *testing.T) {
// A run that recorded nothing is still a run worth keeping, and a nil
// slice marshals to "null", which the jsonb_typeof CHECK refuses.
h := testutil.New(t)
ctx := context.Background()
sink := runtime.NewPostgresSink(h.Pool)
traj := trajectory(h.OrgID, "run_store_empty")
traj.Entries = nil
traj.Termination = runtime.TerminationBudgetExceeded
if err := sink.Save(ctx, traj); err != nil {
t.Fatalf("save: %v", err)
}
var kind string
if err := h.Pool.QueryRow(ctx,
`SELECT jsonb_typeof(entries) FROM agent_runs WHERE run_id = $1`, traj.RunID).Scan(&kind); err != nil {
t.Fatalf("read back: %v", err)
}
if kind != "array" {
t.Errorf("entries stored as %q, want array", kind)
}
}