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()) } }