assign read the whole mesh before its write, to record the seats held by derivation, and that read printed on standard error that the node did not resolve for lack of the seat the act was about to give it. The seat verb answers with both streams, so the stale line came last and named the assignment just made as its remedy. The reads inside an act no longer print 'left out' lines; the node's unmet seat dependencies and whether it resolves come from one planFor taken after the write, and every other machine from blockedElsewhere. unassign now says when its node does not resolve, from the same resolution.
86 lines
2.8 KiB
Go
86 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// TestReplay237 replays novox/hq issue 237 (2026-10-04): a workstation already ran a module that
|
|
// depends on a node seat nothing there held, and the module holding that seat was assigned to it. The
|
|
// answer said the seat was now held, and then that the workstation was left out of the rest of the mesh
|
|
// for lack of it, naming as the remedy the assignment just made. `plan` for the node, right after,
|
|
// resolved. The outcome asserted: every line of the answer, as the controller seat's verb gives it
|
|
// (standard output, then standard error), agrees with `plan` for the node right after the act. Written
|
|
// with only what the controller had before its fix — register, assign's command, planFor — so it is laid
|
|
// over the commit before.
|
|
func TestReplay237(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
register(t, open, aContainer("web"))
|
|
register(t, open, catalogue.Manifest{Module: "docker", Version: "1",
|
|
Claims: []catalogue.Claim{{Name: catalogue.ContainerRuntimeSeat, Scope: catalogue.ScopeNode}}})
|
|
// Already on the node, from before the rule refused it: the node does not resolve until the
|
|
// holder comes.
|
|
if _, err := open.inventory.Assign(ctx, "laptop", "web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
said := answered(t, func() error { return assignCommand(ctx, "assign", []string{"laptop", "docker"}) })
|
|
|
|
if _, _, err := planFor(ctx, open, "laptop"); err != nil {
|
|
t.Fatalf("laptop does not resolve right after the act: %v\nthe answer was:\n%s", err, said)
|
|
}
|
|
if !strings.Contains(said, "web on laptop now has "+catalogue.ContainerRuntimeSeat+" held") {
|
|
t.Errorf("the answer does not say the seat is now held:\n%s", said)
|
|
}
|
|
for _, not := range []string{"laptop is left out", "does not resolve", "which nothing on laptop holds"} {
|
|
if strings.Contains(said, not) {
|
|
t.Errorf("the answer says %q, which `plan` right after contradicts:\n%s", not, said)
|
|
}
|
|
}
|
|
}
|
|
|
|
// answered is what a command says as the controller seat's verb answers it: standard output, then
|
|
// standard error (answerOf).
|
|
func answered(t *testing.T, f func() error) string {
|
|
t.Helper()
|
|
read := func(w **os.File) (func() string, error) {
|
|
old := *w
|
|
r, pw, err := os.Pipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
*w = pw
|
|
got := make(chan string)
|
|
go func() {
|
|
var buf bytes.Buffer
|
|
_, _ = io.Copy(&buf, r)
|
|
got <- buf.String()
|
|
}()
|
|
return func() string {
|
|
_ = pw.Close()
|
|
*w = old
|
|
return <-got
|
|
}, nil
|
|
}
|
|
stdout, err := read(&os.Stdout)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stderr, err := read(&os.Stderr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runErr := f()
|
|
said := stdout() + stderr()
|
|
if runErr != nil {
|
|
t.Fatalf("%v\n%s", runErr, said)
|
|
}
|
|
return said
|
|
}
|