mesh/delivery merged: source.main moved to novox/mesh-controller@2bcd85d2df4c
mesh/check pass ac34bce49f58 on 2bcd85d2df4c | unit 966 run, 171 kept, 2 skipped | integration 1625 run, 232 kept, 7 skipped | over bound: integration…
A catalogue merge that leaves a module's sources alone registers it at the new commit from the same done key, whose build record stands at its first commit, so the commit had no build and the module no fingerprint of the new form: it went into the node's own composition while its want named its own, and every node declared its files twice. currentFingerprints now reads the indexer's trunk value at the commit when no build points at it (1945/AC15), and a module a want or have names by its own composition is left out of the node's own (1945/AC16).
103 lines
4.0 KiB
Go
103 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"strings"
|
|
"testing"
|
|
|
|
"encoding/json"
|
|
|
|
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
|
|
|
sdkdecl "git.novox.be/novox/mesh-sdk/go/declaration"
|
|
)
|
|
|
|
// A composition's file over 64 KiB reaches the node by its digest, and its object is in the node's bucket before the
|
|
// composition is written (ADR 0539 §4; issue 1945, live on 2026-10-25 at 01:22: novox's node-engine could not fetch
|
|
// nats.bus-users, because the body the compositions were split from already named the file by digest, so the writer
|
|
// found no content to put, and nothing else put it once sends were gone).
|
|
func TestALargeFileOfACompositionHasItsObjectInTheBucket(t *testing.T) {
|
|
big := strings.Repeat("user = x\n", sdkdecl.FileReferenceThreshold/8)
|
|
declared := sendable{fileReferences: true, Resources: []map[string]any{{"id": "nats.bus-users", "type": "file",
|
|
"path": "/var/lib/nats-module/conf/accounts.conf", "mode": "0644", "content": big}}}
|
|
body, err := compositionBody(declared)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
comps, _, _, err := compositionsOf("novox", body, nil, nil, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
at, _, _, _ := aPlace(t, "novox")
|
|
if _, err := writeNodeCompositions(context.Background(), at, comps); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sum := sha256.Sum256([]byte(big))
|
|
if _, ok := at.objects.(fakeObjects)[hex.EncodeToString(sum[:])]; !ok {
|
|
t.Fatalf("the object of nats.bus-users is not in the bucket: %d objects", len(at.objects.(fakeObjects)))
|
|
}
|
|
}
|
|
|
|
// A composition that names a file by a digest whose object the bucket does not hold is refused, never written: a node
|
|
// that read it could not apply it.
|
|
func TestACompositionNamingAnObjectTheBucketLacksIsRefused(t *testing.T) {
|
|
big := strings.Repeat("user = x\n", sdkdecl.FileReferenceThreshold/8)
|
|
declared := sendable{fileReferences: true, Resources: []map[string]any{{"id": "nats.bus-users", "type": "file",
|
|
"path": "/var/lib/nats-module/conf/accounts.conf", "mode": "0644", "content": big}}}
|
|
body, err := declared.Body() // the body as a send named it: by digest, the content gone
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
comps, _, _, err := compositionsOf("novox", body, nil, nil, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
at, _, _, _ := aPlace(t, "novox")
|
|
if _, err := writeNodeCompositions(context.Background(), at, comps); err == nil ||
|
|
!strings.Contains(err.Error(), "does not hold") {
|
|
t.Fatalf("a composition naming a missing object was written: %v", err)
|
|
}
|
|
}
|
|
|
|
// Every file the provisioner fills stays filled in the compositions, a moved module's too: the node's own composition
|
|
// carries the path, so the node-engine leaves its content to the provisioner (issue 1945, live on 2026-10-25 at 01:22:
|
|
// ace's node-engine wrote claude-code's, node-env's and power's filled files empty, because the compositions dropped the
|
|
// filled paths of every module moved into the node's own, and their resources say no content).
|
|
func TestEveryFilledFileStaysFilledInTheCompositions(t *testing.T) {
|
|
for _, node := range []string{"ace", "g14"} {
|
|
body, fps := capturedBody(t, node)
|
|
var d struct {
|
|
Filled []string `json:"filled_files"`
|
|
}
|
|
if err := json.Unmarshal(body, &d); err != nil || len(d.Filled) == 0 {
|
|
t.Fatalf("%s: the captured declaration fills nothing (%v)", node, err)
|
|
}
|
|
// power, claude-code and node-env have no fingerprint of the new form, so they move into the node's own.
|
|
for _, m := range []string{"power", "claude-code", "node-env"} {
|
|
delete(fps, m)
|
|
}
|
|
comps, _, _, err := compositionsOf(node, body, fps, nil, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", node, err)
|
|
}
|
|
filled := map[string]bool{}
|
|
for _, c := range comps {
|
|
for _, p := range c.Filled {
|
|
filled[p] = true
|
|
}
|
|
}
|
|
for _, p := range d.Filled {
|
|
if !filled[p] {
|
|
t.Errorf("%s: %s is filled by the provisioner, and no composition says so", node, p)
|
|
}
|
|
}
|
|
for key, c := range comps {
|
|
if _, err := sdkdelivery.EncodeComposed(c); err != nil {
|
|
t.Errorf("%s: %s: %v", node, key, err)
|
|
}
|
|
}
|
|
}
|
|
}
|