From 1b08d70a71f3039460b9de4b7a6e40564de90368 Mon Sep 17 00:00:00 2001 From: Nick Yasnohorodskyi Date: Wed, 17 Jun 2026 10:58:13 +0200 Subject: [PATCH 01/10] feat: implement per-folder reset overrides [IDE-1945] Make the per-folder "Reset overrides" button in the HTML config dialog visible and functional end-to-end. - Show the button (drop display:none) and move it into the folder disclaimer banner, right-aligned, red. - Fix JS reset bugs: key resets by folderPath instead of the compacted folder index, and emit a reset-only folder even when it has no other edits. Emit flat null for all 14 org-scope folder fields (now incl. preferred_org); the IDE maps null -> {value:null, changed:true}. - Make preferred_org resettable in applyPreferredOrg: a null reset Unsets both user:folder:preferred_org and user:folder:org_set_by_user so the folder reverts to its auto-determined / global org. - Add JS folder-reset tests, a Go unit test for the org reset, and an end-to-end UpdateSettings reset test. - Document the folder-override reset contract in configuration.md and configuration-dialog.md. - Regenerate config dialog HTML fixtures. --- application/server/configuration_test.go | 74 +++++++ docs/configuration-dialog.md | 26 +++ docs/configuration.md | 17 ++ .../configuration/template/config.html | 4 +- .../template/js/ui/form-handler.js | 83 +++++-- .../template/js/ui/reset-handler.js | 29 ++- .../configuration/template/styles.css | 24 +- internal/types/config_resolver_test.go | 54 +++++ internal/types/folder_config.go | 18 ++ js-tests/folder-reset.test.mjs | 208 ++++++++++++++++++ .../config_output_multi_project.html | 152 +++++++++---- .../config_output_no_projects.html | 136 +++++++++--- .../config_output_single_solution.html | 140 ++++++++---- 13 files changed, 813 insertions(+), 152 deletions(-) create mode 100644 js-tests/folder-reset.test.mjs diff --git a/application/server/configuration_test.go b/application/server/configuration_test.go index 05f11eb50..5157f3e62 100644 --- a/application/server/configuration_test.go +++ b/application/server/configuration_test.go @@ -717,6 +717,80 @@ func Test_UpdateSettings(t *testing.T) { }) } +// Test_updateFolderConfig_ResetClearsUserOverrides_EndToEnd drives the full HTML-save reset path: +// the IDE plugin maps each flat-null folder field to {Value:nil, Changed:true}, and UpdateSettings +// must Unset the user:folder: override so the effective value falls back to org/LDX/default. +// preferred_org is the special case — its reset also clears org_set_by_user, reverting to auto/global org. +func Test_updateFolderConfig_ResetClearsUserOverrides_EndToEnd(t *testing.T) { + engine, tokenService := testutil.UnitTestWithEngine(t) + deps := di.TestInit(t, engine, tokenService, nil) + conf := engine.GetConfiguration() + logger := engine.GetLogger() + + folderDir := filepath.Join(t.TempDir(), "folder") + require.NoError(t, initTestRepo(t, folderDir)) + folderPath := types.FilePath(folderDir) + + ctx := ctx2.NewContextWithDependencies(t.Context(), map[string]any{ + ctx2.DepNotifier: deps.Notifier, + ctx2.DepAuthService: deps.AuthenticationService, + ctx2.DepConfigResolver: deps.ConfigResolver, + ctx2.DepFeatureFlagService: deps.FeatureFlagService, + ctx2.DepLdxSyncService: deps.LdxSyncService, + }) + resolver := testutil.DefaultConfigResolver(engine) + + // Step 1: seed user overrides on the folder (the "user edited overrides" state). + seed := []types.LspFolderConfig{{ + FolderPath: folderPath, + Settings: map[string]*types.ConfigSetting{ + types.SettingSnykCodeEnabled: {Value: true, Changed: true}, + types.SettingScanAutomatic: {Value: true, Changed: true}, + types.SettingPreferredOrg: {Value: "my-folder-org", Changed: true}, + }, + }} + UpdateSettings(ctx, conf, engine, logger, map[string]*types.ConfigSetting{}, seed, analytics.TriggerSourceTest, resolver) + + require.True(t, types.HasUserOverride(conf, folderPath, types.SettingSnykCodeEnabled), "snyk_code_enabled override seeded") + require.True(t, types.HasUserOverride(conf, folderPath, types.SettingScanAutomatic), "scan_automatic override seeded") + require.True(t, types.HasUserOverride(conf, folderPath, types.SettingPreferredOrg), "preferred_org override seeded") + require.True(t, types.HasUserOverride(conf, folderPath, types.SettingOrgSetByUser), "org_set_by_user set alongside preferred_org") + + seededFC := config.GetFolderConfigFromEngine(engine, resolver, folderPath, logger) + require.Equal(t, "my-folder-org", seededFC.PreferredOrg(), "seeded preferred_org is effective") + + // Step 2: simulate the HTML-save reset — the IDE maps flat-null to {Value:nil, Changed:true}. + reset := []types.LspFolderConfig{{ + FolderPath: folderPath, + Settings: map[string]*types.ConfigSetting{ + types.SettingSnykCodeEnabled: {Value: nil, Changed: true}, + types.SettingScanAutomatic: {Value: nil, Changed: true}, + types.SettingPreferredOrg: {Value: nil, Changed: true}, + }, + }} + UpdateSettings(ctx, conf, engine, logger, map[string]*types.ConfigSetting{}, reset, analytics.TriggerSourceTest, resolver) + + // Step 3: every user:folder: override is cleared and the underlying key is no longer set. + assert.False(t, types.HasUserOverride(conf, folderPath, types.SettingSnykCodeEnabled), "snyk_code_enabled override cleared") + assert.False(t, types.HasUserOverride(conf, folderPath, types.SettingScanAutomatic), "scan_automatic override cleared") + assert.False(t, types.HasUserOverride(conf, folderPath, types.SettingPreferredOrg), "preferred_org override cleared") + assert.False(t, types.HasUserOverride(conf, folderPath, types.SettingOrgSetByUser), "org_set_by_user cleared by org reset") + + // After Unset the key no longer holds an active *LocalConfigField override (Unset writes a + // tombstone, so conf.IsSet may still report true — HasUserOverride is the real contract). + fp := string(types.PathKey(folderPath)) + for _, key := range []string{types.SettingSnykCodeEnabled, types.SettingPreferredOrg, types.SettingOrgSetByUser} { + got := conf.Get(configresolver.UserFolderKey(fp, key)) + lf, ok := got.(*configresolver.LocalConfigField) + assert.False(t, ok && lf != nil && lf.Changed, "no active override should remain for %s after reset", key) + } + + // Effective org reverts to auto/global (empty in this unit setup with no global org). + resetFC := config.GetFolderConfigFromEngine(engine, resolver, folderPath, logger) + assert.False(t, resetFC.OrgSetByUser(), "OrgSetByUser reverts to false after reset") + assert.Empty(t, resetFC.PreferredOrg(), "PreferredOrg falls back after reset") +} + func initTestRepo(t *testing.T, tempDir string) error { t.Helper() repo1, err := git.PlainInit(tempDir, false) diff --git a/docs/configuration-dialog.md b/docs/configuration-dialog.md index 4db285302..f972f3b3b 100644 --- a/docs/configuration-dialog.md +++ b/docs/configuration-dialog.md @@ -151,6 +151,32 @@ On save (or auto-save), `features/auto-save.js` calls `form-handler.collectData( See [Saving Configuration Flow](#saving-configuration-flow) for the sequence diagram. +### 4a. Resetting folder overrides + +The per-folder **"Reset overrides"** button (one per folder tab, top-right in the disclaimer banner) clears all of that folder's user overrides so the effective values fall back to org / LDX-sync / default. + +**JS side (snyk-ls, already implemented):** clicking the button marks the folder *by its `folderPath`* (read from the hidden `folder__folderPath` input — not the index, which is compacted away during diffing). On save, `form-handler.applyFolderResets()` emits **flat `null`** for each of these 14 folder fields on that folder's entry in `folderConfigs`: + +``` +scan_automatic, scan_net_new, +severity_filter_critical, severity_filter_high, severity_filter_medium, severity_filter_low, +snyk_oss_enabled, snyk_code_enabled, snyk_iac_enabled, snyk_secrets_enabled, +issue_view_open_issues, issue_view_ignored_issues, risk_score_threshold, +preferred_org +``` + +A reset is emitted **even if the folder has no other edits** — a reset-only folder still appears in the outbound `folderConfigs` keyed by `folderPath`. The JS deliberately emits **flat snake_case `null`**, not a `ConfigSetting` envelope; building the envelope is the IDE plugin's job (next paragraph). + +**The IDE plugin MUST map a flat `null` folder field → `{value: null, changed: true}`** for that folder key when building `workspace/didChangeConfiguration`. This is the cross-IDE reset contract: + +| Saved JSON folder field | IDE maps to `ConfigSetting` | LS effect | +|---|---|---| +| absent | omit from the map | no-op | +| present, non-null value | `{value: X, changed: true}` | set `user:folder::` | +| **present, `null`** | `{value: null, changed: true}` | **Unset** the `user:folder:` override → fall back | + +A plugin that treats a flat `null` as "absent" (e.g. nullable-type `?.let{}`, `node.isNull() → continue`, `.HasValue == false`) will silently drop the reset and the button will appear to do nothing. The null must survive deserialization (parse the raw JSON tree to distinguish present-with-null from absent when the data model can't) and reach the LS as `{value: null, changed: true}`. See the reset contract in [configuration.md](configuration.md#resetting-a-folder-override). + ### 5. Authentication Flow When the user clicks **Authenticate**, `features/authentication.js` calls `ConfigApp.ideBridge.login(authMethod, endpoint, insecure)`, which invokes **`window.__ideExecuteCommand__('snyk.login', [authMethod, endpoint, insecure])`**. diff --git a/docs/configuration.md b/docs/configuration.md index 4773dc2bf..e84a8ef1a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -456,6 +456,23 @@ The `Changed` field on `ConfigSetting` controls whether the LS processes a setti This applies uniformly to both initialization (`InitializeSettings`) and runtime updates (`UpdateSettings`). The IDE is responsible for setting `Changed: true` only on settings the user explicitly configured. +### Resetting a folder override + +A `ConfigSetting` value of `nil` combined with `Changed: true` is a **reset**: it tells the LS to `Unset` the `user:folder::` override so the effective value falls back through the precedence chain (org / LDX-sync / GAF default). This is how the HTML settings dialog's per-folder "Reset overrides" button clears a folder's user edits. + +| `ConfigSetting` on the wire | LS behaviour | +|---|---| +| absent from the map (`cs == nil`) | no-op | +| `{Changed: false}` (or omitted) | no-op (skipped, as above) | +| `{Changed: true, Value: }` | set `user:folder:` override to `` | +| `{Changed: true, Value: nil}` | **Unset** `user:folder:` override → fall back | + +The null-reset applies to org-scope folder fields (those resolved through `applyGenericFolderOverrides`), which have a lower-precedence layer to fall back to. **Basic folder-native fields** (`base_branch`, `scan_command_config`, etc.) have no fallback layer, so a null value on them is treated as a no-op rather than a reset. + +`preferred_org` is the one exception among the basic-handled fields: it *does* have a fallback (the auto-determined LDX org, then the global org), so its null-reset is honoured. Resetting `preferred_org` Unsets **both** `user:folder:preferred_org` and `user:folder:org_set_by_user`, reverting the folder to its auto-determined / global org. See `applyPreferredOrg` in `internal/types/folder_config.go`. + +Locked-field resets are rejected exactly like locked-field sets: `validateLockedFields` (`application/server/configuration.go`) strips a reset for a field locked by org policy before `ApplyLspUpdate` runs, so the locked value is preserved. + ### IDE → LS Flow (didChangeConfiguration) ```mermaid diff --git a/infrastructure/configuration/template/config.html b/infrastructure/configuration/template/config.html index f38ab6c31..c6c7c7dd9 100644 --- a/infrastructure/configuration/template/config.html +++ b/infrastructure/configuration/template/config.html @@ -397,8 +397,9 @@

Filters and views

-
+

These settings override the {{$.FolderLabel | toLower}} defaults for this specific {{$.FolderLabel | toLower}}.

+
@@ -690,7 +691,6 @@

Filters and views

