fix(workspace): PATCH-runtime compat-check reads resolved model (unblocks seo-template conversion) #2957

Closed
core-devops wants to merge 1 commits from fix/runtime-compat-resolved-model into main
@@ -1031,6 +1031,54 @@ func TestWorkspaceUpdate_RuntimeField_DBErrorReturnsServerError(t *testing.T) {
}
}
func TestWorkspaceUpdate_RuntimeField_ModelUnresolved_SkipsCheckAndProceeds(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
broadcaster := newTestBroadcaster()
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
mock.ExpectQuery("SELECT EXISTS.*workspaces WHERE id").
WithArgs("cccccccc-0006-0000-0000-000000000002").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
// The JRS case: the model lives only in workspace_secrets (the
// workspaces.model column is empty). No MODEL secret row means the
// model is genuinely undeterminable at PATCH time; the strict
// (runtime, model) compat-check is skipped and the PATCH proceeds.
// A bad (runtime, model) pair fail-closes at boot, not here.
mock.ExpectQuery(`SELECT encrypted_value, encryption_version FROM workspace_secrets WHERE workspace_id = \$1 AND key = 'MODEL'`).
WithArgs("cccccccc-0006-0000-0000-000000000002").
WillReturnError(sql.ErrNoRows)
mock.ExpectExec("UPDATE workspaces SET runtime").
WithArgs("cccccccc-0006-0000-0000-000000000002", "claude-code").
WillReturnResult(sqlmock.NewResult(0, 1))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "cccccccc-0006-0000-0000-000000000002"}}
body := `{"runtime":"claude-code"}`
c.Request = httptest.NewRequest("PATCH", "/workspaces/ws-rt", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.Update(c)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 (unresolved model → skip check, proceed), got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp["needs_restart"] != true {
t.Errorf("expected needs_restart=true, got %v", resp["needs_restart"])
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// ==================== DELETE /workspaces/:id ====================
func TestWorkspaceDelete_ConfirmationRequired(t *testing.T) {