package main import ( "context" "testing" "github.com/novox/mesh-controller/internal/inventory" ) // A declaration composed earlier is numbered lower than one composed later, whatever order the two // are sent in (novox/hq issue 204). The number used to be taken at send time, after composing, so a // declaration composed before an assignment changed and sent after a newer one carried the higher // number — and the machine, which refuses a lower number, took the older content as the mesh's // newest word. Taken before the composition reads anything, the order of numbers is the order of // compositions, and the host's refusal does what it is for. func TestADeclarationComposedEarlierIsNumberedLowerWhateverOrderItIsSent(t *testing.T) { allot := numbered() var composed []string compose := func(stamp string) func(string) (sendable, error) { return func(node string) (sendable, error) { composed = append(composed, stamp) return sendable{Resources: []map[string]any{{"id": node + "." + stamp}}}, nil } } // Composed first — before an assignment changed — and sent last. stale, _ := composeEach([]string{"anchor"}, allot, compose("before")) // Composed after the change, sent first. fresh, _ := composeEach([]string{"anchor"}, allot, compose("after")) if stale[0].declared.Sequence != 1 || fresh[0].declared.Sequence != 2 { t.Fatalf("the numbers do not follow the compositions: before=%d after=%d", stale[0].declared.Sequence, fresh[0].declared.Sequence) } // Sent in the other order, the numbers do not change — so the machine that has applied the // fresh one (2) refuses the stale one (1) when it arrives late. if !(stale[0].declared.Sequence < fresh[0].declared.Sequence) { t.Fatal("a declaration composed earlier must carry the lower number, however late it is sent") } if len(composed) != 2 || composed[0] != "before" { t.Fatalf("compositions happened in an unexpected order: %v", composed) } } // The number is taken before the first read of the composition, not after it: an allotter that // fails leaves nothing composed for that machine, and the others are still composed. func TestTheNumberIsTakenBeforeComposingAndItsFailureIsARefusal(t *testing.T) { calls := 0 allot := func(node string) (int64, error) { if node == "anchor" { return 0, context.DeadlineExceeded } return 7, nil } sending, refusals := composeEach([]string{"anchor", "laptop"}, allot, func(node string) (sendable, error) { calls++ if node == "anchor" { t.Fatal("anchor was composed although its number could not be taken") } return sendable{}, nil }) if calls != 1 || len(sending) != 1 || sending[0].node != "laptop" || sending[0].declared.Sequence != 7 { t.Fatalf("laptop should be composed with its number and anchor refused: %v / %v", sending, refusals) } if len(refusals) != 1 { t.Fatalf("anchor's failed number should be a refusal naming it: %v", refusals) } } // What was sent is written down even when the sender's context is already cancelled (issue 204): a // controller replaced mid-send had told the machine and never recorded it, so status read "applied, // current" over a machine that had just been sent something else. func TestASendIsRecordedEvenWhenTheSenderIsBeingCancelled(t *testing.T) { inv := inventory.ForTest(t) ctx, cancel := context.WithCancel(t.Context()) if _, err := inv.AddNode(ctx, "anchor"); err != nil { t.Fatal(err) } cancel() // the sender is going away: its context is cancelled between the send and the record body := []byte(`{"declaration":1,"resources":[]}`) digest, err := recordSent(ctx, inv, "anchor", body) if err != nil { // NodeByName on the cancelled context may itself refuse; the record must still be possible // through the detached context, so look the node up again on a live one. t.Fatalf("recording a send after cancellation failed: %v", err) } outstanding, err := inv.Outstanding(t.Context(), "anchor") if err != nil { t.Fatal(err) } if outstanding != digest || digest != digestOf(body) { t.Fatalf("the send was not recorded: outstanding %q, sent %q", outstanding, digest) } }