diff --git a/workspace-server/internal/handlers/workspace_test.go b/workspace-server/internal/handlers/workspace_test.go index a32c9f3e9..5b1479ffa 100644 --- a/workspace-server/internal/handlers/workspace_test.go +++ b/workspace-server/internal/handlers/workspace_test.go @@ -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) {