mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
fix(signal): honor managed setup lifecycle
This commit is contained in:
@@ -145,4 +145,38 @@ describe("Signal managed setup validation wiring", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("propagates setup cancellation without entering readiness recovery", async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
signal: {
|
||||
account: "+15555550123",
|
||||
transport: { kind: "managed-native", httpPort: 8080 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const { queued, credentialValues } = await prepareLocal(cfg);
|
||||
const abort = new AbortController();
|
||||
const reason = new DOMException("setup cancelled", "AbortError");
|
||||
mocks.probeManagedSignalSetup.mockImplementationOnce(async (params) => {
|
||||
expect(params.abortSignal).toBe(abort.signal);
|
||||
abort.abort(reason);
|
||||
params.abortSignal?.throwIfAborted();
|
||||
return { ok: false };
|
||||
});
|
||||
|
||||
await expect(
|
||||
runSetupWizardFinalize({
|
||||
finalize: signalSetupWizard.finalize,
|
||||
cfg,
|
||||
credentialValues,
|
||||
prompter: queued.prompter,
|
||||
runtime: createRuntimeEnv({ throwOnExit: false }),
|
||||
options: { abortSignal: abort.signal },
|
||||
}),
|
||||
).rejects.toBe(reason);
|
||||
|
||||
expect(queued.select).toHaveBeenCalledTimes(1);
|
||||
expect(queued.note).not.toHaveBeenCalledWith(expect.any(String), "Signal setup");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,9 +26,14 @@ const mocks = vi.hoisted(() => ({
|
||||
isExited: () => false,
|
||||
}),
|
||||
),
|
||||
waitForReady: vi.fn(async (params: { check: () => Promise<SignalTransportProbeResult> }) => {
|
||||
await params.check();
|
||||
}),
|
||||
waitForReady: vi.fn(
|
||||
async (params: {
|
||||
abortSignal?: AbortSignal;
|
||||
check: () => Promise<SignalTransportProbeResult>;
|
||||
}) => {
|
||||
await params.check();
|
||||
},
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/transport-ready-runtime", () => ({
|
||||
@@ -114,6 +119,34 @@ describe("probeManagedSignalSetup", () => {
|
||||
expect(mocks.spawnDaemon).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "localhost connection for an IPv4 loopback bind",
|
||||
candidate: { ...transport, url: "http://localhost:8080" },
|
||||
},
|
||||
{
|
||||
name: "IPv4 loopback connection for a localhost bind",
|
||||
candidate: { ...transport, httpHost: "localhost", url: "http://127.0.0.1:8080" },
|
||||
},
|
||||
])("does not re-probe the managed bind through $name", async ({ candidate }) => {
|
||||
const cfg = {
|
||||
channels: { signal: { accounts: { work: { account, transport: candidate } } } },
|
||||
} as OpenClawConfig;
|
||||
|
||||
await expect(
|
||||
probeManagedSignalSetup(createParams(cfg, candidate, account)),
|
||||
).resolves.toMatchObject({ ok: true });
|
||||
|
||||
expect(mocks.probeTransport).toHaveBeenCalledOnce();
|
||||
expect(mocks.probeTransport).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
transport: expect.objectContaining({
|
||||
url: `http://${candidate.httpHost}:8080`,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses owner-known proof for the exact daemon registered by the monitor lifecycle", async () => {
|
||||
const cfg = {
|
||||
channels: { signal: { accounts: { work: { account, transport } } } },
|
||||
@@ -209,6 +242,22 @@ describe("probeManagedSignalSetup", () => {
|
||||
expect(mocks.stop).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("passes setup cancellation to readiness and rethrows its reason after cleanup", async () => {
|
||||
const abort = new AbortController();
|
||||
const reason = new DOMException("setup cancelled", "AbortError");
|
||||
mocks.waitForReady.mockImplementationOnce(async ({ abortSignal }) => {
|
||||
expect(abortSignal).toBe(abort.signal);
|
||||
abort.abort(reason);
|
||||
});
|
||||
|
||||
await expect(
|
||||
probeManagedSignalSetup({ ...createParams(), abortSignal: abort.signal }),
|
||||
).rejects.toBe(reason);
|
||||
|
||||
expect(mocks.spawnDaemon).toHaveBeenCalledOnce();
|
||||
expect(mocks.stop).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("returns the actual final-port bind category without spawning", async () => {
|
||||
mocks.assertBindAvailable.mockRejectedValueOnce(
|
||||
new Error("address unavailable (EADDRNOTAVAIL)"),
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type SignalManagedNativeTransport,
|
||||
type SignalTransportProbeResult,
|
||||
} from "./setup-transport.js";
|
||||
import { isSignalManagedNativeConnectionUrlForBind } from "./transport-policy.js";
|
||||
import { buildSignalTransportHttpUrl } from "./transport-url.js";
|
||||
|
||||
type ResolvedManagedSignalTransport = Extract<
|
||||
@@ -59,8 +60,10 @@ async function probeManagedBind(params: {
|
||||
resolved: ResolvedManagedSignalTransport;
|
||||
account: string;
|
||||
accountBinding: "selected-account" | "owner-known-bound-account";
|
||||
abortSignal?: AbortSignal;
|
||||
}): Promise<SignalTransportProbeResult> {
|
||||
return await probeSignalTransport({
|
||||
params.abortSignal?.throwIfAborted();
|
||||
const result = await probeSignalTransport({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
transport: {
|
||||
@@ -73,6 +76,8 @@ async function probeManagedBind(params: {
|
||||
nativeAccountBinding: params.accountBinding,
|
||||
timeoutMs: 1_000,
|
||||
}).catch((error: unknown) => ({ ok: false, error: String(error) }));
|
||||
params.abortSignal?.throwIfAborted();
|
||||
return result;
|
||||
}
|
||||
|
||||
async function probeSeparateConnectionUrl(params: {
|
||||
@@ -81,18 +86,28 @@ async function probeSeparateConnectionUrl(params: {
|
||||
transport: SignalManagedNativeTransport;
|
||||
resolved: ResolvedManagedSignalTransport;
|
||||
account: string;
|
||||
abortSignal?: AbortSignal;
|
||||
}): Promise<SignalTransportProbeResult> {
|
||||
const bindUrl = buildSignalTransportHttpUrl(params.resolved.httpHost, params.resolved.httpPort);
|
||||
if (params.resolved.baseUrl === bindUrl) {
|
||||
params.abortSignal?.throwIfAborted();
|
||||
if (
|
||||
isSignalManagedNativeConnectionUrlForBind({
|
||||
...params.transport,
|
||||
url: params.resolved.baseUrl,
|
||||
httpHost: params.resolved.httpHost,
|
||||
httpPort: params.resolved.httpPort,
|
||||
})
|
||||
) {
|
||||
return { ok: true, status: 200, error: null };
|
||||
}
|
||||
return await probeSignalTransport({
|
||||
const result = await probeSignalTransport({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
transport: params.transport,
|
||||
account: params.account,
|
||||
timeoutMs: 1_000,
|
||||
}).catch((error: unknown) => ({ ok: false, error: String(error) }));
|
||||
params.abortSignal?.throwIfAborted();
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function probeManagedSignalSetup(params: {
|
||||
@@ -104,7 +119,9 @@ export async function probeManagedSignalSetup(params: {
|
||||
reusableConfiguredTransport?: string;
|
||||
runtime: RuntimeEnv;
|
||||
prompter: WizardPrompter;
|
||||
abortSignal?: AbortSignal;
|
||||
}): Promise<SignalTransportProbeResult> {
|
||||
params.abortSignal?.throwIfAborted();
|
||||
const resolved = resolveSignalTransport(params.transport);
|
||||
if (resolved.kind !== "managed-native") {
|
||||
throw new Error("Signal setup did not resolve a managed signal-cli transport.");
|
||||
@@ -157,6 +174,7 @@ export async function probeManagedSignalSetup(params: {
|
||||
httpHost: resolved.httpHost,
|
||||
httpPort: resolved.httpPort,
|
||||
});
|
||||
params.abortSignal?.throwIfAborted();
|
||||
const spawnedDaemon = spawnSignalDaemon({
|
||||
cliPath: resolved.cliPath,
|
||||
...(resolved.configPath ? { configPath: resolved.configPath } : {}),
|
||||
@@ -176,6 +194,7 @@ export async function probeManagedSignalSetup(params: {
|
||||
logAfterMs: 10_000,
|
||||
logIntervalMs: 10_000,
|
||||
pollIntervalMs: 150,
|
||||
...(params.abortSignal ? { abortSignal: params.abortSignal } : {}),
|
||||
runtime: params.runtime,
|
||||
check: async () => {
|
||||
if (spawnedDaemon.isExited()) {
|
||||
@@ -189,18 +208,26 @@ export async function probeManagedSignalSetup(params: {
|
||||
return result;
|
||||
},
|
||||
});
|
||||
params.abortSignal?.throwIfAborted();
|
||||
if (result.ok) {
|
||||
result = await probeSeparateConnectionUrl({ ...params, resolved });
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
params.abortSignal?.throwIfAborted();
|
||||
result = { ok: false, error: String(error) };
|
||||
return result;
|
||||
} finally {
|
||||
try {
|
||||
await daemon?.stop();
|
||||
} finally {
|
||||
progress.stop(result.ok ? "Signal setup validated." : "Signal setup validation failed.");
|
||||
progress.stop(
|
||||
params.abortSignal?.aborted
|
||||
? "Signal setup validation cancelled."
|
||||
: result.ok
|
||||
? "Signal setup validated."
|
||||
: "Signal setup validation failed.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ export const signalSetupWizard: ChannelSetupWizard = {
|
||||
params.credentialValues[signalSetupStateKeys.managedReuseTransport],
|
||||
runtime: params.runtime,
|
||||
prompter: params.prompter,
|
||||
...(params.options?.abortSignal ? { abortSignal: params.options.abortSignal } : {}),
|
||||
});
|
||||
if (probe.ok) {
|
||||
if (params.credentialValues[SIGNAL_LINK_COMPLETED_INPUT_KEY] === "true") {
|
||||
|
||||
Reference in New Issue
Block a user