A delivery and the five-minute reconcile were two paths that applied, ordered only by a lock, and each order it allowed was met live (issues 257, 261, 267). Now both only enqueue: one worker takes the newest declaration held when it starts, applies it once and makes one report, and reports leave in the order they are made. A declaration may carry the controller's lease epoch beside its sequence; one older than what this node applied is refused before anything is touched, counted, logged and reported. A report carries the declaration's epoch and sequence and the host's own report sequence, kept on disk so it goes on increasing across restarts and self-updates. Without an epoch, today's behaviour stands.
78 lines
3.0 KiB
Go
78 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/link"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// **A declaration from an older lease epoch is refused before anything is applied** (novox/hq to-be
|
|
// 45 §6, ADR 0227 rule 2). The controller that sent it lost its lease and goes on sending; this node
|
|
// applied the holder's. Refused on the host's own apply path — the one the queue's worker runs — before
|
|
// the machine is read or touched, with a report naming what was refused and what is held, and the
|
|
// kept declaration left as it was.
|
|
func TestADeclarationFromAnOlderEpochIsRefusedBeforeAnythingIsApplied(t *testing.T) {
|
|
dir := t.TempDir()
|
|
opts := options{state: filepath.Join(dir, "state.json")}
|
|
_, private, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
target := filepath.Join(dir, "a.conf")
|
|
declared := func(epoch, sequence int64) store.Declared {
|
|
body, err := json.Marshal(map[string]any{"declaration": 1, "epoch": epoch, "sequence": sequence,
|
|
"resources": []any{map[string]any{"id": "a", "type": "file", "path": target, "content": "x\n"}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return store.Declared{Declaration: body, Signature: ed25519.Sign(private, body)}
|
|
}
|
|
held := declared(57, 3)
|
|
if err := store.SaveDeclared(store.DeclaredPath(opts.state), held); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stale := declared(41, 12)
|
|
report := applyAndKeep(context.Background(), opts, stale.Declaration, &stale, nil, nil)
|
|
if report.OlderThan == nil || *report.OlderThan != (link.Order{Epoch: 57, Sequence: 3}) ||
|
|
report.Order != (link.Order{Epoch: 41, Sequence: 12}) || report.Declared != digestOf(stale.Declaration) ||
|
|
report.Refused == "" {
|
|
t.Fatalf("the refusal does not name what was refused and what is held: %+v", report)
|
|
}
|
|
if _, err := os.Stat(target); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatal("the refused declaration touched the machine")
|
|
}
|
|
kept, err := store.ReadDeclared(store.DeclaredPath(opts.state))
|
|
if err != nil || string(kept.Declaration) != string(held.Declaration) {
|
|
t.Fatal("the refused declaration replaced what this node kept")
|
|
}
|
|
}
|
|
|
|
// The queue's counts are kept beside the state and read back by the next host; unreadable is an
|
|
// error, never zero.
|
|
func TestTheNumbersSurviveTheHost(t *testing.T) {
|
|
state := filepath.Join(t.TempDir(), "state.json")
|
|
if sequence, refused, err := numbersBeside(state).Read(); err != nil || sequence != 0 || refused != 0 {
|
|
t.Fatalf("a node that never reported read %d, %d, %v", sequence, refused, err)
|
|
}
|
|
if err := numbersBeside(state).Save(41, 2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sequence, refused, err := numbersBeside(state).Read(); err != nil || sequence != 41 || refused != 2 {
|
|
t.Fatalf("read back %d, %d, %v", sequence, refused, err)
|
|
}
|
|
if err := os.WriteFile(store.NumbersPath(state), []byte(`{"report_sequence":`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := numbersBeside(state).Read(); err == nil {
|
|
t.Fatal("unreadable numbers were read as zero")
|
|
}
|
|
}
|