Files
mesh-host/internal/apply/apply_test.go
T
jschoubben 4a80cc1002 apply: tier 0 consumes a declaration and converges this machine
Stage 2 begins. The host stops only reporting and starts doing its one
job (ADR 0037): take an ordered list of typed resources and make the
machine match it, from a local file, with no mesh present.

What lands in this slice — the network-free vocabulary ADR 0043 names
first:
- Parse: JSON, refused WHOLE on an unknown version, type, field, a
  missing id/type/path, or a duplicate id. An older host cannot be
  handed a newer vocabulary and do half of it.
- directory and file appliers, each reading back after it writes —
  mode and owner asserted against the machine, content compared byte
  for byte. A value that did not take is a failed apply, not a success.
- store: the applied-state record, authoritative while disconnected,
  written atomically. It is what makes removal possible.
- Convergence: apply in the stated order (the host never reorders),
  record each success AFTER it works (ADR 0035), and remove what was
  applied before and is no longer declared — in reverse order, so a
  file goes before the directory that held it.
- The data-loss guard: the host removes ONLY what it created, never
  what it adopted, and a created directory that now holds data is
  refused (os.Remove, never RemoveAll) rather than deleted (ADR 0018,
  0030). created is sticky across re-applies — caught by running the
  real binary, not just the unit tests: recomputing it from disk made
  a re-applied resource look adopted and leak on the next drop.
- Addressing: a declaration for another node is refused; a host with
  no identity yet applies its bundle (the first-node path).

Not yet: sealed secrets, and the types that need the network or a
runtime (container, package, network, service, archive, user, action)
— they follow, and until then the host refuses them rather than doing
part of a declaration.

CLI: mesh-host apply [--store P] FILE.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-02 22:34:44 +02:00

314 lines
12 KiB
Go