- {{end}} {{end}} diff --git a/infrastructure/configuration/template/js/ui/form-handler.js b/infrastructure/configuration/template/js/ui/form-handler.js index 379f7bcf2..66af3f12f 100644 --- a/infrastructure/configuration/template/js/ui/form-handler.js +++ b/infrastructure/configuration/template/js/ui/form-handler.js @@ -206,41 +206,78 @@ } } - // Mark a folder for complete reset (all org-scope overrides will be set to null) - formHandler.markFolderForReset = function (folderIndex) { + // Org-scope folder override fields cleared by a folder reset. Emitting flat null + // for each tells the IDE plugin to send {value:null, changed:true}, which makes + // snyk-ls Unset the user:folder: override so the value falls back to org/LDX/default. + // preferred_org is included: snyk-ls special-cases its null-reset to also unset + // org_set_by_user, reverting the folder to its auto-determined / global org. + var FOLDER_RESET_FIELDS = [ + "scan_automatic", + "scan_net_new", + "severity_filter_critical", + "severity_filter_high", + "severity_filter_medium", + "severity_filter_low", + "snyk_oss_enabled", + "snyk_code_enabled", + "snyk_iac_enabled", + "snyk_secrets_enabled", + "issue_view_open_issues", + "issue_view_ignored_issues", + "risk_score_threshold", + "preferred_org", + ]; + + // Mark a folder for complete reset (all org-scope overrides will be set to null). + // Keyed by folderPath, not index: collectChangedData() compacts folderConfigs, so an + // index captured at click time no longer maps to the same entry in the saved payload. + formHandler.markFolderForReset = function (folderPath) { + if (!folderPath) return; window.ConfigApp.folderResets = window.ConfigApp.folderResets || {}; - window.ConfigApp.folderResets[folderIndex] = true; + window.ConfigApp.folderResets[folderPath] = true; }; // Check if a folder is marked for reset - formHandler.isFolderMarkedForReset = function (folderIndex) { - return ( + formHandler.isFolderMarkedForReset = function (folderPath) { + return !!( window.ConfigApp.folderResets && - window.ConfigApp.folderResets[folderIndex] + window.ConfigApp.folderResets[folderPath] ); }; - // Apply reset: set all org-scope fields to null on the folder config + // Apply reset: set all org-scope fields to null on each reset-marked folder. + // A reset-only folder (no other edits) is dropped by collectChangedData's diff, so + // add a fresh entry for any marked folderPath missing from data.folderConfigs. formHandler.applyFolderResets = function (data) { - if (!data.folderConfigs) return; + var marked = window.ConfigApp.folderResets; + if (!marked) return; + + data.folderConfigs = data.folderConfigs || []; + + // Index existing entries by folderPath. + var byPath = {}; for (var i = 0; i < data.folderConfigs.length; i++) { - if (formHandler.isFolderMarkedForReset(i) && data.folderConfigs[i]) { - var fc = data.folderConfigs[i]; - fc.scan_automatic = null; - fc.scan_net_new = null; - fc.severity_filter_critical = null; - fc.severity_filter_high = null; - fc.severity_filter_medium = null; - fc.severity_filter_low = null; - fc.snyk_oss_enabled = null; - fc.snyk_code_enabled = null; - fc.snyk_iac_enabled = null; - fc.snyk_secrets_enabled = null; - fc.issue_view_open_issues = null; - fc.issue_view_ignored_issues = null; - fc.risk_score_threshold = null; + var fc = data.folderConfigs[i]; + if (fc && fc.folderPath) { + byPath[fc.folderPath] = fc; + } + } + + for (var folderPath in marked) { + if (!marked.hasOwnProperty(folderPath) || !marked[folderPath]) continue; + + var entry = byPath[folderPath]; + if (!entry) { + entry = { folderPath: folderPath }; + data.folderConfigs.push(entry); + byPath[folderPath] = entry; + } + + for (var f = 0; f < FOLDER_RESET_FIELDS.length; f++) { + entry[FOLDER_RESET_FIELDS[f]] = null; } } + window.ConfigApp.folderResets = {}; }; diff --git a/infrastructure/configuration/template/js/ui/reset-handler.js b/infrastructure/configuration/template/js/ui/reset-handler.js index 1b9877ecc..f13bcab41 100644 --- a/infrastructure/configuration/template/js/ui/reset-handler.js +++ b/infrastructure/configuration/template/js/ui/reset-handler.js @@ -87,17 +87,36 @@ return; } - if (!confirm("Reset all overrides for this folder to defaults? Your custom overrides will be removed.")) { + // Resolve the folderPath from the hidden input so resets are keyed by path, not the + // (later compacted) index. Without a path we cannot reliably target the folder on save. + var folderPath = null; + var pathInputs = dom.getByName("folder_" + folderIndex + "_folderPath"); + if (pathInputs && pathInputs.length > 0) { + folderPath = pathInputs[0].value; + } + if (!folderPath) { + console.warn("No folder path for reset; folderIndex=" + folderIndex); return; } - resetFolderOverrides(parseInt(folderIndex)); + if (!confirm("Reset all overrides for this folder — your custom overrides will be removed")) { + return; + } - // Trigger dirty tracking update + resetFolderOverrides(folderPath); + + // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). if (window.dirtyTracker) { window.dirtyTracker.runChangeListeners(); window.dirtyTracker.checkDirty(); } + + // Persist the reset immediately on every IDE. We call getAndSaveIdeConfig() directly rather + // than formState.triggerChangeHandlers(), which only saves on auto-save IDEs — a reset must + // be a commit point everywhere (incl. OK/Cancel IDEs), since no input event would carry it. + if (window.ConfigApp.autoSave && window.ConfigApp.autoSave.getAndSaveIdeConfig) { + window.ConfigApp.autoSave.getAndSaveIdeConfig(); + } } // Apply default values to form fields @@ -143,11 +162,11 @@ } // Reset folder overrides - marks the folder so all org-scope fields are set to null on save - function resetFolderOverrides(folderIndex) { + function resetFolderOverrides(folderPath) { // Mark the folder for reset — on save, formHandler.applyFolderResets() will // set all org-scope LspFolderConfig fields to null (clear overrides) if (window.ConfigApp.formHandler && window.ConfigApp.formHandler.markFolderForReset) { - window.ConfigApp.formHandler.markFolderForReset(folderIndex); + window.ConfigApp.formHandler.markFolderForReset(folderPath); } } diff --git a/infrastructure/configuration/template/styles.css b/infrastructure/configuration/template/styles.css index aa923fbe2..c629b9af4 100644 --- a/infrastructure/configuration/template/styles.css +++ b/infrastructure/configuration/template/styles.css @@ -229,6 +229,14 @@ h3 { line-height: 1.5; } +/* Folder override disclaimer: text on the left, "Reset overrides" button top-right. */ +.folder-disclaimer { + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: var(--container-padding); +} + .folder-config { margin-bottom: var(--folder-config-margin); padding: var(--section-padding); @@ -708,22 +716,24 @@ button.secondary[disabled] { cursor: not-allowed; } +/* Outlined (ghost) danger button: red text + border on a transparent fill reads clearly on the + dark panel. A solid --error-background fill is a subtle marker color and washes out light text. */ .reset-overrides-btn { - display: none; padding: 4px 12px; + white-space: nowrap; font-size: var(--tiny-font-size); - background-color: var(--error-background); - border: 1px solid var(--error-background); - color: var(--input-foreground); + background-color: transparent; + border: 1px solid var(--error-foreground); + color: var(--error-foreground); border-radius: 4px; cursor: pointer; transition: all 0.2s ease; } .reset-overrides-btn:hover { - color: var(--editor-foreground); - background-color: var(--error-background); - border-color: var(--error-background); + background-color: var(--error-foreground); + border-color: var(--error-foreground); + color: var(--background-color); } /* - - - Settings Tabs - - - */ diff --git a/internal/types/config_resolver_test.go b/internal/types/config_resolver_test.go index b36effe2b..561f0052f 100644 --- a/internal/types/config_resolver_test.go +++ b/internal/types/config_resolver_test.go @@ -1175,6 +1175,60 @@ func TestFolderConfig_ApplyLspUpdate(t *testing.T) { assert.True(t, types.HasUserOverride(conf, fc.FolderPath, types.SettingScanAutomatic), "ApplyLspUpdate should apply setting; lock enforcement is the caller's responsibility (validateLockedFields)") }) + + t.Run("clears preferred_org override via explicit null and reverts to global org", func(t *testing.T) { + conf := configuration.NewWithOpts(configuration.WithAutomaticEnv()) + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + types.RegisterAllConfigurations(fs) + _ = conf.AddFlagSet(fs) + fc := &types.FolderConfig{FolderPath: "/path/to/folder"} + fc.ConfigResolver = types.NewMinimalConfigResolver(conf) + + // Seed a user-set folder org override plus a global org to fall back to. + types.SetPreferredOrgAndOrgSetByUser(conf, fc.FolderPath, "folder-org", true) + types.SetGlobalUser(conf, types.SettingLastSetOrganization, "global-org") + require.True(t, types.HasUserOverride(conf, fc.FolderPath, types.SettingPreferredOrg)) + require.True(t, types.HasUserOverride(conf, fc.FolderPath, types.SettingOrgSetByUser)) + + update := &types.LspFolderConfig{ + FolderPath: "/path/to/folder", + Settings: map[string]*types.ConfigSetting{ + types.SettingPreferredOrg: {Value: nil, Changed: true}, + }, + } + + changed := fc.ApplyLspUpdate(update) + + assert.True(t, changed) + assert.False(t, types.HasUserOverride(conf, fc.FolderPath, types.SettingPreferredOrg), + "preferred_org override should be cleared") + assert.False(t, types.HasUserOverride(conf, fc.FolderPath, types.SettingOrgSetByUser), + "org_set_by_user override should be cleared by the org reset") + assert.False(t, fc.OrgSetByUser(), "OrgSetByUser reverts to false after reset") + assert.Empty(t, fc.PreferredOrg(), "PreferredOrg falls back (no user override) after reset") + }) + + t.Run("preferred_org null reset with no existing override is a no-op", func(t *testing.T) { + conf := configuration.NewWithOpts(configuration.WithAutomaticEnv()) + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + types.RegisterAllConfigurations(fs) + _ = conf.AddFlagSet(fs) + fc := &types.FolderConfig{FolderPath: "/path/to/folder"} + fc.ConfigResolver = types.NewMinimalConfigResolver(conf) + + update := &types.LspFolderConfig{ + FolderPath: "/path/to/folder", + Settings: map[string]*types.ConfigSetting{ + types.SettingPreferredOrg: {Value: nil, Changed: true}, + }, + } + + changed := fc.ApplyLspUpdate(update) + + assert.False(t, changed, "resetting an org that was never overridden changes nothing") + assert.False(t, types.HasUserOverride(conf, fc.FolderPath, types.SettingPreferredOrg)) + assert.False(t, types.HasUserOverride(conf, fc.FolderPath, types.SettingOrgSetByUser)) + }) } func TestFolderConfig_ToLspFolderConfig(t *testing.T) { diff --git a/internal/types/folder_config.go b/internal/types/folder_config.go index 4cec48ac7..45d4fec31 100644 --- a/internal/types/folder_config.go +++ b/internal/types/folder_config.go @@ -571,6 +571,24 @@ func (fc *FolderConfig) applyPreferredOrg(update *LspFolderConfig, handled map[s return false } + // Reset: a {Changed:true, Value:nil} on preferred_org clears the folder org override. + // getSettingValue below treats a nil value as "not present", so the reset must be + // handled explicitly first. Unset both preferred_org and org_set_by_user so the folder + // reverts to its auto-determined (LDX) / global org. Mirrors the generic reset path in + // applyGenericFolderOverrides. + if cs := update.Settings[SettingPreferredOrg]; cs != nil && cs.Changed && cs.Value == nil { + changed := false + if HasUserOverride(conf, fc.FolderPath, SettingPreferredOrg) { + conf.Unset(configresolver.UserFolderKey(fp, SettingPreferredOrg)) + changed = true + } + if HasUserOverride(conf, fc.FolderPath, SettingOrgSetByUser) { + conf.Unset(configresolver.UserFolderKey(fp, SettingOrgSetByUser)) + changed = true + } + return changed + } + preferredOrg, ok := getSettingValue[string](update.Settings, SettingPreferredOrg) if !ok { return false diff --git a/js-tests/folder-reset.test.mjs b/js-tests/folder-reset.test.mjs new file mode 100644 index 000000000..baf3fd514 --- /dev/null +++ b/js-tests/folder-reset.test.mjs @@ -0,0 +1,208 @@ +// ABOUTME: Tests for the per-folder "Reset overrides" flow (form-handler + reset-handler). +// ABOUTME: Verifies resets are keyed by folderPath, emit 14 flat nulls, and survive compaction. + +import assert from "node:assert/strict"; +import test from "node:test"; +import { buildDom } from "./helpers.mjs"; + +// The 14 org-scope folder fields a reset must clear (mirrors FOLDER_RESET_FIELDS in form-handler.js). +const RESET_FIELDS = [ + "scan_automatic", + "scan_net_new", + "severity_filter_critical", + "severity_filter_high", + "severity_filter_medium", + "severity_filter_low", + "snyk_oss_enabled", + "snyk_code_enabled", + "snyk_iac_enabled", + "snyk_secrets_enabled", + "issue_view_open_issues", + "issue_view_ignored_issues", + "risk_score_threshold", + "preferred_org", +]; + +// Folder paths embedded in the dummy-data fixture (js-tests/fixtures/config-page.html). +const PATH_A = "/Users/username/workspace/defaults-project"; +const PATH_B = "/Users/username/workspace/org-set-project"; +const PATH_C = "/Users/username/workspace/org-locked-project"; + +function assertAllNull(entry, fields) { + for (const f of fields) { + assert.equal(entry[f], null, `${f} should be null`); + } +} + +// Enable auto-save and spy on the IDE save bridge; returns the list of JSON payloads sent. +function spySave(win) { + win.__IS_IDE_AUTOSAVE_ENABLED__ = true; + const calls = []; + win.__saveIdeConfig__ = (jsonString) => calls.push(jsonString); + return calls; +} + +test("markFolderForReset / isFolderMarkedForReset are keyed by folderPath", async () => { + const win = await buildDom(); + const fh = win.ConfigApp.formHandler; + + assert.equal(fh.isFolderMarkedForReset(PATH_A), false, "not marked initially"); + fh.markFolderForReset(PATH_A); + assert.equal(fh.isFolderMarkedForReset(PATH_A), true, "marked after mark"); + assert.equal(fh.isFolderMarkedForReset(PATH_B), false, "other path stays unmarked"); +}); + +test("markFolderForReset ignores empty/missing folderPath", async () => { + const win = await buildDom(); + const fh = win.ConfigApp.formHandler; + + fh.markFolderForReset(""); + fh.markFolderForReset(undefined); + assert.equal(fh.isFolderMarkedForReset(""), false, "empty path must not mark"); + assert.equal(fh.isFolderMarkedForReset(undefined), false, "undefined path must not mark"); +}); + +test("applyFolderResets sets all 14 fields to null on an existing edited folder, preserving folderPath", async () => { + const win = await buildDom(); + const fh = win.ConfigApp.formHandler; + + const data = { + folderConfigs: [ + { folderPath: PATH_A, snyk_code_enabled: true, scan_automatic: "true" }, + ], + }; + fh.markFolderForReset(PATH_A); + fh.applyFolderResets(data); + + const entry = data.folderConfigs.find((f) => f.folderPath === PATH_A); + assert.ok(entry, "edited folder entry preserved"); + assert.equal(entry.folderPath, PATH_A, "folderPath preserved"); + assertAllNull(entry, RESET_FIELDS); +}); + +test("applyFolderResets emits a reset-only folder absent from data.folderConfigs (bug #2)", async () => { + const win = await buildDom(); + const fh = win.ConfigApp.formHandler; + + // Reset-only folder: collectChangedData dropped it because it had no other changed field. + const data = { folderConfigs: [] }; + fh.markFolderForReset(PATH_A); + fh.applyFolderResets(data); + + assert.equal(data.folderConfigs.length, 1, "a fresh entry was pushed for the reset-only folder"); + const entry = data.folderConfigs[0]; + assert.equal(entry.folderPath, PATH_A, "pushed entry carries folderPath"); + assertAllNull(entry, RESET_FIELDS); +}); + +test("applyFolderResets creates folderConfigs array when missing", async () => { + const win = await buildDom(); + const fh = win.ConfigApp.formHandler; + + const data = {}; + fh.markFolderForReset(PATH_A); + fh.applyFolderResets(data); + + assert.ok(Array.isArray(data.folderConfigs), "folderConfigs initialized"); + assert.equal(data.folderConfigs.length, 1, "reset-only entry pushed"); + assert.equal(data.folderConfigs[0].folderPath, PATH_A); +}); + +test("applyFolderResets nulls only the marked folder; others untouched (bug #1 — compaction/index drift)", async () => { + const win = await buildDom(); + const fh = win.ConfigApp.formHandler; + + // Folders A, B, C present (as collectChangedData would emit them, compacted). Mark only B. + const data = { + folderConfigs: [ + { folderPath: PATH_A, snyk_code_enabled: true }, + { folderPath: PATH_B, snyk_code_enabled: false }, + { folderPath: PATH_C, scan_automatic: "false" }, + ], + }; + fh.markFolderForReset(PATH_B); + fh.applyFolderResets(data); + + const a = data.folderConfigs.find((f) => f.folderPath === PATH_A); + const b = data.folderConfigs.find((f) => f.folderPath === PATH_B); + const c = data.folderConfigs.find((f) => f.folderPath === PATH_C); + + // B is fully reset. + assertAllNull(b, RESET_FIELDS); + // A and C keep their original edits and are NOT nulled. + assert.equal(a.snyk_code_enabled, true, "A.snyk_code_enabled untouched"); + assert.equal(a.scan_automatic, undefined, "A reset fields not added"); + assert.equal(c.scan_automatic, "false", "C.scan_automatic untouched"); + assert.equal(c.snyk_code_enabled, undefined, "C reset fields not added"); +}); + +test("applyFolderResets clears window.ConfigApp.folderResets after applying", async () => { + const win = await buildDom(); + const fh = win.ConfigApp.formHandler; + + fh.markFolderForReset(PATH_A); + fh.applyFolderResets({ folderConfigs: [{ folderPath: PATH_A }] }); + + // Prototype-agnostic: the reset map is created in the JSDOM realm, so compare by key count + // rather than deepStrictEqual against this realm's {} (different Object.prototype). + assert.equal(Object.keys(win.ConfigApp.folderResets).length, 0, "folderResets cleared"); + assert.equal(fh.isFolderMarkedForReset(PATH_A), false, "no longer marked"); +}); + +test("DOM-driven: clicking .reset-overrides-btn produces 14 nulls for that folderPath in the save payload", async () => { + const win = await buildDom(); + const doc = win.document; + win.confirm = () => true; + const calls = spySave(win); + + // Click the reset button for folder index 1 (PATH_B). The click both marks the folder and + // saves; assert on the actual outbound payload the IDE receives. + const btn = doc.querySelector('.reset-overrides-btn[data-folder-index="1"]'); + assert.ok(btn, "reset button for folder 1 exists in the rendered fixture"); + btn.click(); + + assert.ok(calls.length > 0, "reset click must save"); + const saved = JSON.parse(calls[calls.length - 1]); + const entry = (saved.folderConfigs || []).find((f) => f.folderPath === PATH_B); + assert.ok(entry, "outbound payload contains an entry for the reset folderPath"); + assertAllNull(entry, RESET_FIELDS); +}); + +test("DOM-driven: reset button click does not require the folder to have other edits", async () => { + const win = await buildDom(); + const doc = win.document; + win.confirm = () => true; + const calls = spySave(win); + + // Folder index 0 (PATH_A), no edits made — reset-only. + const btn = doc.querySelector('.reset-overrides-btn[data-folder-index="0"]'); + assert.ok(btn, "reset button for folder 0 exists"); + btn.click(); + + assert.ok(calls.length > 0, "reset-only click must still save"); + const saved = JSON.parse(calls[calls.length - 1]); + const entry = (saved.folderConfigs || []).find((f) => f.folderPath === PATH_A); + assert.ok(entry, "reset-only folder still emitted in payload"); + assertAllNull(entry, RESET_FIELDS); +}); + +test("clicking reset triggers a save even with no other edits", async () => { + const win = await buildDom(); + const doc = win.document; + win.confirm = () => true; + const calls = spySave(win); + + // A folder reset changes no DOM input, so without an explicit save trigger it would never + // persist (this was the VS Code bug). Regression guard: the click alone must reach + // __saveIdeConfig__. The reset handler calls getAndSaveIdeConfig() directly, so this holds for + // every IDE, not just auto-save ones. + const btn = doc.querySelector('.reset-overrides-btn[data-folder-index="1"]'); + assert.ok(btn, "reset button for folder 1 exists"); + btn.click(); + + assert.ok(calls.length > 0, "reset click must trigger a save"); + const saved = JSON.parse(calls[calls.length - 1]); + const entry = (saved.folderConfigs || []).find((f) => f.folderPath === PATH_B); + assert.ok(entry, "saved payload must contain the reset folder"); + assertAllNull(entry, RESET_FIELDS); +}); diff --git a/scripts/config-dialog/config_output_multi_project.html b/scripts/config-dialog/config_output_multi_project.html index d7a6b1787..1f4053e64 100644 --- a/scripts/config-dialog/config_output_multi_project.html +++ b/scripts/config-dialog/config_output_multi_project.html @@ -253,6 +253,14 @@ line-height: 1.5; } +/* Folder override disclaimer: text on the left, "Reset overrides" button top-right. */ +.folder-disclaimer { + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: var(--container-padding); +} + .folder-config { margin-bottom: var(--folder-config-margin); padding: var(--section-padding); @@ -732,22 +740,24 @@ cursor: not-allowed; } +/* Outlined (ghost) danger button: red text + border on a transparent fill reads clearly on the + dark panel. A solid --error-background fill is a subtle marker color and washes out light text. */ .reset-overrides-btn { - display: none; padding: 4px 12px; + white-space: nowrap; font-size: var(--tiny-font-size); - background-color: var(--error-background); - border: 1px solid var(--error-background); - color: var(--input-foreground); + background-color: transparent; + border: 1px solid var(--error-foreground); + color: var(--error-foreground); border-radius: 4px; cursor: pointer; transition: all 0.2s ease; } .reset-overrides-btn:hover { - color: var(--editor-foreground); - background-color: var(--error-background); - border-color: var(--error-background); + background-color: var(--error-foreground); + border-color: var(--error-foreground); + color: var(--background-color); } /* - - - Settings Tabs - - - */ @@ -1252,8 +1262,9 @@

