diff --git a/extensions/signal/src/setup-managed-validation.test.ts b/extensions/signal/src/setup-managed-validation.test.ts index 35d3e7a9a403..233e94b4304d 100644 --- a/extensions/signal/src/setup-managed-validation.test.ts +++ b/extensions/signal/src/setup-managed-validation.test.ts @@ -315,26 +315,58 @@ describe("probeManagedSignalSetup", () => { expect(mocks.spawnDaemon).not.toHaveBeenCalled(); }); - it("probes a separate connection URL with selected-account verification", async () => { + it("requires a separate connection URL to become healthy with the managed daemon", async () => { const candidate = { ...transport, url: "https://signal.example:9443" }; - await probeManagedSignalSetup( - createParams( - { - channels: { - signal: { - accounts: { - work: { account, transport: candidate }, - }, - }, + const cfg = { + channels: { + signal: { + accounts: { + work: { account, transport: candidate }, }, }, - candidate, - ), - ); + }, + } as OpenClawConfig; + mocks.probeTransport + .mockResolvedValueOnce({ ok: false, error: "connection refused" }) + .mockResolvedValueOnce({ ok: true, status: 200 }) + .mockResolvedValueOnce({ ok: true, status: 200 }); + + await expect(probeManagedSignalSetup(createParams(cfg, candidate))).resolves.toMatchObject({ + ok: true, + }); + expect(mocks.probeTransport).toHaveBeenNthCalledWith( + 1, + expect.not.objectContaining({ nativeAccountBinding: "owner-known-bound-account" }), + ); expect(mocks.probeTransport).toHaveBeenNthCalledWith( 2, + expect.objectContaining({ nativeAccountBinding: "owner-known-bound-account" }), + ); + expect(mocks.probeTransport).toHaveBeenNthCalledWith( + 3, expect.not.objectContaining({ nativeAccountBinding: "owner-known-bound-account" }), ); }); + + it("rejects a separate connection URL already served by an independent daemon", async () => { + const candidate = { ...transport, url: "https://signal.example:9443" }; + const cfg = { + channels: { + signal: { + accounts: { + work: { account, transport: candidate }, + }, + }, + }, + } as OpenClawConfig; + + await expect(probeManagedSignalSetup(createParams(cfg, candidate))).resolves.toMatchObject({ + ok: false, + error: expect.stringContaining("external-native"), + }); + + expect(mocks.probeTransport).toHaveBeenCalledOnce(); + expect(mocks.spawnDaemon).not.toHaveBeenCalled(); + }); }); diff --git a/extensions/signal/src/setup-managed-validation.ts b/extensions/signal/src/setup-managed-validation.ts index 38ff5b30b517..d3624fb6b710 100644 --- a/extensions/signal/src/setup-managed-validation.ts +++ b/extensions/signal/src/setup-managed-validation.ts @@ -79,6 +79,18 @@ async function probeManagedBind(params: { return result; } +function hasSeparateConnectionUrl(params: { + transport: SignalManagedNativeTransport; + resolved: ResolvedManagedSignalTransport; +}): boolean { + return !isSignalManagedNativeConnectionUrlForBind({ + ...params.transport, + url: params.resolved.baseUrl, + httpHost: params.resolved.httpHost, + httpPort: params.resolved.httpPort, + }); +} + async function probeSeparateConnectionUrl(params: { cfg: OpenClawConfig; accountId: string; @@ -88,14 +100,7 @@ async function probeSeparateConnectionUrl(params: { abortSignal?: AbortSignal; }): Promise { params.abortSignal?.throwIfAborted(); - if ( - isSignalManagedNativeConnectionUrlForBind({ - ...params.transport, - url: params.resolved.baseUrl, - httpHost: params.resolved.httpHost, - httpPort: params.resolved.httpPort, - }) - ) { + if (!hasSeparateConnectionUrl(params)) { return { ok: true, status: 200, error: null }; } const result = await probeSignalTransport({ @@ -169,6 +174,16 @@ export async function probeManagedSignalSetup(params: { httpHost: resolved.httpHost, httpPort: resolved.httpPort, }); + if (hasSeparateConnectionUrl({ ...params, resolved })) { + const preexistingConnection = await probeSeparateConnectionUrl({ ...params, resolved }); + if (preexistingConnection.ok) { + return { + ok: false, + error: + "Signal managed connection URL was already serving the selected account before OpenClaw started its daemon. Use external-native for an independently operated Signal server.", + }; + } + } params.abortSignal?.throwIfAborted(); const spawnedDaemon = spawnSignalDaemon({ cliPath: resolved.cliPath,