textmachine/platform/cmd/tmplatformctl/runs_test.go

98 lines
3.7 KiB
Go

package main
import (
"os"
"path/filepath"
"strings"
"testing"
"textmachine/platform/internal/runner"
)
// 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)
}
}
}