diff --git a/internal/apply/apply_test.go b/internal/apply/apply_test.go index 034e041..c650e11 100644 --- a/internal/apply/apply_test.go +++ b/internal/apply/apply_test.go @@ -1298,3 +1298,65 @@ func TestAContainerGivenNoNamesIsRunAsBefore(t *testing.T) { } } } + +// Defends the ordering half of novox/hq work breakdown 1.3: resources are applied in the order +// the module declared them. +// +// **Already true, and untested until now** — the apply loop walks `d.Resources` and sorts nothing, +// so a module that needs one thing before another says so by writing it first. Worth an assertion +// because it is the kind of property a later change would break silently: sorting the resources +// for any good reason at all — by type, by identity, for a tidier report — would still pass every +// other test in this package. +// +// It is sequence, not readiness. A container started is not a container ready, and nothing here +// waits: what depends on something being *usable* retries, which is what both example +// provisioners do and is more robust than start ordering, because a dependency can also restart +// long after everything was applied. +func TestResourcesAreAppliedInTheOrderTheyWereDeclared(t *testing.T) { + dir := t.TempDir() + var order []string + done := map[string]bool{} + run := func(_ context.Context, name string, args ...string) (string, error) { + order = append(order, name+" "+strings.Join(args, " ")) + // `verify` is the idempotency check, so it has to fail before the step and pass after — + // a stub that always succeeds means every action is already done and nothing ever runs, + // which is what the first version of this test measured. + if name == "check" { + if !done[args[0]] { + return "", fmt.Errorf("not yet") + } + return "", nil + } + done[args[0]] = true + return "", nil + } + + _ = dir + // Trusted, because the link may not carry an action and only a bundle may (novox/hq ADR 0005). + // Actions are used here because they are the one shape whose execution is observable through + // the runner, which is what makes the order visible at all. + d, err := declaration.ParseTrusted([]byte(`{"declaration":1,"resources":[` + + `{"id":"first","type":"action","command":["step","one"],"verify":["check","one"]},` + + `{"id":"second","type":"action","command":["step","two"],"verify":["check","two"]},` + + `{"id":"third","type":"action","command":["step","three"],"verify":["check","three"]}]}`)) + if err != nil { + t.Fatal(err) + } + + if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, + store.OriginCarried, run, nil, nil); err != nil { + t.Fatal(err) + } + + var ran []string + for _, line := range order { + if strings.HasPrefix(line, "step ") { + ran = append(ran, strings.TrimPrefix(line, "step ")) + } + } + want := []string{"one", "two", "three"} + if strings.Join(ran, ",") != strings.Join(want, ",") { + t.Fatalf("declared one, two, three and ran %v — a module that needs one thing before "+ + "another has no way to say so", ran) + } +}