package apply
import (
"os"
"path/filepath"
"strconv"
"testing"
)
// A declaration this host does not fully understand is refused whole — the property the whole
// project is built around, tested at the boundary it matters most.
func TestParseRefusesWhatItCannotFullyUnderstand(t *testing.T) {
cases := map[string]string{
"unknown version": `{"version":2,"resources":[]}`,
"unknown type": `{"version":1,"resources":[
{"id":"a","type":"container","path":"/x"}]}`,
"unknown field on a known type": `{"version":1,"resources":[
{"id":"a","type":"directory","path":"/x","colour":"blue"}]}`,
"unknown top-level field": `{"version":1,"nodes":[],"resources":[]}`,
"resource without an id": `{"version":1,"resources":[{"type":"directory","path":"/x"}]}`,
"resource without a type": `{"version":1,"resources":[{"id":"a","path":"/x"}]}`,
"resource without a path": `{"version":1,"resources":[{"id":"a","type":"directory"}]}`,
"two resources sharing id": `{"version":1,"resources":[{"id":"a","type":"directory","path":"/x"},{"id":"a","type":"directory","path":"/y"}]}`,
}
for name, raw := range cases {
t.Run(name, func(t *testing.T) {
if _, err := Parse([]byte(raw)); err == nil {
t.Fatalf("accepted a declaration it should have refused whole")
}
})
}
}
func TestParseAcceptsAWellFormedDeclaration(t *testing.T) {
raw := `{"version":1,"for":"anchor","resources":[
{"id":"state","type":"directory","path":"/var/lib/x","mode":"0700"},
{"id":"conf","type":"file","path":"/var/lib/x/conf","mode":"0600","content":"k=v\n"}]}`
d, err := Parse([]byte(raw))
if err != nil {
t.Fatal(err)
}
if d.For != "anchor" || len(d.Resources) != 2 {
t.Fatalf("parsed wrong: %+v", d)
}
if d.Resources[0].Path() != "/var/lib/x" {
t.Fatalf("path accessor wrong: %q", d.Resources[0].Path())
}
}
// Applying a declaration lands the resources, and applying it again changes nothing — the
// idempotency the node host promises.
func TestApplyIsIdempotent(t *testing.T) {
root := t.TempDir()
storePath := filepath.Join(root, "store.json")
dir := filepath.Join(root, "svc")
file := filepath.Join(dir, "conf")
decl := mustParse(t, `{"version":1,"resources":[
{"id":"d","type":"directory","path":"`+dir+`","mode":"0755"},
{"id":"f","type":"file","path":"`+file+`","mode":"0644","content":"hello\n"}]}`)
for i := 0; i < 2; i++ {
store, err := LoadStore(storePath)
if err != nil {
t.Fatal(err)
}
res, err := Apply(decl, "", store, Appliers())
if err != nil {
t.Fatalf("apply %d: %v", i, err)
}
if len(res.Applied) != 2 {
t.Fatalf("apply %d: expected 2 applied, got %d", i, len(res.Applied))
}
}
if got, _ := os.ReadFile(file); string(got) != "hello\n" {
t.Fatalf("file content wrong: %q", got)
}
}
// A resource dropped from a declaration is removed on the next apply — but only because the host
// created it. This is desired-state convergence with the data-loss guard intact.
func TestUndeclaredResourceIsRemovedWhenHostCreatedIt(t *testing.T) {
root := t.TempDir()
storePath := filepath.Join(root, "store.json")
dir := filepath.Join(root, "svc")
gone := filepath.Join(dir, "gone")
kept := filepath.Join(dir, "kept")
first := mustParse(t, `{"version":1,"resources":[
{"id":"d","type":"directory","path":"`+dir+`"},
{"id":"gone","type":"file","path":"`+gone+`","content":"x"},
{"id":"kept","type":"file","path":"`+kept+`","content":"y"}]}`)
store, _ := LoadStore(storePath)
if _, err := Apply(first, "", store, Appliers()); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(gone); err != nil {
t.Fatalf("first apply did not create the file: %v", err)
}
second := mustParse(t, `{"version":1,"resources":[
{"id":"d","type":"directory","path":"`+dir+`"},
{"id":"kept","type":"file","path":"`+kept+`","content":"y"}]}`)
store, _ = LoadStore(storePath)
res, err := Apply(second, "", store, Appliers())
if err != nil {
t.Fatal(err)
}
if len(res.Removed) != 1 || res.Removed[0].ID != "gone" {
t.Fatalf("expected 'gone' removed, got %+v", res.Removed)
}
if _, err := os.Stat(gone); !os.IsNotExist(err) {
t.Fatalf("the undeclared file was not removed")
}
if _, err := os.Stat(kept); err != nil {
t.Fatalf("the still-declared file was wrongly removed: %v", err)
}
}
// "created" is sticky across re-applies. A resource created once, then re-applied (so it already
// exists the second time), must still be removed when later dropped — the host does not forget it
// was the creator just because the resource was present on a subsequent apply.
func TestCreatedIsStickyAcrossReapplies(t *testing.T) {
root := t.TempDir()
storePath := filepath.Join(root, "store.json")
dir := filepath.Join(root, "svc")
file := filepath.Join(dir, "conf")
full := mustParse(t, `{"version":1,"resources":[
{"id":"d","type":"directory","path":"`+dir+`"},
{"id":"f","type":"file","path":"`+file+`","content":"x"}]}`)
// Apply it twice. On the second apply everything already exists, so a naive "created" would
// flip to false and the file would later be treated as adopted.
for i := 0; i < 2; i++ {
store, _ := LoadStore(storePath)
if _, err := Apply(full, "", store, Appliers()); err != nil {
t.Fatalf("apply %d: %v", i, err)
}
}
dropped := mustParse(t, `{"version":1,"resources":[
{"id":"d","type":"directory","path":"`+dir+`"}]}`)
store, _ := LoadStore(storePath)
res, err := Apply(dropped, "", store, Appliers())
if err != nil {
t.Fatal(err)
}
if len(res.Removed) != 1 || res.Removed[0].ID != "f" {
t.Fatalf("re-applied-then-dropped file was not removed: %+v", res.Removed)
}
if _, err := os.Stat(file); !os.IsNotExist(err) {
t.Fatal("host reported the file removed but it is still on disk (created flag was not sticky)")
}
}
// The host never removes what it did not create. A directory it merely adopted — one that
// already existed, holding data — survives being dropped from the declaration.
func TestAdoptedResourceIsNeverRemoved(t *testing.T) {
root := t.TempDir()
storePath := filepath.Join(root, "store.json")
existing := filepath.Join(root, "data") // pre-exists: the host will adopt, not create it
if err := os.Mkdir(existing, 0o755); err != nil {
t.Fatal(err)
}
sentinel := filepath.Join(existing, "precious")
if err := os.WriteFile(sentinel, []byte("workload data"), 0o644); err != nil {
t.Fatal(err)
}
first := mustParse(t, `{"version":1,"resources":[
{"id":"data","type":"directory","path":"`+existing+`","mode":"0755"}]}`)
store, _ := LoadStore(storePath)
res, err := Apply(first, "", store, Appliers())
if err != nil {
t.Fatal(err)
}
if res.Applied[0].Created {
t.Fatalf("host claimed to have created a directory that already existed")
}
// Drop it from the declaration entirely. An adopted directory is not the host's to remove.
empty := mustParse(t, `{"version":1,"resources":[]}`)
store, _ = LoadStore(storePath)
if _, err := Apply(empty, "", store, Appliers()); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(sentinel); err != nil {
t.Fatalf("adopted directory (and its data) was removed — the guard failed: %v", err)
}
}
// A failed step fails the apply, and the store records what actually landed — never more.
func TestFailedStepFailsTheApplyAndRecordsOnlyWhatLanded(t *testing.T) {
root := t.TempDir()
storePath := filepath.Join(root, "store.json")
good := filepath.Join(root, "good")
// The second file's parent does not exist, so writing it fails — a mid-declaration failure.
bad := filepath.Join(root, "nonexistent-dir", "bad")
decl := mustParse(t, `{"version":1,"resources":[
{"id":"good","type":"file","path":"`+good+`","content":"ok"},
{"id":"bad","type":"file","path":"`+bad+`","content":"no"}]}`)
store, _ := LoadStore(storePath)
if _, err := Apply(decl, "", store, Appliers()); err == nil {
t.Fatal("apply reported success despite a step that could not be done")
}
// The store must record 'good' (it landed) and not 'bad' (it did not).
reloaded, _ := LoadStore(storePath)
ids := map[string]bool{}
for _, r := range reloaded.Records() {
ids[r.ID] = true
}
if !ids["good"] {
t.Fatalf("the store forgot a resource that actually landed")
}
if ids["bad"] {
t.Fatalf("the store recorded a resource that never landed — a report from intent")
}
}
// A declaration addressed to another node is refused; one addressed here, or unaddressed, is
// applied.
func TestAddressing(t *testing.T) {
root := t.TempDir()
dir := filepath.Join(root, "d")
decl := mustParse(t, `{"version":1,"for":"anchor","resources":[
{"id":"d","type":"directory","path":"`+dir+`"}]}`)
store, _ := LoadStore(filepath.Join(root, "s1.json"))
if _, err := Apply(decl, "workstation", store, Appliers()); err == nil {
t.Fatal("a node applied a declaration addressed to a different node")
}
store, _ = LoadStore(filepath.Join(root, "s2.json"))
if _, err := Apply(decl, "anchor", store, Appliers()); err != nil {
t.Fatalf("a node refused a declaration addressed to it: %v", err)
}
// The first node — no identity yet — applies whatever it carries.
store, _ = LoadStore(filepath.Join(root, "s3.json"))
if _, err := Apply(decl, "", store, Appliers()); err != nil {
t.Fatalf("a node with no identity refused its own bundle: %v", err)
}
}
// Read-back catches a value that did not take. Mode and owner are asserted against the machine
// after applying, so a file written with the wrong permissions is a failure, not a success.
func TestApplyReadsModeBack(t *testing.T) {
root := t.TempDir()
file := filepath.Join(root, "f")
decl := mustParse(t, `{"version":1,"resources":[
{"id":"f","type":"file","path":"`+file+`","mode":"0600","content":"x"}]}`)
store, _ := LoadStore(filepath.Join(root, "s.json"))
if _, err := Apply(decl, "", store, Appliers()); err != nil {
t.Fatal(err)
}
info, _ := os.Stat(file)
if info.Mode().Perm() != 0o600 {
t.Fatalf("mode not applied: %04o", info.Mode().Perm())
}
}
// A directory the host created but that now holds something is not removed — os.Remove refuses a
// non-empty directory, and that refusal is the guard, surfaced as a failed apply.
func TestCreatedDirectoryHoldingDataIsNotSilentlyDeleted(t *testing.T) {
root := t.TempDir()
storePath := filepath.Join(root, "store.json")
dir := filepath.Join(root, "svc")
first := mustParse(t, `{"version":1,"resources":[
{"id":"d","type":"directory","path":"`+dir+`"}]}`)
store, _ := LoadStore(storePath)
if _, err := Apply(first, "", store, Appliers()); err != nil {
t.Fatal(err)
}
// Something drops data into the host-created directory after the fact.
if err := os.WriteFile(filepath.Join(dir, "appeared"), []byte("data"), 0o644); err != nil {
t.Fatal(err)
}
empty := mustParse(t, `{"version":1,"resources":[]}`)
store, _ = LoadStore(storePath)
if _, err := Apply(empty, "", store, Appliers()); err == nil {
t.Fatal("host deleted, or claimed to delete, a non-empty directory it once created")
}
if _, err := os.Stat(filepath.Join(dir, "appeared")); err != nil {
t.Fatalf("data in the directory was lost: %v", err)
}
}
func mustParse(t *testing.T, raw string) Declaration {
t.Helper()
d, err := Parse([]byte(raw))
if err != nil {
t.Fatalf("test declaration did not parse: %v", err)
}
return d
}
// Sanity: the current uid is what owner read-back compares against, so an owner naming this user
// verifies rather than needing root.
func TestOwnerReadBackAgainstCurrentUser(t *testing.T) {
root := t.TempDir()
dir := filepath.Join(root, "d")
owner := strconv.Itoa(os.Getuid()) + ":" + strconv.Itoa(os.Getgid())
decl := mustParse(t, `{"version":1,"resources":[
{"id":"d","type":"directory","path":"`+dir+`","mode":"0755","owner":"`+owner+`"}]}`)
store, _ := LoadStore(filepath.Join(root, "s.json"))
if _, err := Apply(decl, "", store, Appliers()); err != nil {
t.Fatalf("applying an owner matching the current user failed: %v", err)
}
}