DLV-C3 follow: the builder hears tag-wanted on the checker's own stream, mesh.stream.<checker>.event.tag-wanted

The checker's family asks is its own stream, whose subjects are mesh.stream.<module>.event.* (ruled on A2c's finding 1);
its verdicts on the same stream are acknowledged and not taken as tag asks.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 13:11:57 +01:00
parent 5fbd84091e
commit 8e60a3e078
2 changed files with 16 additions and 2 deletions
@@ -24,10 +24,10 @@ const forgeStream = "FORGE"
const forgeBranchDeleted = "branch-deleted" const forgeBranchDeleted = "branch-deleted"
// checkerAsksStream is the checker's family `asks` as the bus names it, and tagWantedSubject its event tag-wanted // checkerAsksStream is the checker's family `asks` as the bus names it, and tagWantedSubject its event tag-wanted
// (design 51 *Tagged versions*, DLV-C3). // (design 51 *Tagged versions*, DLV-C3): on the checker's own stream, so its subject is mesh.stream.<module>.event.*.
const ( const (
checkerAsksStream = "ASKS" checkerAsksStream = "ASKS"
tagWantedSubject = "mesh.mod." + delivery.CheckerModule + ".event.tag-wanted" tagWantedSubject = "mesh.stream." + delivery.CheckerModule + ".event.tag-wanted"
) )
// forgeEnd is the part of a forge message the builder reads. // forgeEnd is the part of a forge message the builder reads.
@@ -117,6 +117,10 @@ func (h *branchEnds) again(m jetstream.Msg) {
// tagWanted takes the checker's tag-wanted in: written to tag-asks as builder_tag writes it, answered by its watch. // tagWanted takes the checker's tag-wanted in: written to tag-asks as builder_tag writes it, answered by its watch.
func tagWanted(ctx context.Context, kv jetstream.KeyValue, m jetstream.Msg, by string, now time.Time, logf func(string, ...any)) { func tagWanted(ctx context.Context, kv jetstream.KeyValue, m jetstream.Msg, by string, now time.Time, logf func(string, ...any)) {
if m.Subject() != tagWantedSubject {
_ = m.Ack() // the family's other events (the verdicts) are not the builder's
return
}
var t versions.TagWanted var t versions.TagWanted
err := json.Unmarshal(m.Data(), &t) err := json.Unmarshal(m.Data(), &t)
var key string var key string
@@ -282,6 +282,16 @@ func aBranchsEndIsHeard(t *testing.T) {
if !open.acked || !merged.acked || woken != 2 { if !open.acked || !merged.acked || woken != 2 {
t.Errorf("an open pull acked %v, a merge with nothing tagged acked %v, floors woken %d times", open.acked, merged.acked, woken) t.Errorf("an open pull acked %v, a merge with nothing tagged acked %v, floors woken %d times", open.acked, merged.acked, woken)
} }
// The checker's family holds its verdicts beside tag-wanted: a verdict is acknowledged and not kept; a tag-wanted with
// tag-asks not bound is heard again.
verdict := &wakeMsg{subject: "mesh.stream.mesh-delivery-checker.event.verdict", data: []byte(`{"repository":"o/r"}`)}
wanted := &wakeMsg{subject: tagWantedSubject, data: []byte(`{"repository":"o/r","branch":"feat/x","head":"abc","tag":"feat-x"}`)}
for _, m := range []*wakeMsg{verdict, wanted} {
tagWanted(context.Background(), nil, m, delivery.CheckerModule, time.Now(), t.Logf)
}
if !verdict.acked || verdict.delayed != 0 || wanted.acked || wanted.delayed != versions.RemoveAgainAfter || !strings.HasPrefix(tagWantedSubject, "mesh.stream.") {
t.Errorf("a verdict acked %v (delayed %v), a tag-wanted acked %v (delayed %v)", verdict.acked, verdict.delayed, wanted.acked, wanted.delayed)
}
unreadable := &wakeMsg{subject: merged.subject, data: merged.data} unreadable := &wakeMsg{subject: merged.subject, data: merged.data}
h.acts.Numberer.Versions.Store = failingVersions{} h.acts.Numberer.Versions.Store = failingVersions{}
h.heard(context.Background(), unreadable) h.heard(context.Background(), unreadable)