From 113bb9cbfd4a8e6545ec313040711526bbc1e17e Mon Sep 17 00:00:00 2001 From: core-devops Date: Tue, 16 Jun 2026 23:32:58 +0000 Subject: [PATCH 1/2] RFC#2843 #32: fix prevStatus enum-COALESCE that silenced the reconcile trigger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #3002 heartbeat reconcile trigger read prevStatus via SELECT ..., COALESCE(status, '') — but status is a NOT-NULL workspace_status ENUM, so '' is coerced to the enum and Postgres rejects it (), failing the whole row scan. prevStatus stayed "" on every heartbeat, so prevStatus=='provisioning' never matched and the declared-plugin reconcile never fired — the #32 regression returned in prod (live seo-agent: seo-all never installed, observed on tenant box log). Fix: select status BARE (it is never NULL). Verified mechanism on a live prod tenant heartbeat log. Co-Authored-By: Claude Opus 4.8 (1M context) --- workspace-server/internal/handlers/registry.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/workspace-server/internal/handlers/registry.go b/workspace-server/internal/handlers/registry.go index 3e4bca7d..28ea327e 100644 --- a/workspace-server/internal/handlers/registry.go +++ b/workspace-server/internal/handlers/registry.go @@ -898,10 +898,18 @@ func (h *RegistryHandler) Heartbeat(c *gin.Context) { // flip, independent of evaluateStatus. evaluateStatus still owns the OTHER // recovery transitions (offline/degraded/awaiting_agent/failed→online), // which the inline CASE does not touch. + // + // IMPORTANT (enum scan): `status` is a NOT-NULL `workspace_status` ENUM. + // Do NOT wrap it in COALESCE(status, '') — the '' literal is coerced to + // the enum type and Postgres rejects it with `invalid input value for + // enum workspace_status: ""`, failing the WHOLE row scan. That left + // prevStatus = "" on every heartbeat, so the prevStatus=='provisioning' + // reconcile trigger NEVER fired (the #32 regression returned). Select the + // column bare; it is never NULL. var prevTask string var prevSpend int64 var prevStatus string - if err := db.DB.QueryRowContext(ctx, `SELECT COALESCE(current_task, ''), COALESCE(monthly_spend, 0), COALESCE(status, '') FROM workspaces WHERE id = $1`, payload.WorkspaceID).Scan(&prevTask, &prevSpend, &prevStatus); err != nil { + if err := db.DB.QueryRowContext(ctx, `SELECT COALESCE(current_task, ''), COALESCE(monthly_spend, 0), status FROM workspaces WHERE id = $1`, payload.WorkspaceID).Scan(&prevTask, &prevSpend, &prevStatus); err != nil { log.Printf("registry heartbeat: prev_task query failed for workspace %s: %v", payload.WorkspaceID, err) } -- 2.52.0 From fc53ab209506034034ff775043a8c8c09c74a8d6 Mon Sep 17 00:00:00 2001 From: core-devops Date: Tue, 16 Jun 2026 23:33:12 +0000 Subject: [PATCH 2/2] =?UTF-8?q?RFC#2843=20#32:=20unit=20guard=20=E2=80=94?= =?UTF-8?q?=20pin=20prevStatus=20SELECT=20status=20column=20bare?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tighten the ProvisioningToOnline mock query matcher to require `, status FROM workspaces` so a re-introduced COALESCE(status, ...) fails this test. sqlmock does not enforce enum types, so the loose prefix matcher passed despite the prod-breaking COALESCE(status, ''). Co-Authored-By: Claude Opus 4.8 (1M context) --- workspace-server/internal/handlers/registry_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/workspace-server/internal/handlers/registry_test.go b/workspace-server/internal/handlers/registry_test.go index 5104d92b..43bbde06 100644 --- a/workspace-server/internal/handlers/registry_test.go +++ b/workspace-server/internal/handlers/registry_test.go @@ -271,7 +271,14 @@ func TestHeartbeatHandler_ProvisioningToOnline(t *testing.T) { // prevTask + prevStatus SELECT — prevStatus='provisioning' is the state a // freshly-created workspace is in before its first heartbeat. - mock.ExpectQuery("SELECT COALESCE\\(current_task"). + // + // The matcher pins `status` selected BARE (not COALESCE-wrapped): `status` + // is a NOT-NULL workspace_status ENUM, and COALESCE(status, '') coerces '' + // to the enum → Postgres `invalid input value for enum workspace_status: ""` + // → the whole row scan fails → prevStatus stays "" → this reconcile trigger + // NEVER fires (the live #32 regression). Requiring `, status FROM workspaces` + // here makes a re-introduced COALESCE(status, ...) fail this unit test. + mock.ExpectQuery("SELECT COALESCE\\(current_task, ''\\), COALESCE\\(monthly_spend, 0\\), status FROM workspaces"). WithArgs("ws-provisioning"). WillReturnRows(sqlmock.NewRows([]string{"current_task", "monthly_spend", "status"}).AddRow("", 0, "provisioning")) -- 2.52.0