Filters and views

-
+

These settings override the project defaults for this specific project.

+
@@ -1537,14 +1548,14 @@

Filters and views

-
-
+

These settings override the project defaults for this specific project.

+
@@ -1836,14 +1847,14 @@

Filters and views

-
-
+

These settings override the project defaults for this specific project.

+
@@ -2127,14 +2138,14 @@

Filters and views

-
-
+

These settings override the project defaults for this specific project.

+
@@ -2418,7 +2429,6 @@

Filters and views

- @@ -3522,41 +3532,78 @@

Filters and views

} } - // Mark a folder for complete reset (all org-scope overrides will be set to null) - formHandler.markFolderForReset = function (folderIndex) { + // Org-scope folder override fields cleared by a folder reset. Emitting flat null + // for each tells the IDE plugin to send {value:null, changed:true}, which makes + // snyk-ls Unset the user:folder: override so the value falls back to org/LDX/default. + // preferred_org is included: snyk-ls special-cases its null-reset to also unset + // org_set_by_user, reverting the folder to its auto-determined / global org. + var FOLDER_RESET_FIELDS = [ + "scan_automatic", + "scan_net_new", + "severity_filter_critical", + "severity_filter_high", + "severity_filter_medium", + "severity_filter_low", + "snyk_oss_enabled", + "snyk_code_enabled", + "snyk_iac_enabled", + "snyk_secrets_enabled", + "issue_view_open_issues", + "issue_view_ignored_issues", + "risk_score_threshold", + "preferred_org", + ]; + + // Mark a folder for complete reset (all org-scope overrides will be set to null). + // Keyed by folderPath, not index: collectChangedData() compacts folderConfigs, so an + // index captured at click time no longer maps to the same entry in the saved payload. + formHandler.markFolderForReset = function (folderPath) { + if (!folderPath) return; window.ConfigApp.folderResets = window.ConfigApp.folderResets || {}; - window.ConfigApp.folderResets[folderIndex] = true; + window.ConfigApp.folderResets[folderPath] = true; }; // Check if a folder is marked for reset - formHandler.isFolderMarkedForReset = function (folderIndex) { - return ( + formHandler.isFolderMarkedForReset = function (folderPath) { + return !!( window.ConfigApp.folderResets && - window.ConfigApp.folderResets[folderIndex] + window.ConfigApp.folderResets[folderPath] ); }; - // Apply reset: set all org-scope fields to null on the folder config + // Apply reset: set all org-scope fields to null on each reset-marked folder. + // A reset-only folder (no other edits) is dropped by collectChangedData's diff, so + // add a fresh entry for any marked folderPath missing from data.folderConfigs. formHandler.applyFolderResets = function (data) { - if (!data.folderConfigs) return; + var marked = window.ConfigApp.folderResets; + if (!marked) return; + + data.folderConfigs = data.folderConfigs || []; + + // Index existing entries by folderPath. + var byPath = {}; for (var i = 0; i < data.folderConfigs.length; i++) { - if (formHandler.isFolderMarkedForReset(i) && data.folderConfigs[i]) { - var fc = data.folderConfigs[i]; - fc.scan_automatic = null; - fc.scan_net_new = null; - fc.severity_filter_critical = null; - fc.severity_filter_high = null; - fc.severity_filter_medium = null; - fc.severity_filter_low = null; - fc.snyk_oss_enabled = null; - fc.snyk_code_enabled = null; - fc.snyk_iac_enabled = null; - fc.snyk_secrets_enabled = null; - fc.issue_view_open_issues = null; - fc.issue_view_ignored_issues = null; - fc.risk_score_threshold = null; + var fc = data.folderConfigs[i]; + if (fc && fc.folderPath) { + byPath[fc.folderPath] = fc; } } + + for (var folderPath in marked) { + if (!marked.hasOwnProperty(folderPath) || !marked[folderPath]) continue; + + var entry = byPath[folderPath]; + if (!entry) { + entry = { folderPath: folderPath }; + data.folderConfigs.push(entry); + byPath[folderPath] = entry; + } + + for (var f = 0; f < FOLDER_RESET_FIELDS.length; f++) { + entry[FOLDER_RESET_FIELDS[f]] = null; + } + } + window.ConfigApp.folderResets = {}; }; @@ -3679,17 +3726,36 @@

Filters and views

return; } - if (!confirm("Reset all overrides for this folder to defaults? Your custom overrides will be removed.")) { + // Resolve the folderPath from the hidden input so resets are keyed by path, not the + // (later compacted) index. Without a path we cannot reliably target the folder on save. + var folderPath = null; + var pathInputs = dom.getByName("folder_" + folderIndex + "_folderPath"); + if (pathInputs && pathInputs.length > 0) { + folderPath = pathInputs[0].value; + } + if (!folderPath) { + console.warn("No folder path for reset; folderIndex=" + folderIndex); return; } - resetFolderOverrides(parseInt(folderIndex)); + if (!confirm("Reset all overrides for this folder — your custom overrides will be removed")) { + return; + } - // Trigger dirty tracking update + resetFolderOverrides(folderPath); + + // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). if (window.dirtyTracker) { window.dirtyTracker.runChangeListeners(); window.dirtyTracker.checkDirty(); } + + // Persist the reset immediately on every IDE. We call getAndSaveIdeConfig() directly rather + // than formState.triggerChangeHandlers(), which only saves on auto-save IDEs — a reset must + // be a commit point everywhere (incl. OK/Cancel IDEs), since no input event would carry it. + if (window.ConfigApp.autoSave && window.ConfigApp.autoSave.getAndSaveIdeConfig) { + window.ConfigApp.autoSave.getAndSaveIdeConfig(); + } } // Apply default values to form fields @@ -3735,11 +3801,11 @@

Filters and views

} // Reset folder overrides - marks the folder so all org-scope fields are set to null on save - function resetFolderOverrides(folderIndex) { + function resetFolderOverrides(folderPath) { // Mark the folder for reset — on save, formHandler.applyFolderResets() will // set all org-scope LspFolderConfig fields to null (clear overrides) if (window.ConfigApp.formHandler && window.ConfigApp.formHandler.markFolderForReset) { - window.ConfigApp.formHandler.markFolderForReset(folderIndex); + window.ConfigApp.formHandler.markFolderForReset(folderPath); } } diff --git a/scripts/config-dialog/config_output_no_projects.html b/scripts/config-dialog/config_output_no_projects.html index 91995e5ea..260e3794a 100644 --- a/scripts/config-dialog/config_output_no_projects.html +++ b/scripts/config-dialog/config_output_no_projects.html @@ -253,6 +253,14 @@ line-height: 1.5; } +/* Folder override disclaimer: text on the left, "Reset overrides" button top-right. */ +.folder-disclaimer { + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: var(--container-padding); +} + .folder-config { margin-bottom: var(--folder-config-margin); padding: var(--section-padding); @@ -732,22 +740,24 @@ cursor: not-allowed; } +/* Outlined (ghost) danger button: red text + border on a transparent fill reads clearly on the + dark panel. A solid --error-background fill is a subtle marker color and washes out light text. */ .reset-overrides-btn { - display: none; padding: 4px 12px; + white-space: nowrap; font-size: var(--tiny-font-size); - background-color: var(--error-background); - border: 1px solid var(--error-background); - color: var(--input-foreground); + background-color: transparent; + border: 1px solid var(--error-foreground); + color: var(--error-foreground); border-radius: 4px; cursor: pointer; transition: all 0.2s ease; } .reset-overrides-btn:hover { - color: var(--editor-foreground); - background-color: var(--error-background); - border-color: var(--error-background); + background-color: var(--error-foreground); + border-color: var(--error-foreground); + color: var(--background-color); } /* - - - Settings Tabs - - - */ @@ -2305,41 +2315,78 @@

