Registry Migrations Guide
This guide documents the complete process for creating and testing registry migrations in JellyRock. Follow these steps carefully to ensure data integrity and avoid overlooking critical updates.
Table of Contents
Section titled “Table of Contents”- Overview
- When to Create a Migration
- Migration Implementation
- Code Updates Required
- Test Implementation
- Mock Data Organization
- Best Practices
- Common Pitfalls
Overview
Section titled “Overview”Registry migrations run in source/main.bs before any data transformers or session loading occurs. This means:
- Migrations execute once per app version update
- By the time
SessionDataTransformerloads, only NEW names exist in the registry - Default values come from
settings/settings.jsonand are never written to registry unless explicitly changed by the user - Registry cleanup between tests is automatic via
BaseTestSuitewhenm.needsRegistrySetup = true
Migration Flow:
Main.bs startup → runGlobalMigrations() (for "JellyRock" section) → runRegistryUserMigrations() (for user sections) → Session loading starts → SessionDataTransformer reads registry (NEW names only)When to Create a Migration
Section titled “When to Create a Migration”Create a migration when:
- Renaming settings: Changing registry key names (e.g.,
playback.preferredAudioCodec→playbackPreferredMultichannelCodec) - Migrating values: Transforming setting values (e.g.,
"auto"→"eac3", deprecated values) - Removing settings: Deleting obsolete or server-authoritative data from registry
- Schema changes: Restructuring how data is stored (e.g., flat → nested)
Do NOT create migrations for:
- Adding new settings (defaults handle this)
- Server-authoritative data (policy, configuration) - these should never be in registry
- Temporary/session data
Migration Implementation
Section titled “Migration Implementation”Step 1: Add Version Constant
Section titled “Step 1: Add Version Constant”Add a new constant at the top of source/migrations.bs:
' client version when [description of what changed]const YOUR_MIGRATION_VERSION = "X.Y.Z"Naming Convention:
- Use
SCREAMING_SNAKE_CASE - End with
_MIGRATION_VERSIONsuffix - Be descriptive (e.g.,
AUDIO_CODEC_MIGRATION_VERSION,SETTINGS_MIGRATION_VERSION)
Example:
' client version when audio codec preference was renamed and migratedconst AUDIO_CODEC_MIGRATION_VERSION = "1.1.5"Step 2: Add Migration Block
Section titled “Step 2: Add Migration Block”Add a new migration block in chronological order (by version) in the appropriate function:
- Global settings (
globalRememberMe, etc.) → Add torunGlobalMigrations() - User settings (everything else) → Add to
runRegistryUserMigrations()
Template for User Settings Migration:
' YOUR_MIGRATION_VERSION - [Description]if isValid(lastRunVersion) and not versionChecker(lastRunVersion, YOUR_MIGRATION_VERSION) m.wasMigrated = true
' [Describe what this migration does] oldSettingName = "oldName" newSettingName = "newName"
if reg.exists(oldSettingName) print `Migrating [description] to v${YOUR_MIGRATION_VERSION} for userid: ${section}` oldValue = reg.read(oldSettingName)
' [Optional] Transform value if needed newValue = oldValue if oldValue = "deprecatedValue" newValue = "newDefaultValue" end if
reg.write(newSettingName, newValue) reg.delete(oldSettingName) print `Migrated ${oldSettingName}='${oldValue}' to ${newSettingName}='${newValue}'` reg.flush() else print `No migration needed for userid: ${section} (setting not found)` end ifend ifVersion Checking Logic:
not versionChecker(lastRunVersion, YOUR_MIGRATION_VERSION)This returns true when lastRunVersion < YOUR_MIGRATION_VERSION, meaning the migration should run.
Example: User on v1.1.4 with AUDIO_CODEC_MIGRATION_VERSION = "1.1.5":
- ✅ Migration runs because
1.1.4 < 1.1.5
Important: Multiple migrations can run in sequence! A user on v1.0.0 will run ALL migrations where 1.0.0 < MIGRATION_VERSION.
Step 3: Handle Value Migrations
Section titled “Step 3: Handle Value Migrations”When migrating values (not just names), apply transformation logic:
' Migrate deprecated values to new defaultsnewValue = "defaultValue" ' Set default first
if isValid(oldValue) and oldValue <> "" and oldValue <> "deprecatedValue1" and oldValue <> "deprecatedValue2" ' User had a valid non-deprecated value - preserve it newValue = oldValueend if⚠️ Critical Rule: Never write default values to registry! Only write values that the user explicitly set or that are being migrated from old values.
Code Updates Required
Section titled “Code Updates Required”When a migration renames a setting, update these files:
1. settings/settings.json
Section titled “1. settings/settings.json”Update the settingName field:
{ "title": "Setting Display Name", "description": "Setting description", "settingName": "newSettingName", // ← Update this "type": "radio", "default": "defaultValue"}2. components/data/jellyfin/JellyfinUserSettings.xml
Section titled “2. components/data/jellyfin/JellyfinUserSettings.xml”Update the field id:
<field id="newSettingName" type="string" alwaysNotify="true" />Note: Field type must match the setting type (boolean, integer, string).
3. source/data/SessionDataTransformer.bs
Section titled “3. source/data/SessionDataTransformer.bs”Update the transformer to read the NEW name:
' WRONG - reads old name (migrations already ran!)settingsNode.oldSettingName = settingsData["oldSettingName"] ?? ""
' CORRECT - reads new name (after migrations)settingsNode.newSettingName = settingsData["newSettingName"] ?? ""⚠️ Critical: SessionDataTransformer runs AFTER migrations, so it must use NEW names only. No backward compatibility needed!
4. Other Code References
Section titled “4. Other Code References”Search the codebase for any other references to the old setting name:
grep -r "oldSettingName" source/ components/Common places to check:
source/utils/deviceCapabilities.bs- Device profile logicsource/showScenes.bs- Scene configuration- Component logic that reads settings directly
Example:
' WRONGpreferredCodec = globalUserSettings.oldSettingName
' CORRECTpreferredCodec = globalUserSettings.newSettingName5. Update Setting Count Comments
Section titled “5. Update Setting Count Comments”Update the comment in source/migrations.bs that documents total settings:
' Define all XX setting migrations (old dotted name → new camelCase name)Count should include ALL settings in the migrations object, including auth settings like token and primaryimagetag.
Test Implementation
Section titled “Test Implementation”Test File Location
Section titled “Test File Location”Create or update test files in tests/source/integration/migration/:
SettingsMigration.spec.bs- Comprehensive test for ALL migrations (update this when adding new migrations)[YourFeature]Migration.spec.bs- Focused tests for your specific migration
Required Test Structure
Section titled “Required Test Structure”namespace tests
@suite("Your Migration - vX.Y.Z") @tags("migration") class YourMigrationTests extends tests.BaseTestSuite
protected override sub setup() m.needsRegistrySetup = true ' ← Enables automatic cleanup super.setup() end sub
' Tests here...
end class
end namespaceRequired Test Categories
Section titled “Required Test Categories”1. Basic Migration Test
Section titled “1. Basic Migration Test”Test that the migration runs and transforms settings correctly:
@it("migrates oldName to newName")function _() testUserId = "test-your-migration-001"
reg = CreateObject("roRegistrySection", testUserId) reg.write("LastRunVersion", "X.Y.Z-1") ' Version before your migration reg.write("oldSettingName", "testValue") reg.flush()
' WHEN: Migration runs (isolated to this user) runRegistryUserMigrations([testUserId])
' THEN: New setting exists with correct value reg = CreateObject("roRegistrySection", testUserId) m.assertTrue(reg.exists("newSettingName")) m.assertEqual(reg.read("newSettingName"), "testValue")
' AND: Old setting is deleted m.assertFalse(reg.exists("oldSettingName"))end function⚠️ Test Isolation: Always pass [testUserId] array to runRegistryUserMigrations() for edge case tests to prevent cross-test contamination.
2. Value Migration Tests (if applicable)
Section titled “2. Value Migration Tests (if applicable)”Test that deprecated values are transformed:
@it("migrates deprecated value 'auto' to 'newDefault'")function _() testUserId = "test-value-migration-001"
reg = CreateObject("roRegistrySection", testUserId) reg.write("LastRunVersion", "X.Y.Z-1") reg.write("oldSettingName", "auto") reg.flush()
runRegistryUserMigrations([testUserId])
reg = CreateObject("roRegistrySection", testUserId) m.assertEqual(reg.read("newSettingName"), "newDefault")end functionTest that valid values are preserved:
@it("preserves valid value during migration")function _() testUserId = "test-preserve-value-001"
reg = CreateObject("roRegistrySection", testUserId) reg.write("LastRunVersion", "X.Y.Z-1") reg.write("oldSettingName", "validValue") reg.flush()
runRegistryUserMigrations([testUserId])
reg = CreateObject("roRegistrySection", testUserId) m.assertEqual(reg.read("newSettingName"), "validValue")end function3. Edge Cases
Section titled “3. Edge Cases”Test partial/missing settings:
@it("handles missing old setting gracefully")function _() testUserId = "test-missing-001"
reg = CreateObject("roRegistrySection", testUserId) reg.write("LastRunVersion", "X.Y.Z-1") ' Don't write the old setting reg.flush()
runRegistryUserMigrations([testUserId])
reg = CreateObject("roRegistrySection", testUserId) m.assertFalse(reg.exists("newSettingName")) m.assertFalse(reg.exists("oldSettingName"))end functionTest mixed old/new settings (interrupted migration scenario):
@it("handles mixed old and new settings (old overwrites new)")function _() testUserId = "test-mixed-001"
reg = CreateObject("roRegistrySection", testUserId) reg.write("LastRunVersion", "X.Y.Z-1") reg.write("oldSettingName", "oldValue") reg.write("newSettingName", "newValue") ' Already exists! reg.flush()
runRegistryUserMigrations([testUserId])
reg = CreateObject("roRegistrySection", testUserId) m.assertEqual(reg.read("newSettingName"), "oldValue", "Old value should overwrite new") m.assertFalse(reg.exists("oldSettingName"))end function4. Version Checking
Section titled “4. Version Checking”Test that migration skips when already run:
@it("skips migration when version already >= vX.Y.Z")function _() testUserId = "test-skip-001"
reg = CreateObject("roRegistrySection", testUserId) reg.write("LastRunVersion", "X.Y.Z") ' Already on this version reg.write("oldSettingName", "value") reg.flush()
runRegistryUserMigrations([testUserId])
reg = CreateObject("roRegistrySection", testUserId) m.assertTrue(reg.exists("oldSettingName"), "Old setting should remain (no migration)") m.assertFalse(reg.exists("newSettingName"), "New setting should not exist")end functionTest that migration runs for older versions:
@it("runs migration for version vX.Y.Z-1")function _() testUserId = "test-run-001"
reg = CreateObject("roRegistrySection", testUserId) reg.write("LastRunVersion", "X.Y.Z-1") reg.write("oldSettingName", "value") reg.flush()
runRegistryUserMigrations([testUserId])
reg = CreateObject("roRegistrySection", testUserId) m.assertTrue(reg.exists("newSettingName")) m.assertFalse(reg.exists("oldSettingName"))end function5. Multi-User Independence
Section titled “5. Multi-User Independence”Test that multiple users migrate independently:
@it("migrates multiple users independently with different values")function _() user1 = "test-multi-user-001" user2 = "test-multi-user-002"
reg1 = CreateObject("roRegistrySection", user1) reg1.write("LastRunVersion", "X.Y.Z-1") reg1.write("oldSettingName", "value1") reg1.flush()
reg2 = CreateObject("roRegistrySection", user2) reg2.write("LastRunVersion", "X.Y.Z-1") reg2.write("oldSettingName", "value2") reg2.flush()
' Migrate both users runRegistryUserMigrations([user1, user2])
reg1 = CreateObject("roRegistrySection", user1) m.assertEqual(reg1.read("newSettingName"), "value1")
reg2 = CreateObject("roRegistrySection", user2) m.assertEqual(reg2.read("newSettingName"), "value2")end function6. ALL Migrations Test (Update when adding new migrations)
Section titled “6. ALL Migrations Test (Update when adding new migrations)”⚠️ Critical: Update SettingsMigration.spec.bs test “migrates all user settings from old to new names” to include your new migration. This ensures that running ALL migrations in sequence doesn’t corrupt data.
Example:
@it("migrates all user settings from old to new names")function _() testUserId = "test-migration-user-001"
reg = CreateObject("roRegistrySection", testUserId) reg.write("LastRunVersion", "1.0.0") ' Old version - runs ALL migrations
' Write OLD setting names for ALL migrations reg.write("playback.preferredAudioCodec", "ac3") ' v1.1.0 migration reg.write("your.old.setting", "value") ' Your new migration reg.flush()
runRegistryUserMigrations() ' Run ALL migrations
reg = CreateObject("roRegistrySection", testUserId)
' Verify settings migrated through ALL versions correctly m.assertTrue(reg.exists("playbackPreferredMultichannelCodec")) ' After v1.1.0 AND v1.1.5 m.assertEqual(reg.read("playbackPreferredMultichannelCodec"), "ac3") m.assertFalse(reg.exists("playbackPreferredAudioCodec"), "Intermediate should be deleted")
m.assertTrue(reg.exists("yourNewSettingName")) m.assertEqual(reg.read("yourNewSettingName"), "value")end functionTest Registry Section Naming
Section titled “Test Registry Section Naming”⚠️ Always use "test-" prefix for test registry sections:
' CORRECT - auto-detected as test modetestUserId = "test-migration-user-001"testUserId = "test-audio-codec-001"
' WRONG - might touch production data!testUserId = "migration-user-001"The migration code automatically detects test mode when ANY section starts with "test-" and will ONLY migrate test sections in that scenario.
Automatic Cleanup
Section titled “Automatic Cleanup”Cleanup is automatic when you set m.needsRegistrySetup = true in setup():
protected override sub setup() m.needsRegistrySetup = true ' ← Triggers automatic cleanup via BaseTestSuite super.setup()end subManual cleanup should NEVER be needed unless testing global sections like "test-global" (which are explicitly skipped by migrations). In that case:
' Cleanup for explicitly skipped sectionstestGlobalReg.delete("settingName")testGlobalReg.delete("LastRunVersion")testGlobalReg.flush()Mock Data Organization
Section titled “Mock Data Organization”Directory Structure
Section titled “Directory Structure”Organize mock data by data source and category:
tests/source/mocks/├── api/ # Jellyfin Server API responses│ ├── deviceProfiles/ # Device profile mocks│ │ ├── device-profile-8ch-passthrough.json│ │ ├── device-profile-stereo-only.json│ │ └── device-profile-aac-only.json│ └── items/ # Item metadata mocks│ ├── movie-basic.json│ ├── episode-basic.json│ └── series-basic.json├── registry/ # Roku Registry data│ └── userSettings/ # User settings from registry│ ├── user-settings-all-new-names.json│ └── user-settings-all-old-names.json├── roku/ # Roku Device API responses│ ├── deviceInfo/ # Device info mocks│ └── capabilities/ # Capability mocks├── devices/ # Existing device mocks├── servers/ # Existing server mocks└── users/ # Existing user mocksLoading Mock Data in Tests
Section titled “Loading Mock Data in Tests”⚠️ Critical: Use centralized MockDataLoader functions only!
All mock data loading helpers MUST be in tests/source/shared/MockDataLoader.bs. Never create duplicate helper functions in individual test files. This ensures consistency and maintainability.
Available MockDataLoader functions:
' Load item mocks (movies, episodes, series, programs)mockItem = MockDataLoader.LoadItem("movie-basic")
' Load device profile mocksmockProfile = MockDataLoader.LoadDeviceProfile("device-profile-8ch-passthrough")
' Load registry user settings mocksmockSettings = MockDataLoader.LoadRegistryUserSettings("user-settings-all-new-names")
' Load Roku device info mocksmockDeviceInfo = MockDataLoader.LoadRokuDeviceInfo("roku-ultra-capabilities")
' Load Roku capabilities mocksmockCapabilities = MockDataLoader.LoadRokuCapabilities("audio-codecs-full")
' Load server/user/device test fixtures (existing functions)mockServer = MockDataLoader.LoadServer("default")mockUser = MockDataLoader.LoadUser("admin")mockDevice = MockDataLoader.LoadDevice("roku-ultra")If you need a new mock loading function:
- Add it to
tests/source/shared/MockDataLoader.bsnamespace - Follow the naming convention:
Load[Category](name as string) as object - Use the pattern:
filePath = "pkg:/source/tests/mocks/[source]/[category]/" + name + ".json" - Update this documentation to include the new function
Creating New Mock Files
Section titled “Creating New Mock Files”When creating mocks for a migration:
- Create
*-old-names.jsonwith settings using OLD names (pre-migration state) - Create
*-new-names.jsonwith settings using NEW names (post-migration state) - Place in appropriate subfolder based on data source
Example:
tests/source/mocks/registry/userSettings/├── user-settings-all-old-names.json # Pre-migration (dotted names)└── user-settings-all-new-names.json # Post-migration (camelCase names)Best Practices
Section titled “Best Practices”1. Migration Design
Section titled “1. Migration Design”- ✅ DO keep migrations simple and focused on one change
- ✅ DO add migrations in chronological order by version
- ✅ DO preserve user data whenever possible
- ✅ DO print log messages for debugging (visible in test output and production logs)
- ❌ DON’T combine multiple unrelated changes in one migration
- ❌ DON’T write default values to registry (let
settings.jsonhandle defaults) - ❌ DON’T add backward compatibility to transformers (migrations handle this)
2. Version Management
Section titled “2. Version Management”- Use semantic versioning (MAJOR.MINOR.PATCH)
- Migration versions should match the app version where they were introduced
- Test that migrations run correctly when jumping multiple versions (e.g., v1.0.0 → v1.2.0 should run all intermediate migrations)
3. Testing Strategy
Section titled “3. Testing Strategy”- Test in isolation (one user at a time with
[testUserId]parameter) - Test with multiple users to verify independence
- Test version boundaries (version before migration, at migration, after migration)
- Update the comprehensive “all migrations” test when adding new migrations
- Always use
"test-"prefix for test registry sections
4. Code Organization
Section titled “4. Code Organization”- Keep migration logic in
source/migrations.bsonly - Update transformers to use NEW names only (no backward compatibility)
- Use descriptive constant names for migration versions
- Document what each migration does in comments
5. Registry Best Practices
Section titled “5. Registry Best Practices”- NEVER write default values to registry (only write user changes)
- ALWAYS delete old setting names after migration
- ALWAYS flush registry after writes (
reg.flush()) - VERIFY that server-authoritative data (policy, configuration) is NOT in registry
Common Pitfalls
Section titled “Common Pitfalls”1. ❌ Transformer Reading Old Names
Section titled “1. ❌ Transformer Reading Old Names”Problem:
' SessionDataTransformer.bssettingsNode.oldSettingName = settingsData["oldSettingName"] ?? ""Why this fails: Migrations run BEFORE transformers. By the time the transformer runs, only NEW names exist in the registry.
Solution:
```brighterscriptsettingsNode.newSettingName = settingsData["newSettingName"] ?? ""2. ❌ Writing Default Values to Registry
Section titled “2. ❌ Writing Default Values to Registry”Problem:
if not reg.exists("newSettingName") reg.write("newSettingName", "defaultValue") ' ❌ WRONG!end ifWhy this fails: Default values come from settings/settings.json. Writing them to registry defeats the purpose of having a single source of truth.
Solution:
' Only write when migrating from an existing old valueif reg.exists("oldSettingName") oldValue = reg.read("oldSettingName") reg.write("newSettingName", oldValue) reg.delete("oldSettingName")end if' If old setting doesn't exist, don't write anything!3. ❌ Test Registry Section Naming
Section titled “3. ❌ Test Registry Section Naming”Problem:
testUserId = "migration-test-001" ' ❌ Missing "test-" prefixWhy this fails: Migration code won’t detect test mode and might process production registry sections.
Solution:
testUserId = "test-migration-001" ' ✅ Starts with "test-"4. ❌ Not Isolating Tests
Section titled “4. ❌ Not Isolating Tests”Problem:
' Edge case testrunRegistryUserMigrations() ' ❌ Runs on ALL sections!Why this fails: Test data from previous tests accumulates, causing cross-test contamination.
Solution:
```brighterscriptrunRegistryUserMigrations([testUserId]) ' ✅ Isolated to one user5. ❌ Forgetting to Update ALL Migrations Test
Section titled “5. ❌ Forgetting to Update ALL Migrations Test”Problem: Adding a new migration but not updating the comprehensive test in SettingsMigration.spec.bs.
Why this fails: Multi-version upgrades (e.g., v1.0.0 → v1.2.0) might not be tested, leading to data corruption when multiple migrations run in sequence.
Solution: Always update the “migrates all user settings from old to new names” test to include your new migration’s settings.
6. ❌ Incorrect Version Skip Logic
Section titled “6. ❌ Incorrect Version Skip Logic”Problem:
@it("skips migration when version already migrated (v1.1.0+)")function _() reg.write("LastRunVersion", "1.1.0") ' ❌ Only skips v1.1.0, NOT v1.1.5!Why this fails: If there are multiple migrations (e.g., v1.1.0 and v1.1.5), setting LastRunVersion = "1.1.0" will skip v1.1.0 but still run v1.1.5.
Solution:
@it("skips all migrations when version already migrated (v1.1.5+)")function _() reg.write("LastRunVersion", "1.1.5") ' ✅ Skips ALL migrations up to v1.1.57. ❌ Not Searching for All Code References
Section titled “7. ❌ Not Searching for All Code References”Problem: Updating the migration and transformer but missing a reference in deviceCapabilities.bs that still uses the old name.
Solution: Always grep the entire codebase:
grep -r "oldSettingName" source/ components/Task Checklist
Section titled “Task Checklist”When implementing a registry migration, use this checklist to ensure nothing is overlooked.
Note: Checklist states are for local tracking only. Please reset all checkboxes to
[ ]before committing changes to this file.
Phase 1: Migration Code
Section titled “Phase 1: Migration Code”- Add version constant to
source/migrations.bs(SCREAMING_SNAKE_CASE with_MIGRATION_VERSIONsuffix) - Add migration block in chronological order in appropriate function (
runGlobalMigrations()orrunRegistryUserMigrations()) - Implement value transformation logic (if migrating values, not just names)
- Add print statements for debugging and production logs
- Verify migration uses
reg.flush()after writes - Verify old setting names are deleted after migration
Phase 2: Code Updates
Section titled “Phase 2: Code Updates”- Update
settings/settings.jsonwith newsettingName - Update
components/data/jellyfin/JellyfinUserSettings.xmlfieldid - Update
source/data/SessionDataTransformer.bsto read NEW name only - Search codebase for other references:
grep -r "oldSettingName" source/ components/ - Update any component logic that reads the setting directly
- Update setting count comment in
source/migrations.bs - Verify no default values are being written to registry
Phase 3: Test Implementation
Section titled “Phase 3: Test Implementation”- Create or update test file in
tests/source/integration/migration/ - Extend
tests.BaseTestSuitewithm.needsRegistrySetup = trueinsetup() - Add basic migration test (old name → new name)
- Add value migration tests (if applicable): deprecated values and preserved values
- Add edge case tests: missing setting, empty registry, mixed old/new settings
- Add version checking tests: skip when already migrated, run for older versions
- Add multi-user independence test
- Update
SettingsMigration.spec.bs“migrates all user settings” test to include new migration - Verify all test registry sections use
"test-"prefix - Verify edge case tests use isolated execution:
runRegistryUserMigrations([testUserId])
Phase 4: Mock Data
Section titled “Phase 4: Mock Data”- Create mock data files in appropriate
tests/source/mocks/subfolder (i.e.api/,registry/,roku/) - Create
*-old-names.jsonmock (pre-migration state) - Create
*-new-names.jsonmock (post-migration state) - If new helper function needed, add to
tests/source/shared/MockDataLoader.bs(NOT individual test files) - Verify mock data structure matches actual API/registry responses
- Update documentation if new
MockDataLoaderfunction added
Phase 5: Testing & Validation
Section titled “Phase 5: Testing & Validation”- Format code:
npm run format - Verify no new IDE code errors introduced
- Test locally on device with real registry data (if possible)
- Verify logs show migrations running correctly
Phase 6: Documentation & Commit
Section titled “Phase 6: Documentation & Commit”- Add descriptive commit message following conventional commits format
- Update this document if any new patterns or pitfalls were discovered
- Reset this checklist to unchecked state before committing
- Create PR with comprehensive description of migration and testing
Questions or Issues?
Section titled “Questions or Issues?”If you encounter issues not covered in this guide:
- Check the git history for similar migrations:
git log --all --grep="migration" - Search test files for patterns:
grep -r "@suite.*Migration" tests/ - Review existing migration blocks in
source/migrations.bs - Ask for clarification in PR reviews or team discussions
Remember: When in doubt, isolate tests, verify version logic, and always check that only NEW names are used after migrations run!