From 7ccc9cb62d8a2954651b2a5ee66577c81787ff2b Mon Sep 17 00:00:00 2001 From: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:57:28 +1000 Subject: [PATCH] fix(signal): reuse effective managed transport --- .../signal/src/setup-interactive.test.ts | 48 +++++++++++++++++++ .../src/setup-managed-validation.test.ts | 47 +++++++++++++++++- .../signal/src/setup-managed-validation.ts | 11 ++--- 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/extensions/signal/src/setup-interactive.test.ts b/extensions/signal/src/setup-interactive.test.ts index c4632c7aeb58..0250fc3c52c5 100644 --- a/extensions/signal/src/setup-interactive.test.ts +++ b/extensions/signal/src/setup-interactive.test.ts @@ -378,6 +378,54 @@ describe("Signal existing-server setup", () => { expect(queued.note).toHaveBeenCalledWith(expect.stringContaining("not ready"), "Signal setup"); }); + it("does not persist an unverified alias of a configured managed daemon", async () => { + mocks.probeSignalTransport.mockResolvedValue({ + ok: false, + status: 200, + failureKind: "unverifiable-single-account", + error: "server account cannot be verified", + }); + const cfg: OpenClawConfig = { + channels: { + signal: { + account: "+15555550123", + transport: { kind: "managed-native", httpHost: "127.0.0.1", httpPort: 8080 }, + }, + }, + }; + const queued = createQueuedWizardPrompter({ + selectValues: ["existing-server", "stop"], + textValues: ["http://custom-hosts-alias:8080"], + }); + const prepared = await runSetupWizardPrepare({ + prepare: signalSetupWizard.prepare, + cfg, + prompter: queued.prompter, + runtime: createRuntimeEnv({ throwOnExit: false }), + }); + + await expect( + runSetupWizardFinalize({ + finalize: signalSetupWizard.finalize, + cfg, + credentialValues: toCredentialValues(prepared?.credentialValues), + prompter: queued.prompter, + runtime: createRuntimeEnv({ throwOnExit: false }), + }), + ).rejects.toBeInstanceOf(WizardCancelledError); + + expect(queued.confirm).not.toHaveBeenCalled(); + expect(cfg.channels?.signal?.transport).toEqual({ + kind: "managed-native", + httpHost: "127.0.0.1", + httpPort: 8080, + }); + expect(queued.note).toHaveBeenCalledWith( + expect.stringContaining("server account cannot be verified"), + "Signal setup", + ); + }); + it("rejects an alias of a managed daemon and accepts an independent server", async () => { mocks.networkInterfaces.mockImplementationOnce(() => { throw new Error("interface enumeration denied"); diff --git a/extensions/signal/src/setup-managed-validation.test.ts b/extensions/signal/src/setup-managed-validation.test.ts index 32a8fffef871..35d3e7a9a403 100644 --- a/extensions/signal/src/setup-managed-validation.test.ts +++ b/extensions/signal/src/setup-managed-validation.test.ts @@ -10,7 +10,10 @@ import type { SignalDaemonHandle } from "./daemon.js"; import { registerSignalManagedDaemonOwner } from "./managed-daemon-runtime-context.js"; import { setSignalRuntime } from "./runtime.js"; import { clearSignalRuntimeForTest } from "./runtime.test-support.js"; -import type { SignalTransportProbeResult } from "./setup-transport.js"; +import type { + SignalManagedNativeTransport, + SignalTransportProbeResult, +} from "./setup-transport.js"; const mocks = vi.hoisted(() => ({ assertBindAvailable: vi.fn(async () => undefined), @@ -65,7 +68,7 @@ const transport = { function createParams( cfg: OpenClawConfig = {}, - candidate: typeof transport | (typeof transport & { url: string }) = transport, + candidate: SignalManagedNativeTransport = transport, reusableAccount?: string, ) { return { @@ -182,6 +185,46 @@ describe("probeManagedSignalSetup", () => { } }); + it("reuses the monitor-owned effective transport when config only names the account", async () => { + const cfg = { + channels: { signal: { accounts: { work: { account } } } }, + } as OpenClawConfig; + const effectiveTransport = { + kind: "managed-native" as const, + cliPath: "signal-cli", + httpHost: "127.0.0.1", + httpPort: 8080, + }; + const lifecycle = createSignalDaemonLifecycle({}); + const handle = mocks.spawnDaemon(); + mocks.spawnDaemon.mockClear(); + lifecycle.attach(handle); + registerSignalManagedDaemonOwner({ + handle, + owner: { + accountId: "work", + account, + cliPath: effectiveTransport.cliPath, + httpHost: effectiveTransport.httpHost, + httpPort: effectiveTransport.httpPort, + }, + abortSignal: lifecycle.abortSignal, + }); + + try { + await expect( + probeManagedSignalSetup(createParams(cfg, effectiveTransport, account)), + ).resolves.toMatchObject({ ok: true }); + expect(mocks.probeTransport).toHaveBeenCalledWith( + expect.objectContaining({ nativeAccountBinding: "owner-known-bound-account" }), + ); + expect(mocks.assertBindAvailable).not.toHaveBeenCalled(); + expect(mocks.spawnDaemon).not.toHaveBeenCalled(); + } finally { + await lifecycle.stop(); + } + }); + it("does not trust an unverifiable listener as the configured account", async () => { const cfg = { channels: { signal: { accounts: { work: { account, transport } } } }, diff --git a/extensions/signal/src/setup-managed-validation.ts b/extensions/signal/src/setup-managed-validation.ts index 33e3894e1453..38ff5b30b517 100644 --- a/extensions/signal/src/setup-managed-validation.ts +++ b/extensions/signal/src/setup-managed-validation.ts @@ -8,7 +8,6 @@ import { isSignalManagedDaemonOwned } from "./managed-daemon-runtime-context.js" import { assertSignalSetupDaemonBindAvailable } from "./setup-daemon-bind.js"; import { probeSignalTransport, - resolveConfiguredSignalTransport, type SignalManagedNativeTransport, type SignalTransportProbeResult, } from "./setup-transport.js"; @@ -130,23 +129,19 @@ export async function probeManagedSignalSetup(params: { let daemon: ReturnType | undefined; let result: SignalTransportProbeResult = { ok: false, error: "Signal transport probe failed." }; try { - const configured = resolveConfiguredSignalTransport(params.cfg, params.accountId); const configuredAccountInfo = resolveSignalAccount({ cfg: params.cfg, accountId: params.accountId, }); + const configured = configuredAccountInfo.transport; const configuredAccount = normalizeOptionalString(configuredAccountInfo.config.account); if ( - configured?.kind === "managed-native" && + configured.kind === "managed-native" && configuredAccount === params.account && params.reusableConfiguredAccount === params.account && params.reusableConfiguredTransport === managedSignalTransportIdentity(configured) ) { - const configuredResolved = resolveSignalTransport(configured); - if ( - configuredResolved.kind === "managed-native" && - sameManagedTransport(configuredResolved, resolved) - ) { + if (sameManagedTransport(configured, resolved)) { const ownerKnown = isSignalManagedDaemonOwned({ accountId: configuredAccountInfo.accountId, account: params.account,