textmachine/backend/internal/config/terminology_classify_test.go

117 lines
3.4 KiB
Go

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
}
const validClassifyGate = `
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)
}
}
}