Close the loop's own false pins and the twins of the money hole: a preflight pinned by a list is not pinned, and a catalogue entry can rot three ways rather than one
This commit is contained in:
parent
87d800f720
commit
74ffe4f0a5
9 changed files with 828 additions and 39 deletions
|
|
@ -155,7 +155,15 @@ func run(root, catalog, ids string, timeout time.Duration, logs string) error {
|
|||
// before each mutation. Belt and braces on purpose: the per-mutation restore verifies itself too, but
|
||||
// a restore that verifies against the wrong baseline verifies nothing — which is exactly how this
|
||||
// tool failed the first time it was used.
|
||||
//
|
||||
// ⚠ A FILE THIS PASS CANNOT READ IS THAT ENTRY'S ROT, NOT THE RUN'S DEATH. It used to `return` here,
|
||||
// and that quietly undid the whole point of making a moved ANCHOR per-entry: a renamed or deleted file
|
||||
// is the commonest way a catalogue goes stale, and one of them stopped the run BEFORE entry one —
|
||||
// taking every healthy entry behind it down unseen, `-id` runs included, because this loop walks the
|
||||
// whole catalogue and not the selection. Measured: a catalogue with one such entry printed
|
||||
// «fingerprint …: no such file or directory» and ran nothing at all.
|
||||
baseline := map[string]string{}
|
||||
rottedFile := map[string]string{} // mutation id → why its file could not be fingerprinted
|
||||
for _, m := range all {
|
||||
for _, e := range m.Edits {
|
||||
path := filepath.Join(root, e.File)
|
||||
|
|
@ -164,7 +172,11 @@ func run(root, catalog, ids string, timeout time.Duration, logs string) error {
|
|||
}
|
||||
body, rerr := os.ReadFile(path)
|
||||
if rerr != nil {
|
||||
return fmt.Errorf("fingerprint %s: %w", path, rerr)
|
||||
// Recorded against the ENTRY, so the run reports it in place and still ends non-zero. The
|
||||
// file is deliberately left out of the baseline: nothing may be measured against a file
|
||||
// this pass never saw.
|
||||
rottedFile[m.ID] = fmt.Sprintf("%s: %v (the file moved or was deleted under the catalogue)", m.ID, rerr)
|
||||
continue
|
||||
}
|
||||
baseline[path] = hash(body)
|
||||
}
|
||||
|
|
@ -190,6 +202,7 @@ func run(root, catalog, ids string, timeout time.Duration, logs string) error {
|
|||
// source is byte-identical afterwards) was reported RED and the gate exited 0. A baseline is what
|
||||
// makes a later red ATTRIBUTABLE.
|
||||
green := map[string]bool{}
|
||||
rottedPkg := map[string]string{}
|
||||
for _, m := range all {
|
||||
if len(want) > 0 && !want[m.ID] {
|
||||
continue
|
||||
|
|
@ -201,6 +214,16 @@ func run(root, catalog, ids string, timeout time.Duration, logs string) error {
|
|||
base := exec.Command("go", "test", m.Package, "-count=1", fmt.Sprintf("-timeout=%s", timeout))
|
||||
base.Dir = root
|
||||
if out, berr := base.CombinedOutput(); berr != nil {
|
||||
// ⚠ A PACKAGE PATH THAT NO LONGER RESOLVES IS ROT, and it is the third way a catalogue goes
|
||||
// stale after a moved anchor and a moved file. It used to `return` here, so one renamed package
|
||||
// stopped the run before entry one and took every healthy entry with it — the same shape the
|
||||
// other two were fixed out of. A package that resolves and is genuinely RED still stops the
|
||||
// run: a verdict measured against a package that was already failing is unattributable, which
|
||||
// is the whole reason this baseline exists.
|
||||
if setupFailed(out) {
|
||||
rottedPkg[m.Package] = fmt.Sprintf("package %s does not resolve (it was renamed, moved or deleted under the catalogue)", m.Package)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("%s is NOT GREEN before any mutation is planted — every verdict about it would be unattributable:\n%s",
|
||||
m.Package, tailOf(out))
|
||||
}
|
||||
|
|
@ -212,6 +235,19 @@ func run(root, catalog, ids string, timeout time.Duration, logs string) error {
|
|||
if len(want) > 0 && !want[m.ID] {
|
||||
continue
|
||||
}
|
||||
if why, dead := rottedPkg[m.Package]; dead {
|
||||
ran++
|
||||
fmt.Printf("ROTTED %-28s %s\n", m.ID, why)
|
||||
survived = append(survived, m.ID+" (package moved)")
|
||||
continue
|
||||
}
|
||||
if why, dead := rottedFile[m.ID]; dead {
|
||||
// Reported in place, before anything is planted: the entry guards nothing, and the run goes on.
|
||||
ran++
|
||||
fmt.Printf("ROTTED %-28s %s\n", m.ID, why)
|
||||
survived = append(survived, m.ID+" (file moved)")
|
||||
continue
|
||||
}
|
||||
if err := pristine("before " + m.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -226,7 +262,10 @@ func run(root, catalog, ids string, timeout time.Duration, logs string) error {
|
|||
// Counted as an unexpected outcome, exactly like NOTHING: an entry whose anchor moved guards
|
||||
// nothing, and the run must still end non-zero. What it no longer does is stop the run.
|
||||
fmt.Printf("ROTTED %-28s %s\n", m.ID, detail)
|
||||
survived = append(survived, m.ID+" (anchor moved)")
|
||||
// The summary label stays generic: the DETAIL line above already says which kind of rot this
|
||||
// was, and a label naming one kind for all of them would be the same over-claim in miniature —
|
||||
// «anchor moved» printed for an entry whose run filter had been renamed.
|
||||
survived = append(survived, m.ID+" (rotted)")
|
||||
case verdict == inconclusive:
|
||||
// NOT a red. The package did not build, or the run died before a single test reported — so
|
||||
// nothing was judged and the entry proves nothing. Counted as an unexpected outcome on
|
||||
|
|
@ -374,13 +413,12 @@ func one(root string, m Mutation, timeout time.Duration, logs string) (v verdict
|
|||
path := filepath.Join(root, file)
|
||||
body, rerr := os.ReadFile(path)
|
||||
if rerr != nil {
|
||||
// ROT, not a run failure. ⚠ AND IT DOES NOT CLOSE THE MOVED-FILE CASE, which is what a first
|
||||
// draft of this comment claimed: the fingerprint pass above reads every file the catalogue names
|
||||
// BEFORE any entry runs, so a renamed or deleted file still aborts the whole run there, ahead of
|
||||
// this line. Measured: a catalogue with one entry naming a missing file printed
|
||||
// «fingerprint …: no such file or directory» and ran nothing, the healthy entry behind it
|
||||
// included. What this branch does cover is the narrow race — a file that disappears BETWEEN the
|
||||
// fingerprint pass and this read. The moved-file abort is a live gap with a backlog row.
|
||||
// ROT, not a run failure. The moved-file case itself is caught EARLIER, by the fingerprint pass,
|
||||
// which records it against the entry and lets the run continue; what this branch covers is the
|
||||
// narrow race in between — a file that disappears after that pass and before this read.
|
||||
// ⚠ An earlier version of this comment said the moved-file case "still aborts the whole run".
|
||||
// That was true when it was written and false one edit later, and it is the exact class of
|
||||
// defect this tool exists to find: a sentence that outlived the code it described.
|
||||
return rotted, fmt.Sprintf("%s: %v (the file moved or was deleted under the catalogue)", m.ID, rerr), nil
|
||||
}
|
||||
mu.Lock()
|
||||
|
|
@ -420,6 +458,15 @@ func one(root string, m Mutation, timeout time.Duration, logs string) (v verdict
|
|||
return survivedTests, "", fmt.Errorf("%s: write the log: %w", m.ID, werr)
|
||||
}
|
||||
}
|
||||
// ⚠ A `run` FILTER THAT MATCHES NOTHING IS ROT, NOT A SURVIVOR. `go test -run TestRenamedAway` runs
|
||||
// zero tests and exits 0, so a renamed pin turned every entry that names it green forever: an
|
||||
// expect-RED entry printed SURVIVED (a finding that is really a NOTHING — no test was judged), and an
|
||||
// expect-`survives` entry printed exactly what the catalogue records and kept the whole gate at exit 0.
|
||||
// The catalogue has entries with run filters precisely so a red is attributable to ONE pin, which is
|
||||
// what makes this failure quiet: the narrower the filter, the more a rename costs.
|
||||
if strings.Contains(string(out), "no tests to run") {
|
||||
return rotted, fmt.Sprintf("%s: the run filter %q matched no test — it was renamed or removed, and this entry has been asserting nothing", m.ID, m.Run), nil
|
||||
}
|
||||
if terr == nil {
|
||||
return survivedTests, "", nil
|
||||
}
|
||||
|
|
@ -445,3 +492,14 @@ func hash(b []byte) string {
|
|||
sum := sha256.Sum256(b)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// setupFailed reports whether `go test` refused the PACKAGE PATTERN itself rather than running anything —
|
||||
// the package was renamed, moved or deleted under the catalogue. Distinguishable from a genuine red in the
|
||||
// output, and it must be, because the two get opposite treatment: rot is recorded per entry and the run
|
||||
// goes on, a real red stops everything.
|
||||
func setupFailed(out []byte) bool {
|
||||
s := string(out)
|
||||
return strings.Contains(s, "[setup failed]") ||
|
||||
strings.Contains(s, "directory not found") ||
|
||||
strings.Contains(s, "matched no packages")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1322,7 +1322,7 @@
|
|||
},
|
||||
{
|
||||
"id": "WB15-screen-floor-slides-inside-its-interval",
|
||||
"why": "the threshold is ratified as a CONTRACT, not a tuning knob (D39.92 п.1а), and it was pinned only to an INTERVAL — 0.45 is still inside the corpus's empty band, so the slide passed everything AND moved no version, which would silently re-verdict a resumed chunk under a threshold nobody re-billed for. langscreen.Version is now derived from the constants, so the snapshot moves by construction",
|
||||
"why": "a threshold ratified as a CONTRACT (D39.92 п.1а) was pinned only to an INTERVAL — 0.45 is still inside the corpus's empty band — so a slide re-verdicted a resumed chunk under a threshold nobody re-billed for. Version is derived from the constants, so THIS edit moves the snapshot and the golden catches it. ⚠ THAT IS THE WHOLE OF WHAT THIS ENTRY PROVES: the golden sees a slide, and it CANNOT see a literal that stops tracking the constants — under `literal + slide` the string is byte-identical, the golden does not move and this entry survives. The derivation itself is WB36's, in the langscreen package. An earlier `why` here claimed «the snapshot moves by construction» and read as a guarantee against both",
|
||||
"package": "./internal/pipeline/",
|
||||
"run": "TestGoldenDeterminism",
|
||||
"edits": [
|
||||
|
|
@ -1763,5 +1763,233 @@
|
|||
"replace": "\t\t\tsignedSrc[p.entry.src] = true"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB51-flag-reason-declared-elsewhere",
|
||||
"why": "every FlagReason the engine declares must be RANKED; the exhaustiveness test used to read one file, so a reason declared anywhere else fell to the unknown bucket while the test still said «every declared reason is ranked»",
|
||||
"package": "./internal/pipeline/",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/status.go",
|
||||
"find": "const severityUnknown = 8\n",
|
||||
"replace": "const severityUnknown = 8\n\n// planted\nconst FlagPlantedElsewhere FlagReason = \"planted_elsewhere\"\n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB52-builder-hardcodes-the-target",
|
||||
"why": "the classify input's target must come from the BOOK; a hard-coded target makes every book judged as the shipping pair, and no runner fixture translates into anything but Russian, so nothing else can see it",
|
||||
"package": "./internal/pipeline/",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/chunkrun.go",
|
||||
"find": "\t\tTargetLang: r.Book.TargetLang,\n",
|
||||
"replace": "\t\tTargetLang: \"ru\",\n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB53-builder-hardcodes-target-scripts",
|
||||
"why": "the target's scripts must come from the BOOK's target: hard-coded, a →ja book is screened against Cyrillic and every correct chapter is off-target",
|
||||
"package": "./internal/pipeline/",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/chunkrun.go",
|
||||
"find": "\t\tTargetScripts: lang.LangScripts(r.Book.TargetLang),\n",
|
||||
"replace": "\t\tTargetScripts: lang.LangScripts(\"ru\"),\n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB54-builder-hardcodes-source-scripts",
|
||||
"why": "the source's scripts must come from the RUN's compiled checkers: hard-coded to zh, the echo rule measures the wrong script for every other source language",
|
||||
"package": "./internal/pipeline/",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/chunkrun.go",
|
||||
"find": "\t\tSourceScripts: r.checkers.SourceScripts(),\n",
|
||||
"replace": "\t\tSourceScripts: lang.LangScripts(\"zh\"),\n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB56-labelled-preflight-skips-gates",
|
||||
"why": "the LABELLED branch of CheckKeys must demand the keys of the models the gates call: it was pinned only by the reachable LIST, which is exactly the thing «a pin on the list is not a pin on the preflight» rejects",
|
||||
"package": "./internal/config/",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/config/models.go",
|
||||
"find": "\t\tfor _, mdl := range pipe.ReachableModels() {\n\t\t\tneeded[mdl] = struct{}{}\n\t\t}\n\t\treturn m.checkKeysFor(needed)",
|
||||
"replace": "\t\tfor _, st := range pipe.Stages {\n\t\t\tneeded[st.ResolvedModel] = struct{}{}\n\t\t}\n\t\treturn m.checkKeysFor(needed)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB57-banknote-warning-loses-the-pack-arm",
|
||||
"why": "the banknote warning fires when EITHER mining input is missing; keyed on the contrast artifact alone, a book with a pack and no contrast pays for a term table nobody reads and is told nothing",
|
||||
"package": "./internal/pipeline/",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/runner.go",
|
||||
"find": "(r.pack == nil || r.Pipeline.Mining.ContrastPath == \"\")",
|
||||
"replace": "(r.Pipeline.Mining.ContrastPath == \"\")"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB58-banknote-warning-loses-the-contrast-arm",
|
||||
"why": "the mirror arm: keyed on the pack alone, a book with a contrast artifact and no langpack pays for the same unread table in silence",
|
||||
"package": "./internal/pipeline/",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/runner.go",
|
||||
"find": "(r.pack == nil || r.Pipeline.Mining.ContrastPath == \"\")",
|
||||
"replace": "(r.pack == nil)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB59-banknote-warning-fires-on-a-read-open",
|
||||
"why": "a $0 read path buys nothing, so warning there teaches an operator to ignore the line on the run where it costs money",
|
||||
"package": "./internal/pipeline/",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/runner.go",
|
||||
"find": "\tif forWrite && r.Pipeline.Gates.Banknote.Enabled && (",
|
||||
"replace": "\tif r.Pipeline.Gates.Banknote.Enabled && ("
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB60-volume-report-decided-after-reconcile",
|
||||
"why": "the volume report is decided BEFORE reconcile trues the counters up: decided after, a run whose only work outside its grant was a carried unit that came back FLAGGED has Carried==0 and reports nothing at all — the one run that most needs the disclosure goes silent. ⚠ Run-filtered: this planting also strips the report from the DRAFT-ONLY path, which three neighbouring tests catch for a different reason (they lose the report entirely, not out of order). Unfiltered it would go red without proving anything about this pin. WB64 is the faithful both-paths form",
|
||||
"package": "./internal/pipeline/",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/waverun.go",
|
||||
"find": "\tif scope.bound() {\n\t\tres.Volume = &scope.stop\n",
|
||||
"replace": "\tif scope.bound() {\n"
|
||||
},
|
||||
{
|
||||
"file": "internal/pipeline/waverun.go",
|
||||
"find": "\tscope.reconcile(res.Chunks)\n\tr.Log.InfoContext(ctx, \"book run finished\", \"book\"",
|
||||
"replace": "\tscope.reconcile(res.Chunks)\n\tif scope.bound() {\n\t\tres.Volume = &scope.stop\n\t}\n\tr.Log.InfoContext(ctx, \"book run finished\", \"book\""
|
||||
}
|
||||
],
|
||||
"run": "TestTheVolumeReportIsAskedBeforeTheCountersAreTruedUp"
|
||||
},
|
||||
{
|
||||
"id": "WB55-corpus-provenance-swapped",
|
||||
"why": "the corpus digest must cover the ADDRESS a specimen was taken from, not only its counts: a corpus whose numbers are right and whose provenance is wrong is unreproducible in the one way that matters",
|
||||
"package": "./internal/pipeline/",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/testdata/offtarget-corpus.json",
|
||||
"find": "\"provenance\": \"probe-4axes/arms/backups/20260902T075308Z.db#draft\",\n \"head\": \"Рассечение камня",
|
||||
"replace": "\"provenance\": \"planted/not-the-address-it-came-from.db#draft\",\n \"head\": \"Рассечение камня"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB61-builder-hardcodes-the-fixtures-own-pair",
|
||||
"why": "a hard-code to the FIXTURE'S OWN pair is the hole a value-comparing test cannot see: the earlier pin asserted the builder's output against values chosen for one book, so it caught a hard-code to the shipping pair and would have sailed past a hard-code to the pair the test itself uses. Only «two books must answer differently» is unsatisfiable by any constant. ⚠ Run-filtered on purpose: a hard-code to `ja` also breaks twenty ru fixtures, so an unfiltered entry would go red without proving anything about THIS pin. The dangerous hard-code is the one to the SHIPPING pair (WB52), which nothing else sees",
|
||||
"package": "./internal/pipeline/",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/chunkrun.go",
|
||||
"find": "\t\tTargetLang: r.Book.TargetLang,\n",
|
||||
"replace": "\t\tTargetLang: \"ja\",\n"
|
||||
}
|
||||
],
|
||||
"run": "TestClassifyInputForLeavesNoAxisUnset"
|
||||
},
|
||||
{
|
||||
"id": "WB62-innocent-const-must-not-be-accused",
|
||||
"expect": "survives",
|
||||
"why": "THE BOUNDARY OF TestEveryFlagReasonIsRanked, recorded because the test used to cross it. An ordinary untyped string constant sitting in the FlagReason block is NOT a flag — Go types it as untyped string, inheriting nothing — and the walk must ignore it. An earlier version carried the previous spec's type forward and failed the test with a message accusing a documentation URL of being an unranked flag. A test that accuses the innocent is not enforcing its own sentence, so this planting MUST survive",
|
||||
"package": "./internal/pipeline/",
|
||||
"run": "TestEveryFlagReasonIsRanked",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/disposition.go",
|
||||
"find": "\tFlagSanitizerStripped FlagReason = \"sanitizer_stripped\"\n",
|
||||
"replace": "\tFlagSanitizerStripped FlagReason = \"sanitizer_stripped\"\n\n\t// not a flag: a documentation pointer that happens to live in this block\n\tflagReasonDocURL = \"https://example.invalid/flags\"\n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB63-run-compiles-a-constant-source",
|
||||
"why": "the run's declared SOURCE scripts must come from the book. This is the WRITER of the axis whose reader was pinned one commit earlier — pinning the reader alone left the whole axis decided by an unguarded line, and a constant here gives a ko→ru or ja→ru book the wrong declared source while the echo detector measures a script the book does not use",
|
||||
"package": "./internal/pipeline/",
|
||||
"run": "TestTheRunCompilesTheBooksOwnSourceScripts",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/runner.go",
|
||||
"find": "\t\tr.checkers.SetSourceScripts(lang.LangScripts(r.Book.SourceLang))\n",
|
||||
"replace": "\t\tr.checkers.SetSourceScripts(lang.LangScripts(\"ja\"))\n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB64-volume-report-after-reconcile-on-both-paths",
|
||||
"why": "the FAITHFUL form of the ordering defect: the decision moved after reconcile on BOTH the draft-only and the editor path, so no neighbour loses its report for an unrelated reason. Only a fixture whose single unit is carried AND flagged can tell the two orders apart, and until this entry existed the draft-only path had none",
|
||||
"package": "./internal/pipeline/",
|
||||
"run": "TestTheVolumeReportIsAskedBefore",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/waverun.go",
|
||||
"find": "\tif scope.bound() {\n\t\tres.Volume = &scope.stop\n",
|
||||
"replace": "\tif scope.bound() {\n"
|
||||
},
|
||||
{
|
||||
"file": "internal/pipeline/waverun.go",
|
||||
"find": "\t\tscope.reconcile(res.Chunks)\n\t\tr.Log.InfoContext(ctx, \"book run finished (draft-only)\"",
|
||||
"replace": "\t\tscope.reconcile(res.Chunks)\n\t\tif scope.bound() {\n\t\t\tres.Volume = &scope.stop\n\t\t}\n\t\tr.Log.InfoContext(ctx, \"book run finished (draft-only)\""
|
||||
},
|
||||
{
|
||||
"file": "internal/pipeline/waverun.go",
|
||||
"find": "\tscope.reconcile(res.Chunks)\n\tr.Log.InfoContext(ctx, \"book run finished\", \"book\"",
|
||||
"replace": "\tscope.reconcile(res.Chunks)\n\tif scope.bound() {\n\t\tres.Volume = &scope.stop\n\t}\n\tr.Log.InfoContext(ctx, \"book run finished\", \"book\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB65-labelled-preflight-skips-the-hop",
|
||||
"why": "a labelled run CALLS its escalation hop, and BuildClient never refuses an empty key — it only becomes a header. A preflight blind to the hop lets the book open, buy the draft wave and 401 at the escalation: the same late failure as the gates, one field over",
|
||||
"package": "./internal/config/",
|
||||
"run": "TestTheLABELLEDPreflightDemandsEveryCallersKeyToo",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/config/pipeline.go",
|
||||
"find": "\t\tadd(st.ResolvedHop)\n",
|
||||
"replace": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB66-preflight-demands-the-configured-not-the-resolved-model",
|
||||
"why": "routing is what a labelled book is FOR: the model in the yaml may not be the model that answers. A preflight that demands the CONFIGURED model passes a book whose RESOLVED model has no key, and the refusal moves from open time to the first draft call",
|
||||
"package": "./internal/config/",
|
||||
"run": "TestTheLABELLEDPreflightDemandsEveryCallersKeyToo",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/config/pipeline.go",
|
||||
"find": "\t\tadd(st.ResolvedModel)\n",
|
||||
"replace": "\t\tadd(st.Model)\n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WB67-flag-reason-in-the-conversion-spelling",
|
||||
"why": "`X = FlagReason(\"…\")` is as ordinary a declaration as `X FlagReason = \"…\"`, and a walk that reads only the spec's type is blind to it — exhibited as a pair, the typed spelling caught and the conversion spelling not",
|
||||
"package": "./internal/pipeline/",
|
||||
"run": "TestEveryFlagReasonIsRanked",
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/status.go",
|
||||
"find": "const severityUnknown = 8\n",
|
||||
"replace": "const severityUnknown = 8\n\n// planted\nconst FlagPlantedConv = FlagReason(\"planted_conv\")\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -259,3 +259,93 @@ func TestTheUnlabelledPreflightDemandsEveryCallersKey(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTheLABELLEDPreflightDemandsEveryCallersKeyToo is the twin of the test above, and it exists because
|
||||
// the fix that closed the hole closed ONE branch of it.
|
||||
//
|
||||
// ⚠ CheckKeys forks on whether the book carries content labels. The unlabelled branch — the shipping
|
||||
// default — was the one found open and the one pinned. The labelled branch reaches the gates only through
|
||||
// ReachableModels(), and the only thing asserting that was TestABankModelIsAReachableModel, which reads
|
||||
// the LIST. «A pin on the list is not a pin on the preflight» is the sentence this pack was written around;
|
||||
// leaving the second branch pinned by exactly the thing that sentence rejects is the same defect with the
|
||||
// paint changed. Planted — the labelled branch collecting stage models by hand — the whole package stayed
|
||||
// green, and a labelled book with its bank role on a keyless provider bought the draft wave and died at
|
||||
// the gate, which is where this started.
|
||||
func TestTheLABELLEDPreflightDemandsEveryCallersKeyToo(t *testing.T) {
|
||||
m := &Models{
|
||||
Providers: map[string]Provider{
|
||||
"stage-provider": {Kind: "openai", BaseURL: "http://x", APIKeyEnv: "TEST_STAGE_KEY"},
|
||||
"other-provider": {Kind: "openai", BaseURL: "http://y", APIKeyEnv: "TEST_OTHER_KEY"},
|
||||
},
|
||||
Models: map[string]Model{
|
||||
"draft-model": {Provider: "stage-provider"},
|
||||
"bank-model": {Provider: "other-provider"},
|
||||
"classify-model": {Provider: "other-provider"},
|
||||
"repair-model": {Provider: "other-provider"},
|
||||
"hop-model": {Provider: "other-provider"},
|
||||
},
|
||||
}
|
||||
t.Setenv("TEST_STAGE_KEY", "sk-present")
|
||||
t.Setenv("TEST_OTHER_KEY", "")
|
||||
|
||||
base := func() *Pipeline {
|
||||
return &Pipeline{
|
||||
ContentLabels: []string{"violence"},
|
||||
Stages: []Stage{{Name: "draft", Model: "draft-model", ResolvedModel: "draft-model"}},
|
||||
}
|
||||
}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
mut func(*Pipeline)
|
||||
}{
|
||||
{"the terminologist", func(p *Pipeline) {
|
||||
p.Gates.Terminology = TerminologyGate{Enabled: true, Model: "bank-model"}
|
||||
}},
|
||||
{"the classifier phase", func(p *Pipeline) {
|
||||
p.Gates.Terminology = TerminologyGate{Enabled: true, Model: "draft-model",
|
||||
ClassifyTypes: true, ClassifyModel: "classify-model"}
|
||||
}},
|
||||
{"the repair gate", func(p *Pipeline) {
|
||||
p.Gates.Repair = RepairGate{Enabled: true, Model: "repair-model"}
|
||||
}},
|
||||
// ⚠ THE HOP, found by an adversarial pass over this very test: the gates were covered and the
|
||||
// stage's own hop was not. A labelled run calls its escalation hop, BuildClient never refuses an
|
||||
// empty key (it only becomes a header), so under a preflight blind to the hop the book opens, buys
|
||||
// the draft wave, and 401s at the escalation — the same late failure, one field over.
|
||||
{"the stage's escalation hop", func(p *Pipeline) {
|
||||
p.Stages[0].ResolvedHop = "hop-model"
|
||||
}},
|
||||
// ⚠ AND THE RESOLVED MODEL, NOT THE CONFIGURED ONE. Routing is what a labelled book is FOR: the
|
||||
// model in the yaml may not be the model that answers. A preflight that demanded `Model` would pass
|
||||
// here and the first draft call would 401 — the refusal moving from open time to mid-run, which is
|
||||
// precisely the distance this whole family of tests exists to keep at zero.
|
||||
{"the RESOLVED model when routing replaced the configured one", func(p *Pipeline) {
|
||||
p.Stages[0].Model, p.Stages[0].ResolvedModel = "draft-model", "bank-model"
|
||||
}},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
p := base()
|
||||
c.mut(p)
|
||||
if len(p.ContentLabels) == 0 {
|
||||
t.Fatal("premise broken: this test is about the LABELLED branch and must carry labels")
|
||||
}
|
||||
err := m.CheckKeys(p)
|
||||
if err == nil {
|
||||
t.Fatalf("a LABELLED book's %s calls a provider whose key is absent and the preflight passed: "+
|
||||
"the run buys the draft wave and dies at the gate", c.name)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "TEST_OTHER_KEY") {
|
||||
t.Fatalf("the refusal must name the env var an operator has to set, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
// And the disabled direction, so this cannot be satisfied by demanding every model in the file.
|
||||
t.Run("gates off demand nothing", func(t *testing.T) {
|
||||
p := base()
|
||||
p.Gates.Terminology = TerminologyGate{Enabled: false, Model: "bank-model"}
|
||||
p.Gates.Repair = RepairGate{Enabled: false, Model: "repair-model"}
|
||||
if err := m.CheckKeys(p); err != nil {
|
||||
t.Fatalf("a labelled book that never calls those models must not be made to hold their keys: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -297,6 +297,47 @@ func TestTheLawBlockGivesOneOrderPerSourceTerm(t *testing.T) {
|
|||
if n := strings.Count(RenderEditorConstraintBlock(sortByPriority(padded), tx), "元海 → "); n != 1 {
|
||||
t.Errorf("the signed set and the lookup must normalize the surface the same way, got %d lines", n)
|
||||
}
|
||||
|
||||
// (10) ⚠ TWO UNSIGNED ROWS OF ONE SURFACE BOTH STAND — and this is a DECLARED CHANGE OF WIRE BEHAVIOUR,
|
||||
// written down here because it happened and was not declared when it did.
|
||||
//
|
||||
// The rule this block enforces used to dedup on the bare `src`, which collapsed any two renderings of a
|
||||
// surface to whichever the sort put first. That was corrected because it dropped a second SIGNED canon
|
||||
// (青山 the surname and 青山 the direction are two decisions the owner made separately). The correction
|
||||
// keys on (src, dst) and makes only an UNSIGNED row yield, and only to a SIGNED one — so where there is
|
||||
// no signature at all, nothing yields and both renderings reach the model. Measured across the change:
|
||||
// the same two unsigned rows produced ONE line before and TWO after.
|
||||
//
|
||||
// ⚠ WHAT THIS CASE DOES AND DOES NOT SAY. It pins the RENDER's behaviour: with no signature present
|
||||
// there is nothing to prefer, so the block does not arbitrate — it shows both and lets the run's own
|
||||
// unsigned-conflict report (TestUnsignedConflictIsReportedByTheRun) be the place a human is told. A
|
||||
// silent pick-by-sort-order would be the engine choosing between two of its own guesses and hiding
|
||||
// that it did, which is strictly worse at THIS layer.
|
||||
//
|
||||
// It does NOT say that two unsigned renderings of one surface may exist in the first place. That is an
|
||||
// UPSTREAM question and the owner has answered it: an unsigned term must have ONE rendering too, and
|
||||
// the consolidation stage owns that, not the signature. Measured today, the bank accepts the state —
|
||||
// two rows, one src, one (empty) sense, overlapping spoiler windows, different dst — because the
|
||||
// same-sense overlap guard (memseed.go) runs PER FILE and the merge (storeOrder) refuses only an exact
|
||||
// duplicate of the UNIQUE key, so a seed row and a mined-delta row collide only after both have passed
|
||||
// their own loader. When that is fixed upstream this state stops being reachable and this case becomes
|
||||
// a statement about something that no longer occurs — which is the right time to delete it, and the
|
||||
// wrong time to read it as doctrine that two lines are DESIRABLE.
|
||||
unsignedPair := []PickedEntry{
|
||||
{entry: &entry{src: "元石", dst: "юаньши", status: "draft"}, Via: "元石", Disp: Ambiguous, KeyTrusted: true},
|
||||
{entry: &entry{src: "元石", dst: "камень первоисточника", status: "auto"}, Via: "元石", Disp: Ambiguous, KeyTrusted: true},
|
||||
}
|
||||
got10 := RenderEditorConstraintBlock(sortByPriority(unsignedPair), tx)
|
||||
if n := strings.Count(got10, "元石 → "); n != 2 {
|
||||
t.Errorf("two UNSIGNED renderings of one surface must BOTH reach the model — there is no signature "+
|
||||
"for either to yield to, and picking one by sort order hides a disagreement the run reports. "+
|
||||
"Got %d lines:\n%s", n, got10)
|
||||
}
|
||||
// And the draft wire must say the same thing: the two wires disagreeing about how many renderings a
|
||||
// surface has is the shape the original regression took.
|
||||
if draft := RenderGlossaryBlock(sortByPriority(unsignedPair), tx); strings.Count(draft, "元石 → ") != 2 {
|
||||
t.Errorf("the two wires disagree about the unsigned pair:\neditor: %s\ndraft: %s", got10, draft)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTheBudgetKeepsTheTrustworthyMatchFirst is where priorityRank's DISPOSITION axis actually decides
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package pipeline
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"textmachine/backend/internal/checks"
|
||||
"textmachine/backend/internal/config"
|
||||
|
|
@ -24,9 +27,22 @@ import (
|
|||
// the same person who forgets to update the builder, which is the failure it is supposed to prevent. A new
|
||||
// axis added to classifyInput and not wired in fails here on the day it is declared.
|
||||
func TestClassifyInputForLeavesNoAxisUnset(t *testing.T) {
|
||||
ck := checks.CompileCheckersFor(nil, lang.TargetChecksFor("ru"))
|
||||
ck.SetSourceScripts(lang.LangScripts("zh"))
|
||||
r := &Runner{Book: &config.Book{TargetLang: "ru"}, checkers: ck}
|
||||
// ⚠ A NON-ru TARGET AND A NON-zh SOURCE, AND THAT IS THE WHOLE DESIGN OF THIS FIXTURE. The first
|
||||
// version used ru/zh — the shipping pair — so a builder that ignored the book entirely and hard-coded
|
||||
// `lang.LangScripts("ru")` produced exactly the values asserted, and three separate plantings that did
|
||||
// precisely that survived the whole package. Nothing else in the battery could catch them either:
|
||||
// every runner fixture translates INTO Russian. A pair the fixture does not share with the hard-code
|
||||
// is the only shape of this test that can fail.
|
||||
const target, source = "ja", "ko"
|
||||
if len(lang.LangScripts(target)) == len(lang.LangScripts("ru")) || len(lang.LangScripts(source)) == len(lang.LangScripts("zh")) {
|
||||
t.Fatalf("premise broken: this test can only catch a hard-coded ru/zh builder while the script COUNTS "+
|
||||
"differ — %s has %d, ru has %d; %s has %d, zh has %d. Pick another pair or assert by identity.",
|
||||
target, len(lang.LangScripts(target)), len(lang.LangScripts("ru")),
|
||||
source, len(lang.LangScripts(source)), len(lang.LangScripts("zh")))
|
||||
}
|
||||
ck := checks.CompileCheckersFor(nil, lang.TargetChecksFor(target))
|
||||
ck.SetSourceScripts(lang.LangScripts(source))
|
||||
r := &Runner{Book: &config.Book{TargetLang: target}, checkers: ck}
|
||||
|
||||
// Every argument is chosen NON-ZERO, so that any zero field in the result is an unwired axis and not an
|
||||
// artefact of the fixture.
|
||||
|
|
@ -41,16 +57,16 @@ func TestClassifyInputForLeavesNoAxisUnset(t *testing.T) {
|
|||
}
|
||||
}
|
||||
// The two axes the incident was actually about, asserted by value and not merely as non-zero.
|
||||
if in.TargetLang != "ru" {
|
||||
t.Errorf("TargetLang = %q, want the book's", in.TargetLang)
|
||||
if in.TargetLang != target {
|
||||
t.Errorf("TargetLang = %q, want the book's %q", in.TargetLang, target)
|
||||
}
|
||||
if len(in.TargetScripts) != len(lang.LangScripts("ru")) {
|
||||
t.Errorf("TargetScripts must come from the book's target: got %d tables, want %d",
|
||||
len(in.TargetScripts), len(lang.LangScripts("ru")))
|
||||
if len(in.TargetScripts) != len(lang.LangScripts(target)) {
|
||||
t.Errorf("TargetScripts must come from the BOOK's target, not a constant: got %d tables, want %d for %s",
|
||||
len(in.TargetScripts), len(lang.LangScripts(target)), target)
|
||||
}
|
||||
if len(in.SourceScripts) != len(lang.LangScripts("zh")) {
|
||||
t.Errorf("SourceScripts must come from the run's compiled checkers: got %d tables, want %d",
|
||||
len(in.SourceScripts), len(lang.LangScripts("zh")))
|
||||
if len(in.SourceScripts) != len(lang.LangScripts(source)) {
|
||||
t.Errorf("SourceScripts must come from the RUN's compiled checkers, not a constant: got %d tables, want %d for %s",
|
||||
len(in.SourceScripts), len(lang.LangScripts(source)), source)
|
||||
}
|
||||
if !in.NonProseReply {
|
||||
t.Error("NonProseReply must pass through: a bank role's reply is not prose and must not be echo-screened")
|
||||
|
|
@ -59,4 +75,79 @@ func TestClassifyInputForLeavesNoAxisUnset(t *testing.T) {
|
|||
if r.classifyInputFor("源文本", "перевод", llm.FinishStop, false).NonProseReply {
|
||||
t.Error("NonProseReply is pinned to true inside the builder — the prose path would be exempted from the echo rule")
|
||||
}
|
||||
|
||||
// ⛔ TWO BOOKS, AND THIS IS THE ASSERTION A CONSTANT CANNOT SATISFY. Everything above compares the
|
||||
// builder's output against values chosen for THIS book, so it catches a hard-code to the shipping pair
|
||||
// and nothing else — a hard-code to ja/ko would sail through the very fixture written to catch
|
||||
// hard-codes. Two books whose answers must DIFFER is the only shape that cannot be met by any constant,
|
||||
// whichever one is picked.
|
||||
other := checks.CompileCheckersFor(nil, lang.TargetChecksFor("ru"))
|
||||
other.SetSourceScripts(lang.LangScripts("zh"))
|
||||
r2 := &Runner{Book: &config.Book{TargetLang: "ru"}, checkers: other}
|
||||
a := r.classifyInputFor("源文本", "перевод", llm.FinishStop, false)
|
||||
b := r2.classifyInputFor("源文本", "перевод", llm.FinishStop, false)
|
||||
if a.TargetLang == b.TargetLang {
|
||||
t.Errorf("two books with different targets produced the same TargetLang %q — the builder is not "+
|
||||
"reading the book at all", a.TargetLang)
|
||||
}
|
||||
if len(a.TargetScripts) == len(b.TargetScripts) {
|
||||
t.Errorf("two books with different targets produced the same TargetScripts (%d tables) — the builder "+
|
||||
"is not reading the book's target", len(a.TargetScripts))
|
||||
}
|
||||
if len(a.SourceScripts) == len(b.SourceScripts) {
|
||||
t.Errorf("two runs with different compiled checkers produced the same SourceScripts (%d tables) — the "+
|
||||
"builder is not reading the run's checkers", len(a.SourceScripts))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTheRunCompilesTheBooksOwnSourceScripts pins the OTHER end of the same axis, one call site away from
|
||||
// the builder above — and it was found by an adversarial pass over that very fix.
|
||||
//
|
||||
// ⚠ The builder reads r.checkers.SourceScripts(); loadLangPack is what FILLS them, from the book's
|
||||
// SourceLang. Pinning the reader and not the writer leaves the whole axis decided by an unguarded line: a
|
||||
// hard-code there gives a ko→ru or ja→ru book the wrong declared source, the echo detector then measures a
|
||||
// script the book does not use, and nothing in the battery says a word. Same two-book shape as above,
|
||||
// because the same reason applies — any constant makes two books agree.
|
||||
func TestTheRunCompilesTheBooksOwnSourceScripts(t *testing.T) {
|
||||
srv := newJSONProvider(&reqRec{}, draftEdit)
|
||||
defer srv.Close()
|
||||
|
||||
sourceScriptsFor := func(t *testing.T, sourceLang string) []*unicode.RangeTable {
|
||||
t.Helper()
|
||||
bookPath := setupProjectOpts(t, srv.URL, projectOpts{})
|
||||
if sourceLang != "ja" {
|
||||
raw, err := os.ReadFile(bookPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := strings.Replace(string(raw), "source_lang: ja", "source_lang: "+sourceLang, 1)
|
||||
if body == string(raw) {
|
||||
t.Fatalf("premise broken: the fixture's book.yaml no longer carries `source_lang: ja`:\n%s", raw)
|
||||
}
|
||||
writeFile(t, bookPath, body)
|
||||
}
|
||||
r := newRunner(t, bookPath)
|
||||
defer r.Close()
|
||||
return r.checkers.SourceScripts()
|
||||
}
|
||||
|
||||
// ja declares three scripts, ko two — so a constant of either kind is visible as a disagreement.
|
||||
if len(lang.LangScripts("ja")) == len(lang.LangScripts("ko")) {
|
||||
t.Fatalf("premise broken: this test needs two sources whose script COUNTS differ, got ja=%d ko=%d",
|
||||
len(lang.LangScripts("ja")), len(lang.LangScripts("ko")))
|
||||
}
|
||||
ja := sourceScriptsFor(t, "ja")
|
||||
ko := sourceScriptsFor(t, "ko")
|
||||
|
||||
if len(ja) != len(lang.LangScripts("ja")) {
|
||||
t.Errorf("a ja-source book compiled %d source scripts, want %d — the run is not reading the book's "+
|
||||
"source language, so the echo detector measures the wrong script", len(ja), len(lang.LangScripts("ja")))
|
||||
}
|
||||
if len(ko) != len(lang.LangScripts("ko")) {
|
||||
t.Errorf("a ko-source book compiled %d source scripts, want %d", len(ko), len(lang.LangScripts("ko")))
|
||||
}
|
||||
if len(ja) == len(ko) {
|
||||
t.Errorf("two books with different source languages compiled the SAME source scripts (%d tables) — "+
|
||||
"the run is using a constant, and no assertion about one book alone can see that", len(ja))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,3 +198,84 @@ func TestEveryPromptLoadPathRefusesAnUnrenderableTemplate(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTheBanknoteWarningKeysOnEachConditionSeparately closes a WB41-class hole in the test above: its
|
||||
// positive fixture has NEITHER mining input and its negative has BOTH, so every one of the guard's parts
|
||||
// could be deleted and the pair still passed. Planted separately, three of them survived the whole package —
|
||||
// dropping the pack half of the disjunction, dropping the contrast half, and dropping `forWrite` entirely.
|
||||
//
|
||||
// The guard is `forWrite && banknote.Enabled && (pack == nil || contrast == "")`. A disjunction pinned only
|
||||
// at "neither" and "both" is pinned at neither of its arms.
|
||||
func TestTheBanknoteWarningKeysOnEachConditionSeparately(t *testing.T) {
|
||||
packRoot, err := filepath.Abs(filepath.Join("..", "..", "configs", "langpacks"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
open := func(t *testing.T, bookPath string, readOnly bool) string {
|
||||
t.Helper()
|
||||
var log bytes.Buffer
|
||||
h := slog.New(slog.NewTextHandler(&log, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
var r *Runner
|
||||
var oerr error
|
||||
if readOnly {
|
||||
r, oerr = NewReadOnlyRunner(bookPath, h)
|
||||
} else {
|
||||
r, oerr = NewRunner(bookPath, h)
|
||||
}
|
||||
if oerr != nil {
|
||||
t.Fatalf("this configuration is legal and must open — it is wasteful, not broken: %v", oerr)
|
||||
}
|
||||
r.Close()
|
||||
return log.String()
|
||||
}
|
||||
|
||||
// ARM 1 — a pack, but no contrast artifact. The bank-mining stop returns before reading anything, so
|
||||
// the banknote tokens are bought and unread; the pack half of the disjunction must not be what decides.
|
||||
t.Run("a pack but no contrast artifact", func(t *testing.T) {
|
||||
srv := newJSONProvider(&reqRec{}, draftEdit)
|
||||
defer srv.Close()
|
||||
bookPath := setupProjectOpts(t, srv.URL, projectOpts{banknote: true})
|
||||
raw, rerr := os.ReadFile(bookPath)
|
||||
if rerr != nil {
|
||||
t.Fatal(rerr)
|
||||
}
|
||||
body := strings.Replace(string(raw), "source_lang: ja", "source_lang: zh\nlangpack_root: "+packRoot, 1)
|
||||
if !strings.Contains(body, "langpack_root:") {
|
||||
t.Fatalf("premise broken: the fixture's book.yaml no longer carries the anchor this test edits:\n%s", raw)
|
||||
}
|
||||
writeFile(t, bookPath, body)
|
||||
if out := open(t, bookPath, false); !strings.Contains(out, "banknote channel is ON") {
|
||||
t.Errorf("a book with a pack and NO contrast artifact still cannot reach the stop, and still pays "+
|
||||
"for the term table on every draft call — it must be warned:\n%s", out)
|
||||
}
|
||||
})
|
||||
|
||||
// ARM 2 — a contrast artifact, but no pack. The mirror image, and the one a config loader structurally
|
||||
// cannot see: the pack is book data, not pipeline data.
|
||||
t.Run("a contrast artifact but no pack", func(t *testing.T) {
|
||||
srv := newJSONProvider(&reqRec{}, draftEdit)
|
||||
defer srv.Close()
|
||||
bookPath := setupProjectOpts(t, srv.URL, projectOpts{banknote: true,
|
||||
gatesYAML: "\nmining:\n contrast_path: mining-contrast.txt\n"})
|
||||
dir := filepath.Dir(bookPath)
|
||||
writeFile(t, filepath.Join(dir, "mining-contrast.txt"), miningContrastData)
|
||||
if out := open(t, bookPath, false); !strings.Contains(out, "banknote channel is ON") {
|
||||
t.Errorf("a book with a contrast artifact and NO langpack cannot mine either, and must be warned:\n%s", out)
|
||||
}
|
||||
})
|
||||
|
||||
// ARM 3 — `forWrite`. A $0 read path buys nothing, so warning there teaches an operator to ignore the
|
||||
// line on the run where it matters. Planted, dropping the guard entirely stayed green.
|
||||
t.Run("a read-only open is quiet", func(t *testing.T) {
|
||||
srv := newJSONProvider(&reqRec{}, draftEdit)
|
||||
defer srv.Close()
|
||||
bookPath := setupProjectOpts(t, srv.URL, projectOpts{banknote: true})
|
||||
// Premise: this very book WOULD be warned on a write open — otherwise the silence below proves nothing.
|
||||
if out := open(t, bookPath, false); !strings.Contains(out, "banknote channel is ON") {
|
||||
t.Fatalf("premise broken: the write open must warn for this fixture:\n%s", out)
|
||||
}
|
||||
if out := open(t, bookPath, true); strings.Contains(out, "banknote channel is ON") {
|
||||
t.Errorf("a read-only open spends nothing, so it must not warn about what a write run would buy:\n%s", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import (
|
|||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -22,31 +24,75 @@ import (
|
|||
// and would then certify the rot. This is the guarantee that the next flag someone adds cannot land in the
|
||||
// unknown bucket silently: it fails here on the day it is declared, not on the day it is misreported.
|
||||
func TestEveryFlagReasonIsRanked(t *testing.T) {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "disposition.go", nil, 0)
|
||||
// ⚠ EVERY FILE OF THE PACKAGE, not disposition.go alone — the first version of this test named one
|
||||
// file, and a planting that declared a FlagReason anywhere else walked straight past it while the test
|
||||
// still reported «every declared reason is ranked». The guarantee was one file narrower than its own
|
||||
// sentence, which is the defect class this file exists to prevent, committed by the file itself.
|
||||
entries, err := os.ReadDir(".")
|
||||
if err != nil {
|
||||
t.Fatalf("parse disposition.go: %v", err)
|
||||
t.Fatalf("read the package directory: %v", err)
|
||||
}
|
||||
var sources []string
|
||||
for _, e := range entries {
|
||||
n := e.Name()
|
||||
if e.IsDir() || !strings.HasSuffix(n, ".go") || strings.HasSuffix(n, "_test.go") {
|
||||
continue
|
||||
}
|
||||
sources = append(sources, n)
|
||||
}
|
||||
if len(sources) < 10 {
|
||||
t.Fatalf("only %d source files found — the walk stopped seeing the package: %v", len(sources), sources)
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
var decls []ast.Decl
|
||||
for _, name := range sources {
|
||||
f, perr := parser.ParseFile(fset, name, nil, 0)
|
||||
if perr != nil {
|
||||
t.Fatalf("parse %s: %v", name, perr)
|
||||
}
|
||||
decls = append(decls, f.Decls...)
|
||||
}
|
||||
var found int
|
||||
for _, d := range f.Decls {
|
||||
for _, d := range decls {
|
||||
gd, ok := d.(*ast.GenDecl)
|
||||
if !ok || gd.Tok != token.CONST {
|
||||
continue
|
||||
}
|
||||
var lastType string
|
||||
for _, sp := range gd.Specs {
|
||||
vs, ok := sp.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if id, ok := vs.Type.(*ast.Ident); ok {
|
||||
lastType = id.Name
|
||||
}
|
||||
if lastType != "FlagReason" || len(vs.Values) == 0 {
|
||||
// ⚠ THE TYPE MUST BE WRITTEN ON THIS SPEC. An earlier version carried the previous spec's type
|
||||
// forward, which is not how Go types a const block: a spec that has a VALUE and no type is its
|
||||
// own UNTYPED constant and inherits nothing. That version raised a FALSE ALARM — an ordinary
|
||||
// string constant placed after a FlagReason spec in the same block was accused of being an
|
||||
// unranked flag, and a doc-URL constant would have failed this test with a message about the
|
||||
// chapter passport. A test that accuses the innocent is not enforcing its own sentence, and this
|
||||
// one exists to say what IS a declared reason.
|
||||
//
|
||||
// ⚠ AND BOTH GO SPELLINGS COUNT. `X FlagReason = "…"` writes the type in the spec; the equally
|
||||
// ordinary `X = FlagReason("…")` writes it as a CONVERSION, leaving vs.Type nil — and a walk
|
||||
// that looked only at vs.Type was blind to it, which an adversarial pass exhibited as a pair:
|
||||
// the typed spelling of one declaration RED, the conversion spelling of the same declaration
|
||||
// SURVIVED. The sentence that used to stand here — «a missed declaration would trip the found
|
||||
// floor rather than pass silently» — was simply false: one missing constant leaves the count at
|
||||
// 16 against a floor of 15, and nothing trips.
|
||||
if len(vs.Values) == 0 {
|
||||
continue
|
||||
}
|
||||
lit, ok := vs.Values[0].(*ast.BasicLit)
|
||||
if !ok || lit.Kind != token.STRING {
|
||||
var lit *ast.BasicLit
|
||||
switch {
|
||||
case isFlagReasonIdent(vs.Type):
|
||||
lit, _ = vs.Values[0].(*ast.BasicLit)
|
||||
default:
|
||||
call, isCall := vs.Values[0].(*ast.CallExpr)
|
||||
if !isCall || !isFlagReasonIdent(call.Fun) || len(call.Args) != 1 {
|
||||
continue
|
||||
}
|
||||
lit, _ = call.Args[0].(*ast.BasicLit)
|
||||
}
|
||||
if lit == nil || lit.Kind != token.STRING {
|
||||
continue
|
||||
}
|
||||
val, err := strconv.Unquote(lit.Value)
|
||||
|
|
@ -66,8 +112,8 @@ func TestEveryFlagReasonIsRanked(t *testing.T) {
|
|||
// The reader of this test must know it actually read something: a parse that silently matched nothing
|
||||
// would pass forever and guard nothing — the same failure class one layer up.
|
||||
if found < 15 {
|
||||
t.Fatalf("only %d FlagReason constants were read out of disposition.go — the walk stopped matching "+
|
||||
"the source, and this test is no longer checking anything", found)
|
||||
t.Fatalf("only %d FlagReason constants were read out of the package's %d source files — the walk "+
|
||||
"stopped matching the source, and this test is no longer checking anything", found, len(sources))
|
||||
}
|
||||
if len(flagSeverity) != found {
|
||||
t.Errorf("flagSeverity has %d entries for %d declared reasons — a rank exists for a reason that no "+
|
||||
|
|
@ -99,3 +145,10 @@ func TestOffTargetLangIsRankedWithTheContentFailures(t *testing.T) {
|
|||
t.Error("a hard refusal must still outrank an off-target completion")
|
||||
}
|
||||
}
|
||||
|
||||
// isFlagReasonIdent reports whether an expression names the FlagReason type — as a spec's type or as the
|
||||
// function of a conversion. One helper for both spellings, so the walk cannot learn one and forget the other.
|
||||
func isFlagReasonIdent(e ast.Expr) bool {
|
||||
id, ok := e.(*ast.Ident)
|
||||
return ok && id.Name == "FlagReason"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,7 +199,13 @@ func TestTheCorpusStillHoldsItsHardestSpecimens(t *testing.T) {
|
|||
// accounting under its own id — both invisible, because the extremum still held. The corpus is a
|
||||
// frozen measurement, so a digest over every row's accounting is the honest pin: re-taking it is a
|
||||
// deliberate act that re-records this line in the same commit.
|
||||
corpusDigest = "8c77a0947b7bc72b0b62e99291bfbc0955df764635e368cf9b03590606f9d2c7"
|
||||
// ⚠ IT COVERS EVERY FIELD, INCLUDING THE ONES NO MEASUREMENT READS. The first version folded only the
|
||||
// six counting fields, and a planting that rewrote a specimen's `head` — and, worse, its
|
||||
// `provenance` — passed with the whole ledger green. `provenance` is the ADDRESS a specimen was
|
||||
// taken from and the address it would be re-taken from; a corpus whose numbers are right and whose
|
||||
// provenance is wrong is unreproducible in the one way that matters, and nothing else in this file
|
||||
// looks at that field beyond `!= ""`.
|
||||
corpusDigest = "ac4c14ef47e0c42807f47c88abc49da672884864392930fb07ecf4fe71f8fa23"
|
||||
)
|
||||
corpus := loadOffTargetCorpus(t)
|
||||
var offTargets, healthy int
|
||||
|
|
@ -251,9 +257,10 @@ func TestTheCorpusStillHoldsItsHardestSpecimens(t *testing.T) {
|
|||
// The whole population, so that a row which is not an extremum cannot be softened, relabelled, swapped
|
||||
// for an easier one under its own id, or duplicated, without saying so.
|
||||
if got := offTargetCorpusDigest(corpus); got != corpusDigest {
|
||||
t.Errorf("the corpus accounting changed:\n is: %s\n recorded: %s\nEvery row's id, label and "+
|
||||
"four counts feed this. If the corpus was legitimately re-taken, re-record the digest in the same "+
|
||||
"commit and say so in the report; if it was not, find out what edited the fixture.", got, corpusDigest)
|
||||
t.Errorf("the corpus accounting changed:\n is: %s\n recorded: %s\nAll EIGHT fields of every "+
|
||||
"row feed this — id, label, the four counts, provenance and head. If the corpus was legitimately "+
|
||||
"re-taken, re-record the digest in the same commit and say so in the report; if it was not, find "+
|
||||
"out what edited the fixture.", got, corpusDigest)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +271,8 @@ func offTargetCorpusDigest(corpus []offTargetSpecimen) string {
|
|||
sort.Slice(rows, func(i, j int) bool { return rows[i].ID < rows[j].ID })
|
||||
h := sha256.New()
|
||||
for _, s := range rows {
|
||||
fmt.Fprintf(h, "%s|%s|%d|%d|%d|%d\n", s.ID, s.Label, s.SourceScriptRunes, s.TotalRunes, s.Letters, s.TargetScriptLetters)
|
||||
fmt.Fprintf(h, "%s|%s|%d|%d|%d|%d|%s|%s\n", s.ID, s.Label, s.SourceScriptRunes, s.TotalRunes,
|
||||
s.Letters, s.TargetScriptLetters, s.Provenance, s.Head)
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -846,3 +846,142 @@ func TestReconcileLeavesUnpaidAndReworkedUnitsAlone(t *testing.T) {
|
|||
t.Errorf("the sentence's paying count would over-report by one: %+v", rework.stop)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTheVolumeReportIsAskedBeforeTheCountersAreTruedUp pins the ORDER of bound() and reconcile() at the
|
||||
// level where the order exists — the wave driver — because nothing did.
|
||||
//
|
||||
// ⚠ waverun.go's own comment calls this order load-bearing, says it was found by planting, and sends the
|
||||
// reader to TestReconcileMovesAFlaggedCarryOutOfCarried. That test is a unit test of volumeScope: it proves
|
||||
// reconcile moves a flagged carry out of Carried, and it cannot see WHERE the driver asks bound(). Planted —
|
||||
// the bound() block moved to just after reconcile — the whole pipeline package stayed green while
|
||||
// `res.Volume` came back nil, i.e. the run that finished a unit outside its grant and flagged it said
|
||||
// NOTHING about it. That is the disclosure the report exists for, and «the comment explains it» is not a
|
||||
// guarantee; this is the same «a claim about structure passed off as a guarantee» class the round found
|
||||
// four times, here in a comment that is otherwise entirely correct.
|
||||
//
|
||||
// The fixture is the smallest run that can tell the two orders apart: ONE carried unit, nothing left, and
|
||||
// the carry comes back FLAGGED — so bound() is true before reconcile (Carried==1) and false after it
|
||||
// (Carried==0, Flagged==1). A carry that DELIVERS cannot see this, which is why the neighbouring
|
||||
// TestOneCarriedUnitIsStillWorkOutsideTheGrant passes under the planting.
|
||||
func TestTheVolumeReportIsAskedBeforeTheCountersAreTruedUp(t *testing.T) {
|
||||
rec := &reqRec{}
|
||||
var mu sync.Mutex
|
||||
editorRefuses := false
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
body, _ := io.ReadAll(req.Body)
|
||||
rec.record(string(body))
|
||||
if isEditBody(string(body)) {
|
||||
mu.Lock()
|
||||
refuse := editorRefuses
|
||||
mu.Unlock()
|
||||
if !refuse {
|
||||
w.WriteHeader(http.StatusInternalServerError) // run 1: the unit is started and never ships
|
||||
return
|
||||
}
|
||||
writeFakeCompletion(w, "I'm sorry, but I can't help with that request.", "refusal")
|
||||
return
|
||||
}
|
||||
writeFakeCompletion(w, "ЧЕРНОВИК ПЕРЕВОДА", "stop")
|
||||
}))
|
||||
defer srv.Close()
|
||||
bookPath := volumeBook(t, srv.URL, 1)
|
||||
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
||||
|
||||
r1 := newRunner(t, bookPath)
|
||||
if _, err := r1.TranslateBook(ctx); err == nil {
|
||||
t.Fatal("precondition: the editor was supposed to fail this run, leaving one started-but-unshipped unit")
|
||||
}
|
||||
r1.Close()
|
||||
|
||||
mu.Lock()
|
||||
editorRefuses = true
|
||||
mu.Unlock()
|
||||
r2 := newRunner(t, bookPath)
|
||||
defer r2.Close()
|
||||
r2.MaxUnits = 1
|
||||
res, err := r2.TranslateBook(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Premise: the run's ONLY work was the carried unit, and it flagged. Without both halves the assertion
|
||||
// below would pass under either order and prove nothing.
|
||||
if res.Flagged != 1 {
|
||||
t.Fatalf("premise broken: the carried unit must come back FLAGGED, got flagged=%d over %d chunk(s)",
|
||||
res.Flagged, len(res.Chunks))
|
||||
}
|
||||
v := res.Volume
|
||||
if v == nil {
|
||||
t.Fatal("the run finished a unit outside its grant, paid for it, and it FLAGGED — and the run reported " +
|
||||
"no volume at all. bound() is being asked AFTER reconcile has moved that carry out of Carried, so " +
|
||||
"the one run that most needs the disclosure is the one that goes silent")
|
||||
}
|
||||
if !strings.Contains(v.String(), "PAID FOR BUT FLAGGED") {
|
||||
t.Errorf("the report must name the money spent for no readable text: %s", v.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestTheVolumeReportIsAskedBeforeReconcileOnADraftOnlyPipeline is the draft-only twin of the test above,
|
||||
// and it exists because the first version of that pin turned out to be carried by its neighbours.
|
||||
//
|
||||
// ⚠ The bound() decision sits BEFORE the fork into the draft-only and editor paths, so BOTH paths depend on
|
||||
// its position — but only the editor path had a fixture that could tell the two orders apart. A planting
|
||||
// that moved the decision after the editor path's reconcile reddened three draft-only tests for the wrong
|
||||
// reason (they lost the report entirely, not out of order), and a planting that moved it after BOTH
|
||||
// reconciles would have been caught by nothing on this shape. On a draft-only pipeline the SHIPPING wave is
|
||||
// the draft one, so the same carried-and-flagged unit is reachable here and reports the same way.
|
||||
func TestTheVolumeReportIsAskedBeforeReconcileOnADraftOnlyPipeline(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
calls, released := 0, false
|
||||
rec := &reqRec{}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
body, _ := io.ReadAll(req.Body)
|
||||
rec.record(string(body))
|
||||
mu.Lock()
|
||||
calls++
|
||||
n, rel := calls, released
|
||||
mu.Unlock()
|
||||
switch {
|
||||
case rel:
|
||||
// Run 2: the second draft stage answers, and REFUSES — the carried unit is finished and flagged.
|
||||
writeFakeCompletion(w, "I'm sorry, but I can't help with that request.", "refusal")
|
||||
case n == 1:
|
||||
writeFakeCompletion(w, "ЧЕРНОВИК ПЕРЕВОДА", "stop") // stage 1 of run 1 checkpoints
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError) // stage 2 of run 1 dies: started, never shipped
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
eps := []chunktest.Chapter{{ID: "c1", Href: "c1.xhtml", Body: "<p>静かな図書館の朝1。</p>"}}
|
||||
bookPath := setupProjectOpts(t, srv.URL, projectOpts{epub: eps, spine: []string{"c1"}, draftOnly: true, waveWorkers: 1})
|
||||
addSecondDraftStage(t, bookPath)
|
||||
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
||||
|
||||
r1 := newRunner(t, bookPath)
|
||||
if _, err := r1.TranslateBook(ctx); err == nil {
|
||||
t.Fatal("precondition: the second draft stage was supposed to fail this run")
|
||||
}
|
||||
r1.Close()
|
||||
|
||||
mu.Lock()
|
||||
released = true
|
||||
mu.Unlock()
|
||||
r2 := newRunner(t, bookPath)
|
||||
defer r2.Close()
|
||||
r2.MaxUnits = 1
|
||||
res, err := r2.TranslateBook(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Flagged != 1 {
|
||||
t.Fatalf("premise broken: the carried unit must come back FLAGGED on the shipping (draft) wave, got "+
|
||||
"flagged=%d over %d chunk(s)", res.Flagged, len(res.Chunks))
|
||||
}
|
||||
if res.Volume == nil {
|
||||
t.Fatal("a draft-only run finished a unit outside its grant, paid for it, and it FLAGGED — and reported " +
|
||||
"no volume at all. The bound() decision is being taken from counters reconcile has already trued up")
|
||||
}
|
||||
if !strings.Contains(res.Volume.String(), "PAID FOR BUT FLAGGED") {
|
||||
t.Errorf("the report must name the money spent for no readable text: %s", res.Volume.String())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue