diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go index 4930788..3bb3fd1 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go @@ -15,6 +15,7 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/delivery/buildrun" "mesh-delivery-builder/internal/build" ) @@ -31,7 +32,8 @@ func Tools(r build.StuckReader, askAgain func(key string, value []byte) error, q Description: "Every build key that waits (building or queued), oldest first: its module, fingerprint, step words and since when, and each ask it " + "waits on by language with its id, when it was put, its asking (1 for the first; the builder asks an ask again when it can no longer be " + "answered, at most 3 times) and what its build module's steps hold of it (none, or its state on its node since its start). The " + - "seat workers' delivery counts are not readable by the builder. With module, that module's keys only. Read-only.", + "seat workers' delivery counts are not readable by the builder. dead_letters says whether the builder hears the asks the build seats give up " + + "on (issue 734), and why not: heard, a given-up ask is asked again at once; deaf, at its check bound. With module, that module's keys only. Read-only.", Input: map[string]any{"type": "object", "properties": map[string]any{ "module": map[string]any{"type": "string", "description": "only this module's keys"}}}, Run: func(args map[string]any) (any, error) { return r.Stuck(str(args, "module")) }}, @@ -166,7 +168,8 @@ func main() { steps[bm] = read(stdio.State(bm + ".steps")) } reasks := stdio.State(build.ReAsksState) - r := build.StuckReader{Logf: func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-builder-tools] "+f+"\n", a...) }, Builds: read(stdio.State("builds")), ReAsks: read(reasks), Steps: steps} + r := build.StuckReader{Logf: func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-builder-tools] "+f+"\n", a...) }, Builds: read(stdio.State("builds")), ReAsks: read(reasks), Steps: steps, + Hearing: read(stdio.State(buildrun.HearingState))} put := func(key string, value []byte) error { _, err := reasks.Put(key, json.RawMessage(value)) return err diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go index c3223b3..7d71872 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go @@ -151,8 +151,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro asks := buildrun.JetStreamAsks{JS: js} // The asks the build seats gave up on (issue 734): heard from DEAD_LETTERS, each ends the wait for its step at once, // and the lane asks its part again (Builder.waitOnce) instead of waiting for the check bound. + // Heard in the background, and again at a reconnect or an ask while it is not; whether it is heard is kept in the + // state hearing, which builder_stuck says (dead_letters). dead := &buildrun.DeadLetters{} - defer buildrun.HearDeadLetters(ctx, js, delivery.AskLanguages, dead, logger.Printf)() + hearingKV := process.OptionalBucket(ctx, js, delivery.BuilderModule+"_"+buildrun.HearingState, "builder_stuck cannot say whether DEAD_LETTERS is heard", logger.Printf) + hearing := buildrun.NewDeadLetterHearing(ctx, buildrun.JetStreamDeadLetters{JS: js}, delivery.AskLanguages, dead, logger.Printf, + buildrun.SayHearing(ctx, hearingKV, logger.Printf)) + defer hearing.Stop() + conn.SetReconnectHandler(func(*nats.Conn) { hearing.Again() }) // The builder asks the controller, as its own account, whether the artifact store still holds a done key's blobs // (the seat verb held, novox/hq issue 1937) before it leaves the key done. account, err := process.ReadAccount(path) @@ -166,7 +172,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro defer calls.Close() b := &build.Builder{ Claimer: build.Claimer{Store: kvStore{builds}, By: cred.Node + "." + cred.Module}, - Asks: buildrun.Asker{Publisher: asks, Waiter: buildrun.LostAskWaiter{Inner: asks, Dead: dead}, Bound: bound}, + Asks: buildrun.Asker{Publisher: asks, Waiter: buildrun.LostAskWaiter{Inner: asks, Dead: dead, Hearing: hearing}, Bound: bound}, Held: heldOnTheBus{js}, // An ask that can no longer be answered is asked again (issue 1945), read at the step bound: a context's bound. Gone: heldOnTheBus{js}, diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index 2357313..dd89931 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026130046-d96b9fd66069 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026131841-3203358c4cb5 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index 9a97538..ad2ab1a 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026130046-d96b9fd66069 h1:dPwZwGbW8LuT4p2baXuiyVNOWU6XO46kbw46VU5TAUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026130046-d96b9fd66069/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026131841-3203358c4cb5 h1:sBSS509nZDRczmKb/IcHoLtFKJR36eZdQoTMK9tScsU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026131841-3203358c4cb5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/internal/build/run_test.go b/modules/mesh-delivery-builder/internal/build/run_test.go index a65a63b..c8fcd3e 100644 --- a/modules/mesh-delivery-builder/internal/build/run_test.go +++ b/modules/mesh-delivery-builder/internal/build/run_test.go @@ -849,12 +849,17 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { running, _ := json.Marshal(delivery.Step{V: delivery.Version, ID: "mvo53jcm70b5f8", Tool: "delivery ask", Node: "g14", State: delivery.StepRunning, Started: at}) _, reask, _ := NewReAsk("m", fp1, "stuck", "shanks.lead", at) r := StuckReader{Builds: state(map[string]string{key: string(building), done: string(finished)}), ReAsks: state(map[string]string{key: string(reask)}), - Steps: map[string]StateRead{buildrun.GoBuildModule: state(map[string]string{}), ImageBuildModule: state(map[string]string{"g14.mvo53jcm70b5f8": string(running)})}, - Now: func() time.Time { return at.Add(time.Hour) }} + Steps: map[string]StateRead{buildrun.GoBuildModule: state(map[string]string{}), ImageBuildModule: state(map[string]string{"g14.mvo53jcm70b5f8": string(running)})}, + Hearing: state(map[string]string{buildrun.HearingKey: `{"heard":false,"streams":["SEAT_NODE_BUILD_GO"],"why":"DEAD_LETTERS is not heard: SEAT_NODE_BUILD_GO: permissions violation","at":"2026-10-26T13:00:00Z"}`}), + Now: func() time.Time { return at.Add(time.Hour) }} out, err := r.Stuck("") if err != nil { t.Fatal(err) } + // Issue 734, review round 1: a builder that does not hear DEAD_LETTERS says so, with why. + if dl := out["dead_letters"].(map[string]any); dl["heard"] != false || !strings.Contains(dl["why"].(string), "permissions violation") { + t.Errorf("dead_letters = %v, want not heard, saying why", dl) + } stuck := out["stuck"].([]Stuck) if len(stuck) != 1 || stuck[0].Key != key || stuck[0].Age != "1h0m0s" || stuck[0].NoneStep != 1 || stuck[0].ReAsk == nil || stuck[0].ReAsk.Why != "stuck" { t.Fatalf("stuck = %+v, want the building key alone, an hour old, one ask with no step, with its ask again", stuck) diff --git a/modules/mesh-delivery-builder/internal/build/stuck.go b/modules/mesh-delivery-builder/internal/build/stuck.go index 41184c4..50ed667 100644 --- a/modules/mesh-delivery-builder/internal/build/stuck.go +++ b/modules/mesh-delivery-builder/internal/build/stuck.go @@ -106,7 +106,9 @@ type StuckReader struct { ReAsks StateRead // Steps are the build modules' steps states, by build module. Steps map[string]StateRead - Now func() time.Time + // Hearing is the builder's state hearing: whether it hears DEAD_LETTERS (issue 734); without it none is said. + Hearing StateRead + Now func() time.Time // Logf, when set, gets the one line a call logs: the values it read and the time it took. Logf func(format string, args ...any) } @@ -149,6 +151,20 @@ func parallel(n int, read func(i int) error) error { } // Stuck is every key building or queued with its asks, oldest first; with module, that module's only. +// deadLetters says whether the builder hears the asks the build seats give up on (issue 734), and why not: deaf, a lane +// whose ask is given up waits for its check bound (buildrun.HearingHealth over the kept hearing). +func (r StuckReader) deadLetters() map[string]any { + if r.Hearing.Get == nil { + return map[string]any{"heard": false, "why": "the builder's state hearing is not readable here"} + } + raw, _, err := r.Hearing.Get(buildrun.HearingKey) + if err != nil { + return map[string]any{"heard": false, "why": "the builder's state hearing cannot be read: " + err.Error()} + } + a := buildrun.HearingHealth(raw) + return map[string]any{"heard": a.Healthy, "why": a.Why} +} + func (r StuckReader) Stuck(module string) (map[string]any, error) { started := time.Now() reads := 0 @@ -268,6 +284,7 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) { }) answer := map[string]any{"stuck": out, "count": len(out), "delivery_counts": "not readable by the builder: the build seats' workers' information is granted to the delivery view alone (its backlog); an ask given up on is told by the builder at its check bound and asked again"} + answer["dead_letters"] = r.deadLetters() if cut { answer["cut"], answer["note"] = true, fmt.Sprintf("read %d of %d build keys in key order and stopped; give a module to read its keys alone", MaxReads, total) } diff --git a/modules/mesh-delivery-builder/manifest_test.go b/modules/mesh-delivery-builder/manifest_test.go index b629f5c..6679a16 100644 --- a/modules/mesh-delivery-builder/manifest_test.go +++ b/modules/mesh-delivery-builder/manifest_test.go @@ -87,7 +87,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 if m.Module != "mesh-delivery-builder" { t.Errorf("module = %q", m.Module) } - if want := []state{{Name: "builds", History: 5}, {Name: "instances", History: 1}, {Name: "queues", History: 1}, {Name: "reasks", History: 1}}; !reflect.DeepEqual(m.State, want) { // delivery.BuilderBuildsHistory + if want := []state{{Name: "builds", History: 5}, {Name: "hearing", History: 1}, {Name: "instances", History: 1}, {Name: "queues", History: 1}, {Name: "reasks", History: 1}}; !reflect.DeepEqual(m.State, want) { // delivery.BuilderBuildsHistory t.Errorf("state = %+v, want %+v", m.State, want) } // 1887/A4c: the builder chooses no node, so it reads neither presence nor have, invokes no build module's tool and diff --git a/modules/mesh-delivery-builder/module.json b/modules/mesh-delivery-builder/module.json index 94426fc..e4d2859 100644 --- a/modules/mesh-delivery-builder/module.json +++ b/modules/mesh-delivery-builder/module.json @@ -20,6 +20,7 @@ ], "state": [ { "name": "builds", "history": 5 }, + { "name": "hearing", "history": 1 }, { "name": "instances", "history": 1 }, { "name": "queues", "history": 1 }, { "name": "reasks", "history": 1 } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go index c36295c..8e5ec44 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go @@ -12,6 +12,7 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/delivery/buildrun" "mesh-delivery-checker/internal/check" ) @@ -105,7 +106,8 @@ func TryTools(tr Trying) []stdio.Tool { Description: "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, " + "its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; " + check.TryReadTool + " reads " + "where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 wait or run at once, mesh-wide. A repository " + - "the checker checks no level of (one whose trunk the gate does not name) is refused.", + "the checker checks no level of (one whose trunk the gate does not name) is refused. A try whose ask no build node took within the step " + + "bound ends as error; its ask stays in the build seat's stream and may still run later, judged by nobody (issue 734).", Input: map[string]any{"type": "object", "required": []string{"repository", "head"}, "properties": map[string]any{ "repository": map[string]any{"type": "string", "description": "/"}, "head": map[string]any{"type": "string", "description": "the full commit to try, 40 or 64 hex digits"}, @@ -225,7 +227,8 @@ func Tools(r check.Reader) []stdio.Tool { "left_running with why, and not counted. Nothing holds the checker and it has no capacity of its own: a check is an ask on the " + "build seats' streams, whose backlog the delivery view shows (novox/hq ADR 0525). held_by_turn names the candidates whose check an approved " + "merge's turn holds off (novox/hq ADR 0548 §3), with the repository and the merge that waits; the gate's turns tool reads every turn and its " + - "hand-now tool steers one. Read-only.", + "hand-now tool steers one. dead_letters says whether the checker hears the asks the build seats give up on (issue 734), and why not: " + + "deaf, a try or check whose ask is given up waits until its bound. Read-only.", Input: map[string]any{"type": "object", "properties": map[string]any{}}, Run: func(map[string]any) (any, error) { return r.Status() }}, } @@ -245,7 +248,7 @@ func main() { PreviewState: read(stdio.State("previews")), StepStates: map[string]check.StateRead{"mesh-build-go": read(stdio.State("mesh-build-go.steps")), "mesh-build-node": read(stdio.State("mesh-build-node.steps"))}, HaveState: read(stdio.State("node-engine.have")), StepBound: stepBound(), - TurnState: read(stdio.State("mesh-delivery-gate.turns"))} + TurnState: read(stdio.State("mesh-delivery-gate.turns")), HearingState: read(stdio.State(buildrun.HearingState))} r.Runs = runsOf{read(stdio.State("mesh-delivery-indexer.modules"))} // the node-engine at the commit the mesh runs, as the checker reads it tries := stdio.State(check.TriesState) source := stdio.State("mesh-delivery-gate.source") diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go index d3e8e23..4621c46 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go @@ -795,7 +795,7 @@ func TestTheCheckerSaysOwedReadableOnTheBus(t *testing.T) { // R2-1 break } if said.Module != "mesh-delivery-checker" || said.Node != "n1" || !said.Keeps("owed") || !said.Readable("owed") || - !said.Readable("previews") || !said.Readable("verdicts") || !said.Readable("kept") || said.Readable("tries") || len(said.States) != 5 { + !said.Readable("previews") || !said.Readable("verdicts") || !said.Readable("kept") || said.Readable("tries") || said.Readable("hearing") || len(said.States) != 6 { t.Fatalf("the checker said %+v, want its five states with kept, owed, previews and verdicts readable (1929/AC4; 1887 A5b)", said) } } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go index 21a0d2c..e274d4c 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go @@ -116,6 +116,8 @@ func durationEnv(getenv func(string) string, name string, def time.Duration) (ti var keptStates = []announce.State{ // The kept verdicts are read by the Go build module, which takes them for a check ask's unchanged packages (issue 1887's // design limit 1); the checker alone writes them (ADR 0444 clause 5). + // Whether DEAD_LETTERS is heard (issue 734): read by the checker's own status tool alone. + {Name: "hearing"}, {Name: "kept", Readable: true}, {Name: "owed", Readable: true}, {Name: "previews", Readable: true}, @@ -199,9 +201,15 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // free slot (novox/hq ADR 0525 §1, §3): the checker chooses no node and starts no step. asks := buildrun.JetStreamAsks{JS: js} // The asks the build seats gave up on (issue 734): heard from DEAD_LETTERS, each ends the step its asker waits for. + // Heard in the background, and again at a reconnect or an ask while it is not; whether it is heard is kept in the + // state hearing, which checker_status says (dead_letters). dead := &buildrun.DeadLetters{} - defer buildrun.HearDeadLetters(ctx, js, []string{delivery.AskGo, delivery.AskNode}, dead, logger.Printf)() - waiter := buildrun.LostAskWaiter{Inner: asks, Dead: dead} + hearingKV := process.OptionalBucket(ctx, js, check.HearingBucket, "checker_status cannot say whether DEAD_LETTERS is heard", logger.Printf) + hearing := buildrun.NewDeadLetterHearing(ctx, buildrun.JetStreamDeadLetters{JS: js}, []string{delivery.AskGo, delivery.AskNode}, dead, + logger.Printf, buildrun.SayHearing(ctx, hearingKV, logger.Printf)) + defer hearing.Stop() + conn.SetReconnectHandler(func(*nats.Conn) { hearing.Again() }) + waiter := buildrun.LostAskWaiter{Inner: asks, Dead: dead, Hearing: hearing} ch := &check.Checker{ Verdicts: kvStore{buckets[delivery.CheckerVerdictsBucket]}, Owed: kvStore{buckets[delivery.CheckerOwedBucket]}, diff --git a/modules/mesh-delivery-checker/internal/check/lostask.go b/modules/mesh-delivery-checker/internal/check/lostask.go index 4335752..4db60c6 100644 --- a/modules/mesh-delivery-checker/internal/check/lostask.go +++ b/modules/mesh-delivery-checker/internal/check/lostask.go @@ -28,3 +28,7 @@ func IsLostAsk(v delivery.Verdict) bool { func IsNotTaken(v delivery.Verdict) bool { return v.State == delivery.VerdictFail && strings.Contains(v.Failed.Words, buildrun.NotTakenWords) } + +// HearingBucket is the checker's state hearing: whether it hears DEAD_LETTERS (buildrun.Hearing under +// buildrun.HearingKey), which checker_status says (dead_letters). +const HearingBucket = delivery.CheckerModule + "_" + buildrun.HearingState diff --git a/modules/mesh-delivery-checker/internal/check/read.go b/modules/mesh-delivery-checker/internal/check/read.go index ed7eaec..28ed623 100644 --- a/modules/mesh-delivery-checker/internal/check/read.go +++ b/modules/mesh-delivery-checker/internal/check/read.go @@ -8,6 +8,7 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/delivery/buildrun" ) // The read tools (novox/hq ADR 0484 clauses 8 and 13, issue 1686): what the checker stored, as stored, with the line of the @@ -44,7 +45,9 @@ type Reader struct { // Runs says which commit the mesh runs of a module (the indexer's modules), as the checker's decision reads it: the // node-engine a verdict names is compared at its running commit, not at the trunk's (ADR 0548 §6); nil knows none. Runs interface{ CommitOf(module string) string } - Now func() time.Time + // HearingState is the checker's state hearing: whether it hears DEAD_LETTERS (issue 734). + HearingState StateRead + Now func() time.Time } // VerdictRead is a verdict as stored with the full report of the status of each pull request it judged (Report), when the @@ -320,7 +323,29 @@ type Status struct { // HeldByTurn are the candidates whose check an approved merge's turn holds off (ADR 0548 §3), by group: the repository and // the group of the merge that waits. Their check is claimed when that turn goes. HeldByTurn map[string]string `json:"held_by_turn"` - Notes []string `json:"notes,omitempty"` + // DeadLetters says whether the checker hears the asks the build seats give up on (issue 734): deaf, a try or check + // whose ask is given up waits until its bound. + DeadLetters DeadLettersHeard `json:"dead_letters"` + Notes []string `json:"notes,omitempty"` +} + +// DeadLettersHeard is the checker's hearing of DEAD_LETTERS as the status says it: healthy or not, and why. +type DeadLettersHeard struct { + Heard bool `json:"heard"` + Why string `json:"why"` +} + +// deadLetters reads the checker's kept hearing (buildrun.HearingHealth). +func (r Reader) deadLetters() DeadLettersHeard { + if r.HearingState.Get == nil { + return DeadLettersHeard{Why: "the checker's state hearing is not readable here"} + } + raw, err := r.HearingState.Get(buildrun.HearingKey) + if err != nil { + return DeadLettersHeard{Why: "the checker's state hearing cannot be read: " + err.Error()} + } + a := buildrun.HearingHealth(raw) + return DeadLettersHeard{Heard: a.Healthy, Why: a.Why} } // turns reads the gate's merge turns, by repository; nil when they cannot be read, said in a note. @@ -501,6 +526,7 @@ func (r Reader) Status() (Status, error) { } } } + st.DeadLetters = r.deadLetters() st.Notes = append(st.Notes, "nothing holds the checker and it holds no place: running_steps are the build modules' keys that can still be running (started within the step bound, their build module running on the node); a candidate without a verdict has its check ask in a build seat's stream, waiting for a node with a free slot, whose backlog the delivery view shows (ADR 0525 §4, §9)") return st, nil } diff --git a/modules/mesh-delivery-checker/internal/check/read_test.go b/modules/mesh-delivery-checker/internal/check/read_test.go index 0f113c1..6f1b3f9 100644 --- a/modules/mesh-delivery-checker/internal/check/read_test.go +++ b/modules/mesh-delivery-checker/internal/check/read_test.go @@ -144,7 +144,10 @@ func TestTheStatusToolReadsRunningSteps(t *testing.T) { CandidateState: fakeState{ckey: cand}.read(), VerdictState: fakeState{}.read(), HaveState: fakeState{"shanks.mesh-build-go": haveOf(t, delivery.PhaseRunning)}.read(), - Now: func() time.Time { return time.Date(2026, 10, 22, 15, 10, 0, 0, time.UTC) }, + HearingState: fakeState{"dead-letters": []byte(`{"heard":false,"streams":["SEAT_NODE_BUILD_GO"],` + + `"why":"DEAD_LETTERS is not heard, so an ask a build seat gives up on is waited for until its bound: SEAT_NODE_BUILD_GO: permissions violation",` + + `"at":"2026-10-26T13:00:00Z"}`)}.read(), + Now: func() time.Time { return time.Date(2026, 10, 22, 15, 10, 0, 0, time.UTC) }, } st, err := rd.Status() if err != nil { @@ -156,9 +159,13 @@ func TestTheStatusToolReadsRunningSteps(t *testing.T) { if len(st.Unjudged) != 1 || st.Unjudged[0] != "feat/x" { t.Errorf("unjudged = %v", st.Unjudged) } + // Issue 734, review round 1: a checker that does not hear DEAD_LETTERS says so in its status, with why. + if st.DeadLetters.Heard || !strings.Contains(st.DeadLetters.Why, "permissions violation") { + t.Errorf("dead letters = %+v, want not heard, saying why", st.DeadLetters) + } // What cannot be read is said, not guessed. st, err = Reader{}.Status() - if err != nil || len(st.Notes) == 0 { + if err != nil || len(st.Notes) == 0 || st.DeadLetters.Heard { t.Errorf("empty status = %+v, %v", st, err) } } diff --git a/modules/mesh-delivery-checker/manifest_test.go b/modules/mesh-delivery-checker/manifest_test.go index de36137..815ce91 100644 --- a/modules/mesh-delivery-checker/manifest_test.go +++ b/modules/mesh-delivery-checker/manifest_test.go @@ -93,7 +93,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 t.Errorf("module = %q", m.Module) } // delivery.CheckerKeptHistory, CheckerOwedHistory, PreviewsHistory, CheckerVerdictsHistory; the names follow the SDK's buckets. - if want := []state{{Name: "kept", History: 1}, {Name: "owed", History: 5}, {Name: "previews", History: 5}, {Name: "tries", History: 1}, {Name: "verdicts", History: 5}}; !reflect.DeepEqual(m.State, want) { + if want := []state{{Name: "hearing", History: 1}, {Name: "kept", History: 1}, {Name: "owed", History: 5}, {Name: "previews", History: 5}, {Name: "tries", History: 1}, {Name: "verdicts", History: 5}}; !reflect.DeepEqual(m.State, want) { t.Errorf("state = %+v, want %+v", m.State, want) } if want := []string{"mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-node.steps", "mesh-build-node.toolchains", "mesh-controller.assignments", diff --git a/modules/mesh-delivery-checker/module.json b/modules/mesh-delivery-checker/module.json index 53e50b4..a968770 100644 --- a/modules/mesh-delivery-checker/module.json +++ b/modules/mesh-delivery-checker/module.json @@ -10,6 +10,7 @@ { "name": "checks", "events": ["preview.**", "owed.**"] } ], "state": [ + { "name": "hearing", "history": 1 }, { "name": "kept", "history": 1 }, { "name": "owed", "history": 5 }, { "name": "previews", "history": 5 }, diff --git a/modules/mesh-grants/go.mod b/modules/mesh-grants/go.mod index 3d6dc66..423f7c2 100644 --- a/modules/mesh-grants/go.mod +++ b/modules/mesh-grants/go.mod @@ -3,7 +3,7 @@ module mesh-grants go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026131841-3203358c4cb5 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-grants/go.sum b/modules/mesh-grants/go.sum index 8f26273..fa3242c 100644 --- a/modules/mesh-grants/go.sum +++ b/modules/mesh-grants/go.sum @@ -1,11 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026080443-136fa8b2a896 h1:5rHfru5NJ7SK20QOt8YUJSglcn1gsjtLc9AGmWYGnRQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026080443-136fa8b2a896/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026105311-175a5567b8bd h1:ef6y09csev6xY/suFrIFGUXbgvDA29kummFtHcIb3B8= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026105311-175a5567b8bd/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026110029-83ee7769d8ee h1:ZdBMkF4WYJi82jb3xYi9Z2lXN/iMTn9r3SFC122ROY4= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026110029-83ee7769d8ee/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376 h1:by2zBASLb8h5jA1IiYJWykXwLnHYpuZh/zFNa4NsbOU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026131841-3203358c4cb5 h1:sBSS509nZDRczmKb/IcHoLtFKJR36eZdQoTMK9tScsU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026131841-3203358c4cb5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=