diff --git a/docs/issues/feishu-pairing-save-race/plan.md b/docs/issues/feishu-pairing-save-race/plan.md new file mode 100644 index 000000000..6fb7593a6 --- /dev/null +++ b/docs/issues/feishu-pairing-save-race/plan.md @@ -0,0 +1,23 @@ +# Plan + +## Implementation Approach +- Inspect `RemoteControlPresenter.saveFeishuSettings` and binding store ownership of Feishu paired users. +- Update Feishu settings save to persist editable settings while preserving `config.pairedUserOpenIds`. +- Keep runtime rebuild behavior unchanged after saving credentials or enabled state. + +## Affected Interfaces +- `saveFeishuSettings(input: FeishuRemoteSettings)` behavior changes to ignore `input.pairedUserOpenIds` for persistence. +- Returned settings remain `FeishuRemoteSettings` and include the current paired users from storage. + +## Data Flow +- Frontend sends normalized Feishu settings, possibly from stale form state. +- Main process updates editable Feishu config fields. +- Existing `pairedUserOpenIds` is copied from the current config, so pairing state created by command handling is preserved. + +## Compatibility +- No IPC or renderer contract changes. +- Old frontend payloads with `pairedUserOpenIds` continue to be accepted but cannot mutate authorization state. + +## Test Strategy +- Add a main presenter unit test that seeds a paired Feishu user, saves settings with `pairedUserOpenIds: []`, and expects the paired user to remain. +- Run focused remote control presenter tests, then required format/i18n/lint commands if feasible. diff --git a/docs/issues/feishu-pairing-save-race/spec.md b/docs/issues/feishu-pairing-save-race/spec.md new file mode 100644 index 000000000..cb944c9c9 --- /dev/null +++ b/docs/issues/feishu-pairing-save-race/spec.md @@ -0,0 +1,21 @@ +# Feishu Pairing Save Race + +## User Need +Feishu/Lark Remote Control users who complete `/pair ` must stay authorized after the settings page auto-saves stale form state. + +## Goal +Prevent general Feishu settings saves from overwriting the runtime-managed `pairedUserOpenIds` list. + +## Acceptance Criteria +- Saving Feishu settings preserves the existing paired user open IDs when the input contains an older or empty list. +- Pair/unpair operations remain the only path that changes Feishu paired users. +- Existing Feishu settings fields such as brand, credentials, enabled state, default agent, and workdir still save normally. +- Regression coverage verifies stale frontend settings cannot erase a paired Feishu user. + +## Constraints +- Keep public settings shape compatible with the current renderer and IPC contracts. +- Avoid broad changes to unrelated remote channels unless a matching save-race path is confirmed in code. + +## Non-Goals +- Redesign remote settings state management. +- Remove `pairedUserOpenIds` from shared types in this fix. diff --git a/docs/issues/feishu-pairing-save-race/tasks.md b/docs/issues/feishu-pairing-save-race/tasks.md new file mode 100644 index 000000000..256ec5755 --- /dev/null +++ b/docs/issues/feishu-pairing-save-race/tasks.md @@ -0,0 +1,6 @@ +# Tasks + +- [x] Capture issue requirements and SDD artifacts. +- [x] Change Feishu settings save to preserve paired users. +- [x] Add regression test for stale Feishu settings save. +- [x] Run formatting, i18n generation, lint, and focused tests. diff --git a/src/main/presenter/remoteControlPresenter/index.ts b/src/main/presenter/remoteControlPresenter/index.ts index 34ef25602..3180a0e11 100644 --- a/src/main/presenter/remoteControlPresenter/index.ts +++ b/src/main/presenter/remoteControlPresenter/index.ts @@ -532,7 +532,7 @@ export class RemoteControlPresenter { enabled: normalized.remoteEnabled, defaultAgentId, defaultWorkdir: normalized.defaultWorkdir, - pairedUserOpenIds: normalized.pairedUserOpenIds, + pairedUserOpenIds: config.pairedUserOpenIds, lastFatalError: shouldClearFatalError ? null : config.lastFatalError, pairing: config.pairing })) diff --git a/test/main/presenter/remoteControlPresenter/remoteControlPresenter.test.ts b/test/main/presenter/remoteControlPresenter/remoteControlPresenter.test.ts index 30d2f7a0e..4dcd2cfe8 100644 --- a/test/main/presenter/remoteControlPresenter/remoteControlPresenter.test.ts +++ b/test/main/presenter/remoteControlPresenter/remoteControlPresenter.test.ts @@ -491,6 +491,61 @@ describe('RemoteControlPresenter', () => { ) }) + it('preserves paired Feishu users when saving stale settings input', async () => { + const configPresenter = createConfigPresenter() + configPresenter.setSetting('remoteControl', { + feishu: { + brand: 'feishu', + appId: 'cli_old', + appSecret: 'secret', + verificationToken: 'verify', + encryptKey: '', + enabled: true, + defaultAgentId: 'deepchat', + defaultWorkdir: '', + pairedUserOpenIds: ['ou_paired'], + lastFatalError: null, + pairing: { + code: null, + expiresAt: null, + failedAttempts: 0 + }, + bindings: {} + } + }) + + const presenter = new RemoteControlPresenter({ + configPresenter: configPresenter as any, + agentSessionPresenter: {} as any, + agentRuntimePresenter: {} as any, + windowPresenter: {} as any, + tabPresenter: {} as any + }) + + const saved = await presenter.saveFeishuSettings({ + brand: 'lark', + appId: 'cli_new', + appSecret: 'secret', + verificationToken: 'verify', + encryptKey: '', + remoteEnabled: true, + defaultAgentId: 'deepchat', + defaultWorkdir: '', + pairedUserOpenIds: [] + }) + + expect(saved.pairedUserOpenIds).toEqual(['ou_paired']) + expect(configPresenter.setSetting).toHaveBeenCalledWith( + 'remoteControl', + expect.objectContaining({ + feishu: expect.objectContaining({ + appId: 'cli_new', + pairedUserOpenIds: ['ou_paired'] + }) + }) + ) + }) + it('persists the lark brand inside feishu remote settings', async () => { const configPresenter = createConfigPresenter()