116 lines
5.1 KiB
Go
116 lines
5.1 KiB
Go
package pipeline
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"textmachine/backend/internal/obs"
|
||
)
|
||
|
||
// buildhonesty_test.go: the two disclosure defects the cold run of 31.08 found in `build`'s cleanup loop.
|
||
//
|
||
// They are one loop and one law: an act nobody asked for was performed in silence (§2.1), and a failure
|
||
// AFTER an irreversible write threw away the report of that write (§2.5). Both were observed on live data —
|
||
// (a) `build --format epub` deleting a neighbouring .book.txt at exit 0 with no line anywhere; (b) derived
|
||
// from code, and named in the run report as NOT reproduced, which is why it gets a fixture here.
|
||
|
||
// buildTwoFormats translates a small book and builds BOTH formats, returning the project directory.
|
||
func buildTwoFormats(t *testing.T, srvURL string) (dir string, r *Runner) {
|
||
t.Helper()
|
||
bookPath := setupProjectOpts(t, srvURL, projectOpts{source: "ГЛАВАА\fГЛАВАБ", regenerate: 0})
|
||
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
||
r = newRunner(t, bookPath)
|
||
if _, err := r.TranslateBook(ctx); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := r.BuildBook(BuildOptions{}); err != nil {
|
||
t.Fatalf("the two-format build must succeed: %v", err)
|
||
}
|
||
return filepath.Dir(bookPath), r
|
||
}
|
||
|
||
// TestBuildNamesTheFileItDeletes is A3(б) as it actually is — not the `book_files` map, which turned out
|
||
// to be a documented contract on BOTH sides («a PLACE, not a presence») and no defect at all, but the
|
||
// SILENT DELETION next to it. os.Remove returning nil means a file was there and is now gone; the loop
|
||
// knew and said nothing.
|
||
//
|
||
// Mutation this catches: collapse the switch back to `if err != nil && !errors.Is(err, fs.ErrNotExist)`
|
||
// and the removal stops being recorded → both assertions fire.
|
||
func TestBuildNamesTheFileItDeletes(t *testing.T) {
|
||
srv := newJSONProvider(&reqRec{}, multiLineEdit)
|
||
defer srv.Close()
|
||
dir, r := buildTwoFormats(t, srv.URL)
|
||
defer r.Close()
|
||
|
||
txt := filepath.Join(dir, "test-book.db.book.txt")
|
||
if _, err := os.Stat(txt); err != nil {
|
||
t.Fatalf("premise: the two-format build must leave a .txt beside the database: %v", err)
|
||
}
|
||
|
||
rep, err := r.BuildBook(BuildOptions{Formats: []string{"epub"}})
|
||
if err != nil {
|
||
t.Fatalf("an epub-only build is a normal build: %v", err)
|
||
}
|
||
if _, err := os.Stat(txt); !os.IsNotExist(err) {
|
||
t.Fatalf("premise: the unrequested format is removed — that behaviour is correct and is not what "+
|
||
"this test challenges; got %v", err)
|
||
}
|
||
if len(rep.RemovedFiles) != 1 || !strings.HasSuffix(rep.RemovedFiles[0], "test-book.db.book.txt") {
|
||
t.Fatalf("a file the caller did not ask to delete was deleted, and the report must NAME it: %+v",
|
||
rep.RemovedFiles)
|
||
}
|
||
if len(rep.StaleCopies) != 0 {
|
||
t.Fatalf("nothing failed to be removed here: %+v", rep.StaleCopies)
|
||
}
|
||
}
|
||
|
||
// TestACleanupFailureDoesNotEraseTheBuildReport is A3(а), the branch the cold run derived from code and
|
||
// could not reach. The files are committed BEFORE the cleanup runs, so a cleanup failure must not be able
|
||
// to turn a written book into «infra failure, nothing written» — which is exactly what a bare error does,
|
||
// because the exit mapper sends anything unclassified to 1 and the band's reader writes the run off.
|
||
//
|
||
// The failure is produced honestly rather than mocked: a non-empty DIRECTORY at the path os.Remove is
|
||
// about to take. os.Remove cannot remove it, and the error is neither nil nor fs.ErrNotExist.
|
||
//
|
||
// Mutation this catches: return the error from the loop again (`return nil, fmt.Errorf(...)`) and the
|
||
// build fails → the first assertion fires with the files sitting on disk.
|
||
func TestACleanupFailureDoesNotEraseTheBuildReport(t *testing.T) {
|
||
srv := newJSONProvider(&reqRec{}, multiLineEdit)
|
||
defer srv.Close()
|
||
dir, r := buildTwoFormats(t, srv.URL)
|
||
defer r.Close()
|
||
|
||
txt := filepath.Join(dir, "test-book.db.book.txt")
|
||
if err := os.Remove(txt); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.MkdirAll(filepath.Join(txt, "occupied"), 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
rep, err := r.BuildBook(BuildOptions{Formats: []string{"epub"}})
|
||
if err != nil {
|
||
t.Fatalf("the book WAS written before this cleanup ran, so a housekeeping failure must not report "+
|
||
"the build as failed — a bare error here maps to exit 1 and reads as «nothing was written»: %v", err)
|
||
}
|
||
if rep == nil {
|
||
t.Fatal("the report must survive: it is the only record of what landed on disk")
|
||
}
|
||
epub := filepath.Join(dir, "test-book.db.book.epub")
|
||
if got, ok := rep.Files["epub"]; !ok || !strings.HasSuffix(got, "test-book.db.book.epub") {
|
||
t.Fatalf("the report must still name the file it wrote: %+v", rep.Files)
|
||
}
|
||
if _, err := os.Stat(epub); err != nil {
|
||
t.Fatalf("premise: the epub really is on disk: %v", err)
|
||
}
|
||
if len(rep.StaleCopies) != 1 || !strings.HasSuffix(rep.StaleCopies[0], "test-book.db.book.txt") {
|
||
t.Fatalf("the copy that could NOT be removed is stale and does not belong to this build — silence "+
|
||
"about it would leave a file the envelope presents as current: %+v", rep.StaleCopies)
|
||
}
|
||
if len(rep.RemovedFiles) != 0 {
|
||
t.Fatalf("nothing was actually removed: %+v", rep.RemovedFiles)
|
||
}
|
||
}
|