190 lines
7.9 KiB
Go
190 lines
7.9 KiB
Go
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): `<book_id>.mined-delta.yaml` / `<book_id>.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 (`<dir>/./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 (`<book_id>.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)
|
||
}
|
||
}
|