package main // P8-REVIEW axis 3 probe. Reproduces the SCHEDULE half of the sweep composition: `sweep`'s tick runs // its five passes one after another on ONE goroutine, so the RUNS pass — the only thing that settles // money, restarts a run a reboot lost and re-issues a stop the systemd call dropped — runs once per // (sum of every pass's duration), not once per TM_PLATFORM_SWEEP_EVERY. // // Scaled, and only in the direction that makes it observable: the readmodel pass here is slowed by a // fake engine that takes 400 ms per book instead of the real one's minutes, and the tick is 100 ms // instead of 15 s. No constant of the zone is changed. The real numbers substitute directly: // refreshSweepBudget 10m + intakeSweepBudget 21m + two 2m passes on top of the runs pass's own 2m. // // The runs pass is counted by the one side effect it has on a stopped run: `reconcile` re-issues // `Runner.Stop` on every pass while the unit is alive and an intent stands (reconcile.go:387-397). import ( "context" "crypto/rand" "encoding/hex" "log/slog" "net/url" "os" "sync" "sync/atomic" "testing" "time" "github.com/jackc/pgx/v5" "textmachine/platform/internal/ingest" "textmachine/platform/internal/metrics" "textmachine/platform/internal/money" "textmachine/platform/internal/pgstore" "textmachine/platform/internal/readmodel" "textmachine/platform/internal/runner" "textmachine/platform/internal/runs" ) type countingRunner struct{ stops atomic.Int64 } func (c *countingRunner) Start(context.Context, runner.Spec) error { return nil } func (c *countingRunner) Stop(context.Context, string) error { c.stops.Add(1); return nil } func (c *countingRunner) Alive(context.Context, string) (bool, error) { return true, nil } // slowEngine is a readmodel engine that answers, slowly, and then says no. Slow enough to dominate a // pass and fast enough for a test — the real one is bounded by readmodel.MaterializeBudget (5 min). type slowEngine struct{ d time.Duration } func (s slowEngine) Manifest(ctx context.Context, binary, workdir string) (ingest.Manifest, error) { select { case <-time.After(s.d): case <-ctx.Done(): } return ingest.Manifest{}, os.ErrDeadlineExceeded } func (s slowEngine) Export(ctx context.Context, binary, workdir string) (ingest.Export, error) { return ingest.Export{}, os.ErrDeadlineExceeded } func probeStore(t *testing.T) (*pgstore.Store, context.Context) { t.Helper() admin := os.Getenv("TM_PLATFORM_TEST_DSN") if admin == "" { t.Skip("TM_PLATFORM_TEST_DSN not set") } ctx := t.Context() var suffix [6]byte if _, err := rand.Read(suffix[:]); err != nil { t.Fatal(err) } name := "tm_p8rev_sweep_" + hex.EncodeToString(suffix[:]) conn, err := pgx.Connect(ctx, admin) if err != nil { t.Fatal(err) } if _, err := conn.Exec(ctx, "create database "+pgx.Identifier{name}.Sanitize()); err != nil { conn.Close(ctx) t.Skipf("cannot create a scratch database: %v", err) } t.Cleanup(func() { c, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() _, _ = conn.Exec(c, "drop database if exists "+pgx.Identifier{name}.Sanitize()+" with (force)") conn.Close(c) }) u, _ := url.Parse(admin) u.Path = "/" + name if err := pgstore.Migrate(ctx, u.String()); err != nil { t.Fatal(err) } s, err := pgstore.Open(ctx, u.String()) if err != nil { t.Fatal(err) } t.Cleanup(s.Close) return s, ctx } // oneStoppedRun leaves the database with exactly one live run whose stop is on file and whose unit // still exists: the shape whose only cure is the sweep re-issuing the signal. func oneStoppedRun(t *testing.T, db *pgstore.Store, ctx context.Context, now time.Time) { t.Helper() if _, err := db.Pool().Exec(ctx, `insert into users (id, email) values ('u1','u1@example.org')`); err != nil { t.Fatal(err) } amount, _ := money.ParseUSD("50") if _, err := db.Grant(ctx, "u1", amount, "test", "seed", "", now); err != nil { t.Fatal(err) } bookID, err := db.AddBook(ctx, pgstore.NewBook{OwnerID: "u1", Title: "b", SourceLang: "zh", TargetLang: "ru", ChapterCount: 100, Workdir: t.TempDir(), Now: now}) if err != nil { t.Fatal(err) } started, err := db.StartRun(ctx, pgstore.StartRunInput{UserID: "u1", BookID: bookID, CeilingChapters: 10, Ceiling: money.MicroUSD(1_000_000), Now: now}, 0, func(context.Context, pgstore.Tx, string) error { return nil }) if err != nil { t.Fatal(err) } ok, err := db.RecordSpawn(ctx, pgstore.SpawnRecord{AttemptID: started.AttemptID, Unit: "tm-run-x-1", Binary: "/opt/tm/tmctl", EngineRunID: pgstore.EngineStreamID(started.ID, 1), Ceiling: money.MicroUSD(1_000_000), CeilingArg: money.MicroUSD(1_000_000), Baseline: 0}) if err != nil || !ok { t.Fatalf("record spawn: %v %v", ok, err) } if _, _, err := db.RequestStop(ctx, "u1", started.ID, now); err != nil { t.Fatal(err) } } // booksOwingAReadingSurface leaves n quiet books whose surface is due, which is what the readmodel // pass works through. func booksOwingAReadingSurface(t *testing.T, db *pgstore.Store, ctx context.Context, now time.Time, n int) { t.Helper() for range n { id, err := db.AddBook(ctx, pgstore.NewBook{OwnerID: "u1", Title: "owed", SourceLang: "zh", TargetLang: "ru", ChapterCount: 10, Workdir: t.TempDir(), Now: now}) if err != nil { t.Fatal(err) } if _, err := db.Pool().Exec(ctx, `update books set read_model_owed_at = now() where id = $1`, id); err != nil { t.Fatal(err) } } } func TestProbeTheRunsPassRunsOncePerSumOfEveryPass(t *testing.T) { const window = 4 * time.Second const tick = 100 * time.Millisecond run := func(t *testing.T, withReadModel bool) int64 { db, ctx := probeStore(t) now := time.Now().UTC().Truncate(time.Millisecond) oneStoppedRun(t, db, ctx, now) rn := &countingRunner{} svc := &runs.Service{Store: db, Runner: rn, Now: func() time.Time { return now }} s := sweeps{runs: svc, db: db, metrics: metrics.New()} if withReadModel { booksOwingAReadingSurface(t, db, ctx, now, 8) s.reader = &readmodel.Service{Store: db, Engine: slowEngine{400 * time.Millisecond}, Binary: "/opt/tm/tmctl"} } c, cancel := context.WithCancel(ctx) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done(); sweep(c, s, tick, 2*time.Minute, slog.New(slog.DiscardHandler)) }() time.Sleep(window) cancel() wg.Wait() return rn.stops.Load() } var alone, behind int64 t.Run("runs pass alone", func(t *testing.T) { alone = run(t, false) }) t.Run("runs pass behind a busy readmodel pass", func(t *testing.T) { behind = run(t, true) }) t.Logf("in %v at a %v tick: runs passes alone = %d, behind the readmodel pass = %d", window, tick, alone, behind) if behind >= alone { t.Logf("NOT REPRODUCED: the runs pass kept its cadence") } }