diff --git a/contracts/go/seats/meshpipelinerunner/contract.go b/contracts/go/seats/meshpipelinerunner/contract.go index 9a5d4c11..39817255 100644 --- a/contracts/go/seats/meshpipelinerunner/contract.go +++ b/contracts/go/seats/meshpipelinerunner/contract.go @@ -418,8 +418,10 @@ type Pipeline struct { // the watched branch, or the branch of a run on request Branch string `json:"branch"` // the commit the module's entry was last changed at - Commit string `json:"commit"` - Ended *time.Time `json:"ended,omitempty"` + Commit string `json:"commit"` + // how often the pipeline has ended: 1 at its first end, one higher at each end again after a passing rerun, so the assigner takes a later end of the same source revision (design 47 G sections 29, 325) + EndCount int64 `json:"end-count,omitempty"` + Ended *time.Time `json:"ended,omitempty"` // the module's build-input fingerprint at the commit Fp string `json:"fp,omitempty"` // the gathered group, when the pipeline is its @@ -463,8 +465,10 @@ type PipelineEnded struct { // the watched branch, or the branch of a run on request Branch string `json:"branch"` // the commit the module's entry was last changed at - Commit string `json:"commit"` - Ended *time.Time `json:"ended,omitempty"` + Commit string `json:"commit"` + // how often the pipeline has ended: 1 at its first end, one higher at each end again after a passing rerun, so the assigner takes a later end of the same source revision (design 47 G sections 29, 325) + EndCount int64 `json:"end-count,omitempty"` + Ended *time.Time `json:"ended,omitempty"` // the module's build-input fingerprint at the commit Fp string `json:"fp,omitempty"` // the gathered group, when the pipeline is its @@ -630,6 +634,7 @@ const ( StepResultOutcomeWaitingForABuildNode = "waiting for a build node" StepResultOutcomeDidNotBuild = "did not build" StepResultOutcomeAccepted = "accepted" + StepResultOutcomeNotAsked = "not asked" StepResultOutcomeDeclined = "declined" StepResultOutcomeSuperseded = "superseded" StepResultOutcomeCancelled = "cancelled" diff --git a/contracts/go/seats/meshpipelinerunner/contract_test.go b/contracts/go/seats/meshpipelinerunner/contract_test.go index 44f3768e..ded5552b 100644 --- a/contracts/go/seats/meshpipelinerunner/contract_test.go +++ b/contracts/go/seats/meshpipelinerunner/contract_test.go @@ -19,9 +19,9 @@ func TestFixtures(t *testing.T) { }{ {name: "a pipeline started with the steps its policy gives", subject: "mesh.seat.mesh-pipeline-runner.event.pipeline-started.nats.main.0123456789abcdef0123456789abcdef01234567", given: "{\"at\":\"2026-11-10T10:00:00Z\",\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"status\":\"required\",\"step\":\"lint\"},{\"status\":\"bypass\",\"step\":\"review\"}],\"v\":1}", wire: "{\"at\":\"2026-11-10T10:00:00Z\",\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"status\":\"required\",\"step\":\"lint\"},{\"status\":\"bypass\",\"step\":\"review\"}],\"v\":1}", decode: func(b []byte) (any, error) { var v PipelineStartedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-pipeline-runner\\.event\\.pipeline-started\\.[^.*> ]+\\.[^.*> ]+\\.[^.*> ]+$")}, - {name: "a pipeline ended passed, an optional step failed, over budget and widening named", subject: "mesh.seat.mesh-pipeline-runner.event.pipeline-ended.nats.main.0123456789abcdef0123456789abcdef01234567", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"tests\":{\"integration\":3,\"unit\":42},\"v\":1,\"widens\":[{\"name\":\"mesh-build-go:program\",\"said\":\"its test runner\",\"what\":\"serving part\"}]}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"tests\":{\"integration\":3,\"unit\":42},\"v\":1,\"widens\":[{\"name\":\"mesh-build-go:program\",\"said\":\"its test runner\",\"what\":\"serving part\"}]}", + {name: "a pipeline ended passed, an optional step failed, over budget and widening named", subject: "mesh.seat.mesh-pipeline-runner.event.pipeline-ended.nats.main.0123456789abcdef0123456789abcdef01234567", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"end-count\":1,\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"tests\":{\"integration\":3,\"unit\":42},\"v\":1,\"widens\":[{\"name\":\"mesh-build-go:program\",\"said\":\"its test runner\",\"what\":\"serving part\"}]}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"end-count\":1,\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"tests\":{\"integration\":3,\"unit\":42},\"v\":1,\"widens\":[{\"name\":\"mesh-build-go:program\",\"said\":\"its test runner\",\"what\":\"serving part\"}]}", decode: func(b []byte) (any, error) { var v PipelineEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-pipeline-runner\\.event\\.pipeline-ended\\.[^.*> ]+\\.[^.*> ]+\\.[^.*> ]+$")}, - {name: "a pipeline ended failed, naming the step that held it", subject: "mesh.seat.mesh-pipeline-runner.event.pipeline-ended.nats.main.0123456789abcdef0123456789abcdef01234567", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"failed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"outcome\":\"failed\",\"status\":\"required\",\"step\":\"unit\"},{\"on\":\"the operator\",\"outcome\":\"waiting\",\"status\":\"required\",\"step\":\"approval\"}],\"v\":1,\"why\":\"unit of nats:program failed\"}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"failed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"outcome\":\"failed\",\"status\":\"required\",\"step\":\"unit\"},{\"on\":\"the operator\",\"outcome\":\"waiting\",\"status\":\"required\",\"step\":\"approval\"}],\"v\":1,\"why\":\"unit of nats:program failed\"}", + {name: "a pipeline ended failed, naming the step that held it, its approval not asked", subject: "mesh.seat.mesh-pipeline-runner.event.pipeline-ended.nats.main.0123456789abcdef0123456789abcdef01234567", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"end-count\":1,\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"failed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"outcome\":\"failed\",\"status\":\"required\",\"step\":\"unit\"},{\"outcome\":\"not asked\",\"status\":\"required\",\"step\":\"approval\",\"why\":\"unit of nats:program failed\"}],\"v\":1,\"why\":\"unit of nats:program failed\"}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"end-count\":1,\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"failed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"outcome\":\"failed\",\"status\":\"required\",\"step\":\"unit\"},{\"outcome\":\"not asked\",\"status\":\"required\",\"step\":\"approval\",\"why\":\"unit of nats:program failed\"}],\"v\":1,\"why\":\"unit of nats:program failed\"}", decode: func(b []byte) (any, error) { var v PipelineEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-pipeline-runner\\.event\\.pipeline-ended\\.[^.*> ]+\\.[^.*> ]+\\.[^.*> ]+$")}, {name: "a group's one pipeline ended on a watched branch", subject: "mesh.seat.mesh-pipeline-runner.event.group-ended.main.registry-1966", given: "{\"branch\":\"main\",\"ended\":\"2026-11-10T10:20:00Z\",\"group\":\"registry-1966\",\"outcome\":\"passed\",\"pipelines\":[{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"group\":\"registry-1966\",\"module\":\"nats\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"}],\"v\":1}],\"revision\":12,\"source\":3589,\"v\":1}", wire: "{\"branch\":\"main\",\"ended\":\"2026-11-10T10:20:00Z\",\"group\":\"registry-1966\",\"outcome\":\"passed\",\"pipelines\":[{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"group\":\"registry-1966\",\"module\":\"nats\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"}],\"v\":1}],\"revision\":12,\"source\":3589,\"v\":1}", decode: func(b []byte) (any, error) { var v GroupEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-pipeline-runner\\.event\\.group-ended\\.[^.*> ]+\\.[^.*> ]+$")}, @@ -37,7 +37,7 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v WaitHeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-pipeline-runner\\.event\\.wait-held\\.[^.*> ]+\\.[^.*> ]+\\.[^.*> ]+$")}, {name: "the wait ended answered", subject: "mesh.seat.mesh-pipeline-runner.event.wait-ended.nats.main.0123456789abcdef0123456789abcdef01234567", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"how\":\"answered\",\"module\":\"nats\",\"on\":\"person\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"since\":\"2026-11-10T10:10:00Z\",\"source\":3589,\"step\":\"approval\",\"v\":1,\"who\":\"the operator\"}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"how\":\"answered\",\"module\":\"nats\",\"on\":\"person\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"since\":\"2026-11-10T10:10:00Z\",\"source\":3589,\"step\":\"approval\",\"v\":1,\"who\":\"the operator\"}", decode: func(b []byte) (any, error) { var v WaitEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-pipeline-runner\\.event\\.wait-ended\\.[^.*> ]+\\.[^.*> ]+\\.[^.*> ]+$")}, - {name: "a pipeline kept", subject: "", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"v\":1}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"v\":1}", + {name: "a pipeline kept", subject: "", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"end-count\":1,\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"v\":1}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"end-count\":1,\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"v\":1}", decode: func(b []byte) (any, error) { var v PipelinesByBranchRevisionModuleValue err := json.Unmarshal(b, &v) diff --git a/contracts/seats/mesh-pipeline-runner/fixtures/events.json b/contracts/seats/mesh-pipeline-runner/fixtures/events.json index cdd9d6c9..3eb22465 100644 --- a/contracts/seats/mesh-pipeline-runner/fixtures/events.json +++ b/contracts/seats/mesh-pipeline-runner/fixtures/events.json @@ -36,6 +36,7 @@ "branch": "main", "revision": 12, "source": 3589, + "end-count": 1, "commit": "0123456789abcdef0123456789abcdef01234567", "fp": "fp-0123", "v": 1, @@ -109,7 +110,7 @@ } }, { - "name": "a pipeline ended failed, naming the step that held it", + "name": "a pipeline ended failed, naming the step that held it, its approval not asked", "of": "event:pipeline-ended", "subject": "mesh.seat.mesh-pipeline-runner.event.pipeline-ended.nats.main.0123456789abcdef0123456789abcdef01234567", "given": { @@ -118,6 +119,7 @@ "branch": "main", "revision": 12, "source": 3589, + "end-count": 1, "commit": "0123456789abcdef0123456789abcdef01234567", "fp": "fp-0123", "v": 1, @@ -134,8 +136,8 @@ { "step": "approval", "status": "required", - "outcome": "waiting", - "on": "the operator" + "outcome": "not asked", + "why": "unit of nats:program failed" } ], "over-budget": true diff --git a/contracts/seats/mesh-pipeline-runner/fixtures/states.json b/contracts/seats/mesh-pipeline-runner/fixtures/states.json index 318e10d5..f5873247 100644 --- a/contracts/seats/mesh-pipeline-runner/fixtures/states.json +++ b/contracts/seats/mesh-pipeline-runner/fixtures/states.json @@ -8,6 +8,7 @@ "branch": "main", "revision": 12, "source": 3589, + "end-count": 1, "commit": "0123456789abcdef0123456789abcdef01234567", "fp": "fp-0123", "v": 1, diff --git a/contracts/seats/mesh-pipeline-runner/seat.json b/contracts/seats/mesh-pipeline-runner/seat.json index 91df6fff..5d972333 100644 --- a/contracts/seats/mesh-pipeline-runner/seat.json +++ b/contracts/seats/mesh-pipeline-runner/seat.json @@ -678,7 +678,7 @@ { "name": "step-log", "template": "log", - "max-age-days": 90, + "max-age-days": 100, "description": "one entry per step's start and end, its output by name; kept on the runner's old streams section", "key-forms": [ { @@ -856,6 +856,7 @@ "waiting for a build node", "did not build", "accepted", + "not asked", "declined", "superseded", "cancelled" @@ -974,6 +975,10 @@ "type": "integer", "description": "the source revision of the watched branch; the pipeline is keyed by it (ADR 0666 section 9)" }, + "end-count": { + "type": "integer", + "description": "how often the pipeline has ended: 1 at its first end, one higher at each end again after a passing rerun, so the assigner takes a later end of the same source revision (design 47 G sections 29, 325)" + }, "commit": { "type": "string", "description": "the commit the module's entry was last changed at" @@ -1072,6 +1077,10 @@ "type": "integer", "description": "the source revision of the watched branch; the pipeline is keyed by it (ADR 0666 section 9)" }, + "end-count": { + "type": "integer", + "description": "how often the pipeline has ended: 1 at its first end, one higher at each end again after a passing rerun, so the assigner takes a later end of the same source revision (design 47 G sections 29, 325)" + }, "commit": { "type": "string", "description": "the commit the module's entry was last changed at" diff --git a/contracts/ts/seats/meshpipelinerunner/index.d.ts b/contracts/ts/seats/meshpipelinerunner/index.d.ts index e824d349..cf1d8263 100644 --- a/contracts/ts/seats/meshpipelinerunner/index.d.ts +++ b/contracts/ts/seats/meshpipelinerunner/index.d.ts @@ -182,6 +182,8 @@ export interface Pipeline { "branch": string; /** the commit the module's entry was last changed at */ "commit": string; + /** how often the pipeline has ended: 1 at its first end, one higher at each end again after a passing rerun, so the assigner takes a later end of the same source revision (design 47 G sections 29, 325) */ + "end-count"?: number; "ended"?: string; /** the module's build-input fingerprint at the commit */ "fp"?: string; @@ -217,6 +219,8 @@ export interface PipelineEnded { "branch": string; /** the commit the module's entry was last changed at */ "commit": string; + /** how often the pipeline has ended: 1 at its first end, one higher at each end again after a passing rerun, so the assigner takes a later end of the same source revision (design 47 G sections 29, 325) */ + "end-count"?: number; "ended"?: string; /** the module's build-input fingerprint at the commit */ "fp"?: string; @@ -326,7 +330,7 @@ export interface StepResult { /** for a waiting step, who or what it waits on: a person, a build node, the revision view */ "on"?: string; /** never passed for a step that ran nothing (design 47 *Pipeline steps and the policy*) */ - "outcome": "running" | "waiting" | "passed" | "failed" | "nothing to run" | "bypassed" | "carried" | "could not run" | "waiting for a build node" | "did not build" | "accepted" | "declined" | "superseded" | "cancelled"; + "outcome": "running" | "waiting" | "passed" | "failed" | "nothing to run" | "bypassed" | "carried" | "could not run" | "waiting for a build node" | "did not build" | "accepted" | "not asked" | "declined" | "superseded" | "cancelled"; /** the step's output, by its name in the runner's objects step-outputs */ "output"?: string; /** the step was asked again by the runner's rerun */ diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go index a1f9db6a..9c949810 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go @@ -197,11 +197,11 @@ const trunkHead = "3333333333333333333333333333333333333333" // trunk writes the gate's source.main naming head as novox/mesh-catalog's trunk head, and answers its revision. func (b *bed) trunk(t *testing.T, head string) uint64 { t.Helper() - data, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{V: keys.Version, Group: "g", Heads: map[string]string{"novox/mesh-catalog": head}}) + data, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByBranchValue{V: keys.Version, Group: "g", Heads: map[string]string{"novox/mesh-catalog": head}}) if err != nil { t.Fatal(err) } - rev, err := b.source.Put(b.ctx, meshdeliveryclient.SourceMainKey, data) + rev, err := b.source.Put(b.ctx, meshdeliveryclient.SourceKey("main"), data) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/optional.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/optional.go index 09324c26..87ba0c67 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/optional.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/optional.go @@ -6,8 +6,6 @@ import ( "sync" "github.com/nats-io/nats.go/jetstream" - - "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" ) // **A part does not stop for a bucket it can work without** (mesh-sdk process.EssentialBucket, OptionalBucket) (novox/hq issue 1945, met twice on 2026-10-25: the builder of @@ -43,7 +41,7 @@ func (t *trunkSource) ensure() error { if err != nil { return fmt.Errorf("the bucket %s cannot be read (%v)", sourceBucket, err) } - w, err := kv.Watch(t.ctx, meshdeliveryclient.SourceMainKey, jetstream.IncludeHistory()) + w, err := kv.WatchAll(t.ctx, jetstream.IncludeHistory()) // every watched branch's key: a revision names its head whichever branch it is of (design 47 G §15) if err != nil { return fmt.Errorf("the bucket %s cannot be watched (%v)", sourceBucket, err) } diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/integration_test.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/integration_test.go index ce7132fd..048f59d7 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/integration_test.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/integration_test.go @@ -206,7 +206,7 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15 m1 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: h1, Merge: "1111111111111111111111111111111111111111", Root: "r0", Trees: map[string]string{"modules/" + ix: "tree-indexer-2", "modules/a": "tree-a-2"}, - Manifests: map[string]string{"modules/" + ix: string(manifest), "modules/a": `{"module":"a","version":"1"}`}, + Manifests: map[string]string{"modules/" + ix: string(manifest), "modules/a": `{"module":"a","version":"1.0"}`}, Locks: map[string]map[string]string{"modules/" + ix: {"go.sum": "3b5028f0c1e9bde04dd766abd63eb5931bc516bae695cf46f00abcd8510b0381"}}} merged := func(f forgemsg.Forge) uint64 { t.Helper() @@ -305,10 +305,10 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15 {Repository: "novox/mesh-controller", Number: 41, Head: "4141414141414141414141414141414141414141"}} g1 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/g", Number: 40, Head: members[0].Head, Merge: "4000000000000000000000000000000000000000", Root: "r0", Trees: map[string]string{"modules/c": "tree-c-2"}, - Manifests: map[string]string{"modules/c": `{"module":"c","version":"1"}`}, Group: "feat/g", Members: members} + Manifests: map[string]string{"modules/c": `{"module":"c","version":"1.0"}`}, Group: "feat/g", Members: members} g2 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-controller", Branch: "feat/g", Number: 41, Head: members[1].Head, Merge: "4100000000000000000000000000000000000000", Root: "r0", Trees: map[string]string{"modules/k": "tree-k-1"}, - Manifests: map[string]string{"modules/k": `{"module":"k","version":"1"}`}, Group: "feat/g", Members: members} + Manifests: map[string]string{"modules/k": `{"module":"k","version":"1.0"}`}, Group: "feat/g", Members: members} g1Key, groupKey := takein.PendingKey(merged(g1)), takein.GroupTakeInKey("feat/g") if gathered, ok := said(takein.TakeInPending, groupKey); !ok || gathered.What != takein.TakeInGroup || len(gathered.Merges) != 1 { t.Fatalf("take-in.pending.%s = %+v (said %v) within 20 s, want the group with its first member", groupKey, gathered, ok) @@ -618,7 +618,7 @@ func TestIndexerWritesModulesAtAMerge(t *testing.T) { // 1239/AC9; I1, I3: the m const merge = "89abcdef0123456789abcdef0123456789abcdef" data, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567", Merge: merge, Root: "r0", Trees: map[string]string{"modules/a": "newtree"}, - Manifests: map[string]string{"modules/a": `{"module":"a","version":"1"}`}}) + Manifests: map[string]string{"modules/a": `{"module":"a","version":"1.0"}`}}) if err != nil { t.Fatal(err) } @@ -655,9 +655,9 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15 } const goSum = "3b5028f0c1e9bde04dd766abd63eb5931bc516bae695cf46f00abcd8510b0381" judged := map[string]string{ - "good": `{"module":"good","version":"1","resources":[{"id":"d","type":"directory","path":"/srv/good","mode":"0700"}],"data":{"own":[{"id":"d","path":"${dir:d}","class":"valuable"}]}}`, - "bad": `{"module":"bad","version":"1","provides":[{"name":"amqp"}],"resources":[{"id":"d","type":"directory","path":"/srv/bad","mode":"0700"}],"data":{"own":[{"id":"d","path":"${dir:d}","class":"valuable"}]}}`, - "later": `{"module":"later","version":"1","a-field-from-later":1,"resources":[{"id":"d","type":"directory","path":"/srv/later","mode":"0700"}],"data":{"own":[{"id":"d","path":"${dir:d}","class":"valuable"}]}}`, + "good": `{"module":"good","version":"1.0","resources":[{"id":"d","type":"directory","path":"/srv/good","mode":"0700"}],"data":{"own":[{"id":"d","path":"${dir:d}","class":"valuable"}]}}`, + "bad": `{"module":"bad","version":"1.0","provides":[{"name":"amqp"}],"resources":[{"id":"d","type":"directory","path":"/srv/bad","mode":"0700"}],"data":{"own":[{"id":"d","path":"${dir:d}","class":"valuable"}]}}`, + "later": `{"module":"later","version":"1.0","a-field-from-later":1,"resources":[{"id":"d","type":"directory","path":"/srv/later","mode":"0700"}],"data":{"own":[{"id":"d","path":"${dir:d}","class":"valuable"}]}}`, } js, ctx, _, modules := startIndexer(t, func(ctx context.Context, builds buildLog) { builds.end(t, "novox/mesh-catalog", "avahi", "modules/avahi") diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go index 912af36f..40e3abd3 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go @@ -226,7 +226,7 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T) if got, err := index.SeatDefinitionsOf(jsSource{ctx, seatDefs}, nil, ""); err != nil || func() string { raw, _ := json.Marshal(got); return string(raw) }() != `{"revision":0,"seats":[]}` { t.Errorf("seat-definitions of an indexer that kept none = %+v, %v", got, err) } - contracts := `{"module":"mesh-contracts","version":"1","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}` + contracts := `{"module":"mesh-contracts","version":"1.0","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}` merged("novox/mesh-contracts", 80, contractsMerge, forgemsg.Forge{Root: "dddddddddddddddddddddddddddddddddddddddd", Trees: map[string]string{"": "dddddddddddddddddddddddddddddddddddddddd"}, Manifests: map[string]string{"": contracts}}) entry := func(key string, ok func(index.SeatDefinitionEntry) bool) index.SeatDefinitionEntry { diff --git a/modules/mesh-delivery-indexer/internal/index/control_records_test.go b/modules/mesh-delivery-indexer/internal/index/control_records_test.go index f625affa..66f7948c 100644 --- a/modules/mesh-delivery-indexer/internal/index/control_records_test.go +++ b/modules/mesh-delivery-indexer/internal/index/control_records_test.go @@ -76,8 +76,8 @@ func TestTheControlRecords(t *testing.T) { } modules[name] = &views.Entry{Value: raw, Revision: 1} } - asks := `{"module":"cache","version":"1","requires":[{"name":"c","storage":"key-value","template":"state"}]}` - for digest, raw := range map[string]string{dAsk: asks, dNone: `{"module":"plain","version":"1"}`, dRefused: asks} { + asks := `{"module":"cache","version":"1.0","requires":[{"name":"c","storage":"key-value","template":"state"}]}` + for digest, raw := range map[string]string{dAsk: asks, dNone: `{"module":"plain","version":"1.0"}`, dRefused: asks} { put(manifests, digest, 1, meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: "m", Manifest: raw}) } putVerdict(dAsk, 7, meshcatalog.VerdictsByDigestValue{Module: "cache", State: meshcatalogclient.VerdictAccepted, Judge: "j"}) @@ -127,7 +127,7 @@ func TestTheControlRecords(t *testing.T) { // raises generated-types-out-of-step from a trunk build end, cleared only by a clean one of the same module. t.Run("seat definitions", func(t *testing.T) { const repo, m1, m2, m3, head = "novox/mesh-contracts", "m1", "m2", "m3", "h1" - manifest := `{"module":"mesh-contracts","version":"1","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}` + manifest := `{"module":"mesh-contracts","version":"1.0","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}` git := `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"novox/hq ADR 0111","domain":"Core","delivers":"git"}` files := map[string]map[string]string{ m1: {"seats/git/seat.json": git, "seats/gone/seat.json": strings.Replace(git, `"git"`, `"gone"`, 1)}, @@ -162,7 +162,7 @@ func TestTheControlRecords(t *testing.T) { merged := func(merge string) { t.Helper() if err := ix.ApplySeatDefinitions(ctx, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repo, Number: 1, Merge: merge, - Manifests: map[string]string{meshcatalogclient.RootPath: manifest, "modules/other": `{"module":"other","version":"1"}`}}); err != nil { + Manifests: map[string]string{meshcatalogclient.RootPath: manifest, "modules/other": `{"module":"other","version":"1.0"}`}}); err != nil { t.Fatal(err) } } @@ -284,7 +284,7 @@ func TestTheControlRecords(t *testing.T) { // Ruling (lead, on A2i's finding 3): indexer_seat_definitions_again for a pull request reads the head's own manifest's // generated-from: a trunk that names none, a head that names it, is read as the contracts module the head is. ix.Modules, ix.Manifests = newMem(), newMem() - plain := `{"module":"mesh-contracts","version":"1"}` + plain := `{"module":"mesh-contracts","version":"1.0"}` kept, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "mesh-contracts", Manifest: plain}) mkey, _ := meshcatalogclient.ManifestsKey(meshcatalogclient.ManifestDigest([]byte(plain))) ix.Manifests.(*memStore).vals[mkey] = kept @@ -425,7 +425,7 @@ func TestTheControlRecords(t *testing.T) { } return nil, nil }} - sdk := `{"module":"mesh-sdk","version":"1","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"]},` + + sdk := `{"module":"mesh-sdk","version":"1.0","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"]},` + `{"name":"ts","kind":"package","language":"typescript","paths":["src","package.json"],"series":"0.3"}]}}` if err := ix.KeepPackageNames(ctx, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-sdk", Merge: "m1", Manifests: map[string]string{meshcatalogclient.RootPath: sdk}}); err != nil { @@ -498,11 +498,11 @@ func TestTheControlRecords(t *testing.T) { pkgs, imports []string pinned string }{ - "new": {"novox/mesh-catalog", `{"module":"new","version":"1","packages":{"git.novox.be/novox/mesh-sdk/go":"0.2","@novox/ui":"1.0"}}`, nil, nil, ""}, - "old": {"novox/mesh-catalog", `{"module":"old","version":"1"}`, []string{sdkGo}, nil, "v0.1.15"}, - "imports": {"novox/mesh-tools", `{"module":"imports","version":"1"}`, nil, []string{sdkGo + "/views"}, ""}, - "sibling": {"novox/mesh-sdk", `{"module":"sibling","version":"1"}`, []string{sdkGo}, nil, ""}, - "none": {"novox/mesh-catalog", `{"module":"none","version":"1"}`, nil, nil, ""}, + "new": {"novox/mesh-catalog", `{"module":"new","version":"1.0","packages":{"git.novox.be/novox/mesh-sdk/go":"0.2","@novox/ui":"1.0"}}`, nil, nil, ""}, + "old": {"novox/mesh-catalog", `{"module":"old","version":"1.0"}`, []string{sdkGo}, nil, "v0.1.15"}, + "imports": {"novox/mesh-tools", `{"module":"imports","version":"1.0"}`, nil, []string{sdkGo + "/views"}, ""}, + "sibling": {"novox/mesh-sdk", `{"module":"sibling","version":"1.0"}`, []string{sdkGo}, nil, ""}, + "none": {"novox/mesh-catalog", `{"module":"none","version":"1.0"}`, nil, nil, ""}, } { digest := fmt.Sprintf("%064x", len(name)*16+int(name[0])) putJSON(manifests, digest, meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: name, Manifest: u.manifest}) @@ -569,7 +569,7 @@ func TestTheControlRecords(t *testing.T) { ix, _, _ := manifestsIndexer(t, rec(t, "build-1", "avahi", catalogueRepo, "modules/avahi", "main", "", "2026-10-19T09:00:00Z", aInputs)) cands := newMem() ix.ConfigCandidates, ix.Now = cands, func() time.Time { return at } - trunk := `{"module":"avahi","version":"1","config":{"port":{"type":"integer","default":5353,"why":"mDNS"}}}` + trunk := `{"module":"avahi","version":"1.0","config":{"port":{"type":"integer","default":5353,"why":"mDNS"}}}` if err := ix.Apply(ctx, withManifests(mergedMsg(catalogueRepo, map[string]string{"modules/avahi": "t1"}), map[string]string{"modules/avahi": trunk}, nil)); err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-indexer/internal/index/holds_test.go b/modules/mesh-delivery-indexer/internal/index/holds_test.go index 5466786f..eb30b90b 100644 --- a/modules/mesh-delivery-indexer/internal/index/holds_test.go +++ b/modules/mesh-delivery-indexer/internal/index/holds_test.go @@ -34,10 +34,10 @@ func (b fakeBounds) Disarm(_ context.Context, key string) error { delete(b, key) func storeHoldersNewManifestWaitsForEveryCountedEngine(t *testing.T) { ctx := context.Background() const dir, dockerDir = "modules/distribution", "modules/docker" - old := `{"module":"distribution","version":"1","listens":[{"name":"registry","port":5000,"from":"mesh"}]}` - next := `{"module":"distribution","version":"1","claims":[{"name":"container-image-registry","scope":"mesh"}],` + + old := `{"module":"distribution","version":"1.0","listens":[{"name":"registry","port":5000,"from":"mesh"}]}` + next := `{"module":"distribution","version":"1.0","claims":[{"name":"container-image-registry","scope":"mesh"}],` + `"listens":[{"name":"registry","port":5000,"from":"mesh"}]}` - docker := `{"module":"docker","version":"1","resources":[{"id":"daemon","type":"file","path":"/etc/docker/daemon.json","template":"files/daemon.json"}]}` + docker := `{"module":"docker","version":"1.0","resources":[{"id":"daemon","type":"file","path":"/etc/docker/daemon.json","template":"files/daemon.json"}]}` dockerNeeds, _ := catalogue.ModuleNeeds([]byte(docker), map[string]string{"files/daemon.json": `"${seat:container-image-registry:reach}"`}) if needs, _ := catalogue.ManifestNeeds([]byte(next)); !slices.Contains(needs, "form:seat-reach") || !slices.Contains(dockerNeeds, "form:seat-reach") { t.Fatalf("the new manifest needs %v and docker's %v, want form:seat-reach in both (mesh-sdk ecdbf1a, ModuleNeeds)", needs, dockerNeeds) diff --git a/modules/mesh-delivery-indexer/internal/index/index_test.go b/modules/mesh-delivery-indexer/internal/index/index_test.go index 0a20d6f8..92fbeebb 100644 --- a/modules/mesh-delivery-indexer/internal/index/index_test.go +++ b/modules/mesh-delivery-indexer/internal/index/index_test.go @@ -185,7 +185,7 @@ func TestModuleOfADirectory(t *testing.T) { // 1238/AC3, as R4e has it: the modu ctx := context.Background() // The trunk holds modules/odd as the module odd-name (its manifest names it so); modules/plain is held by none. merged := mergedMsg("novox/mesh-delivery", map[string]string{"modules/odd": "t"}) - merged.Manifests["modules/odd"] = `{"module":"odd-name","version":"1"}` + merged.Manifests["modules/odd"] = `{"module":"odd-name","version":"1.0"}` if err := ix.Apply(ctx, merged); err != nil { t.Fatal(err) } @@ -202,7 +202,7 @@ func TestModuleOfADirectory(t *testing.T) { // 1238/AC3, as R4e has it: the modu } // A manifest the pull message carries names its module, whatever directory it is in. f := pull("novox/mesh-delivery", 13, "feat/y", h2, "modules/odd") - f.Manifests = map[string]string{"modules/odd": `{"module":"renamed","version":"1"}`} + f.Manifests = map[string]string{"modules/odd": `{"module":"renamed","version":"1.0"}`} if name := moduleOfDir(held, f, "modules/odd"); name != "renamed" { t.Errorf("modules/odd = %q, want the module its manifest names", name) } diff --git a/modules/mesh-delivery-indexer/internal/index/inputs_test.go b/modules/mesh-delivery-indexer/internal/index/inputs_test.go index 77adda62..74763916 100644 --- a/modules/mesh-delivery-indexer/internal/index/inputs_test.go +++ b/modules/mesh-delivery-indexer/internal/index/inputs_test.go @@ -144,7 +144,7 @@ func TestAnArtifactOfNoToolchainHasNoLanguageAndNoLock(t *testing.T) { // 1450/A func TestAManifestTheParserRefusesLeavesNoInputsAndSaysWhy(t *testing.T) { // 1450/AC5 for name, raw := range map[string]string{ "not json": "not json", - "unknown field": strings.Replace(catalogueManifest(t, "avahi"), `"version": "1",`, `"version": "1", "scope": "everything",`, 1), + "unknown field": strings.Replace(catalogueManifest(t, "avahi"), `"version":"1.0",`, `"version":"1.0", "scope": "everything",`, 1), } { in := inputsOf("avahi", raw, "t", nil, noBases) if in.Kept != nil || !strings.HasPrefix(in.None2, "the manifest cannot be read: ") || len(in.None2) <= len("the manifest cannot be read: ") { diff --git a/modules/mesh-delivery-indexer/internal/index/modules_test.go b/modules/mesh-delivery-indexer/internal/index/modules_test.go index 1381e39b..1d3b78fe 100644 --- a/modules/mesh-delivery-indexer/internal/index/modules_test.go +++ b/modules/mesh-delivery-indexer/internal/index/modules_test.go @@ -82,7 +82,7 @@ func rec(t *testing.T, id, module, repo, path, ref, state, at, inputs string) [2 func mergedMsg(repo string, trees map[string]string) forgemsg.Forge { manifests := map[string]string{} for dir := range trees { - manifests[dir] = `{"module":"` + nameOfDir(dir) + `","version":"1"}` + manifests[dir] = `{"module":"` + nameOfDir(dir) + `","version":"1.0"}` } return forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: repo, Branch: "feat/x", Number: 12, Head: h1, Merge: merge, Root: "r0", Trees: trees, Manifests: manifests} } @@ -515,7 +515,7 @@ func TestAModuleMovesBetweenRepositoriesOnlyWhenItsTrunkDropsIt(t *testing.T) { if repository != to || sha != toHead || dir != "modules/a" { t.Errorf("read %s at %s %q, want only %s at %s modules/a", repository, sha, dir, to, toHead) } - return RepoRead{Root: "r2", Trees: map[string]string{dir: "t2"}, Manifests: map[string]string{dir: `{"module":"a","version":"1"}`}}, nil + return RepoRead{Root: "r2", Trees: map[string]string{dir: "t2"}, Manifests: map[string]string{dir: `{"module":"a","version":"1.0"}`}}, nil } } for _, f := range tc.merges { diff --git a/modules/mesh-delivery-indexer/internal/index/named_test.go b/modules/mesh-delivery-indexer/internal/index/named_test.go index d492a5af..72623093 100644 --- a/modules/mesh-delivery-indexer/internal/index/named_test.go +++ b/modules/mesh-delivery-indexer/internal/index/named_test.go @@ -106,7 +106,7 @@ func TestTheIndexerAsksTheForgeForTheNamedPathsObjectsOfAMerge(t *testing.T) { / return map[string]map[string]string{"": sdkObjects}, nil } raw := fixture(t, "sdk-9cb4d59-mesh-sdk") - f := withManifests(mergedMsg("novox/mesh-sdk", map[string]string{"": "roottree", "modules/a": "t"}), map[string]string{"": raw, "modules/a": `{"module":"a","version":"1"}`}, nil) + f := withManifests(mergedMsg("novox/mesh-sdk", map[string]string{"": "roottree", "modules/a": "t"}), map[string]string{"": raw, "modules/a": `{"module":"a","version":"1.0"}`}, nil) if err := ix.Apply(context.Background(), f); err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-indexer/internal/index/pending_test.go b/modules/mesh-delivery-indexer/internal/index/pending_test.go index 27bff288..0d62d0c5 100644 --- a/modules/mesh-delivery-indexer/internal/index/pending_test.go +++ b/modules/mesh-delivery-indexer/internal/index/pending_test.go @@ -365,7 +365,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633) f.Locks[d] = l } f.Locks[contracts] = map[string]string{} - f.Manifests[contracts] = `{"module":"` + row.module + `","version":"1","build":{"artifacts":[{"name":"go-seats","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}` + f.Manifests[contracts] = `{"module":"` + row.module + `","version":"1.0","build":{"artifacts":[{"name":"go-seats","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}` b.ix.Files = func(_ context.Context, repository, commit string, dirs []string) (map[string]map[string]string, error) { if repository != f.Repository || commit != f.Merge || !slices.Equal(dirs, []string{contracts + "/seats"}) { t.Errorf("listed %v of %s at %s", dirs, repository, commit) diff --git a/modules/mesh-delivery-indexer/internal/index/registered_test.go b/modules/mesh-delivery-indexer/internal/index/registered_test.go index 4e1cd9ee..6492329e 100644 --- a/modules/mesh-delivery-indexer/internal/index/registered_test.go +++ b/modules/mesh-delivery-indexer/internal/index/registered_test.go @@ -15,7 +15,7 @@ import ( func TestARegisteredModuleIsIndexedFromItsRepository(t *testing.T) { // the grants part's gaps: modules registered from their own repositories had no manifest here const forge = "http://novox.internal:20000/jschoubben/snake.git" - snake := `{"module":"snake","version":"1"}` + snake := `{"module":"snake","version":"1.0"}` for _, tc := range []struct { name string path string @@ -41,7 +41,7 @@ func TestARegisteredModuleIsIndexedFromItsRepository(t *testing.T) { // the gran {wantRead: true, name: "no manifest at the path", path: "modules/x", read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": snake}}}, {wantRead: true, name: "a manifest of another module", path: "", - read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": `{"module":"other","version":"1"}`}}}, + read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": `{"module":"other","version":"1.0"}`}}}, {wantRead: true, name: "a repository whose default branch is master", path: "", ref: "master", want: true, read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": snake}}}, {wantRead: true, name: "a repository built from a branch of its own (nox-mesh)", path: "", ref: "nox-mesh", want: true, @@ -151,7 +151,7 @@ func TestARegisteredModuleIsIndexedFromItsRepository(t *testing.T) { // the gran var asked []string ix.Read = func(_ context.Context, _, sha, dir string) (RepoRead, error) { asked = append(asked, sha+":"+dir) - return RepoRead{Root: "r1", Trees: map[string]string{"modules/a": "treeA2"}, Manifests: map[string]string{"modules/a": `{"module":"a","version":"2"}`}}, nil + return RepoRead{Root: "r1", Trees: map[string]string{"modules/a": "treeA2"}, Manifests: map[string]string{"modules/a": `{"module":"a","version":"2.0"}`}}, nil } got := ix.TakeIn(ctx, "a") m, ok := moduleOf(t, mods, "a") @@ -202,7 +202,7 @@ func TestARegistrationOfAnotherRepositoryDoesNotHideTheRealOne(t *testing.T) { / ix.Read = func(_ context.Context, repo, _, dir string) (RepoRead, error) { asked = append(asked, repo+":"+dir) if repo == "novox/Link2Pay" { - return RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": `{"module":"link2pay","version":"1"}`}}, nil + return RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": `{"module":"link2pay","version":"1.0"}`}}, nil } return RepoRead{Root: "r0"}, nil } @@ -221,7 +221,7 @@ func TestARegistrationOfAnotherRepositoryDoesNotHideTheRealOne(t *testing.T) { / func TestAForgeThatCannotSayLeavesTheModuleUnwrittenAndAsksAgain(t *testing.T) { // a timeout or a 404 at the head: no read at an older commit // Issue 1945: car-hunter and snake answered 404 until the forge's account could read them, and were read again only when // the indexer started again. Each row's trigger must now take the module in once the forge reads it. - const snake = `{"module":"snake","version":"1"}` + const snake = `{"module":"snake","version":"1.0"}` ctx := context.Background() for _, tc := range []struct{ failure, trigger string }{ {"timeout", "the indexer starts again"}, diff --git a/modules/mesh-delivery-indexer/internal/index/stale_none2_test.go b/modules/mesh-delivery-indexer/internal/index/stale_none2_test.go index e92d94b7..6ec511d1 100644 --- a/modules/mesh-delivery-indexer/internal/index/stale_none2_test.go +++ b/modules/mesh-delivery-indexer/internal/index/stale_none2_test.go @@ -10,7 +10,7 @@ import ( // not read. func unreadableAvahi(t *testing.T) string { t.Helper() - raw := strings.Replace(catalogueManifest(t, "avahi"), `"version": "1",`, `"version": "1", "scope": "everything",`, 1) + raw := strings.Replace(catalogueManifest(t, "avahi"), `"version":"1.0",`, `"version":"1.0", "scope": "everything",`, 1) if raw == catalogueManifest(t, "avahi") { t.Fatal("the fixture has no version line") } diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index 762649d9..16eb83ce 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -344,7 +344,7 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { // // verb edges of two modules calling each other, from either side; a third calling one of them keeps its edge. invoking := func(name, invokes string) mod { raw := renamed(t, "avahi", name) - return mod{name, strings.Replace(raw, `"version": "1",`, `"version": "1", "invokes": [`+invokes+`],`, 1)} + return mod{name, strings.Replace(raw, `"version":"1.0",`, `"version":"1.0", "invokes": [`+invokes+`],`, 1)} } vx, _ := newFormIndexer(t, invoking("caller", `"callee.avahi_status"`), invoking("callee", `"caller.avahi_browse", "*"`), invoking("third", `"callee.avahi_status"`)) @@ -505,7 +505,7 @@ func TestAModuleKeepsTheToolchainsItsFingerprintWasComputedWith(t *testing.T) { sdkMerge := func(commit, tree string) meshcatalog.ModulesByModuleValue { t.Helper() f := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-sdk", Branch: "feat/x", Number: 3, Head: h1, - Merge: commit, Root: tree, Trees: map[string]string{"": tree}, Manifests: map[string]string{"": `{"module":"mesh-sdk","version":"1"}`}} + Merge: commit, Root: tree, Trees: map[string]string{"": tree}, Manifests: map[string]string{"": `{"module":"mesh-sdk","version":"1.0"}`}} if err := ix.Apply(context.Background(), f); err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go index 09d6cf52..9650ee12 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go @@ -40,8 +40,8 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { ix.Modules.(*memStore).put(module, value) return digest } - good := hold("good", `{"module":"good","version":"1"}`) - bad := hold("bad", `{"module":"bad","version":"1","requires":["amqp"]}`) + good := hold("good", `{"module":"good","version":"1.0"}`) + bad := hold("bad", `{"module":"bad","version":"1.0","requires":["amqp"]}`) // Both taken in by a merge: judged in full. j, err := ix.JudgeAll(ctx, map[string]bool{good: true, bad: true}) if err != nil || j.Judged != 2 || j.Written != 2 || len(j.Refused) != 1 { @@ -91,7 +91,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { t.Fatalf("the new indexer at its start: %+v %v, said %d, want both manifests judged and nothing written", j, err, len(said)) } // The manifest mended: its new digest is accepted and the module's refusal goes. - mended := hold("bad", `{"module":"bad","version":"2"}`) + mended := hold("bad", `{"module":"bad","version":"2.0"}`) if j, err := ix.JudgeAll(ctx, map[string]bool{mended: true}); err != nil || j.Written != 1 || len(j.Refused) != 0 { t.Fatalf("mended: %+v %v", j, err) } @@ -101,7 +101,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { // ADR 0579 §1: a manifest held before any verdict (taken in under the judges until then), which this judge finds a // problem in, stays accepted with the problem as a warning, carried in its value and said nowhere as a refusal; the // same problem in a manifest a merge takes in now, or in one whose digest changed, is refused in full. - old := hold("old", `{"module":"old","version":"1","requires":["amqp"]}`) + old := hold("old", `{"module":"old","version":"1.0","requires":["amqp"]}`) if j, err := ix.JudgeAll(ctx, nil); err != nil || len(j.Outgrown) != 1 || j.Outgrown[0] != "old" { t.Fatalf("an outgrown manifest: %+v %v", j, err) } @@ -111,8 +111,8 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { if m := module("old"); m.Verdict != "" || len(m.Warnings) != 1 || len(said) != 1 { t.Fatalf("modules.old carries %+v, said %d", m, len(said)) } - changed := hold("old", `{"module":"old","version":"2","requires":["amqp"]}`) - fresh := hold("new", `{"module":"new","version":"1","requires":["amqp"]}`) + changed := hold("old", `{"module":"old","version":"2.0","requires":["amqp"]}`) + fresh := hold("new", `{"module":"new","version":"1.0","requires":["amqp"]}`) if j, err := ix.JudgeAll(ctx, map[string]bool{changed: true, fresh: true}); err != nil || len(j.Outgrown) != 0 { t.Fatalf("a changed and a new manifest: %+v %v", j, err) } @@ -142,7 +142,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { // manifest a judge refuses in full, its judge pass fails on the verdicts state, and the next pass, which knows nothing // of that take-in (a start, a pass kicked by a message), still refuses it in full, never as a warning, and ends the key. ix.Unjudged = newMem() - keptDigest, err := ix.keepManifest(ctx, "kept", `{"module":"kept","version":"1","requires":["amqp"]}`) + keptDigest, err := ix.keepManifest(ctx, "kept", `{"module":"kept","version":"1.0","requires":["amqp"]}`) if err != nil { t.Fatal(err) } @@ -182,7 +182,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { accepted, ids = append(accepted, a), append(ids, id) return nil } - moved := hold("moved", `{"module":"moved","version":"1","requires":[`+ + moved := hold("moved", `{"module":"moved","version":"1.0","requires":[`+ `{"name":"cache","storage":"key-value","template":"state","options":{"history":8}},`+ `{"name":"audit","storage":"stream","template":"log","options":{"max-mib":64}}]}`) for pass := 0; pass < 2; pass++ { @@ -208,7 +208,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { t.Fatalf("said accepted %+v", accepted) } // DLV-I9 (design 25 § 12.2): a manifest asking for none, newly accepted, is said too, its entries an empty list. - plain := hold("plain", `{"module":"plain","version":"1"}`) + plain := hold("plain", `{"module":"plain","version":"1.0"}`) if _, err := ix.JudgeAll(ctx, map[string]bool{plain: true}); err != nil { t.Fatal(err) } @@ -217,8 +217,8 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { } // Review round 1 of issue 1966: a trunk's held manifest judged by its digest (indexer_judge, judgeStale) is said as the // full pass says it, accepted or refused; a candidate's digest judged over the trunk is said nowhere. - byDigest := hold("bydigest", `{"module":"bydigest","version":"1"}`) - refusedBy := hold("refusedby", `{"module":"refusedby","version":"1","requires":["amqp"]}`) + byDigest := hold("bydigest", `{"module":"bydigest","version":"1.0"}`) + refusedBy := hold("refusedby", `{"module":"refusedby","version":"1.0","requires":["amqp"]}`) saidBefore := len(said) for module, digest := range map[string]string{"bydigest": byDigest, "refusedby": refusedBy} { if _, err := ix.judgeOver(ctx, nil, module, digest); err != nil { @@ -228,7 +228,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { if last := accepted[len(accepted)-1]; last.Digest != byDigest || len(said) != saidBefore+1 || said[len(said)-1].Digest != refusedBy { t.Fatalf("judged by its digest, the trunk's manifest was said as %+v, refusals %d (were %d)", last, len(said), saidBefore) } - candidate, err := ix.keepManifest(ctx, "bydigest", `{"module":"bydigest","version":"2"}`) + candidate, err := ix.keepManifest(ctx, "bydigest", `{"module":"bydigest","version":"2.0"}`) if err != nil { t.Fatal(err) } @@ -239,12 +239,12 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { // judging pass says again and clears. ix.Findings = newMem() failing = 1 - once := hold("once", `{"module":"once","version":"1"}`) + once := hold("once", `{"module":"once","version":"1.0"}`) if _, err := ix.JudgeAll(ctx, map[string]bool{once: true}); err != nil || accepted[len(accepted)-1].Module != "once" { t.Fatalf("a publish failing once was not published again: %v %+v", err, accepted[len(accepted)-1]) } failing = 2 - flaky := hold("flaky", `{"module":"flaky","version":"1"}`) + flaky := hold("flaky", `{"module":"flaky","version":"1.0"}`) if _, err := ix.JudgeAll(ctx, map[string]bool{flaky: true}); err != nil { t.Fatal(err) } @@ -277,13 +277,13 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { } // DLV-I15: a change's renamed-from is judged against the trunk's manifest of the module: a name the trunk declares is // accepted, one it does not is refused naming the key. - trunkRenamer := hold("renamer", `{"module":"renamer","version":"1","config":{"font-name":{"type":"string","description":"The font.","required":true,"runtime":true}}}`) + trunkRenamer := hold("renamer", `{"module":"renamer","version":"1.0","config":{"font-name":{"type":"string","description":"The font.","required":true,"runtime":true}}}`) if _, err := ix.JudgeAll(ctx, map[string]bool{trunkRenamer: true}); err != nil { t.Fatal(err) } for raw, want := range map[string]string{ - `{"module":"renamer","version":"1","config":{"font":{"type":"string","description":"The font.","required":true,"runtime":true,"renamed-from":"font-name"}}}`: "", - `{"module":"renamer","version":"1","config":{"font":{"type":"string","description":"The font.","required":true,"runtime":true,"renamed-from":"typeface"}}}`: `renamed from "typeface"`, + `{"module":"renamer","version":"1.0","config":{"font":{"type":"string","description":"The font.","required":true,"runtime":true,"renamed-from":"font-name"}}}`: "", + `{"module":"renamer","version":"1.0","config":{"font":{"type":"string","description":"The font.","required":true,"runtime":true,"renamed-from":"typeface"}}}`: `renamed from "typeface"`, } { head, err := ix.keepManifest(ctx, "renamer", raw) if err != nil { @@ -305,8 +305,8 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { ix.Packages.(*memStore).put(SeriesKey(sdkGo, "0.1"), value) } for raw, refusedAtMerge := range map[string]bool{ - `{"module":"pkguser","version":"1","packages":{"@novox/ui":"0.1"}}`: true, - `{"module":"pkguser","version":"2","packages":{"` + sdkGo + `":"0.1"}}`: false, + `{"module":"pkguser","version":"1.0","packages":{"@novox/ui":"0.1"}}`: true, + `{"module":"pkguser","version":"2.0","packages":{"` + sdkGo + `":"0.1"}}`: false, } { head, err := ix.keepManifest(ctx, "pkguser", raw) if err != nil { @@ -328,8 +328,8 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { // 1966/AC11: modules using state and none using logs but the foundation: logs counts 0, so a manifest of another // module using logs is refused and the foundation's is accepted. ix.OldSections = newMem() - hold("user", `{"module":"user","version":"1","state":["x"]}`) - hold("mesh-grants", `{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`) + hold("user", `{"module":"user","version":"1.0","state":["x"]}`) + hold("mesh-grants", `{"module":"mesh-grants","version":"1.0","logs":[{"name":"l"}]}`) if _, err := ix.JudgeAll(ctx, nil); err != nil { t.Fatal(err) } @@ -348,7 +348,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { t.Fatalf("the old sections count %v, want state 1 and the rest 0", c) } // DLV-J2, DLV-J3 (ADR 0615 §1): one reads entry on the assigner's state counts 1 for the assigner and 0 for the indexer. - hold("reader", `{"module":"reader","version":"1","state":["x"],"reads":["mesh-delivery-assigner.wants"]}`) + hold("reader", `{"module":"reader","version":"1.0","state":["x"],"reads":["mesh-delivery-assigner.wants"]}`) if _, err := ix.JudgeAll(ctx, nil); err != nil { t.Fatal(err) } @@ -358,20 +358,24 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { t.Fatalf("the reads on the owners' state count %v", c.Reads) } } - // DLV-J1 (ADR 0619 §1, §3): a foundation part asking bus state in the new form is refused; the nine are the foundation. - cfg := hold("mesh-config", `{"module":"mesh-config","version":"1","requires":[{"name":"cache","storage":"key-value","template":"state"}]}`) - if _, err := ix.JudgeAll(ctx, map[string]bool{cfg: true}); err != nil { - t.Fatal(err) - } - if v := verdict(cfg); !meshcatalogclient.VerdictRefuses(v) || !strings.Contains(strings.Join(v.Problems, " "), "part of the foundation") { - t.Fatalf("mesh-config asking new-form bus state: %+v", v) + // DLV-J1 (ADR 0619 §1, §3): a foundation part asking bus state in the new form is refused; the nine and the pipeline runner are the foundation. + // Design 47 *A module's registration* (b229f5c4 lines 480-481): a template in the manifest of every part on the list is + // refused, the runner's included. + for _, part := range []string{"mesh-config", "mesh-delivery-pipeline"} { + d := hold(part, `{"module":"`+part+`","version":"1.0","requires":[{"name":"cache","storage":"key-value","template":"state"}]}`) + if _, err := ix.JudgeAll(ctx, map[string]bool{d: true}); err != nil { + t.Fatal(err) + } + if v := verdict(d); !meshcatalogclient.VerdictRefuses(v) || !strings.Contains(strings.Join(v.Problems, " "), "part of the foundation") { + t.Fatalf("%s asking new-form bus state: %+v", part, v) + } } if !slices.Equal(judge.Foundation, []string{"nats", "mesh-grants", "mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools", - "mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault"}) { - t.Fatalf("the foundation is %v, want exactly the nine of ADR 0619", judge.Foundation) + "mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault", "mesh-delivery-pipeline"}) { + t.Fatalf("the foundation is %v, want exactly the nine of ADR 0619 and the pipeline runner (ADR 0666 §4)", judge.Foundation) } - late := hold("late", `{"module":"late","version":"1","logs":[{"name":"l"}]}`) - grants := hold("mesh-grants", `{"module":"mesh-grants","version":"2","logs":[{"name":"l"},{"name":"m"}]}`) + late := hold("late", `{"module":"late","version":"1.0","logs":[{"name":"l"}]}`) + grants := hold("mesh-grants", `{"module":"mesh-grants","version":"2.0","logs":[{"name":"l"},{"name":"m"}]}`) if _, err := ix.JudgeAll(ctx, map[string]bool{late: true, grants: true}); err != nil { t.Fatal(err) } @@ -383,11 +387,11 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { } // 1966/AC11: the sole user of an old section changes its manifest and keeps the section: accepted, since it was accepted // with it (the count is the accepted set held from the last pass, not recounted from fresh digests). - hold("solo", `{"module":"solo","version":"1","objects":[{"name":"o","max-mib":1}]}`) + hold("solo", `{"module":"solo","version":"1.0","objects":[{"name":"o","max-mib":1}]}`) if _, err := ix.JudgeAll(ctx, nil); err != nil { t.Fatal(err) } - solo2 := hold("solo", `{"module":"solo","version":"2","objects":[{"name":"o","max-mib":2}]}`) + solo2 := hold("solo", `{"module":"solo","version":"2.0","objects":[{"name":"o","max-mib":2}]}`) if _, err := ix.JudgeAll(ctx, map[string]bool{solo2: true}); err != nil { t.Fatal(err) } @@ -399,7 +403,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { ix.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) } held := ix.OldSections ix.OldSections = unreadable{held} - retry := hold("late", `{"module":"late","version":"2","logs":[{"name":"l"}]}`) + retry := hold("late", `{"module":"late","version":"2.0","logs":[{"name":"l"}]}`) if _, err := ix.JudgeAll(ctx, map[string]bool{retry: true}); err != nil { t.Fatal(err) } @@ -411,9 +415,9 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { // event) while a newer build of the indexer is on the trunk, and is judged again at the next judge; once this program's // build is the trunk's newest it is refused; a wait is written into the module's value as a refusal naming the fields. ownFP := "src2:" + strings.Repeat("e", 64) - hold(Module, `{"module":"mesh-delivery-indexer","version":"1"}`) + hold(Module, `{"module":"mesh-delivery-indexer","version":"1.0"}`) ix.ownFP = ownFP // this program started when the trunk held another build of the indexer than the one it holds now - later := hold("later", `{"module":"later","version":"1","a-field-from-later":1,"another":{"x":1}}`) + later := hold("later", `{"module":"later","version":"1.0","a-field-from-later":1,"another":{"x":1}}`) said = nil before := len(said) j, err = ix.JudgeAll(ctx, map[string]bool{later: true}) @@ -446,7 +450,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { } ix.ownFP = ownFP // A renamed field is a known mistake: refused at once. - renamed := hold("renamed", `{"module":"renamed","version":"1","needs":{"x":"y"}}`) + renamed := hold("renamed", `{"module":"renamed","version":"1.0","needs":{"x":"y"}}`) if j, err := ix.JudgeAll(ctx, map[string]bool{renamed: true}); err != nil || len(j.Waiting) != 1 || !meshcatalogclient.VerdictRefuses(verdict(renamed)) { t.Fatalf("a renamed field: %+v %v", j, err) } @@ -509,8 +513,8 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { } return digest } - atStart := stale("tools", `{"module":"tools","version":"7"}`) - ownRefusal := stale("client", `{"module":"client","version":"7"}`) + atStart := stale("tools", `{"module":"tools","version":"7.0"}`) + ownRefusal := stale("client", `{"module":"client","version":"7.0"}`) if _, _, err := ix.writeVerdict(ctx, ownRefusal, meshcatalog.VerdictsByDigestValue{Module: "client", State: meshcatalogclient.VerdictRefused, Problems: []string{"x"}, Judge: ix.judgeID(), At: ptr(time.Now())}); err != nil { t.Fatal(err) @@ -518,13 +522,13 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { if _, err := ix.JudgeAtStart(ctx); err != nil || meshcatalogclient.VerdictRefuses(verdict(atStart)) || verdict(atStart).Judge != ix.judgeID() || !meshcatalogclient.VerdictRefuses(verdict(ownRefusal)) { t.Fatalf("the start pass: %+v, this judge's own refusal %+v (%v)", verdict(atStart), verdict(ownRefusal), err) } - byPull := stale("tools", `{"module":"tools","version":"8"}`) + byPull := stale("tools", `{"module":"tools","version":"8.0"}`) pull := forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-tools", Number: 3, Branch: "main", - Manifests: map[string]string{"modules/tools": `{"module":"tools","version":"8"}`}} + Manifests: map[string]string{"modules/tools": `{"module":"tools","version":"8.0"}`}} if _, err := ix.ApplyMessage(ctx, pull); err != nil || meshcatalogclient.VerdictRefuses(verdict(byPull)) || verdict(byPull).Judge != ix.judgeID() { t.Fatalf("a pull carrying a digest an older judge refused: %+v %v", verdict(byPull), err) } - byTool := stale("tools", `{"module":"tools","version":"9"}`) + byTool := stale("tools", `{"module":"tools","version":"9.0"}`) if got := ix.JudgeAgain(ctx, "client", byTool); got.Stored || !strings.Contains(got.Answer, "tools's, not client's") || !meshcatalogclient.VerdictRefuses(verdict(byTool)) { t.Fatalf("indexer_judge with another module's digest: %+v", got) } diff --git a/modules/mesh-delivery-indexer/judge/busstate_test.go b/modules/mesh-delivery-indexer/judge/busstate_test.go index 8f68a4b7..bf214994 100644 --- a/modules/mesh-delivery-indexer/judge/busstate_test.go +++ b/modules/mesh-delivery-indexer/judge/busstate_test.go @@ -17,7 +17,7 @@ func TestBusStateIsJudgedOnce(t *testing.T) { const ledger = `{"name":"records","storage":"ledger","template":"ledger","options":{"max-mib":512},"types":[` + `{"name":"decision","schema":{"title":{"type":"text"}},"operations":[{"name":"propose","effect":"set-fields","fields":["title"]}]}]}` manifest := func(entries string) string { - return `{"module":"a","version":"1","requires":[` + entries + `]}` + return `{"module":"a","version":"1.0","requires":[` + entries + `]}` } for _, c := range []struct { name string @@ -30,7 +30,7 @@ func TestBusStateIsJudgedOnce(t *testing.T) { `{"name":"files","storage":"object-store","template":"objects","options":{"max-mib":2048}},` + ledger), nil}, // Design 47: every kept data may say its form and its catch-up tool, in an old section and in `requires` by template, // and is accepted as read; one saying neither reads as before. - {"a form and a catch-up tool on every kept kind", `{"module":"a","version":"1","tools":["a_take_in"],` + + {"a form and a catch-up tool on every kept kind", `{"module":"a","version":"1.0","tools":["a_take_in"],` + `"state":["seen",{"name":"modules","form":1,"catch-up":"a_take_in"}],"logs":[{"name":"acts","form":2}],` + `"streams":[{"name":"forge","events":["merged"],"form":1,"catch-up":"a_take_in"}],` + `"objects":[{"name":"revisions","max-mib":64,"form":1,"catch-up":"a_take_in"}],"requires":[` + @@ -40,9 +40,9 @@ func TestBusStateIsJudgedOnce(t *testing.T) { `{"name":"blobs","storage":"object-store","template":"objects","options":{"max-mib":64},"form":4,"catch-up":"a_take_in"},` + `{"name":"records","storage":"ledger","template":"ledger","options":{"max-mib":512},"form":3,"catch-up":"a_take_in","types":[` + `{"name":"decision","schema":{"title":{"type":"text"}},"operations":[{"name":"propose","effect":"set-fields","fields":["title"]}]}]}]}`, nil}, - {"a catch-up tool the module lacks", `{"module":"a","version":"1","tools":["a_read"],"logs":[{"name":"acts","form":1,"catch-up":"a_take_in"}]}`, + {"a catch-up tool the module lacks", `{"module":"a","version":"1.0","tools":["a_read"],"logs":[{"name":"acts","form":1,"catch-up":"a_take_in"}]}`, []string{"a's kept log \"acts\"", "catch-up tool \"a_take_in\" is not among the module's tools (a_read)"}}, - {"a catch-up tool the module lacks, in requires", `{"module":"a","version":"1","tools":["a_read"],"requires":[` + + {"a catch-up tool the module lacks, in requires", `{"module":"a","version":"1.0","tools":["a_read"],"requires":[` + `{"name":"holds","storage":"key-value","template":"state","form":1,"catch-up":"nonexistent"}]}`, []string{"a's kept state \"holds\"", "catch-up tool \"nonexistent\""}}, {"a form below 0", manifest(`{"name":"x","storage":"key-value","template":"state","form":-1}`), []string{"requires entry \"x\"", "its form is -1"}}, @@ -127,13 +127,13 @@ func TestBusStateIsJudgedOnce(t *testing.T) { "reader": {Module: "reader", Reads: []string{"owner.cache"}}}, ""}, // DLV-I14 (design 32): the packages section names a known package, by its name in its own language (the packages // state's names, and a group member's at its head: known), in a well-formed series; through a judge request too. - {"packages naming a known package", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Packages: map[string]string{sdkGo: "0.2"}}}, ""}, - {"packages naming a known package, by request", catalogue.Shelf{"user": {Module: "user", Packages: map[string]string{sdkGo: "0.2"}}}, ""}, - {"packages naming a package by the builder's name", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Packages: map[string]string{"mesh-sdk/go": "0.1"}}}, + {"packages naming a known package", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Version: "1.0", Packages: map[string]string{sdkGo: "0.2"}}}, ""}, + {"packages naming a known package, by request", catalogue.Shelf{"user": {Module: "user", Version: "1.0", Packages: map[string]string{sdkGo: "0.2"}}}, ""}, + {"packages naming a package by the builder's name", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Version: "1.0", Packages: map[string]string{"mesh-sdk/go": "0.1"}}}, `user's packages name "mesh-sdk/go", which no package of the mesh is named`}, - {"packages naming an unknown package, by request", catalogue.Shelf{"user": {Module: "user", Packages: map[string]string{"@novox/ui": "0.1"}}}, + {"packages naming an unknown package, by request", catalogue.Shelf{"user": {Module: "user", Version: "1.0", Packages: map[string]string{"@novox/ui": "0.1"}}}, `user's packages name "@novox/ui", which no package of the mesh is named`}, - {"packages in an ill-formed series", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Packages: map[string]string{sdkGo: "v0.2"}}}, + {"packages in an ill-formed series", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Version: "1.0", Packages: map[string]string{sdkGo: "v0.2"}}}, `user's packages accept "` + sdkGo + `" in the series "v0.2"`}, } { got := strings.Join(append(BusStateCatalogueProblems(c.shelf), PackagesCatalogueProblems(c.shelf, known)...), "\n") diff --git a/modules/mesh-delivery-indexer/judge/g_words_test.go b/modules/mesh-delivery-indexer/judge/g_words_test.go new file mode 100644 index 00000000..37d8267e --- /dev/null +++ b/modules/mesh-delivery-indexer/judge/g_words_test.go @@ -0,0 +1,33 @@ +package judge + +import ( + "git.novox.be/novox/mesh-sdk/go/catalogue" + "strings" + "testing" +) + +// Design 47 G §368, §386, §387, §89: the manifest validator reads the manifests' new words before any manifest uses them. +func TestTheJudgeReadsMilestoneGsWords(t *testing.T) { + for _, row := range []struct { + name string + raw string + want string // a word of the first problem; empty: accepted + }{ + {name: "MAJOR.MINOR", raw: `{"module":"a","version":"1.4"}`}, + {name: "a pre-release suffix", raw: `{"module":"a","version":"1.4-beta"}`, want: "version"}, + {name: "three numbers", raw: `{"module":"a","version":"1.4.2"}`, want: "version"}, + {name: "a findings state without its stream", raw: `{"module":"a","version":"1.0","state":["findings"]}`, want: "findings"}, + {name: "a findings state with its stream and ask", raw: `{"module":"a","version":"1.0","state":["findings"],` + + `"streams":[{"name":"a-findings","events":["finding-written"],"retention":"history"}],"consumes":["mesh-conditions.findings-asked"]}`}, + } { + raws := map[string][]byte{"a": []byte(row.raw)} + var refused map[string][]string + WithSeats([]catalogue.Seat{{Name: "thing", Scope: catalogue.ScopeMesh, Emits: []string{"finding-written"}}}, func() { + refused, _ = Shelf(raws, nil, 0) + }) + got := strings.Join(refused["a"], "\n") + if (row.want == "") != (got == "") || !strings.Contains(got, row.want) { + t.Errorf("%s: refused with %q, want %q", row.name, got, row.want) + } + } +} diff --git a/modules/mesh-delivery-indexer/judge/gwords.go b/modules/mesh-delivery-indexer/judge/gwords.go new file mode 100644 index 00000000..12088b74 --- /dev/null +++ b/modules/mesh-delivery-indexer/judge/gwords.go @@ -0,0 +1,54 @@ +package judge + +import ( + "fmt" + "regexp" + "slices" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// The manifests' words milestone G adds, read before any manifest uses them (design 47 G §89: the manifest validator's +// reading of the new words is an edge of kind 4 that the delivery plan derives). + +// majorMinor is a manifest's version: two numbers (design 47 G §368). No pre-release or custom suffix exists. +var majorMinor = regexp.MustCompile(`^[0-9]+\.[0-9]+$`) + +// FindingsAsked is the event an observer consumes to say its findings again (design 47 G §387). +const FindingsAsked = "mesh-conditions.findings-asked" + +// FindingStreamOf is the stream an observer says its findings on (design 47 G §383). +func FindingStreamOf(module string) string { return module + "-findings" } + +// versionProblems refuses a manifest whose `version` is not two numbers MAJOR.MINOR, or is missing (design 47 G §368). The +// judge's entry (Manifest) holds a manifest to it; the parser the inputs of a fingerprint are read by does not. +func versionProblems(m catalogue.Manifest) []string { + if majorMinor.MatchString(m.Version) { + return nil + } + return []string{fmt.Sprintf("%s says its version as %q; a manifest names `version` as two numbers MAJOR.MINOR such as 1.4, "+ + "and no pre-release or custom suffix exists (design 47 G §368)", m.Module, m.Version)} +} + +// gWordsProblems are the problems of a manifest's words milestone G adds: the finding stream and +// ask of a module that declares a state `findings` (§387). The `*` emitter of `consumes` (§386) is judged with the +// other consumed patterns (consumePattern). +func gWordsProblems(m catalogue.Manifest) []string { + var problems []string + for _, s := range m.State { + if s.Name != "findings" { + continue + } + stream := FindingStreamOf(m.Module) + if !slices.ContainsFunc(m.Streams, func(d catalogue.StreamDeclaration) bool { return d.Name == stream }) { + problems = append(problems, fmt.Sprintf("%s declares the state findings without the stream %q its findings are said on "+ + "(design 47 G §387)", m.Module, stream)) + } + if !slices.Contains(m.Consumes, FindingsAsked) { + problems = append(problems, fmt.Sprintf("%s declares the state findings without consuming %q, with which the "+ + "keeper asks its findings again (design 47 G §387)", m.Module, FindingsAsked)) + } + break + } + return problems +} diff --git a/modules/mesh-delivery-indexer/judge/judge.go b/modules/mesh-delivery-indexer/judge/judge.go index 59c4921b..d20f441d 100644 --- a/modules/mesh-delivery-indexer/judge/judge.go +++ b/modules/mesh-delivery-indexer/judge/judge.go @@ -45,6 +45,9 @@ func Manifest(raw []byte, predefined catalogue.PredefinedList) (m catalogue.Mani if err != nil { return catalogue.Manifest{}, problemsOf(err) } + if problems = versionProblems(m); len(problems) > 0 { + return catalogue.Manifest{}, problems + } return m, nil } diff --git a/modules/mesh-delivery-indexer/judge/judge_test.go b/modules/mesh-delivery-indexer/judge/judge_test.go index 0eaf20e8..4dbd39c0 100644 --- a/modules/mesh-delivery-indexer/judge/judge_test.go +++ b/modules/mesh-delivery-indexer/judge/judge_test.go @@ -20,27 +20,30 @@ func TestTheJudge(t *testing.T) { raws map[string]string want map[string]string // module: a word of its first problem; absent: accepted }{ - {name: "a valid manifest", raws: map[string]string{"a": `{"module":"a","version":"1"}`}}, - {name: "a refused manifest", raws: map[string]string{"a": `{"module":"a","version":"1","provides":[{"name":"amqp"}]}`}, + {name: "a valid manifest", raws: map[string]string{"a": `{"module":"a","version":"1.0"}`}}, + {name: "a refused manifest", raws: map[string]string{"a": `{"module":"a","version":"1.0","provides":[{"name":"amqp"}]}`}, want: map[string]string{"a": `provides "amqp", which is not a provision`}}, - {name: "a provision named as a credential's", raws: map[string]string{"a": `{"module":"a","version":"1","provides":[{"name":"secret-db","scope":"mesh"}]}`}, + {name: "a provision named as a credential's", raws: map[string]string{"a": `{"module":"a","version":"1.0","provides":[{"name":"secret-db","scope":"mesh"}]}`}, want: map[string]string{"a": `secret-`}}, - {name: "an unknown field", raws: map[string]string{"a": `{"module":"a","version":"1","a-field-from-later":1}`}, + {name: "an unknown field", raws: map[string]string{"a": `{"module":"a","version":"1.0","a-field-from-later":1}`}, want: map[string]string{"a": `"a-field-from-later"`}}, - {name: "a manifest under another name", raws: map[string]string{"b": `{"module":"a","version":"1"}`}, + // Design 47 G §23, §28: a step's status is kept in the registration and never in a manifest. + {name: "a manifest naming a step's status", raws: map[string]string{"a": `{"module":"a","version":"1.0","policy":{"lint":"bypass"}}`}, + want: map[string]string{"a": `"policy"`}}, + {name: "a manifest under another name", raws: map[string]string{"b": `{"module":"a","version":"1.0"}`}, want: map[string]string{"b": `of module "a", not "b"`}}, {name: "a seat declared twice", raws: map[string]string{ - "one": `{"module":"one","version":"1","seats":[{"name":"thing","scope":"mesh"}]}`, - "two": `{"module":"two","version":"1","seats":[{"name":"thing","scope":"mesh"}]}`, - "ok": `{"module":"ok","version":"1"}`}, + "one": `{"module":"one","version":"1.0","seats":[{"name":"thing","scope":"mesh"}]}`, + "two": `{"module":"two","version":"1.0","seats":[{"name":"thing","scope":"mesh"}]}`, + "ok": `{"module":"ok","version":"1.0"}`}, want: map[string]string{"one": "already declares", "two": "already declares"}}, // The lead's ruling 5 for A2h: a module that claims a seat and serves its verbs keeps tools of its own beside them // (the enrolment's first-view tools, DLV-M6), and the judge takes it. {name: "a claimed seat's module with tools of its own", raws: map[string]string{ - "def": `{"module":"def","version":"1","seats":[{"name":"enrolment","scope":"mesh","serves":["issue","again"]}]}`, - "enrolment": `{"module":"enrolment","version":"1","claims":[{"name":"enrolment","scope":"mesh","serves":["issue","again"]}],` + + "def": `{"module":"def","version":"1.0","seats":[{"name":"enrolment","scope":"mesh","serves":["issue","again"]}]}`, + "enrolment": `{"module":"enrolment","version":"1.0","claims":[{"name":"enrolment","scope":"mesh","serves":["issue","again"]}],` + `"tools":["enrolment_first_views","enrolment_first_view_again"]}`}}, } { raws := map[string][]byte{} @@ -64,10 +67,10 @@ func TestTheJudge(t *testing.T) { // A field the judge does not know waits in the indexer's judge and stays a refusal in the checker's (clause 3 of the // record on a release's Approve); a renamed field and any other problem are refused by both. waitRaws := map[string][]byte{ - "later": []byte(`{"module":"later","version":"1","later-field":true,"other":1}`), - "renamed": []byte(`{"module":"renamed","version":"1","needs":{"x":"y"}}`), - "bad": []byte(`{"module":"bad","version":"1","provides":[{"name":"amqp"}]}`), - "ok": []byte(`{"module":"ok","version":"1"}`), + "later": []byte(`{"module":"later","version":"1.0","later-field":true,"other":1}`), + "renamed": []byte(`{"module":"renamed","version":"1.0","needs":{"x":"y"}}`), + "bad": []byte(`{"module":"bad","version":"1.0","provides":[{"name":"amqp"}]}`), + "ok": []byte(`{"module":"ok","version":"1.0"}`), } refused, waits, _ := ShelfWaiting(waitRaws, nil, 0, nil, nil) if !reflect.DeepEqual(waits, map[string][]string{"later": {"later-field", "other"}}) || len(refused) != 2 || refused["renamed"] == nil || refused["bad"] == nil { @@ -79,7 +82,7 @@ func TestTheJudge(t *testing.T) { // The private network's manifest, which the mesh provides itself, is accepted (moved from mesh-sdk overlay's test). if raw, err := json.Marshal(overlay.Manifest()); err != nil { t.Fatal(err) - } else if _, problems := Manifest(raw, nil); len(problems) > 0 { + } else if _, problems := Manifest(versioned(t, raw), nil); len(problems) > 0 { t.Errorf("the provided private network is refused: %v", problems) } shelf := catalogue.Shelf{"nats": {}, "mesh-nats": {}, "a.b": {}} @@ -100,8 +103,8 @@ func TestTheJudge(t *testing.T) { // What cmd/mesh-judge answers the checker (ADR 0578 §2): a composer request in, a verdict out, each manifest accepted // or refused; a manifest left as a digest is a request it cannot run. r := composer.Request{V: composer.Version, Cases: []composer.Case{{Name: "change", Manifests: map[string]composer.Manifest{ - "a": {Raw: json.RawMessage(`{"module":"a","version":"1"}`)}, - "b": {Raw: json.RawMessage(`{"module":"b","version":"1","requires":["amqp"]}`)}, + "a": {Raw: json.RawMessage(`{"module":"a","version":"1.0"}`)}, + "b": {Raw: json.RawMessage(`{"module":"b","version":"1.0","requires":["amqp"]}`)}, }}}} v, err := Request(r) if err != nil || v.V != composer.Version || len(v.Cases) != 1 || v.SDK != Identity() || !v.Cases[0].Manifests["a"].Accepted || @@ -113,13 +116,13 @@ func TestTheJudge(t *testing.T) { // state, which a trunk module uses, is accepted. old := composer.Request{V: composer.Version, Cases: []composer.Case{ {Name: "change", Manifests: map[string]composer.Manifest{ - "user": {Raw: json.RawMessage(`{"module":"user","version":"1","state":["x"]}`)}, - "late": {Raw: json.RawMessage(`{"module":"late","version":"1","logs":[{"name":"l"}]}`)}, - "newer": {Raw: json.RawMessage(`{"module":"newer","version":"1","state":["y"]}`)}, - "mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)}}}, + "user": {Raw: json.RawMessage(`{"module":"user","version":"1.0","state":["x"]}`)}, + "late": {Raw: json.RawMessage(`{"module":"late","version":"1.0","logs":[{"name":"l"}]}`)}, + "newer": {Raw: json.RawMessage(`{"module":"newer","version":"1.0","state":["y"]}`)}, + "mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1.0","logs":[{"name":"l"}]}`)}}}, {Name: "trunk", Manifests: map[string]composer.Manifest{ - "user": {Raw: json.RawMessage(`{"module":"user","version":"1","state":["x"]}`)}, - "mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)}}}}} + "user": {Raw: json.RawMessage(`{"module":"user","version":"1.0","state":["x"]}`)}, + "mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1.0","logs":[{"name":"l"}]}`)}}}}} ov, err := Request(old) if err != nil { t.Fatal(err) @@ -134,3 +137,19 @@ func TestTheJudge(t *testing.T) { t.Fatal("a manifest left as a digest was judged") } } + +// versioned is a manifest whose `version` is a MAJOR.MINOR (design 47 G §368): the private network's manifest is +// mesh-networking's, which G's change gives its version. +func versioned(t *testing.T, raw []byte) []byte { + t.Helper() + var fields map[string]any + if err := json.Unmarshal(raw, &fields); err != nil { + t.Fatal(err) + } + fields["version"] = "1.0" + out, err := json.Marshal(fields) + if err != nil { + t.Fatal(err) + } + return out +} diff --git a/modules/mesh-delivery-indexer/judge/objects_test.go b/modules/mesh-delivery-indexer/judge/objects_test.go index a74bfe5d..36d34ef4 100644 --- a/modules/mesh-delivery-indexer/judge/objects_test.go +++ b/modules/mesh-delivery-indexer/judge/objects_test.go @@ -10,7 +10,7 @@ import ( // knowledge is mesh-knowledge's manifest as ADR 0592 has it: an object bucket granted by its trusted value readers (a // trusted file takes it), and // two streams that keep their history. -const knowledge = `{"module":"mesh-knowledge","version":"1", +const knowledge = `{"module":"mesh-knowledge","version":"1.0", "resources":[{"id":"state","type":"directory","mode":"0700","place":"."}, {"id":"readers","trusted":true,"type":"file","path":"${dir:state}/readers.json","mode":"0600", "merge":"json","takes-settings":["readers"],"content":"{\"readers\": {}}"}], @@ -41,7 +41,7 @@ func TestObjectBucketsAreJudgedAsADR0592Says(t *testing.T) { } } // A bucket whose name on the bus starts with what the mesh derives names with is refused. - if _, err := ParseManifest([]byte(`{"module":"kv","version":"1","objects":[{"name":"x","max-mib":1}]}`)); err == nil || + if _, err := ParseManifest([]byte(`{"module":"kv","version":"1.0","objects":[{"name":"x","max-mib":1}]}`)); err == nil || !strings.Contains(err.Error(), "KV_") { t.Errorf("a bucket kv_x was taken: %v", err) } @@ -54,7 +54,7 @@ func TestObjectBucketsAreJudgedAsADR0592Says(t *testing.T) { return m } owner := parse(knowledge) - reader := parse(`{"module":"records-page","version":"1","reads":["mesh-knowledge.sections"]}`) + reader := parse(`{"module":"records-page","version":"1.0","reads":["mesh-knowledge.sections"]}`) shelf := catalogue.Shelf{"mesh-knowledge": owner, "records-page": reader} if p := objectProblemsOf(CatalogueProblems(shelf)); len(p) > 0 { t.Fatalf("a granted read was refused: %v", p) diff --git a/modules/mesh-delivery-indexer/judge/oldsections.go b/modules/mesh-delivery-indexer/judge/oldsections.go index 91a2446d..a6da40ea 100644 --- a/modules/mesh-delivery-indexer/judge/oldsections.go +++ b/modules/mesh-delivery-indexer/judge/oldsections.go @@ -16,12 +16,13 @@ import ( // OldSections are the sections of a manifest that keep bus state in the old forms. var OldSections = []string{"state", "logs", "streams", "objects"} -// Foundation are the modules that keep using the old sections for good: exactly the nine parts the bus module's own -// composition, assignments, build or delivery needs before it can answer (novox/hq ADR 0619 §1, §3): the bus module, the -// grants part, the indexer, the assigner, the tool runner, the builder, the checker, mesh-config and the vault. Their new -// state goes on their old sections too, so a foundation manifest asking bus state in the new form is refused. +// Foundation are the modules that keep using the old sections for good: exactly the ten parts the bus module's own +// composition, assignments, build or delivery needs before it can answer (novox/hq ADR 0619 §1, §3, as ADR 0666 §4 changes +// them): the bus module, the grants part, the indexer, the assigner, the tool runner, the builder, the checker, mesh-config, +// the vault and the pipeline runner, which runs the bus module's own pipeline. Their new state goes on their old sections +// too, so a foundation manifest asking bus state in the new form is refused. var Foundation = []string{"nats", "mesh-grants", "mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools", - "mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault"} + "mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault", "mesh-delivery-pipeline"} // ViewOwners are the two owners whose state every reader reads through their first views and read tools, never by a // `reads` entry (novox/hq ADR 0615 §1): CountOwnerReads counts what is left of those entries. diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index b2e091a9..1928421c 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -2536,7 +2536,7 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M } m.SetPredefined(predefined) - var problems []string + problems := gWordsProblems(m) if !name.MatchString(m.Module) { problems = append(problems, fmt.Sprintf( "%q is not a usable module name: lower-case letters, digits, dashes and dots", m.Module)) diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index 113c1610..7d9da3e6 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -30,7 +30,7 @@ import ( // catalogue check refuses a hand-written backup line at all since ADR 0233; parsing still reads one, // because a module on the shelf was written before.) func caseABackupNamingAnUndeclaredDirectoryIsRefused(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"pg","version":"1", + _, err := ParseManifest([]byte(`{"module":"pg","version":"1.0", "contributions":[{"seat":"node-backup","kind":"backup","content":"path ${dir:nowhere}"}]}`)) if err == nil || !strings.Contains(err.Error(), "nowhere") { t.Fatalf("a backup of an undeclared directory was accepted: %v", err) @@ -109,7 +109,7 @@ func caseABuildsBundlesAreCarriedOnTheResolvedManifest(t *testing.T) { t.Errorf("the bundle is carried as %+v", b) } // A repository manifest may not write what the build derives. - raw := `{"module":"x","version":"1","build":{"artifacts":[{"name":"t","kind":"bundle","language":"typescript"}]},` + + raw := `{"module":"x","version":"1.0","build":{"artifacts":[{"name":"t","kind":"bundle","language":"typescript"}]},` + `"bundles":[{"name":"t","source":"s","digest":"` + bundleDigest + `"}]}` if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "bundles") { t.Errorf("a manifest stating its build's output by hand was accepted: %v", err) @@ -261,7 +261,7 @@ func caseAClaimOnASeatAnotherModuleDeclaresIsAccepted(t *testing.T) { // would say it where no consumer hears it, so the manifest is refused, naming the seat's subject; the seat's // holder saying nothing of its own is accepted. holder := func(emits string) []byte { - return []byte(`{"module":"mesh-self-check","version":"1","emits":` + emits + `,"claims":[{"name":"mesh-self-check","scope":"mesh","serves":["doctor","probes"]}]}`) + return []byte(`{"module":"mesh-self-check","version":"1.0","emits":` + emits + `,"claims":[{"name":"mesh-self-check","scope":"mesh","serves":["doctor","probes"]}]}`) } if _, err := ParseManifest(holder(`["checked"]`)); err == nil || !strings.Contains(err.Error(), "mesh.seat.mesh-self-check.event.checked") { t.Errorf("a seat's event listed as the module's own: %v", err) @@ -366,10 +366,10 @@ func caseAContainerCannotBeBothRunOnceAndScheduled(t *testing.T) { func caseAContributionLocalNameMustBeUsable(t *testing.T) { for _, bad := range []string{ // Not a usable name. - `{"module":"minio","version":"1","requires":["route"], + `{"module":"minio","version":"1.0","requires":["route"], "contributes":{"route":{"Not OK":{"label":"files","port":9000}}}}`, // A local contribution with nothing in it. - `{"module":"minio","version":"1","requires":["route"], + `{"module":"minio","version":"1.0","requires":["route"], "contributes":{"route":{"api":{}}}}`, } { if _, err := ParseManifest([]byte(bad)); err == nil { @@ -423,20 +423,20 @@ func caseADeclaredStoreParsesAndPassesTheCheck(t *testing.T) { // The node-engine's rules, for directories and — as the engine judges them since novox/hq issue 495 — files. func caseADefinitionIsRefusedWhereTheNodeEngineRefusesItsPaths(t *testing.T) { refused := map[string]string{ - "a stated directory in docker's data": `{"module": "sidecar", "version": "1", "resources": [ + "a stated directory in docker's data": `{"module": "sidecar", "version":"1.0", "resources": [ {"id": "volumes", "type": "directory", "path": "/var/lib/docker/volumes/x"}]}`, - "a file in containerd's data": `{"module": "images", "version": "1", "resources": [ + "a file in containerd's data": `{"module": "images", "version":"1.0", "resources": [ {"id": "f", "type": "file", "path": "/var/lib/containerd/x", "content": "x"}]}`, - "a file beneath a placed directory that climbs out of it": `{"module": "docker", "version": "1", "resources": [ + "a file beneath a placed directory that climbs out of it": `{"module": "docker", "version":"1.0", "resources": [ {"id": "state", "type": "directory", "place": "mesh"}, {"id": "f", "type": "file", "path": "${dir:state}/../../containers/x", "content": "x"}]}`, - "a file in /boot": `{"module": "grub", "version": "1", "resources": [ + "a file in /boot": `{"module": "grub", "version":"1.0", "resources": [ {"id": "cfg", "type": "file", "path": "/boot/grub/custom.cfg", "content": "x"}]}`, - "a directory that is the mesh's whole tree": `{"module": "mesh", "version": "1", "resources": [ + "a directory that is the mesh's whole tree": `{"module": "mesh", "version":"1.0", "resources": [ {"id": "all", "type": "directory", "place": "."}]}`, - "a directory that holds /etc": `{"module": "x", "version": "1", "resources": [ + "a directory that holds /etc": `{"module": "x", "version":"1.0", "resources": [ {"id": "d", "type": "directory", "path": "/"}]}`, - "a directory in the node-engine's own tree, by another module": `{"module": "intruder", "version": "1", + "a directory in the node-engine's own tree, by another module": `{"module": "intruder", "version":"1.0", "resources": [{"id": "d", "type": "directory", "path": "/var/lib/mesh-host/x"}]}`, } for name, raw := range refused { @@ -450,24 +450,24 @@ func caseADefinitionIsRefusedWhereTheNodeEngineRefusesItsPaths(t *testing.T) { // own module, an account's keys, a module's own root, and — not on the engine's lists, so not refused here — // below /lib and at /storage, /data, /services and /var/lock. accepted := map[string]string{ - "a unit in /etc": `{"module": "power", "version": "1", "resources": [ + "a unit in /etc": `{"module": "power", "version":"1.0", "resources": [ {"id": "d", "type": "directory", "path": "/etc/systemd/system/x.service.d"}, {"id": "u", "type": "file", "path": "/etc/systemd/system/x.service.d/a.conf", "content": "x"}]}`, - "a run directory": `{"module": "fail2ban", "version": "1", "resources": [ + "a run directory": `{"module": "fail2ban", "version":"1.0", "resources": [ {"id": "run-dir", "type": "directory", "path": "/var/run/fail2ban"}]}`, - "a program in /usr/local/bin": `{"module": "claude-code", "version": "1", "resources": [ + "a program in /usr/local/bin": `{"module": "claude-code", "version":"1.0", "resources": [ {"id": "start", "type": "file", "path": "/usr/local/bin/claude-agent", "content": "x"}]}`, - "the node-engine's launcher and state, by the node-engine": `{"module": "mesh-host", "version": "1", "resources": [ + "the node-engine's launcher and state, by the node-engine": `{"module": "mesh-host", "version":"1.0", "resources": [ {"id": "launcher", "type": "file", "path": "/usr/lib/nox-mesh-host/launch", "content": "x"}, {"id": "state", "type": "directory", "path": "/var/lib/mesh-host"}]}`, - "an account's .ssh": `{"module": "ssh-client", "version": "1", "resources": [ + "an account's .ssh": `{"module": "ssh-client", "version":"1.0", "resources": [ {"id": "ssh-dir", "type": "directory", "path": "/home/someone/.ssh"}, {"id": "config", "type": "file", "path": "/home/someone/.ssh/config", "content": "x"}]}`, - "a module's own root": `{"module": "mailu", "version": "1", "resources": [ + "a module's own root": `{"module": "mailu", "version":"1.0", "resources": [ {"id": "state", "type": "directory", "place": "."}]}`, - "a file below /lib": `{"module": "udev", "version": "1", "resources": [ + "a file below /lib": `{"module": "udev", "version":"1.0", "resources": [ {"id": "rule", "type": "file", "path": "/lib/udev/rules.d/99-x.rules", "content": "x"}]}`, - "directories at /storage, /data, /services and /var/lock": `{"module": "roots", "version": "1", "resources": [ + "directories at /storage, /data, /services and /var/lock": `{"module": "roots", "version":"1.0", "resources": [ {"id": "a", "type": "directory", "path": "/storage"}, {"id": "b", "type": "directory", "path": "/data"}, {"id": "c", "type": "directory", "path": "/services"}, {"id": "d", "type": "directory", "path": "/var/lock"}]}`, } @@ -508,7 +508,7 @@ func caseADeliveringSeatIsOnlyHeldByAModuleThatProvides(t *testing.T) { // would be the answer anyway, and every consumer would be sent to it. // And refused at registration, where the seat set is the store's: what a seat delivers is // data, so a compiled copy of it may not be what refuses a build (novox/hq ADR 0122). - raw := []byte(`{"module":"thing","version":"1","claims":[{"name":"git","scope":"mesh"}]}`) + raw := []byte(`{"module":"thing","version":"1.0","claims":[{"name":"git","scope":"mesh"}]}`) m, err := ParseManifest(raw) if err != nil { t.Fatalf("the parser judged what a seat delivers: %v", err) @@ -544,7 +544,7 @@ func caseADirectoryBeneathAPlacedOneIsPlacedWithIt(t *testing.T) { func caseADirectoryPlacedInDockersDataIsRefusedWhereTheDefinitionIsJudged(t *testing.T) { // The #205 shape, as it was merged. - _, err := ParseManifest([]byte(`{"module": "docker", "version": "1", + _, err := ParseManifest([]byte(`{"module": "docker", "version":"1.0", "resources": [{"id": "state", "type": "directory", "place": "."}]}`)) if err == nil { t.Fatal("a directory resolving to /var/lib/docker was accepted; the node-engine refuses it at apply") @@ -647,7 +647,7 @@ func caseAGuardedPortMustBeAPort(t *testing.T) { // person (the offer's own secret issued outside). func caseAHandedModulesPlanNamesTheCredentialFormOfItsProvidersOffer(t *testing.T) { storeOffering := func(credential string) catalogue.Manifest { - raw := `{"module":"store","version":"1","provides":[{"name":"postgres-database","scope":"mesh"` + + raw := `{"module":"store","version":"1.0","provides":[{"name":"postgres-database","scope":"mesh"` + credential + `}],"serves":{"postgres-database":{"port":5432}}` if strings.Contains(credential, `"own"`) { raw += `,"own-secrets":{"admin":{"path":"/var/lib/store/admin","taken":"at-start"},` + @@ -685,7 +685,7 @@ func caseAHandedModulesPlanNamesTheCredentialFormOfItsProvidersOffer(t *testing. for credential, refused := range map[string]string{``: "says no credential form", `,"credential":{}`: "says no credential form", `,"credential":{"form":"pair","own":"admin"}`: "beside an own secret", `,"credential":{"form":"maybe"}`: `not "pair" or "none"`, `,"credential":{"form":"none"}`: ""} { - raw := `{"module":"store","version":"1","provides":[{"name":"postgres-database","scope":"mesh"` + credential + `}]}` + raw := `{"module":"store","version":"1.0","provides":[{"name":"postgres-database","scope":"mesh"` + credential + `}]}` var m catalogue.Manifest _ = json.Unmarshal([]byte(raw), &m) problems := strings.Join(CredentialFormProblems(m), "; ") @@ -722,7 +722,7 @@ func caseAHolderOfNodeFilesDeclaringRunsAsIsRefused(t *testing.T) { // novox/hq ADR 0310 §3: a holder of node-reads reads as an account of its own; one the machine's runtime would // carry — as the operator's account — is refused at parse. func caseAHolderOfNodeReadsWithoutAnAccountOfItsOwnIsRefused(t *testing.T) { - _, err := ParseManifest([]byte(`{"module": "reads", "version": "1", + _, err := ParseManifest([]byte(`{"module": "reads", "version":"1.0", "claims": [{"name": "node-reads", "scope": "node"}]}`)) if err == nil || !strings.Contains(err.Error(), "runs-as") || !strings.Contains(err.Error(), "0310") { t.Errorf("a holder of node-reads without runs-as was not refused for it: %v", err) @@ -872,7 +872,7 @@ func caseAKindedBenchNeedsAKindAndNoOtherSeatTakesOne(t *testing.T) { // A local name names one credential: two requirements may not share it (review C2). func caseALocalNameIsUniqueAcrossRequirements(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"x","version":"1","requires":["secret","postgres-database"], + _, err := ParseManifest([]byte(`{"module":"x","version":"1.0","requires":["secret","postgres-database"], "secrets":{"secret":{"x":"/var/lib/x/a"},"postgres-database":{"x":"/var/lib/x/b"}}}`)) if err == nil || !strings.Contains(err.Error(), "both under") { t.Fatalf("two requirements under one local name were accepted: %v", err) @@ -882,15 +882,15 @@ func caseALocalNameIsUniqueAcrossRequirements(t *testing.T) { func caseALocalNameMayNotCollideWithWhatTheModuleAlreadyCallsSomething(t *testing.T) { for _, bad := range []string{ // One of the module's own secrets. - `{"module":"ca","version":"1","requires":["secret"],"own-secrets":{"root-key":"/var/lib/ca/own"}, + `{"module":"ca","version":"1.0","requires":["secret"],"own-secrets":{"root-key":"/var/lib/ca/own"}, "secrets":{"secret":{"root-key":"/var/lib/ca/root.key"}}}`, // Something it requires. - `{"module":"ca","version":"1","requires":["secret","postgres-database"], + `{"module":"ca","version":"1.0","requires":["secret","postgres-database"], "secrets":{"secret":{"postgres-database":"/var/lib/ca/x"}}}`, // Not a usable name. - `{"module":"ca","version":"1","requires":["secret"],"secrets":{"secret":{"Root Key":"/var/lib/ca/x"}}}`, + `{"module":"ca","version":"1.0","requires":["secret"],"secrets":{"secret":{"Root Key":"/var/lib/ca/x"}}}`, // A relative path. - `{"module":"ca","version":"1","requires":["secret"],"secrets":{"secret":{"root-key":"root.key"}}}`, + `{"module":"ca","version":"1.0","requires":["secret"],"secrets":{"secret":{"root-key":"root.key"}}}`, } { if _, err := ParseManifest([]byte(bad)); err == nil { t.Errorf("accepted:\n%s", bad) @@ -902,16 +902,16 @@ func caseALocalNameMayNotCollideWithWhatTheModuleAlreadyCallsSomething(t *testin // field the mesh does not know, or kept by a module whose own name is not one plain token. func caseALogIsNamedLocallyAndCapped(t *testing.T) { for _, c := range []struct{ manifest, says string }{ - {`{"module":"a","version":"1","logs":["mesh.changes"]}`, `keeps log "mesh.changes": a log is named locally`}, - {`{"module":"a","version":"1","logs":["my_changes"]}`, `keeps log "my_changes"`}, - {`{"module":"a","version":"1","logs":["Changes"]}`, `keeps log "Changes"`}, - {`{"module":"a","version":"1","logs":["c","c"]}`, `keeps log "c" twice`}, - {`{"module":"a","version":"1","logs":[{"name":"c","max-mib":8193}]}`, `between 1 and 8192 MiB`}, - {`{"module":"a","version":"1","logs":[{"name":"c","max-mib":-1}]}`, `between 1 and 8192 MiB`}, - {`{"module":"a","version":"1","logs":[{"name":"c","max-mib":0}]}`, `max-mib is between 1 and 8192, not 0`}, - {`{"module":"a","version":"1","logs":[{"name":"c","stream":"LOG_x"}]}`, `{name, max-mib, max-age-days, form, catch-up}`}, - {`{"module":"a","version":"1","logs":[{"name":"c","max-age-days":0}]}`, `max-age-days is between 1 and 3650, not 0`}, - {`{"module":"a.b","version":"1","logs":["c"]}`, `no dot`}, + {`{"module":"a","version":"1.0","logs":["mesh.changes"]}`, `keeps log "mesh.changes": a log is named locally`}, + {`{"module":"a","version":"1.0","logs":["my_changes"]}`, `keeps log "my_changes"`}, + {`{"module":"a","version":"1.0","logs":["Changes"]}`, `keeps log "Changes"`}, + {`{"module":"a","version":"1.0","logs":["c","c"]}`, `keeps log "c" twice`}, + {`{"module":"a","version":"1.0","logs":[{"name":"c","max-mib":8193}]}`, `between 1 and 8192 MiB`}, + {`{"module":"a","version":"1.0","logs":[{"name":"c","max-mib":-1}]}`, `between 1 and 8192 MiB`}, + {`{"module":"a","version":"1.0","logs":[{"name":"c","max-mib":0}]}`, `max-mib is between 1 and 8192, not 0`}, + {`{"module":"a","version":"1.0","logs":[{"name":"c","stream":"LOG_x"}]}`, `{name, max-mib, max-age-days, form, catch-up}`}, + {`{"module":"a","version":"1.0","logs":[{"name":"c","max-age-days":0}]}`, `max-age-days is between 1 and 3650, not 0`}, + {`{"module":"a.b","version":"1.0","logs":["c"]}`, `no dot`}, } { _, err := ParseManifest([]byte(c.manifest)) if c.says == "" { @@ -1043,7 +1043,7 @@ func caseAMalformedPromptIsRefused(t *testing.T) { "a placeholder other than {member}": {"the password of the mail account {name}", "{member}"}, "two placeholders, one unknown": {"the {kind} secret of the mail account {member}", "{member}"}, } { - _, err := ParseManifest([]byte(`{"module":"mesh-mail","version":"1","own-secrets":{ + _, err := ParseManifest([]byte(`{"module":"mesh-mail","version":"1.0","own-secrets":{ "mail-credential-*":{"path":"/var/lib/mesh-mail/mail-credential-*.secret","issued-by":"outside", "prompt":` + jsonString(c.prompt) + `}}}`)) if err == nil || !strings.Contains(err.Error(), c.says) { @@ -1126,11 +1126,11 @@ func caseAManifestKeyNothingReadsIsRefused(t *testing.T) { // `local-network` is a source a manifest may name (§1, changing ADR 0045 §3), and an unknown source is // still refused. func caseAManifestMayListenFromTheLocalNetwork(t *testing.T) { - if _, err := ParseManifest([]byte(`{"module":"mosquitto","version":"1", + if _, err := ParseManifest([]byte(`{"module":"mosquitto","version":"1.0", "listens":[{"port":1883,"from":"local-network"}]}`)); err != nil { t.Fatalf("a listen from the local network is refused: %v", err) } - _, err := ParseManifest([]byte(`{"module":"mosquitto","version":"1", + _, err := ParseManifest([]byte(`{"module":"mosquitto","version":"1.0", "listens":[{"port":1883,"from":"lan"}]}`)) if err == nil || !strings.Contains(err.Error(), `from "lan"`) { t.Fatalf("a source the mesh cannot render is not refused: %v", err) @@ -1154,7 +1154,7 @@ func caseAManifestMixingTheConfigAndSettingsFormsIsRefused(t *testing.T) { } func caseAManifestProvidingAmqpIsRefused(t *testing.T) { - raw := []byte(`{"module":"old-broker","version":"1","provides":[{"name":"amqp","scope":"mesh"}]}`) + raw := []byte(`{"module":"old-broker","version":"1.0","provides":[{"name":"amqp","scope":"mesh"}]}`) _, err := ParseManifest(raw) if err == nil || !strings.Contains(err.Error(), `provides "amqp", which is not a provision`) { t.Fatalf("a module providing amqp was not refused, or not for the reason: %v", err) @@ -1162,7 +1162,7 @@ func caseAManifestProvidingAmqpIsRefused(t *testing.T) { } func caseAManifestRequiringAmqpIsRefused(t *testing.T) { - raw := []byte(`{"module":"forwarder","version":"1","requires":["amqp"]}`) + raw := []byte(`{"module":"forwarder","version":"1.0","requires":["amqp"]}`) _, err := ParseManifest(raw) if err == nil || !strings.Contains(err.Error(), `requires "amqp", which is not a provision`) { t.Fatalf("a module requiring amqp was not refused, or not for the reason: %v", err) @@ -1177,7 +1177,7 @@ func caseAManifestRequiringAmqpIsRefused(t *testing.T) { // and the mesh knows what it is called now. func caseAManifestUsingTheOldNameIsToldTheNewOne(t *testing.T) { _, err := ParseManifest([]byte( - `{"module":"postgres","version":"1","needs":{"superuser":"/var/lib/superuser"}}`)) + `{"module":"postgres","version":"1.0","needs":{"superuser":"/var/lib/superuser"}}`)) if err == nil { t.Fatal("a manifest using the old name was accepted, so two fields now mean one thing") } @@ -1192,7 +1192,7 @@ func caseAManifestUsingTheOldNameIsToldTheNewOne(t *testing.T) { // groups count as a place for it); a placeholder in any other shape is refused where the manifest is read. func caseAManifestWhoseContainerTakesItsGroupFromConfigRegisters(t *testing.T) { m := map[string]any{ - "module": "n8n", "version": "1", + "module": "n8n", "version": "1.0", "config": map[string]any{"media-gid": map[string]any{"type": "number", "minimum": 1, "required": true, "description": "the group that owns the media library on this machine"}}, "resources": []any{aContainerWithGroups("${config:media-gid}")}, @@ -1209,7 +1209,7 @@ func caseAManifestWhoseContainerTakesItsGroupFromConfigRegisters(t *testing.T) { } func caseAManifestWithoutUnhealthyIsNotUrgent(t *testing.T) { - m, err := ParseManifest([]byte(`{"module":"notes","version":"1"}`)) + m, err := ParseManifest([]byte(`{"module":"notes","version":"1.0"}`)) if err != nil { t.Fatal(err) } @@ -1237,7 +1237,7 @@ func caseAMapReferenceToNoDirectoryRefusesAtTheManifest(t *testing.T) { // The catalogue check — the strict parse registration runs too — refuses each by name, with the // module and the field it stands in, and says nothing about the shell's own syntax. func caseAMisspelledPlaceholderIsRefusedAtTheCheck(t *testing.T) { - raw := `{"module":"speller","version":"1","resources":[{"id":"rc","type":"file","path":"/etc/speller.rc",` + + raw := `{"module":"speller","version":"1.0","resources":[{"id":"rc","type":"file","path":"/etc/speller.rc",` + `"content":"a=` + misspelledShell + `\nb=` + undeclaredSetting + `\nc=` + misspelledMachine + `\nd=` + shellsOwn + `\n"}]}` _, err := ParseManifest([]byte(raw)) @@ -1263,7 +1263,7 @@ func caseAMisspelledPlaceholderIsRefusedAtTheCheck(t *testing.T) { {`{"id":"rc","type":"file","path":"/etc/rc","content":"a=${machine:address\nb=1"}`, "${machine:address", "content"}, {`{"id":"rc","type":"process","name":"speller","env":{"NAME":"${machine:name}"}}`, "${machine:name}", "env.NAME"}, } { - raw := `{"module":"speller","version":"1","resources":[` + c.resource + `]}` + raw := `{"module":"speller","version":"1.0","resources":[` + c.resource + `]}` _, err := ParseManifest([]byte(raw)) if err == nil || !strings.Contains(err.Error(), "speller's resource rc holds "+c.token+" in its "+c.field) { t.Errorf("%s in its %s was accepted at the check, or not named: %v", c.token, c.field, err) @@ -1272,7 +1272,7 @@ func caseAMisspelledPlaceholderIsRefusedAtTheCheck(t *testing.T) { // And the shell's syntax alone, beside placeholders every pass knows, is accepted — as is the // shape a lower-case shell variable takes with an operator after its colon. - raw = `{"module":"speller","version":"1","resources":[{"id":"rc","type":"file","path":"${machine:account-home}/.rc",` + + raw = `{"module":"speller","version":"1.0","resources":[{"id":"rc","type":"file","path":"${machine:account-home}/.rc",` + `"content":"` + shellsOwn + ` ${(%):-%n} ${1:-.} ${count:-} ${trial:+ on trial} ${machine:address} ` + shellsOperators + `\n"},` + `{"id":"server","type":"container","name":"server","env":{"PORT":"${PORT:-80}"}}]}` @@ -1303,7 +1303,7 @@ func caseAModuleDeclaresItsOwnSeatAndHoldsIt(t *testing.T) { // A module may define its own seat and claim it — the mesh enforces exclusivity without knowing // what it means (novox/hq ADR 0121). But it may not define one in the mesh's own namespace. func caseAModuleDefinesAndClaimsItsOwnSeat(t *testing.T) { - ok := []byte(`{"module":"showcase","version":"1","seats":[{"name":"the-showcase","scope":"node"}],` + + ok := []byte(`{"module":"showcase","version":"1.0","seats":[{"name":"the-showcase","scope":"node"}],` + `"claims":[{"name":"the-showcase","scope":"node"}]}`) if _, err := ParseManifest(ok); err != nil { t.Fatalf("a module could not define and claim its own seat: %v", err) @@ -1320,7 +1320,7 @@ func caseAModuleDefinesAndClaimsItsOwnSeat(t *testing.T) { t.Fatal("a module claimed a seat nobody defines") } // A module may not carve its seat out of the mesh's own namespace. - bad := []byte(`{"module":"x","version":"1","seats":[{"name":"node-mine","scope":"node"}],` + + bad := []byte(`{"module":"x","version":"1.0","seats":[{"name":"node-mine","scope":"node"}],` + `"claims":[{"name":"node-mine","scope":"node"}]}`) if _, err := ParseManifest(bad); err == nil || !strings.Contains(err.Error(), "own namespace") { t.Fatalf("a module defined a seat in the mesh's namespace and was not refused: %v", err) @@ -1329,7 +1329,7 @@ func caseAModuleDefinesAndClaimsItsOwnSeat(t *testing.T) { func caseAModuleIsEitherWrittenOrComputedNotBoth(t *testing.T) { // Otherwise nobody could say where a given file on a machine came from. - _, err := ParseManifest([]byte(`{"module":"mesh-network","version":"1", + _, err := ParseManifest([]byte(`{"module":"mesh-network","version":"1.0", "computed":"mesh-network", "resources":[{"id":"a","type":"package","package":"wireguard-tools"}]}`)) if err == nil { @@ -1348,7 +1348,7 @@ func caseAModuleIsEitherWrittenOrComputedNotBoth(t *testing.T) { // resolved and pushed — and then refused on the machine, in a log, with nothing tying it back to // the manifest. Enforced at the far end only is enforced and not usable. func caseAModuleMayNotDeclareAnAction(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"probe","version":"1","resources":[ + _, err := ParseManifest([]byte(`{"module":"probe","version":"1.0","resources":[ {"id":"migrate","type":"action","command":["/bin/true"],"verify":["/bin/true"]}]}`)) if err == nil { t.Fatal("a manifest declaring an action was accepted, and the machine would refuse it") @@ -1370,7 +1370,7 @@ func caseAModuleMayNotDeclareAnAction(t *testing.T) { // provider of each name, so no mismatch was expressible and none was caught. func caseAModuleMayNotProvideAGenericDatabase(t *testing.T) { for _, hidden := range []string{"database", "db", "sql", "sql-database"} { - _, err := ParseManifest([]byte(`{"module":"postgres","version":"1", + _, err := ParseManifest([]byte(`{"module":"postgres","version":"1.0", "provides":[{"name":"` + hidden + `","scope":"mesh"}]}`)) if err == nil { t.Fatalf("providing %q was accepted; a requirement for it matches any engine", hidden) @@ -1411,7 +1411,7 @@ func caseAModuleOffersItsOwnStateOnly(t *testing.T) { // A module says what its own tools replace, and only for a tool it declares. func caseAModuleSaysWhatItsOwnToolsReplace(t *testing.T) { - m, err := ParseManifest([]byte(`{"module":"docker","version":"1","tools":["docker_logs","docker_list"],` + + m, err := ParseManifest([]byte(`{"module":"docker","version":"1.0","tools":["docker_logs","docker_list"],` + `"replaces":{"docker_logs":["docker logs"],"docker_list":["docker ps"]}}`)) if err != nil { t.Fatal(err) @@ -1420,15 +1420,15 @@ func caseAModuleSaysWhatItsOwnToolsReplace(t *testing.T) { t.Fatalf("replaces not read: %v", m.Replaces) } for _, c := range []struct{ manifest, says string }{ - {`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_lgos":["docker logs"]}}`, + {`{"module":"docker","version":"1.0","tools":["docker_logs"],"replaces":{"docker_lgos":["docker logs"]}}`, "declares no such tool"}, - {`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":[]}}`, "replaces nothing"}, - {`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":[" "]}}`, "empty command"}, - {`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":["docker logs\nrm -rf /"]}}`, + {`{"module":"docker","version":"1.0","tools":["docker_logs"],"replaces":{"docker_logs":[]}}`, "replaces nothing"}, + {`{"module":"docker","version":"1.0","tools":["docker_logs"],"replaces":{"docker_logs":[" "]}}`, "empty command"}, + {`{"module":"docker","version":"1.0","tools":["docker_logs"],"replaces":{"docker_logs":["docker logs\nrm -rf /"]}}`, "more than one line"}, - {`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":["docker logs","docker logs"]}}`, + {`{"module":"docker","version":"1.0","tools":["docker_logs"],"replaces":{"docker_logs":["docker logs","docker logs"]}}`, "twice"}, - {`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":["` + strings.Repeat("x", catalogue.MaxReplaced+1) + `"]}}`, + {`{"module":"docker","version":"1.0","tools":["docker_logs"],"replaces":{"docker_logs":["` + strings.Repeat("x", catalogue.MaxReplaced+1) + `"]}}`, "longer than"}, } { if _, err := ParseManifest([]byte(c.manifest)); err == nil || !strings.Contains(err.Error(), c.says) { @@ -1497,7 +1497,7 @@ func caseAModuleThatDeclaresASettingNamesItsFilesKeys(t *testing.T) { if problems := TakenSettingsProblems(m); len(problems) != 0 { t.Errorf("what a stored definition is read by refuses it: %v", problems) } - raw, err := json.Marshal(map[string]any{"module": m.Module, "version": "1", "resources": m.Resources, + raw, err := json.Marshal(map[string]any{"module": m.Module, "version": "1.0", "resources": m.Resources, "settings": map[string]any{"workers": map[string]any{"kind": catalogue.KindPreference, "default": 4, "why": "a number"}}}) if err != nil { t.Fatal(err) @@ -1553,13 +1553,13 @@ func caseAMountNothingDeclaresIsRefused(t *testing.T) { func caseAMovedModulesMarkIsJudged(t *testing.T) { for name, raw := range map[string]string{ - "marked moved and naming no template": `{"module":"bare","version":"1","moved":true, + "marked moved and naming no template": `{"module":"bare","version":"1.0","moved":true, "resources":[{"id":"conf","type":"file","path":"/etc/bare","content":"x"}]}`, - "handed written in a repository": `{"module":"bare","version":"1","moved":true, + "handed written in a repository": `{"module":"bare","version":"1.0","moved":true, "handed":{"source":"s","digest":"d","templates":{}}, "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, "resources":[{"id":"conf","type":"file","path":"/etc/bare","template":"files/bare"}]}`, - "handed and not moved": `{"module":"bare","version":"1", + "handed and not moved": `{"module":"bare","version":"1.0", "handed":{"source":"s","digest":"d","templates":{}}, "resources":[{"id":"conf","type":"file","path":"/etc/bare","content":"x"}]}`, } { @@ -1616,7 +1616,7 @@ func caseAnArtifactBuiltFromOutsideItsRepositoryIsRefused(t *testing.T) { // A build reads its own repository and nothing else. A path leaving it would make what gets // built depend on whatever happens to be on the machine doing the building. for _, from := range []string{"/etc/passwd", "../elsewhere"} { - _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[ + _, err := ParseManifest([]byte(`{"module":"a","version":"1.0","build":{"artifacts":[ {"name":"x","kind":"archive","from":"` + from + `"}]}}`)) if err == nil { t.Fatalf("%q was accepted as a build input", from) @@ -1632,7 +1632,7 @@ func caseAnArtifactBuiltFromOutsideItsRepositoryIsRefused(t *testing.T) { // refusal names the artifact and its kind. Each path stays inside the module's directory: no `..`, no absolute path, // no pattern. func caseAnArtifactNamesThePathsItIsBuiltFrom(t *testing.T) { - m, err := ParseManifest([]byte(`{"module":"mesh-sdk","version":"1","slug":"sdk","build":{"artifacts":[ + m, err := ParseManifest([]byte(`{"module":"mesh-sdk","version":"1.0","slug":"sdk","build":{"artifacts":[ {"name":"lib","kind":"package","language":"typescript", "paths":["src","test","package.json","package-lock.json","tsconfig.json"]}]},"resources":[]}`)) if err != nil { @@ -1641,7 +1641,7 @@ func caseAnArtifactNamesThePathsItIsBuiltFrom(t *testing.T) { if got := strings.Join(m.Build.Artifacts[0].Paths, ","); got != "src,test,package.json,package-lock.json,tsconfig.json" { t.Fatalf("the paths were not kept as written: %q", got) } - if _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[ + if _, err := ParseManifest([]byte(`{"module":"a","version":"1.0","build":{"artifacts":[ {"name":"tools","kind":"bundle","language":"typescript","entrypoints":["index.js"],"loads":["index.js"],"paths":["src","package.json"]}]}}`)); err != nil { t.Fatalf("a TypeScript bundle naming its paths was refused: %v", err) } @@ -1660,7 +1660,7 @@ func caseAnArtifactNamesThePathsItIsBuiltFrom(t *testing.T) { {`{"name":"x","kind":"package","language":"typescript","paths":["."]}`, "names the module's whole directory"}, {`{"name":"x","kind":"package","language":"typescript","paths":[]}`, "names no path"}, } { - _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[` + c.artifact + `]}}`)) + _, err := ParseManifest([]byte(`{"module":"a","version":"1.0","build":{"artifacts":[` + c.artifact + `]}}`)) if err == nil || !strings.Contains(err.Error(), c.says) { t.Errorf("%s: %v, wanted a refusal saying %q", c.artifact, err, c.says) } @@ -1668,7 +1668,7 @@ func caseAnArtifactNamesThePathsItIsBuiltFrom(t *testing.T) { } func caseAnArtifactOfAnUnknownKindIsRefused(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[ + _, err := ParseManifest([]byte(`{"module":"a","version":"1.0","build":{"artifacts":[ {"name":"x","kind":"binary","from":"main.go"}]}}`)) if err == nil { t.Fatal("an artifact of an unknown kind was accepted") @@ -1676,7 +1676,7 @@ func caseAnArtifactOfAnUnknownKindIsRefused(t *testing.T) { } func caseANeedIsAnAbsolutePath(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"postgres","version":"1", + _, err := ParseManifest([]byte(`{"module":"postgres","version":"1.0", "own-secrets":{"superuser":"superuser.txt"}}`)) if err == nil { t.Fatal("a relative path was accepted") @@ -1714,7 +1714,7 @@ func caseANeedMustNameANodeSeatSomewhereInTheCatalogue(t *testing.T) { func caseAnEmptyContributionIsRefused(t *testing.T) { // Either a mistake or a requirement written the long way round, and both are better said. - _, err := ParseManifest([]byte(`{"module":"board","version":"1", + _, err := ParseManifest([]byte(`{"module":"board","version":"1.0", "contributes":{"reverse-proxy":{}}}`)) if err == nil { t.Fatal("a module contributed nothing and was accepted") @@ -1804,7 +1804,7 @@ func caseAnInterpretedBundleStillMayNotNameASource(t *testing.T) { // And an ordinary unknown key is still refused as one, rather than being guessed at. func caseAnInventedKeyIsNotTreatedAsARename(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"postgres","version":"1","nonsense":{}}`)) + _, err := ParseManifest([]byte(`{"module":"postgres","version":"1.0","nonsense":{}}`)) if err == nil { t.Fatal("an invented key was accepted") } @@ -1816,7 +1816,7 @@ func caseAnInventedKeyIsNotTreatedAsARename(t *testing.T) { // An entry that names a module and no tool is refused at parse, in the manifest's words, rather than // at the composition of the bus's user list where it would stop the file for everybody. func caseAnInvokeThatNamesNoToolIsRefusedAtParse(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"mesh-console","version":"1","invokes":["shop"]}`)) + _, err := ParseManifest([]byte(`{"module":"mesh-console","version":"1.0","invokes":["shop"]}`)) if err == nil { t.Fatal("an invoke naming no tool was accepted") } @@ -1829,7 +1829,7 @@ func caseAnInvokeThatNamesNoToolIsRefusedAtParse(t *testing.T) { // secret says how it is taken, and a consumer's need carries the name so the plan mints its copy // from the provider's value. func caseAnOfferMayNameAnOwnSecretAsItsCredential(t *testing.T) { - m, err := ParseManifest([]byte(`{"module":"downloader","version":"1", + m, err := ParseManifest([]byte(`{"module":"downloader","version":"1.0", "own-secrets":{"password":{"path":"/var/lib/mesh/downloader/password","taken":"at-start"}}, "provides":[{"name":"downloader-api","credential":{"own":"password"}}], "serves":{"downloader-api":{"port":8080,"username":"admin"}}}`)) @@ -1844,10 +1844,10 @@ func caseAnOfferMayNameAnOwnSecretAsItsCredential(t *testing.T) { } for want, raw := range map[string]string{ - "declares no such secret": `{"module":"d","version":"1","provides":[{"name":"d-api","credential":{"own":"password"}}]}`, - "must say how the module takes it": `{"module":"d","version":"1","own-secrets":{"password":"/p"}, + "declares no such secret": `{"module":"d","version":"1.0","provides":[{"name":"d-api","credential":{"own":"password"}}]}`, + "must say how the module takes it": `{"module":"d","version":"1.0","own-secrets":{"password":"/p"}, "provides":[{"name":"d-api","credential":{"own":"password"}}]}`, - "names no own secret": `{"module":"d","version":"1","provides":[{"name":"d-api","credential":{"own":""}}]}`, + "names no own secret": `{"module":"d","version":"1.0","provides":[{"name":"d-api","credential":{"own":""}}]}`, } { if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("expected a refusal saying %q, got %v", want, err) @@ -1859,7 +1859,7 @@ func caseAnOfferMayNameAnOwnSecretAsItsCredential(t *testing.T) { // the offer says `issued-by: outside` and names neither an own secret nor a tool, and the parser refuses it beside // either. An offer that says `software` keeps ADR 0305's shape. func caseAnOfferMaySayAPersonOutsideTheMeshIssuesItsCredential(t *testing.T) { - m, err := ParseManifest([]byte(`{"module":"tv","version":"1", + m, err := ParseManifest([]byte(`{"module":"tv","version":"1.0", "provides":[{"name":"tv-api","scope":"mesh","credential":{"issued-by":"outside"}}], "serves":{"tv-api":{"port":8989}}}`)) if err != nil { @@ -1879,11 +1879,11 @@ func caseAnOfferMaySayAPersonOutsideTheMeshIssuesItsCredential(t *testing.T) { } for want, raw := range map[string]string{ - "names its own secret": `{"module":"d","version":"1","own-secrets":{"key":{"path":"/p","taken":"at-start"}}, + "names its own secret": `{"module":"d","version":"1.0","own-secrets":{"key":{"path":"/p","taken":"at-start"}}, "provides":[{"name":"d-api","credential":{"issued-by":"outside","own":"key"}}]}`, - "names the tool": `{"module":"d","version":"1", + "names the tool": `{"module":"d","version":"1.0", "provides":[{"name":"d-api","credential":{"issued-by":"outside","tool":"d_key"}}]}`, - "the issuers an offer may name": `{"module":"d","version":"1", + "the issuers an offer may name": `{"module":"d","version":"1.0", "provides":[{"name":"d-api","credential":{"issued-by":"a-person"}}]}`, } { if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) { @@ -1895,7 +1895,7 @@ func caseAnOfferMaySayAPersonOutsideTheMeshIssuesItsCredential(t *testing.T) { // A provision's credential may be issued by its provider's software (novox/hq ADR 0305, issue 526): the offer // names the tool that answers it and no own secret, and every other shape is refused at parse. func caseAnOfferMaySayItsSoftwareIssuesTheCredential(t *testing.T) { - m, err := ParseManifest([]byte(`{"module":"media","version":"1", + m, err := ParseManifest([]byte(`{"module":"media","version":"1.0", "provides":[{"name":"media-api","scope":"mesh","credential":{"issued-by":"software","tool":"media_issued_credential"}}], "serves":{"media-api":{"port":32400}}}`)) if err != nil { @@ -1912,13 +1912,13 @@ func caseAnOfferMaySayItsSoftwareIssuesTheCredential(t *testing.T) { } for want, raw := range map[string]string{ - "the issuers an offer may name": `{"module":"d","version":"1", + "the issuers an offer may name": `{"module":"d","version":"1.0", "provides":[{"name":"d-api","credential":{"issued-by":"vendor","tool":"d_token"}}]}`, - "names no tool that answers it": `{"module":"d","version":"1", + "names no tool that answers it": `{"module":"d","version":"1.0", "provides":[{"name":"d-api","credential":{"issued-by":"software"}}]}`, - "names its own secret": `{"module":"d","version":"1","own-secrets":{"token":{"path":"/p","taken":"at-start"}}, + "names its own secret": `{"module":"d","version":"1.0","own-secrets":{"token":{"path":"/p","taken":"at-start"}}, "provides":[{"name":"d-api","credential":{"issued-by":"software","tool":"d_token","own":"token"}}]}`, - "the issuers an offer may name ": `{"module":"d","version":"1", + "the issuers an offer may name ": `{"module":"d","version":"1.0", "provides":[{"name":"d-api","credential":{"tool":"d_token"}}]}`, } { want = strings.TrimSpace(want) @@ -2001,7 +2001,7 @@ func caseAnOperatorNamedValueAFileFallsBackOnMayHaveADefault(t *testing.T) { // A key asked only in a template is not refused while the manifest still names its templates (issue 817), and // is judged once they are filled: WithTemplatesFrom over a tree whose template does not ask for it refuses it. func caseAnOwnerSettingAskedOnlyInATemplateIsJudgedOnceFilled(t *testing.T) { - raw := `{"module":"sonarr","version":"1", + raw := `{"module":"sonarr","version":"1.0", "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, "resources":[{"id":"recycle-bin","type":"directory","path":"/srv/sonarr/recycle","mode":"0755","owner":"${setting:puid}:${setting:pgid}"}, {"id":"identity","type":"file","path":"/srv/sonarr/identity.env","mode":"0644","template":"files/identity.env"}]}` @@ -2077,7 +2077,7 @@ func caseAnOwnerSettingIsJudgedWhereTheManifestIsRead(t *testing.T) { // A secret an outside party issues says so (novox/hq ADR 0228), is written back as it was read, and // is the one at-start secret the mesh may not make; any other word for who issued it is refused. func caseAnOwnSecretSaysWhenAnOutsidePartyIssuesIt(t *testing.T) { - m, err := ParseManifest([]byte(`{"module":"letta","version":"1","own-secrets":{ + m, err := ParseManifest([]byte(`{"module":"letta","version":"1.0","own-secrets":{ "server-password":{"path":"/var/lib/letta/server-password","taken":"at-start"}, "openai-api-key":{"path":"/var/lib/letta/openai-api-key","taken":"at-start","issued-by":"outside"}, "telegram-token":{"path":"/var/lib/letta/telegram-token","issued-by":"outside"}, @@ -2106,7 +2106,7 @@ func caseAnOwnSecretSaysWhenAnOutsidePartyIssuesIt(t *testing.T) { if strings.Contains(string(out), `"taken":""`) { t.Fatalf("a secret that says only who issued it gained an empty taken: %s", out) } - _, err = ParseManifest([]byte(`{"module":"letta","version":"1","own-secrets":{ + _, err = ParseManifest([]byte(`{"module":"letta","version":"1.0","own-secrets":{ "openai-api-key":{"path":"/var/lib/letta/openai-api-key","issued-by":"openai"}}}`)) if err == nil || !strings.Contains(err.Error(), `issued by "openai"`) { t.Fatalf("an unknown word for who issued a secret was accepted: %v", err) @@ -2121,7 +2121,7 @@ func caseAnOwnSecretsOwnerIsRefusedWhenItIsNotAnAccountTheModuleMakes(t *testing "the operator account": {"${machine:account}", "operator account"}, "an account not declared": {"not-made", "not-made"}, } { - _, err := ParseManifest([]byte(`{"module":"mesh-mail","version":"1","own-secrets":{` + + _, err := ParseManifest([]byte(`{"module":"mesh-mail","version":"1.0","own-secrets":{` + `"mail-credential-*":{"path":"/var/lib/mesh-mail/mail-credential-*.secret","issued-by":"outside","owner":"` + c.owner + `"}},"resources":[{"id":"account","type":"user","name":"mesh-mail-send"}]}`)) if err == nil || !strings.Contains(err.Error(), "mail-credential-*") || !strings.Contains(err.Error(), c.says) { @@ -2131,12 +2131,12 @@ func caseAnOwnSecretsOwnerIsRefusedWhenItIsNotAnAccountTheModuleMakes(t *testing } func caseAnOwnSecretTakenSomeOtherWayIsRefused(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"idp","version":"1","own-secrets":{ + _, err := ParseManifest([]byte(`{"module":"idp","version":"1.0","own-secrets":{ "admin":{"path":"/var/lib/idp/admin.secret","taken":"sometimes"}}}`)) if err == nil || !strings.Contains(err.Error(), `taken "sometimes"`) { t.Fatalf("an unknown word for how a secret is taken was accepted: %v", err) } - _, err = ParseManifest([]byte(`{"module":"idp","version":"1","own-secrets":{ + _, err = ParseManifest([]byte(`{"module":"idp","version":"1.0","own-secrets":{ "admin":{"path":"/var/lib/idp/admin.secret","rotate":"yes"}}}`)) if err == nil { t.Fatal("an unknown field on an own secret was accepted") @@ -2161,7 +2161,7 @@ func caseAnUncleanSpellingOfASeatsPathIsTheSeatsPath(t *testing.T) { } func caseAnUnhealthySeverityOtherThanUrgentOrWarningIsRefused(t *testing.T) { - if _, err := ParseManifest([]byte(`{"module":"mail","version":"1","unhealthy":{"severity":"loud","why":"mail stops"}}`)); err == nil || + if _, err := ParseManifest([]byte(`{"module":"mail","version":"1.0","unhealthy":{"severity":"loud","why":"mail stops"}}`)); err == nil || !strings.Contains(err.Error(), `not "loud"`) { t.Errorf("a manifest declaring its unhealthy loud parsed: %v", err) } @@ -2173,7 +2173,7 @@ func caseAnUnhealthyWithoutWhyIsRefused(t *testing.T) { t.Errorf("urgent with why %q was accepted", why) } } - if _, err := ParseManifest([]byte(`{"module":"mail","version":"1","unhealthy":{"severity":"urgent"}}`)); err == nil || + if _, err := ParseManifest([]byte(`{"module":"mail","version":"1.0","unhealthy":{"severity":"urgent"}}`)); err == nil || !strings.Contains(err.Error(), "unhealthy says why") { t.Errorf("a manifest declaring its unhealthy urgent without why parsed: %v", err) } @@ -2182,7 +2182,7 @@ func caseAnUnhealthyWithoutWhyIsRefused(t *testing.T) { // A reference to an access the definition does not declare is refused where the author is. func caseAnUnknownAccessReferenceIsRefusedAtParse(t *testing.T) { _, err := ParseManifest([]byte(`{ - "module": "arr", "version": "1", + "module": "arr", "version":"1.0", "accesses": [{"id": "series", "mode": "read-write"}], "resources": [ {"id": "state", "type": "directory", "place": ".", "mode": "0700"}, @@ -2192,7 +2192,7 @@ func caseAnUnknownAccessReferenceIsRefusedAtParse(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "${access:movies}") { t.Fatalf("a reference to an undeclared access was accepted: %v", err) } - _, err = ParseManifest([]byte(`{"module": "arr", "version": "1", "accesses": [{"mode": "read"}]}`)) + _, err = ParseManifest([]byte(`{"module": "arr", "version":"1.0", "accesses": [{"mode": "read"}]}`)) if err == nil || !strings.Contains(err.Error(), "neither an id nor a path") { t.Fatalf("an access with no id and no path was accepted: %v", err) } @@ -2237,7 +2237,7 @@ func caseAnUpstreamFileFromAnAddressOrAReservedNameIsRefused(t *testing.T) { for _, u := range []string{"https://git.novox.internal/r.tar.xz", "https://10.1.2.3/r.tar.xz", "https://203.0.113.9/r.tar.xz", "https://[2001:db8::1]/r.tar.xz", "https://nas.lan/r.tar.xz", "https://vendor/r.tar.xz"} { _, err := ParseManifest([]byte(upstreamManifest(`{"name":"x","kind":"archive","url":"` + u + `","sha256":"` + - pinnedSum + `","version":"1"}`))) + pinnedSum + `","version":"1.0"}`))) if err == nil || !strings.Contains(err.Error(), "published under a public name") { t.Errorf("%s: %v", u, err) } @@ -2272,27 +2272,27 @@ func caseAnUpstreamFilesVendorIsNamedOnPurposeAndAnInstallationsIsRefused(t *tes func caseAnUpstreamFileThatIsNotPinnedIsRefused(t *testing.T) { cases := map[string]string{ // No digest: what the address serves changes under the mesh. - `{"name":"x","kind":"archive","url":"https://vendor.invalid/r.tar.xz","version":"1"}`: "without a pinned sha256", - `{"name":"x","kind":"archive","url":"https://vendor.invalid/r.tar.xz","sha256":"ABC","version":"1"}`: "without a pinned sha256", + `{"name":"x","kind":"archive","url":"https://vendor.invalid/r.tar.xz","version":"1.0"}`: "without a pinned sha256", + `{"name":"x","kind":"archive","url":"https://vendor.invalid/r.tar.xz","sha256":"ABC","version":"1.0"}`: "without a pinned sha256", // No version: a person reading the pin cannot tell which release. `{"name":"x","kind":"archive","url":"https://vendor.invalid/r.tar.xz","sha256":"` + pinnedSum + `"}`: "says no version", // Not https. - `{"name":"x","kind":"archive","url":"http://vendor.invalid/r.tar.xz","sha256":"` + pinnedSum + `","version":"1"}`: "over https", + `{"name":"x","kind":"archive","url":"http://vendor.invalid/r.tar.xz","sha256":"` + pinnedSum + `","version":"1.0"}`: "over https", // Both a directory of the repository and a URL. `{"name":"x","kind":"archive","from":"files","url":"https://vendor.invalid/r.tar.xz","sha256":"` + pinnedSum + - `","version":"1"}`: "one or the other", + `","version":"1.0"}`: "one or the other", // A pin without a URL. `{"name":"x","kind":"archive","from":"files","sha256":"` + pinnedSum + `"}`: "no url", // Only an archive is mirrored from a file. `{"name":"x","kind":"image","from":"Dockerfile","url":"https://vendor.invalid/r.tar.xz","sha256":"` + pinnedSum + - `","version":"1"}`: "only an archive", + `","version":"1.0"}`: "only an archive", // A file name that is a path, a directory within that leaves the archive, and both at once. `{"name":"x","kind":"archive","url":"https://vendor.invalid/a.xpi","sha256":"` + pinnedSum + - `","version":"1","file":"../a.xpi"}`: "not a plain relative file name", + `","version":"1.0","file":"../a.xpi"}`: "not a plain relative file name", `{"name":"x","kind":"archive","url":"https://vendor.invalid/r.tar.xz","sha256":"` + pinnedSum + - `","version":"1","within":"../up"}`: "not a plain relative directory", + `","version":"1.0","within":"../up"}`: "not a plain relative directory", `{"name":"x","kind":"archive","url":"https://vendor.invalid/r.tar.xz","sha256":"` + pinnedSum + - `","version":"1","within":"r","file":"r.tar.xz"}`: "a file is kept whole", + `","version":"1.0","within":"r","file":"r.tar.xz"}`: "a file is kept whole", } for artifact, want := range cases { _, err := ParseManifest([]byte(upstreamManifest(artifact))) @@ -2308,7 +2308,7 @@ func caseAnUpstreamFileThatIsNotPinnedIsRefused(t *testing.T) { func caseAPackageParsesLikeABundleAndNeedsALanguage(t *testing.T) { // The SDK's shape: a package built from the module's own directory, naming a language. - m, err := ParseManifest([]byte(`{"module":"mesh-sdk","version":"1","slug":"sdk", + m, err := ParseManifest([]byte(`{"module":"mesh-sdk","version":"1.0","slug":"sdk", "build":{"artifacts":[{"name":"lib","kind":"package","language":"typescript"}]}, "resources":[]}`)) if err != nil { @@ -2320,12 +2320,12 @@ func caseAPackageParsesLikeABundleAndNeedsALanguage(t *testing.T) { // A package that names what it is built from is refused, exactly as a bundle is: it is built // from the module's own directory. - if _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[ + if _, err := ParseManifest([]byte(`{"module":"a","version":"1.0","build":{"artifacts":[ {"name":"lib","kind":"package","language":"typescript","from":"Dockerfile"}]}}`)); err == nil { t.Fatal("a package naming a source was accepted") } // A package with no language cannot choose a toolchain. - if _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[ + if _, err := ParseManifest([]byte(`{"module":"a","version":"1.0","build":{"artifacts":[ {"name":"lib","kind":"package"}]}}`)); err == nil { t.Fatal("a package with no language was accepted") } @@ -2346,9 +2346,9 @@ func caseAPackageParsesLikeABundleAndNeedsALanguage(t *testing.T) { {`{"name":"go","kind":"package","language":"go","paths":["go"],"generated-from":"."}` + claims, `a contracts module, which holds contracts only and claims no seat; it claims mesh-vault`}, } { - raw := `{"module":"a","version":"1","build":{"artifacts":[` + c.artifact + `]}}` + raw := `{"module":"a","version":"1.0","build":{"artifacts":[` + c.artifact + `]}}` if before, ok := strings.CutSuffix(c.artifact, claims); ok { - raw = `{"module":"a","version":"1","claims":[{"name":"mesh-vault","scope":"mesh"}],"build":{"artifacts":[` + before + `]}}` + raw = `{"module":"a","version":"1.0","claims":[{"name":"mesh-vault","scope":"mesh"}],"build":{"artifacts":[` + before + `]}}` } _, err := ParseManifest([]byte(raw)) switch { @@ -2362,7 +2362,7 @@ func caseAPackageParsesLikeABundleAndNeedsALanguage(t *testing.T) { func caseAPathThroughAnAccessIsJudgedWithTheAccessFilledIn(t *testing.T) { // A file's path may name an access; the access's default path is the definition's, so it is judged with it. - _, err := ParseManifest([]byte(`{"module": "backup", "version": "1", + _, err := ParseManifest([]byte(`{"module": "backup", "version":"1.0", "accesses": [{"id": "images", "path": "/var/lib/docker/volumes"}], "resources": [{"id": "marker", "type": "file", "path": "${access:images}/marker", "content": "x"}]}`)) if err == nil || !strings.Contains(err.Error(), "/var/lib/docker/volumes/marker") || @@ -2371,7 +2371,7 @@ func caseAPathThroughAnAccessIsJudgedWithTheAccessFilledIn(t *testing.T) { } // An access the definition gives no path is placed by a setting, which Places and AccessPlaces judge, and on // the machine by the node-engine: nothing here to resolve it against, so nothing is refused for it. - if _, err := ParseManifest([]byte(`{"module": "backup", "version": "1", + if _, err := ParseManifest([]byte(`{"module": "backup", "version":"1.0", "accesses": [{"id": "images"}], "resources": [{"id": "marker", "type": "file", "path": "${access:images}/marker", "content": "x"}]}`)); err != nil { t.Fatalf("an access placed only by a setting cannot be judged at the definition: %v", err) @@ -2427,7 +2427,7 @@ func caseAProcessBusAccountMustBeTheModulesOwn(t *testing.T) { "user root": {"root", "root"}, "the operator account": {"${machine:account}", "operator"}, } { - raw := `{"module":"mesh-mail","version":"1","emits":["mail.send.sent"],"state":["sends"],` + + raw := `{"module":"mesh-mail","version":"1.0","emits":["mail.send.sent"],"state":["sends"],` + `"streams":[{"name":"mail-sends","events":["mail.send.approved"],"retention":"work-queue",` + `"max-deliveries":5}],"resources":[{"id":"account","type":"user","name":"mesh-mail-send"},` + `{"id":"send","type":"process","run":["./mesh-mail","send"]` @@ -2499,7 +2499,7 @@ func caseAProcessDeclaresItsBusAndIsAccepted(t *testing.T) { // process's, named after its binary and kept through resolution; a removal is from the closed set, alone, and the // bus module's (mesh-broker) alone. toolsIn := func(claims, bus, mode, where string) string { - return `{"module":"nats","version":"1","claims":` + claims + `,"own-secrets":{"broker":"${dir:mesh-state}/broker",` + + return `{"module":"nats","version":"1.0","claims":` + claims + `,"own-secrets":{"broker":"${dir:mesh-state}/broker",` + `"broker-nats-tools":"${dir:tools-secret}/broker-nats-tools"},"resources":[{"id":"mesh-state","type":"directory",` + `"mode":"0700","place":"mesh","unknown":"program"},{"id":"tools-secret","type":"directory","mode":"` + mode + `","path":"` + where + `","unknown":"program"}],"build":{"artifacts":[{"name":"tools","kind":"bundle",` + @@ -2553,7 +2553,7 @@ func caseAProcessDeclaresItsBusAndIsAccepted(t *testing.T) { // {member} is replaced only for a family's member: a single secret's prompt may not hold it (novox/hq issue 789). func caseAPromptHoldingMemberOnASingleSecretIsRefused(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"mesh-mail","version":"1","own-secrets":{ + _, err := ParseManifest([]byte(`{"module":"mesh-mail","version":"1.0","own-secrets":{ "smtp-relay-key":{"path":"/var/lib/mesh-mail/smtp-relay-key","taken":"at-start", "prompt":"For {member}"}}}`)) if err == nil || !strings.Contains(err.Error(), "{member} is replaced only for a family's member") { @@ -2625,7 +2625,7 @@ func caseARenamedEntryIsRefusedWhereItCouldNotHold(t *testing.T) { func caseARenamedEntryIsTheConfigFormsOnly(t *testing.T) { m := map[string]any{ - "module": "notifier", "version": "1", + "module": "notifier", "version": "1.0", "resources": []any{ map[string]any{"id": "rules", "type": "file", "path": "/x/rules.json", "merge": "json", "content": "{}", "takes-settings": []any{map[string]any{"key": "url", "as": "NOTIFIER_URL"}}}, @@ -2657,7 +2657,7 @@ func caseARequirementOnAKeylessProvisionComposesWithALongName(t *testing.T) { // as naming an installation. Archive resources are the same kind of reference and get the same. func caseAResolvedReferenceIsKeptNotRouted(t *testing.T) { m, err := ParseManifest([]byte(`{ - "module": "sample", "version": "1", + "module": "sample", "version":"1.0", "tools": ["one"], "build": {"artifacts": [ {"name": "code", "kind": "bundle", "language": "typescript", "entrypoints": ["tools/index.js"]}, @@ -2882,7 +2882,7 @@ func caseASiteScopedProvisionIsRefused(t *testing.T) { // Meaningful for a claim — one DHCP server per segment — and not yet meaningful for a // provision, because nothing knows how to reach "the one at my site". _, err := ParseManifest([]byte( - `{"module":"dns","version":"1","provides":[{"name":"resolver","scope":"site"}]}`)) + `{"module":"dns","version":"1.0","provides":[{"name":"resolver","scope":"site"}]}`)) if err == nil { t.Fatal("a site-scoped provision was accepted") } @@ -2918,18 +2918,18 @@ func caseASourceNobodyDefinedIsRefused(t *testing.T) { // registration in the manifest's words. func caseAStateNameIsLocalAndOneToken(t *testing.T) { for _, c := range []struct{ manifest, says string }{ - {`{"module":"a","version":"1","state":["mesh.servers"]}`, `keeps state "mesh.servers": a state is named locally`}, - {`{"module":"a","version":"1","state":["my_servers"]}`, `keeps state "my_servers"`}, - {`{"module":"a","version":"1","state":["s","s"]}`, `keeps state "s" twice`}, - {`{"module":"a","version":"1","state":[{"name":"s","history":65}]}`, `a key keeps 0 or 1 to 64 (0 is taken as 1)`}, - {`{"module":"a","version":"1","state":[{"name":"s","history":0}]}`, ``}, // 0 passes, as before (ADR 0579 §1) + {`{"module":"a","version":"1.0","state":["mesh.servers"]}`, `keeps state "mesh.servers": a state is named locally`}, + {`{"module":"a","version":"1.0","state":["my_servers"]}`, `keeps state "my_servers"`}, + {`{"module":"a","version":"1.0","state":["s","s"]}`, `keeps state "s" twice`}, + {`{"module":"a","version":"1.0","state":[{"name":"s","history":65}]}`, `a key keeps 0 or 1 to 64 (0 is taken as 1)`}, + {`{"module":"a","version":"1.0","state":[{"name":"s","history":0}]}`, ``}, // 0 passes, as before (ADR 0579 §1) // 1966/AC9: one name, one form - {`{"module":"a","version":"1","state":["x"],"requires":[{"name":"x","storage":"key-value","template":"state"}]}`, + {`{"module":"a","version":"1.0","state":["x"],"requires":[{"name":"x","storage":"key-value","template":"state"}]}`, `a's bus state "x": the name is also kept in the old section state`}, - {`{"module":"a.b","version":"1","state":["s"]}`, `no dot`}, - {`{"module":"a","version":"1","reads":["bindings"]}`, `a read is .`}, - {`{"module":"a","version":"1","reads":["a.s"]}`, `which is its own state`}, - {`{"module":"a","version":"1","state":[{"name":"s","shared":true}]}`, `{name, history, ttl-seconds, per-machine, key-ttl, read-only, form, catch-up}`}, + {`{"module":"a.b","version":"1.0","state":["s"]}`, `no dot`}, + {`{"module":"a","version":"1.0","reads":["bindings"]}`, `a read is .`}, + {`{"module":"a","version":"1.0","reads":["a.s"]}`, `which is its own state`}, + {`{"module":"a","version":"1.0","state":[{"name":"s","shared":true}]}`, `{name, history, ttl-seconds, per-machine, key-ttl, read-only, form, catch-up}`}, } { _, err := ParseManifest([]byte(c.manifest)) if c.says == "" { @@ -3014,7 +3014,7 @@ func caseAStreamIsNamedBoundedAndGrantedByATrustedConfigValue(t *testing.T) { {mailManifest(t, `[{"name":"mail","events":["mail.**"],"grant":{"config":"grants","events":"{key}.{key}"}}]`, ""), `exactly once`}, {mailManifest(t, `[{"name":"mail","events":["mail.**"],`+grants+`,"why":1}]`, ""), `{name, events`}, - {`{"module":"a.b","version":"1","streams":[{"name":"x","events":["x"]}]}`, `no dot`}, + {`{"module":"a.b","version":"1.0","streams":[{"name":"x","events":["x"]}]}`, `no dot`}, } { _, err := ParseManifest([]byte(c.manifest)) if err == nil { @@ -3091,7 +3091,7 @@ func caseATemplatesDirectoryNoArchiveIsBuiltFromIsRefused(t *testing.T) { } func caseATemplatesDirectoryOnAModuleNamingNoTemplateIsRefused(t *testing.T) { - raw := strings.Replace(unpackingInline, `"version":"1",`, `"version":"1","templates":"templates",`, 1) + raw := strings.Replace(unpackingInline, `"version":"1.0",`, `"version":"1.0","templates":"templates",`, 1) if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "names no template") { t.Fatalf("a templates directory read by nothing was not refused: %v", err) } @@ -3135,7 +3135,7 @@ func caseATrustedHolderMustRunAsAnAccountOfItsOwn(t *testing.T) { // A misspelt key inside a verb's definition is refused, like one anywhere else in the manifest. func caseAVerbWithAnUnknownKeyIsRefused(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"till","version":"1",` + + _, err := ParseManifest([]byte(`{"module":"till","version":"1.0",` + `"seats":[{"name":"shop-till","serves":[{"name":"price","descripton":"typo"}]}]}`)) if err == nil || !strings.Contains(err.Error(), "descripton") { t.Fatalf("a verb with a misspelt key was accepted: %v", err) @@ -3146,16 +3146,16 @@ func caseAVerbWithAnUnknownKeyIsRefused(t *testing.T) { // each refusal is said in the manifest's words, at parse, rather than at the composition of the bus's users. func caseAWaitIsRefusedUnlessItNamesOneFamilyOfTheToolRunner(t *testing.T) { for manifest, want := range map[string]string{ - `{"module":"shop","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only the tool runner's module", - `{"module":"node-tools","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only the tool runner's module", // its name says nothing (issue 774) - `{"module":"node-tools","version":"1","tool-runner":true,"waits":["mesh-knowledge.*"]}`: "never a wildcard", - `{"module":"node-tools","version":"1","tool-runner":true,"waits":["**"]}`: "never a wildcard", - `{"module":"node-tools","version":"1","tool-runner":true,"waits":["mesh-knowledge.>"]}`: "never a wildcard", - `{"module":"node-tools","version":"1","tool-runner":true,"waits":["*.reviewed"]}`: "never a wildcard", - `{"module":"node-tools","version":"1","tool-runner":true,"waits":["reviewed"]}`: "does not name an emitter and an event", - `{"module":"node-tools","version":"1","tool-runner":true,"waits":["mesh-knowledge..reviewed"]}`: "does not name an emitter and an event", - `{"module":"node-tools","version":"1","tool-runner":true,"waits":["Mesh-Knowledge.reviewed x"]}`: "does not name an emitter and an event", - `{"module":"node-tools","version":"1","tool-runner":true,"waits":["a.b","a.b"]}`: "twice", + `{"module":"shop","version":"1.0","waits":["mesh-knowledge.reviewed"]}`: "only the tool runner's module", + `{"module":"node-tools","version":"1.0","waits":["mesh-knowledge.reviewed"]}`: "only the tool runner's module", // its name says nothing (issue 774) + `{"module":"node-tools","version":"1.0","tool-runner":true,"waits":["mesh-knowledge.*"]}`: "never a wildcard", + `{"module":"node-tools","version":"1.0","tool-runner":true,"waits":["**"]}`: "never a wildcard", + `{"module":"node-tools","version":"1.0","tool-runner":true,"waits":["mesh-knowledge.>"]}`: "never a wildcard", + `{"module":"node-tools","version":"1.0","tool-runner":true,"waits":["*.reviewed"]}`: "never a wildcard", + `{"module":"node-tools","version":"1.0","tool-runner":true,"waits":["reviewed"]}`: "does not name an emitter and an event", + `{"module":"node-tools","version":"1.0","tool-runner":true,"waits":["mesh-knowledge..reviewed"]}`: "does not name an emitter and an event", + `{"module":"node-tools","version":"1.0","tool-runner":true,"waits":["Mesh-Knowledge.reviewed x"]}`: "does not name an emitter and an event", + `{"module":"node-tools","version":"1.0","tool-runner":true,"waits":["a.b","a.b"]}`: "twice", } { _, err := ParseManifest([]byte(manifest)) if err == nil { @@ -3230,13 +3230,13 @@ func caseAWorkQueueStreamsValuesAreBounded(t *testing.T) { // A zone names the listen that answers it, or there is nothing to forward to (novox/hq ADR 0199). func caseAZoneMustNameOneOfTheModulesListens(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"lab","version":"1", + _, err := ParseManifest([]byte(`{"module":"lab","version":"1.0", "listens":[{"name":"dns","port":5353,"protocol":"udp","from":"mesh","why":"the lab's names"}], "zone":{"name":"${setting:zone}","listen":"web"}}`)) if err == nil || !strings.Contains(err.Error(), `has no listen of that name`) { t.Fatalf("a zone answered by a listen the module does not have was accepted: %v", err) } - if _, err := ParseManifest([]byte(`{"module":"lab","version":"1", + if _, err := ParseManifest([]byte(`{"module":"lab","version":"1.0", "listens":[{"name":"dns","port":5353,"protocol":"udp","from":"mesh","why":"the lab's names"}], "zone":{"name":"${setting:zone}","listen":"dns"}}`)); err != nil { t.Fatalf("a well-formed zone was refused: %v", err) @@ -3244,7 +3244,7 @@ func caseAZoneMustNameOneOfTheModulesListens(t *testing.T) { } func caseBindingWhatYouDoNotRequireIsRefused(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"app","version":"1", + _, err := ParseManifest([]byte(`{"module":"app","version":"1.0", "binds":{"postgres-database":"/etc/app/db.json"}}`)) if err == nil { t.Fatal("a module was told about something it never asked for") @@ -3299,27 +3299,27 @@ func caseCodeForAPowerMomentDependsOnThePowerSeat(t *testing.T) { func caseCollectionsAndItemsAreJudged(t *testing.T) { for name, c := range map[string]struct{ raw, says string }{ - "an item of a module not moved": {`{"module":"m","version":"1", + "an item of a module not moved": {`{"module":"m","version":"1.0", "items":[{"collection":"shell:zsh:normal","template":"files/x"}], "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}`, "is marked moved"}, - "two items to one collection": {`{"module":"m","version":"1","moved":true, + "two items to one collection": {`{"module":"m","version":"1.0","moved":true, "items":[{"collection":"a","template":"files/x"},{"collection":"a","template":"files/y"}], "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}`, "contributes to a twice"}, - "an item to no slot": {`{"module":"m","version":"1","moved":true, + "an item to no slot": {`{"module":"m","version":"1.0","moved":true, "items":[{"collection":"shell:zsh:middle","template":"files/x"}], "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}`, "is no slot"}, - "an item outside the templates": {`{"module":"m","version":"1","moved":true, + "an item outside the templates": {`{"module":"m","version":"1.0","moved":true, "items":[{"collection":"a","template":"x"}], "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}`, "is not a file under files/"}, - "an item naming a secret": {`{"module":"m","version":"1", + "an item naming a secret": {`{"module":"m","version":"1.0", "items":[{"collection":"a","content":"t=${secret:token}"}]}`, "where no secret is"}, - "a collection not declared": {`{"module":"m","version":"1", + "a collection not declared": {`{"module":"m","version":"1.0", "resources":[{"id":"f","type":"file","path":"/f","content":"${collected:a}"}]}`, "does not collect"}, - "an old namespace declared": {`{"module":"m","version":"1","collections":[{"name":"shell:zsh:normal"}], + "an old namespace declared": {`{"module":"m","version":"1.0","collections":[{"name":"shell:zsh:normal"}], "resources":[{"id":"f","type":"file","path":"/f","content":"x"}]}`, "read from its placeholder"}, - "a collection placed nowhere": {`{"module":"m","version":"1","collections":[{"name":"a"}], + "a collection placed nowhere": {`{"module":"m","version":"1.0","collections":[{"name":"a"}], "resources":[{"id":"f","type":"file","path":"/f","content":"x"}]}`, "no file of its places it"}, - "a collection in a path": {`{"module":"m","version":"1","collections":[{"name":"a"}], + "a collection in a path": {`{"module":"m","version":"1.0","collections":[{"name":"a"}], "resources":[{"id":"f","type":"file","path":"/${collected:a}","content":"${collected:a}"}]}`, "only in a file's content"}, } { @@ -3523,7 +3523,7 @@ func caseConfigRefusesAValueWithoutTypeOrDescription(t *testing.T) { // 1658/AC2: a declared value placed nowhere, with an item that does not say it, is still refused. func caseConfigStillRefusesAValueNoItemPlaces(t *testing.T) { m := map[string]any{ - "module": "m", "version": "1", + "module": "m", "version": "1.0", "config": map[string]any{ "k": map[string]any{"type": "string", "description": "a word nothing says", "default": "x"}, }, @@ -3535,7 +3535,7 @@ func caseConfigStillRefusesAValueNoItemPlaces(t *testing.T) { // Consumer data said to live in a provision the module requires is protected only as well as that // provision's provider protects its consumers' data. func caseConsumerDataInARequiredProvisionIsJudgedAgainstItsProvider(t *testing.T) { - idp := mustParse(t, `{"module":"idp","version":"1","requires":["postgres-database"], + idp := mustParse(t, `{"module":"idp","version":"1.0","requires":["postgres-database"], "provides":[{"name":"oidc-client","scope":"mesh"}],"grants":{"oidc-client":"${dir:g}"}, "resources":[{"id":"g","type":"directory","mode":"0700"}], "data":{"consumers":{"oidc-client":{"class":"irreplaceable","in":"postgres-database"}}}}`) @@ -3807,10 +3807,10 @@ func caseIfACapabilityNamesOneTheNodeEngineDetectsOnAnOfferedKindOnly(t *testing // Data a consumer keeps with a provider as irreplaceable must be protected there: the check refuses a // provider that keeps its consumers' data as a cache, or in an item with no protection. func caseIrreplaceableDataKeptWithAProviderMustBeProtectedThere(t *testing.T) { - photos := mustParse(t, `{"module":"photos","version":"1","requires":["s3-bucket"], + photos := mustParse(t, `{"module":"photos","version":"1.0","requires":["s3-bucket"], "data":{"kept-by":{"s3-bucket":{"class":"irreplaceable"}}}}`) store := func(backup string) catalogue.Manifest { - return mustParse(t, `{"module":"objects","version":"1","provides":[{"name":"s3-bucket","scope":"mesh"}], + return mustParse(t, `{"module":"objects","version":"1.0","provides":[{"name":"s3-bucket","scope":"mesh"}], "grants":{"s3-bucket":"${dir:g}"}, "data":{"own":[{"id":"data","path":"${dir:data}","class":"valuable","backup":"`+backup+`"}], "consumers":{"s3-bucket":{"class":"valuable","in":"data"}}}, @@ -3852,11 +3852,11 @@ func caseItemNamesAttributesAndCapabilitiesAreJudged(t *testing.T) { } func caseKeepsMustBeAnAbsolutePath(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"mesh-vault","version":"1","keeps":"root"}`)) + _, err := ParseManifest([]byte(`{"module":"mesh-vault","version":"1.0","keeps":"root"}`)) if err == nil || !strings.Contains(err.Error(), "keeps") { t.Fatalf("a relative keeps path was not refused: %v", err) } - if _, err := ParseManifest([]byte(`{"module":"mesh-vault","version":"1","keeps":"/var/lib/mesh-vault/root"}`)); err != nil { + if _, err := ParseManifest([]byte(`{"module":"mesh-vault","version":"1.0","keeps":"/var/lib/mesh-vault/root"}`)); err != nil { t.Fatalf("an absolute keeps path was refused: %v", err) } } @@ -3907,7 +3907,7 @@ func caseNodeBuildAgentIsRetiredForTheBuildSeats(t *testing.T) { t.Errorf("the mesh still defines %s", gone) } } - _, err := ParseManifest([]byte(`{"module":"build-agent","version":"1","claims":[{"name":"node-build-agent","scope":"node"}]}`)) + _, err := ParseManifest([]byte(`{"module":"build-agent","version":"1.0","claims":[{"name":"node-build-agent","scope":"node"}]}`)) if err == nil || !strings.Contains(err.Error(), `"node-build-agent", which is retired`) || !strings.Contains(err.Error(), "node-build-go") { t.Fatalf("the claim is not refused naming its replacement: %v", err) } @@ -3918,7 +3918,7 @@ func caseNodeBuildAgentIsRetiredForTheBuildSeats(t *testing.T) { } for _, name := range []string{"node-build-go", "node-build-node", "node-build-image"} { s, ok := catalogue.SeatNamed(name) - if !ok || s.Scope != catalogue.ScopeNode || !s.PulledByKind || strings.Join(s.Accepts, ",") != "delivery,check,recheck" { + if !ok || s.Scope != catalogue.ScopeNode || !s.PulledByKind || !slices.Contains(s.Accepts, "delivery") || !slices.Contains(s.Accepts, "check") || !slices.Contains(s.Accepts, "step") || !slices.Contains(s.Accepts, "recheck") { t.Fatalf("%s = %+v", name, s) } if !s.Yields { @@ -4001,7 +4001,7 @@ func caseOneNamingFileMakesEveryMergeableFileName(t *testing.T) { if len(problems) != 1 || !strings.Contains(problems[0], "sidecar merges and names no takes-settings") { t.Fatalf("problems: %v", problems) } - raw, err := json.Marshal(map[string]any{"module": m.Module, "version": "1", "resources": m.Resources}) + raw, err := json.Marshal(map[string]any{"module": m.Module, "version": "1.0", "resources": m.Resources}) if err != nil { t.Fatal(err) } @@ -4042,7 +4042,7 @@ func caseOnlyAClaimantDeclaresASeatsPath(t *testing.T) { // An image names the Go package its recipe compiles (novox/hq ADR 0267): within the tree it is built in, // and only an image says one. func caseOnlyAnImageNamesThePackageItCompilesWithinItsTree(t *testing.T) { - m, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[ + m, err := ParseManifest([]byte(`{"module":"a","version":"1.0","build":{"artifacts":[ {"name":"x","kind":"image","from":"Dockerfile","compiles":"cmd/x"}]}}`)) if err != nil || m.Build.Artifacts[0].Compiles != "cmd/x" { t.Fatalf("an image naming its package was refused or lost it: %v", err) @@ -4053,7 +4053,7 @@ func caseOnlyAnImageNamesThePackageItCompilesWithinItsTree(t *testing.T) { {`{"name":"x","kind":"image","from":"Dockerfile","compiles":"/x"}`, "leaves the tree"}, {`{"name":"x","kind":"image","from":"Dockerfile","compiles":"a/../../x"}`, "leaves the tree"}, } { - _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[` + c.artifact + `]}}`)) + _, err := ParseManifest([]byte(`{"module":"a","version":"1.0","build":{"artifacts":[` + c.artifact + `]}}`)) if err == nil || !strings.Contains(err.Error(), c.says) { t.Errorf("%s: %v, wanted a refusal saying %q", c.artifact, err, c.says) } @@ -4121,7 +4121,7 @@ func casePlaceIsValidatedAtTheManifest(t *testing.T) { func caseReceivingWhatYouDoNotProvideIsRefused(t *testing.T) { // It would create a file nobody ever writes to, on a machine where nothing asked for it. - _, err := ParseManifest([]byte(`{"module":"traefik","version":"1", + _, err := ParseManifest([]byte(`{"module":"traefik","version":"1.0", "receives":{"reverse-proxy":"/etc/traefik/mesh.json"}}`)) if err == nil { t.Fatal("a module received contributions to something it does not provide") @@ -4193,7 +4193,7 @@ func caseRuntimeHealthIsRefusedWithoutAnAccountOfItsOwn(t *testing.T) { } func caseServingWhatYouDoNotProvideIsRefused(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"app","version":"1", + _, err := ParseManifest([]byte(`{"module":"app","version":"1.0", "serves":{"postgres-database":{"port":5432}}}`)) if err == nil { t.Fatal("a module served something it does not provide") @@ -4246,7 +4246,7 @@ func caseTakesSettingsIsRefusedWhereItCannotMeanAnything(t *testing.T) { // circle the rule of novox/hq 04-ISSUES/029 refused, a holder whose build publishes through itself, is caught where a // fresh mesh starts (issue 1152), not here. func caseAStoreHolderThatBuildsIsNotRefusedByItsManifest(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"registry","version":"1",` + + _, err := ParseManifest([]byte(`{"module":"registry","version":"1.0",` + `"provides":[{"name":"artifact-store","scope":"mesh"}],` + `"build":{"artifacts":[{"name":"registry","kind":"upstream","from":"registry:2"}]},` + `"resources":[{"id":"store","type":"container","name":"mesh-registry",` + @@ -4272,7 +4272,7 @@ func caseAStoreHolderThatBuildsIsNotRefusedByItsManifest(t *testing.T) { if got := strings.Join(CatalogueProblems(catalogue.Shelf{"registry": holder}), "; "); strings.Contains(got, "artifact-store") { t.Errorf("the store's holder providing nothing was refused: %s", got) } - if _, err := ParseManifest([]byte(`{"module":"old","version":"1","serves":{"artifact-store":{"port":5000}}}`)); err == nil || + if _, err := ParseManifest([]byte(`{"module":"old","version":"1.0","serves":{"artifact-store":{"port":5000}}}`)); err == nil || !strings.Contains(err.Error(), `serves "artifact-store" to whoever requires it, and does not provide it`) { t.Errorf("a module serving artifact-store it does not provide: %v", err) } @@ -4282,7 +4282,7 @@ func caseAStoreHolderThatBuildsIsNotRefusedByItsManifest(t *testing.T) { // nothing else (novox/hq ADR 0235). Anything it declared to say or hear on the bus would be granted // nothing, so it is refused at the parser rather than silently dropped. func caseTheBussAccountSaysNothingOnTheBus(t *testing.T) { - raw := []byte(`{"module":"bus","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}], + raw := []byte(`{"module":"bus","version":"1.0","provides":[{"name":"mesh-bus","scope":"mesh"}], "claims":[{"name":"mesh-broker","scope":"mesh"}],"own-secrets":{"broker":"/run/broker"}, "emits":["something.happened"]}`) _, err := ParseManifest(raw) @@ -4290,7 +4290,7 @@ func caseTheBussAccountSaysNothingOnTheBus(t *testing.T) { t.Fatalf("a bus module declaring what it emits was not refused, or not for the reason: %v", err) } // Nor what it invokes, with no bundle for the runtime to ask on its behalf. - unbundledInvoke := []byte(`{"module":"bus","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}], + unbundledInvoke := []byte(`{"module":"bus","version":"1.0","provides":[{"name":"mesh-bus","scope":"mesh"}], "claims":[{"name":"mesh-broker","scope":"mesh"}],"own-secrets":{"broker":"/run/broker"}, "invokes":["seat:mesh-hand-acts.hand-act"]}`) if _, err := ParseManifest(unbundledInvoke); err == nil || !strings.Contains(err.Error(), "its invokes would be granted nothing") { @@ -4301,7 +4301,7 @@ func caseTheBussAccountSaysNothingOnTheBus(t *testing.T) { // node's runtime, which is granted every tool and seat verb (mesh-sdk go/buscompose), so a bus module that // ships one may declare them. The shape of mesh-catalog's modules/nats/module.json at fc5b849d, cut to the // parts the rule reads, was refused and held every bus user list from 2026-11-02 01:35. - nats := []byte(`{"module":"nats","version":"1","provides":[{"name":"mesh-bus","scope":"mesh","credential":{"form":"none"}}], + nats := []byte(`{"module":"nats","version":"1.0","provides":[{"name":"mesh-bus","scope":"mesh","credential":{"form":"none"}}], "claims":[{"name":"mesh-broker","scope":"mesh"}], "own-secrets":{"broker":"/run/broker","broker-nats-tools":"${dir:tools-secret}/broker-nats-tools"}, "resources":[{"id":"tools-secret","type":"directory","path":"/var/lib/mesh/nats-tools","mode":"0711","unknown":"program"}], @@ -4327,7 +4327,7 @@ func caseTheBussAccountSaysNothingOnTheBus(t *testing.T) { // Its tools bundle says what it emits (novox/hq ADR 0357): the node's runtime carries the bundle and is // granted the module's events, so a bus module that ships one may declare them. - bundled := []byte(`{"module":"bus","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}], + bundled := []byte(`{"module":"bus","version":"1.0","provides":[{"name":"mesh-bus","scope":"mesh"}], "claims":[{"name":"mesh-broker","scope":"mesh"}],"own-secrets":{"broker":"/run/broker"}, "emits":["bus.slow-consumer"],"tools":["bus_streams"], "build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go","system":"arch","from":"cmd/t","binary":"t"}]}}`) @@ -4353,7 +4353,7 @@ func caseTheBussAccountSaysNothingOnTheBus(t *testing.T) { // sections are the keeper's state and the bus's own stream. The shape of A5's nats manifest (issue 1966), cut to the // parts the rule reads. keeper := func(bus string, more string) []byte { - return []byte(`{"module":"nats","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}], + return []byte(`{"module":"nats","version":"1.0","provides":[{"name":"mesh-bus","scope":"mesh"}], "claims":[{"name":"mesh-broker","scope":"mesh","serves":["list"]}],"own-secrets":{"broker":"/run/broker"}, "tools":["nats_server"],"consumes":["mesh-catalog.manifest-accepted"],"uses":["operator-channel"], "state":[{"name":"requests","history":5}],"streams":[{"name":"broker-events","events":["created"]}], @@ -4391,13 +4391,13 @@ func caseTheBussAccountSaysNothingOnTheBus(t *testing.T) { if _, err := ParseManifest(reads); err == nil || !strings.Contains(err.Error(), "its reads would be granted nothing") { t.Errorf("a bus module reading another's state: %v", err) } - other := []byte(`{"module":"app","version":"1","resources":[{"id":"u","type":"user","name":"app"}, + other := []byte(`{"module":"app","version":"1.0","resources":[{"id":"u","type":"user","name":"app"}, {"id":"p","type":"process","user":"app","run":["./a"],"bus":{"keeps":true}}]}`) if _, err := ParseManifest(other); err == nil || !strings.Contains(err.Error(), "only the keeper") { t.Errorf("keeps on another module's process: %v", err) } - quiet := []byte(`{"module":"bus","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}], + quiet := []byte(`{"module":"bus","version":"1.0","provides":[{"name":"mesh-bus","scope":"mesh"}], "claims":[{"name":"mesh-broker","scope":"mesh"}],"own-secrets":{"broker":"/run/broker"}, "tools":["bus_streams"]}`) m, err = ParseManifest(quiet) @@ -4412,12 +4412,12 @@ func caseTheBussAccountSaysNothingOnTheBus(t *testing.T) { // The catalogue check: a granting provider says what it keeps, says it in one place, writes no backup // line by hand, and every directory a container writes is declared (novox/hq ADR 0233). func caseTheCatalogueCheckAsksEveryModuleForItsData(t *testing.T) { - unsaid := mustParse(t, `{"module":"q","version":"1","provides":[{"name":"queue","scope":"mesh"}], + unsaid := mustParse(t, `{"module":"q","version":"1.0","provides":[{"name":"queue","scope":"mesh"}], "grants":{"queue":"${dir:g}"},"resources":[{"id":"g","type":"directory","mode":"0700"}]}`) - onTheOffer := mustParse(t, `{"module":"r","version":"1","provides":[{"name":"resolver","scope":"mesh","keeps-consumer-data":false}]}`) - byHand := mustParse(t, `{"module":"h","version":"1","resources":[{"id":"d","type":"directory","mode":"0700"}], + onTheOffer := mustParse(t, `{"module":"r","version":"1.0","provides":[{"name":"resolver","scope":"mesh","keeps-consumer-data":false}]}`) + byHand := mustParse(t, `{"module":"h","version":"1.0","resources":[{"id":"d","type":"directory","mode":"0700"}], "contributions":[{"seat":"node-backup","kind":"backup","content":"path ${dir:d}"}]}`) - writes := mustParse(t, `{"module":"w","version":"1","resources":[{"id":"d","type":"directory","mode":"0700"}, + writes := mustParse(t, `{"module":"w","version":"1.0","resources":[{"id":"d","type":"directory","mode":"0700"}, {"id":"c","type":"directory","mode":"0700"}, {"id":"app","type":"container","image":"x@sha256:aa","volumes":["${dir:d}:/data","${dir:c}:/etc/app:ro","${dir:d}/f:/f"]}]}`) problems := strings.Join(DataProblems(catalogue.Shelf{"q": unsaid, "r": onTheOffer, "h": byHand, "w": writes}), "\n") @@ -4435,7 +4435,7 @@ func caseTheCatalogueCheckAsksEveryModuleForItsData(t *testing.T) { t.Errorf("a read-only mount was taken for written data:\n%s", problems) } // The same module declaring the directory, as a cache, passes. - declared := mustParse(t, `{"module":"w","version":"1","data":{"own":[{"id":"d","path":"${dir:d}","class":"cache"}]}, + declared := mustParse(t, `{"module":"w","version":"1.0","data":{"own":[{"id":"d","path":"${dir:d}","class":"cache"}]}, "resources":[{"id":"d","type":"directory","mode":"0700"}, {"id":"app","type":"container","image":"x@sha256:aa","volumes":["${dir:d}:/data"]}]}`) if p := DataProblems(catalogue.Shelf{"w": declared}); len(p) > 0 { @@ -4603,7 +4603,7 @@ func caseTheDataSectionIsRefusedWhereItIsWrong(t *testing.T) { {"consumer data in an item less precious", `{"own":[{"id":"a","path":"${dir:d}","class":"rebuildable"}],"consumers":{"q":{"class":"irreplaceable","in":"a"}}}`, "which is only rebuildable"}, {"consumer data in something unknown", `{"consumers":{"q":{"class":"rebuildable","in":"elsewhere"}}}`, "neither one of its data items"}, } { - raw := `{"module":"m","version":"1","provides":["q"],"resources":[{"id":"d","type":"directory","mode":"0700"}],"data":` + c.data + `}` + raw := `{"module":"m","version":"1.0","provides":["q"],"resources":[{"id":"d","type":"directory","mode":"0700"}],"data":` + c.data + `}` _, err := ParseManifest([]byte(raw)) if err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("%s: %v, want %q", c.name, err, c.want) @@ -4683,7 +4683,7 @@ func caseTheMeshsOwnSeatsAreStillClaimable(t *testing.T) { // It delivers the bus, so its holder provides the bus — the rule this check now enforces. m := catalogue.Manifest{Module: "nats", Provides: []catalogue.Offer{{Name: "mesh-bus", Scope: catalogue.ScopeMesh, Credential: &catalogue.OfferCredential{Form: catalogue.CredentialFormNone}}}, - Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}} + Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh, Serves: []string{"snapshot-kept", "restore-kept"}}}} if got := problemsFor(t, catalogue.Shelf{"nats": m}); got != "" { t.Fatalf("a mesh seat was refused by the derived check: %s", got) } @@ -4749,7 +4749,7 @@ func caseTheParserDoesNotJudgeWhatOnlyTheStoreKnows(t *testing.T) { // A store whose bus seat delivers something this module does provide. catalogue.UseSeats([]catalogue.Seat{{Name: "mesh-broker", Scope: catalogue.ScopeMesh, Delivers: "mesh-bus", Decision: "test"}}) - raw := []byte(`{"module":"a-bus","version":"1",` + + raw := []byte(`{"module":"a-bus","version":"1.0",` + `"provides":[{"name":"mesh-bus","scope":"mesh","credential":{"form":"none"}}],` + `"claims":[{"name":"mesh-broker","scope":"mesh"}]}`) @@ -4979,7 +4979,7 @@ func caseTheSessionsFilesArePlacedByTheDisplayServersHolderAlone(t *testing.T) { // `requires` would have stopped every plan after a rollback. Stored, `requires` is plain names, as main's // manifest type reads it, and which are optional sits beside it in a key such a controller sets aside. func caseTheStoredFormReadsWhereRequiresIsAListOfNames(t *testing.T) { - m := mustParse(t, `{"module":"grafana","version":"1", + m := mustParse(t, `{"module":"grafana","version":"1.0", "requires":["route",{"name":"influxdb-api","optional":true}]}`) raw, err := json.Marshal(m) if err != nil { @@ -5017,7 +5017,7 @@ func caseTheStoredFormReadsWhereRequiresIsAListOfNames(t *testing.T) { } } // A stored list naming something the module does not require is refused. - if _, err := ParseManifest([]byte(`{"module":"x","version":"1","requires":["route"],"optional-requires":["mqtt-topic"]}`)); err == nil { + if _, err := ParseManifest([]byte(`{"module":"x","version":"1.0","requires":["route"],"optional-requires":["mqtt-topic"]}`)); err == nil { t.Error("an optional requirement the module does not require passed the check") } } @@ -5036,7 +5036,7 @@ func caseTheUplinkIsANodeSeatThatDeliversNothing(t *testing.T) { t.Fatalf("the uplink is %+v, not a node seat delivering nothing by ADR 0117", seat) } // And a manager's module can hold it without providing anything. - raw := []byte(`{"module":"networkmanager","version":"1","claims":[{"name":"node-uplink","scope":"node"}]}`) + raw := []byte(`{"module":"networkmanager","version":"1.0","claims":[{"name":"node-uplink","scope":"node"}]}`) if _, err := ParseManifest(raw); err != nil { t.Fatalf("a network manager's module could not hold the uplink: %v", err) } @@ -5055,7 +5055,7 @@ func caseTheWorldsNamesAreNotAnInstallations(t *testing.T) { } func caseTwoArtifactsWithOneNameAreRefused(t *testing.T) { - _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[ + _, err := ParseManifest([]byte(`{"module":"a","version":"1.0","build":{"artifacts":[ {"name":"x","kind":"image","from":"Dockerfile"}, {"name":"x","kind":"archive","from":"files"}]}}`)) if err == nil { @@ -5191,12 +5191,12 @@ func caseWhatReplacesMayNameIsRefusedNearItsAuthor(t *testing.T) { "another module's": `{"id":"p","type":"process","name":"p","run":["./p"],"replaces":["other.old"]}`, "not a list": `{"id":"p","type":"process","name":"p","run":["./p"],"replaces":"old"}`, } { - raw := `{"module":"m","version":"1","resources":[` + resource + `]}` + raw := `{"module":"m","version":"1.0","resources":[` + resource + `]}` if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "replace") { t.Errorf("replaces on %s was accepted: %v", what, err) } } - ok := `{"module":"m","version":"1","resources":[{"id":"p","type":"process","name":"p","run":["./p"],"replaces":["old"]}]}` + ok := `{"module":"m","version":"1.0","resources":[{"id":"p","type":"process","name":"p","run":["./p"],"replaces":["old"]}]}` if _, err := ParseManifest([]byte(ok)); err != nil { t.Errorf("a process replacing what its module no longer declares was refused: %v", err) } @@ -5328,7 +5328,7 @@ func problemsFor(t *testing.T, shelf catalogue.Shelf) string { } func claimed(claims string) []byte { - return []byte(`{"module":"thing","version":"1","provides":[{"name":"npm-package-registry","scope":"mesh","credential":{"form":"none"}}],"claims":` + claims + `}`) + return []byte(`{"module":"thing","version":"1.0","provides":[{"name":"npm-package-registry","scope":"mesh","credential":{"form":"none"}}],"claims":` + claims + `}`) } func hostnameSeat(t *testing.T) catalogue.Seat { @@ -5354,7 +5354,7 @@ func mustParse(t *testing.T, raw string) catalogue.Manifest { return m } -const declaredStore = `{"module":"pg","version":"1", +const declaredStore = `{"module":"pg","version":"1.0", "provides":[{"name":"postgres-database","scope":"mesh"}], "grants":{"postgres-database":"${dir:grants}"}, "data":{ @@ -5376,7 +5376,7 @@ func shallowCopy(resource map[string]any) map[string]any { return copied } -const templatedTwin = `{"module":"tidy","version":"1", +const templatedTwin = `{"module":"tidy","version":"1.0", "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, "resources":[ {"id":"conf","type":"directory","path":"${dir:conf}"}, @@ -5417,7 +5417,7 @@ func aChannel(module, kind string) catalogue.Manifest { func aStoreManifest(step map[string]any) []byte { m := map[string]any{ - "module": "distribution", "version": "1", + "module": "distribution", "version": "1.0", "resources": []any{ map[string]any{"id": "store", "type": "container", "name": "mesh-registry", "image": "registry@sha256:" + strings.Repeat("a", 64)}, @@ -5430,7 +5430,7 @@ func aStoreManifest(step map[string]any) []byte { func familyManifest(t *testing.T, ownSecrets string) (catalogue.Manifest, error) { t.Helper() - return ParseManifest([]byte(`{"module":"mounts","version":"1","own-secrets":{` + ownSecrets + `}}`)) + return ParseManifest([]byte(`{"module":"mounts","version":"1.0","own-secrets":{` + ownSecrets + `}}`)) } // jsonString quotes a Go string the way encoding/json would, for building a literal into a test's manifest JSON. @@ -5471,7 +5471,7 @@ func fallingBackTo(t *testing.T, key string) []byte { func inConfigForm() map[string]any { return map[string]any{ "module": "notifier", - "version": "1", + "version": "1.0", "config": map[string]any{ "font-size": map[string]any{"type": "number", "description": "the notification's font size, in points", "minimum": 6, "maximum": 40, "default": 10}, @@ -5617,7 +5617,7 @@ func parsed(t *testing.T, m catalogue.Manifest) error { } func upstreamManifest(artifact string) string { - return `{"module":"a","version":"1","build":{"artifacts":[` + artifact + `]}}` + return `{"module":"a","version":"1.0","build":{"artifacts":[` + artifact + `]}}` } const pinnedSum = "38dd24890a66b4cf9a45bc24e15192e00c26b929e1bb8df7974c16a4539129e2" @@ -5639,7 +5639,7 @@ func mailSenderProcessManifest(t *testing.T, bus string) string { // stream the module owns, never its retention — that rule is 1178's (WorkQueueStreamProblems), and is // tested against its own fixture there (work_queue_consumer_test.go) so this one stays accepted whether // or not a process currently names "mail-sends" in bus.queues. - return `{"module":"mesh-mail","version":"1","emits":["mail.send.sent","mail.send.failed"],` + + return `{"module":"mesh-mail","version":"1.0","emits":["mail.send.sent","mail.send.failed"],` + `"state":["sends"],"streams":[{"name":"mail-sends","events":["mail.send.approved"],"max-age-days":1,` + `"max-mib":64}],"resources":[{"id":"account","type":"user","name":"mesh-mail-send"},` + process + `]}` } @@ -5659,7 +5659,7 @@ func strSliceEqual(a, b []string) bool { return true } -const handedProvider = `{"module":"db","version":"1","moved":true, +const handedProvider = `{"module":"db","version":"1.0","moved":true, "provides":[{"name":"postgres-database","scope":"mesh","identity":{"max":63,"in":"a PostgreSQL role"}}], "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, "resources":[ @@ -5738,7 +5738,7 @@ func ownAccountManifest(t *testing.T, runsAs bool, health any) []byte { return raw } -const aRegistryForge = `{"module":"forge","version":"1","requires":["route"], +const aRegistryForge = `{"module":"forge","version":"1.0","requires":["route"], "provides":[{"name":"npm-package-registry","scope":"mesh"}], "contributes":{"route":{"web":{"label":"git","port":3000}}}, "serves":{"npm-package-registry":{"scheme":"http","port":3000,"npm-path":"/api/packages/novox/npm/", @@ -5768,7 +5768,7 @@ func store() catalogue.Manifest { // that another entry knows is kept (novox/hq ADR 0262). func aModuleWithAFieldSetAsideStillProvidesWhatItProvides(t *testing.T) { var m catalogue.Manifest - raw := `{"module": "later", "version": "2", + raw := `{"module": "later", "version":"2.0", "provides": [{"name": "db", "scope": "mesh"}, {"name": "cache", "a-field-from-later": 1}], "state": [{"name": "s", "history": 3}], "data": {"own": [{"id": "d", "path": "${dir:d}", "class": "valuable", "backup": {"dump": "x", "into": "d", "class": "later"}}]}, @@ -5807,13 +5807,13 @@ func notifier() catalogue.Manifest { // the trusted mergeable file that takes its grants setting; extra is spliced in at the top. func mailManifest(t *testing.T, streams, extra string) string { t.Helper() - return `{"module":"mesh-mail","version":"1",` + extra + `"streams":` + streams + `,` + + return `{"module":"mesh-mail","version":"1.0",` + extra + `"streams":` + streams + `,` + `"resources":[{"id":"state","type":"directory","mode":"0700","place":"."},` + `{"id":"grants","trusted":true,"type":"file","path":"${dir:state}/grants.json","mode":"0600",` + `"merge":"json","takes-settings":["grants"],"content":"{\"grants\": {}}"}]}` } -const unpackingTemplated = `{"module":"tidy","version":"1", +const unpackingTemplated = `{"module":"tidy","version":"1.0", "templates":"templates", "build":{"artifacts":[{"name":"scripts","kind":"archive","from":"files"}, {"name":"templates","kind":"archive","from":"templates"}]}, @@ -5823,7 +5823,7 @@ const unpackingTemplated = `{"module":"tidy","version":"1", {"id":"main","type":"file","path":"/etc/tidy.conf","mode":"0644","template":"templates/tidy.conf"}, {"id":"extra","type":"file","path":"/etc/extra.conf","template":"templates/deeper/extra.conf"}]}` -const unpackingInline = `{"module":"tidy","version":"1", +const unpackingInline = `{"module":"tidy","version":"1.0", "build":{"artifacts":[{"name":"scripts","kind":"archive","from":"files"}]}, "resources":[ {"id":"scripts","type":"archive","path":"/usr/local/lib/tidy","artifact":"scripts"}, @@ -5852,7 +5852,7 @@ func mod(name string, provides, requires, capabilities []string, claims ...catal Capabilities: capabilities, Claims: claims} } -const collectorZsh = `{"module":"zsh","version":"1","moved":true, +const collectorZsh = `{"module":"zsh","version":"1.0","moved":true, "claims":[{"name":"node-login-shell","scope":"node"}], "collections":[{"name":"widgets","comment":"#"}], "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, @@ -5968,7 +5968,7 @@ func catalogueManifest(t *testing.T, module string) catalogue.Manifest { } func optionalModule(resources string) string { - return `{"module":"grafana","version":"1", + return `{"module":"grafana","version":"1.0", "requires":["route",{"name":"influxdb-api","optional":true}], "secrets":{"influxdb-api":"/var/lib/grafana/influxdb-api"}, "resources":[` + resources + `]}` @@ -5978,7 +5978,7 @@ const aContainerMounting = `{"id":"server","type":"container","name":"store","im `0000000000000000000000000000000000000000000000000000000000000000","volumes":["%s:/inside"]}` func botMoved() string { - return strings.Replace(handedBot, `"version":"1",`, `"version":"1","moved":true,`, 1) + return strings.Replace(handedBot, `"version":"1.0",`, `"version":"1.0","moved":true,`, 1) } // held is a manifest as registration holds it: resolved with what was built, then read again strictly. @@ -6015,7 +6015,7 @@ const aServiceDigest = "sha256:ddddddddddddddddddddddddddddddddddddddddddddddddd const healthDigest = "@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" -const handedNotifier = `{"module":"notify","version":"1", +const handedNotifier = `{"module":"notify","version":"1.0", "config":{"width":{"type":"number","description":"How wide a notification is, in pixels.","default":250}}, "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, "resources":[ @@ -6025,7 +6025,7 @@ const handedNotifier = `{"module":"notify","version":"1", {"id":"notifier","type":"service","unit":"notify.service","scope":"user","user":"${machine:account}","reload-on":["configuration"]}]}` func notifierMoved() string { - return strings.Replace(handedNotifier, `"version":"1",`, `"version":"1","moved":true,`, 1) + return strings.Replace(handedNotifier, `"version":"1.0",`, `"version":"1.0","moved":true,`, 1) } const mainText = `# tidy\nroot = ${dir:conf}\nquoted = \"yes\" é\n` @@ -6082,7 +6082,7 @@ func moduleTree(t *testing.T, module string) (root, dir string) { return beside.Dir(t, "mesh-catalog"), filepath.Join(beside.Catalogue(t), module) } -const handedBot = `{"module":"bot","version":"1", +const handedBot = `{"module":"bot","version":"1.0", "own-secrets":{"token":"${dir:state}/token"}, "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, "resources":[