diff --git a/scripts/lib/install-options.py b/scripts/lib/install-options.py index 102bcb4..6b610cf 100644 --- a/scripts/lib/install-options.py +++ b/scripts/lib/install-options.py @@ -129,11 +129,9 @@ def merge_toml(current_text, managed_text, statusline_enabled=True): owned_hook = any(re.search(r'(?:flashbang|statusline|record-compact|session-state-pointer)\.(?:ps1|sh)', line, re.I) for line in block) if plugin_table and header in managed_headers: raise ValueError('Cannot merge conflicting managed plugin tables.') - if (plugin_table and header not in managed_headers) or (array_table and not owned_hook): + if (header not in managed_headers and not array_table) or (array_table and not owned_hook): extras.extend(block) continue - if header not in managed_headers: - continue if array_table: continue generated_block = next(lines for name, lines in managed_sections if name == header) diff --git a/scripts/tests/test-install-options.py b/scripts/tests/test-install-options.py index 12988a6..0aa361a 100644 --- a/scripts/tests/test-install-options.py +++ b/scripts/tests/test-install-options.py @@ -31,6 +31,19 @@ class InstallOptionsTests(unittest.TestCase): self.assertIn(plugin_state, result) + def test_merge_toml_preserves_unmanaged_tables(self): + foreign = ('[mcp_servers.demo]\ncommand = "demo"\n\n[mcp_servers.demo.env]\nTOKEN_NAME = "x"\n\n' + '[model_providers.custom]\nname = "Custom"\nbase_url = "https://example.invalid"\n') + current = "model = 'old'\n\n[tui]\nstatus_line = ['a']\ntheme = 'dark'\n\n" + foreign + managed = "model = 'new'\n\n[tui]\nstatus_line = ['b']\n" + + result = tomllib.loads(OPTIONS.merge_toml(current, managed)) + + self.assertEqual(result['model'], 'new') + self.assertEqual(result['tui'], {'status_line': ['b'], 'theme': 'dark'}) + self.assertEqual(result['mcp_servers'], {'demo': {'command': 'demo', 'env': {'TOKEN_NAME': 'x'}}}) + self.assertEqual(result['model_providers'], {'custom': {'name': 'Custom', 'base_url': 'https://example.invalid'}}) + def test_filter_preserves_other_claude_hooks_and_status(self): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / 'settings.json' diff --git a/scripts/tests/test-managed-install.ps1 b/scripts/tests/test-managed-install.ps1 index acef5b7..5e57fd2 100644 --- a/scripts/tests/test-managed-install.ps1 +++ b/scripts/tests/test-managed-install.ps1 @@ -63,7 +63,7 @@ try { $null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins' $mergedConfig = [System.IO.File]::ReadAllText($configPath) if (-not ($mergedConfig -replace "`r`n", "`n").Contains(($pluginState -replace "`r`n", "`n"))) { throw 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.' } - if (-not $mergedConfig.Contains("model = 'old'") -or -not $mergedConfig.Contains("model_reasoning_effort = 'high'") -or -not $mergedConfig.Contains("approvals_reviewer = 'auto_review'") -or $mergedConfig.Contains("approvals_reviewer = 'user'") -or $mergedConfig.Contains('[other]')) { throw 'Updating config.toml must preserve local model settings and enforce automatic review.' } + if (-not $mergedConfig.Contains("model = 'old'") -or -not $mergedConfig.Contains("model_reasoning_effort = 'high'") -or -not $mergedConfig.Contains("approvals_reviewer = 'auto_review'") -or $mergedConfig.Contains("approvals_reviewer = 'user'") -or -not $mergedConfig.Contains('[other]')) { throw 'Updating config.toml must preserve local settings and tables and enforce automatic review.' } if ($mergedConfig.IndexOf("model = 'old'") -gt $mergedConfig.IndexOf('[tui]') -or $mergedConfig.IndexOf("model_reasoning_effort = 'high'") -gt $mergedConfig.IndexOf('[tui]')) { throw 'Model settings must remain in the TOML root table.' } if ([System.IO.File]::ReadAllText((Join-Path $destination 'backups/plugins/config.toml')) -ne $originalConfig) { throw 'Original plugin configuration must be backed up.' } $null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins-repeat' diff --git a/scripts/tests/test-managed-install.sh b/scripts/tests/test-managed-install.sh index b93ddb4..9c2f013 100644 --- a/scripts/tests/test-managed-install.sh +++ b/scripts/tests/test-managed-install.sh @@ -74,7 +74,7 @@ merged_config=$(cat "$destination/config.toml") merged_config_normalized=${merged_config//$'\r'/} plugin_state_normalized=${plugin_state//$'\r'/} [[ "$merged_config_normalized" == *"$plugin_state_normalized"* ]] || { printf 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.\n' >&2; exit 1; } -[[ "$merged_config" == *"model = 'old'"* && "$merged_config" == *"model_reasoning_effort = 'high'"* && "$merged_config" == *"approvals_reviewer = 'auto_review'"* && "$merged_config" != *"approvals_reviewer = 'user'"* && "$merged_config" != *'[other]'* ]] || { printf 'Updating config.toml must preserve local model settings and enforce automatic review.\n' >&2; exit 1; } +[[ "$merged_config" == *"model = 'old'"* && "$merged_config" == *"model_reasoning_effort = 'high'"* && "$merged_config" == *"approvals_reviewer = 'auto_review'"* && "$merged_config" != *"approvals_reviewer = 'user'"* && "$merged_config" == *'[other]'* ]] || { printf 'Updating config.toml must preserve local model settings and enforce automatic review.\n' >&2; exit 1; } [ "$(printf '%s\n' "$merged_config" | grep -n "model = 'old'" | cut -d: -f1)" -lt "$(printf '%s\n' "$merged_config" | grep -n '^\[tui\]$' | cut -d: -f1)" ] || { printf 'Model must remain in TOML root table.\n' >&2; exit 1; } [ "$(printf '%s\n' "$merged_config" | grep -n "model_reasoning_effort = 'high'" | cut -d: -f1)" -lt "$(printf '%s\n' "$merged_config" | grep -n '^\[tui\]$' | cut -d: -f1)" ] || { printf 'Model effort must remain in TOML root table.\n' >&2; exit 1; } [ "$(cat "$destination/backups/plugins/config.toml")" = "$original_config" ] || { printf 'Original plugin configuration must be backed up.\n' >&2; exit 1; }