208 lines
9 KiB
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))
|
|
}
|
|
}
|
|
}
|