Files
mesh-host/internal/bundle/bundle_test.go
T
jschoubben ebba16ce4a Per-system bundles, and Android's start problem closed by narrowing it
Two gaps.

The bundle's contents are per system even though its mechanism is not, so there
are now three: substrate-arch.lock, substrate-alpine.lock and
substrate-android.lock. All three are embedded and a host reads only the one it
was built for. Arch and Alpine remain placeholders -- the closure for a one-node
mesh is still research 011/012's open question, and inventing it here would be
worse than an honest placeholder.

Android's is not a placeholder. It says a partial host cannot raise a mesh and
why: every step of a bootstrap is a package, a container, or an action against
one, and those are exactly the shapes it refuses. So a partial host can JOIN a
mesh and cannot BE the first node. That belongs where somebody looking for the
android bundle will find it.

Also separated two things that were being conflated: "this system has no
bundle" and "this system was never built". Loading a bundle for debian is not
ErrEmpty, and the test asserts they differ.

0062 -- a host may be episodic. There is no way to keep a process running on an
ordinary Android device: init needs root, a foreground service can be killed
for memory. The answer is not to fight that. It is that being killed IS
disconnection, which ADR 0036 already made an ordinary situation -- and
everything the design does for a laptop that closes is what an episodic host
needs, at a shorter period. An authoritative local store, reconcile on start,
last-heard-from reported without an alarm.

So the gap closes by requiring less rather than building something. No keep-
alive, no Android daemon, no fighting the platform's process management.

Two consequences recorded rather than glossed. Last-heard-from is a much weaker
signal on an episodic host, so a healthy phone reads as a dead server unless
the reader knows which kind it is looking at. And a declaration may take a long
time to land, which makes 0058's separation of outstanding from failed
load-bearing rather than tidy.

Left open deliberately: how an episodic host is actually started, and -- first --
what an Android node is for. Building the start mechanism before deciding that
would be building it for nobody.
2026-08-28 01:24:06 +02:00

114 lines
4.8 KiB
Go

package bundle
import (
"errors"
"strings"
"testing"
"github.com/novox/mesh-host/internal/declaration"
)
// declarationParse is the parser Load uses, named here so the test reads as the assertion it is.
func declarationParse(raw []byte) (any, error) { return declaration.Parse(raw) }
func TestADefaultBuildCarriesNothingAndSaysSo(t *testing.T) {
// The important one. A host built without a bundle that applied nothing and reported
// success would look exactly like a host that raised a first node — and the difference
// would surface as a mesh that never came up, with nothing to point at.
if !IsEmpty("arch") {
t.Fatal("the default build claims to carry a substrate")
}
_, err := Load("arch")
if !errors.Is(err, ErrEmpty) {
t.Fatalf("an empty bundle did not refuse: %v", err)
}
if !strings.Contains(err.Error(), "would look exactly like applying something") {
t.Errorf("the refusal does not say why it matters: %v", err)
}
}
func TestCommentsAreNotContent(t *testing.T) {
// The placeholder is a comment. If comments counted as content, every default build would
// claim to carry a substrate and then fail to parse it — the right outcome for the wrong
// reason, and a confusing error at the worst moment.
if got := stripComments([]byte("// a\n{\"a\":1}\n // b\n")); strings.Contains(string(got), "//") {
t.Errorf("comments survived stripping: %q", got)
}
}
func TestOnlyWholeLineCommentsAreStripped(t *testing.T) {
// Anything cleverer would have to know where strings begin and end. A parser that
// half-understands its input is worse than one that does not try — a path containing a
// double slash is ordinary, and losing half of it would be silent.
raw := []byte(`{"path":"https://example.invalid/a"}`)
if got := string(stripComments(raw)); got != string(raw) {
t.Errorf("a slash inside a string was treated as a comment: %q", got)
}
}
func TestABundleWithContentIsParsedByTheSameParserTheLinkWillUse(t *testing.T) {
// A bundle that reaches a machine and is then refused by the host carrying it would be a
// build-time mistake found at the worst possible moment.
real := []byte(`// pinned
{"declaration":1,"resources":[{"id":"d","type":"directory","path":"/etc/mesh"}]}`)
stripped := stripComments(real)
if strings.Contains(string(stripped), "pinned") {
t.Fatal("the comment survived")
}
if !strings.Contains(string(stripped), "declaration") {
t.Fatal("the declaration did not survive")
}
}
func TestWhatValidatesIsWhatIsApplied(t *testing.T) {
// Found on a real machine. `mesh-host bundle` validated the carried bundle through Load,
// which strips comments; `reconcile` handed the RAW bytes to the parser, which does not.
// So the command whose whole job is to check the bundle said yes, and the command that
// uses it said no — two paths to one artefact, disagreeing.
//
// There is now one path. This asserts the property that made the bug possible cannot
// return: whatever Load accepts is what gets applied, byte for byte.
annotated := []byte("// a comment\n" + `{"declaration":1,"resources":[{"id":"d","type":"directory","path":"/etc/mesh"}]}`)
if _, err := parseFor(annotated); err != nil {
t.Fatalf("an annotated bundle was refused: %v", err)
}
}
// parseFor mirrors what Load does to arbitrary bytes, so the test can exercise the path
// without rebuilding the binary with a different embedded file.
func parseFor(raw []byte) (any, error) {
return declarationParse(stripComments(raw))
}
func TestEverySystemHasABundleAndAndroidsRefuses(t *testing.T) {
// The bundle's contents are per system even though its mechanism is not (novox/hq ADR
// 0060), so a host must find one built for it — and a host built for a system with no
// bundle at all must say that rather than behave like an empty one.
for _, system := range []string{"arch", "alpine", "android"} {
if _, err := Load(system); err == nil {
t.Errorf("%s: a placeholder bundle loaded as if it had contents", system)
} else if !errors.Is(err, ErrEmpty) {
t.Errorf("%s: refused for the wrong reason: %v", system, err)
}
}
if _, err := Load("debian"); err == nil {
t.Error("a bundle was loaded for a system nobody has built")
} else if errors.Is(err, ErrEmpty) {
t.Error("an unbuilt system was reported as an empty bundle; those are different things")
}
}
func TestAndroidsBundleSaysWhyThereIsNone(t *testing.T) {
// Not a placeholder waiting to be filled in. An android host implements neither `package`
// nor `container` nor `service`, so every step of the bootstrap is a shape it does not
// have — a partial host can JOIN a mesh and cannot BE the first node.
text := string(Raw("android"))
for _, want := range []string{"cannot raise a mesh", "JOIN", "first node"} {
if !strings.Contains(text, want) {
t.Errorf("the android bundle does not explain itself; missing %q", want)
}
}
}