{"record":{"id":"c7668120c4cbdbdd","repo":"jamiepine/voicebox","slug":"cannot-modify-built-in-presets","errorCode":null,"errorMessage":"Cannot modify built-in presets","messagePattern":"Cannot modify built-in presets","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"backend/services/effects.py","lineNumber":91,"sourceCode":"        is_builtin=False,\n    )\n    db.add(preset)\n    try:\n        db.commit()\n    except IntegrityError:\n        db.rollback()\n        raise ValueError(f\"A preset named '{data.name}' already exists\")\n    db.refresh(preset)\n    return _preset_response(preset)\n\n\ndef update_preset(preset_id: str, data: EffectPresetUpdate, db: Session) -> Optional[EffectPresetResponse]:\n    \"\"\"Update a user effect preset. Cannot modify built-in presets.\"\"\"\n    preset = db.query(DBEffectPreset).filter_by(id=preset_id).first()\n    if not preset:\n        return None\n    if preset.is_builtin:\n        raise ValueError(\"Cannot modify built-in presets\")\n\n    if data.name is not None:\n        preset.name = data.name\n    if data.description is not None:\n        preset.description = data.description\n    if data.effects_chain is not None:\n\n        chain_dicts = [e.model_dump() for e in data.effects_chain]\n        error = validate_effects_chain(chain_dicts)\n        if error:\n            raise ValueError(error)\n        preset.effects_chain = json.dumps(chain_dicts)\n\n    db.commit()\n    db.refresh(preset)\n    return _preset_response(preset)\n\n","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/services/effects.py#L73-L109","documentation":"update_preset() refuses to mutate any row where is_builtin is True. Built-in presets (seeded from BUILTIN_PRESETS in utils/effects.py: robotic, radio, echo_chamber, deep_voice) are read-only reference data. The check runs before any field assignment.","triggerScenarios":"PUT /presets/{id} where {id} resolves to a preset with is_builtin=True.","commonSituations":"UI lists built-in and user presets together without distinguishing them; user clicks Edit on a built-in row.","solutions":["Disable edit controls in the UI for presets where is_builtin is true.","If a variant is wanted, clone the built-in into a new user preset (create_preset) and edit that copy.","Verify the preset_id being updated resolves to a user preset before calling update_preset()."],"exampleFix":"# before\nupdate_preset(builtin_id, data, db)  # raises\n\n# after — clone then edit a user copy\nsrc = get_preset(builtin_id, db)\nuser = create_preset(EffectPresetCreate(\n    name=f'{src.name} (copy)',\n    description=src.description,\n    effects_chain=src.effects_chain,\n), db)\nupdate_preset(user.id, data, db)","handlingStrategy":"validation","validationCode":"from backend.services.effects import get_preset\n\ndef is_editable(preset_id, db) -> bool:\n    p = get_preset(preset_id, db)\n    return p is not None and not p.is_builtin","typeGuard":"def is_builtin_preset(p) -> bool:\n    return bool(getattr(p, \"is_builtin\", False))","tryCatchPattern":"try:\n    update_preset(preset_id, data, db)\nexcept ValueError as e:\n    if \"built-in\" in str(e):\n        # hide/disable the edit affordance for this preset\n        ...\n    raise","preventionTips":["Surface is_builtin in list responses so the UI can mark non-editable rows.","Clone-to-edit instead of mutating built-ins."],"tags":["effects","presets","readonly","built-in"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}