162 lines
5.9 KiB
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
|
|
}
|