textmachine/backend/internal/pipeline/runner_readonly_test.go

78 lines
2.6 KiB
Go

package pipeline
import (
"context"
"testing"
"textmachine/backend/internal/obs"
)
// runner_readonly_test.go pins the operator-visibility contract (пакет №4): the
// read-only runner (tmctl status/report) must work WHILE a write runner owns the
// project — «оператор слеп ровно когда висит 300-я глава» was the top smoke
// finding, caused by status/report sharing the writer's exclusive flock.
// TestReadOnlyRunnerWorksDuringLiveRun: a NewReadOnlyRunner must open and project
// Status while a NewRunner (write path, flock held) is alive on the same book.
func TestReadOnlyRunnerWorksDuringLiveRun(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := setupProject(t, srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
// The write runner owns the project (flock held for its whole lifetime) and
// has completed a run, so there are rows to project.
w := newRunner(t, bookPath)
defer w.Close()
if _, err := w.TranslateBook(ctx); err != nil {
t.Fatal(err)
}
// Simulates `tmctl status` from a second shell mid-run: the write runner is
// still open. The old store.Open path failed here with "project database is
// in use by another tmctl process".
ro, err := NewReadOnlyRunner(bookPath, obs.NewLogger())
if err != nil {
t.Fatalf("read-only runner must open during a live run: %v", err)
}
defer ro.Close()
rep, err := ro.Status(ctx)
if err != nil {
t.Fatal(err)
}
if rep.TotalChunks != 1 || rep.Done != 1 || rep.Flagged != 0 {
t.Fatalf("status projection over a live-locked book = %+v", rep)
}
if rep.CommittedUSD == 0 {
t.Fatal("status must see the writer's committed spend")
}
}
// TestReadOnlyRunnerFirstTouchCreates: before the first run there is no DB file —
// the read-only runner falls back to the creating Open (мигрирует пустую базу), so
// «status до первого прогона» keeps showing 0/N pending instead of erroring.
func TestReadOnlyRunnerFirstTouchCreates(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := setupProject(t, srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
ro, err := NewReadOnlyRunner(bookPath, obs.NewLogger())
if err != nil {
t.Fatal(err)
}
defer ro.Close()
rep, err := ro.Status(ctx)
if err != nil {
t.Fatal(err)
}
if rep.TotalChunks != 1 || rep.Done != 0 || rep.Pending != 1 {
t.Fatalf("pre-run status = %+v", rep)
}
if rec.count() != 0 {
t.Fatalf("read-only status must make zero provider calls, got %d", rec.count())
}
}