Answer plans what-if as data, the document delivery-plan answers (issue 1324)

The gate reads a delivery's plan as data once delivery-plan goes (ADR 0450 §2). The data form is an explicit
switch, `data: true`, as plans wait asks for its data: a person still gets the text. Both verbs build their
document from one function, deliveryPlanDocument, so they cannot differ; delivery-plan is unchanged (ADR 0450 §8).
This commit is contained in:
2026-10-19 21:40:26 +00:00
parent caa3fc8995
commit 23a209f3d0
5 changed files with 209 additions and 14 deletions
+140
View File
@@ -0,0 +1,140 @@
package main
import (
"encoding/json"
"reflect"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
)
// flagsOf reads a command line's flags (and its first positional after --what-if) into a map.
func flagsOf(argv []string) map[string]string {
out := map[string]string{}
for i := 0; i < len(argv); i++ {
if !strings.HasPrefix(argv[i], "--") {
continue
}
name := strings.TrimPrefix(argv[i], "--")
if i+1 < len(argv) && !strings.HasPrefix(argv[i+1], "--") {
out[name] = argv[i+1]
i++
} else {
out[name] = "true"
}
}
return out
}
// **The plan of a change as data from `plans`** (novox/hq ADR 0450 §2, issue 1324 AC1): asked for one change
// both ways, `plans` with data and `delivery-plan` answer the same plan in every field.
func TestPlansWhatIfAsDataAnswersWhatDeliveryPlanAnswers(t *testing.T) {
change := map[string]any{"repository": "novox/mesh-catalog", "paths": "modules/gitea/index.ts,modules/gitea/old.txt",
"head": "abc123", "base": "main", "module-dirs": "modules/gitea", "removed": "modules/gitea/old.txt"}
with := func(extra map[string]any) map[string]any {
out := map[string]any{}
for k, v := range change {
out[k] = v
}
for k, v := range extra {
out[k] = v
}
return out
}
plansArgv, err := argvFor("plans", with(map[string]any{"data": "true"}))
if err != nil {
t.Fatal(err)
}
deliveryArgv, err := argvFor("delivery-plan", change)
if err != nil {
t.Fatal(err)
}
if plansArgv[0] != "plans" || !slicesHas(plansArgv, "--data") || !slicesHas(plansArgv, "--what-if") {
t.Fatalf("plans with data became %q", strings.Join(plansArgv, " "))
}
p, d := flagsOf(plansArgv), flagsOf(deliveryArgv)
for _, name := range []string{"paths", "head", "base", "module-dirs", "removed"} {
if p[name] == "" || p[name] != d[name] {
t.Errorf("--%s: plans says %q, delivery-plan %q", name, p[name], d[name])
}
}
if p["what-if"] != d["repository"] {
t.Errorf("repository: plans says %q, delivery-plan %q", p["what-if"], d["repository"])
}
// Both commands build their document from the one function, from the same member.
const src = "http://forge.internal:20000/novox/mesh-catalog.git"
gitea := fromRepo("gitea", src, "modules/gitea")
gitea.Source.BuiltFrom = "old"
gitea.On = []string{"anchor", "laptop"}
entries := []inventory.Entry{gitea}
policy := func(string) (inventory.Upgrade, bool) { return inventory.Upgrade{RollOut: true}, true }
member := func(f map[string]string, repository string) orderMember {
return changeMember(repository, f["base"], f["head"], splitList(f["paths"]), splitList(f["removed"]),
moduleDirsOf(f["module-dirs"]))
}
a := deliveryPlanDocument(member(p, p["what-if"]), entries, nil, nil, policy)
b := deliveryPlanDocument(member(d, d["repository"]), entries, nil, nil, policy)
ja, _ := json.Marshal(a)
jb, _ := json.Marshal(b)
if !reflect.DeepEqual(ja, jb) {
t.Errorf("the two plans differ:\n%s\n%s", ja, jb)
}
plan := a["plan"].(interface{})
raw, _ := json.Marshal(plan)
var fields map[string]any
_ = json.Unmarshal(raw, &fields)
for _, want := range []string{"moved", "tiers", "machines", "summary", "class", "head", "base", "repository"} {
if _, ok := fields[want]; !ok {
t.Errorf("the plan has no %q: %s", want, raw)
}
}
if fields["head"] != "abc123" || fields["base"] != "main" {
t.Errorf("head and base are %v and %v", fields["head"], fields["base"])
}
}
// Without the data form the text answer is unchanged (AC2): no data flag, and the data-only arguments are not passed.
func TestPlansWhatIfWithoutDataIsUnchanged(t *testing.T) {
argv, err := argvFor("plans", map[string]any{"repository": "novox/x", "paths": "a,b"})
if err != nil {
t.Fatal(err)
}
if got := strings.Join(argv, " "); got != "plans --what-if novox/x --paths a,b" {
t.Errorf("the text what-if reads %q", got)
}
if _, err := argvFor("plans", map[string]any{"repository": "novox/x", "paths": "a", "head": "h"}); err == nil {
t.Error("a head without data was passed over, not refused")
}
argv, _ = argvFor("plans", map[string]any{"repository": "novox/x", "paths": "a", "data": "false"})
if got := strings.Join(argv, " "); got != "plans --what-if novox/x --paths a" {
t.Errorf("data false reads %q", got)
}
}
// delivery-plan stays and answers as before (AC3, ADR 0450 §8).
func TestDeliveryPlanStaysAsItWas(t *testing.T) {
argv, err := argvFor("delivery-plan", map[string]any{"repository": "novox/x", "paths": "a", "head": "h"})
if err != nil {
t.Fatal(err)
}
if got := strings.Join(argv, " "); got != "delivery plan --repository novox/x --paths a --head h" {
t.Errorf("delivery-plan reads %q", got)
}
if _, ok := catalogueVerb("delivery-plan"); !ok {
t.Error("delivery-plan is gone")
}
}
func catalogueVerb(name string) (catalogue.Verb, bool) { return controllerVerb(name) }
func slicesHas(s []string, v string) bool {
for _, x := range s {
if x == v {
return true
}
}
return false
}