package main import ( "context" "os" "path/filepath" "strings" "testing" "time" "unicode" "unicode/utf8" "github.com/jackc/pgx/v5" "textmachine/platform/internal/runner" ) // seedALiveRun writes the smallest world the runs listing reads: an account, a book, a live run with // a live attempt, and the hold against it. Written as SQL because the subject is the LISTING, and // going through the service would drag the whole runner in to prove a table. func seedALiveRun(t *testing.T, dsn string) { t.Helper() now := time.Now().UTC() execSQL(t, dsn, `insert into users (id, email) values ('u1','u1@example.org')`) execSQL(t, dsn, `insert into books (id, owner_id, title, source_lang, target_lang, status, chapter_count, workdir, engine_book_id, added_at, revision) values ('bk1','u1','蛊真人','zh','ru','translating',10,'/srv/books/bk1','bk1',$1,1)`, now) execSQL(t, dsn, `insert into runs (id, book_id, status, verify_bank, ceiling_chapters, started_at, revision) values ('run_probe','bk1','translating',false,10,$1,1)`, now) execSQL(t, dsn, `insert into run_attempts (run_id, attempt_no, started_at, last_offset, engine_run_id) values ('run_probe',1,$1,0,'tm-stream-run_probe-1')`, now) } func execSQL(t *testing.T, dsn, sql string, args ...any) { t.Helper() ctx := context.Background() conn, err := pgx.Connect(ctx, dsn) if err != nil { t.Fatal(err) } defer conn.Close(ctx) if _, err := conn.Exec(ctx, sql, args...); err != nil { t.Fatalf("%s: %v", sql, err) } } // exit-marker runs INSIDE a run's transient unit, as ExecStopPost, where there is no database and no // credential to open one with. Reaching for the DSN there would fail every marker in production and // lose the only record of how the run ended — so the dispatch has to answer this command before it // reads a secret. Pinned here because the dispatch order is invisible at the call site. func TestTheExitMarkerCommandNeedsNoDatabase(t *testing.T) { t.Setenv("TM_PLATFORM_DSN", "") t.Setenv("TM_PLATFORM_DSN_FILE", "") t.Setenv("SERVICE_RESULT", "oom-kill") t.Setenv("EXIT_CODE", "killed") t.Setenv("EXIT_STATUS", "TERM") path := filepath.Join(t.TempDir(), "runs", "run_1-1.exit") if err := run([]string{"exit-marker", path, "tm-run-run_1-1"}, os.Stdout); err != nil { t.Fatalf("exit-marker with no DSN: %v", err) } m, err := runner.ReadMarker(path) if err != nil { t.Fatal(err) } if m.Result != "oom-kill" || m.Code != "killed" || m.Status != "TERM" || m.Unit != "tm-run-run_1-1" { t.Errorf("marker: %+v", m) } // Every other command still needs one, or it would silently do nothing. if err := run([]string{"balance", "--user", "u1"}, os.Stdout); err == nil { t.Error("a database command ran without a DSN") } } func TestTheExitMarkerCommandNeedsBothArguments(t *testing.T) { t.Setenv("TM_PLATFORM_DSN", "") for _, args := range [][]string{{"exit-marker"}, {"exit-marker", "only-a-path"}} { if err := run(append([]string{}, args...), os.Stdout); err == nil { t.Errorf("%v was accepted", args) } } } // The development intake is what puts a book in the read model until the contract's upload exists. // It refuses a directory that is not an engine project, because the alternative is a run that dies // at argument parsing inside a transient unit, where the only trace is "exit-code 1". func TestAddingABookRefusesADirectoryTheEngineCannotUse(t *testing.T) { dir := t.TempDir() args := []string{"--user", "u1", "--workdir", dir, "--title", "b", "--source-lang", "zh", "--target-lang", "ru", "--chapters", "10", "--characters", "23000000"} _, err := parseBookIntake(args) if err == nil || !strings.Contains(err.Error(), "engine project directory") { t.Fatalf("a directory without book.yaml gave %v", err) } if err := os.WriteFile(filepath.Join(dir, runner.ConfigFile), []byte("book_id: x\n"), 0o600); err != nil { t.Fatal(err) } got, err := parseBookIntake(args) if err != nil { t.Fatalf("a valid project directory was refused: %v", err) } if got.ChapterCount != 10 || got.CharacterCount != 23_000_000 || got.OwnerID != "u1" { t.Errorf("intake: %+v", got) } if !filepath.IsAbs(got.Workdir) { t.Errorf("the workdir was not made absolute: %q — the unit runs with its own working directory", got.Workdir) } } // Chapters are what bound the run-ceiling scale, so a book registered without them would offer a // scale of zero and no run could ever start on it. func TestAddingABookRequiresTheFactsTheScaleIsBuiltFrom(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, runner.ConfigFile), []byte("book_id: x\n"), 0o600); err != nil { t.Fatal(err) } full := []string{"--user", "u1", "--workdir", dir, "--title", "b", "--source-lang", "zh", "--target-lang", "ru", "--chapters", "10"} drop := func(flag string) []string { out := []string{} for i := 0; i < len(full); i += 2 { if full[i] != flag { out = append(out, full[i], full[i+1]) } } return out } for _, flag := range []string{"--user", "--workdir", "--title", "--source-lang", "--target-lang", "--chapters"} { if _, err := parseBookIntake(drop(flag)); err == nil { t.Errorf("a book without %s was accepted", flag) } } } // `tmplatformctl runs` shows LIVE runs, and `--stalled` narrows that to the ones the reconciler // keeps failing on. Both halves are what the usage line promises, and the first half was missing: // the floor was one failure, so on a healthy deployment the command answered "no run is failing to // reconcile" and never showed the runs that were going perfectly well — the same answer it gives // while a run IS wedged and the deferral has not counted it yet. // // Mutation caught: raising the plain form's floor above zero. func TestTheRunsListingShowsLiveRunsAndNarrowsToStalledOnes(t *testing.T) { dsn := freshDB(t) t.Setenv("TM_PLATFORM_DSN", dsn) seedALiveRun(t, dsn) if got := capture(t, "runs"); !strings.Contains(got, "run_probe") { t.Errorf("the plain listing does not show a live run:\n%s", got) } if got := capture(t, "runs", "--stalled"); strings.Contains(got, "run_probe") { t.Errorf("a run with no failures is reported as stalled:\n%s", got) } // …and once it has failed enough, the narrowed form names it. execSQL(t, dsn, `update run_attempts set reconcile_failures = 99`) if got := capture(t, "runs", "--stalled"); !strings.Contains(got, "run_probe") { t.Errorf("a run past the threshold is missing from --stalled:\n%s", got) } } // The table stays a table, and the figures in it are the ones an operator may act on. // // Both columns it prints from the database are hostile: the engine's stderr arrives verbatim and // multi-byte (the product translates zh/ja), and the TITLE is what the uploader typed — the intake // strips control characters only from titles taken from a filename. A tab forges a column, a newline // forges a row, and a byte-slice through a three-byte rune leaves the tail unreadable. // // The SPENT column is the money half: `spend_micro_usd` holds the engine's BOOK-lifetime figure, so // the attempt's own cost is the difference from its baseline — and with no baseline there is no // honest figure at all, which is what `settle` itself concludes. // // Mutation caught: cutting by byte in oneLine; dropping the control-character map; printing // spend_micro_usd raw; printing zero for an attempt with no baseline. func TestTheOperatorsTableCannotBeForgedByABooksOwnText(t *testing.T) { dsn := freshDB(t) t.Setenv("TM_PLATFORM_DSN", dsn) seedALiveRun(t, dsn) execSQL(t, dsn, `update books set title = 'A' || chr(9) || 'FORGED' || chr(10) || 'ROW'`) execSQL(t, dsn, `update run_attempts set reconcile_failures = 99, reconcile_error = $1, spend_micro_usd = 123456, spend_baseline_micro_usd = 100000`, "engine: "+strings.Repeat("蛊真人", 100)) got := capture(t, "runs", "--stalled") rows := 0 for _, line := range strings.Split(strings.TrimSpace(got), "\n") { if strings.Contains(line, "FORGED") { rows++ } } switch { case rows != 1: t.Errorf("the book's title spans %d lines of the table: a newline in it forges rows\n%s", rows, got) case strings.Contains(got, "A\tFORGED"): t.Errorf("the book's title kept its tab: it forges a column in a table aligned by them\n%s", got) case !utf8.ValidString(got): t.Errorf("the table is not valid UTF-8: the engine's error was cut through a rune\n%q", got) case !strings.Contains(got, "0.023456"): t.Errorf("SPENT is not this attempt's own cost (123456 - 100000):\n%s", got) } // With no baseline the settlement refuses to price the attempt, and so does the table. execSQL(t, dsn, `update run_attempts set spend_baseline_micro_usd = null`) if got := capture(t, "runs", "--stalled"); !strings.Contains(got, "?") { t.Errorf("an attempt the settlement will not price is given a figure anyway:\n%s", got) } // ⚠ AND THE CONTROL CHARACTERS `strings.Fields` DOES NOT TOUCH, asserted on the function because // the table above cannot show them: tabwriter consumes a tab into padding, so the forged column // is invisible in the output it produces. ESC is the one that matters — a title carrying terminal // escapes rewrites the screen of the operator reading the table. for _, r := range oneLine("A\x1b[2JB\x00C\x07D") { if unicode.IsControl(r) { t.Errorf("oneLine passes %q through to an operator's terminal", r) } } }