Bound ai_runs saves and redact every stored column
- runOnce no longer saves with an unbounded context.WithoutCancel. Work that ended in time must write its row before the skill deadline; if the writer is not free by then the run fails with llm.ErrTimeout and its row records the timeout. A failed run's row is written within runSaveTimeout (5s); if that write fails too, the run returns both errors and no row is stored. A stored row's status now always matches what the caller received. The row ID is fixed per run, so a second save attempt can never add a second row. - skill, provider and model are redacted with the same redactor as the bodies; TestRunRowsHoldNoSecrets seeds the API key and a webhook URL into the model value. - guard_test: "row saved late" now expects a provider_error timeout row; new "writer blocked" case proves Run returns ErrTimeout within the bound and stores no row. - Purge doc and ARCHITECTURE: the daily purge is owned by Feature 20's scheduler, which must call harness.Purge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -300,8 +300,9 @@ func TestRunTimeout(t *testing.T) {
|
||||
cfg := testConfig(t, &f)
|
||||
f.QueueGenerate(reply(`{"echo":"taco"}`))
|
||||
timeout := 100 * time.Millisecond
|
||||
// Holding the store's only writer connection makes the save wait
|
||||
// past the deadline, after the work itself has ended in time.
|
||||
// Holding the store's only writer connection past the deadline
|
||||
// keeps the ok row from being written in time, after the work
|
||||
// itself has ended in time.
|
||||
conn, err := cfg.Store.Writer.Conn(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -316,11 +317,39 @@ func TestRunTimeout(t *testing.T) {
|
||||
t.Fatalf("out = %+v, want no output", out)
|
||||
}
|
||||
assertCalls(t, &f, 1)
|
||||
// An ok row with no error proves the work ended in time and only
|
||||
// the save ran past the deadline.
|
||||
// The row records the timeout the caller received, not the ok
|
||||
// outcome the work had.
|
||||
rows := runRows(t, cfg.Store)
|
||||
if len(rows) != 1 || rows[0].Status != string(StatusOK) || rows[0].Error.Valid {
|
||||
t.Fatalf("rows = %+v, want one ok row with no error", rows)
|
||||
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("writer blocked", func(t *testing.T) {
|
||||
var f llm.Fake
|
||||
cfg := testConfig(t, &f)
|
||||
f.QueueGenerate(reply(`{"echo":"taco"}`))
|
||||
timeout := 50 * time.Millisecond
|
||||
saveTimeout := runSaveTimeout
|
||||
runSaveTimeout = 100 * time.Millisecond
|
||||
defer func() { runSaveTimeout = saveTimeout }()
|
||||
// The writer stays held until Run has returned, so neither save
|
||||
// can be written.
|
||||
conn, err := cfg.Store.Writer.Conn(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
started := time.Now()
|
||||
_, err = Run(context.Background(), cfg, timeoutSkill{timeout: timeout}, "week-1", echoInput{Word: "taco"})
|
||||
elapsed := time.Since(started)
|
||||
conn.Close()
|
||||
if !errors.Is(err, llm.ErrTimeout) || !strings.Contains(err.Error(), "record ai_runs row") {
|
||||
t.Fatalf("err = %v, want ErrTimeout and the row write failure", err)
|
||||
}
|
||||
if bound := timeout + runSaveTimeout + time.Second; elapsed > bound {
|
||||
t.Fatalf("Run took %v with the writer blocked, want at most %v", elapsed, bound)
|
||||
}
|
||||
if rows := runRows(t, cfg.Store); len(rows) != 0 {
|
||||
t.Fatalf("rows = %+v, want none", rows)
|
||||
}
|
||||
})
|
||||
t.Run("waiting for a slot", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user