From 125956300123e7f7b30717fd7e2da7dfae6dc762 Mon Sep 17 00:00:00 2001 From: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com> Date: Wed, 19 Aug 2026 00:35:50 +0300 Subject: [PATCH] Fix account removal confirmation handling --- web/modules/harness_accounts.js | 24 +++++++++++++------ web/tests/agents_tab.test.js | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/web/modules/harness_accounts.js b/web/modules/harness_accounts.js index 2e8f1ceb..bd82d1b8 100644 --- a/web/modules/harness_accounts.js +++ b/web/modules/harness_accounts.js @@ -1026,25 +1026,35 @@ async function toggleAccountEnabled(harness, profileId, enabled) { renderRows(); } -async function confirmRemoveAccount(harness, profileId) { +/** + * Complete destructive flow behind a row's Remove button. Injectable deps let + * the node suite drive the production handler; confirm mode authorizes only on + * its documented strict boolean `true`, never the input mode's object shape. + */ +export async function confirmRemoveAccount(harness, profileId, { + dialogImpl = openConfirmDialog, + removeImpl = removeAccount, + store = state.store, + renderImpl = renderRows, +} = {}) { if (!harness || !profileId) return; - const family = familyLabel(harness, state.store.snapshot || {}); - const answer = await openConfirmDialog({ + const family = familyLabel(harness, store.snapshot || {}); + const answer = await dialogImpl({ title: 'Remove account', body: removeAccountConfirmBody(profileId, family), confirmLabel: 'Remove', danger: true, }); - if (!answer?.confirmed) return; + if (answer !== true) return; state.removeError = ''; try { - await removeAccount(harness, profileId); + await removeImpl(harness, profileId); } catch (error) { state.removeError = `Could not remove "${profileId}": ${error.message || error}. ` + 'The account is unchanged.'; } - await state.store.refresh(); - renderRows(); + await store.refresh(); + renderImpl(); } /** diff --git a/web/tests/agents_tab.test.js b/web/tests/agents_tab.test.js index 250e9ea3..8af4c4bd 100644 --- a/web/tests/agents_tab.test.js +++ b/web/tests/agents_tab.test.js @@ -22,6 +22,7 @@ import { accountGroups, accountMetaLine, accountName, + confirmRemoveAccount, familyActionLabel, familyLabel, familyStatus, @@ -627,6 +628,46 @@ test('a partial gap obeys the SAME fault precedence as a total one', () => { // Removal. // --------------------------------------------------------------------------- +test('Remove consumes the non-input confirm boolean and performs the mutation once', async () => { + const calls = []; + const store = { + snapshot: { harnesses: [{ id: 'claude', display_name: 'Claude Code' }] }, + refresh: async () => calls.push(['refresh']), + }; + + await confirmRemoveAccount('claude', 'work', { + dialogImpl: async (options) => { + assert.notEqual(options.input, true); + assert.equal(options.danger, true); + assert.equal(options.confirmLabel, 'Remove'); + calls.push(['dialog']); + return true; + }, + removeImpl: async (harness, profileId) => calls.push(['remove', harness, profileId]), + store, + renderImpl: () => calls.push(['render']), + }); + + assert.deepEqual(calls, [ + ['dialog'], + ['remove', 'claude', 'work'], + ['refresh'], + ['render'], + ]); + + for (const resolution of [false, undefined, null, { confirmed: true }]) { + const skipped = []; + await confirmRemoveAccount('claude', 'work', { + dialogImpl: async () => { skipped.push(['dialog']); return resolution; }, + removeImpl: async () => skipped.push(['remove']), + store: { snapshot: store.snapshot, refresh: async () => skipped.push(['refresh']) }, + renderImpl: () => skipped.push(['render']), + }); + assert.deepEqual(skipped, [['dialog']], + `resolution ${JSON.stringify(resolution)} must stop after the dialog`); + } +}); + test('removing a named account goes through the engine contract, and says so', () => { const calls = []; const fetchImpl = async (url, init) => {