fix(signal): reuse effective managed transport

This commit is contained in:
jesse-merhi
2026-08-11 16:57:28 +10:00
parent 678e2b93e1
commit 7ccc9cb62d
3 changed files with 96 additions and 10 deletions
@@ -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");
@@ -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 } } } },
@@ -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<typeof spawnSignalDaemon> | 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,