Filters and views

} } - // Mark a folder for complete reset (all org-scope overrides will be set to null) - formHandler.markFolderForReset = function (folderIndex) { + // Org-scope folder override fields cleared by a folder reset. Emitting flat null + // for each tells the IDE plugin to send {value:null, changed:true}, which makes + // snyk-ls Unset the user:folder: override so the value falls back to org/LDX/default. + // preferred_org is included: snyk-ls special-cases its null-reset to also unset + // org_set_by_user, reverting the folder to its auto-determined / global org. + var FOLDER_RESET_FIELDS = [ + "scan_automatic", + "scan_net_new", + "severity_filter_critical", + "severity_filter_high", + "severity_filter_medium", + "severity_filter_low", + "snyk_oss_enabled", + "snyk_code_enabled", + "snyk_iac_enabled", + "snyk_secrets_enabled", + "issue_view_open_issues", + "issue_view_ignored_issues", + "risk_score_threshold", + "preferred_org", + ]; + + // Mark a folder for complete reset (all org-scope overrides will be set to null). + // Keyed by folderPath, not index: collectChangedData() compacts folderConfigs, so an + // index captured at click time no longer maps to the same entry in the saved payload. + formHandler.markFolderForReset = function (folderPath) { + if (!folderPath) return; window.ConfigApp.folderResets = window.ConfigApp.folderResets || {}; - window.ConfigApp.folderResets[folderIndex] = true; + window.ConfigApp.folderResets[folderPath] = true; }; // Check if a folder is marked for reset - formHandler.isFolderMarkedForReset = function (folderIndex) { - return ( + formHandler.isFolderMarkedForReset = function (folderPath) { + return !!( window.ConfigApp.folderResets && - window.ConfigApp.folderResets[folderIndex] + window.ConfigApp.folderResets[folderPath] ); }; - // Apply reset: set all org-scope fields to null on the folder config + // Apply reset: set all org-scope fields to null on each reset-marked folder. + // A reset-only folder (no other edits) is dropped by collectChangedData's diff, so + // add a fresh entry for any marked folderPath missing from data.folderConfigs. formHandler.applyFolderResets = function (data) { - if (!data.folderConfigs) return; + var marked = window.ConfigApp.folderResets; + if (!marked) return; + + data.folderConfigs = data.folderConfigs || []; + + // Index existing entries by folderPath. + var byPath = {}; for (var i = 0; i < data.folderConfigs.length; i++) { - if (formHandler.isFolderMarkedForReset(i) && data.folderConfigs[i]) { - var fc = data.folderConfigs[i]; - fc.scan_automatic = null; - fc.scan_net_new = null; - fc.severity_filter_critical = null; - fc.severity_filter_high = null; - fc.severity_filter_medium = null; - fc.severity_filter_low = null; - fc.snyk_oss_enabled = null; - fc.snyk_code_enabled = null; - fc.snyk_iac_enabled = null; - fc.snyk_secrets_enabled = null; - fc.issue_view_open_issues = null; - fc.issue_view_ignored_issues = null; - fc.risk_score_threshold = null; + var fc = data.folderConfigs[i]; + if (fc && fc.folderPath) { + byPath[fc.folderPath] = fc; } } + + for (var folderPath in marked) { + if (!marked.hasOwnProperty(folderPath) || !marked[folderPath]) continue; + + var entry = byPath[folderPath]; + if (!entry) { + entry = { folderPath: folderPath }; + data.folderConfigs.push(entry); + byPath[folderPath] = entry; + } + + for (var f = 0; f < FOLDER_RESET_FIELDS.length; f++) { + entry[FOLDER_RESET_FIELDS[f]] = null; + } + } + window.ConfigApp.folderResets = {}; }; @@ -2462,17 +2509,36 @@

Filters and views

return; } - if (!confirm("Reset all overrides for this folder to defaults? Your custom overrides will be removed.")) { + // Resolve the folderPath from the hidden input so resets are keyed by path, not the + // (later compacted) index. Without a path we cannot reliably target the folder on save. + var folderPath = null; + var pathInputs = dom.getByName("folder_" + folderIndex + "_folderPath"); + if (pathInputs && pathInputs.length > 0) { + folderPath = pathInputs[0].value; + } + if (!folderPath) { + console.warn("No folder path for reset; folderIndex=" + folderIndex); return; } - resetFolderOverrides(parseInt(folderIndex)); + if (!confirm("Reset all overrides for this folder — your custom overrides will be removed")) { + return; + } - // Trigger dirty tracking update + resetFolderOverrides(folderPath); + + // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). if (window.dirtyTracker) { window.dirtyTracker.runChangeListeners(); window.dirtyTracker.checkDirty(); } + + // Persist the reset immediately on every IDE. We call getAndSaveIdeConfig() directly rather + // than formState.triggerChangeHandlers(), which only saves on auto-save IDEs — a reset must + // be a commit point everywhere (incl. OK/Cancel IDEs), since no input event would carry it. + if (window.ConfigApp.autoSave && window.ConfigApp.autoSave.getAndSaveIdeConfig) { + window.ConfigApp.autoSave.getAndSaveIdeConfig(); + } } // Apply default values to form fields @@ -2518,11 +2584,11 @@

Filters and views

} // Reset folder overrides - marks the folder so all org-scope fields are set to null on save - function resetFolderOverrides(folderIndex) { + function resetFolderOverrides(folderPath) { // Mark the folder for reset — on save, formHandler.applyFolderResets() will // set all org-scope LspFolderConfig fields to null (clear overrides) if (window.ConfigApp.formHandler && window.ConfigApp.formHandler.markFolderForReset) { - window.ConfigApp.formHandler.markFolderForReset(folderIndex); + window.ConfigApp.formHandler.markFolderForReset(folderPath); } } diff --git a/scripts/config-dialog/config_output_single_solution.html b/scripts/config-dialog/config_output_single_solution.html index c31d9ffcb..2ea27b3cd 100644 --- a/scripts/config-dialog/config_output_single_solution.html +++ b/scripts/config-dialog/config_output_single_solution.html @@ -253,6 +253,14 @@ line-height: 1.5; } +/* Folder override disclaimer: text on the left, "Reset overrides" button top-right. */ +.folder-disclaimer { + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: var(--container-padding); +} + .folder-config { margin-bottom: var(--folder-config-margin); padding: var(--section-padding); @@ -732,22 +740,24 @@ cursor: not-allowed; } +/* Outlined (ghost) danger button: red text + border on a transparent fill reads clearly on the + dark panel. A solid --error-background fill is a subtle marker color and washes out light text. */ .reset-overrides-btn { - display: none; padding: 4px 12px; + white-space: nowrap; font-size: var(--tiny-font-size); - background-color: var(--error-background); - border: 1px solid var(--error-background); - color: var(--input-foreground); + background-color: transparent; + border: 1px solid var(--error-foreground); + color: var(--error-foreground); border-radius: 4px; cursor: pointer; transition: all 0.2s ease; } .reset-overrides-btn:hover { - color: var(--editor-foreground); - background-color: var(--error-background); - border-color: var(--error-background); + background-color: var(--error-foreground); + border-color: var(--error-foreground); + color: var(--background-color); } /* - - - Settings Tabs - - - */ @@ -1216,8 +1226,9 @@

Filters and views

-
+

These settings override the solution defaults for this specific solution.

+
@@ -1501,7 +1512,6 @@

Filters and views

- @@ -2605,41 +2615,78 @@

Filters and views

} } - // Mark a folder for complete reset (all org-scope overrides will be set to null) - formHandler.markFolderForReset = function (folderIndex) { + // Org-scope folder override fields cleared by a folder reset. Emitting flat null + // for each tells the IDE plugin to send {value:null, changed:true}, which makes + // snyk-ls Unset the user:folder: override so the value falls back to org/LDX/default. + // preferred_org is included: snyk-ls special-cases its null-reset to also unset + // org_set_by_user, reverting the folder to its auto-determined / global org. + var FOLDER_RESET_FIELDS = [ + "scan_automatic", + "scan_net_new", + "severity_filter_critical", + "severity_filter_high", + "severity_filter_medium", + "severity_filter_low", + "snyk_oss_enabled", + "snyk_code_enabled", + "snyk_iac_enabled", + "snyk_secrets_enabled", + "issue_view_open_issues", + "issue_view_ignored_issues", + "risk_score_threshold", + "preferred_org", + ]; + + // Mark a folder for complete reset (all org-scope overrides will be set to null). + // Keyed by folderPath, not index: collectChangedData() compacts folderConfigs, so an + // index captured at click time no longer maps to the same entry in the saved payload. + formHandler.markFolderForReset = function (folderPath) { + if (!folderPath) return; window.ConfigApp.folderResets = window.ConfigApp.folderResets || {}; - window.ConfigApp.folderResets[folderIndex] = true; + window.ConfigApp.folderResets[folderPath] = true; }; // Check if a folder is marked for reset - formHandler.isFolderMarkedForReset = function (folderIndex) { - return ( + formHandler.isFolderMarkedForReset = function (folderPath) { + return !!( window.ConfigApp.folderResets && - window.ConfigApp.folderResets[folderIndex] + window.ConfigApp.folderResets[folderPath] ); }; - // Apply reset: set all org-scope fields to null on the folder config + // Apply reset: set all org-scope fields to null on each reset-marked folder. + // A reset-only folder (no other edits) is dropped by collectChangedData's diff, so + // add a fresh entry for any marked folderPath missing from data.folderConfigs. formHandler.applyFolderResets = function (data) { - if (!data.folderConfigs) return; + var marked = window.ConfigApp.folderResets; + if (!marked) return; + + data.folderConfigs = data.folderConfigs || []; + + // Index existing entries by folderPath. + var byPath = {}; for (var i = 0; i < data.folderConfigs.length; i++) { - if (formHandler.isFolderMarkedForReset(i) && data.folderConfigs[i]) { - var fc = data.folderConfigs[i]; - fc.scan_automatic = null; - fc.scan_net_new = null; - fc.severity_filter_critical = null; - fc.severity_filter_high = null; - fc.severity_filter_medium = null; - fc.severity_filter_low = null; - fc.snyk_oss_enabled = null; - fc.snyk_code_enabled = null; - fc.snyk_iac_enabled = null; - fc.snyk_secrets_enabled = null; - fc.issue_view_open_issues = null; - fc.issue_view_ignored_issues = null; - fc.risk_score_threshold = null; + var fc = data.folderConfigs[i]; + if (fc && fc.folderPath) { + byPath[fc.folderPath] = fc; + } + } + + for (var folderPath in marked) { + if (!marked.hasOwnProperty(folderPath) || !marked[folderPath]) continue; + + var entry = byPath[folderPath]; + if (!entry) { + entry = { folderPath: folderPath }; + data.folderConfigs.push(entry); + byPath[folderPath] = entry; + } + + for (var f = 0; f < FOLDER_RESET_FIELDS.length; f++) { + entry[FOLDER_RESET_FIELDS[f]] = null; } } + window.ConfigApp.folderResets = {}; }; @@ -2762,17 +2809,36 @@

Filters and views

