115 lines
5.3 KiB
Go
115 lines
5.3 KiB
Go
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"strings"
|
||
"testing"
|
||
|
||
"textmachine/backend/internal/pipeline"
|
||
)
|
||
|
||
// export_cli_test.go pins the tmctl surface of the read-only export projection (D39 layer 6): the
|
||
// parse of the `export` command + `--plaintext`, the default stable-JSON render, and the human
|
||
// plaintext render (banner + text, flagged chunks marked, dropped chunks empty).
|
||
|
||
func TestParseExportPlaintext(t *testing.T) {
|
||
inv, err := parseInvocation([]string{"export", "--config", "b.yaml", "--plaintext"}, &bytes.Buffer{})
|
||
if err != nil {
|
||
t.Fatalf("export --plaintext must parse: %v", err)
|
||
}
|
||
if inv.cmd != "export" || inv.cfgPath != "b.yaml" || !inv.asPlaintext {
|
||
t.Fatalf("inv = %+v", inv)
|
||
}
|
||
// Default (no flag) is JSON, not plaintext.
|
||
inv2, err := parseInvocation([]string{"export", "--config", "b.yaml"}, &bytes.Buffer{})
|
||
if err != nil || inv2.asPlaintext {
|
||
t.Fatalf("export default must be JSON (asPlaintext=false): inv=%+v err=%v", inv2, err)
|
||
}
|
||
}
|
||
|
||
func sampleExport() *pipeline.BookExport {
|
||
return &pipeline.BookExport{BookID: "b1", Chunks: []pipeline.ChunkExport{
|
||
{Chapter: 1, ChunkIdx: 0, SnapshotID: "snap", Disposition: "ok", FinalText: "Чистый текст."},
|
||
{Chapter: 2, ChunkIdx: 0, SnapshotID: "snap", Disposition: "flagged", FlagReason: "sanitizer_stripped", FinalText: "Очищенный текст."},
|
||
{Chapter: 3, ChunkIdx: 0, SnapshotID: "snap", Disposition: "flagged", FlagReason: "sanitizer_defect", FinalText: ""},
|
||
}}
|
||
}
|
||
|
||
func TestRenderExportJSONIsStable(t *testing.T) {
|
||
var b bytes.Buffer
|
||
if err := renderExport(&b, sampleExport(), false); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// The default surface is valid JSON the polygon extractor can consume: it round-trips and carries
|
||
// every chunk's export bytes and metadata.
|
||
var got pipeline.BookExport
|
||
if err := json.Unmarshal(b.Bytes(), &got); err != nil {
|
||
t.Fatalf("export JSON must round-trip: %v\n%s", err, b.String())
|
||
}
|
||
if got.BookID != "b1" || len(got.Chunks) != 3 {
|
||
t.Fatalf("round-tripped export = %+v", got)
|
||
}
|
||
if got.Chunks[1].FinalText != "Очищенный текст." || got.Chunks[1].FlagReason != "sanitizer_stripped" {
|
||
t.Fatalf("stripped chunk lost fidelity: %+v", got.Chunks[1])
|
||
}
|
||
}
|
||
|
||
func TestRenderExportPlaintextBanners(t *testing.T) {
|
||
var b bytes.Buffer
|
||
if err := renderExport(&b, sampleExport(), true); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
out := b.String()
|
||
for _, want := range []string{
|
||
"=== CHAPTER 1 CHUNK 0 ===\nЧистый текст.",
|
||
"=== CHAPTER 2 CHUNK 0 — flagged (sanitizer_stripped) (leak cleaned, verify) ===\nОчищенный текст.",
|
||
"=== CHAPTER 3 CHUNK 0 — flagged (sanitizer_defect) (not translated, flagged for a human) ===",
|
||
} {
|
||
if !strings.Contains(out, want) {
|
||
t.Fatalf("plaintext export must contain %q, got:\n%s", want, out)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestThePlaintextExportDoesNotCallAStopMarkAHumansProblem is the third surface of one sentence, and the
|
||
// one the repair of the other two missed: a position whose re-attack a ceiling refused, or whose call a
|
||
// person cut, is NOT «flagged for a human» — it was paid for and not finished, and the next run does it.
|
||
// The book writer and the stopped-run account were corrected first; this banner kept saying the opposite
|
||
// in the same file the correction touched.
|
||
//
|
||
// The control rows are the point: a real verdict must KEEP the old banner, or the fix would have replaced
|
||
// one false sentence with another.
|
||
func TestThePlaintextExportDoesNotCallAStopMarkAHumansProblem(t *testing.T) {
|
||
exp := &pipeline.BookExport{
|
||
BookID: "syn", TotalUnits: 4,
|
||
Chunks: []pipeline.ChunkExport{
|
||
{Chapter: 1, ChunkIdx: 0, Disposition: "flagged", FlagReason: string(pipeline.FlagRetryUnaffordable)},
|
||
{Chapter: 2, ChunkIdx: 0, Disposition: "flagged", FlagReason: string(pipeline.FlagCancelled)},
|
||
{Chapter: 3, ChunkIdx: 0, Disposition: "flagged", FlagReason: string(pipeline.FlagHardRefusal)},
|
||
{Chapter: 4, ChunkIdx: 0, Disposition: "flagged", FlagReason: string(pipeline.FlagSanitizerDefect)},
|
||
// A c-lite unit that lost a member FOR GOOD and whose assembling stage merely ran out of money:
|
||
// «the next run re-does it» would promise back a text no purchase can bring, so this one asks
|
||
// for a human — the half the cheerier sentence would have hidden.
|
||
{Chapter: 5, ChunkIdx: 0, Disposition: "flagged", FlagReason: string(pipeline.FlagRetryUnaffordable), DroppedMembers: 1, DroppedReason: string(pipeline.FlagHardRefusal)},
|
||
},
|
||
}
|
||
var b bytes.Buffer
|
||
if err := renderExport(&b, exp, true); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
printed := b.String()
|
||
t.Logf("what the operator reads:\n%s", printed)
|
||
for _, want := range []string{
|
||
"=== CHAPTER 1 CHUNK 0 — flagged (retry_unaffordable) (paid for and NOT done — the next run re-does it) ===",
|
||
"=== CHAPTER 2 CHUNK 0 — flagged (cancelled) (paid for and NOT done — the next run re-does it) ===",
|
||
// The controls: a verdict a person really must look at keeps its banner.
|
||
"=== CHAPTER 3 CHUNK 0 — flagged (hard_refusal) (not translated, flagged for a human) ===",
|
||
"=== CHAPTER 4 CHUNK 0 — flagged (sanitizer_defect) (not translated, flagged for a human) ===",
|
||
"=== CHAPTER 5 CHUNK 0 — flagged (retry_unaffordable) (not translated, flagged for a human) ===",
|
||
} {
|
||
if !strings.Contains(printed, want) {
|
||
t.Fatalf("the export does not carry %q.\nWhat it printed:\n%s", want, printed)
|
||
}
|
||
}
|
||
}
|