91 lines
4 KiB
Go
91 lines
4 KiB
Go
package config
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCheckRunnableRejectsPhase2Mechanics(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
p Pipeline
|
|
want string // substring the error must mention
|
|
}{
|
|
{"c1 ok", Pipeline{Core: "C1", Stages: []Stage{{Name: "draft", Role: "translator"}}}, ""},
|
|
{"c0 ok", Pipeline{Core: "C0", Stages: []Stage{{Name: "draft", Role: "translator"}}}, ""},
|
|
{"c2 rejected", Pipeline{Core: "C2", Stages: []Stage{{Name: "draft", Role: "translator"}}}, "C2"},
|
|
{"fanout rejected", Pipeline{Core: "C1", Fanout: Fanout{Candidates: 3}, Stages: []Stage{{Name: "draft", Role: "translator"}}}, "fanout"},
|
|
{"judge rejected", Pipeline{Core: "C1", Stages: []Stage{{Name: "select", Role: "judge"}}}, "judge"},
|
|
// The coverage gate is EXECUTABLE from Milestone 2.5 — CheckRunnable no longer
|
|
// rejects it (threshold sanity is LoadPipeline's job, not a mechanics gate).
|
|
{"enabled gate runnable", Pipeline{Core: "C1", Gates: Gates{Coverage: CoverageGate{Enabled: true}}, Stages: []Stage{{Name: "draft", Role: "translator"}}}, ""},
|
|
}
|
|
for _, c := range cases {
|
|
err := c.p.CheckRunnable()
|
|
if c.want == "" {
|
|
if err != nil {
|
|
t.Errorf("%s: expected runnable, got %v", c.name, err)
|
|
}
|
|
continue
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), c.want) {
|
|
t.Errorf("%s: expected error mentioning %q, got %v", c.name, c.want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// (TestCheckAdultChannel — the D20.2-Q3 "adult:true needs a channel:adult stage" lint — was DELETED
|
|
// with the mechanism it covered: the engine no longer knows a content type, and its successor invariant
|
|
// is stronger and generic (every model a labelled run can call must accept every label of the book).
|
|
// Its replacement lives in content_labels_test.go, exercised on a SYNTHETIC label value.)
|
|
|
|
func TestCheckKeysOnlyUsedNonLocalModels(t *testing.T) {
|
|
m := &Models{
|
|
Providers: map[string]Provider{
|
|
"cloud": {Kind: "openai", BaseURL: "http://x", APIKeyEnv: "TEST_CLOUD_KEY"},
|
|
"local": {Kind: "local", BaseURL: "http://127.0.0.1", Model: "q"},
|
|
},
|
|
Models: map[string]Model{
|
|
"cloud-model": {Provider: "cloud"},
|
|
"local-model": {Provider: "local"},
|
|
"unused-cloud": {Provider: "cloud"},
|
|
},
|
|
}
|
|
// local-only pipeline must not fail on a cloud key.
|
|
localPipe := &Pipeline{Stages: []Stage{{Model: "local-model"}}}
|
|
if err := m.CheckKeys(localPipe); err != nil {
|
|
t.Fatalf("local-only pipeline must not require cloud keys: %v", err)
|
|
}
|
|
// Pipeline with a cloud model but no key — fail-fast.
|
|
t.Setenv("TEST_CLOUD_KEY", "")
|
|
cloudPipe := &Pipeline{Stages: []Stage{{Model: "cloud-model"}}}
|
|
if err := m.CheckKeys(cloudPipe); err == nil || !strings.Contains(err.Error(), "TEST_CLOUD_KEY") {
|
|
t.Fatalf("missing cloud key must fail-fast mentioning the env var, got %v", err)
|
|
}
|
|
// Key is set — passes.
|
|
t.Setenv("TEST_CLOUD_KEY", "sk-xxx")
|
|
if err := m.CheckKeys(cloudPipe); err != nil {
|
|
t.Fatalf("present key must pass: %v", err)
|
|
}
|
|
// Escalation keys are NOT required while escalation is off (budget_usd=0,
|
|
// opt-in). An enabled coverage gate does NOT make escalation reachable — the gate
|
|
// does not call models (self-review: otherwise enabling the gate would block a valid
|
|
// deepseek+glm run because of the stub chain's missing Kimi key).
|
|
t.Setenv("TEST_CLOUD_KEY", "")
|
|
escOffPipe := &Pipeline{
|
|
Stages: []Stage{{Model: "local-model"}},
|
|
Escal: Escalation{Chains: map[string][]string{"default": {"cloud-model"}}},
|
|
Gates: Gates{Coverage: CoverageGate{Enabled: true}}, // gate ON, budget 0
|
|
}
|
|
if err := m.CheckKeys(escOffPipe); err != nil {
|
|
t.Fatalf("enabling the coverage gate must NOT require escalation keys at budget 0: %v", err)
|
|
}
|
|
// At budget_usd>0 escalation is reachable → the chain key is required.
|
|
escOnPipe := &Pipeline{
|
|
Stages: []Stage{{Model: "local-model"}},
|
|
Escal: Escalation{Chains: map[string][]string{"default": {"cloud-model"}}, BudgetUSD: 1.0},
|
|
}
|
|
if err := m.CheckKeys(escOnPipe); err == nil {
|
|
t.Fatal("escalation chain model without a key must fail-fast when budget_usd>0")
|
|
}
|
|
}
|