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) } } }