143 lines
5.9 KiB
Go
143 lines
5.9 KiB
Go
package lang
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// realRoot is the in-repo langpack root, relative to this package's test cwd (internal/lang/).
|
||
const realRoot = "../../configs/langpacks"
|
||
|
||
// TestLoadResolvesRealZhRu loads the REAL zh→ru pack from disk and pins a spot-check of the moved data
|
||
// (the const→file move must be byte-faithful; the full-book parity that pins the exact miner output is
|
||
// the stand-gated TestMinerFullBookParity, so this is the CI-runnable anchor). It exercises the real
|
||
// disk-loading path end to end.
|
||
func TestLoadResolvesRealZhRu(t *testing.T) {
|
||
p, err := Load(realRoot, "zh", "ru")
|
||
if err != nil {
|
||
t.Fatalf("Load(zh,ru): %v", err)
|
||
}
|
||
if p.Pair != "zh-ru" {
|
||
t.Errorf("Pair = %q, want zh-ru", p.Pair)
|
||
}
|
||
// Source morphology spot-check (byte-faithful move).
|
||
if !p.SurnamesSingle['赵'] || !p.SurnamesSingle['方'] {
|
||
t.Error("surnames-single missing 赵/方")
|
||
}
|
||
if p.SurnamesSingle['凝'] {
|
||
t.Error("surnames-single must NOT contain 凝 (discarded — not a surname)")
|
||
}
|
||
if !p.SurnamesCompound["古月"] || !p.SurnamesCompound["欧阳"] {
|
||
t.Error("surnames-compound missing 古月/欧阳")
|
||
}
|
||
if len(p.TitleSuffix) == 0 || p.TitleSuffix[0] != "公子" {
|
||
t.Errorf("title-suffix order not preserved: %v", p.TitleSuffix)
|
||
}
|
||
if !p.TopoSuffix['山'] || !p.Numeral['三'] || !p.GradePrefix['甲'] || !p.AliasParticle['的'] {
|
||
t.Error("rune-set membership missing an expected char (山/三/甲/的)")
|
||
}
|
||
// Pair transliteration spot-check.
|
||
if p.PalladiusInitials["b"] != "б" || p.PalladiusInitials["zh"] != "чж" {
|
||
t.Errorf("palladius initials wrong: b=%q zh=%q", p.PalladiusInitials["b"], p.PalladiusInitials["zh"])
|
||
}
|
||
if p.PalladiusSpecialI["zhi"] != "чжи" {
|
||
t.Errorf("palladius special_i zhi = %q, want чжи", p.PalladiusSpecialI["zhi"])
|
||
}
|
||
if !strings.HasPrefix(p.Version(), packAlgoVersion+"-") {
|
||
t.Errorf("Version() = %q, want %s-<hash>", p.Version(), packAlgoVersion)
|
||
}
|
||
}
|
||
|
||
// TestRoutesByPairToDifferentBytes is the load-bearing generality proof: a SECOND pair, dropped as data
|
||
// (no pipeline/ edit, no recompile), routes to DIFFERENT bytes and a DIFFERENT version. Proves "add a
|
||
// language = data only" — the pair is a genuine routing key, not a baked-in zh constant.
|
||
func TestRoutesByPairToDifferentBytes(t *testing.T) {
|
||
root := t.TempDir()
|
||
writeSyntheticPack(t, root, "xx", "yy")
|
||
|
||
real, err := Load(realRoot, "zh", "ru")
|
||
if err != nil {
|
||
t.Fatalf("load real: %v", err)
|
||
}
|
||
synth, err := Load(root, "xx", "yy")
|
||
if err != nil {
|
||
t.Fatalf("load synthetic xx-yy: %v", err)
|
||
}
|
||
|
||
// The synthetic pair carries its OWN surnames (not zh's), proving the resolver routes by pair, not
|
||
// to a hardcoded set.
|
||
if !synth.SurnamesSingle['甴'] {
|
||
t.Error("synthetic pack must carry its own surname 甴")
|
||
}
|
||
if synth.SurnamesSingle['赵'] {
|
||
t.Error("synthetic pack must NOT inherit zh's surname 赵 (would mean a baked-in constant)")
|
||
}
|
||
if !real.SurnamesSingle['赵'] || real.SurnamesSingle['甴'] {
|
||
t.Error("real zh pack routing leaked/borrowed synthetic bytes")
|
||
}
|
||
if real.Version() == synth.Version() {
|
||
t.Errorf("different-byte packs must have different versions (real=%s synth=%s)", real.Version(), synth.Version())
|
||
}
|
||
}
|
||
|
||
// TestFailsLoudOnMissingPair pins the fail-loud contract: a pair with no pack directory errors, naming the
|
||
// missing file — never a silent empty pack (mirrors the prompt seam's PromptPathFor fail-loud). This is
|
||
// the load-time invariant a live consumer relies on (fail before billing).
|
||
func TestFailsLoudOnMissingPair(t *testing.T) {
|
||
_, err := Load(realRoot, "ja", "ru")
|
||
if err == nil {
|
||
t.Fatal("Load(ja,ru) must fail loud (no ja langpack), got nil error")
|
||
}
|
||
if !strings.Contains(err.Error(), "ja-ru") {
|
||
t.Errorf("error must name the missing pair ja-ru, got: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestFailsLoudOnEmptyTable pins the "never silently empty" contract: a present-but-empty (comment-only)
|
||
// required file is a corrupt pack and must fail loud at load — not parse to an empty set that silently
|
||
// disables a miner channel once packs are hand-authored (R1).
|
||
func TestFailsLoudOnEmptyTable(t *testing.T) {
|
||
root := t.TempDir()
|
||
writeSyntheticPack(t, root, "xx", "yy")
|
||
// Blank out one required table (keep the file present, drop its content).
|
||
if err := os.WriteFile(filepath.Join(root, "xx", "surnames-single.txt"), []byte("# emptied by a bad edit\n"), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_, err := Load(root, "xx", "yy")
|
||
if err == nil {
|
||
t.Fatal("an empty required table must fail loud, got nil error")
|
||
}
|
||
if !strings.Contains(err.Error(), "surnames-single") {
|
||
t.Errorf("error must name the empty table, got: %v", err)
|
||
}
|
||
}
|
||
|
||
// writeSyntheticPack writes a minimal, VALID pack for (src, tgt) under root with bytes distinct from zh —
|
||
// enough for Load to succeed and for the routing assertion to bite.
|
||
func writeSyntheticPack(t *testing.T, root, src, tgt string) {
|
||
t.Helper()
|
||
pair := src + "-" + tgt
|
||
files := map[string]string{
|
||
filepath.Join(src, "surnames-single.txt"): "# synthetic\n甴甶甹\n",
|
||
filepath.Join(src, "surnames-compound.txt"): "# synthetic\n甲乙\n",
|
||
filepath.Join(src, "title-suffix.txt"): "# synthetic\n阁下\n",
|
||
filepath.Join(src, "ordinal-title.txt"): "# synthetic\n第甲\n",
|
||
filepath.Join(src, "rank-word.txt"): "# synthetic\n級\n",
|
||
filepath.Join(src, "topo-suffix.txt"): "# synthetic\n峰\n",
|
||
filepath.Join(src, "grade-prefix.txt"): "# synthetic\n子丑\n",
|
||
filepath.Join(src, "numeral.txt"): "# synthetic\n壹貳\n",
|
||
filepath.Join(src, "alias-particle.txt"): "# synthetic\n之乎\n",
|
||
filepath.Join(pair, "palladius.txt"): "# synthetic\ninitials\tb\tб\nfinals\ta\tа\nyw\tyi\tи\nspecial_i\tzhi\tчжи\n",
|
||
}
|
||
for rel, body := range files {
|
||
p := filepath.Join(root, rel)
|
||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
}
|