return; } - if (!confirm("Reset all overrides for this folder to defaults? Your custom overrides will be removed.")) { + // Resolve the folderPath from the hidden input so resets are keyed by path, not the + // (later compacted) index. Without a path we cannot reliably target the folder on save. + var folderPath = null; + var pathInputs = dom.getByName("folder_" + folderIndex + "_folderPath"); + if (pathInputs && pathInputs.length > 0) { + folderPath = pathInputs[0].value; + } + if (!folderPath) { + console.warn("No folder path for reset; folderIndex=" + folderIndex); + return; + } + + if (!confirm("Reset all overrides for this folder — your custom overrides will be removed")) { return; } - resetFolderOverrides(parseInt(folderIndex)); + resetFolderOverrides(folderPath); - // Trigger dirty tracking update + // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). if (window.dirtyTracker) { window.dirtyTracker.runChangeListeners(); window.dirtyTracker.checkDirty(); } + + // Persist the reset immediately on every IDE. We call getAndSaveIdeConfig() directly rather + // than formState.triggerChangeHandlers(), which only saves on auto-save IDEs — a reset must + // be a commit point everywhere (incl. OK/Cancel IDEs), since no input event would carry it. + if (window.ConfigApp.autoSave && window.ConfigApp.autoSave.getAndSaveIdeConfig) { + window.ConfigApp.autoSave.getAndSaveIdeConfig(); + } } // Apply default values to form fields @@ -2818,11 +2884,11 @@

Filters and views

} // Reset folder overrides - marks the folder so all org-scope fields are set to null on save - function resetFolderOverrides(folderIndex) { + function resetFolderOverrides(folderPath) { // Mark the folder for reset — on save, formHandler.applyFolderResets() will // set all org-scope LspFolderConfig fields to null (clear overrides) if (window.ConfigApp.formHandler && window.ConfigApp.formHandler.markFolderForReset) { - window.ConfigApp.formHandler.markFolderForReset(folderIndex); + window.ConfigApp.formHandler.markFolderForReset(folderPath); } } From 3e60ff84a4b8976b7d995cebab76bf846c8e3fc6 Mon Sep 17 00:00:00 2001 From: Nick Yasnohorodskyi Date: Wed, 17 Jun 2026 18:31:06 +0200 Subject: [PATCH 02/10] fix: make reset overrides work in VSCode webview + clear all overrides [IDE-1945] Two defects stopped 'Reset overrides' from working: 1. No-op in sandboxed webview. VSCode renders the settings dialog in a sandboxed iframe without 'allow-modals', so window.confirm() is silently ignored and returns false. Both reset handlers gated their work behind confirm(), so the click bailed and nothing happened. Remove the confirm() prompts (reset is reversible and the dialog shows a disclaimer banner). 2. Incomplete reset. additional_parameters, additional_environment and scan_command_config are folder overrides editable in the dialog but were never cleared: the JS omitted them from FOLDER_RESET_FIELDS, and on the LS side they are processed by applyBasicFolderFields (marked handled), so the generic null-reset loop skipped them while their own handlers ignored a nil value -- making the reset unreachable. Add the three keys to FOLDER_RESET_FIELDS and teach applyStringField / applyStringSliceField / applyScanCommandConfig to Unset the override on {Changed:true, Value:nil}. Extend the end-to-end reset test to cover the three non-scalar fields and drop the dead win.confirm stubs / section-name formatter. --- application/server/configuration_test.go | 26 ++++++++++---- .../template/js/ui/form-handler.js | 11 ++++-- .../template/js/ui/reset-handler.js | 24 ++----------- internal/types/folder_config.go | 27 ++++++++++++++ js-tests/folder-reset.test.mjs | 3 -- .../config_output_multi_project.html | 35 ++++++------------- .../config_output_no_projects.html | 35 ++++++------------- .../config_output_single_solution.html | 35 ++++++------------- 8 files changed, 87 insertions(+), 109 deletions(-) diff --git a/application/server/configuration_test.go b/application/server/configuration_test.go index 5157f3e62..79478f4dd 100644 --- a/application/server/configuration_test.go +++ b/application/server/configuration_test.go @@ -744,9 +744,14 @@ func Test_updateFolderConfig_ResetClearsUserOverrides_EndToEnd(t *testing.T) { seed := []types.LspFolderConfig{{ FolderPath: folderPath, Settings: map[string]*types.ConfigSetting{ - types.SettingSnykCodeEnabled: {Value: true, Changed: true}, - types.SettingScanAutomatic: {Value: true, Changed: true}, - types.SettingPreferredOrg: {Value: "my-folder-org", Changed: true}, + types.SettingSnykCodeEnabled: {Value: true, Changed: true}, + types.SettingScanAutomatic: {Value: true, Changed: true}, + types.SettingPreferredOrg: {Value: "my-folder-org", Changed: true}, + types.SettingAdditionalParameters: {Value: []string{"--all-projects"}, Changed: true}, + types.SettingAdditionalEnvironment: {Value: "FOO=bar", Changed: true}, + types.SettingScanCommandConfig: {Value: map[product.Product]types.ScanCommandConfig{ + product.ProductOpenSource: {PreScanCommand: "echo pre"}, + }, Changed: true}, }, }} UpdateSettings(ctx, conf, engine, logger, map[string]*types.ConfigSetting{}, seed, analytics.TriggerSourceTest, resolver) @@ -755,6 +760,9 @@ func Test_updateFolderConfig_ResetClearsUserOverrides_EndToEnd(t *testing.T) { require.True(t, types.HasUserOverride(conf, folderPath, types.SettingScanAutomatic), "scan_automatic override seeded") require.True(t, types.HasUserOverride(conf, folderPath, types.SettingPreferredOrg), "preferred_org override seeded") require.True(t, types.HasUserOverride(conf, folderPath, types.SettingOrgSetByUser), "org_set_by_user set alongside preferred_org") + require.True(t, types.HasUserOverride(conf, folderPath, types.SettingAdditionalParameters), "additional_parameters override seeded") + require.True(t, types.HasUserOverride(conf, folderPath, types.SettingAdditionalEnvironment), "additional_environment override seeded") + require.True(t, types.HasUserOverride(conf, folderPath, types.SettingScanCommandConfig), "scan_command_config override seeded") seededFC := config.GetFolderConfigFromEngine(engine, resolver, folderPath, logger) require.Equal(t, "my-folder-org", seededFC.PreferredOrg(), "seeded preferred_org is effective") @@ -763,9 +771,12 @@ func Test_updateFolderConfig_ResetClearsUserOverrides_EndToEnd(t *testing.T) { reset := []types.LspFolderConfig{{ FolderPath: folderPath, Settings: map[string]*types.ConfigSetting{ - types.SettingSnykCodeEnabled: {Value: nil, Changed: true}, - types.SettingScanAutomatic: {Value: nil, Changed: true}, - types.SettingPreferredOrg: {Value: nil, Changed: true}, + types.SettingSnykCodeEnabled: {Value: nil, Changed: true}, + types.SettingScanAutomatic: {Value: nil, Changed: true}, + types.SettingPreferredOrg: {Value: nil, Changed: true}, + types.SettingAdditionalParameters: {Value: nil, Changed: true}, + types.SettingAdditionalEnvironment: {Value: nil, Changed: true}, + types.SettingScanCommandConfig: {Value: nil, Changed: true}, }, }} UpdateSettings(ctx, conf, engine, logger, map[string]*types.ConfigSetting{}, reset, analytics.TriggerSourceTest, resolver) @@ -775,6 +786,9 @@ func Test_updateFolderConfig_ResetClearsUserOverrides_EndToEnd(t *testing.T) { assert.False(t, types.HasUserOverride(conf, folderPath, types.SettingScanAutomatic), "scan_automatic override cleared") assert.False(t, types.HasUserOverride(conf, folderPath, types.SettingPreferredOrg), "preferred_org override cleared") assert.False(t, types.HasUserOverride(conf, folderPath, types.SettingOrgSetByUser), "org_set_by_user cleared by org reset") + assert.False(t, types.HasUserOverride(conf, folderPath, types.SettingAdditionalParameters), "additional_parameters override cleared") + assert.False(t, types.HasUserOverride(conf, folderPath, types.SettingAdditionalEnvironment), "additional_environment override cleared") + assert.False(t, types.HasUserOverride(conf, folderPath, types.SettingScanCommandConfig), "scan_command_config override cleared") // After Unset the key no longer holds an active *LocalConfigField override (Unset writes a // tombstone, so conf.IsSet may still report true — HasUserOverride is the real contract). diff --git a/infrastructure/configuration/template/js/ui/form-handler.js b/infrastructure/configuration/template/js/ui/form-handler.js index 66af3f12f..6db3d8abf 100644 --- a/infrastructure/configuration/template/js/ui/form-handler.js +++ b/infrastructure/configuration/template/js/ui/form-handler.js @@ -206,11 +206,13 @@ } } - // Org-scope folder override fields cleared by a folder reset. Emitting flat null - // for each tells the IDE plugin to send {value:null, changed:true}, which makes - // snyk-ls Unset the user:folder: override so the value falls back to org/LDX/default. + // Folder override fields cleared by a folder reset. Emitting flat null for each + // tells the IDE plugin to send {value:null, changed:true}, which makes snyk-ls + // Unset the user:folder: override so the value falls back to org/LDX/default. // preferred_org is included: snyk-ls special-cases its null-reset to also unset // org_set_by_user, reverting the folder to its auto-determined / global org. + // additional_parameters / additional_environment / scan_command_config are + // non-scalar overrides; snyk-ls's basic folder-field handlers honor the null reset. var FOLDER_RESET_FIELDS = [ "scan_automatic", "scan_net_new", @@ -226,6 +228,9 @@ "issue_view_ignored_issues", "risk_score_threshold", "preferred_org", + "additional_parameters", + "additional_environment", + "scan_command_config", ]; // Mark a folder for complete reset (all org-scope overrides will be set to null). diff --git a/infrastructure/configuration/template/js/ui/reset-handler.js b/infrastructure/configuration/template/js/ui/reset-handler.js index f13bcab41..2dcd1af1a 100644 --- a/infrastructure/configuration/template/js/ui/reset-handler.js +++ b/infrastructure/configuration/template/js/ui/reset-handler.js @@ -65,12 +65,8 @@ return; } - if (!confirm("Reset " + formatSectionName(section) + " to defaults?")) { - return; - } - var defaults = sectionDefaults[section]; - applyDefaults(defaults, section); + applyDefaults(defaults); // Trigger dirty tracking update if (window.dirtyTracker) { @@ -99,10 +95,6 @@ return; } - if (!confirm("Reset all overrides for this folder — your custom overrides will be removed")) { - return; - } - resetFolderOverrides(folderPath); // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). @@ -120,7 +112,7 @@ } // Apply default values to form fields - function applyDefaults(defaults, section) { + function applyDefaults(defaults) { for (var fieldName in defaults) { if (!defaults.hasOwnProperty(fieldName)) continue; @@ -170,18 +162,6 @@ } } - // Format section name for display - function formatSectionName(section) { - var names = { - scanConfiguration: "Scan configuration", - filteringDisplay: "Filters and views", - authentication: "Authentication", - cliConfiguration: "CLI configuration", - permissions: "Trust settings" - }; - return names[section] || section; - } - // Trigger change event on element function triggerChange(element) { var event; diff --git a/internal/types/folder_config.go b/internal/types/folder_config.go index 45d4fec31..80846f761 100644 --- a/internal/types/folder_config.go +++ b/internal/types/folder_config.go @@ -451,7 +451,28 @@ func (fc *FolderConfig) applyLocalBranches(update *LspFolderConfig, conf configu return true } +// isFolderReset reports whether a setting carries a {Changed:true, Value:nil} +// reset marker. getSettingValue treats a nil value as "not present", so basic +// folder-field handlers must check this explicitly to clear their override +// (mirrors the generic reset path in applyGenericFolderOverrides). +func isFolderReset(settings map[string]*ConfigSetting, name string) bool { + cs := settings[name] + return cs != nil && cs.Changed && cs.Value == nil +} + +// unsetFolderOverride clears the user:folder: override for name if one exists. +func (fc *FolderConfig) unsetFolderOverride(conf configuration.Configuration, fp, name string) bool { + if !HasUserOverride(conf, fc.FolderPath, name) { + return false + } + conf.Unset(configresolver.UserFolderKey(fp, name)) + return true +} + func (fc *FolderConfig) applyStringField(update *LspFolderConfig, conf configuration.Configuration, fp, name string, setUser func(string, any)) bool { + if isFolderReset(update.Settings, name) { + return fc.unsetFolderOverride(conf, fp, name) + } val, ok := getSettingValue[string](update.Settings, name) if !ok { return false @@ -464,6 +485,9 @@ func (fc *FolderConfig) applyStringField(update *LspFolderConfig, conf configura } func (fc *FolderConfig) applyStringSliceField(update *LspFolderConfig, conf configuration.Configuration, fp, name string, setUser func(string, any)) bool { + if isFolderReset(update.Settings, name) { + return fc.unsetFolderOverride(conf, fp, name) + } val, ok := getStringSliceFromSetting(update.Settings, name) if !ok { return false @@ -476,6 +500,9 @@ func (fc *FolderConfig) applyStringSliceField(update *LspFolderConfig, conf conf } func (fc *FolderConfig) applyScanCommandConfig(update *LspFolderConfig, conf configuration.Configuration, fp string, setUser func(string, any)) bool { + if isFolderReset(update.Settings, SettingScanCommandConfig) { + return fc.unsetFolderOverride(conf, fp, SettingScanCommandConfig) + } scanCmdConfig, ok := getScanCommandConfigFromSetting(update.Settings, SettingScanCommandConfig) if !ok || len(scanCmdConfig) == 0 { return false diff --git a/js-tests/folder-reset.test.mjs b/js-tests/folder-reset.test.mjs index baf3fd514..5aed24085 100644 --- a/js-tests/folder-reset.test.mjs +++ b/js-tests/folder-reset.test.mjs @@ -152,7 +152,6 @@ test("applyFolderResets clears window.ConfigApp.folderResets after applying", as test("DOM-driven: clicking .reset-overrides-btn produces 14 nulls for that folderPath in the save payload", async () => { const win = await buildDom(); const doc = win.document; - win.confirm = () => true; const calls = spySave(win); // Click the reset button for folder index 1 (PATH_B). The click both marks the folder and @@ -171,7 +170,6 @@ test("DOM-driven: clicking .reset-overrides-btn produces 14 nulls for that folde test("DOM-driven: reset button click does not require the folder to have other edits", async () => { const win = await buildDom(); const doc = win.document; - win.confirm = () => true; const calls = spySave(win); // Folder index 0 (PATH_A), no edits made — reset-only. @@ -189,7 +187,6 @@ test("DOM-driven: reset button click does not require the folder to have other e test("clicking reset triggers a save even with no other edits", async () => { const win = await buildDom(); const doc = win.document; - win.confirm = () => true; const calls = spySave(win); // A folder reset changes no DOM input, so without an explicit save trigger it would never diff --git a/scripts/config-dialog/config_output_multi_project.html b/scripts/config-dialog/config_output_multi_project.html index 1f4053e64..2695db8cc 100644 --- a/scripts/config-dialog/config_output_multi_project.html +++ b/scripts/config-dialog/config_output_multi_project.html @@ -3532,11 +3532,13 @@

Filters and views

} } - // Org-scope folder override fields cleared by a folder reset. Emitting flat null - // for each tells the IDE plugin to send {value:null, changed:true}, which makes - // snyk-ls Unset the user:folder: override so the value falls back to org/LDX/default. + // Folder override fields cleared by a folder reset. Emitting flat null for each + // tells the IDE plugin to send {value:null, changed:true}, which makes snyk-ls + // Unset the user:folder: override so the value falls back to org/LDX/default. // preferred_org is included: snyk-ls special-cases its null-reset to also unset // org_set_by_user, reverting the folder to its auto-determined / global org. + // additional_parameters / additional_environment / scan_command_config are + // non-scalar overrides; snyk-ls's basic folder-field handlers honor the null reset. var FOLDER_RESET_FIELDS = [ "scan_automatic", "scan_net_new", @@ -3552,6 +3554,9 @@

Filters and views

"issue_view_ignored_issues", "risk_score_threshold", "preferred_org", + "additional_parameters", + "additional_environment", + "scan_command_config", ]; // Mark a folder for complete reset (all org-scope overrides will be set to null). @@ -3704,12 +3709,8 @@

Filters and views

return; } - if (!confirm("Reset " + formatSectionName(section) + " to defaults?")) { - return; - } - var defaults = sectionDefaults[section]; - applyDefaults(defaults, section); + applyDefaults(defaults); // Trigger dirty tracking update if (window.dirtyTracker) { @@ -3738,10 +3739,6 @@

Filters and views

return; } - if (!confirm("Reset all overrides for this folder — your custom overrides will be removed")) { - return; - } - resetFolderOverrides(folderPath); // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). @@ -3759,7 +3756,7 @@

Filters and views

} // Apply default values to form fields - function applyDefaults(defaults, section) { + function applyDefaults(defaults) { for (var fieldName in defaults) { if (!defaults.hasOwnProperty(fieldName)) continue; @@ -3809,18 +3806,6 @@

Filters and views

} } - // Format section name for display - function formatSectionName(section) { - var names = { - scanConfiguration: "Scan configuration", - filteringDisplay: "Filters and views", - authentication: "Authentication", - cliConfiguration: "CLI configuration", - permissions: "Trust settings" - }; - return names[section] || section; - } - // Trigger change event on element function triggerChange(element) { var event; diff --git a/scripts/config-dialog/config_output_no_projects.html b/scripts/config-dialog/config_output_no_projects.html index 260e3794a..6cdc613ca 100644 --- a/scripts/config-dialog/config_output_no_projects.html +++ b/scripts/config-dialog/config_output_no_projects.html @@ -2315,11 +2315,13 @@

