diff --git a/extensions/signal/src/setup-core.test.ts b/extensions/signal/src/setup-core.test.ts index 48af4625a986..9c57c28e0b4f 100644 --- a/extensions/signal/src/setup-core.test.ts +++ b/extensions/signal/src/setup-core.test.ts @@ -145,6 +145,47 @@ describe("signalSetupAdapter", () => { }, ); + it("clears the UUID when setup replaces the normalized Signal account", () => { + const next = signalSetupAdapter.applyAccountConfig?.({ + cfg: { + channels: { + signal: { + accounts: { + work: { + account: "+15555550123", + accountUuid: "123e4567-e89b-12d3-a456-426614174000", + }, + }, + }, + }, + }, + accountId: "work", + input: { signalNumber: "+15555550124" }, + }); + + expect(next?.channels?.signal?.accounts?.work?.account).toBe("+15555550124"); + expect(next?.channels?.signal?.accounts?.work?.accountUuid).toBeUndefined(); + }); + + it("preserves the UUID when setup keeps the same normalized Signal account", () => { + const accountUuid = "123e4567-e89b-12d3-a456-426614174000"; + const next = signalSetupAdapter.applyAccountConfig?.({ + cfg: { + channels: { + signal: { + account: "+15555550123", + accountUuid, + }, + }, + }, + accountId: "default", + input: { signalNumber: "signal: +1 (555) 555-0123" }, + }); + + expect(next?.channels?.signal?.account).toBe("+15555550123"); + expect(next?.channels?.signal?.accountUuid).toBe(accountUuid); + }); + it("restores a generically promoted default account before writing a named account", () => { const next = signalSetupAdapter.applyAccountConfig?.({ cfg: { diff --git a/extensions/signal/src/setup-core.ts b/extensions/signal/src/setup-core.ts index 92a0fa320b91..a072ac53a48e 100644 --- a/extensions/signal/src/setup-core.ts +++ b/extensions/signal/src/setup-core.ts @@ -10,6 +10,7 @@ import { createPatchedAccountSetupAdapter, createSetupInputPresenceValidator, DEFAULT_ACCOUNT_ID, + patchChannelConfigForAccount, promptParsedAllowFromForAccount, setAccountAllowFromForChannel, setSetupChannelEnabled, @@ -169,6 +170,10 @@ export function normalizeSignalAccountInput(value: string | null | undefined): s return `+${digits}`; } +export function isSameSignalAccount(left: string | null | undefined, right: string): boolean { + return normalizeSignalAccountInput(left) === normalizeSignalAccountInput(right); +} + function isUuidLike(value: string): boolean { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value); } @@ -504,7 +509,18 @@ export const signalSetupAdapter: ChannelSetupAdapter = { // transport. Rejoin that pair here so Signal keeps one canonical default-account shape. const cfg = restorePromotedSignalDefaultAccount(params.cfg); const previousTransport = resolveConfiguredSignalTransport(cfg, accountId); - const next = signalSetupAdapterBase.applyAccountConfig?.({ ...params, cfg, accountId }) ?? cfg; + let next = signalSetupAdapterBase.applyAccountConfig?.({ ...params, cfg, accountId }) ?? cfg; + if ( + suppliedAccount && + !isSameSignalAccount(resolveSignalAccount({ cfg, accountId }).config.account, suppliedAccount) + ) { + next = patchChannelConfigForAccount({ + cfg: next, + channel, + accountId, + patch: { accountUuid: undefined }, + }); + } const configuredTransport = resolveConfiguredSignalTransport(next, accountId); if (configuredTransport && configuredTransport.kind !== "managed-native") { const transport = diff --git a/extensions/signal/src/setup-interactive.test.ts b/extensions/signal/src/setup-interactive.test.ts index 0250fc3c52c5..b2f848d8d703 100644 --- a/extensions/signal/src/setup-interactive.test.ts +++ b/extensions/signal/src/setup-interactive.test.ts @@ -308,6 +308,39 @@ describe("Signal existing-server setup", () => { expect(finalized?.cfg?.channels?.signal?.account).toBe("+15555550123"); }); + it("preserves the UUID when recovery keeps the same normalized account", async () => { + mocks.probeSignalTransport.mockResolvedValueOnce({ + ok: false, + error: "selected account is unavailable", + }); + const accountUuid = "123e4567-e89b-12d3-a456-426614174000"; + const queued = createQueuedWizardPrompter({ + selectValues: ["account"], + textValues: ["signal: +1 (555) 555-0123"], + }); + + const finalized = await runSetupWizardFinalize({ + finalize: signalSetupWizard.finalize, + cfg: { + channels: { + signal: { + account: "+15555550123", + accountUuid, + }, + }, + }, + credentialValues: { + signalTransportKind: "external-native", + signalServerUrl: "http://signal-helper:8080", + }, + prompter: queued.prompter, + runtime: createRuntimeEnv({ throwOnExit: false }), + }); + + expect(finalized?.cfg?.channels?.signal?.account).toBe("+15555550123"); + expect(finalized?.cfg?.channels?.signal?.accountUuid).toBe(accountUuid); + }); + it("rejects an existing-server recovery account owned by a sibling", async () => { mocks.probeSignalTransport.mockResolvedValueOnce({ ok: false, diff --git a/extensions/signal/src/setup-interactive.ts b/extensions/signal/src/setup-interactive.ts index 7157dcc00343..d1b4e9e6b6f8 100644 --- a/extensions/signal/src/setup-interactive.ts +++ b/extensions/signal/src/setup-interactive.ts @@ -15,6 +15,7 @@ import { } from "./accounts.js"; import { assertSignalAccountNotAssignedToSibling, + isSameSignalAccount, normalizeSignalAccountInput, signalSetupStateKeys, } from "./setup-core.js"; @@ -67,7 +68,15 @@ export async function finalizeSignalExistingServerSetup(params: SignalFinalizePa cfg, channel: "signal", accountId: params.accountId, - patch: { account, accountUuid: undefined }, + patch: { + account, + ...(isSameSignalAccount( + resolveSignalAccount({ cfg, accountId: params.accountId }).config.account, + account, + ) + ? {} + : { accountUuid: undefined }), + }, }); shouldPromptAccount = false; }