textmachine/platform/internal/ingest/build_test.go

102 lines
5.1 KiB
Go

package ingest
import "testing"
// A document that does not identify itself as a build report is REFUSED, and the reason it has to be
// is what the test name says: `{}` decodes into a report claiming a whole book with no holes,
// nothing removed and no drift — the most reassuring thing this type can say, produced for free by
// any process at the engine's path that is not the engine.
//
// Mutation caught: dropping the `build_version` presence check in DecodeBuild.
func TestADocumentWithNoBuildVersionIsNotAReportEvenThoughItDecodesIntoAReassuringOne(t *testing.T) {
for _, body := range []string{`{}`, `{"complete":true,"total_units":14}`, `{"build_version":""}`} {
if _, err := DecodeBuild([]byte(body)); err == nil {
t.Errorf("DecodeBuild(%s) was accepted; a report with no build_version is not a report", body)
}
}
if _, err := DecodeBuild([]byte(`{"build_version":"tm-build-v1"}`)); err != nil {
t.Errorf("a report that names its shape was refused: %v", err)
}
}
// The value of `build_version` is NOT gated, unlike the manifest's, and the asymmetry is deliberate:
// a build report is a receipt for a file that already exists on disk, so refusing to read it over a
// version string would throw away a completed artifact. Pinned because the two decoders sit next to
// each other and the manifest's rule is the one a reader reaches for first.
//
// Mutation caught: adding a `doc.Version != KnownBuildVersion` refusal to DecodeBuild.
func TestAnUnknownBuildShapeIsStillReadBecauseTheFileAlreadyExists(t *testing.T) {
rep, err := DecodeBuild([]byte(`{"build_version":"tm-build-v9","complete":true,"book_id":"bk_1"}`))
if err != nil {
t.Fatalf("a report from a later engine was refused: %v", err)
}
if rep.Version != "tm-build-v9" || !rep.Complete || rep.BookID != "bk_1" {
t.Fatalf("the report was not read as it stands: %+v", rep)
}
}
// Every field the door acts on is read, and the allowlist does not quietly drop one. Written as a
// whole document rather than field by field because the failure this catches is a RENAME on the
// engine's side, which leaves a zero value that reads as "nothing to report".
//
// Mutation caught: removing any of the json tags below from wireBuild.
func TestTheWholeOfWhatTheDoorActsOnIsRead(t *testing.T) {
rep, err := DecodeBuild([]byte(`{
"build_version":"tm-build-v1","book_id":"bk_1","total_units":14,
"pending_units":1,"withheld_units":2,"incomplete_units":3,"stale_units":4,
"stale_unknown":true,"ghost_rows":5,"config_drift":true,
"removed_files":["/a"],"stale_copies":["/b"],"complete":false}`))
if err != nil {
t.Fatal(err)
}
want := BuildReport{Version: "tm-build-v1", BookID: "bk_1", Complete: false,
PendingUnits: 1, WithheldUnits: 2, IncompleteUnits: 3, StaleUnits: 4, GhostRows: 5,
StaleUnknown: true, ConfigDrift: true,
RemovedFiles: []string{"/a"}, StaleCopies: []string{"/b"}}
if rep.Version != want.Version || rep.BookID != want.BookID || rep.Complete != want.Complete ||
rep.PendingUnits != want.PendingUnits || rep.WithheldUnits != want.WithheldUnits ||
rep.IncompleteUnits != want.IncompleteUnits || rep.StaleUnits != want.StaleUnits ||
rep.GhostRows != want.GhostRows || rep.StaleUnknown != want.StaleUnknown ||
rep.ConfigDrift != want.ConfigDrift || len(rep.RemovedFiles) != 1 || len(rep.StaleCopies) != 1 {
t.Fatalf("read %+v, want %+v", rep, want)
}
}
// The door's ratified second duty is to check the REPORT and not only the file (D39.175 п.2,
// D39.178 п.3), and NeedsOperator is the one place that decides what is worth saying. A clean report
// says nothing — a line on every export would be a line an operator filters.
//
// Mutation caught: dropping any of the four disjuncts, or making it always true.
func TestOnlyAReportWithSomethingForTheOperatorAsksForALine(t *testing.T) {
if (BuildReport{Complete: true}).NeedsOperator() {
t.Error("a clean report asked for an operator's attention")
}
for name, rep := range map[string]BuildReport{
"config drift": {ConfigDrift: true},
"stale unknown": {StaleUnknown: true},
"removed files": {RemovedFiles: []string{"/a"}},
"stale copies": {StaleCopies: []string{"/a"}},
} {
if !rep.NeedsOperator() {
t.Errorf("%s did not reach the operator", name)
}
}
}
// The engine withdrew «nothing was written» as a promise of the refusal band and this side used to
// repeat it (unified backlog row 246). The tail of that row is this constant: the engine's
// `exitBookIncomplete` had no name here at all, so `build`'s own refusal class arrived as an
// unrecognised number.
//
// Mutation caught: changing the constant, or letting it fall outside the band.
func TestTheBuildRefusalHasItsOwnNumberAndItIsInsideTheBand(t *testing.T) {
if ExitBookIncomplete != 16 {
t.Errorf("ExitBookIncomplete = %d, want 16 (backend/cmd/tmctl/main.go exitBookIncomplete)", ExitBookIncomplete)
}
if !Refused(ExitBookIncomplete) {
t.Error("the build refusal is outside the refusal band, so a caller would read it as a failure")
}
if OutcomeOf(ExitBookIncomplete) != OutcomeRefused {
t.Errorf("OutcomeOf(16) = %q, want %q", OutcomeOf(ExitBookIncomplete), OutcomeRefused)
}
}