Filters and views

} } - // Org-scope folder override fields cleared by a folder reset. Emitting flat null - // for each tells the IDE plugin to send {value:null, changed:true}, which makes - // snyk-ls Unset the user:folder: override so the value falls back to org/LDX/default. + // Folder override fields cleared by a folder reset. Emitting flat null for each + // tells the IDE plugin to send {value:null, changed:true}, which makes snyk-ls + // Unset the user:folder: override so the value falls back to org/LDX/default. // preferred_org is included: snyk-ls special-cases its null-reset to also unset // org_set_by_user, reverting the folder to its auto-determined / global org. + // additional_parameters / additional_environment / scan_command_config are + // non-scalar overrides; snyk-ls's basic folder-field handlers honor the null reset. var FOLDER_RESET_FIELDS = [ "scan_automatic", "scan_net_new", @@ -2335,6 +2337,9 @@

Filters and views

"issue_view_ignored_issues", "risk_score_threshold", "preferred_org", + "additional_parameters", + "additional_environment", + "scan_command_config", ]; // Mark a folder for complete reset (all org-scope overrides will be set to null). @@ -2487,12 +2492,8 @@

Filters and views

return; } - if (!confirm("Reset " + formatSectionName(section) + " to defaults?")) { - return; - } - var defaults = sectionDefaults[section]; - applyDefaults(defaults, section); + applyDefaults(defaults); // Trigger dirty tracking update if (window.dirtyTracker) { @@ -2521,10 +2522,6 @@

Filters and views

return; } - if (!confirm("Reset all overrides for this folder — your custom overrides will be removed")) { - return; - } - resetFolderOverrides(folderPath); // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). @@ -2542,7 +2539,7 @@

Filters and views

} // Apply default values to form fields - function applyDefaults(defaults, section) { + function applyDefaults(defaults) { for (var fieldName in defaults) { if (!defaults.hasOwnProperty(fieldName)) continue; @@ -2592,18 +2589,6 @@

Filters and views

} } - // Format section name for display - function formatSectionName(section) { - var names = { - scanConfiguration: "Scan configuration", - filteringDisplay: "Filters and views", - authentication: "Authentication", - cliConfiguration: "CLI configuration", - permissions: "Trust settings" - }; - return names[section] || section; - } - // Trigger change event on element function triggerChange(element) { var event; diff --git a/scripts/config-dialog/config_output_single_solution.html b/scripts/config-dialog/config_output_single_solution.html index 2ea27b3cd..3a18b8e0c 100644 --- a/scripts/config-dialog/config_output_single_solution.html +++ b/scripts/config-dialog/config_output_single_solution.html @@ -2615,11 +2615,13 @@

Filters and views

} } - // Org-scope folder override fields cleared by a folder reset. Emitting flat null - // for each tells the IDE plugin to send {value:null, changed:true}, which makes - // snyk-ls Unset the user:folder: override so the value falls back to org/LDX/default. + // Folder override fields cleared by a folder reset. Emitting flat null for each + // tells the IDE plugin to send {value:null, changed:true}, which makes snyk-ls + // Unset the user:folder: override so the value falls back to org/LDX/default. // preferred_org is included: snyk-ls special-cases its null-reset to also unset // org_set_by_user, reverting the folder to its auto-determined / global org. + // additional_parameters / additional_environment / scan_command_config are + // non-scalar overrides; snyk-ls's basic folder-field handlers honor the null reset. var FOLDER_RESET_FIELDS = [ "scan_automatic", "scan_net_new", @@ -2635,6 +2637,9 @@

Filters and views

"issue_view_ignored_issues", "risk_score_threshold", "preferred_org", + "additional_parameters", + "additional_environment", + "scan_command_config", ]; // Mark a folder for complete reset (all org-scope overrides will be set to null). @@ -2787,12 +2792,8 @@

Filters and views

return; } - if (!confirm("Reset " + formatSectionName(section) + " to defaults?")) { - return; - } - var defaults = sectionDefaults[section]; - applyDefaults(defaults, section); + applyDefaults(defaults); // Trigger dirty tracking update if (window.dirtyTracker) { @@ -2821,10 +2822,6 @@

Filters and views

return; } - if (!confirm("Reset all overrides for this folder — your custom overrides will be removed")) { - return; - } - resetFolderOverrides(folderPath); // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). @@ -2842,7 +2839,7 @@

Filters and views

} // Apply default values to form fields - function applyDefaults(defaults, section) { + function applyDefaults(defaults) { for (var fieldName in defaults) { if (!defaults.hasOwnProperty(fieldName)) continue; @@ -2892,18 +2889,6 @@

