textmachine/backend/internal/pipeline/volumeledger_test.go

162 lines
5.9 KiB
Go

package pipeline
import (
"bufio"
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
"textmachine/backend/internal/obs"
"textmachine/backend/internal/runevents"
)
// volumeledger_test.go: A5 — a volume stop had no MACHINE carrier at all, only a prose line, and prose is
// the one channel the platform reads nowhere.
//
// Ratified D39.181 п.2: the fact travels as NUMBERS on the terminal frame, and the field's PRESENCE is the
// boolean. Not as a new outcome word — that would make the exit code and the stream name different
// outcomes for one run — and not as a new exit code, because the top-level band is frozen.
// readFinished returns the terminal frame of the book's journal, and fails loudly if there is none: a
// stream without it is a run that did not end on purpose, which would make every assertion below vacuous.
func readFinished(t *testing.T, dir string) runevents.Finished {
t.Helper()
f, err := os.Open(filepath.Join(dir, "events.jsonl"))
if err != nil {
t.Fatalf("the run must have written a journal: %v", err)
}
defer f.Close()
var fin *runevents.Finished
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 0, 1<<20), 1<<20)
for sc.Scan() {
var env runevents.Envelope
if err := json.Unmarshal(sc.Bytes(), &env); err != nil {
t.Fatalf("unreadable journal line %q: %v", sc.Text(), err)
}
if env.Type != runevents.TypeFinished {
continue
}
var got runevents.Finished
if err := json.Unmarshal(env.Data, &got); err != nil {
t.Fatalf("unreadable finished payload: %v", err)
}
fin = &got
}
if fin == nil {
t.Fatal("no terminal frame: the run did not end on purpose, so there is nothing to assert about")
}
return *fin
}
// TestAVolumeStopReachesTheStreamAsNumbers is the landing. A grant smaller than the book must leave a
// delivery ledger on the terminal frame; the outcome word must stay exactly what it was.
//
// Mutation this catches: drop the Volume field from the emitted frame (or return nil from volumeLedger)
// and the run becomes machine-identical to one that finished the whole book — which is the defect.
func TestAVolumeStopReachesTheStreamAsNumbers(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := volumeBook(t, srv.URL, 4)
dir := filepath.Dir(bookPath)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r := newRunner(t, bookPath)
defer r.Close()
r.MaxUnits = 2
res, err := r.TranslateBook(ctx)
if err != nil {
t.Fatal(err)
}
if res.Volume == nil {
t.Fatal("premise: a grant of 2 on a 4-unit book must stop on the volume ceiling")
}
fin := readFinished(t, dir)
if fin.Outcome != runevents.OutcomeClean {
t.Fatalf("a volume stop is a COMPLETION: the outcome word must not change, got %q", fin.Outcome)
}
if fin.Volume == nil {
t.Fatal("a run that stopped because its grant ran out is machine-identical to one that finished the " +
"whole book unless it says so — and the only thing that said so was a prose line the platform " +
"reads nowhere (A5)")
}
got := *fin.Volume
if got.MaxUnits != 2 || got.Delivered != 2 || got.Reworked != 0 {
t.Fatalf("the ledger must say what the grant bought: %+v", got)
}
if got.LeftFresh != 2 || got.LeftRework != 0 {
t.Fatalf("two units were never delivered and none awaits a re-make: %+v", got)
}
// The consumer must not have to reproduce reconcile()'s arithmetic to learn what was PAID: every term
// of it is on the frame.
if paid := got.Delivered + got.Reworked + got.Flagged; paid != 2 {
t.Fatalf("delivered+reworked+flagged must be the units actually paid for, got %d from %+v", paid, got)
}
}
// TestAnOrdinaryCompletionCarriesNoLedger is what makes the field's PRESENCE meaningful. Without this a
// reader could not use presence as the signal, and the whole design (numbers instead of a new word) would
// collapse back into needing one.
func TestAnOrdinaryCompletionCarriesNoLedger(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := volumeBook(t, srv.URL, 2)
dir := filepath.Dir(bookPath)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r := newRunner(t, bookPath)
defer r.Close()
r.MaxUnits = 10 // larger than the book: an ordinary completion
res, err := r.TranslateBook(ctx)
if err != nil {
t.Fatal(err)
}
if res.Volume != nil {
t.Fatalf("premise: a grant larger than the book is an ordinary completion, got %+v", res.Volume)
}
if fin := readFinished(t, dir); fin.Volume != nil {
t.Fatalf("a run that reached the end of the book must NOT look like one held back by a grant — "+
"presence is the signal, so a ledger here would make the signal meaningless: %+v", fin.Volume)
}
}
// TestTheLedgerIsOmittedNotNulled pins the wire shape a consumer decodes against: an absent ledger must be
// an ABSENT KEY, so a reader that checks for the field's presence is not defeated by a `"volume": null`.
func TestTheLedgerIsOmittedNotNulled(t *testing.T) {
line, err := runevents.Line(1, runevents.TypeFinished, time.Unix(1700000000, 0).UTC(), runevents.Finished{Outcome: runevents.OutcomeClean})
if err != nil {
t.Fatal(err)
}
if got := string(line); jsonHasKey(t, got, "volume") {
t.Fatalf("a run with no volume stop must omit the key entirely, not send null: %s", got)
}
withLedger, err := runevents.Line(1, runevents.TypeFinished, time.Unix(1700000000, 0).UTC(),
runevents.Finished{Outcome: runevents.OutcomeClean, Volume: &runevents.VolumeLedger{MaxUnits: 3}})
if err != nil {
t.Fatal(err)
}
if !jsonHasKey(t, string(withLedger), "volume") {
t.Fatalf("a run held back by its grant must carry the key: %s", string(withLedger))
}
}
func jsonHasKey(t *testing.T, line, key string) bool {
t.Helper()
var env runevents.Envelope
if err := json.Unmarshal([]byte(line), &env); err != nil {
t.Fatal(err)
}
var m map[string]json.RawMessage
if err := json.Unmarshal(env.Data, &m); err != nil {
t.Fatal(err)
}
_, ok := m[key]
return ok
}