package runner import ( "os" "path/filepath" "slices" "testing" "gopkg.in/yaml.v3" "textmachine/platform/internal/ingest" ) // `--dry-run` is the whole of what separates a preview from a write, so its presence is pinned at // the argv — the one place a dropped flag is a one-token diff. // // Mutation caught: passing preview through without the flag (every call becomes a write), or // passing the flag unconditionally (every write becomes a no-op). func TestAPreviewIsTheDryRunFlagAndNothingElse(t *testing.T) { if !slices.Contains(BankApplyArgs("/w", "/d.json", true), "--dry-run") { t.Error("a preview call carries no --dry-run: it would WRITE") } if slices.Contains(BankApplyArgs("/w", "/d.json", false), "--dry-run") { t.Error("an apply call carries --dry-run: it would never write") } } // The canon's central promise about the preview, proven against the REAL engine: «"preview": true // answers the same receipt without changing anything» (§applyBankCorrections). A regress of one // constant turns the safe look into an irreversible write, and no unit test can make this claim — // what writes is the engine, so the engine is what must be seen not writing. // // Gated like every live-engine test: a bare clone stays green and says why. func TestALivePreviewWritesNothingAndALiveApplyWrites(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 preview's no-write promise is not checked against a real engine") } dir := t.TempDir() writeProbeBook(t, tpl, dir, "bk_PREVIEWPROBE") doc, err := ingest.EncodeDecisions("bk_PREVIEWPROBE", []ingest.BankDecision{ {Action: "approve", Src: "测试", Sense: "", Dst: "проба", Note: "preview pin"}, }) if err != nil { t.Fatal(err) } decisions := filepath.Join(t.TempDir(), "decisions.json") if err := os.WriteFile(decisions, doc, 0o600); err != nil { t.Fatal(err) } before := dirListing(t, dir) rn := New(nil) out, err := rn.BankApply(t.Context(), bin, dir, decisions, true) if err != nil || !out.Exited || out.ExitCode != 0 || !out.Decoded { t.Fatalf("the live preview did not answer: %+v (%v; stderr %q)", out, err, out.Stderr) } if out.Report.Mode != "projection" || !out.Report.Changed { t.Fatalf("the preview's report: mode %q changed %v, want a projection that WOULD change", out.Report.Mode, out.Report.Changed) } // Nothing appeared and nothing moved — except the lock file, which the verb's own contract // names: a projection computed without the arbiter is a projection of nothing. after := dirListing(t, dir) for name, sum := range after { if filepath.Ext(name) == ".lock" { continue } if prev, ok := before[name]; !ok { t.Errorf("the preview CREATED %s", name) } else if prev != sum { t.Errorf("the preview CHANGED %s", name) } } // The same document applied for real is the counter-proof: the engine both can and does write // here, so the silence above was the preview's doing and not a fixture that cannot write. out, err = rn.BankApply(t.Context(), bin, dir, decisions, false) if err != nil || out.ExitCode != 0 || !out.Decoded || out.Report.Mode != "apply" || !out.Report.Changed { t.Fatalf("the live apply did not land: %+v (%v; stderr %q)", out, err, out.Stderr) } applied := dirListing(t, dir) grew := false for name := range applied { if _, ok := after[name]; !ok && filepath.Ext(name) != ".lock" { grew = true } } if !grew { t.Error("the apply wrote no decision file: this fixture proves nothing about the preview") } } // writeProbeBook renders a minimal live book: the deployment template with the identity, languages // and source the intake would have filled in (the four keys books.Service.provision sets). func writeProbeBook(t *testing.T, tpl, dir, bookID string) { t.Helper() raw, err := os.ReadFile(tpl) if err != nil { t.Fatal(err) } var cfg map[string]any if err := yaml.Unmarshal(raw, &cfg); err != nil { t.Fatal(err) } cfg["book_id"] = bookID cfg["source_lang"] = "zh" cfg["target_lang"] = "ru" cfg["source_file"] = "source.txt" out, err := yaml.Marshal(cfg) if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, ConfigFile), out, 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "source.txt"), []byte("第1章 测试\n这是一个测试文本。\n"), 0o600); err != nil { t.Fatal(err) } } // dirListing is the book directory as facts: every file name with a digest of its bytes. func dirListing(t *testing.T, dir string) map[string]string { t.Helper() out := map[string]string{} entries, err := os.ReadDir(dir) if err != nil { t.Fatal(err) } for _, e := range entries { if e.IsDir() { continue } b, err := os.ReadFile(filepath.Join(dir, e.Name())) if err != nil { t.Fatal(err) } out[e.Name()] = string(rune(len(b))) + "-" + string(b[:min(len(b), 64)]) } return out }