84 lines
4.4 KiB
Go
84 lines
4.4 KiB
Go
package pipeline
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"textmachine/backend/internal/config"
|
||
)
|
||
|
||
// placeholderplacement_test.go pins WHERE the load gate looks.
|
||
//
|
||
// ⚠ LoadPromptTemplate's own comment says «the whole canonical text is scanned, not the parts, so a marker
|
||
// in a few-shot block or in the user tail is caught too». Nothing held that. Planted — the scan narrowed to
|
||
// the system part — the whole pipeline package stayed green, because every existing fixture puts its bad
|
||
// marker in the system half. Two independent adversarial passes reached this same square from different
|
||
// directions. A retired placeholder sitting after `---USER---` therefore loaded clean and failed inside
|
||
// Render, mid-run, after the wave that stage sits behind had been bought — which is the one failure this
|
||
// gate exists to prevent, and the placement an author is MOST likely to produce: the user tail is where the
|
||
// per-chunk variables live.
|
||
func TestTheLoadGateScansEveryPartOfTheFile(t *testing.T) {
|
||
for _, c := range []struct {
|
||
name, tpl, want string
|
||
}{
|
||
{"in the system part", "Роль {{chapter_title}}.\n---USER---\n{{text}}", "{{chapter_title}}"},
|
||
{"in the user tail", "Роль.\n---USER---\n{{chapter_title}}\n{{text}}", "{{chapter_title}}"},
|
||
{"in the few-shot block", "Роль.\n---FEWSHOT---\nПример: {{chapter_title}}\n---USER---\n{{text}}", "{{chapter_title}}"},
|
||
// The unterminated class, which the renderer refuses and the load gate must refuse FIRST — same
|
||
// reason, same moment. Planted separately, removing it also survived everything.
|
||
{"unterminated in the system part", "Роль {{chapter_title.\n---USER---\n{{text}}", "unterminated"},
|
||
{"unterminated in the user tail", "Роль.\n---USER---\n{{text}} и {{хвост", "unterminated"},
|
||
} {
|
||
t.Run(c.name, func(t *testing.T) {
|
||
path := filepath.Join(t.TempDir(), "p.md")
|
||
if err := os.WriteFile(path, []byte(c.tpl), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_, err := LoadPromptTemplate(path)
|
||
if err == nil {
|
||
t.Fatalf("a marker the engine cannot render must be refused at LOAD wherever it sits in the "+
|
||
"file — this one loaded clean and would have failed inside Render, after the wave: %q", c.tpl)
|
||
}
|
||
if !strings.Contains(err.Error(), c.want) {
|
||
t.Errorf("the refusal must name what is wrong (%q), got: %v", c.want, err)
|
||
}
|
||
})
|
||
}
|
||
|
||
// The control: a template using only real placeholders, in all three parts, must LOAD. Without it the
|
||
// assertions above would also pass on a gate that refused every file.
|
||
good := "Роль для {{title}}.\n---FEWSHOT---\nПример на {{target_lang}}\n---USER---\n{{text}}\n{{draft}}"
|
||
path := filepath.Join(t.TempDir(), "ok.md")
|
||
if err := os.WriteFile(path, []byte(good), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := LoadPromptTemplate(path); err != nil {
|
||
t.Fatalf("a template using only real placeholders must load, in every part of the file: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestTheLoadGateAcceptsExactlyWhatTheRendererDoes is the direction that costs money, and it is asserted by
|
||
// CONSTRUCTION rather than by example. The old validator kept its own copy of the accepted set, so the two
|
||
// could disagree; the set-equality test that was supposed to catch that walked the RENDERER's table and
|
||
// could only ever prove renderer ⊆ validator. The expensive direction is the other one — a name the gate
|
||
// blesses and the renderer refuses loads clean and dies mid-run — and it was pinned against four literal
|
||
// words, so a fifth word (planted: «chapter») walked straight through.
|
||
//
|
||
// The check below cannot be satisfied by a list: it asks the gate about a name the renderer does not have,
|
||
// and the only way to pass is for the gate to consult the renderer.
|
||
func TestTheLoadGateAcceptsExactlyWhatTheRendererDoes(t *testing.T) {
|
||
for _, name := range []string{"chapter", "chapter_title", "genre", "book", "Text", "TEXT", "tex", "текст"} {
|
||
if err := CheckPlaceholders("{{" + name + "}}"); err == nil {
|
||
t.Errorf("{{%s}} is not a placeholder the renderer substitutes, and the load gate accepted it: "+
|
||
"the pack loads and the run dies inside Render, after the wave", name)
|
||
}
|
||
}
|
||
// And every name the renderer DOES substitute must load — the gate must not be strict by being wrong.
|
||
for name := range renderVals(RenderVars{Book: &config.Book{}}) {
|
||
if err := CheckPlaceholders("{{" + name + "}}"); err != nil {
|
||
t.Errorf("{{%s}} is substituted by the renderer and the load gate refused it: %v", name, err)
|
||
}
|
||
}
|
||
}
|