Filters and views

} } - // Format section name for display - function formatSectionName(section) { - var names = { - scanConfiguration: "Scan configuration", - filteringDisplay: "Filters and views", - authentication: "Authentication", - cliConfiguration: "CLI configuration", - permissions: "Trust settings" - }; - return names[section] || section; - } - // Trigger change event on element function triggerChange(element) { var event; From 15ed254b9c7dbeb63b74837b68622946ccd0841b Mon Sep 17 00:00:00 2001 From: Ben Durrans Date: Tue, 23 Jun 2026 09:50:17 +0100 Subject: [PATCH 03/10] refactor: simplify preferred_org reset using isFolderReset + unsetFolderOverride Co-Authored-By: Claude Sonnet 4.6 --- internal/types/folder_config.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/internal/types/folder_config.go b/internal/types/folder_config.go index 80846f761..593e819ba 100644 --- a/internal/types/folder_config.go +++ b/internal/types/folder_config.go @@ -603,17 +603,10 @@ func (fc *FolderConfig) applyPreferredOrg(update *LspFolderConfig, handled map[s // handled explicitly first. Unset both preferred_org and org_set_by_user so the folder // reverts to its auto-determined (LDX) / global org. Mirrors the generic reset path in // applyGenericFolderOverrides. - if cs := update.Settings[SettingPreferredOrg]; cs != nil && cs.Changed && cs.Value == nil { - changed := false - if HasUserOverride(conf, fc.FolderPath, SettingPreferredOrg) { - conf.Unset(configresolver.UserFolderKey(fp, SettingPreferredOrg)) - changed = true - } - if HasUserOverride(conf, fc.FolderPath, SettingOrgSetByUser) { - conf.Unset(configresolver.UserFolderKey(fp, SettingOrgSetByUser)) - changed = true - } - return changed + if isFolderReset(update.Settings, SettingPreferredOrg) { + preferredOrgChanged := fc.unsetFolderOverride(conf, fp, SettingPreferredOrg) + orgSetByUserChanged := fc.unsetFolderOverride(conf, fp, SettingOrgSetByUser) + return preferredOrgChanged || orgSetByUserChanged } preferredOrg, ok := getSettingValue[string](update.Settings, SettingPreferredOrg) From 824b5d35813ecc6450c138e4871324660a33dc1a Mon Sep 17 00:00:00 2001 From: Ben Durrans Date: Tue, 23 Jun 2026 10:26:37 +0100 Subject: [PATCH 04/10] fix: address PR review feedback on docs, tests, and test names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs: update reset field count 14→17, add additional_parameters, additional_environment, scan_command_config to the reset field list - docs: correct incorrect claim that scan_command_config null-reset is a no-op; the LS handler now honours it via unsetFolderOverride - tests: mirror FOLDER_RESET_FIELDS in RESET_FIELDS (14→17 entries) - tests: remove AI-style bug annotation from test name - tests: update "14 nulls" count to "17 nulls" in DOM-driven test name - tests: rename test to say "auto-org" not "global org" Co-Authored-By: Claude Sonnet 4.6 --- docs/configuration-dialog.md | 5 +++-- docs/configuration.md | 2 +- internal/types/config_resolver_test.go | 2 +- js-tests/folder-reset.test.mjs | 9 ++++++--- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/configuration-dialog.md b/docs/configuration-dialog.md index f972f3b3b..dcbac9f8e 100644 --- a/docs/configuration-dialog.md +++ b/docs/configuration-dialog.md @@ -155,14 +155,15 @@ See [Saving Configuration Flow](#saving-configuration-flow) for the sequence dia The per-folder **"Reset overrides"** button (one per folder tab, top-right in the disclaimer banner) clears all of that folder's user overrides so the effective values fall back to org / LDX-sync / default. -**JS side (snyk-ls, already implemented):** clicking the button marks the folder *by its `folderPath`* (read from the hidden `folder__folderPath` input — not the index, which is compacted away during diffing). On save, `form-handler.applyFolderResets()` emits **flat `null`** for each of these 14 folder fields on that folder's entry in `folderConfigs`: +**JS side (snyk-ls, already implemented):** clicking the button marks the folder *by its `folderPath`* (read from the hidden `folder__folderPath` input — not the index, which is compacted away during diffing). On save, `form-handler.applyFolderResets()` emits **flat `null`** for each of these 17 folder fields on that folder's entry in `folderConfigs`: ``` scan_automatic, scan_net_new, severity_filter_critical, severity_filter_high, severity_filter_medium, severity_filter_low, snyk_oss_enabled, snyk_code_enabled, snyk_iac_enabled, snyk_secrets_enabled, issue_view_open_issues, issue_view_ignored_issues, risk_score_threshold, -preferred_org +preferred_org, +additional_parameters, additional_environment, scan_command_config ``` A reset is emitted **even if the folder has no other edits** — a reset-only folder still appears in the outbound `folderConfigs` keyed by `folderPath`. The JS deliberately emits **flat snake_case `null`**, not a `ConfigSetting` envelope; building the envelope is the IDE plugin's job (next paragraph). diff --git a/docs/configuration.md b/docs/configuration.md index e84a8ef1a..ddcc90c26 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -467,7 +467,7 @@ A `ConfigSetting` value of `nil` combined with `Changed: true` is a **reset**: i | `{Changed: true, Value: }` | set `user:folder:` override to `` | | `{Changed: true, Value: nil}` | **Unset** `user:folder:` override → fall back | -The null-reset applies to org-scope folder fields (those resolved through `applyGenericFolderOverrides`), which have a lower-precedence layer to fall back to. **Basic folder-native fields** (`base_branch`, `scan_command_config`, etc.) have no fallback layer, so a null value on them is treated as a no-op rather than a reset. +The null-reset applies to org-scope folder fields (those resolved through `applyGenericFolderOverrides`), which have a lower-precedence layer to fall back to. Most basic folder-native fields (`base_branch`, etc.) have no fallback layer, so a null value on them is treated as a no-op rather than a reset. **Exception:** `additional_parameters`, `additional_environment`, and `scan_command_config` are basic folder fields whose null-reset is honoured — their handlers in `applyBasicFolderFields` call `unsetFolderOverride` on `{Changed: true, Value: nil}`, clearing the user override. `preferred_org` is the one exception among the basic-handled fields: it *does* have a fallback (the auto-determined LDX org, then the global org), so its null-reset is honoured. Resetting `preferred_org` Unsets **both** `user:folder:preferred_org` and `user:folder:org_set_by_user`, reverting the folder to its auto-determined / global org. See `applyPreferredOrg` in `internal/types/folder_config.go`. diff --git a/internal/types/config_resolver_test.go b/internal/types/config_resolver_test.go index 561f0052f..45e68c38d 100644 --- a/internal/types/config_resolver_test.go +++ b/internal/types/config_resolver_test.go @@ -1176,7 +1176,7 @@ func TestFolderConfig_ApplyLspUpdate(t *testing.T) { "ApplyLspUpdate should apply setting; lock enforcement is the caller's responsibility (validateLockedFields)") }) - t.Run("clears preferred_org override via explicit null and reverts to global org", func(t *testing.T) { + t.Run("clears preferred_org override via explicit null and reverts to auto-org", func(t *testing.T) { conf := configuration.NewWithOpts(configuration.WithAutomaticEnv()) fs := pflag.NewFlagSet("test", pflag.ContinueOnError) types.RegisterAllConfigurations(fs) diff --git a/js-tests/folder-reset.test.mjs b/js-tests/folder-reset.test.mjs index 5aed24085..7bacf92c8 100644 --- a/js-tests/folder-reset.test.mjs +++ b/js-tests/folder-reset.test.mjs @@ -5,7 +5,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { buildDom } from "./helpers.mjs"; -// The 14 org-scope folder fields a reset must clear (mirrors FOLDER_RESET_FIELDS in form-handler.js). +// The 17 folder fields a reset must clear (mirrors FOLDER_RESET_FIELDS in form-handler.js). const RESET_FIELDS = [ "scan_automatic", "scan_net_new", @@ -21,6 +21,9 @@ const RESET_FIELDS = [ "issue_view_ignored_issues", "risk_score_threshold", "preferred_org", + "additional_parameters", + "additional_environment", + "scan_command_config", ]; // Folder paths embedded in the dummy-data fixture (js-tests/fixtures/config-page.html). @@ -108,7 +111,7 @@ test("applyFolderResets creates folderConfigs array when missing", async () => { assert.equal(data.folderConfigs[0].folderPath, PATH_A); }); -test("applyFolderResets nulls only the marked folder; others untouched (bug #1 — compaction/index drift)", async () => { +test("applyFolderResets nulls only the marked folder; others untouched", async () => { const win = await buildDom(); const fh = win.ConfigApp.formHandler; @@ -149,7 +152,7 @@ test("applyFolderResets clears window.ConfigApp.folderResets after applying", as assert.equal(fh.isFolderMarkedForReset(PATH_A), false, "no longer marked"); }); -test("DOM-driven: clicking .reset-overrides-btn produces 14 nulls for that folderPath in the save payload", async () => { +test("DOM-driven: clicking .reset-overrides-btn produces 17 nulls for that folderPath in the save payload", async () => { const win = await buildDom(); const doc = win.document; const calls = spySave(win); From 1bad55e5c520e4e543518a4efcc888e11782dbc7 Mon Sep 17 00:00:00 2001 From: Ben Durrans Date: Tue, 23 Jun 2026 10:29:38 +0100 Subject: [PATCH 05/10] test: replace duplicate reset test with dirty-tracker clean assertion Remove the duplicate "clicking reset triggers a save even with no other edits" test (already covered by the DOM-driven no-edits test above it) and add a test asserting the dirty tracker returns to clean after a successful reset save. Co-Authored-By: Claude Sonnet 4.6 --- js-tests/folder-reset.test.mjs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/js-tests/folder-reset.test.mjs b/js-tests/folder-reset.test.mjs index 7bacf92c8..26f889c16 100644 --- a/js-tests/folder-reset.test.mjs +++ b/js-tests/folder-reset.test.mjs @@ -187,22 +187,17 @@ test("DOM-driven: reset button click does not require the folder to have other e assertAllNull(entry, RESET_FIELDS); }); -test("clicking reset triggers a save even with no other edits", async () => { +test("reset click leaves dirty tracker clean after save", async () => { const win = await buildDom(); - const doc = win.document; const calls = spySave(win); - // A folder reset changes no DOM input, so without an explicit save trigger it would never - // persist (this was the VS Code bug). Regression guard: the click alone must reach - // __saveIdeConfig__. The reset handler calls getAndSaveIdeConfig() directly, so this holds for - // every IDE, not just auto-save ones. - const btn = doc.querySelector('.reset-overrides-btn[data-folder-index="1"]'); - assert.ok(btn, "reset button for folder 1 exists"); + win.dirtyTracker.setDirtyState(true); + assert.ok(win.dirtyTracker.isDirty, "precondition: tracker is dirty"); + + const btn = win.document.querySelector('.reset-overrides-btn[data-folder-index="0"]'); + assert.ok(btn, "reset button for folder 0 exists"); btn.click(); - assert.ok(calls.length > 0, "reset click must trigger a save"); - const saved = JSON.parse(calls[calls.length - 1]); - const entry = (saved.folderConfigs || []).find((f) => f.folderPath === PATH_B); - assert.ok(entry, "saved payload must contain the reset folder"); - assertAllNull(entry, RESET_FIELDS); + assert.ok(calls.length > 0, "reset must trigger a save"); + assert.equal(win.dirtyTracker.isDirty, false, "dirty tracker must be clean after a successful reset save"); }); From 926b09140054a8204dc4ac767cd55d815bfaf9e4 Mon Sep 17 00:00:00 2001 From: Ben Durrans Date: Tue, 23 Jun 2026 11:19:47 +0100 Subject: [PATCH 06/10] =?UTF-8?q?fix:=20update=20stale=2014=E2=86=9217=20c?= =?UTF-8?q?ounts=20and=20remove=20iteration=20label=20in=20test=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- js-tests/folder-reset.test.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js-tests/folder-reset.test.mjs b/js-tests/folder-reset.test.mjs index 26f889c16..f507852d3 100644 --- a/js-tests/folder-reset.test.mjs +++ b/js-tests/folder-reset.test.mjs @@ -1,5 +1,5 @@ // ABOUTME: Tests for the per-folder "Reset overrides" flow (form-handler + reset-handler). -// ABOUTME: Verifies resets are keyed by folderPath, emit 14 flat nulls, and survive compaction. +// ABOUTME: Verifies resets are keyed by folderPath, emit 17 flat nulls, and survive compaction. import assert from "node:assert/strict"; import test from "node:test"; @@ -65,7 +65,7 @@ test("markFolderForReset ignores empty/missing folderPath", async () => { assert.equal(fh.isFolderMarkedForReset(undefined), false, "undefined path must not mark"); }); -test("applyFolderResets sets all 14 fields to null on an existing edited folder, preserving folderPath", async () => { +test("applyFolderResets sets all 17 fields to null on an existing edited folder, preserving folderPath", async () => { const win = await buildDom(); const fh = win.ConfigApp.formHandler; @@ -83,7 +83,7 @@ test("applyFolderResets sets all 14 fields to null on an existing edited folder, assertAllNull(entry, RESET_FIELDS); }); -test("applyFolderResets emits a reset-only folder absent from data.folderConfigs (bug #2)", async () => { +test("applyFolderResets emits a reset-only folder absent from data.folderConfigs", async () => { const win = await buildDom(); const fh = win.ConfigApp.formHandler; From afc756224132f587b69fbad5a2da087c126b1f4f Mon Sep 17 00:00:00 2001 From: Ben Durrans Date: Tue, 23 Jun 2026 12:33:47 +0100 Subject: [PATCH 07/10] fix: guard updateFolderConfigOrg from re-adding keys after a reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After applyPreferredOrg unsets preferred_org and org_set_by_user, updateFolderConfigOrg was calling SetPreferredOrgAndOrgSetByUser("",false) which re-added both keys as {Value:""/false, Changed:true} — making HasUserOverride return true even though the user had just reset the overrides. Add a guard: only normalise to empty/false when at least one of the keys is still present (i.e. a non-reset transition). If both are already absent the call is a no-op for resolution purposes but creates a spurious active-override entry in the store. Also add SettingIsLspInitialized=true and DepScanStateAggregator to the end-to-end reset test so it exercises the production code path through updateFolderOrgIfNeeded. Co-Authored-By: Claude Sonnet 4.6 --- application/server/configuration.go | 9 ++++++++- application/server/configuration_test.go | 14 +++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/application/server/configuration.go b/application/server/configuration.go index 375884cfc..278cb09ae 100644 --- a/application/server/configuration.go +++ b/application/server/configuration.go @@ -1457,7 +1457,14 @@ func updateFolderConfigOrg(conf configuration.Configuration, logger *zerolog.Log orgHasJustChanged := currentSnap.PreferredOrg != oldSnapshot.PreferredOrg if orgSetByUserJustChanged { if !currentSnap.OrgSetByUser { - types.SetPreferredOrgAndOrgSetByUser(conf, folderConfig.FolderPath, "", false) + // Only normalise to {"","false"} if the keys are still present. A reset via + // applyPreferredOrg already Unset both; re-writing them here would turn the + // clean absence into an explicit {Value:""/false, Changed:true} entry that + // HasUserOverride treats as an active override. + if types.HasUserOverride(conf, folderConfig.FolderPath, types.SettingPreferredOrg) || + types.HasUserOverride(conf, folderConfig.FolderPath, types.SettingOrgSetByUser) { + types.SetPreferredOrgAndOrgSetByUser(conf, folderConfig.FolderPath, "", false) + } } } else if orgHasJustChanged { types.SetPreferredOrgAndOrgSetByUser(conf, folderConfig.FolderPath, currentSnap.PreferredOrg, true) diff --git a/application/server/configuration_test.go b/application/server/configuration_test.go index cd1b0537d..884ef1902 100644 --- a/application/server/configuration_test.go +++ b/application/server/configuration_test.go @@ -732,14 +732,18 @@ func Test_updateFolderConfig_ResetClearsUserOverrides_EndToEnd(t *testing.T) { folderPath := types.FilePath(folderDir) ctx := ctx2.NewContextWithDependencies(t.Context(), map[string]any{ - ctx2.DepNotifier: deps.Notifier, - ctx2.DepAuthService: deps.AuthenticationService, - ctx2.DepConfigResolver: deps.ConfigResolver, - ctx2.DepFeatureFlagService: deps.FeatureFlagService, - ctx2.DepLdxSyncService: deps.LdxSyncService, + ctx2.DepNotifier: deps.Notifier, + ctx2.DepAuthService: deps.AuthenticationService, + ctx2.DepConfigResolver: deps.ConfigResolver, + ctx2.DepFeatureFlagService: deps.FeatureFlagService, + ctx2.DepLdxSyncService: deps.LdxSyncService, + ctx2.DepScanStateAggregator: scanstates.NewNoopStateAggregator(), }) resolver := testutil.DefaultConfigResolver(engine) + // Exercise the production code path: updateFolderOrgIfNeeded is gated on this flag. + conf.Set(types.SettingIsLspInitialized, true) + // Step 1: seed user overrides on the folder (the "user edited overrides" state). seed := []types.LspFolderConfig{{ FolderPath: folderPath, From 1218b35c031f12e230daca72f50d4bc077d1ae5e Mon Sep 17 00:00:00 2001 From: Ben Durrans Date: Tue, 23 Jun 2026 13:40:57 +0100 Subject: [PATCH 08/10] fix: apply HasUserOverride guard to else-if branch in updateFolderConfigOrg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The else-if !currentSnap.OrgSetByUser branch was missing the same guard added to the orgSetByUserJustChanged branch. After a reset clears both overrides, a subsequent update changing only AutoDeterminedOrg would trigger this branch and re-add preferred_org / org_set_by_user as explicit {Value:""/false, Changed:true} entries, making HasUserOverride return true again. Apply the same guard: skip the normalisation call when both keys are already absent. Add a regression test that seeds the exact scenario: reset → AutoDeterminedOrg change → assert keys stay absent. Co-Authored-By: Claude Sonnet 4.6 --- application/server/configuration.go | 5 ++- application/server/configuration_test.go | 54 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/application/server/configuration.go b/application/server/configuration.go index 278cb09ae..eee9dbe1d 100644 --- a/application/server/configuration.go +++ b/application/server/configuration.go @@ -1469,7 +1469,10 @@ func updateFolderConfigOrg(conf configuration.Configuration, logger *zerolog.Log } else if orgHasJustChanged { types.SetPreferredOrgAndOrgSetByUser(conf, folderConfig.FolderPath, currentSnap.PreferredOrg, true) } else if !currentSnap.OrgSetByUser { - types.SetPreferredOrgAndOrgSetByUser(conf, folderConfig.FolderPath, "", false) + if types.HasUserOverride(conf, folderConfig.FolderPath, types.SettingPreferredOrg) || + types.HasUserOverride(conf, folderConfig.FolderPath, types.SettingOrgSetByUser) { + types.SetPreferredOrgAndOrgSetByUser(conf, folderConfig.FolderPath, "", false) + } } } diff --git a/application/server/configuration_test.go b/application/server/configuration_test.go index 884ef1902..4abfcbfc2 100644 --- a/application/server/configuration_test.go +++ b/application/server/configuration_test.go @@ -809,6 +809,60 @@ func Test_updateFolderConfig_ResetClearsUserOverrides_EndToEnd(t *testing.T) { assert.Empty(t, resetFC.PreferredOrg(), "PreferredOrg falls back after reset") } +// Test_updateFolderConfig_AutoDeterminedOrgChangeAfterReset guards the else-if branch in +// updateFolderConfigOrg: after a reset clears both overrides, a subsequent update that +// changes only AutoDeterminedOrg must not re-add preferred_org / org_set_by_user. +func Test_updateFolderConfig_AutoDeterminedOrgChangeAfterReset(t *testing.T) { + engine, tokenService := testutil.UnitTestWithEngine(t) + deps := di.TestInit(t, engine, tokenService, nil) + conf := engine.GetConfiguration() + logger := engine.GetLogger() + + folderDir := filepath.Join(t.TempDir(), "folder") + require.NoError(t, initTestRepo(t, folderDir)) + folderPath := types.FilePath(folderDir) + + ctx := ctx2.NewContextWithDependencies(t.Context(), map[string]any{ + ctx2.DepNotifier: deps.Notifier, + ctx2.DepAuthService: deps.AuthenticationService, + ctx2.DepConfigResolver: deps.ConfigResolver, + ctx2.DepFeatureFlagService: deps.FeatureFlagService, + ctx2.DepLdxSyncService: deps.LdxSyncService, + ctx2.DepScanStateAggregator: scanstates.NewNoopStateAggregator(), + }) + resolver := testutil.DefaultConfigResolver(engine) + conf.Set(types.SettingIsLspInitialized, true) + + // Seed a folder org override, then reset it. + seed := []types.LspFolderConfig{{ + FolderPath: folderPath, + Settings: map[string]*types.ConfigSetting{ + types.SettingPreferredOrg: {Value: "my-folder-org", Changed: true}, + }, + }} + UpdateSettings(ctx, conf, engine, logger, map[string]*types.ConfigSetting{}, seed, analytics.TriggerSourceTest, resolver) + require.True(t, types.HasUserOverride(conf, folderPath, types.SettingPreferredOrg)) + + reset := []types.LspFolderConfig{{ + FolderPath: folderPath, + Settings: map[string]*types.ConfigSetting{types.SettingPreferredOrg: {Value: nil, Changed: true}}, + }} + UpdateSettings(ctx, conf, engine, logger, map[string]*types.ConfigSetting{}, reset, analytics.TriggerSourceTest, resolver) + require.False(t, types.HasUserOverride(conf, folderPath, types.SettingPreferredOrg), "precondition: override cleared") + require.False(t, types.HasUserOverride(conf, folderPath, types.SettingOrgSetByUser), "precondition: org_set_by_user cleared") + + // Simulate an AutoDeterminedOrg change (e.g. from LDX-sync) while org-user settings stay empty. + // This triggers the else-if !currentSnap.OrgSetByUser branch in updateFolderConfigOrg. + types.SetAutoDeterminedOrg(conf, folderPath, "ldx-org") + + noop := []types.LspFolderConfig{{FolderPath: folderPath, Settings: map[string]*types.ConfigSetting{}}} + UpdateSettings(ctx, conf, engine, logger, map[string]*types.ConfigSetting{}, noop, analytics.TriggerSourceTest, resolver) + + // The reset keys must remain absent — the AutoDeterminedOrg change must not re-add them. + assert.False(t, types.HasUserOverride(conf, folderPath, types.SettingPreferredOrg), "preferred_org must stay absent after AutoDeterminedOrg change") + assert.False(t, types.HasUserOverride(conf, folderPath, types.SettingOrgSetByUser), "org_set_by_user must stay absent after AutoDeterminedOrg change") +} + func initTestRepo(t *testing.T, tempDir string) error { t.Helper() repo1, err := git.PlainInit(tempDir, false) From 731d5644fd082b2f30be0566b61e9e8c7d990e4d Mon Sep 17 00:00:00 2001 From: Ben Durrans Date: Tue, 23 Jun 2026 09:12:52 +0100 Subject: [PATCH 09/10] feat: route reset confirm dialogs through native IDE UI [IDE-1945] VSCode sandboxed webviews block window.confirm() silently (no allow-modals). Add ideBridge.confirm(message, callback) that routes through the existing executeCommand bridge to snyk.showConfirmationDialog in VSCode, falling back to window.confirm() in IDEs with no bridge (JetBrains, Eclipse). Both reset handlers (section and folder override) now gate their work behind this callback. Co-Authored-By: Claude Sonnet 4.6 --- .../configuration/template/js/ide/bridge.js | 16 +++++ .../template/js/ui/reset-handler.js | 46 ++++++++------ .../config_output_multi_project.html | 62 +++++++++++++------ .../config_output_no_projects.html | 62 +++++++++++++------ .../config_output_single_solution.html | 62 +++++++++++++------ 5 files changed, 172 insertions(+), 76 deletions(-) diff --git a/infrastructure/configuration/template/js/ide/bridge.js b/infrastructure/configuration/template/js/ide/bridge.js index bd9a95124..d389ee3d6 100644 --- a/infrastructure/configuration/template/js/ide/bridge.js +++ b/infrastructure/configuration/template/js/ide/bridge.js @@ -101,6 +101,22 @@ } }; + /** + * Show a confirmation dialog via the IDE native UI. + * In VSCode the sandboxed webview blocks window.confirm(), so this routes through + * the executeCommand bridge to show a native modal. Falls back to window.confirm() + * in IDEs that inject no bridge (JetBrains, Eclipse, etc.). + * @param {string} message - Message to display + * @param {function} callback - Called with true if confirmed, false if cancelled + */ + ideBridge.confirm = function (message, callback) { + if (typeof window.__ideExecuteCommand__ === "function") { + window.__ideExecuteCommand__("snyk.showConfirmationDialog", [message], callback); + } else if (typeof callback === "function") { + callback(window.confirm(message)); + } + }; + // Expose window-level functions for IDE to call /** diff --git a/infrastructure/configuration/template/js/ui/reset-handler.js b/infrastructure/configuration/template/js/ui/reset-handler.js index 2dcd1af1a..555acbc8d 100644 --- a/infrastructure/configuration/template/js/ui/reset-handler.js +++ b/infrastructure/configuration/template/js/ui/reset-handler.js @@ -65,14 +65,18 @@ return; } - var defaults = sectionDefaults[section]; - applyDefaults(defaults); + window.ConfigApp.ideBridge.confirm("Reset all settings in this section to defaults?", function (confirmed) { + if (!confirmed) return; - // Trigger dirty tracking update - if (window.dirtyTracker) { - window.dirtyTracker.runChangeListeners(); - window.dirtyTracker.checkDirty(); - } + var defaults = sectionDefaults[section]; + applyDefaults(defaults); + + // Trigger dirty tracking update + if (window.dirtyTracker) { + window.dirtyTracker.runChangeListeners(); + window.dirtyTracker.checkDirty(); + } + }); } // Handle folder override reset button click @@ -95,20 +99,24 @@ return; } - resetFolderOverrides(folderPath); + window.ConfigApp.ideBridge.confirm("Reset all overrides for this folder?", function (confirmed) { + if (!confirmed) return; - // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). - if (window.dirtyTracker) { - window.dirtyTracker.runChangeListeners(); - window.dirtyTracker.checkDirty(); - } + resetFolderOverrides(folderPath); - // Persist the reset immediately on every IDE. We call getAndSaveIdeConfig() directly rather - // than formState.triggerChangeHandlers(), which only saves on auto-save IDEs — a reset must - // be a commit point everywhere (incl. OK/Cancel IDEs), since no input event would carry it. - if (window.ConfigApp.autoSave && window.ConfigApp.autoSave.getAndSaveIdeConfig) { - window.ConfigApp.autoSave.getAndSaveIdeConfig(); - } + // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). + if (window.dirtyTracker) { + window.dirtyTracker.runChangeListeners(); + window.dirtyTracker.checkDirty(); + } + + // Persist the reset immediately on every IDE. We call getAndSaveIdeConfig() directly rather + // than formState.triggerChangeHandlers(), which only saves on auto-save IDEs — a reset must + // be a commit point everywhere (incl. OK/Cancel IDEs), since no input event would carry it. + if (window.ConfigApp.autoSave && window.ConfigApp.autoSave.getAndSaveIdeConfig) { + window.ConfigApp.autoSave.getAndSaveIdeConfig(); + } + }); } // Apply default values to form fields diff --git a/scripts/config-dialog/config_output_multi_project.html b/scripts/config-dialog/config_output_multi_project.html index 2695db8cc..04ce4c6f3 100644 --- a/scripts/config-dialog/config_output_multi_project.html +++ b/scripts/config-dialog/config_output_multi_project.html @@ -3264,6 +3264,22 @@

