test(agent): enumerate the orientations instead of patching them one by one
CI / Tidy (pull_request) Successful in 9m21s
CI / Build & Test (pull_request) Successful in 10m3s

Three consecutive rounds found the same rule broken in a different
orientation — content after the pointer, then content before it in the
same sentence — because each round's cases only covered the direction
that round was about, and each fix was then tested only in the direction
I had just thought of. A fourth patch was not the answer; the missing
thing was a harness.

TestBareAbovePointerOrientations crosses every pointer form with every
position content can occupy: alone, before in the same sentence, before
in its own sentence, on its own line, as a list item, after in each of
those, and behind a filler opener. 36 cells, one assertion — bare IFF
there is no content — and each cell first asserts the input really is a
pointer, so a mistyped fixture fails loudly instead of passing vacuously.

It earned itself immediately: three cells failed on the first run, and
the bug was mine and shipped. clauseBoundaryChars gained the em dash last
round, and the cut did start = k + 1 — but LastIndexAny returns the BYTE
index of the boundary rune, and an em dash is three bytes. "Done — as
shown above." sliced mid-rune, left a stray continuation byte in the
remainder that no trim removes, and a genuinely bare pointer stopped
being recovered. Now advances by the rune's width.

Break-check: sixteen mutations, each killed by a named test, control
survives. Two had to be reformulated after the harness started failing
mutations that were only "killed" by breaking the build — including the
one for this very fix.
This commit is contained in:
2026-08-22 00:32:45 -04:00
parent 184627c570
commit 285789c89b
2 changed files with 72 additions and 1 deletions
+7 -1
View File
@@ -3,6 +3,7 @@ package agent
import (
"regexp"
"strings"
"unicode/utf8"
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
)
@@ -190,7 +191,12 @@ func bareAbovePointer(t string) bool {
}
start := 0
if k := strings.LastIndexAny(t[:loc[0]], clauseBoundaryChars); k >= 0 {
start = k + 1
// k is the BYTE index of the boundary rune's first byte, and one of
// those runes is a 3-byte em dash — k+1 would slice into the middle of
// it and leave a stray continuation byte in the remainder, which then
// never trims away and makes a genuinely bare pointer look occupied.
_, w := utf8.DecodeRuneInString(t[k:])
start = k + w
}
rest := strings.TrimSpace(t[:start]) + " " + strings.TrimSpace(t[loc[1]:])
return bareRemainderRe.MatchString(strings.Trim(rest, pointerResidueCutset))
+65
View File
@@ -146,6 +146,71 @@ func TestPointsAbove(t *testing.T) {
}
}
// TestBareAbovePointerOrientations is the anti-drift harness for the one rule
// that kept coming back: a deictic pointer only makes a terminal disposable
// when the pointer is ALL there is.
//
// Three consecutive review rounds found that rule broken in a DIFFERENT
// orientation — content after the pointer, then content before it in the same
// sentence — because each round's cases only covered the orientation that
// round was about, and each fix was tested only in the direction I had just
// thought of. Enumerating the placements is the fix a fourth patch would not
// have been: every pointer form is now checked against every position content
// can occupy, so a new pointer form or a new placement covers the whole grid
// rather than one cell of it.
//
// The rule under test is a single line: bare IFF there is no content.
func TestBareAbovePointerOrientations(t *testing.T) {
// Each pointer form, written so it reads naturally both as a whole
// sentence and as a trailing clause.
pointers := map[string]struct{ sentence, clause string }{
"demonstrative": {"That's the chain above.", "as shown above"},
"imperative": {"See the note above.", "per the note above"},
"copular": {"The breakdown is above.", "which is above"},
}
// Where the answer can sit relative to the pointer. "" = nowhere: the
// pointer is alone, which is the only bare case.
const answer = "Ship Tuesday"
placements := map[string]func(p struct{ sentence, clause string }) string{
"alone": func(p struct{ sentence, clause string }) string { return p.sentence },
"alone as a clause": func(p struct{ sentence, clause string }) string { return p.clause },
"before, same sentence": func(p struct{ sentence, clause string }) string { return answer + ", " + p.clause + "." },
"before, own sentence": func(p struct{ sentence, clause string }) string { return answer + ". " + p.sentence },
"before, own line": func(p struct{ sentence, clause string }) string { return answer + "\n" + p.sentence },
"before, list item": func(p struct{ sentence, clause string }) string { return "- " + answer + "\n- " + p.sentence },
"after, same sentence": func(p struct{ sentence, clause string }) string { return p.clause + ", " + answer + "." },
"after, own sentence": func(p struct{ sentence, clause string }) string { return p.sentence + " " + answer + "." },
"after, own line": func(p struct{ sentence, clause string }) string { return p.sentence + "\n" + answer + "." },
"after a filler opener": func(p struct{ sentence, clause string }) string { return "OK. " + answer + ", " + p.clause + "." },
"filler then pointer": func(p struct{ sentence, clause string }) string { return "OK. " + p.sentence },
"done-dash then pointer": func(p struct{ sentence, clause string }) string {
return "Done — " + p.clause + "."
},
}
// The only terminals with no answer in them.
bare := map[string]bool{
"alone": true,
"alone as a clause": true,
"filler then pointer": true,
"done-dash then pointer": true,
}
for pname, p := range pointers {
for placement, build := range placements {
t.Run(pname+"/"+placement, func(t *testing.T) {
in := build(p)
if !pointsAbove(in) {
t.Fatalf("setup is not a pointer at all, so the case proves nothing: %q", in)
}
want := bare[placement]
if got := bareAbovePointer(in); got != want {
t.Errorf("bareAbovePointer(%q) = %v, want %v", in, got, want)
}
})
}
}
}
func TestIsCitationsOnly(t *testing.T) {
cases := []struct {
name string