Merged in feature/bot-turn-tests (pull request #225)

add tests for bot coverage

* add tests

* fix test name
This commit is contained in:
Jay Brown
2026-05-07 23:28:06 +00:00
parent 72de690894
commit 7b820b18c2
+169
View File
@@ -117,6 +117,21 @@ func seedSessionDirect(t *testing.T, ctx context.Context, dbCfg *serviceconfig.B
return id
}
// seedTerminalTurn inserts a bot_turns row in a terminal status
// (errored, abandoned, session_deleted). The CHECK constraint
// bot_turns_status_fields_consistent requires `error` to be non-null
// for these statuses; this helper supplies a placeholder JSON value so
// callers do not have to repeat the boilerplate.
func seedTerminalTurn(t *testing.T, ctx context.Context, dbCfg *serviceconfig.BaseConfig,
sessionID uuid.UUID, ordinal int, status, prompt string) {
t.Helper()
_, err := dbCfg.GetDBPool().Exec(ctx, `
INSERT INTO bot_turns (session_id, ordinal, prompt, status, attempt_id, error, attempt_started_at, created_at)
VALUES ($1, $2, $3, $4, $5, $6::jsonb, NOW() - INTERVAL '1 hour', NOW() - INTERVAL '1 hour')
`, sessionID, ordinal, prompt, status, uuid.New(), `{"code":"x","message":"y"}`)
require.NoError(t, err)
}
// TestAddTurn_TenConcurrentCreates_OneWinner: ten goroutines POST
// ordinal 1 simultaneously to a fresh session. The partial unique
// index `bot_turns_one_inflight_per_session` and the FOR UPDATE lock
@@ -360,3 +375,157 @@ func TestAddTurn_StaleTx2CallbackZeroRows(t *testing.T) {
assert.Equal(t, currentAttemptID, attemptID,
"row's attempt_id must remain the current value (B), not the stale (A)")
}
// ---------- classifyExistingTurn branch coverage ----------
//
// The next three tests exercise the errored/abandoned arms of
// service_turn.go classifyExistingTurn that the public HTTP handler
// cannot reach because FindTargetOrdinalForPrompt pre-resolves the
// target ordinal. They call svc.AddTurn directly with crafted DB state
// so coverage on these defensive arms does not depend on integration
// flakes elsewhere in the suite.
// TestAddTurn_ExistingErroredOrdinalMismatch_OutOfRange covers the
// errored/abandoned -> input.Ordinal != session.LastTerminalOrdinal arm.
//
// Setup: ord 1 errored ("X"), ord 2 completed -> LastTerminalOrdinal = 2.
// AddTurn(Ordinal=1) lands on the errored row at ord 1, but 1 != 2 so
// classifyExistingTurn returns ErrOrdinalOutOfRange before touching
// the prompt-match check.
func TestAddTurn_ExistingErroredOrdinalMismatch_OutOfRange(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
ctx := t.Context()
dbCfg := &serviceconfig.BaseConfig{}
test.CreateDB(t, dbCfg)
const (
clientID = "BOT_M4_CET_ORD"
actor = "ord-mismatch@example.com"
)
resetForTurnTests(t, ctx, dbCfg, clientID, "bot-m4-cet-ord")
sessionID := seedSessionDirect(t, ctx, dbCfg, clientID, actor)
seedTerminalTurn(t, ctx, dbCfg, sessionID, 1, "errored", "X")
_, err := dbCfg.GetDBPool().Exec(ctx, `
INSERT INTO bot_turns (session_id, ordinal, prompt, status, attempt_id, completion, attempt_started_at, created_at)
VALUES ($1, 2, 'second', 'completed', $2, 'a', NOW(), NOW())
`, sessionID, uuid.New())
require.NoError(t, err)
server, _ := fastAPIServerForTurnTests(t, "unused")
cfg := turnTestCfg(server.URL)
svc, err := bot.New(cfg, dbCfg)
require.NoError(t, err)
_, err = svc.AddTurn(ctx, bot.AddTurnInput{
SessionID: sessionID,
ClientID: clientID,
Actor: actor,
Ordinal: 1,
Prompt: "X",
})
require.Error(t, err)
assert.Truef(t, errors.Is(err, bot.ErrOrdinalOutOfRange),
"expected ErrOrdinalOutOfRange when ord != LastTerminalOrdinal in errored arm; got %v", err)
}
// TestAddTurn_ExistingErroredPromptMismatch covers the
// errored/abandoned -> existing.Prompt != input.Prompt arm.
//
// Setup: ord 1 errored ("X"), no other turns -> LastTerminalOrdinal = 1.
// AddTurn(Ordinal=1, Prompt="Y") clears the ord check (1 == 1) then
// trips the prompt-match check ("X" != "Y").
func TestAddTurn_ExistingErroredPromptMismatch(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
ctx := t.Context()
dbCfg := &serviceconfig.BaseConfig{}
test.CreateDB(t, dbCfg)
const (
clientID = "BOT_M4_CET_PRM"
actor = "prompt-mismatch@example.com"
)
resetForTurnTests(t, ctx, dbCfg, clientID, "bot-m4-cet-prm")
sessionID := seedSessionDirect(t, ctx, dbCfg, clientID, actor)
seedTerminalTurn(t, ctx, dbCfg, sessionID, 1, "errored", "X")
server, _ := fastAPIServerForTurnTests(t, "unused")
cfg := turnTestCfg(server.URL)
svc, err := bot.New(cfg, dbCfg)
require.NoError(t, err)
_, err = svc.AddTurn(ctx, bot.AddTurnInput{
SessionID: sessionID,
ClientID: clientID,
Actor: actor,
Ordinal: 1,
Prompt: "Y",
})
require.Error(t, err)
assert.Truef(t, errors.Is(err, bot.ErrPromptMismatch),
"expected ErrPromptMismatch when existing.Prompt != input.Prompt; got %v", err)
}
// TestAddTurn_RetryRejectedByInflightIndex
// covers the errored/abandoned -> ResetBotTurnForRetry unique-violation
// arm (isUniqueViolation -> ErrPriorTurnInFlight).
//
// Setup: ord 1 errored ("X"), ord 2 in_flight (fresh, so the grace-
// abandon pass leaves it alone). LastTerminalOrdinal = 1 because in_flight
// rows are excluded from the MAX. AddTurn(Ordinal=1, Prompt="X") clears
// the ord and prompt checks, then ResetBotTurnForRetry tries to flip ord
// 1 to in_flight — the partial unique index
// bot_turns_one_inflight_per_session rejects the second in_flight row
// for this session, surfacing SQLSTATE 23505.
func TestAddTurn_RetryRejectedByInflightIndex(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
ctx := t.Context()
dbCfg := &serviceconfig.BaseConfig{}
test.CreateDB(t, dbCfg)
const (
clientID = "BOT_M4_CET_PIF"
actor = "prior-inflight@example.com"
)
resetForTurnTests(t, ctx, dbCfg, clientID, "bot-m4-cet-pif")
sessionID := seedSessionDirect(t, ctx, dbCfg, clientID, actor)
seedTerminalTurn(t, ctx, dbCfg, sessionID, 1, "errored", "X")
_, err := dbCfg.GetDBPool().Exec(ctx, `
INSERT INTO bot_turns (session_id, ordinal, prompt, status, attempt_id, attempt_started_at, created_at)
VALUES ($1, 2, 'second', 'in_flight', $2, NOW(), NOW())
`, sessionID, uuid.New())
require.NoError(t, err)
server, _ := fastAPIServerForTurnTests(t, "unused")
cfg := turnTestCfg(server.URL)
svc, err := bot.New(cfg, dbCfg)
require.NoError(t, err)
_, err = svc.AddTurn(ctx, bot.AddTurnInput{
SessionID: sessionID,
ClientID: clientID,
Actor: actor,
Ordinal: 1,
Prompt: "X",
})
require.Error(t, err)
assert.Truef(t, errors.Is(err, bot.ErrPriorTurnInFlight),
"expected ErrPriorTurnInFlight when reset trips partial unique index; got %v", err)
// Sanity: the errored row at ord 1 must not have been flipped.
var ord1Status string
err = dbCfg.GetDBPool().QueryRow(ctx, `
SELECT status FROM bot_turns WHERE session_id = $1 AND ordinal = 1
`, sessionID).Scan(&ord1Status)
require.NoError(t, err)
assert.Equal(t, "errored", ord1Status,
"reset must roll back when the unique index rejects it; ord 1 stays errored")
}