textmachine/backend/internal/config/promptlabel_pair_test.go

208 lines
9 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
// promptlabel_pair_test.go: a pair labels its OWN prompts.
//
// ⛔ WHY THIS IS A MONEY PIN AND NOT TIDINESS. `prompt_version` folds into the snapshot, and a run config
// is pair-agnostic: one string in pipeline-c1.yaml labels the translator prompt of EVERY pair that config
// serves. Before this key, a pack that edited one pair's prompts had two ways to go and both were wrong —
// bump the shared label and re-snapshot (re-buy) the books of the pairs whose bytes never moved, or leave
// it and let one label stand over two different texts, which is exactly what the label ledger exists to
// catch. The third way is this file's subject.
// TestAPairLabelsItsOwnPromptsWithoutMovingAnother is the live proof on the SHIPPING files, not a fixture:
// the same run config, read for two pairs, must give the English prompts the pair's own label and leave
// the Chinese ones on the run config's.
func TestAPairLabelsItsOwnPromptsWithoutMovingAnother(t *testing.T) {
models, err := LoadModels(filepath.Join("..", "..", "configs", "models.yaml"))
if err != nil {
t.Fatalf("load models: %v", err)
}
cfg := filepath.Join("..", "..", "configs", "pipeline-c1.yaml")
labels := func(pair string) map[string]string {
t.Helper()
p, err := LoadPipeline(cfg, models, pair, nil)
if err != nil {
t.Fatalf("pipeline-c1 must load for %s: %v", pair, err)
}
out := map[string]string{}
for _, st := range p.Stages {
out[st.Name] = st.PromptVersion
}
return out
}
en, zh := labels("en-ru"), labels("zh-ru")
if len(en) == 0 || len(zh) == 0 {
t.Fatal("premise broken: the shipping config resolved no stages, so nothing below is about labels")
}
// The RUN CONFIG's own literal, read from the file rather than copied here — a copy would keep passing
// after somebody edited the config, which is the exact class of staleness this whole ledger is about.
raw, err := os.ReadFile(cfg)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(raw), "prompt_version: "+zh["draft"]) {
t.Errorf("the zh-ru draft label %q is not the one pipeline-c1.yaml states — a pair that declares no label of its own must keep the run config's", zh["draft"])
}
if en["draft"] == zh["draft"] {
t.Errorf("both pairs' draft stages carry %q, so the en-ru prompt edit either did not happen or is riding the Chinese book's label — the pair layer's prompt_versions is what keeps them apart", en["draft"])
}
if en["edit"] == zh["edit"] {
t.Errorf("both pairs' edit stages carry %q; the same reasoning as the draft stage applies", en["edit"])
}
}
// TestAPairLabelKeyHasToNameAPromptFile pins the refusals. An empty label is the dangerous one: the stage
// check that demands a label has already passed by the time the pair layer is applied, so an empty value
// would REPLACE a stated label with nothing and the run would carry no comparability statement at all.
func TestAPairLabelKeyHasToNameAPromptFile(t *testing.T) {
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "pairs"), 0o755); err != nil {
t.Fatal(err)
}
write := func(body string) {
t.Helper()
if err := os.WriteFile(filepath.Join(dir, "pairs", "xx-yy.yaml"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
// PREMISE: a well-formed declaration loads and carries the label, or the refusals below would be
// indistinguishable from "this key does nothing at all".
write("pair: xx-yy\nprompt_versions:\n translator: v1-xx\n")
pc, err := LoadPair(dir, "xx-yy")
if err != nil || pc == nil {
t.Fatalf("a well-formed pair label must load: %v", err)
}
if pc.PromptVersions["translator"] != "v1-xx" {
t.Fatalf("the label must reach the pair config, got %+v", pc.PromptVersions)
}
for _, bad := range []struct{ name, body string }{
{"a path instead of a base name", "pair: xx-yy\nprompt_versions:\n ../translator: v1\n"},
{"a separator in the key", "pair: xx-yy\nprompt_versions:\n en-ru/translator: v1\n"},
{"an empty label", "pair: xx-yy\nprompt_versions:\n translator: \" \"\n"},
} {
t.Run(bad.name, func(t *testing.T) {
write(bad.body)
if _, err := LoadPair(dir, "xx-yy"); err == nil {
t.Error("must fail loud at load — a label nobody can resolve is not a smaller statement, it is a missing one")
}
})
}
}
// TestBriefPolicyFieldsAndTheirWiringAreOneList closes the gap between the list a pair's declaration is
// JUDGED against (BriefPolicyFields, checked in LoadPair) and the list that actually judges a book
// (briefTargets). A field in the first and not the second loads clean and enforces nothing: the pair
// believes it declared a policy, the book violates it, and the engine says nothing.
func TestBriefPolicyFieldsAndTheirWiringAreOneList(t *testing.T) {
wired := briefTargets(&Book{})
if len(wired) != len(BriefPolicyFields) {
t.Fatalf("the two lists differ in size: judged %v, wired %d entries", BriefPolicyFields, len(wired))
}
for _, name := range BriefPolicyFields {
if _, ok := wired[name]; !ok {
t.Errorf("%q is a field a pair may declare and nothing in the engine reads it", name)
}
}
}
// TestAPairsPromptLabelNamesAPromptItHas is the guarantee this file's neighbour only claims: a key of
// prompt_versions is the BASE NAME of a prompt file, and a typo in it is a dead key that silently leaves
// the shared, snapshot-folded label in force — the exact state the pair-scoped label exists to end. The
// shape check at load cannot see this (a typo is well-shaped); only the pair's own directory can.
func TestAPairsPromptLabelNamesAPromptItHas(t *testing.T) {
dir := filepath.Join("..", "..", "configs")
pairs, err := filepath.Glob(filepath.Join(dir, "pairs", "*.yaml"))
if err != nil || len(pairs) == 0 {
t.Fatalf("premise broken: no pair files to walk (%v)", err)
}
checked := 0
for _, path := range pairs {
pair := strings.TrimSuffix(filepath.Base(path), ".yaml")
pc, err := LoadPair(dir, pair)
if err != nil || pc == nil {
t.Fatalf("pair %s must load: %v", pair, err)
}
for name := range pc.PromptVersions {
checked++
p := filepath.Join(pc.PromptsRoot, pair, name+".md")
if _, err := os.Stat(p); err != nil {
t.Errorf("pair %s labels %q, which names no prompt: %s does not exist — the label is dead and the run keeps the shared one", pair, name, p)
}
}
}
if checked == 0 {
t.Skip("no pair declares its own prompt labels yet; nothing to check")
}
}
// TestAPairThatDeclaresAGlyphTeachesItToItsPrompts is the second half of a contract both the data file and
// the loader's doc comment state and nothing enforced: the ingest wraps emphasis in the pair's glyphs, and
// only that pair's PROMPTS can tell the model what they mean. A pair changing its glyph, or copying the
// mechanism without the instruction, would hand a model unexplained punctuation mid-prose — and the label
// ledger would not see it, because it watches prompt bytes against labels, not data against prompts.
func TestAPairThatDeclaresAGlyphTeachesItToItsPrompts(t *testing.T) {
dir := filepath.Join("..", "..", "configs")
packs, err := filepath.Glob(filepath.Join(dir, "langpacks", "*", "inline-markup.txt"))
if err != nil {
t.Fatal(err)
}
if len(packs) == 0 {
t.Skip("no pair declares inline markup yet")
}
for _, path := range packs {
pair := filepath.Base(filepath.Dir(path))
pc, err := LoadPair(dir, pair)
if err != nil || pc == nil {
t.Fatalf("pair %s declares inline markup but has no pair config: %v", pair, err)
}
body, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var glyphs []string
for _, ln := range strings.Split(string(body), "\n") {
if f := strings.SplitN(strings.TrimSpace(ln), "\t", 2); len(f) == 2 && strings.HasPrefix(f[0], "italic_") {
glyphs = append(glyphs, strings.TrimSpace(f[1]))
}
}
if len(glyphs) == 0 {
t.Fatalf("premise broken: %s declares no glyph, so this test would judge nothing", path)
}
prompts, err := filepath.Glob(filepath.Join(pc.PromptsRoot, pair, "*.md"))
if err != nil || len(prompts) == 0 {
t.Fatalf("pair %s has no prompt pack to teach the glyph to: %v", pair, err)
}
// ⛔ A BULLET IS NOT AN EXPLANATION. With `*` as the glyph, `strings.Contains` is satisfied by every
// markdown list in the file: cutting every sentence that actually teaches the glyph out of the
// pair's prompts left this gate green, which was measured, not imagined. Lines that are list items
// are therefore not evidence, and the glyph must appear where prose can explain it.
taught := 0
for _, pr := range prompts {
b, err := os.ReadFile(pr)
if err != nil {
t.Fatal(err)
}
for _, ln := range strings.Split(string(b), "\n") {
t := strings.TrimSpace(ln)
if strings.HasPrefix(t, glyphs[0]+" ") || strings.HasPrefix(t, "- ") && !strings.Contains(t[2:], glyphs[0]) {
continue // a list item: its leading glyph says "bullet", not "emphasis"
}
if strings.Contains(t, glyphs[0]) {
taught++
break
}
}
}
if taught == 0 {
t.Errorf("pair %s ships the glyph %q and not one of its %d prompts mentions it: the model would meet it with nothing said",
pair, glyphs[0], len(prompts))
}
}
}