From 9836de23be6ba166b1a43a23d86f3f34a104dbd7 Mon Sep 17 00:00:00 2001 From: core-devops Date: Thu, 18 Jun 2026 21:05:51 -0700 Subject: [PATCH 1/2] =?UTF-8?q?test(provision):=20contract=20SSOT=20+=20pr?= =?UTF-8?q?oducer-pin=20for=20the=20core=E2=86=92CP=20provision=20request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core and the control-plane define the /cp/workspaces/provision request as two DUPLICATED structs in two repos with no shared compile unit, so a field added on the sender silently does nothing if the receiver lacks it. That is exactly how `template_assets` was dropped (config.yaml/prompts never materialized on SaaS via that channel) — RFC #2843 / project_saas_restart_re_stub_config. Adds provision_request.contract.json as the SSOT wire shape, and a producer-pin test asserting cpProvisionRequest's JSON tags == the contract exactly. Adding or removing a wire field now FAILS until the contract is updated deliberately, with an explicit cp_consumes flag per field; cp_consumes:false (a dead sent-but-ignored field, e.g. template_assets today) must carry a justifying note. A CP-side companion test (molecule-controlplane) will enforce that every cp_consumes:true field is present on wsProvisionRequest. Step 1 of the local-test parity plan: catch contract drift at the seam, in-repo, on the PR that introduces it — instead of via post-merge black-box staging e2e. Verified: passes on the current struct; fails when a drift field is injected. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../provision_request.contract.json | 40 ++++++ .../provision_request_contract_test.go | 135 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 workspace-server/internal/provisioner/provision_request.contract.json create mode 100644 workspace-server/internal/provisioner/provision_request_contract_test.go diff --git a/workspace-server/internal/provisioner/provision_request.contract.json b/workspace-server/internal/provisioner/provision_request.contract.json new file mode 100644 index 000000000..cc84beadc --- /dev/null +++ b/workspace-server/internal/provisioner/provision_request.contract.json @@ -0,0 +1,40 @@ +{ + "_doc": [ + "SSOT for the core -> control-plane provision request wire shape.", + "Endpoint: POST {cp}/cp/workspaces/provision (core sender = cpProvisionRequest in cp_provisioner.go;", + "CP receiver = wsProvisionRequest in molecule-controlplane internal/handlers/workspace_provision.go).", + "", + "WHY THIS EXISTS: core and the CP define the request as two DUPLICATED structs in two repos with no", + "shared compile unit, so a field added on the sender silently does nothing if the receiver lacks it.", + "That is exactly how `template_assets` was dropped for weeks (config.yaml/prompts never materialized on", + "SaaS via that channel) -- see memory project_saas_restart_re_stub_config / RFC #2843.", + "", + "TWO GUARDS keep the channels honest:", + " 1. PRODUCER PIN (this repo, provision_request_contract_test.go): cpProvisionRequest's JSON tags MUST", + " equal `fields` keys exactly. Adding/removing a wire field forces a deliberate, reviewed edit here.", + " 2. CONSUMER COMPLETENESS (molecule-controlplane companion test): wsProvisionRequest MUST have a json", + " tag for every field marked cp_consumes:true. A field with cp_consumes:false is a DEAD wire field", + " (sent but ignored) -- it must carry a `note` justifying why, so dead channels are explicit, not silent." + ], + "endpoint": "POST /cp/workspaces/provision", + "fields": { + "org_id": {"type": "string", "cp_consumes": true}, + "workspace_id": {"type": "string", "cp_consumes": true}, + "runtime": {"type": "string", "cp_consumes": true}, + "tier": {"type": "int", "cp_consumes": true}, + "instance_type": {"type": "string", "cp_consumes": true}, + "disk_gb": {"type": "int", "cp_consumes": true}, + "provider": {"type": "string", "cp_consumes": true}, + "data_persistence": {"type": "string", "cp_consumes": true}, + "kind": {"type": "string", "cp_consumes": true}, + "display": {"type": "object", "cp_consumes": true}, + "platform_url": {"type": "string", "cp_consumes": true}, + "env": {"type": "map", "cp_consumes": true}, + "config_files": {"type": "map", "cp_consumes": true}, + "template_assets": { + "type": "map", + "cp_consumes": false, + "note": "DEAD WIRE FIELD as of 2026-06-19: core fetches+sends template assets here (RFC #2843 #24) but the deployed CP wsProvisionRequest has no `template_assets` field, so it is silently dropped. config.yaml/prompts reach SaaS boxes via config_files instead. Follow-up: either remove this send from core OR add the consumer to the CP. Tracked alongside project_saas_restart_re_stub_config." + } + } +} diff --git a/workspace-server/internal/provisioner/provision_request_contract_test.go b/workspace-server/internal/provisioner/provision_request_contract_test.go new file mode 100644 index 000000000..029e188d3 --- /dev/null +++ b/workspace-server/internal/provisioner/provision_request_contract_test.go @@ -0,0 +1,135 @@ +package provisioner + +// Producer-side guard for the core -> control-plane provision request contract. +// +// cpProvisionRequest is the wire shape core POSTs to {cp}/cp/workspaces/provision. +// The control-plane decodes it into its OWN duplicated struct (wsProvisionRequest) +// in a SEPARATE repo, so a field added here silently does nothing if the CP lacks +// it (this is exactly how `template_assets` was dropped — RFC #2843 / +// project_saas_restart_re_stub_config). +// +// This test pins cpProvisionRequest's JSON tags to provision_request.contract.json +// (the SSOT). Adding or removing a wire field FAILS here until the contract is +// updated deliberately — at which point the reviewer must decide cp_consumes +// true/false, and the CP-side companion test (molecule-controlplane) enforces that +// every cp_consumes:true field is actually present on wsProvisionRequest. + +import ( + "encoding/json" + "os" + "reflect" + "sort" + "strings" + "testing" +) + +const provisionRequestContractPath = "provision_request.contract.json" + +type provisionRequestContract struct { + Fields map[string]struct { + Type string `json:"type"` + CPConsumes bool `json:"cp_consumes"` + Note string `json:"note"` + } `json:"fields"` +} + +// jsonWireTags returns the set of JSON wire field names for a struct type, +// stripping ",omitempty"/options and skipping "-" and untagged fields. +func jsonWireTags(t reflect.Type) map[string]bool { + tags := map[string]bool{} + for i := 0; i < t.NumField(); i++ { + raw := t.Field(i).Tag.Get("json") + if raw == "" { + continue + } + name := strings.Split(raw, ",")[0] + if name == "" || name == "-" { + continue + } + tags[name] = true + } + return tags +} + +func sortedKeys(m map[string]bool) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + sort.Strings(out) + return out +} + +func TestProvisionRequestContract_ProducerMatchesSSOT(t *testing.T) { + data, err := os.ReadFile(provisionRequestContractPath) + if err != nil { + t.Fatalf("read contract %s: %v", provisionRequestContractPath, err) + } + var contract provisionRequestContract + if err := json.Unmarshal(data, &contract); err != nil { + t.Fatalf("parse contract %s: %v", provisionRequestContractPath, err) + } + if len(contract.Fields) == 0 { + t.Fatalf("contract %s has no fields — refusing to pass a vacuous contract", provisionRequestContractPath) + } + + structTags := jsonWireTags(reflect.TypeOf(cpProvisionRequest{})) + + contractFields := map[string]bool{} + for name := range contract.Fields { + contractFields[name] = true + } + + var addedInStruct, missingFromStruct []string + for name := range structTags { + if !contractFields[name] { + addedInStruct = append(addedInStruct, name) + } + } + for name := range contractFields { + if !structTags[name] { + missingFromStruct = append(missingFromStruct, name) + } + } + sort.Strings(addedInStruct) + sort.Strings(missingFromStruct) + + if len(addedInStruct) > 0 { + t.Errorf("cpProvisionRequest sends wire field(s) NOT in the contract: %v\n"+ + " -> Add each to %s with an explicit cp_consumes (true if the CP's wsProvisionRequest already\n"+ + " consumes it; false ONLY with a note explaining the dead channel). A field core sends that\n"+ + " the CP does not consume is silently dropped — this is the template_assets failure class.", + addedInStruct, provisionRequestContractPath) + } + if len(missingFromStruct) > 0 { + t.Errorf("contract declares wire field(s) NOT present on cpProvisionRequest: %v\n"+ + " -> If core intentionally stopped sending these, remove them from %s (and confirm the CP no\n"+ + " longer requires them). Otherwise restore the struct field.", + missingFromStruct, provisionRequestContractPath) + } + + if t.Failed() { + t.Logf("cpProvisionRequest json tags: %v", sortedKeys(structTags)) + t.Logf("contract fields: %v", sortedKeys(contractFields)) + } +} + +// TestProvisionRequestContract_DeadFieldsAreJustified ensures any cp_consumes:false +// field carries a note — so a dead wire field (sent-but-ignored) is an explicit, +// reviewed decision, never silent. +func TestProvisionRequestContract_DeadFieldsAreJustified(t *testing.T) { + data, err := os.ReadFile(provisionRequestContractPath) + if err != nil { + t.Fatalf("read contract: %v", err) + } + var contract provisionRequestContract + if err := json.Unmarshal(data, &contract); err != nil { + t.Fatalf("parse contract: %v", err) + } + for name, f := range contract.Fields { + if !f.CPConsumes && strings.TrimSpace(f.Note) == "" { + t.Errorf("field %q is cp_consumes:false but has no `note` — a dead wire field MUST justify why "+ + "(remove the send, or document why it is intentionally unconsumed)", name) + } + } +} -- 2.52.0 From 80f8e0e86e46dcddf0dbef44906931e90a481ec9 Mon Sep 17 00:00:00 2001 From: core-devops Date: Thu, 18 Jun 2026 21:09:18 -0700 Subject: [PATCH 2/2] docs(contract): repo-explicit guard prose so the byte-identical CP copy reads correctly Co-Authored-By: Claude Opus 4.8 (1M context) --- .../provisioner/provision_request.contract.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/workspace-server/internal/provisioner/provision_request.contract.json b/workspace-server/internal/provisioner/provision_request.contract.json index cc84beadc..c26c0a51c 100644 --- a/workspace-server/internal/provisioner/provision_request.contract.json +++ b/workspace-server/internal/provisioner/provision_request.contract.json @@ -10,11 +10,14 @@ "SaaS via that channel) -- see memory project_saas_restart_re_stub_config / RFC #2843.", "", "TWO GUARDS keep the channels honest:", - " 1. PRODUCER PIN (this repo, provision_request_contract_test.go): cpProvisionRequest's JSON tags MUST", - " equal `fields` keys exactly. Adding/removing a wire field forces a deliberate, reviewed edit here.", - " 2. CONSUMER COMPLETENESS (molecule-controlplane companion test): wsProvisionRequest MUST have a json", - " tag for every field marked cp_consumes:true. A field with cp_consumes:false is a DEAD wire field", - " (sent but ignored) -- it must carry a `note` justifying why, so dead channels are explicit, not silent." + " 1. PRODUCER PIN (molecule-core provisioner/provision_request_contract_test.go): cpProvisionRequest's", + " JSON tags MUST equal `fields` keys exactly. Adding/removing a wire field forces a deliberate edit.", + " 2. CONSUMER COMPLETENESS (molecule-controlplane handlers/provision_request_contract_test.go):", + " wsProvisionRequest MUST have a json tag for every field marked cp_consumes:true. A field with", + " cp_consumes:false is a DEAD wire field (sent but ignored) -- it must carry a `note` justifying why,", + " so dead channels are explicit, not silent.", + "", + " This file is the SSOT; the copy in the other repo MUST stay byte-identical (a CI diff-check is a follow-up)." ], "endpoint": "POST /cp/workspaces/provision", "fields": { -- 2.52.0