package pipeline import ( "context" "os" "path/filepath" "regexp" "strings" "testing" "textmachine/backend/internal/config" "textmachine/backend/internal/obs" ) // content_routing_test.go drives the content-label mechanism through the REAL wave driver (rule 11: // sandbox reproductions through the driver, not diff reading). Every label VALUE here is synthetic — // if the engine knew "adult", these tests would not be able to prove genericity. // // The scenarios are the ones the pack owes (D39.26 point 14): a load refusal for a model that may not // receive the label, the runtime assert firing BEFORE money moves, refusal→hop→OK→re-gate, refusal with // a refusing hop → flag+skip, the read path staying alive on a book whose routing is not runnable, and // the rate guard reaching the label-resolved model. const synthLabel = "restricted-synth" // labelled is the fixture knob set for a routable labelled project: the endpoint accepts the synthetic // label, the book declares it, the registry routes it, and the draft stage's label model + the chain // head are both the fixture's fallback slug (a distinct model from the unlabelled primary). func labelledRouting() *labelRouting { return &labelRouting{ acceptsLabels: []string{synthLabel}, bookLabels: []string{synthLabel}, policy: synthLabel, // BOTH differ from the configured pair (model: fake-model, escalate_to: fake-fallback) on purpose: // with the label routing as an identity map, reverting the resolved-model wiring in stagerun / // escalation / snapshot would leave every test green — the mechanism's load-bearing half unguarded. labelModel: "fake-label", chainModel: "fake-labelhop", } } // TestLabelledLoadRefusesIncapableModel is rubezh 1: a book carrying a label the endpoint does not // accept must not load for the money path, and the message must name the model, its provider and the // MISSING label (the operator fixes a data row, so the row has to be identified). func TestLabelledLoadRefusesIncapableModel(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, echoOrClean) defer srv.Close() // The book declares the label; the provider accepts NOTHING → every reachable model is incapable. lbl := labelledRouting() lbl.acceptsLabels = nil bookPath := setupEscalationProject(t, srv.URL, 1.0, lbl) _, err := NewRunner(bookPath, obs.NewLogger()) if err == nil { t.Fatal("a labelled book whose models may not receive the label must NOT load for translate/redrive") } for _, want := range []string{"may not receive content label", synthLabel, "fake", "accepts_labels"} { if !strings.Contains(err.Error(), want) { t.Errorf("load error must mention %q, got: %v", want, err) } } if rec.count() != 0 { t.Errorf("a refused load must make zero provider calls, got %d", rec.count()) } } // TestLabelledReadPathStaysAlive is D39.26 point 9: the SAME unroutable book must remain readable // through the $0 projections. Otherwise the only way to inspect a book labelled after it was paid for // is to strip the label — the silent bypass the mechanism exists to prevent. func TestLabelledReadPathStaysAlive(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, echoOrClean) defer srv.Close() lbl := labelledRouting() lbl.acceptsLabels = nil bookPath := setupEscalationProject(t, srv.URL, 1.0, lbl) r, err := NewReadOnlyRunner(bookPath, obs.NewLogger()) if err != nil { t.Fatalf("read-only projections must survive an unroutable label set (D20.4): %v", err) } defer r.Close() rep, err := r.Status(context.Background()) if err != nil { t.Fatalf("status on an unroutable labelled book must work: %v", err) } // The reason must travel WITH the projection, not only in a log line the operator may never see. if len(rep.ContentLabels) != 1 || rep.ContentLabels[0] != synthLabel { t.Errorf("status must report the book's labels, got %v", rep.ContentLabels) } if len(rep.Routing) != 2 { t.Errorf("status must report one routing row per stage, got %v", rep.Routing) } if len(rep.ContentRoutingProblems) == 0 { t.Error("status must carry the routing problems of an unroutable labelled book") } if rec.count() != 0 { t.Errorf("read-only path must make zero provider calls, got %d", rec.count()) } } // TestLabelledRuntimeAssertPrecedesMoney is rubezh 2. The load-time gate is BYPASSED on purpose (the // runner is built unlabelled, then the book's labels are set post-construction, exactly the trick the // repair tests use to isolate a gate) so the assert inside runAttempt is the only thing left. It must // fire on a FRESH call, be identifiable as an error, and leave the ledger untouched. func TestLabelledRuntimeAssertPrecedesMoney(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() bookPath := setupEscalationProject(t, srv.URL, 1.0, nil) // loads clean: no labels declared anywhere r := newRunner(t, bookPath) defer r.Close() // A label nobody declared as accepted, injected AFTER validation. r.Book.ContentLabels = []string{synthLabel} _, err := r.TranslateBook(context.Background()) if err == nil || !strings.Contains(err.Error(), "refusing to send content labelled") { t.Fatalf("the runtime assert must refuse a model that may not receive the label, got: %v", err) } if rec.count() != 0 { t.Errorf("the assert must fire BEFORE the provider call, got %d call(s)", rec.count()) } committed, reserved, serr := r.Store.SpentUSD(r.Book.BookID) if serr != nil { t.Fatal(serr) } if committed != 0 || reserved != 0 { t.Errorf("the assert must fire BEFORE Reserve: committed=%v reserved=%v, want 0/0", committed, reserved) } } // TestLabelledRefusalEscalatesThroughChainAndRegates is the refusal cascade the repo never had (every // prior "hop cures it" test used cjk_artifact): the label-routed primary REFUSES, the hop comes from the // LABEL'S CHAIN, its answer is re-gated and ships. Money is attributed to the hop. func TestLabelledRefusalEscalatesThroughChainAndRegates(t *testing.T) { rec := &reqRec{} // The primary refuses in the provider's own voice (finish_reason=refusal → hard_refusal, a // deterministic content failure); the chain head answers cleanly. No 18+ text anywhere — the // marker-proxy is the model slug in the request body. srv := newJSONProvider(rec, func(body string) (string, string) { if isEditBody(body) { return "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД", "stop" } if strings.Contains(body, "fake-labelhop") { // the LABEL's chain head, not the configured escalate_to return "Тихое утро в библиотеке.", "stop" } return "Не могу помочь с этим фрагментом.", "refusal" }) defer srv.Close() r := newRunner(t, setupEscalationProject(t, srv.URL, 1.0, labelledRouting())) defer r.Close() res, err := r.TranslateBook(context.Background()) if err != nil { t.Fatalf("translate: %v", err) } if len(res.Chunks) != 1 { t.Fatalf("want 1 unit, got %d", len(res.Chunks)) } oc := res.Chunks[0] if oc.Disposition != DispOK { t.Fatalf("the re-gated hop answer must ship: disposition=%s flag=%s", oc.Disposition, oc.FlagReason) } draft := oc.Stages[0] if !draft.Escalated || draft.EscalationModel != "fake-labelhop" { t.Errorf("the hop must come from the LABEL's chain head (fake-labelhop), NOT the configured escalate_to (fake-fallback): escalated=%v model=%q", draft.Escalated, draft.EscalationModel) } // The WIRE is the only witness that the resolved model actually reached the provider (the mock // canonicalises every response's model field, so modelActual cannot serve): the first draft body must // carry the label's model and the second the label's chain head, and the configured pair must appear // nowhere. This is what makes a revert of the ResolvedModel/ResolvedHop wiring RED. bodies := rec.all() if len(bodies) < 2 { t.Fatalf("want at least 2 request bodies, got %d", len(bodies)) } if !strings.Contains(bodies[0], `"model":"fake-label"`) { t.Errorf("the first draft call must go to the LABEL's model: %s", bodies[0]) } if !strings.Contains(bodies[1], `"model":"fake-labelhop"`) { t.Errorf("the hop must go to the LABEL's chain head: %s", bodies[1]) } for i, b := range bodies[:2] { if strings.Contains(b, `"model":"fake-model"`) || strings.Contains(b, `"model":"fake-fallback"`) { t.Errorf("draft call %d went to a CONFIGURED model although the label re-routes both legs: %s", i, b) } } // And the converse, which is the whole point of the per-stage substitution (D39.26 point 1): the EDIT // stage carries no label_models entry here, so it stays on its CONFIGURED model — a label re-routes // exactly what it claims, never a leg it was not pointed at. if len(bodies) > 2 && !strings.Contains(bodies[2], `"model":"fake-model"`) { t.Errorf("the edit stage has no label route, so it must stay on its configured model: %s", bodies[2]) } // primary refusal + hop + edit = 3 calls (regenerate_before_escalate: 0, so no same-model retry of a // deterministic refusal — the ladder semantics are unchanged). if rec.count() != 3 { t.Errorf("want 3 provider calls (primary refusal → chain hop → edit), got %d", rec.count()) } spent, err := r.Store.EscalationSpentUSD(r.Book.BookID) if err != nil { t.Fatal(err) } if spent <= 0 { t.Errorf("the hop must be booked as escalation spend, got %v", spent) } } // TestLabelledRefusalWithRefusingHopFlagsAndSkips is the terminal branch: a refusal ANY provider repeats // stays flag+skip (D2) — no new "18+ refusal" class, no retry of a deterministic refusal, and the edit // stage is skipped so nothing garbage reaches TM/export. func TestLabelledRefusalWithRefusingHopFlagsAndSkips(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, func(body string) (string, string) { if isEditBody(body) { return "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД", "stop" } return "Не могу помочь с этим фрагментом.", "refusal" // primary AND hop refuse }) defer srv.Close() r := newRunner(t, setupEscalationProject(t, srv.URL, 1.0, labelledRouting())) defer r.Close() res, err := r.TranslateBook(context.Background()) if err != nil { t.Fatalf("a refusal is a disposition, never a run error: %v", err) } oc := res.Chunks[0] if oc.Disposition != DispFlagged || oc.FlagReason != FlagHardRefusal { t.Fatalf("want flagged/hard_refusal, got %s/%s", oc.Disposition, oc.FlagReason) } if oc.FinalText != "" { t.Errorf("a flagged unit must ship nothing (no garbage into TM/export), got %q", oc.FinalText) } if len(oc.Stages) < 2 || oc.Stages[1].Disposition != DispSkipped { t.Errorf("the edit stage must be SKIPPED after a flagged draft, got %+v", oc.Stages) } if rec.count() != 2 { t.Errorf("want exactly 2 calls (primary + one hop, never a retry of a deterministic refusal), got %d", rec.count()) } if res.Flagged != 1 || res.ExitCode() != 2 { t.Errorf("want flagged=1 exit=2, got flagged=%d exit=%d", res.Flagged, res.ExitCode()) } } // TestLabelledRoutingReachesGuardsAndSnapshot is the positive assertion the acceptance добор C asks for // (a rate guard degrades SILENTLY into "unlimited", unlike r.clientFor which is loud): the label-resolved // models must be in the eager client set AND own a rate guard. It also pins that the resolved model — // not the configured one — is what the snapshot folds. func TestLabelledRoutingReachesGuardsAndSnapshot(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, echoOrClean) defer srv.Close() r := newRunner(t, setupEscalationProject(t, srv.URL, 1.0, labelledRouting())) defer r.Close() // The precompute pass is what builds both sets; TranslateBook runs it, so a test that inspects them // without translating must run it too (idempotent). if err := r.buildClients(); err != nil { t.Fatal(err) } r.buildRateGuards() if got := r.Pipeline.Stages[0].ResolvedModel; got != "fake-label" { t.Fatalf("draft must resolve to the label model, got %q", got) } for _, m := range r.Pipeline.ReachableModels() { if _, ok := r.clients[m]; !ok { t.Errorf("reachable model %q has no pre-built client (the wave would fail mid-run)", m) } if r.rateGuard(m) == nil { t.Errorf("reachable model %q has no rate guard — a nil guard degrades SILENTLY to unlimited, unlike the loud client lookup (D39.26 добор C)", m) } } snapID, payload, err := r.snapshotIDForWave(waveDraft) if err != nil { t.Fatal(err) } if !strings.Contains(payload, `"model":"fake-label"`) { t.Errorf("the draft snapshot must fold the RESOLVED model (the assigned snapshot layer for labels, D39.26 point 1): %s", payload) } // The HOP slot too: without this a revert of the escalate_to fold would keep the CONFIGURED fallback in // the snapshot while the run calls another model — a resumed escalated chunk would then serve a // checkpoint paid on a different hop (the silent false-hit the fold exists to prevent). if !strings.Contains(payload, `"escalate_to":"fake-labelhop"`) { t.Errorf("the draft snapshot must fold the RESOLVED hop: %s", payload) } if snapID == "" { t.Error("snapshot id must render") } } // TestLabelledBookWithoutLabelsIsUnchanged is the byte-identity guard in behavioural form: writing the // label DATA into a project the book does not claim must leave routing exactly where the config put it. func TestLabelledBookWithoutLabelsIsUnchanged(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, echoOrClean) defer srv.Close() lbl := labelledRouting() lbl.bookLabels = nil // registry + capability declared, but the BOOK claims no label r := newRunner(t, setupEscalationProject(t, srv.URL, 1.0, lbl)) defer r.Close() st := r.Pipeline.Stages[0] if st.ResolvedModel != st.Model || st.ResolvedHop != st.EscalateTo { t.Fatalf("an unlabelled book must resolve to its configured models, got (%q,%q) want (%q,%q)", st.ResolvedModel, st.ResolvedHop, st.Model, st.EscalateTo) } if len(r.labelsFor()) != 0 { t.Errorf("labelsFor must be empty for an unlabelled book, got %v", r.labelsFor()) } } // TestRetiredAdultKeyForms pins the three shapes of the retired book key: `true` is a migration error // naming the replacement, while `false` and an ABSENT key both load. Tolerance is not a convenience — // 13 book-shaped files outside git carry `adult: false`, and the golden fixture omits the key entirely, // so neither form is exercised by any other test. func TestRetiredAdultKeyForms(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, echoOrClean) defer srv.Close() base := setupEscalationProject(t, srv.URL, 1.0, nil) write := func(t *testing.T, line string) string { t.Helper() raw, err := os.ReadFile(base) if err != nil { t.Fatal(err) } dir := t.TempDir() body := strings.Replace(string(raw), "pipeline: pipeline.yaml", line+"pipeline: "+filepath.Join(filepath.Dir(base), "pipeline.yaml"), 1) body = strings.Replace(body, "models: models.yaml", "models: "+filepath.Join(filepath.Dir(base), "models.yaml"), 1) body = strings.Replace(body, "source_file: source.txt", "source_file: "+filepath.Join(filepath.Dir(base), "source.txt"), 1) p := filepath.Join(dir, "book.yaml") if err := os.WriteFile(p, []byte(body), 0o644); err != nil { t.Fatal(err) } return p } if _, err := NewRunner(write(t, "adult: true\n"), obs.NewLogger()); err == nil || !strings.Contains(err.Error(), "content_labels") { t.Errorf("`adult: true` must fail loud naming content_labels, got: %v", err) } for _, line := range []string{"adult: false\n", ""} { r, err := NewRunner(write(t, line), obs.NewLogger()) if err != nil { t.Errorf("book with %q must load (the key is retired, its zero value is tolerated): %v", strings.TrimSpace(line), err) continue } if len(r.Book.ContentLabels) != 0 { t.Errorf("book with %q must carry no labels, got %v", strings.TrimSpace(line), r.Book.ContentLabels) } r.Close() } } // TestProviderEgressSeamIsSingle is the mechanical guard for the choke point the routing assert sits in: // exactly ONE production call site may reach a provider, plus a NAMED allowlist for the live-gated // conformance probe. A new egress path (a Ф2 judge, an annotator) must extend the assert, not slip past // it — and this test, unlike a grep in a report, cannot be forgotten. func TestProviderEgressSeamIsSingle(t *testing.T) { const allowedTest = "live_conformance_test.go" // live-gated probe, calls the real catalog on purpose // The MODULE root, not just internal/: an egress added under cmd/ would otherwise be invisible. root := filepath.Join("..", "..") // Built from parts so this file's own source cannot match the needle it looks for — the previous // literal форм made the guard exempt itself by name. needle := regexp.MustCompile(`\.` + "Complete" + `\(`) var offenders []string err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() || !strings.HasSuffix(path, ".go") { return nil } // The adapters themselves are the implementation of Complete — they are the seam, not a bypass. if strings.Contains(filepath.ToSlash(path), "/llm/") { return nil } base := filepath.Base(path) // stagerun.go is exempted by its PACKAGE PATH, not by basename, so a same-named file in another // package cannot inherit the exemption. if base == allowedTest || strings.HasSuffix(filepath.ToSlash(path), "internal/pipeline/stagerun.go") { return nil } raw, rerr := os.ReadFile(path) if rerr != nil { return rerr } if needle.Match(raw) { offenders = append(offenders, path) } return nil }) if err != nil { t.Fatal(err) } if len(offenders) > 0 { t.Fatalf("provider egress must stay in ONE place (stagerun.go runAttempt, behind clientFor's label assert); found other call sites: %v — route them through runAttempt or extend the assert deliberately", offenders) } } // TestClientForRefusesUnknownModel keeps the pre-existing loud behaviour of the client lookup intact: // the label assert is an ADDITION to it, not a replacement (an un-enumerated model is still an error, // not a lazy build under a race). func TestClientForRefusesUnknownModel(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, echoOrClean) defer srv.Close() r := newRunner(t, setupEscalationProject(t, srv.URL, 1.0, nil)) defer r.Close() if err := r.buildClients(); err != nil { t.Fatal(err) } if _, err := r.clientFor("never-enumerated", nil); err == nil || !strings.Contains(err.Error(), "no pre-built client") { t.Fatalf("an un-enumerated model must stay a loud error, got: %v", err) } if _, err := r.clientFor("fake-model", nil); err != nil { t.Fatalf("an enumerated model with no labels wanted must resolve: %v", err) } if _, err := r.clientFor("fake-model", []string{synthLabel}); err == nil || !strings.Contains(err.Error(), "refusing to send content labelled") { t.Fatalf("a label the provider does not accept must be refused, got: %v", err) } } // TestSourceEchoExposureWarnsGenerically covers the echo half of the reasoning-off closure (D39.26 добор // B). It is a WARNING by design, so the test asserts the PREDICATE rather than a log line: a stage that // ships the source to a thinking-suppressing model on a CJK source is reported, and the three ways out // (a model that does not suppress, a stage that does not render the source, a non-CJK source) are not. // Reverting ThinksOnWire to "reasoning != off" makes the suppressing case stop being reported. func TestSourceEchoExposureWarnsGenerically(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, echoOrClean) defer srv.Close() r := newRunner(t, setupEscalationProject(t, srv.URL, 1.0, nil)) defer r.Close() // Baseline: the fixture's provider has no off-switch capability, so reasoning "off" is a NO-OP — // thinking stays on and nothing is exposed (this is the deepseek shape of the shipping stack). if got := r.sourceEchoExposure(); len(got) != 0 { t.Fatalf("a model with no off-switch does not suppress thinking, want no exposure, got %v", got) } // Give the model a real off-switch: now reasoning "off" reaches the wire and both source-bearing // stages (translator AND the bilingual editor — D30.1) are exposed. mod := r.Models.Models["fake-model"] mod.Capabilities = &config.CapabilitiesConfig{Reasoning: &config.ReasoningCapCfg{Control: "effort", OffEffort: "none"}} r.Models.Models["fake-model"] = mod got := r.sourceEchoExposure() if len(got) != 2 { t.Fatalf("both source-bearing stages must be reported (the editor is bilingual since D30.1), got %v", got) } for _, want := range []string{"draft→fake-model", "edit→fake-model"} { if !strings.Contains(strings.Join(got, ","), want) { t.Errorf("exposure %v must contain %q", got, want) } } // A non-CJK source is out of the measured class → no warning, no noise. r.Book.SourceLang = "en" if got := r.sourceEchoExposure(); len(got) != 0 { t.Errorf("a non-CJK source must not be reported, got %v", got) } } // TestRoutingMoneyProvenanceIsDerived covers the two read-only aggregates B6 added (EscalationHops, // SpendByModel): both answer "what did the label-routed endpoint cost" without a schema migration and // without a synthetic call class, so both are derived from durable checkpoints — and both were otherwise // untested, i.e. a future column rename would turn the money report into a silent zero. func TestRoutingMoneyProvenanceIsDerived(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, func(body string) (string, string) { if isEditBody(body) { return "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД", "stop" } if strings.Contains(body, "fake-labelhop") { return "Тихое утро в библиотеке.", "stop" } return "Не могу помочь с этим фрагментом.", "refusal" }) defer srv.Close() r := newRunner(t, setupEscalationProject(t, srv.URL, 1.0, labelledRouting())) defer r.Close() if _, err := r.TranslateBook(context.Background()); err != nil { t.Fatalf("translate: %v", err) } hops, err := r.Store.EscalationHops(r.Book.BookID) if err != nil { t.Fatal(err) } if hops != 1 { t.Errorf("EscalationHops = %d, want 1 — the per-unit `escalated` boolean cannot count hops, which is why this exists", hops) } byModel, err := r.Store.SpendByModel(r.Book.BookID) if err != nil { t.Fatal(err) } // One line per model the run actually paid: the label's primary (refused, still billed), the label's // chain head (the hop) and the configured editor the label does not re-route. for _, want := range []string{"fake-label", "fake-labelhop", "fake-model"} { if byModel[want] <= 0 { t.Errorf("SpendByModel is missing a positive line for %q: %v", want, byModel) } } if _, unexpected := byModel["fake-fallback"]; unexpected { t.Errorf("the configured escalate_to was substituted by the label chain and never called, so it must not appear: %v", byModel) } // The quality report is the surface that ships them; it must agree with the store. q, err := r.QualityReport() if err != nil { t.Fatal(err) } if q.EscalationHops != hops || len(q.SpendByModel) != len(byModel) { t.Errorf("quality report money provenance diverged from the store: hops=%d/%d models=%d/%d", q.EscalationHops, hops, len(q.SpendByModel), len(byModel)) } if len(q.ContentLabels) != 1 || len(q.Routing) != 2 { t.Errorf("quality report must carry the label provenance, got labels=%v routing=%v", q.ContentLabels, q.Routing) } } // TestEditorOnlyLabelLeavesTheDraftWaveUntouched is the MONEY claim of D39.26 point 1, asserted on the // artefact that decides it: a label that re-routes ONLY the editor must leave the DRAFT-wave snapshot id // byte-identical to the unlabelled book's, so an already-paid draft still resumes at $0. An earlier // version of this pack substituted the label's chain hop on every escalating stage, which silently moved // the draft-wave snapshot (its folded escalate_to changed) and re-billed a wave nobody asked to change. func TestEditorOnlyLabelLeavesTheDraftWaveUntouched(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, echoOrClean) defer srv.Close() plain := newRunner(t, setupEscalationProject(t, srv.URL, 1.0, nil)) defer plain.Close() plainDraft, _, err := plain.snapshotIDForWave(waveDraft) if err != nil { t.Fatal(err) } plainEdit, _, err := plain.snapshotIDForWave(waveEdit) if err != nil { t.Fatal(err) } lbl := labelledRouting() lbl.labelModel = "" // the label claims the EDITOR only lbl.editModel = "fake-label" labelled := newRunner(t, setupEscalationProject(t, srv.URL, 1.0, lbl)) defer labelled.Close() if got := labelled.Pipeline.Stages[0].ResolvedHop; got != "fake-fallback" { t.Errorf("the draft stage keeps its CONFIGURED fallback when the label does not claim it, got %q", got) } if got := labelled.Pipeline.Stages[1].ResolvedModel; got != "fake-label" { t.Fatalf("the edit stage must resolve to the label model, got %q", got) } labelledDraft, _, err := labelled.snapshotIDForWave(waveDraft) if err != nil { t.Fatal(err) } labelledEdit, _, err := labelled.snapshotIDForWave(waveEdit) if err != nil { t.Fatal(err) } if labelledDraft != plainDraft { t.Errorf("an editor-only label moved the DRAFT-wave snapshot (%.12s → %.12s) — the paid draft would be re-billed, against D39.26 point 1", plainDraft, labelledDraft) } if labelledEdit == plainEdit { t.Errorf("the EDIT-wave snapshot must move (%.12s) — the editor is called on another model, so its checkpoints are not the same calls", labelledEdit) } } // TestAssertOnlyLabelRunsWithoutMovingAnything drives the third policy shape through the REAL driver: a // book whose label only ASSERTS must run exactly like the unlabelled one — same wire, same snapshots, // same money — while the capability invariant still guards every call. It needs no escalation budget and // no chain, which is the whole reason the shape exists. func TestAssertOnlyLabelRunsWithoutMovingAnything(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() plain := newRunner(t, setupEscalationProject(t, srv.URL, 0, nil)) plainDraft, _, err := plain.snapshotIDForWave(waveDraft) if err != nil { t.Fatal(err) } plainEdit, _, err := plain.snapshotIDForWave(waveEdit) if err != nil { t.Fatal(err) } plainRes, err := plain.TranslateBook(context.Background()) if err != nil { t.Fatalf("unlabelled translate: %v", err) } plainBodies := append([]string(nil), rec.all()...) plain.Close() // Same project, plus a label whose policy only asserts (no chain, no label_models, budget 0). rec2 := &reqRec{} srv2 := newJSONProvider(rec2, draftEdit) defer srv2.Close() r := newRunner(t, setupEscalationProject(t, srv2.URL, 0, &labelRouting{ acceptsLabels: []string{synthLabel}, bookLabels: []string{synthLabel}, policy: synthLabel, action: "allow", })) defer r.Close() res, err := r.TranslateBook(context.Background()) if err != nil { t.Fatalf("an asserting label must not disturb the run: %v", err) } if res.Chunks[0].Disposition != DispOK || res.TotalUSD != plainRes.TotalUSD { t.Errorf("run diverged: disposition=%s cost=%v want ok/%v", res.Chunks[0].Disposition, res.TotalUSD, plainRes.TotalUSD) } // The wire must be identical body for body — an assertion is not a wire change. bodies := rec2.all() if len(bodies) != len(plainBodies) { t.Fatalf("call count changed: %d vs %d", len(bodies), len(plainBodies)) } for i := range bodies { if bodies[i] != plainBodies[i] { t.Errorf("call %d body differs under an asserting label:\n got %s\nwant %s", i, bodies[i], plainBodies[i]) } } // …and so must both snapshots: an asserting label moves NO route, so it can re-bill nothing. draft, _, err := r.snapshotIDForWave(waveDraft) if err != nil { t.Fatal(err) } edit, _, err := r.snapshotIDForWave(waveEdit) if err != nil { t.Fatal(err) } if draft != plainDraft || edit != plainEdit { t.Errorf("an asserting label moved a snapshot (draft %.12s→%.12s, edit %.12s→%.12s) — it changes no model, so it must re-bill nothing", plainDraft, draft, plainEdit, edit) } // The invariant is still the point: strip the capability and the SAME book must refuse to run. incapable := setupEscalationProject(t, srv2.URL, 0, &labelRouting{ bookLabels: []string{synthLabel}, policy: synthLabel, action: "allow", }) if _, err := NewRunner(incapable, obs.NewLogger()); err == nil || !strings.Contains(err.Error(), "may not receive content label") { t.Fatalf("an asserting label must still refuse incapable endpoints, got %v", err) } }