154 lines
5.8 KiB
Go
154 lines
5.8 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// TestTheThinkingColumnKeepsSilenceApartFromZero is the durable half of the distinction the adapter
|
|
// makes: a row whose provider reported a thinking count of 0 and a row whose provider reported nothing
|
|
// must not read the same way out of the database. They did for the whole life of the project — the
|
|
// column that was supposed to answer «did thinking eat max_tokens» is NOT NULL DEFAULT 0, so every row
|
|
// said zero and no reader could tell a measured zero from a question nobody asked.
|
|
func TestTheThinkingColumnKeepsSilenceApartFromZero(t *testing.T) {
|
|
s, _ := openTemp(t)
|
|
zero, eightK := 0, 8496
|
|
|
|
for _, row := range []RequestLog{
|
|
{BookID: "b", Chapter: 1, ChunkIdx: 0, Stage: "draft", Role: "translator", CompletionTokens: 8496, ReasoningInCompletion: &eightK, OK: false},
|
|
{BookID: "b", Chapter: 1, ChunkIdx: 1, Stage: "draft", Role: "translator", CompletionTokens: 1617, ReasoningInCompletion: &zero, OK: true},
|
|
{BookID: "b", Chapter: 1, ChunkIdx: 2, Stage: "draft", Role: "translator", CompletionTokens: 1617, OK: true},
|
|
} {
|
|
if err := s.InsertRequestLog(row); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
rows, err := s.RequestLogRows("b")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rows) != 3 {
|
|
t.Fatalf("rows = %d, want 3", len(rows))
|
|
}
|
|
|
|
// say renders the column the way a reader has to read it: a number, or the absence of one. Printing
|
|
// the pointer itself put an ADDRESS in the failure text, which tells whoever broke this nothing
|
|
// about what the column now says.
|
|
say := func(n *int) string {
|
|
if n == nil {
|
|
return "no answer"
|
|
}
|
|
return fmt.Sprintf("%d", *n)
|
|
}
|
|
|
|
spent, measuredZero, unasked := rows[0], rows[1], rows[2]
|
|
if spent.ReasoningInCompletion == nil || *spent.ReasoningInCompletion != 8496 {
|
|
t.Fatalf("a call that spent its whole budget thinking must say so, got %s", say(spent.ReasoningInCompletion))
|
|
}
|
|
if measuredZero.ReasoningInCompletion == nil {
|
|
t.Fatal("a MEASURED zero must survive the round trip as an answer, not as silence")
|
|
}
|
|
if *measuredZero.ReasoningInCompletion != 0 {
|
|
t.Fatalf("a measured zero must stay zero, got %d", *measuredZero.ReasoningInCompletion)
|
|
}
|
|
if unasked.ReasoningInCompletion != nil {
|
|
t.Fatalf("a row whose provider reported nothing must stay unanswered, got %s", say(unasked.ReasoningInCompletion))
|
|
}
|
|
}
|
|
|
|
// TestRowsWrittenBeforeTheThinkingColumnReadAsUnanswered covers the books already on disk, and it does
|
|
// so by actually MIGRATING one: the database is built at the schema an older binary wrote, a row is
|
|
// inserted through that older INSERT, and only then is the column added. A `DEFAULT 0` would have made
|
|
// that row claim its call did not think — a claim nobody measured, about the very runs whose money this
|
|
// column exists to explain.
|
|
//
|
|
// ⚠ THE OLD VINTAGE IS BUILT, NOT SIMULATED. Inserting a nil pointer into a database that already has
|
|
// the column proves only that nil writes NULL — it never runs the ALTER over a populated table, which
|
|
// is the whole subject. Measured: the earlier version of this test passed with the migration removed.
|
|
func TestRowsWrittenBeforeTheThinkingColumnReadAsUnanswered(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "old.db")
|
|
|
|
// One vintage back: every migration except the head, recorded exactly the way applyStep records
|
|
// them, so the real migrator sees a project that is genuinely behind rather than one pretending.
|
|
db, err := sql.Open("sqlite", "file:"+path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for v := 0; v < len(migrations)-1; v++ {
|
|
if _, err := db.Exec(migrations[v]); err != nil {
|
|
t.Fatalf("build vintage %d: %v", v+1, err)
|
|
}
|
|
if _, err := db.Exec(`INSERT INTO schema_version (version) VALUES (?)`, v+1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// The column must NOT exist yet, or the fixture is not one vintage back and the test is about
|
|
// nothing.
|
|
if columnExists(t, db, "request_log", "reasoning_in_completion") {
|
|
t.Fatal("the fixture is meant to be ONE VINTAGE BEHIND: request_log already carries the column the head migration adds")
|
|
}
|
|
// A row written by that older binary: the INSERT it had, without the column.
|
|
if _, err := db.Exec(`INSERT INTO request_log (book_id, chapter, chunk_idx, stage, role, completion_tokens, ok)
|
|
VALUES ('old', 1, 0, 'edit', 'editor', 16000, 0)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// The real migrator, over a populated table.
|
|
s, err := Open(path)
|
|
if err != nil {
|
|
t.Fatalf("migrate the old project: %v", err)
|
|
}
|
|
defer s.Close()
|
|
|
|
rows, err := s.RequestLogRows("old")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rows) != 1 {
|
|
t.Fatalf("rows = %d, want 1 — the row written before the column must survive the migration", len(rows))
|
|
}
|
|
if n := rows[0].ReasoningInCompletion; n != nil {
|
|
t.Fatalf("a row from before the column must read as unanswered, got %d", *n)
|
|
}
|
|
if rows[0].CompletionTokens != 16000 {
|
|
t.Fatalf("the rest of the row must be untouched, got completion=%d", rows[0].CompletionTokens)
|
|
}
|
|
}
|
|
|
|
// columnExists asks the database itself, so the fixture's premise is checked against sqlite rather than
|
|
// against what the test author believed the migration list contained.
|
|
func columnExists(t *testing.T, db *sql.DB, table, column string) bool {
|
|
t.Helper()
|
|
rows, err := db.Query(`SELECT name FROM pragma_table_info(?)`, table)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer rows.Close()
|
|
seen := 0
|
|
for rows.Next() {
|
|
var name string
|
|
if err := rows.Scan(&name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seen++
|
|
if name == column {
|
|
return true
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if seen == 0 {
|
|
t.Fatalf("table %q has no columns at all — the fixture never built it", table)
|
|
}
|
|
return false
|
|
}
|