Join item 7: a key's history through the owners' read tools; the verdict and its event name each series change
History (A2j finding 1, J4): the indexer's and the assigner's read tools are views.ReadTool over views.KeptSource, which
answers `{key, history: true}` now; modules (20), heard (64) and wants (20) keep more than one revision. Rows in the
view's "each tool answers through the owners' read tools as before": indexer_module_history and indexer_heard answer
the owner's kept history, and a want's history (view_tiers' Since) is read through assigner_read_wants (the row it
replaces held that the owner answered no history).
Series (A2j finding 2, J4): the checker keeps each series change as a delivery.SeriesChange {module, artifact, from,
to} and renders the status line from it; delivery.Verdict.Series holds the case's changes (each module's artifact once),
and the verdict event's series the pull request's (meshdeliverychecker.VerdictHolderEventSeriesItem). An artifact new
on the head is no series change (the lead's ruling). Rows: TestTheIndexersJudgeJudgesTheChangesManifests (the verdict's
series; failed first, empty) and TestTheStatusIsPendingWhileTheCheckRunsAndEndsSuccessOrFailure (the event's; failed
with the fill cut).
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -47,7 +47,9 @@ type Case struct {
|
||||
warnings map[string]string
|
||||
// seriesOf are, by pull request, the changes of a package series it makes (series.go), named in its status line.
|
||||
seriesOf map[string][]string
|
||||
notes []string
|
||||
// seriesChanges are, by pull request, the same changes as structures (series.go), named in the verdict and its event.
|
||||
seriesChanges map[string][]delivery.SeriesChange
|
||||
notes []string
|
||||
// asks are the ids of the asks the check put on the build seats' streams, in order: a try names them (Try.Asks).
|
||||
asks []string
|
||||
// Watch, when set, hears each ask the check puts, as it is put (step nil), and each value its step key takes while
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
@@ -430,6 +431,7 @@ func TestTheStatusIsPendingWhileTheCheckRunsAndEndsSuccessOrFailure(t *testing.T
|
||||
raw, _, _, _ := r.owed.Get(ctx, key)
|
||||
o, _ := delivery.DecodeCheckerOwed(raw)
|
||||
seen = append(seen, o.State)
|
||||
c.series(repoA+"#7", []string{"web/pkg series 0.1 → 0.2"}, []delivery.SeriesChange{{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}})
|
||||
return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "m/a.TestX"}},
|
||||
OutOfStep: map[string][]string{repoA + "#7": {"go/seats/x/contract.go"}}}, nil
|
||||
}}
|
||||
@@ -450,7 +452,9 @@ func TestTheStatusIsPendingWhileTheCheckRunsAndEndsSuccessOrFailure(t *testing.T
|
||||
// DLV-C2 (design 26 *Out of step*): the check's end says one verdict event for the pull request, naming its out-of-step
|
||||
// files; the running status says none.
|
||||
if len(says.said) != 1 || says.said[0].Repository != repoA || says.said[0].Pull != 7 || says.said[0].Head != head1 ||
|
||||
says.said[0].Verdict != delivery.VerdictFail || strings.Join(says.said[0].OutOfStep, ",") != "go/seats/x/contract.go" {
|
||||
says.said[0].Verdict != delivery.VerdictFail || strings.Join(says.said[0].OutOfStep, ",") != "go/seats/x/contract.go" ||
|
||||
// The join (A2j finding 2): and its series changes, as the seat's event names them.
|
||||
len(says.said[0].Series) != 1 || says.said[0].Series[0] != (meshdeliverychecker.VerdictHolderEventSeriesItem{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}) {
|
||||
t.Errorf("verdict events %+v", says.said)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1151,6 +1151,14 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
|
||||
if line := Line(v, cs, cs.Pulls[0], nil, nil, nil); row.seriesWords != "" && !strings.Contains(line, row.seriesWords) || row.seriesWords == "" && strings.Contains(line, "series") {
|
||||
t.Errorf("%s: the line %q, want it to name %q", row.name, line, row.seriesWords)
|
||||
}
|
||||
// The join (A2j finding 2): the verdict names each series change as {module, artifact, from, to}.
|
||||
if want := []delivery.SeriesChange(nil); row.series == "0.2" {
|
||||
if want = []delivery.SeriesChange{{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}}; !reflect.DeepEqual(v.Series, want) {
|
||||
t.Errorf("%s: the verdict's series %+v, want %+v", row.name, v.Series, want)
|
||||
}
|
||||
} else if len(v.Series) != 0 {
|
||||
t.Errorf("%s: the verdict's series %+v, want none", row.name, v.Series)
|
||||
}
|
||||
if len(judged) != 1 || judged[0].Fetch["repository"] != DeliveryRepository || judged[0].Fetch["commit"] != trunk ||
|
||||
judged[0].Package != JudgePackage || judged[0].Dir != JudgeDir {
|
||||
t.Fatalf("%s: judged %+v (verdict %+v)", row.name, judged, v)
|
||||
|
||||
@@ -327,6 +327,7 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) {
|
||||
|
||||
// outcome fills the verdict's levels, times, machines and composition from what ran.
|
||||
func (st *state) outcome(v delivery.Verdict) Outcome {
|
||||
v.Series = st.c.allSeries() // the series changes, as structures (A2j finding 2)
|
||||
if v.State == delivery.VerdictPass || len(v.Levels) == 0 {
|
||||
v.Levels = map[string]delivery.LevelResult{}
|
||||
for level, a := range st.levels {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// A change of a package series is named in the verdict, so the review of the change sees it (design 32 *A package, its
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
func (st *state) seriesChanges(ctx context.Context) {
|
||||
for _, p := range st.c.Pulls {
|
||||
var words []string
|
||||
var changes []delivery.SeriesChange
|
||||
for _, module := range p.Modules {
|
||||
was, now, ok, err := st.manifestPair(ctx, p, module)
|
||||
if err != nil {
|
||||
@@ -25,11 +27,15 @@ func (st *state) seriesChanges(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
if ok {
|
||||
words = append(words, seriesMoved(module, was, now)...)
|
||||
moved := seriesMoved(module, was, now)
|
||||
changes = append(changes, moved...)
|
||||
for _, c := range moved {
|
||||
words = append(words, seriesLine(c))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(words) > 0 {
|
||||
st.c.series(pullKey(p), words)
|
||||
st.c.series(pullKey(p), words, changes)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,8 +71,14 @@ func (st *state) manifestPair(ctx context.Context, p Pull, module string) (was,
|
||||
return was, now, true, nil
|
||||
}
|
||||
|
||||
// seriesMoved are the artifacts of module whose series the head's manifest changes from the trunk's.
|
||||
func seriesMoved(module string, was, now catalogue.Manifest) []string {
|
||||
// seriesLine is a series change as the status line names it: `<module>/<artifact> series <trunk> → <head>`.
|
||||
func seriesLine(c delivery.SeriesChange) string {
|
||||
return fmt.Sprintf("%s/%s series %s → %s", c.Module, c.Artifact, c.From, c.To)
|
||||
}
|
||||
|
||||
// seriesMoved are the artifacts of module whose series the head's manifest changes from the trunk's, by artifact. An
|
||||
// artifact new on the head is no series change (the lead's ruling): it has no series on the trunk.
|
||||
func seriesMoved(module string, was, now catalogue.Manifest) []delivery.SeriesChange {
|
||||
if was.Build == nil || now.Build == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -74,24 +86,50 @@ func seriesMoved(module string, was, now catalogue.Manifest) []string {
|
||||
for _, a := range was.Build.Artifacts {
|
||||
before[a.Name] = a.SeriesOf()
|
||||
}
|
||||
var out []string
|
||||
var out []delivery.SeriesChange
|
||||
for _, a := range now.Build.Artifacts {
|
||||
if old, ok := before[a.Name]; ok && old != a.SeriesOf() {
|
||||
out = append(out, fmt.Sprintf("%s/%s series %s → %s", module, a.Name, old, a.SeriesOf()))
|
||||
out = append(out, delivery.SeriesChange{Module: module, Artifact: a.Name, From: old, To: a.SeriesOf()})
|
||||
}
|
||||
}
|
||||
slices.Sort(out)
|
||||
slices.SortFunc(out, func(a, b delivery.SeriesChange) int { return strings.Compare(a.Artifact, b.Artifact) })
|
||||
return out
|
||||
}
|
||||
|
||||
// series keeps, of a pull request (pullKey), the words of the series changes it makes.
|
||||
func (c *Case) series(key string, words []string) {
|
||||
// series keeps, of a pull request (pullKey), the words of the series changes it makes and the changes themselves.
|
||||
func (c *Case) series(key string, words []string, changes []delivery.SeriesChange) {
|
||||
c.notesMu.Lock()
|
||||
defer c.notesMu.Unlock()
|
||||
if c.seriesOf == nil {
|
||||
c.seriesOf = map[string][]string{}
|
||||
c.seriesOf, c.seriesChanges = map[string][]string{}, map[string][]delivery.SeriesChange{}
|
||||
}
|
||||
c.seriesOf[key] = words
|
||||
c.seriesOf[key], c.seriesChanges[key] = words, changes
|
||||
}
|
||||
|
||||
// SeriesChanges are the series changes a pull request (pullKey) makes, as the verdict and its event name them.
|
||||
func (c *Case) SeriesChanges(key string) []delivery.SeriesChange {
|
||||
c.notesMu.Lock()
|
||||
defer c.notesMu.Unlock()
|
||||
return slices.Clone(c.seriesChanges[key])
|
||||
}
|
||||
|
||||
// allSeries are the series changes of every pull request of the case, each module's artifact once, by module and
|
||||
// artifact: what the verdict names (delivery.Verdict.Series).
|
||||
func (c *Case) allSeries() []delivery.SeriesChange {
|
||||
var out []delivery.SeriesChange
|
||||
seen := map[string]bool{}
|
||||
for _, p := range c.Pulls {
|
||||
for _, s := range c.SeriesChanges(pullKey(p)) {
|
||||
if k := s.Module + "/" + s.Artifact; !seen[k] {
|
||||
seen[k] = true
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
slices.SortFunc(out, func(a, b delivery.SeriesChange) int {
|
||||
return strings.Compare(a.Module+"/"+a.Artifact, b.Module+"/"+b.Artifact)
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// Series are the series changes a pull request (pullKey) makes, as the check named them; none when it makes none.
|
||||
|
||||
@@ -28,8 +28,11 @@ func (ch *Checker) sayVerdict(ctx context.Context, cs *Case, v delivery.Verdict,
|
||||
return
|
||||
}
|
||||
for _, p := range cs.Pulls {
|
||||
e := VerdictEvent{Repository: p.Repository, Pull: int64(p.Number), Head: p.Head, Verdict: v.State,
|
||||
OutOfStep: out.OutOfStep[pullKey(Pull{Repository: p.Repository, Number: p.Number})]}
|
||||
key := pullKey(Pull{Repository: p.Repository, Number: p.Number})
|
||||
e := VerdictEvent{Repository: p.Repository, Pull: int64(p.Number), Head: p.Head, Verdict: v.State, OutOfStep: out.OutOfStep[key]}
|
||||
for _, s := range cs.SeriesChanges(key) {
|
||||
e.Series = append(e.Series, meshdeliverychecker.VerdictHolderEventSeriesItem{Module: s.Module, Artifact: s.Artifact, From: s.From, To: s.To})
|
||||
}
|
||||
if err := ch.Says.Say(ctx, e); err != nil {
|
||||
ch.logf("%s: the verdict of %s#%d was written, and its event could not be said: %v", cs.Key, p.Repository, p.Number, err)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,15 @@ func (s source) Get(key string) (*views.Entry, bool, error) {
|
||||
return &views.Entry{Value: json.RawMessage(v), Revision: uint64(len(key))}, true, nil
|
||||
}
|
||||
|
||||
// kept is a fake owner state that keeps its keys' history, as views.KeptSource does: what the owner's read tool answers a
|
||||
// `history` from.
|
||||
type kept struct {
|
||||
source
|
||||
history map[string][]views.Revision
|
||||
}
|
||||
|
||||
func (k kept) History(key string) ([]views.Revision, error) { return k.history[key], nil }
|
||||
|
||||
// ownersAnswerTheToolsAsBefore is DLV-V1: every view tool over the indexer's and the assigner's states answers through
|
||||
// the owners' read tools (the real owner side, views.Read, over a fake state) what it answered reading the states.
|
||||
func ownersAnswerTheToolsAsBefore(t *testing.T) {
|
||||
@@ -55,6 +64,17 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) {
|
||||
delivery.IndexerModule + "." + delivery.PackagesState: {pkgKey: `{"v":1,"module":"git.novox.be/novox/mesh-sdk/go","commit":"c1"}`},
|
||||
delivery.IndexerModule + "." + graph.State: {},
|
||||
}
|
||||
// The kept history of the indexer's modules and heard and the assigner's wants (each declared with more than one
|
||||
// revision in its owner's manifest), which indexer_module_history, indexer_heard and view_tiers' times read.
|
||||
at := time.Date(2026, 11, 4, 9, 0, 0, 0, time.UTC)
|
||||
const heard1 = `{"v":1,"subject":"s","sequence":41,"repository":"novox/mesh-catalog","commit":"9cdac09a"}`
|
||||
states[delivery.IndexerModule+".heard"] = source{"forge": heard1}
|
||||
histories := map[string]map[string][]views.Revision{
|
||||
delivery.IndexerModule + ".modules": {"a": {{Revision: 7, At: at, Op: "put", Value: json.RawMessage(withNone)},
|
||||
{Revision: 9, At: at.Add(time.Minute), Op: "put", Value: json.RawMessage(withFP2)}}},
|
||||
delivery.IndexerModule + ".heard": {"forge": {{Revision: 3, At: at, Op: "put", Value: json.RawMessage(heard1)}}},
|
||||
delivery.AssignerModule + ".wants": {"want.ace.zsh": {{Revision: 12, At: at, Op: "put", Value: json.RawMessage(`{"fp":"` + fp2A + `"}`)}}},
|
||||
}
|
||||
// The direct reading, as the tools read the states before (the reads the manifest no longer has).
|
||||
st := func(name string) view.StateRead { return fake(states[name]).reader().State }
|
||||
direct := view.Reader{State: st(delivery.IndexerModule + ".modules"), CandidatesState: st(delivery.IndexerModule + ".candidates"),
|
||||
@@ -82,6 +102,14 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) {
|
||||
all, _ := args["values"].(bool)
|
||||
after, _ := args["after"].(string)
|
||||
owner, state, _ := strings.Cut(name, ".")
|
||||
if h, _ := args["history"].(bool); h {
|
||||
// The join (J4): the owner's read tool answers a key's kept history.
|
||||
a, err := views.ReadHistory(owner, state, kept{states[name], histories[name]}, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
a, err := views.Read(owner, state, states[name], nil, k, all, after, time.Unix(0, 0))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -118,14 +146,23 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) {
|
||||
t.Errorf("%s %v through the owner's read tool = %s, %v; reading the state it was %s, %v", row.tool, row.args, gj, gerr, wj, werr)
|
||||
}
|
||||
}
|
||||
// A key's history is asked of the owner's read tool and answered from its kept values (A2j finding 1, J4).
|
||||
out, err := run(t, owned, view.ModuleHistoryTool, map[string]any{"module": "a"})
|
||||
if h, _ := out.(view.ModuleHistory); err != nil || len(h.Values) != 2 || h.Values[0].Revision != 9 || h.Values[1].FP2 != "" || !h.Values[1].At.Equal(at) {
|
||||
t.Errorf("indexer_module_history a through the indexer's read tool = %+v, %v", out, err)
|
||||
}
|
||||
out, err = run(t, owned, view.HeardTool, map[string]any{})
|
||||
if rows, _ := out.(view.HeardAnswer); err != nil || len(rows.Heard) != 1 || rows.Heard[0].Revision != 3 || string(rows.Heard[0].Heard) != heard1 {
|
||||
t.Errorf("indexer_heard through the indexer's read tool = %+v, %v", out, err)
|
||||
}
|
||||
// view_tiers' Since of a want is its first kept revision's time, read through the assigner's read tool.
|
||||
if revs, err := owned.WantsState.History("want.ace.zsh"); err != nil || len(revs) != 1 || !revs[0].At.Equal(at) || revs[0].Revision != 12 {
|
||||
t.Errorf("the want's history through the assigner's read tool = %+v, %v", revs, err)
|
||||
}
|
||||
// Each owner state the tools read was asked of its owner's read tool.
|
||||
for tool, name := range tools {
|
||||
if name != delivery.IndexerModule+"."+graph.State && asked[tool] == 0 {
|
||||
t.Errorf("%s was never asked", tool)
|
||||
}
|
||||
}
|
||||
// A history the owner's read tool does not answer is said, never answered as none.
|
||||
if _, err := run(t, owned, view.ModuleHistoryTool, map[string]any{"module": "a"}); err == nil || !strings.Contains(err.Error(), "no history argument") {
|
||||
t.Errorf("indexer_module_history with an owner answering no history: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user