diff --git a/go/catalogue/reader_release_test.go b/go/catalogue/reader_release_test.go index 43cdf35..93d0912 100644 --- a/go/catalogue/reader_release_test.go +++ b/go/catalogue/reader_release_test.go @@ -1,6 +1,9 @@ package catalogue -import "testing" +import ( + "strings" + "testing" +) // A manifest carrying the words design 47 G writes is read whole by the reader release (design 47 G §340, §409): // `version` as MAJOR.MINOR (§368), a `*` emitter in `consumes` (§386), an observer's finding stream (§387) and an @@ -21,3 +24,27 @@ func TestTheReaderReleaseReadsTheManifestWordsGWrites(t *testing.T) { t.Fatalf("read as %+v", m) } } + +// The second reader release reads the words G's manifests write beyond the first's (design 47 G §494, §621): each row a +// manifest read whole, or refused naming what is wrong, and what the reader gives of it. +func TestTheSecondReaderReleaseReadsGsManifestWords(t *testing.T) { + for _, c := range []struct { + name, raw, refused string + check func(Manifest) bool + }{ + {name: "a state's cap (§469)", raw: `{"module":"m","version":"1.0","state":["plain",{"name":"pipelines","max-mib":256}]}`, + check: func(m Manifest) bool { return m.State[0].MaxMiB == 0 && m.State[1].MaxMiB == 256 }}, + {name: "a state's cap out of the objects section's bounds (§469)", + raw: `{"module":"m","version":"1.0","state":[{"name":"pipelines","max-mib":0}]}`, refused: "max-mib is between 1 and 8192"}, + } { + m, err := decodeForTest([]byte(c.raw)) + switch { + case c.refused != "" && (err == nil || !strings.Contains(err.Error(), c.refused)): + t.Errorf("%s: read as %v, want refused naming %q", c.name, err, c.refused) + case c.refused == "" && err != nil: + t.Errorf("%s: refused: %v", c.name, err) + case c.refused == "" && (len(m.UnknownField()) != 0 || !c.check(m)): + t.Errorf("%s: read as %+v (set aside %v)", c.name, m, m.UnknownField()) + } + } +} diff --git a/go/catalogue/state.go b/go/catalogue/state.go index 2bca54e..fc6a289 100644 --- a/go/catalogue/state.go +++ b/go/catalogue/state.go @@ -35,11 +35,14 @@ type StateDeclaration struct { // the tool runner's account, read it and never write it (the lead's ruling, 2026-11-04: nats' warrant spend record // `spent`). The judge's rule and the grants follow from it. ReadOnly bool `json:"read-only,omitempty"` + // MaxMiB is the bucket's cap in MiB, between ObjectsLeastMiB and ObjectsMostMiB, as an old objects section's + // is (design 47 G §469, §494); zero is no cap, as before. + MaxMiB int `json:"max-mib,omitempty"` // KeptLayout is the layout its holder says it writes: its form and its catch-up tool (design 47). KeptLayout } -// UnmarshalJSON reads a bucket as its bare name, or as {name, history, ttl-seconds, per-machine, key-ttl, read-only, form, catch-up}. +// UnmarshalJSON reads a bucket as its bare name, or as {name, history, ttl-seconds, per-machine, key-ttl, read-only, max-mib, form, catch-up}. func (s *StateDeclaration) UnmarshalJSON(raw []byte) error { trimmed := bytes.TrimSpace(raw) if len(trimmed) > 0 && trimmed[0] == '"' { @@ -47,21 +50,28 @@ func (s *StateDeclaration) UnmarshalJSON(raw []byte) error { } type plain StateDeclaration var full plain + var said struct { + MaxMiB *int `json:"max-mib"` + } + _ = json.Unmarshal(trimmed, &said) dec := json.NewDecoder(bytes.NewReader(trimmed)) dec.DisallowUnknownFields() if err := dec.Decode(&full); err != nil { - return fmt.Errorf("a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl, read-only, form, catch-up}: %w", typedUnknown(err)) + return fmt.Errorf("a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl, read-only, max-mib, form, catch-up}: %w", typedUnknown(err)) } if err := full.KeptLayout.refuse(fmt.Sprintf("state %q", full.Name)); err != nil { return err } + if said.MaxMiB != nil && (*said.MaxMiB < ObjectsLeastMiB || *said.MaxMiB > ObjectsMostMiB) { + return fmt.Errorf("state %q: max-mib is between %d and %d, not %d", full.Name, ObjectsLeastMiB, ObjectsMostMiB, *said.MaxMiB) + } *s = StateDeclaration(full) return nil } // MarshalJSON writes back the short form when there is nothing else to say. func (s StateDeclaration) MarshalJSON() ([]byte, error) { - if s.History == 0 && s.TTLSeconds == 0 && !s.PerMachine && !s.KeyTTL && !s.ReadOnly && s.KeptLayout == (KeptLayout{}) { + if s.History == 0 && s.TTLSeconds == 0 && !s.PerMachine && !s.KeyTTL && !s.ReadOnly && s.MaxMiB == 0 && s.KeptLayout == (KeptLayout{}) { return json.Marshal(s.Name) } type plain StateDeclaration