package check import ( "context" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/delivery" ) // One check per delivery (novox/hq ADR 0548): the checker's part. A merge turn holds new checks off its repository (§3); // a trunk or sibling move never ends a running check and carries its verdict once it ended (§1, §5, §6); only a head git // refused gets a whole check after a move (§7); the line of a carried verdict says so (§8). // turnOf is a turn's value on a repository for a delivery of the heads hash. func turnOf(t testing.TB, repo, group, heads string) []byte { t.Helper() at := time.Date(2026, 10, 26, 9, 0, 0, 0, time.UTC) data, err := delivery.EncodeTurn(delivery.Turn{Repository: repo, Waiting: []delivery.TurnWait{{Group: group, Heads: heads, Approved: at, Since: at}}}) if err != nil { t.Fatal(err) } return data } func turnKey(t testing.TB, repo string) string { t.Helper() k, err := delivery.TurnsKey(repo) if err != nil { t.Fatal(err) } return k } // waitFor polls a condition the checker's goroutines make true, failing after two seconds. func waitFor(t testing.TB, what string, ok func() bool) { t.Helper() for deadline := time.Now().Add(2 * time.Second); !ok(); time.Sleep(time.Millisecond) { if time.Now().After(deadline) { t.Fatalf("%s did not happen", what) } } } // pullWithFP is a pull request touching the module m at the fingerprint `src2:` and 64 times the hex digit d. func pullWithFP(repo string, number int, head string, d string) delivery.CandidatePull { p := pullOf(repo, number, head) p.Modules = []string{"m"} p.Fps = map[string]delivery.CandidateModule{"m": {FP2: "src2:" + strings.Repeat(d, 64)}} return p } func TestOneCheckPerDelivery(t *testing.T) { ctx := context.Background() for name, run := range map[string]func(t *testing.T){ // §3: no claim while a turn names one of its repositories; a claim on the turn's delete event. "a turn on its repository holds a new check off until the turn goes": func(t *testing.T) { j := passJudge() r := newRig(t, j) turns := &Turns{} r.ch.Turns = turns turns.Put(turnKey(t, repoA), turnOf(t, repoA, "fix/other", "0123456789ab"), false) cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() if j.count() != 0 || len(r.verdicts.vals) != 0 { t.Fatalf("judged %d times with a turn on %s, verdicts %v", j.count(), repoA, r.verdicts.vals) } if held := r.ch.Held()[keyOf(t, "feat/x", cand)]; !strings.Contains(held, repoA) || !strings.Contains(held, "fix/other") { t.Errorf("the hold is said as %q, want the turn's repository and group", held) } if !turns.Put(turnKey(t, repoA), nil, true) { t.Fatal("the turn's delete changed nothing") } r.ch.Reconsider(ctx) // what the watch of turns does on the delete r.ch.Wait() if j.count() != 1 || r.verdict(t, keyOf(t, "feat/x", cand)).State != delivery.VerdictPass { t.Errorf("judged %d times after the turn went, want once", j.count()) } }, "a turn on another repository, or of its own heads, holds nothing": func(t *testing.T) { j := passJudge() r := newRig(t, j) turns := &Turns{} r.ch.Turns = turns cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) c, _ := delivery.DecodeCandidate(cand) turns.Put(turnKey(t, repoSDK), turnOf(t, repoSDK, "fix/other", "0123456789ab"), false) turns.Put(turnKey(t, repoA), turnOf(t, repoA, "feat/x", GateHeads(c)), false) r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() if j.count() != 1 { t.Errorf("judged %d times, want once: the turns hold another repository and its own merge", j.count()) } }, // §3, the race: a check claimed before the gate's turn is judged on the commit it read, never restarted. "a check claimed before a turn was written runs to its verdict": func(t *testing.T) { release, started := make(chan struct{}), make(chan struct{}) j := &judgeFunc{f: func(context.Context, *Case, int) (Outcome, error) { close(started) <-release return Outcome{Verdict: passing()}, nil }} r := newRig(t, j) turns := &Turns{} r.ch.Turns = turns cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) r.ch.Candidate(ctx, "feat%2Fx", cand) <-started turns.Put(turnKey(t, repoA), turnOf(t, repoA, "fix/other", "0123456789ab"), false) r.ch.Reconsider(ctx) close(release) r.ch.Wait() if j.count() != 1 || r.verdict(t, keyOf(t, "feat/x", cand)).State != delivery.VerdictPass { t.Errorf("judged %d times; the running check is not ended by the turn", j.count()) } }, // §1, §5: a trunk move during a running check calls the judge once, writes its verdict, keeps the status pending (a pass // on an old trunk is never shown), then asks one carry, on the recheck stream, and no check. "a trunk move during a check ends nothing and asks one carry after it": func(t *testing.T) { var r *rig var kinds []string var from []map[string]string judged := make(chan struct{}) j := &judgeFunc{f: func(ctx context.Context, c *Case, call int) (Outcome, error) { kinds, from = append(kinds, c.Kind), append(from, c.CarriedFrom) if call == 1 { r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS}) // the trunk moves while it runs r.ch.TrunkMoved(ctx) // as the watch of the source says it close(judged) } else if s := owedState(t, r, repoA, 7, head1); s != "pending" { t.Errorf("the status during the carry is %q, want pending: the pass on the old trunk is never shown", s) } return Outcome{Verdict: passing()}, nil }} r = newRig(t, j) cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) r.ch.Candidate(ctx, "feat%2Fx", cand) <-judged r.ch.Wait() v := r.verdict(t, keyOf(t, "feat/x", cand)) if j.count() != 2 || kinds[0] != KindNew || kinds[1] != KindTrunk || from[0] != nil || from[1][repoA] != trunkA { t.Fatalf("judged %d times as %v carried from %v; want a whole check, then one carry from %s", j.count(), kinds, from, trunkA[:12]) } if v.State != delivery.VerdictPass || v.Trunk[repoA] != trunkA2 || v.CarriedFrom[repoA] != trunkA { t.Errorf("the verdict is %s on %v carried from %v; want a pass on the new trunk carried from the old", v.State, v.Trunk, v.CarriedFrom) } if s := owedState(t, r, repoA, 7, head1); s != "success" { t.Errorf("the status after the carry is %q, want success", s) } if r.verdicts.creates[keyOf(t, "feat/x", cand)] != 1 { t.Error("the verdict key was made again: a carry rewrites it") } }, // §5: the composition is kept when no fingerprint of a module it composed changed, and runs again otherwise. "a carry keeps a passed composition while the fingerprints stay the same": func(t *testing.T) { var kept []bool j := &judgeFunc{f: func(_ context.Context, c *Case, _ int) (Outcome, error) { kept = append(kept, c.Composition != nil) v := passing() v.Composition = &delivery.Composition{State: delivery.CompositionPass, SDK: "v0.1.14-x", FPSet: c.FPSet} return Outcome{Verdict: v}, nil }} r := newRig(t, j) cand := candidateOf(t, "feat/x", pullWithFP(repoA, 7, head1, "a")) r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS}) r.ch.TrunkMoved(ctx) // the indexer's fingerprints of the candidate stay the same r.ch.Wait() r.ch.Candidate(ctx, "feat%2Fx", candidateOf(t, "feat/x", pullWithFP(repoA, 7, head1, "b"))) // a fingerprint changed with the move r.setTrunks(t, map[string]string{repoA: trunkA, repoSDK: trunkS}) r.ch.TrunkMoved(ctx) r.ch.Wait() if len(kept) != 3 || kept[0] || !kept[1] || kept[2] { t.Errorf("the composition was kept in the checks %v; want composed, kept on the carry, composed again once a fingerprint changed", kept) } }, // §6: a sibling's move puts the verdict out of date, its status pending saying it is carried, and the carry's verdict // names the sibling's old commit; the status follows the carry's verdict. Which packages run is the digest's (only // those that name MESH_CHECK_BESIDE change with a sibling), the build module's part. "a sibling's move is carried and the status follows the carry": func(t *testing.T) { var r *rig var from map[string]string j := &judgeFunc{f: func(_ context.Context, c *Case, call int) (Outcome, error) { if call == 2 { from = c.CarriedFrom if s := owedState(t, r, repoA, 7, head1); s != "pending" { t.Errorf("the status during the sibling's carry is %q, want pending", s) } } return Outcome{Verdict: passing(), Beside: map[string]string{catalogueRepository: r.trunks.Heads()[catalogueRepository]}}, nil }} r = newRig(t, j) r.setTrunks(t, map[string]string{repoA: trunkA, repoSDK: trunkS, catalogueRepository: trunkS}) cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() if b := r.verdict(t, keyOf(t, "feat/x", cand)).Beside; b[catalogueRepository] != trunkS { t.Fatalf("the verdict names the siblings %v, want the catalogue at %s", b, trunkS[:12]) } r.setTrunks(t, map[string]string{repoA: trunkA, repoSDK: trunkS, catalogueRepository: trunkS2}) r.ch.TrunkMoved(ctx) r.ch.Wait() v := r.verdict(t, keyOf(t, "feat/x", cand)) if j.count() != 2 || from[catalogueRepository] != trunkS || v.CarriedFrom[catalogueRepository] != trunkS || v.Beside[catalogueRepository] != trunkS2 { t.Errorf("judged %d times, carried from %v, verdict beside %v carried from %v; want one carry over the catalogue's move", j.count(), from, v.Beside, v.CarriedFrom) } if s := owedState(t, r, repoA, 7, head1); s != "success" { t.Errorf("the status after the carry is %q, want success", s) } }, // §7: a carry whose merge conflicts fails naming the files and asks no check; a later move onto which the head merges // cleanly asks a whole check, not a carry. "a carry git refuses fails naming the files, and the next move asks a whole check": func(t *testing.T) { var kinds []string var from []map[string]string j := &judgeFunc{f: func(_ context.Context, c *Case, call int) (Outcome, error) { kinds, from = append(kinds, c.Kind), append(from, c.CarriedFrom) if call == 2 { return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "ace", Words: repoA + "#7 " + ConflictWords + "go.mod"}}}, nil } return Outcome{Verdict: passing()}, nil }} r := newRig(t, j) cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS}) r.ch.TrunkMoved(ctx) r.ch.Wait() r.ch.Reconsider(ctx) // anything else that looks again asks nothing of a conflict r.ch.Wait() if v := r.verdict(t, keyOf(t, "feat/x", cand)); j.count() != 2 || !IsConflictFailure(v) || !strings.Contains(v.Failed.Words, "go.mod") { t.Fatalf("judged %d times, verdict %+v; want the carry failed naming go.mod and nothing asked after it", j.count(), v.Failed) } r.setTrunks(t, map[string]string{repoA: trunkA, repoSDK: trunkS}) r.ch.TrunkMoved(ctx) r.ch.Wait() if j.count() != 3 || kinds[1] != KindTrunk || from[1] == nil || kinds[2] != KindNew || from[2] != nil { t.Errorf("checks %v carried from %v; want a whole check, a carry, then a whole check after the conflict", kinds, from) } }, // §8, §6: the line of a carried verdict names the head on the new trunk commit first, as the gate and the merge tool // read a line, then where it was carried from and what the carry ran and kept; while it is carried it is pending and says so. "the line of a carry says where it was carried from": func(t *testing.T) { cs, p := lineCase(t, pullOf(repoA, 7, head1)) v := passVerdict() v.Trunk = map[string]string{repoA: trunkA} v.Beside = map[string]string{catalogueRepository: trunkS2} v.CarriedFrom = map[string]string{repoA: trunkA2, catalogueRepository: trunkS} line := Line(v, cs, p, nil, nil, nil) if !strings.HasPrefix(line, "pass 111111111111 on aaaaaaaaaaaa | carried from abababababab, mesh-catalog 555555555555 | unit 12 run") { t.Errorf("the carried line = %q", line) } if head, trunk := checkWordsOf(line); head != head1[:12] || trunk != trunkA[:12] { t.Errorf("the line names %s on %s first, want the head on the new trunk", head, trunk) } cs.CarriedFrom = map[string]string{repoA: trunkA2} pending := Line(delivery.Verdict{State: delivery.VerdictRunning}, cs, p, nil, nil, nil) if !strings.HasPrefix(pending, "being carried to 111111111111 on aaaaaaaaaaaa | from abababababab") { t.Errorf("the pending line of a carry = %q", pending) } }, // §3 (review round 1): two approved merges in one turn whose trunk moved each get their carry; neither holds the other's. "two merges waiting in one turn each get their carry": func(t *testing.T) { var r *rig j := &judgeFunc{f: func(context.Context, *Case, int) (Outcome, error) { return Outcome{Verdict: passing()}, nil }} r = newRig(t, j) candA, candC := candidateOf(t, "feat/a", pullOf(repoA, 7, head1)), candidateOf(t, "feat/c", pullOf(repoA, 8, head2)) r.ch.Candidate(ctx, "feat%2Fa", candA) r.ch.Candidate(ctx, "feat%2Fc", candC) r.ch.Wait() a, _ := delivery.DecodeCandidate(candA) c, _ := delivery.DecodeCandidate(candC) at := time.Date(2026, 10, 26, 9, 0, 0, 0, time.UTC) turn, err := delivery.EncodeTurn(delivery.Turn{Repository: repoA, Waiting: []delivery.TurnWait{ {Group: "feat/a", Heads: GateHeads(a), Approved: at, Since: at}, {Group: "feat/c", Heads: GateHeads(c), Approved: at.Add(time.Minute), Since: at}}}) if err != nil { t.Fatal(err) } turns := &Turns{} turns.Put(turnKey(t, repoA), turn, false) r.ch.Turns = turns r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS}) // a third merge moves the trunk under both r.ch.TrunkMoved(ctx) r.ch.Wait() for _, cand := range [][]byte{candA, candC} { group := map[bool]string{true: "feat/a", false: "feat/c"}[string(cand) == string(candA)] if v := r.verdict(t, keyOf(t, group, cand)); v.Trunk[repoA] != trunkA2 || v.CarriedFrom[repoA] != trunkA { t.Errorf("%s: verdict on %v carried from %v; want its carry claimed and done", group, v.Trunk, v.CarriedFrom) } } }, // §6 (review round 1): the node-engine read at the commit the mesh runs is named, and a new running commit is carried. "a new running node-engine is carried": func(t *testing.T) { runs := runsAt{nodeEngineModule: trunkS} j := &judgeFunc{f: func(_ context.Context, c *Case, _ int) (Outcome, error) { return Outcome{Verdict: passing(), Beside: map[string]string{nodeEngineRepo: runs[nodeEngineModule]}}, nil }} r := newRig(t, j) r.ch.Runs = runs cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() r.ch.TrunkMoved(ctx) // nothing moved: nothing is carried r.ch.Wait() runs[nodeEngineModule] = trunkS2 r.ch.TrunkMoved(ctx) // what the indexer's watch does when mesh-host's running commit changes r.ch.Wait() v := r.verdict(t, keyOf(t, "feat/x", cand)) if j.count() != 2 || v.CarriedFrom[nodeEngineRepo] != trunkS || v.Beside[nodeEngineRepo] != trunkS2 { t.Errorf("judged %d times, verdict beside %v carried from %v; want one carry over the node-engine's new running commit", j.count(), v.Beside, v.CarriedFrom) } }, // §5: a carry that fails names the move it failed on. "a carry that fails names the move": func(t *testing.T) { j := &judgeFunc{f: func(_ context.Context, c *Case, call int) (Outcome, error) { if call == 1 { return Outcome{Verdict: passing()}, nil } return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "unit: m/a.TestX", Words: "want 4"}}}, nil }} r := newRig(t, j) cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS}) r.ch.TrunkMoved(ctx) r.ch.Wait() v := r.verdict(t, keyOf(t, "feat/x", cand)) if v.State != delivery.VerdictFail || v.CarriedFrom != nil || !strings.HasPrefix(v.Failed.Words, "want 4") || !strings.Contains(v.Failed.Words, repoA+" "+trunkA[:12]+" to "+trunkA2[:12]) { t.Errorf("the failed carry is %s carried from %v with words %q; want the test's words, then the move", v.State, v.CarriedFrom, v.Failed.Words) } }, } { t.Run(name, run) } } // checkWordsOf is the head and trunk a line names first, as the gate reads it (mesh-delivery-gate onestatus.go checkLine). func checkWordsOf(line string) (string, string) { for _, part := range strings.Split(line, " | ") { f := strings.Fields(part) for i := 0; i+2 < len(f); i++ { if f[i+1] == "on" && len(f[i]) == 12 && len(f[i+2]) == 12 { return f[i], f[i+2] } } } return "", "" }