A newer sequence carrying an older view of the assignments undeclared four assigned modules on 2026-10-04; the sequence cannot tell it, the generation can. The engine keeps the highest generation it applied, refuses an older one before reading the machine unless the declaration is a gate's put-back, and says the refusal in its report so the controller can name the sender. A declaration without a generation is applied as before, and every report says it reads one, so the controller sends it only to engines that do.
171 lines
7.1 KiB
Go
171 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/link"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// The assignment generation a declaration was composed from (novox/hq issue 234).
|
|
//
|
|
// On 2026-10-04 a declaration newer in sequence than every other, and composed from a view of the
|
|
// assignments older than the one this machine had applied, named four fewer modules — and the host,
|
|
// ordering by sequence alone, undeclared all four. The sequence orders arrival; it cannot tell a later
|
|
// send carrying earlier content. The generation can: the node-engine refuses a declaration composed from
|
|
// an older generation than the one it applied, before anything is read from the machine.
|
|
//
|
|
// These read what the report says through its JSON, as the controller does, so they run against a
|
|
// node-engine that has never heard of a generation and fail there for that reason.
|
|
|
|
// generationFixture is a machine that applied generation 7: a file the mesh declared, kept in its state,
|
|
// and the declaration it kept — signed — that named it.
|
|
type generationFixture struct {
|
|
opts options
|
|
private ed25519.PrivateKey
|
|
kept store.Declared
|
|
// stays is the file the older generation's declaration does not name.
|
|
stays string
|
|
}
|
|
|
|
func machineThatApplied(t *testing.T, generation int64) generationFixture {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
opts := options{state: filepath.Join(dir, "state.json")}
|
|
_, private, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stays := filepath.Join(dir, "docker.conf")
|
|
if err := os.WriteFile(stays, []byte("kept\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The state as a node-engine that applied generation 7 keeps it: the file it owns, and the highest
|
|
// generation it applied. Written as JSON, so the fixture is the same bytes for any build under test.
|
|
state := map[string]any{"resources": []any{map[string]any{"id": "docker.conf", "type": "file",
|
|
"origin": store.OriginDeclared, "target": stays}}, "generation": generation}
|
|
raw, err := json.Marshal(state)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(opts.state, raw, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f := generationFixture{opts: opts, private: private, stays: stays}
|
|
f.kept = f.declared(t, map[string]any{"sequence": 3, "generation": generation}, "docker.conf", stays)
|
|
if err := store.SaveDeclared(store.DeclaredPath(opts.state), f.kept); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return f
|
|
}
|
|
|
|
// declared is a signed declaration with the envelope's keys given and one file.
|
|
func (f generationFixture) declared(t *testing.T, envelope map[string]any, id, path string) store.Declared {
|
|
t.Helper()
|
|
body := map[string]any{"declaration": 1,
|
|
"resources": []any{map[string]any{"id": id, "type": "file", "path": path, "content": "x\n"}}}
|
|
for k, v := range envelope {
|
|
body[k] = v
|
|
}
|
|
raw, err := json.Marshal(body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return store.Declared{Declaration: raw, Signature: ed25519.Sign(f.private, raw)}
|
|
}
|
|
|
|
// reportJSON is a report as the controller reads it.
|
|
func reportJSON(t *testing.T, r link.Report) map[string]any {
|
|
t.Helper()
|
|
raw, err := json.Marshal(r)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out map[string]any
|
|
if err := json.Unmarshal(raw, &out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// **A newer sequence from an older generation is refused, and nothing is undeclared** (issue 234).
|
|
func TestANewerSequenceFromAnOlderGenerationIsRefusedAndNothingIsUndeclared(t *testing.T) {
|
|
f := machineThatApplied(t, 7)
|
|
other := filepath.Join(filepath.Dir(f.opts.state), "sudo.conf")
|
|
stale := f.declared(t, map[string]any{"sequence": 9, "generation": 6}, "sudo.conf", other)
|
|
|
|
report := applyAndKeep(context.Background(), f.opts, stale.Declaration, &stale, nil, nil)
|
|
said := reportJSON(t, report)
|
|
older, _ := said["older_generation"].(map[string]any)
|
|
if older == nil || older["generation"] != float64(6) || older["applied"] != float64(7) {
|
|
t.Fatalf("the refusal does not name the generation refused and the one applied: %v", said)
|
|
}
|
|
if report.Refused == "" || !strings.Contains(report.Refused, "generation 6") ||
|
|
!strings.Contains(report.Refused, "generation 7") {
|
|
t.Errorf("the refusal is not said in words naming both generations: %q", report.Refused)
|
|
}
|
|
if report.Declared != digestOf(stale.Declaration) || report.Sequence != 9 {
|
|
t.Errorf("the refusal does not name the declaration refused, so the mesh cannot name who sent it: %+v", report)
|
|
}
|
|
if _, err := os.Stat(f.stays); err != nil {
|
|
t.Fatalf("a refused declaration undeclared what the machine applied: %v", err)
|
|
}
|
|
if _, err := os.Stat(other); err == nil {
|
|
t.Error("the refused declaration touched the machine")
|
|
}
|
|
state, err := store.Load(f.opts.state)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, owned := state.Find("docker.conf"); !owned {
|
|
t.Error("the refused declaration took a resource out of what this machine owns")
|
|
}
|
|
kept, err := store.ReadDeclared(store.DeclaredPath(f.opts.state))
|
|
if err != nil || string(kept.Declaration) != string(f.kept.Declaration) {
|
|
t.Error("the refused declaration replaced what this node kept, so the next reconcile would apply it")
|
|
}
|
|
}
|
|
|
|
// refusedForItsGeneration says a report refused its declaration for the generation it came from.
|
|
func refusedForItsGeneration(t *testing.T, r link.Report) bool {
|
|
t.Helper()
|
|
return reportJSON(t, r)["older_generation"] != nil || strings.Contains(r.Refused, "generation")
|
|
}
|
|
|
|
// refusedAsUnread says a report refused its declaration as one this node-engine cannot read.
|
|
func refusedAsUnread(r link.Report) bool { return strings.Contains(r.Refused, "not a declaration") }
|
|
|
|
// **A gate's put-back is applied, whatever generation it carries**: it carries the generation of what it
|
|
// puts back, and says it is a put-back (issue 234).
|
|
func TestAPutBackFromAnOlderGenerationIsNotRefused(t *testing.T) {
|
|
f := machineThatApplied(t, 7)
|
|
back := f.declared(t, map[string]any{"sequence": 9, "generation": 5, "put_back": true}, "docker.conf", f.stays)
|
|
report := applyAndKeep(context.Background(), f.opts, back.Declaration, &back, nil, nil)
|
|
if refusedForItsGeneration(t, report) || refusedAsUnread(report) {
|
|
t.Fatalf("a put-back was refused: %s", report.Refused)
|
|
}
|
|
}
|
|
|
|
// **A declaration that carries no generation is applied as it always was**: an older controller sends
|
|
// none, and a node-engine that refused it would strand every machine the moment the controller is older.
|
|
func TestADeclarationWithoutAGenerationIsNotRefusedForOne(t *testing.T) {
|
|
f := machineThatApplied(t, 7)
|
|
older := f.declared(t, map[string]any{"sequence": 9}, "docker.conf", f.stays)
|
|
report := applyAndKeep(context.Background(), f.opts, older.Declaration, &older, nil, nil)
|
|
if refusedForItsGeneration(t, report) || refusedAsUnread(report) {
|
|
t.Fatalf("a declaration without a generation was refused: %s", report.Refused)
|
|
}
|
|
// And the same generation again is no older.
|
|
same := f.declared(t, map[string]any{"sequence": 10, "generation": 7}, "docker.conf", f.stays)
|
|
report = applyAndKeep(context.Background(), f.opts, same.Declaration, &same, nil, nil)
|
|
if refusedForItsGeneration(t, report) || refusedAsUnread(report) {
|
|
t.Fatalf("a declaration of the generation applied was refused: %s", report.Refused)
|
|
}
|
|
}
|