package seed import ( "strings" "testing" ) // decode_test.go: the UNKNOWN-KEY contract of the two decision files (backlog row 212). // // The defect these pin is silent substitution, not a parse failure: yaml.v3 DROPS a key the struct does // not have, so `gendr:` for `gender:` was thrown away BEFORE any validation ran — and `seed-lint` could // not see it either, because it validates VALUES and the key never reached it. The same class pack-16 // bought strictness against for book.yaml. func TestSeedDecodeRefusesAnUnknownKey(t *testing.T) { _, err := DecodeFile([]byte("terms:\n - src: 方源\n dst: Фан Юань\n gendr: male\n")) if err == nil { t.Fatal("a misspelled term key must be a loud error, not a dropped field") } if !strings.Contains(err.Error(), "gendr") { t.Errorf("the error must name the key it did not recognise, got: %v", err) } } func TestSeedDecodeRefusesAnUnknownTopLevelKey(t *testing.T) { if _, err := DecodeFile([]byte("termz:\n - src: 方源\n")); err == nil { t.Fatal("a misspelled SECTION name silently yields an empty seed — it must be loud") } } func TestRejectsDecodeRefusesAnUnknownKey(t *testing.T) { // `rejcts:` used to read as an empty reject list, silently un-declining every term the owner // declined — they all re-enter the signature map and the auto-bank as if the review never happened. if _, err := DecodeRejects([]byte("rejcts:\n - src: 方源\n")); err == nil { t.Fatal("a misspelled reject section must be a loud error") } if _, err := DecodeRejects([]byte("rejects:\n - src: 方源\n reasn: no\n")); err == nil { t.Fatal("a misspelled reject field must be a loud error") } } // TestDecodeAcceptsAnEmptyDocument keeps the strictness from breaking a legitimate state: a decision // file the engine itself wrote with nothing in it, a file of whitespace, and — the case the first // version of this list missed — a file holding only COMMENTS. // // yaml.Decoder reports a stream with no document as io.EOF where yaml.Unmarshal accepted it as "no // records", and every caller here used the second one. A whitespace-only pre-check is NOT the same // test: comment bytes are not blank, so a header-only reject list came back `seed: EOF` and broke a // file that had always loaded — in a format whose own entries carry a `note` to be self-documenting, // on the path that governs every book's glossary_seed. Found by review; reproduced against a build of // HEAD before the fix. func TestDecodeAcceptsAnEmptyDocument(t *testing.T) { for _, body := range []string{"", "\n", " \n\n", "# declined terms\n", "# a\n# b\n", " # indented\n"} { f, err := DecodeFile([]byte(body)) if err != nil || len(f.Terms) != 0 { t.Errorf("document with no records %q: err=%v terms=%d, want no error and no terms", body, err, len(f.Terms)) } r, err := DecodeRejects([]byte(body)) if err != nil || len(r.Rejects) != 0 { t.Errorf("reject document with no records %q: err=%v rejects=%d, want no error and no rejects", body, err, len(r.Rejects)) } } } // TestDecodeRoundTripsEveryTermField is the guard against the strictness itself becoming the defect: a // document naming every field of the schema must load, or a field added to Term without a matching tag // would make previously valid files unloadable. func TestDecodeRoundTripsEveryTermField(t *testing.T) { body := `terms: - src: 方源 dst: Фан Юань type: name sense: герой gender: male speech: сухая decl: { invariant: false, forms: [Фан Юаня] } translit_policy: palladius first_person: я nickname_translation: keep since_ch: 1 until_ch: 100 status: approved allow_short: true note: n aliases: - { alias: 方, type: short } voices: - src: 方源 sense: герой register: r self_ref: я address_default: informal lexicon_markers: [a] ng_lexicon: [b] exemplars: [c] brightness: high since_ch: 1 until_ch: 2 addresses: - speaker: 方源 speaker_sense: герой addressee: 花月 addressee_sense: "" register: informal form: ты closeness: близко since_ch: 1 until_ch: 2 ` f, err := DecodeFile([]byte(body)) if err != nil { t.Fatalf("a document naming every schema field must load: %v", err) } if len(f.Terms) != 1 || len(f.Voices) != 1 || len(f.Addresses) != 1 { t.Fatalf("got %d terms, %d voices, %d addresses", len(f.Terms), len(f.Voices), len(f.Addresses)) } if f.Terms[0].Decl == nil || len(f.Terms[0].Decl.Forms) != 1 || len(f.Terms[0].Aliases) != 1 { t.Errorf("nested records lost: decl=%v aliases=%v", f.Terms[0].Decl, f.Terms[0].Aliases) } }