package config import ( "path/filepath" "strings" "testing" ) // terminology_classify_test.go pins the fix-pack §г config branches for the §2 classifier PHASE. The // classifier is a phase of the terminology gate, so — like the render phase — an enabled phase must be able to // fire (a resolvable non-additive model, a positive budget, its own prompt), and classify_types under a // DISABLED gate is a loud error rather than the silent no-op it was (runClassifier is reached only through the // enabled gate). // termProject writes a zh-ru pack with a terminologist prompt and, optionally, a classifier prompt. func termProject(t *testing.T, withClassifier bool) string { t.Helper() dir := promptProject(t, "zh-ru") body := "sys\n---USER---\n{{text}}" writeTmp(t, filepath.Join(dir, "prompts", "zh-ru", "terminologist.md"), body) if withClassifier { writeTmp(t, filepath.Join(dir, "prompts", "zh-ru", "classifier.md"), body) } return dir } // loadClassify assembles a config with the given terminology-gate body and loads it for zh-ru. func loadClassify(t *testing.T, withClassifier bool, gateBody string) error { t.Helper() dir := termProject(t, withClassifier) cfg := draftOnlyConfig(t, dir, gateBody) _, err := LoadPipeline(cfg, miniModels(t), "zh-ru", nil) return err } // ⚠ `mining:` IS PART OF A VALID GATE NOW (backlog row 140). Both bank roles run inside the bank-mining // stop, which returns early without a contrast artifact, so an enabled gate with no `contrast_path` is a // gate that can never fire — the class this loader refuses everywhere else. The fixture declares it. const validClassifyGate = ` mining: contrast_path: contrast.txt gates: terminology: enabled: true model: fake budget_usd: 1 target_script: Cyrillic classify_types: true classify_budget_usd: 0.5 ` func TestClassifyValidConfigLoads(t *testing.T) { if err := loadClassify(t, true, validClassifyGate); err != nil { t.Fatalf("a well-formed classify config must load: %v", err) } } func TestClassifyMissingPromptFailsLoud(t *testing.T) { err := loadClassify(t, false, validClassifyGate) // no classifier.md written if err == nil { t.Fatal("classify_types on with no classifier.md must fail loud") } for _, want := range []string{"classify_types", filepath.Join("zh-ru", "classifier.md")} { if !strings.Contains(err.Error(), want) { t.Errorf("error must name %q, got: %v", want, err) } } } func TestClassifyBudgetMustBePositive(t *testing.T) { body := ` gates: terminology: enabled: true model: fake budget_usd: 1 target_script: Cyrillic classify_types: true classify_budget_usd: 0 ` err := loadClassify(t, true, body) if err == nil || !strings.Contains(err.Error(), "classify_budget_usd") { t.Fatalf("a zero classify budget must fail loud (a phase that can never spend), got: %v", err) } } func TestClassifyModelMustExist(t *testing.T) { body := ` gates: terminology: enabled: true model: fake budget_usd: 1 target_script: Cyrillic classify_types: true classify_budget_usd: 0.5 classify_model: ghost ` err := loadClassify(t, true, body) if err == nil || !strings.Contains(err.Error(), "classify_model") { t.Fatalf("an unknown classify_model must fail loud, got: %v", err) } } // The fix-pack §г hygiene: classify_types under a disabled gate was a silent no-op — now a loud error. func TestClassifyTypesRequiresEnabledGate(t *testing.T) { body := ` gates: terminology: enabled: false classify_types: true ` err := loadClassify(t, true, body) if err == nil { t.Fatal("classify_types with the gate disabled must fail loud, not run as a silent no-op") } for _, want := range []string{"classify_types", "enabled"} { if !strings.Contains(err.Error(), want) { t.Errorf("error must name %q, got: %v", want, err) } } }