|
| 1 | +const test = require('node:test'); |
| 2 | +const assert = require('node:assert/strict'); |
| 3 | + |
| 4 | +const { resolveEffectiveSettings } = require('../resolve-effective-settings'); |
| 5 | +const claude = require('../harnesses/claude'); |
| 6 | +const codex = require('../harnesses/codex'); |
| 7 | + |
| 8 | +// Mirrors the shape of main.js's SETTING_DEFAULTS for the keys that matter here. |
| 9 | +const DEFAULTS = { |
| 10 | + permissionMode: null, |
| 11 | + dangerouslySkipPermissions: false, |
| 12 | + worktree: false, |
| 13 | + visibleSessionCount: 5, |
| 14 | + shellProfile: 'auto', |
| 15 | +}; |
| 16 | + |
| 17 | +test('returns the defaults when nothing has been saved', () => { |
| 18 | + assert.deepEqual(resolveEffectiveSettings(DEFAULTS, {}, {}), DEFAULTS); |
| 19 | +}); |
| 20 | + |
| 21 | +test('project scope overrides global, which overrides defaults', () => { |
| 22 | + const effective = resolveEffectiveSettings( |
| 23 | + DEFAULTS, |
| 24 | + { permissionMode: 'plan', visibleSessionCount: 10 }, |
| 25 | + { permissionMode: 'acceptEdits' }, |
| 26 | + ); |
| 27 | + assert.equal(effective.permissionMode, 'acceptEdits', 'project wins over global'); |
| 28 | + assert.equal(effective.visibleSessionCount, 10, 'global still applies where project is silent'); |
| 29 | + assert.equal(effective.shellProfile, 'auto', 'untouched keys keep their default'); |
| 30 | +}); |
| 31 | + |
| 32 | +test('a project can narrow permissionMode back to Default over a global mode', () => { |
| 33 | + // The settings panel saves the "Default (none)" option as `value || null`, so |
| 34 | + // an explicit null means "pass no --permission-mode flag". Claude's own |
| 35 | + // configuration still applies. It must beat a broader-scope mode. |
| 36 | + const effective = resolveEffectiveSettings( |
| 37 | + DEFAULTS, |
| 38 | + { permissionMode: 'bypassPermissions' }, |
| 39 | + { permissionMode: null }, |
| 40 | + ); |
| 41 | + assert.equal(effective.permissionMode, null, |
| 42 | + 'an explicitly saved null must not fall back to the global mode'); |
| 43 | +}); |
| 44 | + |
| 45 | +test('an explicit global null beats a non-null default', () => { |
| 46 | + // With a null default this is invisible, so pin it against a default that is |
| 47 | + // not null — otherwise any future non-null SETTING_DEFAULTS value silently |
| 48 | + // becomes unreachable. |
| 49 | + const effective = resolveEffectiveSettings( |
| 50 | + { permissionMode: 'acceptEdits' }, |
| 51 | + { permissionMode: null }, |
| 52 | + {}, |
| 53 | + ); |
| 54 | + assert.equal(effective.permissionMode, null, |
| 55 | + 'an explicitly saved null must override a non-null default'); |
| 56 | +}); |
| 57 | + |
| 58 | +test('undefined means "never saved" and falls through', () => { |
| 59 | + const effective = resolveEffectiveSettings( |
| 60 | + DEFAULTS, |
| 61 | + { permissionMode: 'plan' }, |
| 62 | + { permissionMode: undefined }, |
| 63 | + ); |
| 64 | + assert.equal(effective.permissionMode, 'plan', |
| 65 | + 'an absent project key must not shadow the global value'); |
| 66 | +}); |
| 67 | + |
| 68 | +test('null and undefined are not conflated', () => { |
| 69 | + const withNull = resolveEffectiveSettings(DEFAULTS, { permissionMode: 'plan' }, { permissionMode: null }); |
| 70 | + const withUndefined = resolveEffectiveSettings(DEFAULTS, { permissionMode: 'plan' }, {}); |
| 71 | + assert.notEqual(withNull.permissionMode, withUndefined.permissionMode, |
| 72 | + 'an explicit null and an absent key must resolve differently'); |
| 73 | +}); |
| 74 | + |
| 75 | +test('other falsy values are preserved', () => { |
| 76 | + const effective = resolveEffectiveSettings( |
| 77 | + { worktree: true, visibleSessionCount: 5, shellProfile: 'auto' }, |
| 78 | + {}, |
| 79 | + { worktree: false, visibleSessionCount: 0, shellProfile: '' }, |
| 80 | + ); |
| 81 | + assert.equal(effective.worktree, false, 'false must override a true default'); |
| 82 | + assert.equal(effective.visibleSessionCount, 0, '0 must override a non-zero default'); |
| 83 | + assert.equal(effective.shellProfile, '', 'an empty string must override a non-empty default'); |
| 84 | +}); |
| 85 | + |
| 86 | +test('keys absent from the defaults are ignored', () => { |
| 87 | + const effective = resolveEffectiveSettings(DEFAULTS, { notADefault: 'x' }, { alsoNot: 'y' }); |
| 88 | + assert.equal('notADefault' in effective, false); |
| 89 | + assert.equal('alsoNot' in effective, false); |
| 90 | +}); |
| 91 | + |
| 92 | +test('the inputs are not mutated', () => { |
| 93 | + const defaults = { permissionMode: null }; |
| 94 | + const global = { permissionMode: 'plan' }; |
| 95 | + const project = { permissionMode: null }; |
| 96 | + resolveEffectiveSettings(defaults, global, project); |
| 97 | + assert.deepEqual(defaults, { permissionMode: null }); |
| 98 | + assert.deepEqual(global, { permissionMode: 'plan' }); |
| 99 | + assert.deepEqual(project, { permissionMode: null }); |
| 100 | +}); |
| 101 | + |
| 102 | +test('global and project default to empty when omitted', () => { |
| 103 | + assert.deepEqual(resolveEffectiveSettings(DEFAULTS), DEFAULTS); |
| 104 | +}); |
| 105 | + |
| 106 | +test('project Default removes the inherited Claude permission flag on launch and resume', () => { |
| 107 | + const global = { permissionMode: 'bypassPermissions' }; |
| 108 | + for (const isNew of [true, false]) { |
| 109 | + const inherited = claude.buildLaunchArgs({ |
| 110 | + sessionId: 'session', isNew, |
| 111 | + options: resolveEffectiveSettings(DEFAULTS, global), |
| 112 | + }); |
| 113 | + assert.ok(inherited.includes('--permission-mode')); |
| 114 | + assert.ok(inherited.includes('bypassPermissions')); |
| 115 | + |
| 116 | + const overridden = claude.buildLaunchArgs({ |
| 117 | + sessionId: 'session', isNew, |
| 118 | + options: resolveEffectiveSettings(DEFAULTS, global, { permissionMode: null }), |
| 119 | + }); |
| 120 | + assert.ok(!overridden.includes('--permission-mode')); |
| 121 | + assert.ok(!overridden.includes('bypassPermissions')); |
| 122 | + assert.ok(!overridden.includes('--dangerously-skip-permissions')); |
| 123 | + } |
| 124 | +}); |
| 125 | + |
| 126 | +test('Claude Default preserves independently configured Codex and task shell settings', () => { |
| 127 | + const options = resolveEffectiveSettings( |
| 128 | + { ...DEFAULTS, codexSandbox: '', codexApproval: '', codexModel: '' }, |
| 129 | + { |
| 130 | + permissionMode: 'bypassPermissions', |
| 131 | + codexSandbox: 'workspace-write', |
| 132 | + codexApproval: 'never', |
| 133 | + shellProfile: 'custom-shell', |
| 134 | + }, |
| 135 | + { permissionMode: null, codexSandbox: 'read-only', codexApproval: 'on-request' }, |
| 136 | + ); |
| 137 | + const args = codex.buildLaunchArgs({ sessionId: 'session', isNew: true, options }); |
| 138 | + assert.equal(options.permissionMode, null); |
| 139 | + assert.equal(options.shellProfile, 'custom-shell'); |
| 140 | + assert.ok(args.includes('read-only')); |
| 141 | + assert.ok(args.includes('on-request')); |
| 142 | + assert.ok(!args.includes('workspace-write')); |
| 143 | + assert.ok(!args.includes('never')); |
| 144 | + assert.ok(!args.includes('--permission-mode')); |
| 145 | +}); |
0 commit comments