package main import ( "context" "crypto/ed25519" "encoding/json" "errors" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/asks" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // novox/hq ADR 0328 §7, §8 and §9 on the controller's side: a library's wait is excused only for a defined library // that reaches the machine and is not yet taken, or whose disk is not there; the gate passes it as a wait for a person // and the operator reads it as needs-operator in words with no path; the operator is asked once per library, never // about one taken; and the library is taken only on the warrant, where it still waits. var radarrManifest = catalogue.Manifest{Module: "radarr", Version: "1", Libraries: []catalogue.LibraryUse{{ID: "movies", Library: "media", Path: "movies", Use: catalogue.AccessReadWrite}}} var plexManifest = catalogue.Manifest{Module: "plex", Version: "1", Libraries: []catalogue.LibraryUse{{ID: "films", Library: "media", Path: "movies", Use: catalogue.AccessRead}}} var mediaLib = catalogue.Library{Name: "media", Home: "ace", Root: "/storage/media", Reach: []string{"novox"}} func keepWait(access string) inventory.Wait { return inventory.Wait{Part: "radarr", Access: access, Reason: link.WaitKeep, What: "let the mesh keep the library media"} } func placedWait(access, placed string) inventory.Wait { w := keepWait(access) w.Placed = placed return w } func diskWait(access string) inventory.Wait { return inventory.Wait{Part: "radarr", Access: access, Disk: true, Reason: link.WaitDisk, What: "mount the disk that holds media"} } // libraryFacts is the facts with media defined, and each library given taken on ace on fs-000000000001. func libraryFactsWith(taken ...string) operatorWaitFacts { t := map[string]inventory.TakenLibrary{} for _, l := range taken { t[l] = inventory.TakenLibrary{Library: l, Home: "ace", Root: "/storage/media", Placed: "fs-000000000001", Done: true} } return operatorWaitFacts{ manifests: map[string]catalogue.Manifest{"radarr": radarrManifest, "plex": plexManifest}, given: map[string]map[string]time.Time{"radarr@ace": {}, "plex@ace": {}}, libraries: map[string]catalogue.Library{"media": mediaLib}, taken: t, librariesRead: true, } } // Rows 8 and 9: excused for a defined library reaching the machine and not yet taken — by the module's entry or the // library's own wait — and for one whose disk is not there, taken or not; judged unhealthy when undefined, not // reaching the machine, already taken on the filesystem it is on, for an access that is not a library, or for a // reason the controller does not know. func TestALibraryWaitIsExcusedOnlyForALibraryNotYetTaken(t *testing.T) { for _, c := range []struct { name string machine string w inventory.Wait f operatorWaitFacts says string }{ {"the module's entry, not taken", "ace", keepWait("movies"), libraryFactsWith(), ""}, {"the library's own wait, not taken", "ace", keepWait("library:media"), libraryFactsWith(), ""}, {"on a machine it reaches", "novox", keepWait("movies"), libraryFactsWith(), ""}, {"its disk not there", "ace", diskWait("movies"), libraryFactsWith(), ""}, {"its disk not there, taken before", "ace", diskWait("movies"), libraryFactsWith("media"), ""}, {"taken already", "ace", keepWait("movies"), libraryFactsWith("media"), "taken already"}, {"taken on the filesystem it is on", "ace", placedWait("library:media", "fs-000000000001"), libraryFactsWith("media"), "taken already"}, {"taken on another filesystem: it moved", "ace", placedWait("library:media", "fs-0000000000ff"), libraryFactsWith("media"), ""}, {"taken at another root: its root changed", "ace", keepWait("movies"), func() operatorWaitFacts { f := libraryFactsWith("media") f.taken["media"] = inventory.TakenLibrary{Library: "media", Home: "ace", Root: "/storage/old-media", Done: true, Placed: "fs-000000000001"} return f }(), ""}, {"a machine it does not reach", "far", keepWait("movies"), libraryFactsWith(), "does not reach far"}, {"undefined", "ace", keepWait("library:books"), libraryFactsWith(), "not defined"}, {"not a library", "ace", keepWait("downloads"), libraryFactsWith(), "only a library waits"}, {"a reason not known", "ace", inventory.Wait{Part: "radarr", Access: "movies", Reason: "later", What: "w"}, libraryFactsWith(), "take it or for its disk"}, {"libraries unread", "ace", keepWait("movies"), operatorWaitFacts{manifests: libraryFactsWith().manifests}, "could not be read"}, } { err := checkWait("radarr", c.machine, c.w, c.f) switch { case c.says == "" && err != nil: t.Errorf("%s: refused: %v", c.name, err) case c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)): t.Errorf("%s: %v; want a refusal saying %q", c.name, err, c.says) } } } func libraryWaiting(module string, w inventory.Wait) inventory.ResourceHealth { return inventory.ResourceHealth{Module: module, Resource: module + ".app", Kind: "container", Target: module, State: link.StateWaiting, Waits: []inventory.Wait{w}} } // Rows 8 and 9 at the gate: both excused waits pass the first-node gate as a wait for a person, and the module is said // as needs-operator in words with no path; a wait on a library taken already does not pass. func TestALibraryWaitPassesTheGateAndAsksTheOperatorWithoutAPath(t *testing.T) { now := time.Now() for _, w := range []inventory.Wait{keepWait("movies"), diskWait("movies")} { w.What += " at /storage/media" // an engine's words carrying a path never reach the operator's f := gateFacts{now: now, waits: libraryFactsWith(), groupsAdded: map[string]bool{"radarr": false}, health: map[string]inventory.NodeHealth{"ace": {Node: "ace", HeardAt: now, Resources: []inventory.ResourceHealth{libraryWaiting("radarr", w)}}}} h, why := moduleHealthWord("radarr", "ace", now.Add(-time.Minute), f) if h != healthPerson || !strings.Contains(why, "waits for the operator") || strings.Contains(why, "/") { t.Fatalf("an excused library wait reads %v %q; want a wait for a person, with no path", h, why) } o := needsOperatorObservation("radarr", "ace", []inventory.Wait{w}, nil) for _, words := range []string{o.Summary, o.Headline, o.Explanation, o.Needs} { if strings.Contains(words, "/") { t.Errorf("the operator reads a path: %q", words) } } want := "let the mesh keep the library entry movies when you are asked" if w.Disk { want = "mount the disk that holds the library entry movies on ace" } if o.Kind != kindNeedsOperator || !strings.Contains(o.Needs, want) { t.Errorf("needs-operator %s asks %q; want %q", o.Kind, o.Needs, want) } } f := gateFacts{now: now, waits: libraryFactsWith("media"), groupsAdded: map[string]bool{"radarr": false}, health: map[string]inventory.NodeHealth{"ace": {Node: "ace", HeardAt: now, Resources: []inventory.ResourceHealth{libraryWaiting("radarr", keepWait("movies"))}}}} if h, why := moduleHealthWord("radarr", "ace", now.Add(-time.Minute), f); h != healthNotYet || !strings.Contains(why, "taken") { t.Fatalf("a wait on a taken library reads %v %q", h, why) } } func takeRig(t *testing.T, waiting func() map[string][]waitingFolder) (*askerRig, *[]string) { r := newAskerRig(t) var taken []string libs := map[string]catalogue.Library{"media": mediaLib} r.a.folders = func(context.Context) ([]libraryTake, error) { return takesOf(waiting(), libs), nil } r.a.machines = func(context.Context) ([]string, error) { return []string{"ace", "novox"}, nil } r.a.take = func(_ context.Context, lt libraryTake, askID, by string) (string, error) { taken = append(taken, lt.Library+" "+lt.Node+" "+lt.Root+" "+lt.Placed+" "+askID+" "+by) return "taking", nil } return r, &taken } func takeAsks(r *askerRig) []asked { var out []asked for _, a := range r.store { if a.Take != nil { out = append(out, a) } } return out } func openTakes(r *askerRig) []asked { var out []asked for _, a := range takeAsks(r) { if a.State == askOpen { out = append(out, a) } } return out } // Row 7: one ask per library however many modules and machines wait on it, in the clause's words, at approve; none // again while it is open; taken back when the library no longer waits. func TestTheOperatorIsAskedOncePerLibrary(t *testing.T) { current := map[string][]waitingFolder{"media": { {module: "radarr", machine: "ace", placed: "fs-000000000001"}, {module: "plex", machine: "novox"}, {module: "", machine: "ace", placed: "fs-000000000001"}}} r, _ := takeRig(t, func() map[string][]waitingFolder { return current }) ctx := context.Background() for i := 0; i < 3; i++ { if err := r.a.reconcile(ctx); err != nil { t.Fatal(err) } } kept := takeAsks(r) if len(kept) != 1 { t.Fatalf("%d asks for one library", len(kept)) } q := kept[0].Ask if err := q.Check(r.now); err != nil { t.Fatal(err) } if q.Highest() != asks.Approve || len(q.Options) != 1 { t.Fatalf("the ask offers %+v; want one answer at approve", q.Options) } // The root may not leave the mesh (the router's content rule): it is said as "its root". want := "Let the mesh keep library media at its root on ace: its group becomes mesh-media, the group may read " + "and write, folders keep the group; owners and everyone else's rights stay as they are. Waiting on it: plex, radarr." if q.Explanation != want { t.Errorf("the ask says\n %q\nwant\n %q", q.Explanation, want) } if kept[0].Take.Node != "ace" || kept[0].Take.Placed != "fs-000000000001" { t.Errorf("the take is for %+v; want the home and the filesystem its home names", kept[0].Take) } current = nil if err := r.a.reconcile(ctx); err != nil { t.Fatal(err) } if got := takeAsks(r); len(got) != 1 || got[0].State != askCancelled { t.Fatalf("the ask about a library that no longer waits: %+v", got) } } // Row 7: nothing is taken without the operator's warrant — a warrant for another asker, an ask that expired, or a // record changed after the ask takes nothing; the warrant takes it once, on its home, naming library:. func TestALibraryIsTakenOnlyOnTheWarrant(t *testing.T) { waits := func() map[string][]waitingFolder { return map[string][]waitingFolder{"media": {{module: "radarr", machine: "ace", placed: "fs-000000000001"}}} } ctx := context.Background() r, taken := takeRig(t, waits) if err := r.a.reconcile(ctx); err != nil { t.Fatal(err) } a := takeAsks(r)[0] other := warrantOn(a, "let-the-mesh-keep-it", r.now) other.Asker = "someone-else" answerWith(t, r, other) changed := r.store[a.ID] changed.Take = &libraryTake{Library: "media", Node: "ace", Root: "/", Modules: a.Take.Modules, Placed: a.Take.Placed} r.store[a.ID] = changed answerWith(t, r, warrantOn(a, "let-the-mesh-keep-it", r.now)) if len(*taken) != 0 || !strings.HasPrefix(r.store[a.ID].Acted, "failed") { t.Fatalf("a take whose record changed after the ask: %v, %q", *taken, r.store[a.ID].Acted) } r2, taken2 := takeRig(t, waits) if err := r2.a.reconcile(ctx); err != nil { t.Fatal(err) } b := takeAsks(r2)[0] answerWith(t, r2, asks.Warrant{Ask: b.ID, Asker: "mesh-controller", Outcome: asks.OutcomeExpired, At: r2.now}) if len(*taken2) != 0 { t.Fatal("taken on an ask that expired") } r3, taken3 := takeRig(t, waits) if err := r3.a.reconcile(ctx); err != nil { t.Fatal(err) } c := takeAsks(r3)[0] w := warrantOn(c, "let-the-mesh-keep-it", r3.now) answerWith(t, r3, w) answerWith(t, r3, w) if len(*taken3) != 1 || !strings.HasPrefix((*taken3)[0], "media ace /storage/media fs-000000000001 "+c.ID) || !strings.Contains((*taken3)[0], "telegram") { t.Fatalf("the warrant took %v", *taken3) } if !strings.HasPrefix(r3.store[c.ID].Acted, "done") { t.Fatalf("the take was recorded %q", r3.store[c.ID].Acted) } } // A warrant for a library on one filesystem is never carried out on another: when it comes and the library waits on // another, nothing is sent, the ask is spent, and the operator is asked again for the filesystem it is on now. func TestAWarrantForOneFilesystemIsNotCarriedOutOnAnother(t *testing.T) { placed := "fs-000000000001" r, taken := takeRig(t, func() map[string][]waitingFolder { return map[string][]waitingFolder{"media": {{module: "radarr", machine: "ace", placed: placed}}} }) ctx := context.Background() if err := r.a.reconcile(ctx); err != nil { t.Fatal(err) } a := takeAsks(r)[0] placed = "fs-0000000000ff" answerWith(t, r, warrantOn(a, "let-the-mesh-keep-it", r.now)) if len(*taken) != 0 { t.Fatalf("a warrant for %s was carried out on %s: %v", a.Take.Placed, placed, *taken) } if spent := r.store[a.ID]; spent.State == askOpen || !strings.HasPrefix(spent.Acted, "nothing") { t.Fatalf("the ask after the library moved: %s, %q", spent.State, spent.Acted) } if err := r.a.reconcile(ctx); err != nil { t.Fatal(err) } if again := openTakes(r); len(again) != 1 || again[0].Take.Placed != placed { t.Fatalf("not asked again for the filesystem it is on now: %+v", again) } } // The take act names library: and the warrant, is signed over its own context, and is recorded only on the // engine's word. func TestATakeAskNamesTheLibraryAndItsWarrant(t *testing.T) { _, key, _ := ed25519.GenerateKey(nil) signer := keySigner{key} ask := link.AccessActAsk{Node: "ace", Path: "/storage/media", By: "the operator", Access: "library:media"} if _, err := link.SignTake(context.Background(), signer, ask); err == nil { t.Fatal("a take with no warrant was signed") } ask.Warrant = "c0123456789abcdef" body, err := link.SignTake(context.Background(), signer, ask) if err != nil { t.Fatal(err) } var signed link.SignedHandOver if err := json.Unmarshal(body, &signed); err != nil { t.Fatal(err) } if !ed25519.Verify(key.Public().(ed25519.PublicKey), append([]byte(link.AccessActContext), signed.Ask...), signed.Signature) { t.Fatal("the take is not signed over its own context") } var got map[string]any if err := json.Unmarshal(signed.Ask, &got); err != nil { t.Fatal(err) } if got["act"] != "take" || got["warrant"] != "c0123456789abcdef" || got["access"] != "library:media" || got["path"] != "/storage/media" { t.Fatalf("the ask carries %v", got) } var asked link.AccessActAsk refusing := takeFolderIn(nil, func(_ context.Context, a link.AccessActAsk) (link.HandOverAnswer, error) { asked = a return link.HandOverAnswer{Refused: "the disk is not there"}, nil }) if _, err := refusing(context.Background(), libraryTake{Library: "media", Node: "ace", Root: "/storage/media"}, "c01", "the operator"); err == nil { t.Fatal("a refused take was answered as done") } if asked.Access != "library:media" || asked.Node != "ace" || asked.Path != "/storage/media" || asked.Warrant != "c01" { t.Fatalf("the engine was asked %+v", asked) } unreachable := takeFolderIn(nil, func(context.Context, link.AccessActAsk) (link.HandOverAnswer, error) { return link.HandOverAnswer{}, errors.New("nothing on ace answers") }) if _, err := unreachable(context.Background(), libraryTake{Library: "media", Node: "ace", Root: "/storage/media"}, "c01", "the operator"); err == nil { t.Fatal("an unanswered take was answered as done") } } type keySigner struct{ key ed25519.PrivateKey } func (s keySigner) Sign(_ context.Context, b []byte) ([]byte, error) { return ed25519.Sign(s.key, b), nil } // The envelope carries groups, identities and libraries only when composed — for an engine at link.LibraryContract — // and an older engine's body is what it was. func TestTheDeclarationSaysItsLibrariesOnlyWhenComposed(t *testing.T) { older, err := sendable{Resources: []map[string]any{{"id": "a"}}}.Body() if err != nil { t.Fatal(err) } for _, key := range []string{"groups", "identities", "libraries"} { if strings.Contains(string(older), key) { t.Fatalf("an older engine's body names %s: %s", key, older) } } newer, err := sendable{Resources: []map[string]any{{"id": "a"}}, Groups: []catalogue.GroupEntry{{Name: "mesh-media", GID: 50010}}, Identities: []catalogue.IdentityEntry{{Module: "radarr", Name: "mesh-radarr", UID: 50020, GID: 50010}}, Libraries: []catalogue.LibraryEntry{{Library: "media", Root: "/storage/media", Group: "mesh-media", GID: 50010, Home: true, Paths: []string{"movies"}, Runner: "jochen"}}}.Body() if err != nil { t.Fatal(err) } for _, want := range []string{`"groups":[{"name":"mesh-media","gid":50010}]`, `"identities":[{"module":"radarr","name":"mesh-radarr","uid":50020,"gid":50010}]`, `"libraries":[{"library":"media","root":"/storage/media","group":"mesh-media","gid":50010,"home":true,` + `"taken":false,"paths":["movies"],"runner":"jochen"}]`} { if !strings.Contains(string(newer), want) { t.Errorf("the body lacks %s: %s", want, newer) } } if link.LibraryContract != 6 { t.Fatalf("the contract the engine states for ADR 0328 is 6, and this controller reads %d", link.LibraryContract) } } // The gate: an access or library the engine cannot judge this look, or entries left while a disk was away, of a module // whose move leaves its accesses and libraries as they were, are not counted; of one whose move changes them — a // library added, or joining — or is not known, they hold the gate to its bound. func TestAnUnknownLibraryIsJudgedByWhetherTheMoveChangesIt(t *testing.T) { now := time.Now() since := now.Add(-time.Minute) unknown := inventory.ResourceHealth{Module: "radarr", Resource: "radarr.library-movies", Kind: link.KindAccess, Target: "/storage/media/movies", State: link.StateUnknown, Since: now} left := inventory.ResourceHealth{Module: "radarr", Resource: "radarr.access-gone~entries", Kind: link.KindKeptEntries, Target: "/storage/media", State: link.StateUnhealthy, Since: since.Add(-time.Hour)} facts := func(changes map[string]bool, rs ...inventory.ResourceHealth) gateFacts { return gateFacts{now: now, waits: libraryFactsWith(), groupsAdded: map[string]bool{"radarr": false}, accessesChanged: changes, health: map[string]inventory.NodeHealth{"ace": {Node: "ace", HeardAt: now, Resources: rs}}} } unchanged := map[string]bool{"radarr": false} if h, why := moduleHealthWord("radarr", "ace", since, facts(unchanged, unknown)); h != healthGood { t.Errorf("an unknown library of a move that changes nothing reads %v %q", h, why) } if h, why := moduleHealthWord("radarr", "ace", since, facts(unchanged, left)); h != healthPerson || !strings.Contains(why, "access-gone~entries") { t.Errorf("entries left, a move that changes nothing: %v %q", h, why) } for name, changes := range map[string]map[string]bool{"changes them": {"radarr": true}, "not known": nil} { for _, r := range []inventory.ResourceHealth{unknown, left} { if h, _ := moduleHealthWord("radarr", "ace", since, facts(changes, r)); h != healthNotYet { t.Errorf("%s %s of a move that %s reads %v; want held to the bound", r.Kind, r.State, name, h) } } } plain := catalogue.Manifest{Module: "radarr"} if sameAccessUse(plain, radarrManifest) || !sameAccessUse(radarrManifest, radarrManifest) { t.Fatal("a move that adds a library, and joins, reads unchanged — or an unchanged one reads changed") } } // The engine states a library once, of no module; each module assigned to the machine that uses it is judged and said // by a copy of that verdict — its wait excused and passed by the gate as a wait for a person. A library unknown or // starting (a take walking under it) of a move that changes no library is not counted. func TestALibrarysVerdictIsSaidForTheModulesThatUseIt(t *testing.T) { inv := inventory.ForTest(t) ctx := t.Context() if _, err := inv.AddNode(ctx, "ace"); err != nil { t.Fatal(err) } for _, m := range []catalogue.Manifest{radarrManifest, plexManifest, {Module: "plain", Version: "1"}} { if err := inv.RegisterModule(ctx, m, inventory.Source{}); err != nil { t.Fatal(err) } if m.Module == "plex" { continue // pulled in by a requirement, not assigned: it is in what the machine was sent (item 4) } if _, err := inv.Assign(ctx, "ace", m.Module); err != nil { t.Fatal(err) } } record, err := inv.NodeByName(ctx, "ace") if err != nil { t.Fatal(err) } if err := inv.RecordSentWith(ctx, record.ID, "d1", map[string]string{"radarr": "c1", "plex": "c2", "plain": "c3"}, 1, 1, inventory.Send{Sequence: 1}); err != nil { t.Fatal(err) } lib := inventory.ResourceHealth{Resource: "library:media", Kind: link.KindLibrary, Target: "/storage/media", State: link.StateWaiting, Waits: []inventory.Wait{{Part: "media", Access: "library:media", Reason: link.WaitKeep, Placed: "fs-000000000001", What: "let the mesh keep the library media"}}} copies := libraryVerdictsFor(ctx, inv, "ace", []inventory.ResourceHealth{lib}) var modules []string for _, c := range copies { modules = append(modules, c.Module) } if strings.Join(modules, ",") != "plex,radarr" { t.Fatalf("the library's verdict is said for %v; want plex and radarr, the modules using it", modules) } paths, err := inv.LibraryPaths(ctx) if err != nil { t.Fatal(err) } if strings.Join(paths["media"], ",") != "movies" { t.Fatalf("the paths kept of media are %v", paths["media"]) } now := time.Now() f := gateFacts{now: now, waits: libraryFactsWith(), groupsAdded: map[string]bool{"radarr": false}, accessesChanged: map[string]bool{"radarr": false}, health: map[string]inventory.NodeHealth{"ace": {Node: "ace", HeardAt: now, Resources: copies}}} if h, why := moduleHealthWord("radarr", "ace", now.Add(-time.Minute), f); h != healthPerson { t.Fatalf("radarr waiting on media reads %v %q; want a wait for a person", h, why) } for _, state := range []string{link.StateUnknown, link.StateStarting} { c := copies[1] c.State, c.Waits = state, nil f.health["ace"] = inventory.NodeHealth{Node: "ace", HeardAt: now, Resources: []inventory.ResourceHealth{c}} if h, why := moduleHealthWord("radarr", "ace", now.Add(-time.Minute), f); h != healthGood { t.Errorf("media %s, a move that changes no library: %v %q", state, h, why) } f.accessesChanged = map[string]bool{"radarr": true} if h, _ := moduleHealthWord("radarr", "ace", now.Add(-time.Minute), f); h != healthNotYet { t.Errorf("media %s, a move that changes a library: %v; want held to the bound", state, h) } f.accessesChanged = map[string]bool{"radarr": false} } } // The review of 7ea6d36e, item 5: the engine saying it started a take does not make the library taken. While the act // runs the library still waits and the operator is not asked again; it is taken when the engine states the act done // under that warrant. func TestALibraryIsTakenOnlyWhenTheEngineSaysTheActIsDone(t *testing.T) { open := aMesh(t) ctx := t.Context() inv := open.inventory register(t, open, catalogue.Manifest{Module: catalogue.LibrariesModule, Version: "1"}) if err := inv.SetSettings(ctx, "", catalogue.LibrariesModule, map[string]any{catalogue.LibrariesSetting: map[string]any{ "media": map[string]any{"home": "anchor", "root": "/storage/media"}}}); err != nil { t.Fatal(err) } take := takeFolderIn(open, func(context.Context, link.AccessActAsk) (link.HandOverAnswer, error) { return link.HandOverAnswer{Said: "the take of library:media started"}, nil }) lt := libraryTake{Library: "media", Node: "anchor", Root: "/storage/media", Placed: "fs-000000000001"} if _, err := take(ctx, lt, "c01", "the operator"); err != nil { t.Fatal(err) } libs, err := inv.Libraries(ctx) if err != nil { t.Fatal(err) } taken, err := librariesTaken(ctx, inv, libs) if err != nil { t.Fatal(err) } if taken["media"] { t.Fatal("a take only started made the library taken") } // While it runs: the wait is still excused, and no new ask is made for it. var f operatorWaitFacts readWaitFacts(ctx, inv, "anchor", nil, nil, &f) if err := checkWait("radarr", "anchor", placedWait("library:media", "fs-000000000001"), f); err != nil { t.Fatalf("the wait while the take runs: %v", err) } if got := withoutTakesUnderWay([]libraryTake{lt}, f.taken); len(got) != 0 { t.Fatalf("a library whose take runs is asked about again: %+v", got) } // The engine states the act done under that warrant. act, _ := json.Marshal(map[string]any{"act": "take", "access": "library:media", "warrant": "c01", "state": "done"}) if err := stateHealth(ctx, inv, nil, "anchor", link.Health{Contract: link.LibraryContract, At: time.Now(), Resources: []link.ResourceHealth{{Resource: "library:media", Kind: link.KindLibrary, Target: "/storage/media", State: link.StateHealthy, Since: time.Now(), Act: act}}}, time.Now()); err != nil { t.Fatal(err) } if taken, err = librariesTaken(ctx, inv, libs); err != nil || !taken["media"] { t.Fatalf("the act done did not make the library taken (%v)", err) } }