diff --git a/background.js b/background.js index d826c65..0343a99 100644 --- a/background.js +++ b/background.js @@ -3574,6 +3574,97 @@ function projectSettingsSchemaView(settingsSchemaApi, normalizedInput = {}, payl return settingsSchemaApi.buildSettingsView(normalizedSettingsState, payload); } +function setSettingsStatePatchValue(patch, path, value) { + let cursor = patch; + for (let index = 0; index < path.length - 1; index += 1) { + const key = path[index]; + if (!isPlainObjectValue(cursor[key])) { + cursor[key] = {}; + } + cursor = cursor[key]; + } + cursor[path[path.length - 1]] = value; +} + +function mergeSettingsStatePatch(baseValue = {}, patchValue = {}) { + if (!isPlainObjectValue(patchValue)) { + return isPlainObjectValue(baseValue) ? { ...baseValue } : {}; + } + const next = { + ...(isPlainObjectValue(baseValue) ? baseValue : {}), + }; + Object.entries(patchValue).forEach(([key, value]) => { + next[key] = isPlainObjectValue(value) + ? mergeSettingsStatePatch(next[key], value) + : value; + }); + return next; +} + +function buildSettingsStatePatchFromFlatUpdates(updates = {}) { + const patch = {}; + const hasUpdate = (key) => Object.prototype.hasOwnProperty.call(updates, key); + const assignIfUpdated = (key, path) => { + if (hasUpdate(key)) { + setSettingsStatePatchValue(patch, path, updates[key]); + } + }; + + assignIfUpdated('activeFlowId', ['activeFlowId']); + if (hasUpdate('openaiIntegrationTargetId') || hasUpdate('panelMode')) { + setSettingsStatePatchValue( + patch, + ['flows', 'openai', 'integrationTargetId'], + hasUpdate('openaiIntegrationTargetId') ? updates.openaiIntegrationTargetId : updates.panelMode + ); + } + assignIfUpdated('kiroTargetId', ['flows', 'kiro', 'targetId']); + assignIfUpdated('vpsUrl', ['flows', 'openai', 'integrationTargets', 'cpa', 'vpsUrl']); + assignIfUpdated('vpsPassword', ['flows', 'openai', 'integrationTargets', 'cpa', 'vpsPassword']); + assignIfUpdated('localCpaStep9Mode', ['flows', 'openai', 'integrationTargets', 'cpa', 'localCpaStep9Mode']); + assignIfUpdated('sub2apiUrl', ['flows', 'openai', 'integrationTargets', 'sub2api', 'sub2apiUrl']); + assignIfUpdated('sub2apiEmail', ['flows', 'openai', 'integrationTargets', 'sub2api', 'sub2apiEmail']); + assignIfUpdated('sub2apiPassword', ['flows', 'openai', 'integrationTargets', 'sub2api', 'sub2apiPassword']); + assignIfUpdated('sub2apiGroupName', ['flows', 'openai', 'integrationTargets', 'sub2api', 'sub2apiGroupName']); + assignIfUpdated('sub2apiGroupNames', ['flows', 'openai', 'integrationTargets', 'sub2api', 'sub2apiGroupNames']); + assignIfUpdated('sub2apiAccountPriority', ['flows', 'openai', 'integrationTargets', 'sub2api', 'sub2apiAccountPriority']); + assignIfUpdated('sub2apiDefaultProxyName', ['flows', 'openai', 'integrationTargets', 'sub2api', 'sub2apiDefaultProxyName']); + assignIfUpdated('codex2apiUrl', ['flows', 'openai', 'integrationTargets', 'codex2api', 'codex2apiUrl']); + assignIfUpdated('codex2apiAdminKey', ['flows', 'openai', 'integrationTargets', 'codex2api', 'codex2apiAdminKey']); + assignIfUpdated('customPassword', ['services', 'account', 'customPassword']); + assignIfUpdated('signupMethod', ['flows', 'openai', 'signup', 'signupMethod']); + assignIfUpdated('phoneVerificationEnabled', ['flows', 'openai', 'signup', 'phoneVerificationEnabled']); + assignIfUpdated('phoneSignupReloginAfterBindEmailEnabled', ['flows', 'openai', 'signup', 'phoneSignupReloginAfterBindEmailEnabled']); + assignIfUpdated('plusModeEnabled', ['flows', 'openai', 'plus', 'plusModeEnabled']); + assignIfUpdated('plusPaymentMethod', ['flows', 'openai', 'plus', 'plusPaymentMethod']); + assignIfUpdated('plusAccountAccessStrategy', ['flows', 'openai', 'plus', 'plusAccountAccessStrategy']); + assignIfUpdated('mailProvider', ['services', 'email', 'provider']); + assignIfUpdated('ipProxyEnabled', ['services', 'proxy', 'enabled']); + assignIfUpdated('ipProxyService', ['services', 'proxy', 'provider']); + assignIfUpdated('ipProxyMode', ['services', 'proxy', 'mode']); + assignIfUpdated('kiroRsUrl', ['flows', 'kiro', 'targets', 'kiro-rs', 'baseUrl']); + assignIfUpdated('kiroRsKey', ['flows', 'kiro', 'targets', 'kiro-rs', 'apiKey']); + + if (hasUpdate('stepExecutionRangeByFlow') && isPlainObjectValue(updates.stepExecutionRangeByFlow)) { + if (isPlainObjectValue(updates.stepExecutionRangeByFlow.openai)) { + setSettingsStatePatchValue( + patch, + ['flows', 'openai', 'autoRun', 'stepExecutionRange'], + updates.stepExecutionRangeByFlow.openai + ); + } + if (isPlainObjectValue(updates.stepExecutionRangeByFlow.kiro)) { + setSettingsStatePatchValue( + patch, + ['flows', 'kiro', 'autoRun', 'stepExecutionRange'], + updates.stepExecutionRangeByFlow.kiro + ); + } + } + + return patch; +} + function buildPersistedSettingsStoragePayload(payload = {}) { const storagePayload = {}; Object.entries(payload || {}).forEach(([key, value]) => { @@ -3924,6 +4015,10 @@ async function setPersistentSettings(updates) { ? settingsSchemaApi.mergeSettingsState(currentSettingsState, nextUpdates.settingsState) : nextUpdates.settingsState) : currentSettingsState; + mergedSettingsState = mergeSettingsStatePatch( + mergedSettingsState, + buildSettingsStatePatchFromFlatUpdates(explicitFlatUpdates) + ); } const nextPayloadInput = { diff --git a/tests/background-settings-schema-persistence.test.js b/tests/background-settings-schema-persistence.test.js index d7bd25c..ff5df40 100644 --- a/tests/background-settings-schema-persistence.test.js +++ b/tests/background-settings-schema-persistence.test.js @@ -174,6 +174,9 @@ function resolveLegacyAutoStepDelaySeconds() { return undefined; } ${extractFunction('normalizePersistentSettingValue')} ${extractFunction('getSettingsSchemaApi')} ${extractFunction('projectSettingsSchemaView')} +${extractFunction('setSettingsStatePatchValue')} +${extractFunction('mergeSettingsStatePatch')} +${extractFunction('buildSettingsStatePatchFromFlatUpdates')} ${extractFunction('buildPersistedSettingsStoragePayload')} ${extractFunction('buildPersistentSettingsPayload')} ${extractFunction('getPersistedSettings')} @@ -513,3 +516,176 @@ function getRemovedKeys() { assert.ok(api.getRemovedKeys().includes('panelMode')); assert.ok(api.getRemovedKeys().includes('kiroRsUrl')); }); + +test('setPersistentSettings mirrors flat mail provider updates into canonical settingsState', async () => { + const api = buildHarness(` +const persistedWrites = []; +const removedKeys = []; +const chrome = { + storage: { + local: { + async get() { + return { + settingsSchemaVersion: 4, + settingsState: { + activeFlowId: 'openai', + services: { + account: { customPassword: '' }, + email: { provider: '163' }, + proxy: { enabled: false, provider: '711proxy', mode: 'account' }, + }, + flows: {}, + }, + }; + }, + async remove(keys) { + removedKeys.push(...(Array.isArray(keys) ? keys : [keys])); + }, + async set(payload) { + persistedWrites.push(JSON.parse(JSON.stringify(payload))); + }, + }, + }, +}; +function getPersistedWrites() { + return persistedWrites; +} +function getRemovedKeys() { + return removedKeys; +} +`); + + const persisted = await api.setPersistentSettings({ + mailProvider: 'cloudflare-temp-email', + }); + const write = api.getPersistedWrites().at(-1); + + assert.equal(persisted.mailProvider, 'cloudflare-temp-email'); + assert.equal(persisted.settingsState.services.email.provider, 'cloudflare-temp-email'); + assert.equal(write.settingsState.services.email.provider, 'cloudflare-temp-email'); + assert.equal(Object.prototype.hasOwnProperty.call(write, 'mailProvider'), false); +}); + +test('setPersistentSettings mirrors flat schema updates without resetting other canonical settings', async () => { + const api = buildHarness(` +const persistedWrites = []; +const removedKeys = []; +const chrome = { + storage: { + local: { + async get() { + return { + settingsSchemaVersion: 4, + settingsState: { + activeFlowId: 'openai', + services: { + account: { customPassword: 'old-password' }, + email: { provider: '163' }, + proxy: { enabled: false, provider: '711proxy', mode: 'account' }, + }, + flows: { + openai: { + integrationTargetId: 'cpa', + integrationTargets: { + cpa: { + vpsUrl: 'https://old-cpa.example.com', + vpsPassword: 'old-vps-password', + localCpaStep9Mode: 'submit', + }, + sub2api: { + sub2apiUrl: 'https://sub2api.example.com', + sub2apiEmail: 'owner@example.com', + sub2apiPassword: 'sub2api-secret', + sub2apiGroupName: 'kept-group', + sub2apiGroupNames: ['kept-group'], + sub2apiAccountPriority: 7, + sub2apiDefaultProxyName: 'proxy-a', + }, + codex2api: { + codex2apiUrl: 'https://codex2api.example.com', + codex2apiAdminKey: 'codex-key', + }, + }, + signup: { + signupMethod: 'email', + phoneVerificationEnabled: false, + phoneSignupReloginAfterBindEmailEnabled: false, + }, + plus: { + plusModeEnabled: false, + plusPaymentMethod: 'paypal', + plusAccountAccessStrategy: 'oauth', + }, + autoRun: { + stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 }, + }, + }, + kiro: { + targetId: 'kiro-rs', + targets: { + 'kiro-rs': { + baseUrl: 'https://kiro.example.com/admin', + apiKey: 'kiro-key', + }, + }, + autoRun: { + stepExecutionRange: { enabled: false, fromStep: 1, toStep: 9 }, + }, + }, + }, + }, + }; + }, + async remove(keys) { + removedKeys.push(...(Array.isArray(keys) ? keys : [keys])); + }, + async set(payload) { + persistedWrites.push(JSON.parse(JSON.stringify(payload))); + }, + }, + }, +}; +function getPersistedWrites() { + return persistedWrites; +} +function getRemovedKeys() { + return removedKeys; +} +`); + + const persisted = await api.setPersistentSettings({ + panelMode: 'sub2api', + mailProvider: 'cloudflare-temp-email', + ipProxyEnabled: true, + ipProxyMode: 'api', + stepExecutionRangeByFlow: { + openai: { enabled: true, fromStep: 2, toStep: 4 }, + }, + }); + const write = api.getPersistedWrites().at(-1); + + assert.equal(persisted.panelMode, 'sub2api'); + assert.equal(persisted.mailProvider, 'cloudflare-temp-email'); + assert.equal(persisted.ipProxyEnabled, true); + assert.equal(persisted.ipProxyMode, 'api'); + assert.deepEqual(persisted.stepExecutionRangeByFlow.openai, { + enabled: true, + fromStep: 2, + toStep: 4, + }); + assert.equal(write.settingsState.flows.openai.integrationTargetId, 'sub2api'); + assert.equal(write.settingsState.services.email.provider, 'cloudflare-temp-email'); + assert.equal(write.settingsState.services.proxy.enabled, true); + assert.equal(write.settingsState.services.proxy.mode, 'api'); + assert.deepEqual(write.settingsState.flows.openai.autoRun.stepExecutionRange, { + enabled: true, + fromStep: 2, + toStep: 4, + }); + assert.equal(write.settingsState.flows.openai.integrationTargets.sub2api.sub2apiUrl, 'https://sub2api.example.com'); + assert.equal(write.settingsState.flows.openai.integrationTargets.sub2api.sub2apiEmail, 'owner@example.com'); + assert.equal(write.settingsState.flows.kiro.targets['kiro-rs'].apiKey, 'kiro-key'); + assert.equal(Object.prototype.hasOwnProperty.call(write, 'mailProvider'), false); + assert.equal(Object.prototype.hasOwnProperty.call(write, 'panelMode'), false); + assert.equal(Object.prototype.hasOwnProperty.call(write, 'ipProxyMode'), false); +});