CAT-G1, G2: the forge holds both package seats over mesh-sdk's receiving side and caller list
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
// Package packageseats is the forge module's holding of the two package seats, `npm-package-registry` and
|
||||
// `go-package-registry` (novox/hq design 51 *The four registry seats*, ADR 0605, ADR 0611). A build module publishes
|
||||
// a package through `publish` and `publish-part`: the package's one file lands in the forge module's own object store
|
||||
// `incoming`, is hashed, checked against the builder's numbering and then uploaded into the forge's package registry
|
||||
// with the module's own write side. The receiving side is mesh-sdk's seat-neutral parts.Receiver and the caller list
|
||||
// mesh-sdk's VerbCallers (ADR 0627 §2): this package binds both to the two seats and to the forge.
|
||||
package packageseats
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/parts"
|
||||
)
|
||||
|
||||
// The seats the forge module holds.
|
||||
const (
|
||||
NpmSeat = "npm-package-registry"
|
||||
GoSeat = "go-package-registry"
|
||||
)
|
||||
|
||||
// Seats are the package seats, each with the forge's own package type behind it.
|
||||
var Seats = map[string]string{NpmSeat: "npm", GoSeat: "go"}
|
||||
|
||||
// The verbs both seats serve (design 51 *One contract*).
|
||||
const (
|
||||
VerbPublish = "publish"
|
||||
VerbPublishPart = "publish-part"
|
||||
VerbVersions = "versions"
|
||||
VerbRemove = "remove"
|
||||
)
|
||||
|
||||
// Verbs are the verbs of both seats, in order.
|
||||
var Verbs = []string{VerbPublish, VerbPublishPart, VerbRemove, VerbVersions}
|
||||
|
||||
// The events of both seats.
|
||||
const (
|
||||
EventPublished = "published"
|
||||
EventRefused = "refused"
|
||||
EventRemoved = "removed"
|
||||
)
|
||||
|
||||
// The bounds of incoming (design 51 *Publishing*, ADR 0611): a chunk at most 1 MiB, a transfer with no chunk for 24
|
||||
// hours dropped, a finding past 80 % of the cap. A package is one file, far smaller than an image, so the cap is the
|
||||
// manifest's 2 GiB.
|
||||
const (
|
||||
MaxChunk = 1 << 20
|
||||
IncomingCap = 2 << 30
|
||||
TransferIdle = 24 * time.Hour
|
||||
IncomingFullPct = 80
|
||||
IncomingObjects = "incoming"
|
||||
TransfersState = "transfers"
|
||||
// KindPackage is the one kind a package seat holds.
|
||||
KindPackage = "package"
|
||||
// Self is the forge module, the holder every outcome names.
|
||||
Self = "gitea"
|
||||
// ReasonRetention is the reason of the forge's own retention removing a version nothing keeps.
|
||||
ReasonRetention = "retention"
|
||||
)
|
||||
|
||||
// Callers is the trusted config value `callers`, read as mesh-sdk's list per verb.
|
||||
type Callers = stdio.VerbCallers
|
||||
|
||||
// DefaultCallers is design 51's list for the package seats: publish and publish-part the two build modules that
|
||||
// publish packages; remove the builder (a tagged version whose branch ended); versions anyone on the mesh, as the
|
||||
// registry's reads are anonymous.
|
||||
func DefaultCallers() Callers {
|
||||
builds := []string{"mesh-build-go", "mesh-build-node"}
|
||||
return Callers{VerbPublish: builds, VerbPublishPart: builds, VerbRemove: {"mesh-delivery-builder"},
|
||||
VerbVersions: {stdio.AnyCaller}}
|
||||
}
|
||||
|
||||
// ParseCallers reads the config value; empty is DefaultCallers.
|
||||
func ParseCallers(raw string) (Callers, error) {
|
||||
return stdio.ParseVerbCallers(raw, Verbs, DefaultCallers())
|
||||
}
|
||||
|
||||
// Publish is a publish as the seats' contract names it (mesh-contracts npmpackageregistry and gopackageregistry share
|
||||
// one shape): what the tools bundle reads from either seat's PublishInput.
|
||||
type Publish struct {
|
||||
Transfer string `json:"transfer"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
Digest string `json:"digest"`
|
||||
Parts []parts.Want `json:"parts,omitempty"`
|
||||
}
|
||||
|
||||
// Opening is what the holder keeps of a publish beside the receiving side's own fields.
|
||||
type Opening struct {
|
||||
Publish
|
||||
Seat string `json:"seat"`
|
||||
}
|
||||
|
||||
// Transfer is one open publish in the state transfers.
|
||||
type Transfer = parts.Transfer[Opening]
|
||||
|
||||
// Outcome is the body of published, refused and removed, as the contract's Outcome names it.
|
||||
type Outcome struct {
|
||||
Name, Version, Kind, Tag, Digest, Fingerprint, Commit, Transfer, Reference, Reason string
|
||||
// Module is the Go module path a Go package is fetched under; empty on the npm seat.
|
||||
Module string
|
||||
}
|
||||
|
||||
// Answer is what publish and publish-part answer: the transfer, the parts lacked, and whether it ended.
|
||||
type Answer struct {
|
||||
Transfer string
|
||||
Lacks []string
|
||||
Ended bool
|
||||
}
|
||||
|
||||
// Version is one version a seat holds of a name.
|
||||
type Version struct {
|
||||
Version string
|
||||
Digest string
|
||||
Published time.Time
|
||||
}
|
||||
|
||||
// Builder answers whether the builder numbered a version with a digest (mesh-sdk delivery.BuilderVersions).
|
||||
type Builder interface {
|
||||
Recorded(name, version, digest string) (ok bool, why string, err error)
|
||||
}
|
||||
|
||||
// Store is the forge's package registry, written with the module's own write side alone.
|
||||
type Store interface {
|
||||
// Held is the sha256 digest of name@version's file when the registry holds it.
|
||||
Held(seat, name, version string) (digest string, ok bool, err error)
|
||||
// Put uploads the package's file and answers the Go module path (empty for npm).
|
||||
Put(seat string, p Publish, file []byte) (module string, err error)
|
||||
// Versions is every version of name, newest first.
|
||||
Versions(seat, name string) ([]Version, error)
|
||||
// Remove deletes name@version.
|
||||
Remove(seat, name, version string) error
|
||||
}
|
||||
|
||||
// Holder answers the verbs of both seats.
|
||||
type Holder struct {
|
||||
Callers Callers
|
||||
Objects parts.Objects
|
||||
Transfers parts.Transfers[Opening]
|
||||
Builder Builder
|
||||
Store Store
|
||||
// Say says an event on a seat's family.
|
||||
Say func(seat, event string, o Outcome) error
|
||||
// Finding writes (raised) or clears the module's finding of a kind for a subject.
|
||||
Finding func(kind, subject, words string, raised bool)
|
||||
Now func() time.Time
|
||||
Cap uint64
|
||||
}
|
||||
|
||||
func (h *Holder) now() time.Time {
|
||||
if h.Now != nil {
|
||||
return h.Now().UTC()
|
||||
}
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
// Reference is the seat's answer for a package: <name>@<version> (ADR 0605 §9).
|
||||
func Reference(name, version string) string { return name + "@" + version }
|
||||
|
||||
// key is a transfer's key: the caller's own name under its seat, so the two seats' transfers never meet.
|
||||
func key(seat, transfer string) string { return seat + "_" + transfer }
|
||||
|
||||
func (h *Holder) receiver() *parts.Receiver[Opening] {
|
||||
cap := h.Cap
|
||||
if cap == 0 {
|
||||
cap = IncomingCap
|
||||
}
|
||||
return &parts.Receiver[Opening]{Objects: h.Objects, Transfers: h.Transfers, Cap: cap, MaxChunk: MaxChunk,
|
||||
Idle: TransferIdle, Full: IncomingFullPct, Now: h.now,
|
||||
// The builder's numbering is asked before anything is uploaded (ADR 0605 §4).
|
||||
Check: func(t Transfer) error {
|
||||
ok, why, err := h.Builder.Recorded(t.Shape.Name, t.Shape.Version, t.Shape.Digest)
|
||||
if err != nil {
|
||||
return errors.New("builder_versions did not answer, so nothing is stored: " + err.Error())
|
||||
}
|
||||
if !ok {
|
||||
return errors.New("the builder did not number it: " + why)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Store: func(t Transfer, held map[string][]byte) error {
|
||||
file := held[t.Shape.Digest]
|
||||
module, err := h.Store.Put(t.Shape.Seat, t.Shape.Publish, file)
|
||||
if err != nil {
|
||||
reason := "the forge did not take it: " + err.Error()
|
||||
return errors.Join(errors.New("refused: "+reason), h.refused(t, reason))
|
||||
}
|
||||
h.Finding("publish-refused", t.Shape.Name+"@"+t.Shape.Version, "", false)
|
||||
h.Finding("publish-refused", IncomingObjects, "", false)
|
||||
o := outcomeOf(t.Shape.Publish, "")
|
||||
o.Reference, o.Module = Reference(t.Shape.Name, t.Shape.Version), module
|
||||
return h.Say(t.Shape.Seat, EventPublished, o)
|
||||
},
|
||||
Refuse: h.refused,
|
||||
Filled: func(used, cap uint64, full bool) {
|
||||
h.Finding("incoming-full", IncomingObjects, fmt.Sprintf("incoming holds %d of its %d bytes (%d %%)", used, cap, used*100/cap), full)
|
||||
}}
|
||||
}
|
||||
|
||||
func (h *Holder) refused(t Transfer, reason string) error {
|
||||
subject := t.Shape.Name + "@" + t.Shape.Version
|
||||
if parts.RefusedAtCap(reason) {
|
||||
subject = IncomingObjects
|
||||
}
|
||||
h.Finding("publish-refused", subject, reason, true)
|
||||
return h.Say(t.Shape.Seat, EventRefused, outcomeOf(t.Shape.Publish, reason))
|
||||
}
|
||||
|
||||
func outcomeOf(p Publish, reason string) Outcome {
|
||||
return Outcome{Name: p.Name, Version: p.Version, Kind: p.Kind, Tag: p.Tag, Digest: p.Digest, Fingerprint: p.Fingerprint,
|
||||
Commit: p.Commit, Transfer: p.Transfer, Reason: reason}
|
||||
}
|
||||
|
||||
func (h *Holder) answer(transfer string, r parts.Received) Answer {
|
||||
return Answer{Transfer: transfer, Lacks: r.Lacks, Ended: r.Ended}
|
||||
}
|
||||
|
||||
// Publish is `publish` on seat for the proved caller: a package is one file, its digest the file's sha256.
|
||||
func (h *Holder) Publish(seat, caller string, p Publish) (Answer, error) {
|
||||
if err := h.Callers.Allowed(VerbPublish, caller); err != nil {
|
||||
return Answer{}, err
|
||||
}
|
||||
if err := checkPublish(seat, p); err != nil {
|
||||
return Answer{}, err
|
||||
}
|
||||
_, _ = h.receiver().DropIdle()
|
||||
// A rebuild of a published version answers it held and uploads nothing; the same version with other content is
|
||||
// refused, as the registry would refuse it.
|
||||
if d, ok, err := h.Store.Held(seat, p.Name, p.Version); err != nil {
|
||||
return Answer{}, fmt.Errorf("publish: the forge cannot say whether it holds %s@%s: %w", p.Name, p.Version, err)
|
||||
} else if ok {
|
||||
if d != p.Digest {
|
||||
reason := fmt.Sprintf("%s@%s is published with %s, not %s", p.Name, p.Version, d, p.Digest)
|
||||
return Answer{Transfer: p.Transfer, Ended: true}, h.refused(Transfer{Shape: Opening{Publish: p, Seat: seat}}, reason)
|
||||
}
|
||||
o := outcomeOf(p, "")
|
||||
o.Reference = Reference(p.Name, p.Version)
|
||||
return Answer{Transfer: p.Transfer, Ended: true}, h.Say(seat, EventPublished, o)
|
||||
}
|
||||
got, err := h.receiver().Open(key(seat, p.Transfer), caller, p.Parts, Opening{Publish: p, Seat: seat})
|
||||
if err != nil {
|
||||
return h.answer(p.Transfer, got), fmt.Errorf("publish: %w", err)
|
||||
}
|
||||
return h.answer(p.Transfer, got), nil
|
||||
}
|
||||
|
||||
// PublishPart is `publish-part`: one chunk, from the caller that opened the transfer alone.
|
||||
func (h *Holder) PublishPart(seat, caller, transfer, part string, sequence int, last bool, data []byte) (Answer, error) {
|
||||
if err := h.Callers.Allowed(VerbPublishPart, caller); err != nil {
|
||||
return Answer{}, err
|
||||
}
|
||||
got, err := h.receiver().Take(key(seat, transfer), caller, part, sequence, last, data, func(t Transfer) error {
|
||||
if t.Shape.Seat != seat {
|
||||
return fmt.Errorf("transfer %s is %s's, not %s's", transfer, t.Shape.Seat, seat)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return h.answer(transfer, got), fmt.Errorf("publish-part: %w", err)
|
||||
}
|
||||
return h.answer(transfer, got), nil
|
||||
}
|
||||
|
||||
// Versions is `versions`: every version the seat holds of name, newest first.
|
||||
func (h *Holder) Versions(seat, caller, name string) ([]Version, error) {
|
||||
if err := h.Callers.Allowed(VerbVersions, caller); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := Seats[seat]; !ok {
|
||||
return nil, fmt.Errorf("versions: the forge holds no seat %s", seat)
|
||||
}
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return nil, errors.New("versions: no name")
|
||||
}
|
||||
v, err := h.Store.Versions(seat, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("versions: the forge cannot list %s: %w", name, err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Remove is `remove`: the builder removes a tagged version (its branch ended); an untagged version goes only by the
|
||||
// forge's own retention, never by a call. A version already gone is said removed all the same, so a retry ends.
|
||||
func (h *Holder) Remove(seat, caller, name, version, reason string) (Outcome, error) {
|
||||
if err := h.Callers.Allowed(VerbRemove, caller); err != nil {
|
||||
return Outcome{}, err
|
||||
}
|
||||
if _, ok := Seats[seat]; !ok {
|
||||
return Outcome{}, fmt.Errorf("remove: the forge holds no seat %s", seat)
|
||||
}
|
||||
if strings.TrimSpace(name) == "" || strings.TrimSpace(reason) == "" {
|
||||
return Outcome{}, errors.New("remove: a name and a reason are required")
|
||||
}
|
||||
if !strings.Contains(version, "-") {
|
||||
return Outcome{}, fmt.Errorf("remove: refused: %s@%s has no tag; only a tagged version is removed, an untagged one by retention alone", name, version)
|
||||
}
|
||||
d, held, err := h.Store.Held(seat, name, version)
|
||||
if err != nil {
|
||||
return Outcome{}, fmt.Errorf("remove: the forge cannot read %s@%s: %w", name, version, err)
|
||||
}
|
||||
if held {
|
||||
if err := h.Store.Remove(seat, name, version); err != nil {
|
||||
return Outcome{}, fmt.Errorf("remove: the forge did not remove %s@%s: %w", name, version, err)
|
||||
}
|
||||
}
|
||||
o := Outcome{Name: name, Version: version, Kind: KindPackage, Digest: d, Reason: reason}
|
||||
return o, h.Say(seat, EventRemoved, o)
|
||||
}
|
||||
|
||||
// Retained says `removed` for a version the forge's own retention deleted (design 51 *Retention*).
|
||||
func (h *Holder) Retained(seat, name, version, digest string) error {
|
||||
return h.Say(seat, EventRemoved, Outcome{Name: name, Version: version, Kind: KindPackage, Digest: digest, Reason: ReasonRetention})
|
||||
}
|
||||
|
||||
// Drop drops a transfer by hand; DropIdle every transfer idle for TransferIdle.
|
||||
func (h *Holder) Drop(seat, transfer string) (Transfer, error) {
|
||||
return h.receiver().Drop(key(seat, transfer))
|
||||
}
|
||||
|
||||
func checkPublish(seat string, p Publish) error {
|
||||
switch {
|
||||
case Seats[seat] == "":
|
||||
return fmt.Errorf("publish: the forge holds no seat %s", seat)
|
||||
case strings.TrimSpace(p.Name) == "":
|
||||
return errors.New("publish: no name")
|
||||
case strings.TrimSpace(p.Version) == "":
|
||||
return errors.New("publish: a package is published under the version the builder gave it")
|
||||
case p.Kind != KindPackage:
|
||||
return fmt.Errorf("publish: %s holds packages, not a %s", seat, p.Kind)
|
||||
case !strings.HasPrefix(p.Digest, "sha256:"):
|
||||
return fmt.Errorf("publish: digest %q is no sha256 digest", p.Digest)
|
||||
case len(p.Parts) != 1 || p.Parts[0].Digest != p.Digest:
|
||||
return errors.New("publish: a package is one file, its one part the package's digest")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package packageseats
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/parts"
|
||||
)
|
||||
|
||||
type memObjects map[string][]byte
|
||||
|
||||
func (m memObjects) Put(n string, d []byte) error { m[n] = append([]byte(nil), d...); return nil }
|
||||
func (m memObjects) Get(n string) ([]byte, error) {
|
||||
if d, ok := m[n]; ok {
|
||||
return d, nil
|
||||
}
|
||||
return nil, errors.New("no object " + n)
|
||||
}
|
||||
func (m memObjects) Delete(n string) error { delete(m, n); return nil }
|
||||
func (m memObjects) Used() (uint64, error) {
|
||||
var u uint64
|
||||
for _, d := range m {
|
||||
u += uint64(len(d))
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
type memTransfers map[string]Transfer
|
||||
|
||||
func (m memTransfers) Get(id string) (Transfer, bool, error) { t, ok := m[id]; return t, ok, nil }
|
||||
func (m memTransfers) Put(t Transfer) error { m[t.ID] = t; return nil }
|
||||
func (m memTransfers) Delete(id string) error { delete(m, id); return nil }
|
||||
func (m memTransfers) List() ([]Transfer, error) {
|
||||
var out []Transfer
|
||||
for _, t := range m {
|
||||
out = append(out, t)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type builder struct{ ok bool }
|
||||
|
||||
func (b builder) Recorded(_, _, _ string) (bool, string, error) { return b.ok, "not numbered", nil }
|
||||
|
||||
// forge is the forge's package registry: seat -> name@version -> file.
|
||||
type forge struct {
|
||||
files map[string]map[string][]byte
|
||||
removed []string
|
||||
}
|
||||
|
||||
func (f *forge) Held(seat, name, version string) (string, bool, error) {
|
||||
b, ok := f.files[seat][name+"@"+version]
|
||||
if !ok {
|
||||
return "", false, nil
|
||||
}
|
||||
return digest(b), true, nil
|
||||
}
|
||||
func (f *forge) Put(seat string, p Publish, file []byte) (string, error) {
|
||||
if f.files[seat] == nil {
|
||||
f.files[seat] = map[string][]byte{}
|
||||
}
|
||||
f.files[seat][p.Name+"@"+p.Version] = file
|
||||
if seat == GoSeat {
|
||||
return "git.example/" + p.Name, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
func (f *forge) Versions(seat, name string) ([]Version, error) {
|
||||
var out []Version
|
||||
for k, b := range f.files[seat] {
|
||||
if n, v, _ := strings.Cut(k, "@"); n == name {
|
||||
out = append(out, Version{Version: v, Digest: digest(b)})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (f *forge) Remove(seat, name, version string) error {
|
||||
delete(f.files[seat], name+"@"+version)
|
||||
f.removed = append(f.removed, seat+" "+name+"@"+version)
|
||||
return nil
|
||||
}
|
||||
|
||||
func digest(b []byte) string { s := sha256.Sum256(b); return "sha256:" + hex.EncodeToString(s[:]) }
|
||||
|
||||
type said struct {
|
||||
seat, event string
|
||||
o Outcome
|
||||
}
|
||||
|
||||
type bed struct {
|
||||
h *Holder
|
||||
forge *forge
|
||||
said []said
|
||||
findings map[string]bool
|
||||
}
|
||||
|
||||
func newBed(numbered bool) *bed {
|
||||
b := &bed{forge: &forge{files: map[string]map[string][]byte{}}, findings: map[string]bool{}}
|
||||
b.h = &Holder{Callers: DefaultCallers(), Objects: memObjects{}, Transfers: memTransfers{}, Builder: builder{ok: numbered},
|
||||
Store: b.forge, Now: func() time.Time { return time.Unix(1e9, 0) },
|
||||
Say: func(seat, event string, o Outcome) error { b.said = append(b.said, said{seat, event, o}); return nil },
|
||||
Finding: func(kind, subject, _ string, raised bool) { b.findings[kind+" "+subject] = raised }}
|
||||
return b
|
||||
}
|
||||
|
||||
func pub(name, version string, file []byte) Publish {
|
||||
return Publish{Transfer: "t-" + name, Name: name, Version: version, Kind: KindPackage, Digest: digest(file),
|
||||
Parts: []parts.Want{{Digest: digest(file), Size: int64(len(file))}}}
|
||||
}
|
||||
|
||||
const goBuild, nodeBuild = "ace.mesh-build-go", "ace.mesh-build-node"
|
||||
|
||||
// **The forge module holds both package seats** (design 51 *Publishing*, *Testing* for the holder): a package's one
|
||||
// file comes in parts, is checked against the builder's numbering and its digest, and is uploaded with the forge's own
|
||||
// write side; the outcome is said on the seat's events.
|
||||
func TestTheForgeHoldsBothPackageSeats(t *testing.T) {
|
||||
file := []byte("a package's tarball")
|
||||
t.Run("a publish in parts is uploaded and said published", func(t *testing.T) {
|
||||
for _, seat := range []string{NpmSeat, GoSeat} {
|
||||
b := newBed(true)
|
||||
a, err := b.h.Publish(seat, goBuild, pub("mesh-sdk", "0.1.15", file))
|
||||
if err != nil || len(a.Lacks) != 1 || a.Transfer != "t-mesh-sdk" {
|
||||
t.Fatalf("%s: publish answered %+v, %v", seat, a, err)
|
||||
}
|
||||
a, err = b.h.PublishPart(seat, goBuild, "t-mesh-sdk", digest(file), 0, true, file)
|
||||
if err != nil || !a.Ended {
|
||||
t.Fatalf("%s: the last chunk answered %+v, %v", seat, a, err)
|
||||
}
|
||||
if len(b.said) != 1 || b.said[0].event != EventPublished || b.said[0].o.Reference != "mesh-sdk@0.1.15" {
|
||||
t.Fatalf("%s: said %+v", seat, b.said)
|
||||
}
|
||||
if (seat == GoSeat) != (b.said[0].o.Module != "") {
|
||||
t.Errorf("%s: the Go module path said is %q", seat, b.said[0].o.Module)
|
||||
}
|
||||
if len(b.h.Objects.(memObjects)) != 0 || len(b.h.Transfers.(memTransfers)) != 0 {
|
||||
t.Errorf("%s: the transfer's chunks or key stayed after its end", seat)
|
||||
}
|
||||
if v, _ := b.h.Versions(seat, "g14.anyone", "mesh-sdk"); len(v) != 1 || v[0].Digest != digest(file) {
|
||||
t.Errorf("%s: versions answered %+v", seat, v)
|
||||
}
|
||||
// A rebuild of the published version is held: nothing uploaded, published said again.
|
||||
if a, err := b.h.Publish(seat, goBuild, pub("mesh-sdk", "0.1.15", file)); err != nil || !a.Ended || len(a.Lacks) != 0 {
|
||||
t.Errorf("%s: a held version answered %+v, %v", seat, a, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("refusals", func(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
do func(b *bed) error
|
||||
want string
|
||||
}{
|
||||
{"a caller not on publish's list", func(b *bed) error {
|
||||
_, err := b.h.Publish(NpmSeat, "ace.claude-code", pub("x", "0.1.0", file))
|
||||
return err
|
||||
}, "not on the holder's caller list"},
|
||||
{"an unproven caller", func(b *bed) error {
|
||||
_, err := b.h.Publish(NpmSeat, stdio.Unproven, pub("x", "0.1.0", file))
|
||||
return err
|
||||
}, "no caller the tool runner proved"},
|
||||
{"a seat the forge does not hold", func(b *bed) error {
|
||||
_, err := b.h.Publish("cargo-package-registry", goBuild, pub("x", "0.1.0", file))
|
||||
return err
|
||||
}, "holds no seat"},
|
||||
{"a package in two files", func(b *bed) error {
|
||||
p := pub("x", "0.1.0", file)
|
||||
p.Parts = append(p.Parts, parts.Want{Digest: digest([]byte("b")), Size: 1})
|
||||
_, err := b.h.Publish(NpmSeat, goBuild, p)
|
||||
return err
|
||||
}, "one file"},
|
||||
{"a part from another module than its opener", func(b *bed) error {
|
||||
if _, err := b.h.Publish(NpmSeat, nodeBuild, pub("x", "0.1.0", file)); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := b.h.PublishPart(NpmSeat, goBuild, "t-x", digest(file), 0, true, file)
|
||||
return err
|
||||
}, "opened"},
|
||||
{"a transfer of the other seat", func(b *bed) error {
|
||||
if _, err := b.h.Publish(NpmSeat, nodeBuild, pub("x", "0.1.0", file)); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := b.h.PublishPart(GoSeat, nodeBuild, "t-x", digest(file), 0, true, file)
|
||||
return err
|
||||
}, "not open"},
|
||||
{"remove by a build module", func(b *bed) error { _, err := b.h.Remove(GoSeat, goBuild, "x", "0.1.1-feat.1", "tag-gone"); return err }, "not on the holder's caller list"},
|
||||
{"remove of a version with no tag", func(b *bed) error {
|
||||
_, err := b.h.Remove(GoSeat, "novox.mesh-delivery-builder", "x", "0.1.1", "tag-gone")
|
||||
return err
|
||||
}, "has no tag"},
|
||||
} {
|
||||
b := newBed(true)
|
||||
if err := c.do(b); err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s: %v, want %q", c.name, err, c.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("a refused publish is said refused and raises the finding; nothing is uploaded", func(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
numbered bool
|
||||
data []byte
|
||||
want string
|
||||
}{{"the builder did not number it", false, file, "did not number"}} {
|
||||
b := newBed(c.numbered)
|
||||
if _, err := b.h.Publish(NpmSeat, nodeBuild, pub("x", "0.1.0", file)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = b.h.PublishPart(NpmSeat, nodeBuild, "t-x", digest(file), 0, true, c.data)
|
||||
if len(b.said) != 1 || b.said[0].event != EventRefused || !strings.Contains(b.said[0].o.Reason, c.want) {
|
||||
t.Errorf("%s: said %+v", c.name, b.said)
|
||||
}
|
||||
if !b.findings["publish-refused x@0.1.0"] || len(b.forge.files[NpmSeat]) != 0 {
|
||||
t.Errorf("%s: finding %v, uploaded %v", c.name, b.findings, b.forge.files)
|
||||
}
|
||||
}
|
||||
// A part whose bytes do not match its digest is refused to its sender, to send again; nothing is said yet.
|
||||
b := newBed(true)
|
||||
if _, err := b.h.Publish(NpmSeat, nodeBuild, pub("x", "0.1.0", file)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := b.h.PublishPart(NpmSeat, nodeBuild, "t-x", digest(file), 0, true, []byte("other bytes")); err == nil ||
|
||||
!strings.Contains(err.Error(), "send the part again") || len(b.said) != 0 || len(b.forge.files[NpmSeat]) != 0 {
|
||||
t.Errorf("a part with other bytes: %v, said %+v", err, b.said)
|
||||
}
|
||||
// The same version published with other content is refused, never uploaded over.
|
||||
b = newBed(true)
|
||||
b.forge.files[NpmSeat] = map[string][]byte{"x@0.1.0": []byte("first")}
|
||||
if _, err := b.h.Publish(NpmSeat, nodeBuild, pub("x", "0.1.0", file)); err != nil || len(b.said) != 1 || b.said[0].event != EventRefused {
|
||||
t.Errorf("a published version with other content: %v, said %+v", err, b.said)
|
||||
}
|
||||
})
|
||||
t.Run("the builder removes a tagged version; one already gone is said removed all the same", func(t *testing.T) {
|
||||
b := newBed(true)
|
||||
b.forge.files[GoSeat] = map[string][]byte{"x@0.1.1-feat.1": file}
|
||||
for range 2 {
|
||||
if _, err := b.h.Remove(GoSeat, "novox.mesh-delivery-builder", "x", "0.1.1-feat.1", "tag-gone"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(b.forge.removed) != 1 || len(b.said) != 2 || b.said[0].event != EventRemoved || b.said[0].o.Digest != digest(file) {
|
||||
t.Errorf("removed %v, said %+v", b.forge.removed, b.said)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user