textmachine/backend/internal/pipeline/eagerclients_test.go

92 lines
4.1 KiB
Go

package pipeline
import (
"os"
"path/filepath"
"strings"
"testing"
)
// eagerclients_test.go pins that the RUNNER pre-builds a client for every model a run can call — including
// the models only a GATE calls.
//
// ⚠ THE HOLE AND WHY NO TEST SAW IT. `reachableModels()` feeds buildClients, and the clients map is
// read-only during the waves: a miss there is not a fallback, it is the run dying at the moment the gate
// fires — after the draft wave has been bought. Planted, the runner's list was narrowed to stage models
// only (no repair model, no bank roles) and the ENTIRE pipeline package stayed green, mining-stop and
// repair integration tests included. The reason is a coincidence reproduced in fixture form: every fixture
// that enables the terminology gate points it at `fake-model`, which is also the stages' model, exactly as
// pipeline-c1.yaml points its bank role at the draft's model. A test can only see this hole on a book whose
// gate model is its OWN, so this one gives it one.
func TestTheEagerClientMapCoversTheGateModels(t *testing.T) {
srv := newJSONProvider(&reqRec{}, draftEdit)
defer srv.Close()
bookPath := setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true, classify: true})
dir := filepath.Dir(bookPath)
// A second model on the same fake provider, and the two bank roles pointed at it. Everything else about
// the fixture is untouched.
models := filepath.Join(dir, "models.yaml")
raw, err := os.ReadFile(models)
if err != nil {
t.Fatal(err)
}
body := string(raw) + " bank-model:\n provider: fake\n price: { input_per_m: 1.0, cached_per_m: 0.1, cache_write_per_m: 0, output_per_m: 2.0 }\n"
if err := os.WriteFile(models, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
pipePath := filepath.Join(dir, "pipeline.yaml")
praw, err := os.ReadFile(pipePath)
if err != nil {
t.Fatal(err)
}
const gateAnchor = " terminology:\n enabled: true\n model: fake-model\n"
ptxt := string(praw)
if !strings.Contains(ptxt, gateAnchor) {
t.Fatalf("premise broken: the fixture's terminology block moved, so this test is no longer pointing "+
"the gate anywhere:\n%s", ptxt)
}
ptxt = strings.Replace(ptxt, gateAnchor, " terminology:\n enabled: true\n model: bank-model\n", 1)
ptxt = strings.Replace(ptxt, " classify_types: true\n", " classify_types: true\n classify_model: bank-model\n", 1)
if err := os.WriteFile(pipePath, []byte(ptxt), 0o644); err != nil {
t.Fatal(err)
}
r := newRunner(t, bookPath)
defer r.Close()
// Premise: the bank role really does run a model no stage runs. Without this the assertion below is the
// coincidence again, dressed as a test.
stageModels := map[string]bool{}
for _, st := range r.Pipeline.Stages {
stageModels[st.ResolvedModel] = true
}
if r.Pipeline.Gates.Terminology.Model != "bank-model" || stageModels["bank-model"] {
t.Fatalf("premise broken: the bank role must run a model of its own, got gate=%q stages=%v",
r.Pipeline.Gates.Terminology.Model, stageModels)
}
// The precompute pass, run explicitly — it is what TranslateBook does before any wave goroutine starts,
// and running it here keeps the test at $0 while judging the exact map the waves read.
if err := r.buildClients(); err != nil {
t.Fatalf("the precompute pass must build every reachable client: %v", err)
}
for _, m := range r.Pipeline.ReachableModels() {
if _, ok := r.clients[m]; !ok {
t.Errorf("no client was pre-built for %q, which this book can call: the clients map is read-only "+
"during the waves, so the run does not fall back — it dies when that model is first needed, "+
"which for a bank role is after the whole draft wave is paid for", m)
}
}
// And the two bank roles are IN the reachable set to begin with — the list and its consumer, asserted
// together, because each was green while the other was wrong.
reach := map[string]bool{}
for _, m := range r.Pipeline.ReachableModels() {
reach[m] = true
}
for _, m := range []string{r.Pipeline.Gates.Terminology.Model, r.Pipeline.Gates.Terminology.ClassifierModel()} {
if !reach[m] {
t.Errorf("%q is called by a bank role and is not in the reachable set", m)
}
}
}