package pipeline import ( "reflect" "testing" ) // projectBankProposals is a pure fold, and the property that matters about it — no deciding field is // dropped, swapped with its neighbour or rescaled on the way to the sidecar — is a property of the fold // alone. It gets its own test here rather than riding on the stop's integration fixture, because that // fixture's values are whatever one run happens to produce: 0 for the counters a small corpus never // raises, empty for both string lists, false for a rendering the drafts did propose. Compared against a // row of that shape, a swap reads as `0 != 0` and a tenfold rescale as `!= 0` — the assertion passes // while the projection lies. Measured on 08.09: four mutations planted at once (Freq×10, Invented forced // false, Contradicts↔BankHolds swapped) left the whole package green in 16.8 s. // // So the first row below gives EVERY field a value that is distinguishable from every other field's: the // two string lists differ in length AND in content, Freq/Spread/Conf are three different non-zero numbers, // Kind and Channel are two different strings, and the rendering is one no draft proposed. // // And the table is TWO rows, not one, for two reasons a single row cannot carry. ORDER: the section's own // contract (bankexport.go — "Order is the stop's own RANKING") says which row an owner reads first, and a // one-row table makes a reversal unobservable by construction — measured 08.09, reversing the fold left // the whole package green. NOTHING CONSOLIDATED: the second row's Dst is "", the state the field's comment // names in as many words, and no fixture in this package carried one — so a fold substituting the source // surface for the missing rendering published 青茅山 → 青茅山, a decision where there is none, and stayed // green too. func TestEveryDecidingFieldSurvivesTheProjection(t *testing.T) { decided := BankStopRow{ Src: "方源", Dst: "Фан-Юань", Type: "name", Origin: "banknote", Freq: 37, Spread: 4, Conf: 61, Invented: true, Contradicts: []string{ `this run also renders 方 as "Фан"`, }, BankHolds: []string{ `unsigned draft "方源"→"Странник" [ch 1..20]`, `approved "方源"→"Фан Юань" [ch 21..40]`, }, Variants: []BankStopVariant{ {Dst: "Фан Юань", Chunks: 9}, {Dst: "Фан", Chunks: 2, Via: "方"}, }, // Fields the sidecar deliberately does NOT carry, set anyway: a projection that started copying // them into the wrong slot would move a value that is supposed to stay behind. Contexts: []string{"方源来到青茅山"}, Evidence: []string{"ch 3"}, Conventions: 2, Signals: []string{"majority"}, } // The row where the role consolidated NOTHING. Dst stays empty all the way to the sheet: an owner // reading 青茅山 → 青茅山 would be reading a decision that was never made. Conf is negative because // the role stated none, which the field's comment calls a different fact from "0% sure". undecided := BankStopRow{ Src: "青茅山", Dst: "", Type: "place", Origin: "mined", Freq: 5, Spread: 2, Conf: -1, Variants: []BankStopVariant{ {Dst: "гора Цинмао", Chunks: 3}, }, } // The row the SETTLED BASIS answered: a real rendering, carried over from an earlier purchase. It is a // third state rather than a flag on one of the two above, because the combination is production's own: // `Invented` is deliberately NOT computed for such a row (it is a finding about THIS run's drafts, which // were never offered the term), so a fixture that set both would encode a state the engine cannot reach. settledEarlier := BankStopRow{ Src: "元石", Dst: "юаньши", Type: "name", Origin: "mined", Freq: 9, Spread: 1, Conf: -1, SettledByBasis: true, Variants: []BankStopVariant{{Dst: "юаньши", Chunks: 2}}, } got := projectBankProposals([]BankStopRow{decided, undecided, settledEarlier}) want := []BankExportProposal{ { Src: "方源", Dst: "Фан-Юань", Kind: "name", Channel: "banknote", Freq: 37, Spread: 4, Conventions: 2, Conf: 61, Invented: true, Contradicts: []string{`this run also renders 方 as "Фан"`}, BankHolds: []string{ `unsigned draft "方源"→"Странник" [ch 1..20]`, `approved "方源"→"Фан Юань" [ch 21..40]`, }, Variants: []string{"Фан Юань ×9", "Фан ×2 (proposed for 方)"}, }, { Src: "青茅山", Dst: "", Kind: "place", Channel: "mined", Freq: 5, Spread: 2, Conf: -1, Variants: []string{"гора Цинмао ×3"}, }, { Src: "元石", Dst: "юаньши", Kind: "name", Channel: "mined", Freq: 9, Spread: 1, Conf: -1, SettledEarlier: true, Variants: []string{"юаньши ×2"}, }, } // Compared as a WHOLE SLICE, not row by row found by Src: the ranking is the information, so the // position each row holds is part of what is being asserted. if !reflect.DeepEqual(got, want) { t.Fatalf("the projection is not the table it projects:\n got %+v\n want %+v", got, want) } // AND: no field of the proposal is left at its zero value — read off the FIRST row, the one built to // have every field set (the second is deliberately half empty, that being its whole point). The // comparison above pins the fields that exist today; this pins the ones added tomorrow — a field // appended to BankExportProposal and never filled by the fold ships as an absent key in the sidecar, // and every assertion written against today's fields stays green while the signing screen loses a // column. // // ⚠ Its limit, named: this catches a field the PROPOSAL grew and the fold ignored. A field BankStopRow // grows that ought to be projected and is not cannot be caught mechanically — whether a new fact is // one an owner signs on is a judgement, and it belongs to whoever adds the field. // // ⚠ ACROSS THE ROWS AND NOT WITHIN ONE, for the same reason its sibling over the consolidation section // gives: some pairs of fields cannot both be set on one row. `Invented` and `SettledEarlier` are such a // pair — the engine refuses to compute the first for a row the basis answered — so a single all-fields // fixture would have to encode an unreachable state or carry an exclusion list, and an exclusion list is // the thing that goes stale in silence. Requiring each field in AT LEAST ONE row needs no list. rows := make([]reflect.Value, 0, len(got)) for _, p := range got { rows = append(rows, reflect.ValueOf(p)) } for i := range rows[0].NumField() { carried := false for _, v := range rows { if !v.Field(i).IsZero() { carried = true break } } if !carried { t.Errorf("field %s is zero in EVERY projected row, including the ones built to set it — the fold does not carry it", rows[0].Type().Field(i).Name) } } } // An empty table must project to nil rather than to an empty slice: `omitempty` then keeps the key out of // the sidecar entirely, and a reader that finds `proposed` present with zero rows cannot tell "the stop // presented nothing" from "this boundary is not a stop". func TestATableWithNoRowsProjectsToNoSection(t *testing.T) { if got := projectBankProposals(nil); got != nil { t.Errorf("no rows projected to %#v, want nil so the key is omitted entirely", got) } if got := projectBankProposals([]BankStopRow{}); got != nil { t.Errorf("an empty table projected to %#v, want nil", got) } } // TestTheProjectionPublishesConventionsBesideSpread pins the field whose absence produced two wrong readings // of the same run. `spread` counts raw renderings and `conventions` counts decisions, they differ whenever a // draft wrote one decision two ways, and the variants list has one entry per CONVENTION — so a consumer // holding only `spread` and the list is holding two different measurements and no way to know it. // // The fixture is a candidate on which the two numbers MUST differ; on equal numbers the pin would pass // against a projection that published the wrong field, which is how it would have passed before the fix. func TestTheProjectionPublishesConventionsBesideSpread(t *testing.T) { rows := []BankStopRow{{ Src: "开窍大典", Dst: "церемония открытия апертуры", Freq: 10, Spread: 4, Conventions: 3, Variants: []BankStopVariant{ {Dst: "церемония открытия апертуры", Chunks: 3}, {Dst: "Великая церемония открытия апертуры", Chunks: 1}, {Dst: "Обряд отверзания", Chunks: 1}, }, }} got := projectBankProposals(rows) if len(got) != 1 { t.Fatalf("one row in, one proposal out: %+v", got) } p := got[0] if p.Spread == p.Conventions { t.Fatalf("premise broken: the fixture must be a row where the two numbers DIFFER, got spread=%d conventions=%d", p.Spread, p.Conventions) } if p.Spread != 4 || p.Conventions != 3 { t.Errorf("the projection must carry BOTH of the stop table's numbers: got spread=%d conventions=%d, want 4 and 3", p.Spread, p.Conventions) } // The relation that makes the list readable: its length is `conventions`, never `spread`. This is the // arithmetic two sessions could not check because the second number was not published. if p.Conventions != len(p.Variants) { t.Errorf("conventions=%d but the variants list has %d entries — the list is per convention, and a consumer comparing it to spread is comparing different measurements", p.Conventions, len(p.Variants)) } }