Implements novox/hq ADR 0110 and 0111. The seat set lives in internal/catalogue/seats.go: fourteen seats, each with a scope, what occupying it delivers, and the record that made it one. A test asserts the count and a decision per entry, so changing the set means finding the argument, as the host's vocabulary test does. The first set is every seat already claimed — including the-private-network, which the network module claims from a manifest composed in this repository's code, not from any module.json — plus npm-package-registry (ADR 0109) and git (ADR 0111). A test parses every catalogue manifest and this repository's own and fails on any refused claim, so closing the set refuses nothing in use. ParseManifest now refuses a claim on a seat the mesh does not define, a seat claimed at another scope, and a delivering seat claimed by a module that does not provide what it delivers. A malformed claim is refused once, for being malformed. Resolution: among several providers of a mesh provision, a pin still wins; then the holder of the seat that delivers it; then the only provider; otherwise refused as before. ADR 0009's "never guessed" holds — the seat is the choice made once, mesh-wide, rather than a pin per consumer node. A provider now carries the module it came from, because a provider is a (node, module) pair and the pair is what tells a holder from a neighbour on the same machine. The planner's second pass is now given the first pass's holdings. Without them, a node consuming a seat-delivered provision was refused there, and a refused node's own claims dropped out of what the mesh holds — letting a second holder of one of its seats pass unrefused. `seats [--json]` lists every seat, what it delivers, and each holder, derived from assignments every time and never stored. Unheld seats are listed. A stored claim outside the set — possible for a manifest registered before the set closed, since stored manifests are not re-validated — is shown rather than hidden. `build --self <owner>/<repo>` builds from a repository on the git seat's holder. The clone URL is composed at build time from the holder's node and what it serves for git; the recorded source is the path and the seat (migration 0032), never an address, so a moved forge changes nothing recorded. Nobody holding the seat refuses self-hosted builds and says so; external URLs are unchanged. An address passed with --self is refused rather than recorded as a path. Replaces three foundation tests that defended the builder's carried package binding. The catalogue removed that binding when the builder began requiring the registry through a real grant, so the tests were already failing on main; they now assert the builder requires what the npm seat delivers and carries no copy of its own, and that the forge holds the npm and git seats. Verified: go vet clean; the whole suite passes against a throwaway Postgres (make postgres), the new inventory tests included; gofmt clean apart from cmd/mesh-builder/stdout_test.go, which fails on main too.
678 lines
22 KiB
Go
678 lines
22 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
func manifest(name string, provides, requires []string) catalogue.Manifest {
|
|
return catalogue.Manifest{Module: name, Provides: catalogue.Offers(provides...), Requires: requires}
|
|
}
|
|
|
|
func TestAModuleRoundTripsWholeAndUnshredded(t *testing.T) {
|
|
// The manifest is held as it was given. Every field of it is read together when a node is
|
|
// resolved, and a manifest that gains a field should not need a migration before it can be
|
|
// stored — the module system is the thing most likely to grow.
|
|
inv := fresh(t)
|
|
m := catalogue.Manifest{
|
|
Module: "xorg", Provides: catalogue.Offers("display-server"),
|
|
Capabilities: []string{"seat"},
|
|
Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}},
|
|
Resources: []map[string]any{{"id": "conf", "type": "file", "path": "/etc/X11/x.conf"}},
|
|
}
|
|
if err := inv.RegisterModule(t.Context(), m, Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
shelf, err := inv.Catalogue(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, ok := shelf["xorg"]
|
|
if !ok {
|
|
t.Fatal("the module was not in the catalogue")
|
|
}
|
|
if len(back.Claims) != 1 || back.Claims[0].Name != "the-seat" {
|
|
t.Errorf("the claims did not survive: %+v", back.Claims)
|
|
}
|
|
if len(back.Resources) != 1 || back.Resources[0]["path"] != "/etc/X11/x.conf" {
|
|
t.Errorf("the resources did not survive: %+v", back.Resources)
|
|
}
|
|
}
|
|
|
|
func TestRegisteringAgainReplacesTheManifest(t *testing.T) {
|
|
// A manifest changing is the ordinary case — a module gains a requirement, a claim, a
|
|
// resource. What matters is that the change is what the next resolution sees.
|
|
inv := fresh(t)
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", []string{"a-thing"}, nil), Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
shelf, err := inv.Catalogue(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(shelf) != 1 {
|
|
t.Fatalf("registering twice made %d modules", len(shelf))
|
|
}
|
|
if len(shelf["thing"].Provides) != 1 {
|
|
t.Error("the second manifest did not replace the first")
|
|
}
|
|
}
|
|
|
|
func TestAModuleAMachineIsRunningCannotBeForgotten(t *testing.T) {
|
|
// Not a fault. It means a machine is running that module now, and removing the record would
|
|
// leave the mesh unable to describe what is on it.
|
|
inv := fresh(t)
|
|
if _, err := inv.AddNode(t.Context(), "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.Assign(t.Context(), "laptop", "thing"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err := inv.ForgetModule(t.Context(), "thing")
|
|
if !errors.Is(err, ErrStillAssigned) {
|
|
t.Fatalf("a module in use was forgotten: %v", err)
|
|
}
|
|
if err := inv.Unassign(t.Context(), "laptop", "thing"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.ForgetModule(t.Context(), "thing"); err != nil {
|
|
t.Errorf("an unassigned module could not be forgotten: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRemovingANodeTakesItsAssignments(t *testing.T) {
|
|
// The asymmetry with modules above, and it is deliberate: a node that is gone cannot be
|
|
// running anything, so its assignments are meaningless rather than dangerous.
|
|
inv := fresh(t)
|
|
node, err := inv.AddNode(t.Context(), "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.Assign(t.Context(), "laptop", "thing"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.store.Pool().Exec(t.Context(), `delete from node where id = $1`, node.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var left int
|
|
if err := inv.store.Pool().QueryRow(t.Context(),
|
|
`select count(*) from assignment`).Scan(&left); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if left != 0 {
|
|
t.Errorf("%d assignment(s) outlived the node they were on", left)
|
|
}
|
|
// And the module itself survives, because other nodes may be running it.
|
|
shelf, err := inv.Catalogue(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(shelf) != 1 {
|
|
t.Error("removing a node took a module with it")
|
|
}
|
|
}
|
|
|
|
func TestAssigningAModuleTheMeshDoesNotKnowIsRefused(t *testing.T) {
|
|
// Said as "no module of that name" rather than as a foreign key. A person mistyping a module
|
|
// name should be told that, not shown a constraint.
|
|
inv := fresh(t)
|
|
if _, err := inv.AddNode(t.Context(), "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err := inv.Assign(t.Context(), "laptop", "not-a-module")
|
|
if !errors.Is(err, ErrNoSuchModule) {
|
|
t.Fatalf("assigning an unknown module gave %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAssigningTwiceIsNotAnError(t *testing.T) {
|
|
// It is a statement of what should be true, and it already is.
|
|
inv := fresh(t)
|
|
if _, err := inv.AddNode(t.Context(), "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
if err := inv.Assign(t.Context(), "laptop", "thing"); err != nil {
|
|
t.Fatalf("assigning again failed: %v", err)
|
|
}
|
|
}
|
|
assigned, err := inv.Assigned(t.Context(), "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(assigned) != 1 {
|
|
t.Errorf("assigned three times and got %v", assigned)
|
|
}
|
|
}
|
|
|
|
func TestANodeThatNeverReportedHasNoCapabilities(t *testing.T) {
|
|
// Not "everything". A node that has never spoken will refuse anything needing a capability,
|
|
// which is wrong but visible — where assuming it can do everything would assign work it
|
|
// cannot do and find out on the machine.
|
|
inv := fresh(t)
|
|
if _, err := inv.AddNode(t.Context(), "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
caps, err := inv.ProfileOf(t.Context(), "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(caps) != 0 {
|
|
t.Errorf("a node that never reported has capabilities: %v", caps)
|
|
}
|
|
}
|
|
|
|
func TestOnlyPresentCapabilitiesCount(t *testing.T) {
|
|
// A profile lists what was looked for and whether it was found. A capability that was looked
|
|
// for and absent is the same as one nobody looked for, as far as what may run here goes —
|
|
// and reading the list without the verdict would let a module onto a machine that reported
|
|
// "no".
|
|
inv := fresh(t)
|
|
node, err := inv.AddNode(t.Context(), "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordProfile(t.Context(), node.ID, map[string]any{
|
|
"capabilities": []any{
|
|
map[string]any{"name": "seat", "present": true},
|
|
map[string]any{"name": "firewall", "present": false},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
caps, err := inv.ProfileOf(t.Context(), "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !caps["seat"] {
|
|
t.Error("a capability the node reported as present is missing")
|
|
}
|
|
if caps["firewall"] {
|
|
t.Error("a capability the node reported as ABSENT was counted as present")
|
|
}
|
|
}
|
|
|
|
func TestAModuleWithNoSourceIsNeverBehind(t *testing.T) {
|
|
// It was handed over directly, which is how a one-off arrives and how every module got here
|
|
// before provenance existed. Saying "out of date" about it would be inventing a comparison
|
|
// against nothing.
|
|
inv := fresh(t)
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
from, err := inv.SourceOf(t.Context(), "thing")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !from.Current() {
|
|
t.Error("a module with no source was reported as behind")
|
|
}
|
|
behind, err := inv.Behind(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(behind) != 0 {
|
|
t.Errorf("a module with no source is in the behind list: %v", behind)
|
|
}
|
|
}
|
|
|
|
func TestASourceThatMovedMakesTheModuleBehind(t *testing.T) {
|
|
inv := fresh(t)
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil),
|
|
Source{Repository: "novox/thing", Ref: "main", BuiltFrom: "aaaa1111"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
from, err := inv.SourceOf(t.Context(), "thing")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !from.Current() {
|
|
t.Fatal("a module built from the only commit its source has is behind")
|
|
}
|
|
|
|
if err := inv.SourceMoved(t.Context(), "thing", "bbbb2222"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
from, err = inv.SourceOf(t.Context(), "thing")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if from.Current() {
|
|
t.Error("the source moved and the module still reports as current")
|
|
}
|
|
}
|
|
|
|
func TestBeingBehindNamesTheMachinesRunningTheOldOne(t *testing.T) {
|
|
// The question somebody actually has. A module being out of date is a fact about the
|
|
// catalogue; machines running last week's version is the thing with consequences.
|
|
inv := fresh(t)
|
|
for _, n := range []string{"laptop", "workstation"} {
|
|
if _, err := inv.AddNode(t.Context(), n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil),
|
|
Source{Repository: "novox/thing", BuiltFrom: "aaaa1111"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, n := range []string{"laptop", "workstation"} {
|
|
if err := inv.Assign(t.Context(), n, "thing"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := inv.SourceMoved(t.Context(), "thing", "bbbb2222"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
behind, err := inv.Behind(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(behind["thing"]) != 2 {
|
|
t.Errorf("running on %v; both machines have the old one", behind["thing"])
|
|
}
|
|
}
|
|
|
|
func TestRebuildingCatchesUp(t *testing.T) {
|
|
inv := fresh(t)
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil),
|
|
Source{Repository: "novox/thing", BuiltFrom: "aaaa1111"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.SourceMoved(t.Context(), "thing", "bbbb2222"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", []string{"a-thing"}, nil),
|
|
Source{Repository: "novox/thing", BuiltFrom: "bbbb2222"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
from, err := inv.SourceOf(t.Context(), "thing")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !from.Current() {
|
|
t.Errorf("built from the commit the source has and still behind: %+v", from)
|
|
}
|
|
}
|
|
|
|
func TestHandingOverAManifestDoesNotEraseWhereItComesFrom(t *testing.T) {
|
|
// Fixing something in a hurry is legitimate. Silently forgetting where the module normally
|
|
// comes from is not: it is the only thing that would say, afterwards, that a machine is
|
|
// running something nobody can rebuild.
|
|
inv := fresh(t)
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil),
|
|
Source{Repository: "novox/thing", Ref: "main", BuiltFrom: "aaaa1111"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", []string{"patched"}, nil),
|
|
Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
from, err := inv.SourceOf(t.Context(), "thing")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if from.Repository != "novox/thing" {
|
|
t.Errorf("handing over a manifest erased the source: %+v", from)
|
|
}
|
|
}
|
|
|
|
func TestASourceNobodyHasCheckedIsNotBehind(t *testing.T) {
|
|
// A module built from a commit, where nothing has yet told the mesh whether that source has
|
|
// moved. It is not behind — nobody has looked. Reporting it as behind would put every module
|
|
// on the list the moment provenance was recorded, which makes the list say nothing.
|
|
inv := fresh(t)
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil),
|
|
Source{Repository: "novox/thing", BuiltFrom: "aaaa1111"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
from, err := inv.SourceOf(t.Context(), "thing")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Registering sets the head to what was built, so the two agree until something says
|
|
// otherwise. Either way it must not read as behind.
|
|
if !from.Current() {
|
|
t.Errorf("a source nobody has checked reports as behind: %+v", from)
|
|
}
|
|
|
|
// And with the head genuinely unknown, which is what a module registered before provenance
|
|
// existed looks like after somebody adds a source to it.
|
|
if (Source{Repository: "novox/thing", BuiltFrom: "aaaa1111"}).Current() == false {
|
|
t.Error("a module with no known head reports as behind")
|
|
}
|
|
}
|
|
|
|
func TestAPinSurvivesAndCanBeChanged(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := context.Background()
|
|
for _, n := range []string{"user", "first", "second"} {
|
|
if _, err := inv.AddNode(ctx, n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := inv.PinProvision(ctx, "user", "postgres-database", "first"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Changing the answer replaces it rather than adding a second, or a machine would be told to
|
|
// use two databases and nothing would say which.
|
|
if err := inv.PinProvision(ctx, "user", "postgres-database", "second"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pins, err := inv.PinsFor(ctx, "user")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(pins) != 1 || pins["postgres-database"] != "second" {
|
|
t.Fatalf("got %v", pins)
|
|
}
|
|
if err := inv.UnpinProvision(ctx, "user", "postgres-database"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pins, _ := inv.PinsFor(ctx, "user"); len(pins) != 0 {
|
|
t.Fatalf("the choice outlived being removed: %v", pins)
|
|
}
|
|
// Removing something that was never said is a mistake worth reporting, not a silent success.
|
|
if err := inv.UnpinProvision(ctx, "user", "postgres-database"); err == nil {
|
|
t.Fatal("unpinning something nobody pinned reported success")
|
|
}
|
|
}
|
|
|
|
func TestAPinGoesWhenTheProviderLeavesTheMesh(t *testing.T) {
|
|
// Otherwise a machine is pointed at something that no longer exists and reported as
|
|
// configured, which is the failure mode this whole project keeps refusing.
|
|
inv := fresh(t)
|
|
ctx := context.Background()
|
|
for _, n := range []string{"consumer", "provider"} {
|
|
if _, err := inv.AddNode(ctx, n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := inv.PinProvision(ctx, "consumer", "postgres-database", "provider"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.store.Pool().Exec(ctx, `delete from node where name = 'provider'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Counted in the table, not read through PinsFor. PinsFor joins on the provider, so a pin
|
|
// left behind by a departed node is invisible through it whether or not it was cleaned up —
|
|
// which made the first version of this test pass with the cascade removed.
|
|
rows, err := inv.pinRows(ctx, "consumer")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rows != 0 {
|
|
t.Fatalf("a choice outlived the machine it named: %d row(s) left", rows)
|
|
}
|
|
}
|
|
|
|
func TestTheCatalogueSaysWhereEachModuleCameFromAndWhoRunsIt(t *testing.T) {
|
|
// The provenance was recorded from the first build and nothing showed it, which made "is this
|
|
// current?" a question you could only answer by reading the database.
|
|
inv := fresh(t)
|
|
ctx := context.Background()
|
|
for _, n := range []string{"workstation", "laptop"} {
|
|
if _, err := inv.AddNode(ctx, n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := inv.RegisterModule(ctx, manifest("shell", []string{"login-shell"}, nil),
|
|
Source{Repository: "https://forge.invalid/shell.git", BuiltFrom: "aaa", Head: "aaa"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(ctx, manifest("byhand", nil, nil), Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.Provide(ctx, manifest("networking", nil, []string{"login-shell"})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, n := range []string{"workstation", "laptop"} {
|
|
if err := inv.Assign(ctx, n, "shell"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
entries, err := inv.Catalogued(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
by := map[string]Entry{}
|
|
for _, e := range entries {
|
|
by[e.Manifest.Module] = e
|
|
}
|
|
if len(by) != 3 {
|
|
t.Fatalf("the catalogue has %d modules", len(by))
|
|
}
|
|
|
|
// Sorted, and both nodes, so a person reading it twice sees the same thing.
|
|
if got := strings.Join(by["shell"].On, ","); got != "laptop,workstation" {
|
|
t.Fatalf("shell runs on %q", got)
|
|
}
|
|
if by["shell"].Source.Repository != "https://forge.invalid/shell.git" {
|
|
t.Fatalf("shell came from %q", by["shell"].Source.Repository)
|
|
}
|
|
if by["shell"].Provided {
|
|
t.Fatal("a module built from a repository was reported as shipped with the control plane")
|
|
}
|
|
|
|
// A module nobody runs is in the catalogue: the catalogue is what EXISTS, and what runs is a
|
|
// different question the same row answers.
|
|
if len(by["byhand"].On) != 0 {
|
|
t.Fatalf("byhand runs on %v", by["byhand"].On)
|
|
}
|
|
// Handed over by hand is its own state. Nothing can rebuild it, and showing it as a
|
|
// repository would send somebody looking for one.
|
|
if by["byhand"].Source.Repository != "" || by["byhand"].Provided {
|
|
t.Fatalf("byhand: %+v", by["byhand"])
|
|
}
|
|
|
|
if !by["networking"].Provided {
|
|
t.Fatal("a module the control plane ships was not marked as such")
|
|
}
|
|
if by["networking"].Source.Repository != "" {
|
|
// It is not a repository, and showing it as one would have somebody go looking for it.
|
|
t.Fatalf("networking claims to come from %q", by["networking"].Source.Repository)
|
|
}
|
|
}
|
|
|
|
func TestACatalogueEntryKnowsWhetherItIsBehind(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := context.Background()
|
|
if err := inv.RegisterModule(ctx, manifest("shell", nil, nil),
|
|
Source{Repository: "https://forge.invalid/shell.git", BuiltFrom: "aaa", Head: "aaa"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.SourceMoved(ctx, "shell", "bbb"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries, err := inv.Catalogued(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if entries[0].Source.Current() {
|
|
t.Fatal("a module whose source moved reported itself current")
|
|
}
|
|
}
|
|
|
|
// A capability's detail is half of what the machine said, and it was being thrown away.
|
|
//
|
|
// novox/hq ADR 0009: a capability's presence gates an assignment and its detail carries a value —
|
|
// `seat: card1-DP-1`. So *can this run here* and *what should it be configured as* are one fact
|
|
// read two ways, and keeping only the first read makes the second unanswerable.
|
|
func TestWhatAMachineSaidAboutItselfIsKeptWhole(t *testing.T) {
|
|
inv := ForTest(t)
|
|
ctx := t.Context()
|
|
node, err := inv.AddNode(ctx, "workstation")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordProfile(ctx, node.ID, map[string]any{"capabilities": []map[string]any{
|
|
{"name": "seat", "present": true, "detail": "card1-DP-1, card1-HDMI-A-1"},
|
|
{"name": "container-runtime", "present": false, "detail": "docker is not installed"},
|
|
}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
held, err := inv.Profile(ctx, "workstation")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(held) != 2 {
|
|
t.Fatalf("the machine reported two things and the mesh kept %d", len(held))
|
|
}
|
|
found := map[string]Capability{}
|
|
for _, c := range held {
|
|
found[c.Name] = c
|
|
}
|
|
if found["seat"].Detail != "card1-DP-1, card1-HDMI-A-1" {
|
|
t.Fatalf("the value the machine reported was discarded: %+v", found["seat"])
|
|
}
|
|
// And the absent one keeps its reason, which is the case a person most needs explaining:
|
|
// "this machine has no container runtime" is the answer and "docker is not installed" is why.
|
|
if found["container-runtime"].Present {
|
|
t.Fatal("something the machine said it does not have was recorded as present")
|
|
}
|
|
if found["container-runtime"].Detail != "docker is not installed" {
|
|
t.Fatalf("the reason a capability is absent was discarded: %+v", found["container-runtime"])
|
|
}
|
|
|
|
// The gating read is unchanged, and takes only what is present.
|
|
gates, err := inv.ProfileOf(ctx, "workstation")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !gates["seat"] || gates["container-runtime"] {
|
|
t.Fatalf("the gating read disagrees with what the machine said: %+v", gates)
|
|
}
|
|
}
|
|
|
|
// Never reported is not the same as reported nothing: one machine has not run the host, the other
|
|
// ran it and can do nothing, and the remedies are different.
|
|
func TestNeverReportedAndReportedNothingAreDifferentProfiles(t *testing.T) {
|
|
inv := ForTest(t)
|
|
ctx := t.Context()
|
|
silent, err := inv.AddNode(ctx, "silent")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
empty, err := inv.AddNode(ctx, "empty")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordProfile(ctx, empty.ID,
|
|
map[string]any{"capabilities": []map[string]any{}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = silent
|
|
|
|
never, err := inv.Profile(ctx, "silent")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if never != nil {
|
|
t.Fatalf("a machine that never reported looks like one that reported nothing: %+v", never)
|
|
}
|
|
nothing, err := inv.Profile(ctx, "empty")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if nothing == nil {
|
|
t.Fatal("a machine that reported nothing looks like one that never reported")
|
|
}
|
|
}
|
|
|
|
// Defends novox/hq ADR 0111: a source on a seat is recorded as a path and the seat, never an
|
|
// address, and a module recorded before the column existed keeps meaning a URL.
|
|
func TestASourceOnASeatIsRecordedAsItsPathAndTheSeat(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := t.Context()
|
|
if err := inv.RegisterModule(ctx, manifest("gitea-built", nil, nil), Source{
|
|
Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/gitea", Ref: "main",
|
|
BuiltFrom: "aaaa1111",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(ctx, manifest("external", nil, nil), Source{
|
|
Repository: "https://example.invalid/someone/something.git", BuiltFrom: "bbbb2222",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
own, err := inv.SourceOf(ctx, "gitea-built")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if own.Seat != "git" || own.Repository != "novox/mesh-catalog" || own.Path != "modules/gitea" {
|
|
t.Fatalf("recorded as %+v", own)
|
|
}
|
|
if strings.Contains(own.Repository, "://") {
|
|
t.Fatalf("an address was recorded for a source on a seat: %s", own.Repository)
|
|
}
|
|
|
|
elsewhere, err := inv.SourceOf(ctx, "external")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if elsewhere.Seat != "" {
|
|
t.Fatalf("an external repository was put on a seat: %+v", elsewhere)
|
|
}
|
|
|
|
// And the list every rebuild walks carries the seat, or `build --behind` would clone the path
|
|
// as though it were a URL.
|
|
all, err := inv.Catalogued(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, e := range all {
|
|
if e.Manifest.Module == "gitea-built" && e.Source.Seat != "git" {
|
|
t.Fatalf("the rebuild list lost the seat: %+v", e.Source)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegisteringWithoutProvenanceKeepsTheSeat(t *testing.T) {
|
|
// A manifest handed over by hand keeps the record of where the module normally comes from —
|
|
// the seat included, or the next rebuild would treat a path as a URL.
|
|
inv := fresh(t)
|
|
ctx := t.Context()
|
|
if err := inv.RegisterModule(ctx, manifest("thing", nil, nil), Source{
|
|
Repository: "novox/thing", Seat: "git", BuiltFrom: "aaaa1111",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(ctx, manifest("thing", []string{"a-thing"}, nil), Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := inv.SourceOf(ctx, "thing")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Seat != "git" || got.Repository != "novox/thing" {
|
|
t.Fatalf("a hand-registered manifest erased where the module comes from: %+v", got)
|
|
}
|
|
}
|