140 lines
4.8 KiB
Go
140 lines
4.8 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// writeTmp writes a file under a temp path and returns it.
|
|
func writeTmp(t *testing.T, path, body string) string {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
// miniModels loads a minimal valid models.yaml with a single `fake` model.
|
|
func miniModels(t *testing.T) *Models {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
body := "prices_checked: " + time.Now().UTC().Format("2006-01-02") + `
|
|
default_model: fake
|
|
providers:
|
|
p: { kind: openai, base_url: http://x }
|
|
models:
|
|
fake: { provider: p, price: { input_per_m: 1, output_per_m: 2 } }
|
|
`
|
|
m, err := LoadModels(writeTmp(t, filepath.Join(dir, "models.yaml"), body))
|
|
if err != nil {
|
|
t.Fatalf("miniModels: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// prompt_pack_test.go pins the pair-keyed prompt seam (D39 слой 2, L4-prompts-not-per-pair-zh-baked):
|
|
// a stage resolves its prompt from a pair-keyed pack by the book's LangPair(), FAIL-LOUD on a missing
|
|
// pair; a legacy single `prompt` stays pair-agnostic; prod zh→ru resolves the same file (SHA-neutral).
|
|
|
|
func TestPromptPathForResolvesPair(t *testing.T) {
|
|
st := Stage{Name: "edit", Prompts: map[string]string{"zh-ru": "/x/editor.md", "ja-ru": "/y/editor.md"}}
|
|
got, err := st.PromptPathFor("zh-ru")
|
|
if err != nil || got != "/x/editor.md" {
|
|
t.Fatalf("PromptPathFor(zh-ru) = %q, %v; want /x/editor.md, nil", got, err)
|
|
}
|
|
}
|
|
|
|
func TestPromptPathForFailsLoudOnMissingPair(t *testing.T) {
|
|
st := Stage{Name: "edit", Prompts: map[string]string{"zh-ru": "/x/editor.md"}}
|
|
_, err := st.PromptPathFor("ja-ru")
|
|
if err == nil {
|
|
t.Fatal("PromptPathFor(ja-ru) must FAIL LOUD when the pack has no ja-ru (the latent ja-runs-zh-prompt bug)")
|
|
}
|
|
// The error must name the missing pair AND list what the pack has (operator-actionable).
|
|
if !strings.Contains(err.Error(), "ja-ru") || !strings.Contains(err.Error(), "zh-ru") {
|
|
t.Errorf("fail-loud error must name the missing pair and the pack's pairs, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPromptPathForLegacySingleIsPairAgnostic(t *testing.T) {
|
|
st := Stage{Name: "draft", Prompt: "/x/translator.md"}
|
|
for _, pair := range []string{"zh-ru", "ja-ru", "en-ru"} {
|
|
got, err := st.PromptPathFor(pair)
|
|
if err != nil || got != "/x/translator.md" {
|
|
t.Errorf("legacy single prompt must resolve for %q: got %q, %v", pair, got, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadPipelineRejectsAmbiguousPromptForms(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeTmp(t, filepath.Join(dir, "translator.md"), "sys\n---USER---\n{{text}}")
|
|
models := miniModels(t)
|
|
|
|
// Both `prompt` and `prompts` set → rejected.
|
|
both := writeTmp(t, filepath.Join(dir, "both.yaml"), `
|
|
core: C1
|
|
stages:
|
|
- name: draft
|
|
role: translator
|
|
model: fake
|
|
prompt: translator.md
|
|
prompts: { zh-ru: translator.md }
|
|
prompt_version: v1
|
|
`)
|
|
if _, err := LoadPipeline(both, models); err == nil || !strings.Contains(err.Error(), "not both") {
|
|
t.Errorf("both prompt+prompts must be rejected, got: %v", err)
|
|
}
|
|
|
|
// Neither set → rejected.
|
|
neither := writeTmp(t, filepath.Join(dir, "neither.yaml"), `
|
|
core: C1
|
|
stages:
|
|
- name: draft
|
|
role: translator
|
|
model: fake
|
|
prompt_version: v1
|
|
`)
|
|
if _, err := LoadPipeline(neither, models); err == nil || !strings.Contains(err.Error(), "prompt is required") {
|
|
t.Errorf("neither prompt nor prompts must be rejected, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestBoevoyPipelineC1ResolvesZhRu proves the migrated prod config is behaviour-neutral for zh→ru:
|
|
// the draft/edit stages resolve the same translator.md/editor.md the legacy path did (same file →
|
|
// same PromptSHA256), and a NON-zh pair fails loud.
|
|
func TestBoevoyPipelineC1ResolvesZhRu(t *testing.T) {
|
|
models, err := LoadModels(filepath.Join("..", "..", "configs", "models.yaml"))
|
|
if err != nil {
|
|
t.Fatalf("load models: %v", err)
|
|
}
|
|
p, err := LoadPipeline(filepath.Join("..", "..", "configs", "pipeline-c1.yaml"), models)
|
|
if err != nil {
|
|
t.Fatalf("load pipeline-c1: %v", err)
|
|
}
|
|
for _, st := range p.Stages {
|
|
zh, err := st.PromptPathFor("zh-ru")
|
|
if err != nil {
|
|
t.Errorf("stage %q must resolve zh-ru: %v", st.Name, err)
|
|
}
|
|
if !strings.HasSuffix(zh, ".md") {
|
|
t.Errorf("stage %q zh-ru prompt should be a .md template, got %q", st.Name, zh)
|
|
}
|
|
// A book of a pair the pack does not carry must fail loud (closes the ja-runs-zh-prompt bug).
|
|
if _, err := st.PromptPathFor("ja-ru"); err == nil {
|
|
t.Errorf("stage %q must fail loud for ja-ru (pack is zh-ru only)", st.Name)
|
|
}
|
|
}
|
|
// The draft stage must resolve to the shared translator.md (the zh→ru pack content is unchanged).
|
|
for _, st := range p.Stages {
|
|
if st.Role == "translator" {
|
|
zh, _ := st.PromptPathFor("zh-ru")
|
|
if !strings.HasSuffix(zh, filepath.Join("prompts", "translator.md")) {
|
|
t.Errorf("draft zh-ru must be prompts/translator.md, got %q", zh)
|
|
}
|
|
}
|
|
}
|
|
}
|