152 lines
6.9 KiB
Go
152 lines
6.9 KiB
Go
package runner
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"textmachine/platform/internal/ingest"
|
|
)
|
|
|
|
// The three flags that decide what this door IS, pinned at the argv — the one place where losing one
|
|
// is a single-token diff and the consequence is a class of its own.
|
|
//
|
|
// Mutation caught: dropping --out (the engine writes beside its project database and DELETES the
|
|
// formats it was not asked for, so one user's `txt` export destroys the operator's `epub`);
|
|
// dropping --partial (a book with any hole is REFUSED, which is exactly what D39.178 п.1 forbids
|
|
// this door from ever answering); adding --keys-file (the engine refuses it on every command but
|
|
// `translate`, so every export would fail on a $0 command).
|
|
func TestTheBuildArgvIsWhatMakesTheDoorADoor(t *testing.T) {
|
|
args := BuildArgs("/w", "epub", "/artifacts/exp_1.epub")
|
|
if !slices.Contains(args, "--partial") {
|
|
t.Error("no --partial: the door would REFUSE a book with a hole instead of marking it (D39.178 п.1)")
|
|
}
|
|
if i := slices.Index(args, "--out"); i < 0 || i+1 >= len(args) || args[i+1] != "/artifacts/exp_1.epub" {
|
|
t.Errorf("--out does not name the artifact: %v — without it the engine writes beside its "+
|
|
"project database and deletes the formats it was not asked for", args)
|
|
}
|
|
if i := slices.Index(args, "--format"); i < 0 || i+1 >= len(args) || args[i+1] != "epub" {
|
|
t.Errorf("--format does not name one format: %v", args)
|
|
}
|
|
if slices.Contains(args, "--keys-file") {
|
|
t.Error("--keys-file on a $0 command: the engine refuses the flag on everything but translate (D20.4)")
|
|
}
|
|
if i := slices.Index(args, "--config"); i < 0 || i+1 >= len(args) ||
|
|
args[i+1] != filepath.Join("/w", ConfigFile) {
|
|
t.Errorf("--config does not name the book: %v", args)
|
|
}
|
|
if args[0] != "build" {
|
|
t.Errorf("the verb is %q, want build", args[0])
|
|
}
|
|
}
|
|
|
|
// The door's whole promise, against the REAL engine: a book with a hole comes back as a FILE with
|
|
// the notice, not as a refusal (D39.178 п.1, owner 30.08). No unit test can make this claim — what
|
|
// refuses is the engine, so the engine is what must be seen not refusing.
|
|
//
|
|
// The probe book is deliberately UNTRANSLATED, which is the strongest form of the hole: every unit
|
|
// is `pending`. Without --partial this is exit 16 and nothing on disk.
|
|
//
|
|
// Gated like every live-engine test: a bare clone stays green and says why.
|
|
func TestALiveBuildOfAHollowBookWritesTheMarkedCopyInsteadOfRefusing(t *testing.T) {
|
|
bin := os.Getenv("TM_PLATFORM_TEST_ENGINE_BIN")
|
|
tpl := os.Getenv("TM_PLATFORM_TEST_BOOK_TEMPLATE")
|
|
if bin == "" || tpl == "" {
|
|
t.Skip("TM_PLATFORM_TEST_ENGINE_BIN and TM_PLATFORM_TEST_BOOK_TEMPLATE not set: " +
|
|
"the door's «always builds» promise is not checked against a real engine")
|
|
}
|
|
dir := t.TempDir()
|
|
writeProbeBook(t, tpl, dir, "bk_BUILDPROBE")
|
|
out := filepath.Join(t.TempDir(), "exp_probe.txt")
|
|
rn := New(nil)
|
|
res, err := rn.Build(t.Context(), bin, dir, "txt", out)
|
|
if err != nil {
|
|
t.Fatalf("the live build did not run: %v (stderr %q)", err, res.Stderr)
|
|
}
|
|
if !res.Exited || res.ExitCode != ingest.ExitClean {
|
|
t.Fatalf("the live build exited %d (exited=%v, stderr %q); with --partial a hollow book is "+
|
|
"written with the notice, never refused", res.ExitCode, res.Exited, res.Stderr)
|
|
}
|
|
if !res.Decoded {
|
|
t.Fatalf("a clean build carried no report (decode err %v, stderr %q)", res.DecodeErr, res.Stderr)
|
|
}
|
|
if res.Report.Version != ingest.KnownBuildVersion {
|
|
t.Errorf("the live report identifies as %q and this build reads %s — the constant mirrors the "+
|
|
"engine's own and moves when it moves", res.Report.Version, ingest.KnownBuildVersion)
|
|
}
|
|
if res.Report.Complete {
|
|
t.Error("the engine called an untranslated book complete: the probe's own premise is gone")
|
|
}
|
|
if res.Report.PendingUnits == 0 {
|
|
t.Error("the report counts no pending unit in a book nothing has translated")
|
|
}
|
|
// The engine does its neighbour-deleting housekeeping ONLY without --out, so the report of a
|
|
// door's build must never carry it. A non-empty list here means the flag did not survive.
|
|
if len(res.Report.RemovedFiles) > 0 || len(res.Report.StaleCopies) > 0 {
|
|
t.Errorf("a build with --out reported housekeeping beside the project database: removed=%v stale=%v",
|
|
res.Report.RemovedFiles, res.Report.StaleCopies)
|
|
}
|
|
body, err := os.ReadFile(out)
|
|
if err != nil {
|
|
t.Fatalf("the engine reported a build and there is no file at --out: %v", err)
|
|
}
|
|
if len(body) == 0 {
|
|
t.Fatal("the artifact is empty")
|
|
}
|
|
// The artifact went where the DOOR said, not beside the engine's database. Pinned because the
|
|
// whole TTL-and-GC half of this door stands on the platform owning the file's place.
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, e := range entries {
|
|
if strings.Contains(e.Name(), ".book.") {
|
|
t.Errorf("the build left %s in the BOOK's directory: --out did not decide where it wrote", e.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
// The refusal class this door must never see, seen once so the mapping is not a guess: WITHOUT
|
|
// --partial the same hollow book is exit 16 and nothing is written. It is the control for the test
|
|
// above — without it, «--partial worked» is indistinguishable from «this book had no holes».
|
|
func TestWithoutPartialTheSameBookIsRefusedWithTheBuildsOwnNumber(t *testing.T) {
|
|
bin := os.Getenv("TM_PLATFORM_TEST_ENGINE_BIN")
|
|
tpl := os.Getenv("TM_PLATFORM_TEST_BOOK_TEMPLATE")
|
|
if bin == "" || tpl == "" {
|
|
t.Skip("TM_PLATFORM_TEST_ENGINE_BIN and TM_PLATFORM_TEST_BOOK_TEMPLATE not set: " +
|
|
"the build's own refusal class is not checked against a real engine")
|
|
}
|
|
dir := t.TempDir()
|
|
writeProbeBook(t, tpl, dir, "bk_REFUSEPROBE")
|
|
out := filepath.Join(t.TempDir(), "exp_probe.txt")
|
|
// The door's own argv minus the one flag. Written out rather than taken from BuildArgs, because
|
|
// the point is that BuildArgs is the thing that must not lose it.
|
|
args := []string{"build", "--config", filepath.Join(dir, ConfigFile), "--format", "txt", "--out", out}
|
|
res := runVerb(t, bin, dir, args)
|
|
if res != ingest.ExitBookIncomplete {
|
|
t.Fatalf("a hollow book built without --partial exited %d, want %d (exitBookIncomplete): "+
|
|
"the class this door's mapping is written against", res, ingest.ExitBookIncomplete)
|
|
}
|
|
if _, err := os.Stat(out); !os.IsNotExist(err) {
|
|
t.Errorf("the refusal left a file at --out (%v): a refusal writes nothing", err)
|
|
}
|
|
}
|
|
|
|
// runVerb runs one tmctl invocation and returns its exit code. A local helper and not a use of
|
|
// Build, because what it exists to exercise is an argv Build deliberately cannot produce.
|
|
func runVerb(t *testing.T, bin, dir string, args []string) int {
|
|
t.Helper()
|
|
cmd := exec.CommandContext(t.Context(), bin, args...)
|
|
cmd.Dir = dir
|
|
var errOut bytes.Buffer
|
|
cmd.Stderr = &errOut
|
|
err := cmd.Run()
|
|
if cmd.ProcessState == nil || !cmd.ProcessState.Exited() {
|
|
t.Fatalf("the engine did not exit on its own: %v (stderr %q)", err, errOut.String())
|
|
}
|
|
return cmd.ProcessState.ExitCode()
|
|
}
|