DLV-B9 readers: the indexer and the view pass over build-log entries that are no build's start or end

The builder writes design 51's old-form close into its build log; the indexer's Builds.Put refused it
(no repository) and the view's history stopped on it. Both read the entry's event first and skip any other
kind, whatever its form.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 13:06:57 +01:00
parent 5865d3c632
commit 606e2b9212
5 changed files with 59 additions and 5 deletions
@@ -1,6 +1,7 @@
package index
import (
"encoding/json"
"fmt"
"maps"
"slices"
@@ -28,9 +29,13 @@ type Builds struct {
func NewBuilds() *Builds { return &Builds{records: map[string]delivery.BuildRecord{}} }
// Put takes one entry of the builder's log. A build's end is an outcome: done registers, failed or queued does not; its
// start is a request, which registers nothing either. An entry that does not read, or names no module or repository, is
// refused and said.
// start is a request, which registers nothing either. An entry of another kind (the old reference form's close, design 51
// *The node-engine's own build*) is no build and is passed over, whatever its form. A build's entry that does not read, or
// names no module or repository, is refused and said.
func (b *Builds) Put(value []byte) error {
if !IsBuildEntry(value) {
return nil
}
e, err := delivery.DecodeBuildLogEntry(value)
if err != nil {
return err
@@ -179,3 +184,16 @@ func (b *Builds) Registered() []Registration {
}
return out
}
// IsBuildEntry says whether an entry of the builder's log is a build's start or end, read from its event alone, so an
// entry of another kind is passed over before its form is read. One whose event cannot be read is taken as a build's, so
// its decoding says what is wrong with it.
func IsBuildEntry(value []byte) bool {
var e struct {
Event *string `json:"event"`
}
if json.Unmarshal(value, &e) != nil || e.Event == nil {
return true
}
return *e.Event == delivery.BuildStartedEvent || *e.Event == delivery.BuildEndedEvent
}
@@ -23,6 +23,9 @@ import (
// Imports that cannot be resolved (a package the forge cannot list) are kept with no ImportForm: the next recompute that
// resolves them writes the import form, one rebuild too many, never a stale module.
func (ix *Indexer) ApplyBuildEnd(ctx context.Context, value []byte) error {
if !IsBuildEntry(value) {
return nil
}
e, err := delivery.DecodeBuildLogEntry(value)
if err != nil {
return err
@@ -3,6 +3,7 @@ package index
import (
"context"
"errors"
"fmt"
"strings"
"testing"
@@ -313,4 +314,17 @@ func TestARecordOfAPullRequestDoesNotReplaceTheRegisteredRef(t *testing.T) { //
if got := b.RegistrationsOf("photos"); len(got) != 1 || got[0].Ref != "main" {
t.Fatalf("registrations = %+v", got)
}
// An entry of another kind in the builder's log (the old reference form's close, design 51), of this form or another,
// is no build: passed over, never refused.
for _, other := range []string{
fmt.Sprintf(`{"v":%d,"event":"old-form-closed","module":"node-engine","at":"2026-10-04T09:00:00Z"}`, delivery.Version),
`{"v":99,"event":"old-form-closed","at":"2026-10-04T09:00:00Z","nodes":["ace"]}`,
} {
if err := b.Put([]byte(other)); err != nil {
t.Errorf("an entry of another kind is refused: %v", err)
}
}
if got := b.RegistrationsOf("photos"); len(got) != 1 || got[0].Ref != "main" {
t.Fatalf("after entries of another kind, registrations = %+v", got)
}
}
@@ -209,7 +209,9 @@ func TestBuilderBuildsListsKeysAsStored(t *testing.T) {
r.BuildLog = func(at time.Time, keys string, limit int) (*stdio.LogPage, error) {
asked = keys
return &stdio.LogPage{Entries: []stdio.LogEntry{{Sequence: 1, Value: entry(delivery.BuildStartedEvent, 0)},
{Sequence: 2, Value: entry(delivery.BuildEndedEvent, 90*time.Second)}, {Sequence: 3, Value: entry(delivery.BuildStartedEvent, time.Hour)}}}, nil
{Sequence: 2, Value: entry(delivery.BuildEndedEvent, 90*time.Second)}, {Sequence: 3, Value: entry(delivery.BuildStartedEvent, time.Hour)},
// an entry of another kind (the old reference form's close, design 51), of a form no build has: passed over
{Sequence: 4, Value: []byte(`{"v":99,"event":"old-form-closed","at":"2026-10-29T23:00:00Z","nodes":["ace"]}`)}}}, nil
}
out, err = run(t, r, view.BuildHistoryTool, map[string]any{"module": "netcheck", "since": t0.Format(time.RFC3339)})
if h, _ := out.(view.HistoryAnswer); err != nil || len(h.Builds) != 1 || h.Builds[0].Seconds != 90 || h.Builds[0].Outcome != delivery.BuildDone || asked != "netcheck" || h.More {
@@ -870,8 +870,9 @@ type HistoryAnswer struct {
}
// BuildHistory is every build of module (every module when empty) that ended at or after since, oldest first, each with its
// duration from the start the log holds of the same module and fingerprint before it (delivery.BuildDuration). An entry that
// does not read is refused, naming its sequence.
// duration from the start the log holds of the same module and fingerprint before it (delivery.BuildDuration). An entry of
// another kind than a build's start or end is passed over, whatever its form; a build's entry that does not read is
// refused, naming its sequence.
func (r Reader) BuildHistory(module string, since time.Time) (HistoryAnswer, error) {
out := HistoryAnswer{Since: since.UTC(), Builds: []HistoryRow{}}
if r.BuildLog == nil {
@@ -894,6 +895,9 @@ func (r Reader) BuildHistory(module string, since time.Time) (HistoryAnswer, err
}
var entries []delivery.BuildLogEntry
for _, le := range page.Entries {
if !isBuildEntry(le.Value) {
continue // an entry of another kind (the old reference form's close, design 51) is no build
}
e, err := delivery.DecodeBuildLogEntry(le.Value)
if err != nil {
return out, fmt.Errorf("the builder's log %s, entry %d: %w", delivery.BuildLogRead, le.Sequence, err)
@@ -923,3 +927,16 @@ func (r Reader) BuildHistory(module string, since time.Time) (HistoryAnswer, err
out.More = page.Next != 0
return out, nil
}
// isBuildEntry says whether an entry of the builder's log is a build's start or end, read from its event alone, so an entry
// of another kind is passed over before its form is read. One whose event cannot be read is taken as a build's, so its
// decoding says what is wrong with it.
func isBuildEntry(value []byte) bool {
var e struct {
Event *string `json:"event"`
}
if json.Unmarshal(value, &e) != nil || e.Event == nil {
return true
}
return *e.Event == delivery.BuildStartedEvent || *e.Event == delivery.BuildEndedEvent
}