package config import ( "os" "path/filepath" "strings" "testing" ) // decisionpaths_test.go: the two owner-decision paths are a CONVENTION and only a convention // (D39.156 п.3, hardened by fix-pack-2): `.mined-delta.yaml` / `.mined-rejects.yaml` // beside book.yaml, a pure function of the config's location. Nobody declares them, nobody creates the // files in advance, and — since the keys are retired — no two books can point their decisions at one // file, which is what made the cross-book write race reachable. // // ⚠ The declared-path era's tests lived here; their subject was REMOVED by fix-pack-2 §4.9(б) (the keys // are refused, not honoured), so the tests were rewritten to pin the removal itself. func writeBook(t *testing.T, dir, extra string) string { t.Helper() src := filepath.Join(dir, "source.txt") if err := os.WriteFile(src, []byte("текст"), 0o644); err != nil { t.Fatal(err) } body := `book_id: decide-book title: Т source_lang: zh target_lang: ru pipeline: pipeline.yaml models: models.yaml source_file: source.txt ceilings: { book_usd: 1.0 } ` + extra p := filepath.Join(dir, "book.yaml") if err := os.WriteFile(p, []byte(body), 0o644); err != nil { t.Fatal(err) } return p } func TestDecisionPathsAreAPureFunctionOfTheConvention(t *testing.T) { dir := t.TempDir() b, err := LoadBook(writeBook(t, dir, "")) if err != nil { t.Fatalf("a book that declares no decision keys must load: %v", err) } // Same convention project_db uses, one directory and one book_id prefix — and NOT hung off // project_db, which may point outside the book's directory entirely. if want := filepath.Join(dir, "decide-book"+MinedDeltaSuffix); b.MinedDelta != want { t.Errorf("mined_delta = %q, want %q", b.MinedDelta, want) } if want := filepath.Join(dir, "decide-book"+MinedRejectsSuffix); b.MinedRejects != want { t.Errorf("mined_rejects = %q, want %q", b.MinedRejects, want) } // The files do NOT have to exist: an absent file is "nobody has decided anything yet", the same // standing an absent project database has before the first run. if _, err := os.Stat(b.MinedDelta); err == nil { t.Error("loading a book must not CREATE the decision files") } } // TestRetiredDecisionPathKeysAreRefused pins §4.9(б): a config written for the declarable era fails // NAMING THE CURE, whether or not the declared file exists — silently honouring the convention instead // of the declared path would send the run's decisions to a different file than the operator believes. func TestRetiredDecisionPathKeysAreRefused(t *testing.T) { for _, tc := range []struct { key string exists bool }{ {"mined_delta", true}, {"mined_delta", false}, {"mined_rejects", true}, {"mined_rejects", false}, } { dir := t.TempDir() if tc.exists { if err := os.WriteFile(filepath.Join(dir, "my.yaml"), []byte("terms: []\n"), 0o644); err != nil { t.Fatal(err) } } _, err := LoadBook(writeBook(t, dir, tc.key+": my.yaml\n")) if err == nil { t.Fatalf("%s (file exists=%v) must be refused as retired", tc.key, tc.exists) } for _, want := range []string{"retired", "convention", tc.key} { if !strings.Contains(err.Error(), want) { t.Errorf("%s: the refusal must carry %q (the cure, not a bare decode error): %v", tc.key, want, err) } } } // The `adult: false` discipline: an EMPTY declared value asked for the convention, which is what it // gets — refusing it would break configs for nothing. if _, err := LoadBook(writeBook(t, t.TempDir(), "mined_delta: \"\"\n")); err != nil { t.Fatalf("an empty retired key must still load: %v", err) } } // --- the dofix pack --------------------------------------------------------------------------------- func TestTheDecisionFilesTravelWithTheBookAndNotWithTheDatabase(t *testing.T) { // The rule the convention rests on. They are the USER's decisions: a backup or an export of the // book's directory has to contain them, and `project_db` may point anywhere at all — a scratch disk, // a tmpfs, a directory the operator wipes between runs. dir := t.TempDir() db := filepath.Join(t.TempDir(), "state") // a DIFFERENT tree entirely if err := os.MkdirAll(db, 0o755); err != nil { t.Fatal(err) } p := writeBook(t, dir, "project_db: "+filepath.Join(db, "decide-book.db")+"\n") b, err := LoadBook(p) if err != nil { t.Fatal(err) } for _, tc := range []struct{ what, got, want string }{ {"mined_delta", b.MinedDelta, filepath.Join(dir, "decide-book"+MinedDeltaSuffix)}, {"mined_rejects", b.MinedRejects, filepath.Join(dir, "decide-book"+MinedRejectsSuffix)}, } { if tc.got != tc.want { t.Errorf("%s resolved to %q, want it beside the BOOK at %q", tc.what, tc.got, tc.want) } if filepath.Dir(tc.got) == db { t.Errorf("%s followed project_db out of the book's directory", tc.what) } } } func TestDeclaredKeysCannotLandOnTheDecisionFiles(t *testing.T) { // The blast radius the same-path guard still covers now that the decision keys are gone: the two // remaining declarable paths could still be pointed AT a conventional decision file, and this door // would then write YAML over the book's sqlite store — or read the seed and the delta as one file. dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "decide-book"+MinedDeltaSuffix), nil, 0o644); err != nil { t.Fatal(err) } p := writeBook(t, dir, "project_db: decide-book"+MinedDeltaSuffix+"\n") if _, err := LoadBook(p); err == nil || !strings.Contains(err.Error(), "project_db") { t.Fatalf("project_db on the conventional mined_delta path must be refused, got %v", err) } // …and a book that names its paths normally is untouched. if _, err := LoadBook(writeBook(t, t.TempDir(), "")); err != nil { t.Fatalf("an ordinary book must still load: %v", err) } } // TestAbsolutePathsAreCleanedBeforeTheSamePathGuard pins §4.9(а): the guard compares STRINGS, and an // uncleaned absolute path (`/./x`) used to be a second spelling of a guarded path — the acceptance // walked through exactly that hole and reproduced the original data-loss blocker whole. func TestAbsolutePathsAreCleanedBeforeTheSamePathGuard(t *testing.T) { dir := t.TempDir() target := filepath.Join(dir, "decide-book"+MinedDeltaSuffix) if err := os.WriteFile(target, nil, 0o644); err != nil { t.Fatal(err) } dotted := dir + string(filepath.Separator) + "." + string(filepath.Separator) + "decide-book" + MinedDeltaSuffix if dotted == target { t.Fatalf("fixture is vacuous: %q already clean", dotted) } p := writeBook(t, dir, "project_db: "+dotted+"\n") b, err := LoadBook(p) if err == nil { t.Fatalf("an uncleaned absolute project_db on the mined_delta path must be refused; loaded with project_db=%q", b.ProjectDB) } if !strings.Contains(err.Error(), "project_db") { t.Errorf("the refusal must name project_db: %v", err) } } func TestBookIDIsRefusedWhenItIsNotAPathElement(t *testing.T) { // book_id is a FILE NAME: four of the engine's files are named after it, and two of them now carry the // owner's decisions. `../x` puts all four outside the book's directory — somewhere no backup of the // book contains and no export of it carries. The class is older than this pack (`.db` has // always been built this way); what the pack changed is that it went from one file to four. for _, id := range []string{"../escape", "a/b", "..", "."} { dir := t.TempDir() p := writeBook(t, dir, "") raw, err := os.ReadFile(p) if err != nil { t.Fatal(err) } body := strings.Replace(string(raw), "book_id: decide-book", "book_id: "+id, 1) if err := os.WriteFile(p, []byte(body), 0o644); err != nil { t.Fatal(err) } if _, err := LoadBook(p); err == nil { t.Errorf("book_id %q names a path, not a file, and must be refused", id) } } // An ordinary slug stays legitimate — every book_id in the repository and on the stand is one. if _, err := LoadBook(writeBook(t, t.TempDir(), "")); err != nil { t.Fatalf("an ordinary book_id must still load: %v", err) } }