Filters and views

} }; + /** + * Show a confirmation dialog via the IDE native UI. + * In VSCode the sandboxed webview blocks window.confirm(), so this routes through + * the executeCommand bridge to show a native modal. Falls back to window.confirm() + * in IDEs that inject no bridge (JetBrains, Eclipse, etc.). + * @param {string} message - Message to display + * @param {function} callback - Called with true if confirmed, false if cancelled + */ + ideBridge.confirm = function (message, callback) { + if (typeof window.__ideExecuteCommand__ === "function") { + window.__ideExecuteCommand__("snyk.showConfirmationDialog", [message], callback); + } else if (typeof callback === "function") { + callback(window.confirm(message)); + } + }; + // Expose window-level functions for IDE to call /** @@ -3709,14 +3725,18 @@

Filters and views

return; } - var defaults = sectionDefaults[section]; - applyDefaults(defaults); + window.ConfigApp.ideBridge.confirm("Reset all settings in this section to defaults?", function (confirmed) { + if (!confirmed) return; - // Trigger dirty tracking update - if (window.dirtyTracker) { - window.dirtyTracker.runChangeListeners(); - window.dirtyTracker.checkDirty(); - } + var defaults = sectionDefaults[section]; + applyDefaults(defaults); + + // Trigger dirty tracking update + if (window.dirtyTracker) { + window.dirtyTracker.runChangeListeners(); + window.dirtyTracker.checkDirty(); + } + }); } // Handle folder override reset button click @@ -3739,20 +3759,24 @@

Filters and views

return; } - resetFolderOverrides(folderPath); + window.ConfigApp.ideBridge.confirm("Reset all overrides for this folder?", function (confirmed) { + if (!confirmed) return; - // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). - if (window.dirtyTracker) { - window.dirtyTracker.runChangeListeners(); - window.dirtyTracker.checkDirty(); - } + resetFolderOverrides(folderPath); - // Persist the reset immediately on every IDE. We call getAndSaveIdeConfig() directly rather - // than formState.triggerChangeHandlers(), which only saves on auto-save IDEs — a reset must - // be a commit point everywhere (incl. OK/Cancel IDEs), since no input event would carry it. - if (window.ConfigApp.autoSave && window.ConfigApp.autoSave.getAndSaveIdeConfig) { - window.ConfigApp.autoSave.getAndSaveIdeConfig(); - } + // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). + if (window.dirtyTracker) { + window.dirtyTracker.runChangeListeners(); + window.dirtyTracker.checkDirty(); + } + + // Persist the reset immediately on every IDE. We call getAndSaveIdeConfig() directly rather + // than formState.triggerChangeHandlers(), which only saves on auto-save IDEs — a reset must + // be a commit point everywhere (incl. OK/Cancel IDEs), since no input event would carry it. + if (window.ConfigApp.autoSave && window.ConfigApp.autoSave.getAndSaveIdeConfig) { + window.ConfigApp.autoSave.getAndSaveIdeConfig(); + } + }); } // Apply default values to form fields diff --git a/scripts/config-dialog/config_output_no_projects.html b/scripts/config-dialog/config_output_no_projects.html index 6cdc613ca..133eb41ff 100644 --- a/scripts/config-dialog/config_output_no_projects.html +++ b/scripts/config-dialog/config_output_no_projects.html @@ -2047,6 +2047,22 @@

Filters and views

} }; + /** + * Show a confirmation dialog via the IDE native UI. + * In VSCode the sandboxed webview blocks window.confirm(), so this routes through + * the executeCommand bridge to show a native modal. Falls back to window.confirm() + * in IDEs that inject no bridge (JetBrains, Eclipse, etc.). + * @param {string} message - Message to display + * @param {function} callback - Called with true if confirmed, false if cancelled + */ + ideBridge.confirm = function (message, callback) { + if (typeof window.__ideExecuteCommand__ === "function") { + window.__ideExecuteCommand__("snyk.showConfirmationDialog", [message], callback); + } else if (typeof callback === "function") { + callback(window.confirm(message)); + } + }; + // Expose window-level functions for IDE to call /** @@ -2492,14 +2508,18 @@

Filters and views

return; } - var defaults = sectionDefaults[section]; - applyDefaults(defaults); + window.ConfigApp.ideBridge.confirm("Reset all settings in this section to defaults?", function (confirmed) { + if (!confirmed) return; - // Trigger dirty tracking update - if (window.dirtyTracker) { - window.dirtyTracker.runChangeListeners(); - window.dirtyTracker.checkDirty(); - } + var defaults = sectionDefaults[section]; + applyDefaults(defaults); + + // Trigger dirty tracking update + if (window.dirtyTracker) { + window.dirtyTracker.runChangeListeners(); + window.dirtyTracker.checkDirty(); + } + }); } // Handle folder override reset button click @@ -2522,20 +2542,24 @@

Filters and views

return; } - resetFolderOverrides(folderPath); + window.ConfigApp.ideBridge.confirm("Reset all overrides for this folder?", function (confirmed) { + if (!confirmed) return; - // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). - if (window.dirtyTracker) { - window.dirtyTracker.runChangeListeners(); - window.dirtyTracker.checkDirty(); - } + resetFolderOverrides(folderPath); - // Persist the reset immediately on every IDE. We call getAndSaveIdeConfig() directly rather - // than formState.triggerChangeHandlers(), which only saves on auto-save IDEs — a reset must - // be a commit point everywhere (incl. OK/Cancel IDEs), since no input event would carry it. - if (window.ConfigApp.autoSave && window.ConfigApp.autoSave.getAndSaveIdeConfig) { - window.ConfigApp.autoSave.getAndSaveIdeConfig(); - } + // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). + if (window.dirtyTracker) { + window.dirtyTracker.runChangeListeners(); + window.dirtyTracker.checkDirty(); + } + + // Persist the reset immediately on every IDE. We call getAndSaveIdeConfig() directly rather + // than formState.triggerChangeHandlers(), which only saves on auto-save IDEs — a reset must + // be a commit point everywhere (incl. OK/Cancel IDEs), since no input event would carry it. + if (window.ConfigApp.autoSave && window.ConfigApp.autoSave.getAndSaveIdeConfig) { + window.ConfigApp.autoSave.getAndSaveIdeConfig(); + } + }); } // Apply default values to form fields diff --git a/scripts/config-dialog/config_output_single_solution.html b/scripts/config-dialog/config_output_single_solution.html index 3a18b8e0c..523fd205e 100644 --- a/scripts/config-dialog/config_output_single_solution.html +++ b/scripts/config-dialog/config_output_single_solution.html @@ -2347,6 +2347,22 @@

Filters and views

} }; + /** + * Show a confirmation dialog via the IDE native UI. + * In VSCode the sandboxed webview blocks window.confirm(), so this routes through + * the executeCommand bridge to show a native modal. Falls back to window.confirm() + * in IDEs that inject no bridge (JetBrains, Eclipse, etc.). + * @param {string} message - Message to display + * @param {function} callback - Called with true if confirmed, false if cancelled + */ + ideBridge.confirm = function (message, callback) { + if (typeof window.__ideExecuteCommand__ === "function") { + window.__ideExecuteCommand__("snyk.showConfirmationDialog", [message], callback); + } else if (typeof callback === "function") { + callback(window.confirm(message)); + } + }; + // Expose window-level functions for IDE to call /** @@ -2792,14 +2808,18 @@

Filters and views

return; } - var defaults = sectionDefaults[section]; - applyDefaults(defaults); + window.ConfigApp.ideBridge.confirm("Reset all settings in this section to defaults?", function (confirmed) { + if (!confirmed) return; - // Trigger dirty tracking update - if (window.dirtyTracker) { - window.dirtyTracker.runChangeListeners(); - window.dirtyTracker.checkDirty(); - } + var defaults = sectionDefaults[section]; + applyDefaults(defaults); + + // Trigger dirty tracking update + if (window.dirtyTracker) { + window.dirtyTracker.runChangeListeners(); + window.dirtyTracker.checkDirty(); + } + }); } // Handle folder override reset button click @@ -2822,20 +2842,24 @@

Filters and views

return; } - resetFolderOverrides(folderPath); + window.ConfigApp.ideBridge.confirm("Reset all overrides for this folder?", function (confirmed) { + if (!confirmed) return; - // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). - if (window.dirtyTracker) { - window.dirtyTracker.runChangeListeners(); - window.dirtyTracker.checkDirty(); - } + resetFolderOverrides(folderPath); - // Persist the reset immediately on every IDE. We call getAndSaveIdeConfig() directly rather - // than formState.triggerChangeHandlers(), which only saves on auto-save IDEs — a reset must - // be a commit point everywhere (incl. OK/Cancel IDEs), since no input event would carry it. - if (window.ConfigApp.autoSave && window.ConfigApp.autoSave.getAndSaveIdeConfig) { - window.ConfigApp.autoSave.getAndSaveIdeConfig(); - } + // Keep dirty state in sync (a reset changes no DOM input, so no change/blur event fires). + if (window.dirtyTracker) { + window.dirtyTracker.runChangeListeners(); + window.dirtyTracker.checkDirty(); + } + + // Persist the reset immediately on every IDE. We call getAndSaveIdeConfig() directly rather + // than formState.triggerChangeHandlers(), which only saves on auto-save IDEs — a reset must + // be a commit point everywhere (incl. OK/Cancel IDEs), since no input event would carry it. + if (window.ConfigApp.autoSave && window.ConfigApp.autoSave.getAndSaveIdeConfig) { + window.ConfigApp.autoSave.getAndSaveIdeConfig(); + } + }); } // Apply default values to form fields From 9e60a56ceea713761da54d1f3f970fa62368f3a2 Mon Sep 17 00:00:00 2001 From: VulnShade Date: Tue, 23 Jun 2026 10:34:37 -0400 Subject: [PATCH 10/10] test: testing enhanced gate --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4b6610100..0e31a66d6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 orbs: - prodsec: snyk/prodsec-orb@1.2.20 + prodsec: snyk/prodsec-orb@1 jobs: security-scans: @@ -14,6 +14,7 @@ jobs: mode: auto open-source-additional-arguments: --exclude=testdata iac-scan: disabled + debug: true workflows: version: 2