novox/hq 04-ISSUES/107. The controller already held a per-node lock while it composed and recorded each send; the order existed and was thrown away at the wire. Each send now takes the next number for its node, one higher than the last, under that hold and before the body exists — so the number is inside what the mesh signs, and a replayed older declaration cannot borrow a newer one's. Zero is not sent. A host reads absence as "no order claimed", which is the shape of every declaration before this, so nothing that worked before changes for a machine sent nothing since numbering existed. One subtlety, and it is the one that would have read every machine as behind for ever: the mesh decides a machine is behind by comparing the digest of what it WOULD send against what it DID send, and a number changes the bytes. The read-only comparison composes with the number the machine was LAST sent, not a fresh one, so it is byte for byte what was sent when nothing else changed. Hosts went first and every machine runs one that understands the field.
78 lines
2.6 KiB
Go
78 lines
2.6 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// A declaration's only identity was the digest of its bytes; the controller already held a per-node
|
|
// lock and recorded each send, so the order existed and was thrown away at the wire (novox/hq
|
|
// 04-ISSUES/107).
|
|
|
|
func TestASendCarriesItsNumberInsideTheSignedBytes(t *testing.T) {
|
|
body, err := sendable{Resources: []map[string]any{{"id": "x", "type": "file"}}, Sequence: 7}.Body()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var env map[string]any
|
|
if err := json.Unmarshal(body, &env); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, _ := env["sequence"].(float64); got != 7 {
|
|
t.Fatalf("the body carries sequence %v, wanted 7", env["sequence"])
|
|
}
|
|
}
|
|
|
|
func TestAnUnnumberedSendIsByteForByteWhatItWasBefore(t *testing.T) {
|
|
// Zero is not sent at all. A host reads absence as "no order claimed" — the shape of every
|
|
// declaration before this — so an older host, or the read-only comparison against a machine
|
|
// sent nothing since sends were numbered, sees exactly the bytes it always saw.
|
|
body, err := sendable{Resources: []map[string]any{{"id": "x", "type": "file"}}}.Body()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var env map[string]any
|
|
if err := json.Unmarshal(body, &env); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, present := env["sequence"]; present {
|
|
t.Fatalf("a send numbered zero put a sequence on the wire: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestEachSendToANodeIsOneHigherAndReadable(t *testing.T) {
|
|
open := aMesh(t)
|
|
record, err := open.inventory.NodeByName(t.Context(), "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Sent nothing since numbering existed: what it would be sent is composed with zero, which is
|
|
// not on the wire, which is what it was actually sent.
|
|
if n, err := open.inventory.Sequence(t.Context(), record.ID); err != nil || n != 0 {
|
|
t.Fatalf("a fresh node reads sequence %d, %v", n, err)
|
|
}
|
|
first, err := open.inventory.NextSequence(t.Context(), record.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := open.inventory.NextSequence(t.Context(), record.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first != 1 || second != 2 {
|
|
t.Fatalf("two sends were numbered %d and %d", first, second)
|
|
}
|
|
// And the read path sees the last one taken, so the comparison composes what was sent.
|
|
if n, err := open.inventory.Sequence(t.Context(), record.ID); err != nil || n != 2 {
|
|
t.Fatalf("after two sends the node reads sequence %d, %v", n, err)
|
|
}
|
|
// Another node counts on its own.
|
|
other, err := open.inventory.NodeByName(t.Context(), "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n, err := open.inventory.NextSequence(t.Context(), other.ID); err != nil || n != 1 {
|
|
t.Fatalf("a second node's first send was numbered %d, %v", n, err)
|
|
}
|
|
}
|