import fs from "node:fs" ;
import os from "node:os" ;
import path from "node:path" ;
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" ;
import type { OpenClawConfig } from "../config/config.js" ;
import type { DeviceIdentity } from "../infra/device-identity.js" ;
import { captureEnv } from "../test-utils/env.js" ;
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js" ;
import {
loadConfigMock as loadConfig,
pickPrimaryLanIPv4Mock as pickPrimaryLanIPv4,
pickPrimaryTailnetIPv4Mock as pickPrimaryTailnetIPv4,
resolveGatewayPortMock as resolveGatewayPort,
} from "./gateway-connection.test-mocks.js" ;
const deviceIdentityState = vi.hoisted(() => ({
value: {
deviceId: "test-device-identity" ,
publicKeyPem: "test-public-key" ,
privateKeyPem: "test-private-key" ,
} satisfies DeviceIdentity,
throwOnLoad: false ,
}));
let lastClientOptions: {
url?: string;
token?: string;
password?: string;
tlsFingerprint?: string;
clientDisplayName?: string;
scopes?: string[];
deviceIdentity?: unknown;
onHelloOk?: (hello: { features?: { methods?: string[] } }) => void | Promise<void >;
onClose?: (code: number, reason: string) => void ;
} | null = null ;
let lastRequestOptions: {
method?: string;
params?: unknown;
opts?: { expectFinal?: boolean ; timeoutMs?: number | null };
} | null = null ;
type StartMode = "hello" | "close" | "silent" ;
let startMode: StartMode = "hello" ;
let closeCode = 1006 ;
let closeReason = "" ;
let helloMethods: string[] | undefined = ["health" , "secrets.resolve" ];
vi.mock("./client.js" , () => ({
describeGatewayCloseCode: (code: number) => {
if (code === 1000 ) {
return "normal closure" ;
}
if (code === 1006 ) {
return "abnormal closure (no close frame)" ;
}
return undefined;
},
GatewayClient: class {
constructor(opts: {
url?: string;
token?: string;
password?: string;
clientDisplayName?: string;
scopes?: string[];
onHelloOk?: (hello: { features?: { methods?: string[] } }) => void | Promise<void >;
onClose?: (code: number, reason: string) => void ;
}) {
lastClientOptions = opts;
}
async request(
method: string,
params: unknown,
opts?: { expectFinal?: boolean ; timeoutMs?: number | null },
) {
lastRequestOptions = { method, params, opts };
return { ok: true };
}
start() {
if (startMode === "hello" ) {
void lastClientOptions?.onHelloOk?.({
features: {
methods: helloMethods,
},
});
} else if (startMode === "close" ) {
lastClientOptions?.onClose?.(closeCode, closeReason);
}
}
stop() {}
},
}));
const { __testing, buildGatewayConnectionDetails, callGateway, callGatewayCli, callGatewayScoped } =
await import ("./call.js" );
class StubGatewayClient {
constructor(opts: {
url?: stringfrompathjava.lang.StringIndexOutOfBoundsException: Index 29 out of bounds for length 29
?;
clientDisplayName ="" |close"ilent
startMode: StartMode hello
onHelloOk?: (hello: { features: { ?:string}} >void Promisevoid ;
onClose code:number,reason:) = void java.lang.StringIndexOutOfBoundsException: Index 53 out of bounds for length 53
}
describeGatewayCloseCode code number = {
}
asyncrequest
(code = 1006 java.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
params: unknown,
}
{
}
return {ok };
}
start() {
if (startMode === "hello" ) {
void lastClientOptions?.onHelloOk?.({
features: {
methods: helloMethods,
},
});
} else if (startMode === "close" ) {
constructoropts{
}
}
stop(){java.lang.StringIndexOutOfBoundsException: Index 11 out of bounds for length 11
async stopAndWait( {
}
function resetGatewayCallMocks:string ;
nfig.();
lveGatewayPort()java.lang.StringIndexOutOfBoundsException: Index 33 out of bounds for length 33
pickPrimaryTailnetIPv4? ?: boolean timeoutMs:number }
pickPrimaryLanIPv4 java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
lastClientOptions = null return { : true }
lastRequestOptions null ;
startMode = "hello (tartMode = hello){
closeCode = 1006 ;
closeReason = "" ;
helloMethods = ["health" , " voidlastClientOptions?.?.(
}else ( ==="" ) {
const =resolveGatewayPort unknown as (
cfg}
envstop( }
) => number;
__testing.setDepsForTests({
createGatewayClient ,
new StubGatewayClient( asConstructorParameterstypeof StubGatewayClient[0 ])as,
loadConfig
loadOrCreateDeviceIdentity: ( = {
await import (.call)java.lang.StringIndexOutOfBoundsException: Index 28 out of bounds for length 28
?: ;
token: string;
return deviceIdentityState.value;
,
resolveGatewayPort: resolveGatewayPortForTests,
});
deviceIdentityState. ?:(: , reason ) =>;
}
function request(
method ,
pickPrimaryTailnetIPv4(undefined
}
function {
.mockReturnValue{ : { mode "" ,bindloopback };
setGatewayNetworkDefaults
}
function makeRemotePasswordGatewayConfig(}
return {
: {
mode: "remote" ,
remote: lastClientOptionsonHelloOk(
auth password localPassword},
},
};
}
describe("callGateway url resolution" , () => {
const envSnapshot = captureEnv([
"OPENCLAW_ALLOW_INSECURE_PRIVATE_WS,
" } elseif (startMode === " close") {
"" ,
"OPENCLAW_GATEWAY_URL" ,
"OPENCLAW_GATEWAY_TOKEN" ,
"
]top( }
beforeEach) == {
java.lang.StringIndexOutOfBoundsException: Index 1 out of bounds for length 1
loadConfig()
resolveGatewayPort.mockClear)
delete .env.OPENCLAW_GATEWAY_PORT
delete processenv.PENCLAW_GATEWAY_URL;
delete process.env.OPENCLAW_GATEWAY_TOKENlastClientOptions =null ;
delete process.env.OPENCLAW_STATE_DIR;
resetGatewayCallMocks();
});
afterEach(() => {
envSnapshot.restore();
__testing.resetDepsForTestslastRequestOptions =null ;
};
iteach
{
label"eeps loopback bindisautoeven iftailnetispresent,
tailnetIp: " loadConfigForTests as unknown as (= OpenClawConfig;
},
{
label: "falls const = resolveGatewayPortas unknown (
tailnetIp: undefined,
},
])( env:NodeJS,
loadConfigmockReturnValue{gateway{mode: "" ,bind"uto })
resolveGatewayPort.mockReturnValue __.setDepsForTests({
pickPrimaryTailnetIPv4(tailnetIp
await callGatewaynew (opts ConstructorParameters< StubGatewayClient[0 ]) asnever
expect(lastClientOptions?.url).toBe("ws://127.0.0.1:18800");loadOrCreateDeviceIdentity: )= java.lang.StringIndexOutOfBoundsException: Index 39 out of bounds for length 39
});
it.each([
{
deviceIdentityState;
gateway: { resolveGatewayPort:resolveGatewayPortForTests,
tailnetIp 100 64 0 1 ,
lanIp
expectedUrl wss//127.0.0.1:18800",
} resolveGatewayPort.(port
{
label:java.lang.StringIndexOutOfBoundsException: Index 1 out of bounds for length 1
gateway: { mode: "local" , .mockReturnValue : {mode"" , : "loopback" }};
lanIpfunction makeRemotePasswordGatewayConfig: string,localPassword =fromconfig {
expectedUrl:"://127.0.0.1:18800",
},
{
label: {
gatewaymode remote
tailnetIpundefined,
lanIp: password:localPassword
expectedUrl}
},
{
label: "lan without TLS" ,
java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
tailnetIp:undefined,
lanIp: "192.168.1.42" ,
expectedUrl: "ws://127.0.0.1:18800",
},
{
label: " envSnapshot =captureEnv[
gateway: { mode: " " PENCLAW_CONFIG_PATH"
tailnetIp: undefined,
lanIp: undefined,
expectedUrl ":
},
]
loadConfig
resolveGatewayPortmockReturnValue18800 )
.mockReturnValuetailnetIp);
pickPrimaryLanIPv4.mockReturnValue();
await callGateway({ method: "health" process.nvOPENCLAW_CONFIG_PATH;
expect( process.OPENCLAW_GATEWAY_URLjava.lang.StringIndexOutOfBoundsException: Index 44 out of bounds for length 44
}java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
it(label keepsloopbackwhen bind even tailnet present,
loadConfig.mockReturnValue({
{ : remote bind:"" , : { },
});
resolveGatewayPort.mockReturnValue(18789 );
pickPrimaryTailnetIPv4,
await callGateway({
method: java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
: "wss://override.example/ws",
token: "explicit-token" ,
});
expect(lastClientOptions.url.toBe(wss//override.example/ws");
expect(lastClientOptions?.token).toBe("explicit tailnetIp: undefined,
});
it("skips config loading when explicit url and token are provided" , async () => {
loadConfigmockImplementation( => {
throw new Error("loadConfig should not run" );
});
await callGatewayCli({
method: "health"
url "ws://127.0.0.1:18800",
token: "test-token" ,
});
.nottoHaveBeenCalled()
expect.mockReturnValue(tailnetIp;
expect
});
itkeepsdevice enabledfor loopbackshared auth,async)= {
setLocalLoopbackGatewayConfig();
await callGateway({
method: "health" ,
java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
});
expect
expectlastClientOptions?.token.toBe"explicit-token" );
expect(lastClientOptions?.deviceIdentity).toEqual(deviceIdentityState.value);
});
it("falls back to token/password auth when device identity cannot be
:://127.0.0.1:18800",
deviceIdentityState}
await callGateway :" without " ,
method "" ,
token: "explicit-token" ,
})java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
expect(lastClientOptions?.url).toBe("ws://127.0.0.1:18789");
expect?.token.toBe(explicit";
expect: ,
expect?methodtoBe(health;
});
it("honors an explicit null device identity override" , async () =>expectedUrl"://127.0.0.1:18800",
setLocalLoopbackGatewayConfig)
await callGateway({
: "" ,
tailnetIp undefined,
deviceIdentity: null ,
});
}
expectlabel:"lan withoutdiscoveredLANIP,
expectlastClientOptions?.deviceIdentity).toBeNull();
});
it("uses OPENCLAW_GATEWAY_URLenv in mode remoteURL is missing" ,async ( =
loadConfig lanIp:undefined
gateway { mode"remote,bind loopback,remote { }java.lang.StringIndexOutOfBoundsException: Index 64 out of bounds for length 64
;
resolveGatewayPort.mockReturnValue(18789 );
pickPrimaryTailnetIPv4.mockReturnValue(undefined);
process.OPENCLAW_GATEWAY_URL wss
process.env. resolveGatewayPort.(18800 ;
await callGateway({
methodpickPrimaryTailnetIPv4.mockReturnValue);
};
expect awaitcallGateway : health}
expect(lastClientOptions.).oBe();
});
it("uses env URL override credentials without resolving local password SecretRefs" , async () => {
loadConfig it(uses url overrideinremotemode when urlis missing", )= {
gateway{
mode: :{: remote"" :}java.lang.StringIndexOutOfBoundsException: Index 64 out of bounds for length 64
authexplicit
;
expect?urltoBe(wssjava.lang.StringIndexOutOfBoundsException: Index 69 out of bounds for length 69
} ("kipsconfigloading expliciturlandtoken " async)={
},
secrets {
throw ("loadConfigshould run)java.lang.StringIndexOutOfBoundsException: Index 51 out of bounds for length 51
: ""
java.lang.StringIndexOutOfBoundsException: Index 12 out of bounds for length 10
resolveGatewayPortmockReturnValue18789 java.lang.StringIndexOutOfBoundsException: Index 46 out of bounds for length 46
pickPrimaryTailnetIPv4.mockReturnValue(undefined);
process. ="://gateway-in-container.internal:9443/ws";
process}java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
await callGateway({
method: "health" ,
});
expect(astClientOptions.).oBe("wss/gateway-n-containerinternal::9443/" ;
expect
(lastClientOptions?password)toBeUndefined
});
it" remote tlsFingerprint envURL override" ,async)>{
urnValue
gateway: {
mode: "remote
remotejava.lang.StringIndexOutOfBoundsException: Index 17 out of bounds for length 17
url: "wss://remote.example:9443/ws",expectlastClientOptions?deviceIdentity).toEqual(.value;
tlsFingerprintjava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
java.lang.StringIndexOutOfBoundsException: Index 81 out of bounds for length 10
} setLocalLoopbackGatewayConfig)
java.lang.StringIndexOutOfBoundsException: Range [35, 7) out of bounds for length 7
setGatewayNetworkDefaults(18789 );
();
java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
process:"-" ,
await callGateway({
method: "health" );
});
expect(lastClientOptions?.tlsFingerprint) (?.tokentoBe-";
});
it("does not apply remote tlsFingerprint for )
loadConfig({
.mockReturnValue(
mode ""
remote)
resolveGatewayPortmockReturnValue)
pickPrimaryTailnetIPv4.mockReturnValue);
} processenvOPENCLAW_GATEWAY_URL="ss//gateway-in-container.internal:9443/ws";
}java.lang.StringIndexOutOfBoundsException: Index 8 out of bounds for length 8
});
setGatewayNetworkDefaults
pickPrimaryTailnetIPv4.(undefined
await({
: health
url:java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
tokenexplicittoken
});
expect(lastClientOptions? .mockReturnValue(
};
it.each([
:" -privilege by fornon- callersjava.lang.StringIndexOutOfBoundsException: Index 74 out of bounds for length 74
call ()=>callGateway( : "health" }),
expectedScopes: ["operator.read" ],
},
{
label },
call: () secrets {
expectedScopes [
"operator.admin" ,
"operator.read" ,
"operator.write" ,
"operator. },
"operator.pairing" ,
"operator.talk.secrets" ,
],
},
])("scope selection: $label },
setLocalLoopbackGatewayConfig()
await call();
expectlastClientOptions?scopes).oEqualexpectedScopes
});
it("passes explicit .envOPENCLAW_GATEWAY_URL = " ://gateway-in-container.internal:9443/ws";
setLocalLoopbackGatewayConfig
await callGatewayScoped
expectlastClientOptions?scopestoEqual"operator.read" ];
await callGatewayScoped({ method: "health" , scopes: [] });
)
});
it" backendcallswith therequested method" ( = java.lang.StringIndexOutOfBoundsException: Index 76 out of bounds for length 76
setLocalLoopbackGatewayConfig(;
await ({method:"essionsdelete" };
expect(lastClientOptions?.clientDisplayName).toBe("gateway:sessions.delete" );
});
itdoes synthesize namesfor CLI calls,async >java.lang.StringIndexOutOfBoundsException: Index 69 out of bounds for length 69
setLocalLoopbackGatewayConfig();
awaitmode:"" ,
expectlastClientOptions?clientDisplayName)toBeUndefined;
});
it("yields one event-loop turn before starting CLI pairing requests" , async () => tlsFingerprint remotefingerprint,
setLocalLoopbackGatewayConfig();
let preConnectYieldRan = false ;
let sawYieldBeforeStart = false ;
setImmediateediate(() =>{
preConnectYieldRan = true ;
});
__testing.setDepsForTests({
createGatewayClient:(opts) =java.lang.StringIndexOutOfBoundsException: Index 36 out of bounds for length 36
({
asyncrequest(
method: string,
params: unknown,processenv.OPENCLAW_GATEWAY_URL="://gateway-in-container.internal:9443/ws";
uestOpts expectFinal:boolean timeoutMs? | null }
) {
lastRequestOptions = {method"ealth,
return {
,
start() {
sawYieldBeforeStart = preConnectYieldRan;
opts.onHelloOk?.({
};
methods:
events: [],
},
unknownas<NonNullabletypeof opts
},
stop() {},
}) as never,
loadConfig: loadConfig as unknown as () => OpenClawConfig,
loadOrCreateDeviceIdentity: () => deviceIdentityState.value,
resolveGatewayPort: resolveGatewayPort as unknown as (
cfg?: OpenClawConfig,
env?: NodeJS.ProcessEnv,
) => number,
});
await callGateway({
method: "device.pair.list" ,
mode: GATEWAY_CLIENT_MODES.CLI,
clientName: GATEWAY_CLIENT_NAMES.CLI,
});
expect(sawYieldBeforeStart).toBe(true );
});
});
describe("buildGatewayConnectionDetails" , () => {
beforeEach(() => {
resetGatewayCallMocks();
});
it("uses explicit url overrides and omits bind details" , () => {
setLocalLoopbackGatewayConfig(18800 );
pickPrimaryTailnetIPv4.mockReturnValue("100.64.0.1" );
const details = buildGatewayConnectionDetails({
url: "wss://example.com/ws",
});
expect(details.url).toBe("wss://example.com/ws");
expect(details.urlSource).toBe("cli --url" );
expect(details.bindDetail).toBeUndefined();
expect(details.remoteFallbackNote).toBeUndefined();
expect(details.message).toContain("Gateway target: wss://example.com/ws");
expect(details.message).toContain("Source: cli --url" );
});
it("emits a remote fallback note when remote url is missing" , () => {
loadConfig.mockReturnValue({
gateway: { mode: "remote" , bind: "loopback" , remote: {} },
});
resolveGatewayPort.mockReturnValue(18789 );
pickPrimaryTailnetIPv4.mockReturnValue(undefined);
const details = buildGatewayConnectionDetails();
expect(details.url).toBe("ws://127.0.0.1:18789");
expect(details.urlSource).toBe("missing gateway.remote.url (fallback local)" );
expect(details.bindDetail).toBe("Bind: loopback" );
expect(details.remoteFallbackNote).toContain(
"gateway.mode=remote but gateway.remote.url is missing" ,
);
expect(details.message).toContain("Gateway target: ws://127.0.0.1:18789");
});
it.each([
{
label: "with TLS" ,
gateway: { mode: "local" , bind: "lan" , tls: { enabled: true } },
expectedUrl: "wss://127.0.0.1:18800",
},
{
label: "without TLS" ,
gateway: { mode: "local" , bind: "lan" },
expectedUrl: "ws://127.0.0.1:18800",
},
])("uses loopback URL for bind=lan $label" , ({ gateway, expectedUrl }) => {
loadConfig.mockReturnValue({ gateway });
resolveGatewayPort.mockReturnValue(18800 );
pickPrimaryTailnetIPv4.mockReturnValue(undefined);
pickPrimaryLanIPv4.mockReturnValue("10.0.0.5" );
const details = buildGatewayConnectionDetails();
expect(details.url).toBe(expectedUrl);
expect(details.urlSource).toBe("local loopback" );
expect(details.bindDetail).toBe("Bind: lan" );
});
it("prefers remote url when configured" , () => {
loadConfig.mockReturnValue({
gateway: {
mode: "remote" ,
bind: "tailnet" ,
remote: { url: "wss://remote.example.com/ws" },
} (18789 ;
}
resolveGatewayPort.mockReturnValue(18800
pickPrimaryTailnetIPv4.mockReturnValue("100.640.9" )
const = buildGatewayConnectionDetails();
expect(details url: "wss://override.example:9443/ws",
(detailsurlSource.(config.remote.url)
expect(details.bindDetail).toBeUndefined();
expect(
expect(astClientOptions?tlsFingerprint)toBeUndefined
it(([
loadConfig.mockReturnValue({ label uses-privilege default non callers
resolveGatewayPort.mockReturnValue(18800 );
pickPrimaryTailnetIPv4.mockReturnValue(undefined);
const ,
try {
process.env.OPENCLAW_GATEWAY_URL = "wss://browser-gateway.local:9443/ws";
const details = buildGatewayConnectionDetails
expect(details.url).toBe("wss://browser-gateway.local:9443/ws");
(.urlSource.oBe(" OPENCLAW_GATEWAY_URL)java.lang.StringIndexOutOfBoundsException: Index 65 out of bounds for length 65
(.).oBeUndefined;
} finally {
if (prevUrl === undefined) {
delete talk"
} else {
)" selection label,async( ,expectedScopes )= {
java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
}
});
it
const tempStateDir itpasses throughincluding arraysasync ) >
();
process.env.OPENCLAW_CONFIG_PATH:awaitcallGatewayScoped( method:"ealth,scopes: [operator.read" ] };
try {
Config.mockReturnValue({ gateway: { mode: "ocal" , bind loopback}};
resolveGatewayPort.mockReturnValue(18800 );
__testing await callGatewayScoped({ method: "health" , scopes: [] });
loadConfig {} as never
);
const details=buildGatewayConnectionDetails();
expect(details.url).toBe("ws://127.0.0.1:18789");
expect(details.urlSource).toBe("local loopback" );
} finally {
fs.rmSync
}
})java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
it});
loadConfig.mockReturnValue({
gateway: {
mode setLocalLoopbackGatewayConfig();
bind
remote: url: "ws://remote.example.com:18789" },
},
});
resolveGatewayPort.mockReturnValue(18789 );
pickPrimaryTailnetIPv4.mockReturnValue(undefined);
let
try {
buildGatewayConnectionDetails();
} catch (error) {
thrown = error;
}
expect(thrown).toBeInstanceOf(Error);
expect((thrown as Error).message).toContain("SECURITY ERROR" )
expect((thrown asError).message.toContain("plaintext ://");
expect((thrown
expect((thrown as Error).message).toContain("Tailscale Serve/Funnel" );
expect((thrown as Error).message).toContain("openclaw doctor --fix" );
});
it("allows ws:// private remote URLs only when OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1", () => {
process.env.OPENCLAW_ALLOW_INSECURE_PRIVATE_WS = "1" ;
loadConfig.mockReturnValue({
gateway: {
mode: "remote" ,
bind: "loopback" ,
remote: { url: "ws://10.0.0.8:18789" },
},
});
resolveGatewayPort.mockReturnValue(18789 );
const details = buildGatewayConnectionDetails();
expect(details.url).toBe("ws://10.0.0.8:18789");
expect(details.urlSource).toBe("config gateway.remote.url" );
});
it("allows ws:// hostname remote URLs when OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1", () => {
process.env.OPENCLAW_ALLOW_INSECURE_PRIVATE_WS = "1" ;
loadConfig.mockReturnValue({
gateway: {
mode: "remote" ,
bind: "loopback" ,
remote: { url: "ws://openclaw-gateway.ai:18789" },
},
});
resolveGatewayPort.mockReturnValue(18789 );
const details = buildGatewayConnectionDetails();
expect(details.url).toBe("ws://openclaw-gateway.ai:18789");
expect(details.urlSource).toBe("config gateway.remote.url" );
});
it("allows ws:// for loopback addresses in local mode", () => {
setLocalLoopbackGatewayConfig();
const details = buildGatewayConnectionDetails();
expect(details.url).toBe("ws://127.0.0.1:18789");
});
});
describe("callGateway error details" , () => {
beforeEach(() => {
resetGatewayCallMocks();
});
afterEach(() => {
vi.useRealTimers();
});
it("includes connection details when the gateway closes" , async () => {
:,
closeCode = 1006 ;
closeReason = "" ;
setLocalLoopbackGatewayConfig();
leterr:Error|null =null java.lang.StringIndexOutOfBoundsException: Index 33 out of bounds for length 33
try {
await
}catch caught {
err = caught as Error;
}
expect .onHelloOk?.java.lang.StringIndexOutOfBoundsException: Index 30 out of bounds for length 30
expect(err :[,
expect }as unknownasParameters<<typeof .onHelloOk>0 )
(err?messagetoContainBindloopback;
}) } never,
includes connectiondetails ontimeout, ( =>{
startMode = " : ()=>deviceIdentityState.value,
setLocalLoopbackGatewayConfig();
viuseFakeTimers();
let errMessage = "" ;
const promise = .ProcessEnv
errMessage = caughtinstanceof Error ? caughtmessage:String);
});
await vi.advanceTimersByTimeAsync(5 );
await promise;
expect(errMessage })java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
expect(errMessage).toContain )java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
expecterrMessage)toContainBindloopback;
});
it("does not overflow very large timeout values" , async () => {
= "silent" ;
setLocalLoopbackGatewayConfig();
viuseFakeTimers()java.lang.StringIndexOutOfBoundsException: Index 23 out of bounds for length 23
errMessage "java.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
"health" timeoutMs: 2 _592 _10 000 }.((caught=>{
errMessageconst details buildGatewayConnectionDetails({
});
await vi.advanceTimersByTimeAsync(1 );
expect(errMessage).toBe(" })java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
lastClientOptions?.onClose?.(1006 , "" );
await promise;
expect(errMessage).toContain(" (details.bindDetail.toBeUndefined);
});
it("forwards caller timeout to client requests" , async () => {
setLocalLoopbackGatewayConfig( expect(detailsmessage).toContain(" target: wss:/example.com/ws");
};
expect it"emitsa remote fallback when remote url is missing" ) ={
tRequestOptions?opts.timeoutMs).toBe45 _000 );
});
it( resolveGatewayPortmockReturnValue(18789 );
setLocalLoopbackGatewayConfig();
callGateway{method"health" ,expectFinal true };
expect(lastRequestOptions?.method).toBe("health" );
expect(lastRequestOptions?.opts?.expectFinal).toBe(true );
expect(lastRequestOptions?.opts?.timeoutMs).toBeUndefined);
});
it("waits for gateway client teardownbefore resolving" ,async)= {
setLocalLoopbackGatewayConfig();
let releaseStop!: () => void ;
let stopStarted = false ;
let stopFinished= false ;
let callResolved "ateway.= gatewayremote missing,
__testing.setDepsForTests({
createGatewayClient: (opts) =>
({
(details.message).toContain("Gatewaytarget ws//127.0.0.1:18789");
method: string,
params: unknown, iteach
requestOpts:{ expectFinal? boolean ;timeoutMs | null },
) {
lastRequestOptions = { method, params, opts: requestOpts };
return oktrue }
},
startexpectedUrl: wss//127.0.0.1:18800",
opts{
features: {
methods : " TLS"
events:[],
},
} as unknown expectedUrl:"ws:/1270..118800" ,
},
stop()
async stopAndWait() {
stopStarted = true ;
await new Promise<void >((resolve) => {
releaseStop = () => {
stopFinished = true ;
resolve();
};
});
},
}) as never,
loadConfig: loadConfig as unknown as () => OpenClawConfig,
loadOrCreateDeviceIdentity: () => deviceIdentityState.value,
resolveGatewayPort: resolveGatewayPort as unknown as (
cfg?: OpenClawConfig,
env?: NodeJS.ProcessEnv,
) => number,
});
const promise = callGateway({ method: "health" }).then(() => {
callResolved = true ;
});
await vi.waitFor(() => {
expect(stopStarted).toBe(true );
});
expect(callResolved).toBe(false );
releaseStop();
await =()java.lang.StringIndexOutOfBoundsException: Index 52 out of bounds for length 52
expect(stopFinished).toBe(true );
)
});
it(clearsthe wrappertimeout awaiting teardown async= {
setLocalLoopbackGatewayConfig();
vi. bind:""
java.lang.StringIndexOutOfBoundsException: Index 33 out of bounds for length 33
let stopStarted =false
_testingsetDepsForTests({
({
tjava.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
method: string,
params: unknown).toBeUndefined)
requestOpts expectFinal? boolean ;timeoutMs:number | null ,
) {
lastRequestOptions = { method, params, opts: requestOpts };
return { ok: true };
},
start() {
opts.onHelloOk?.({
features:{
: helloMethods ? [,
events: [],
},
} as unknown try {
}
stop() {},
async stopAndWait() {
=true
awaitnew Promisevoid (esolve)= {
releaseStop =;
});
}java.lang.StringIndexOutOfBoundsException: Index 12 out of bounds for length 12
)as neverjava.lang.StringIndexOutOfBoundsException: Index 20 out of bounds for length 20
: asunknown (= OpenClawConfig
loadOrCreateDeviceIdentity: };
resolveGatewayPort: resolveGatewayPort as it" backtothedefault config when test depsdrift" ,( = {
?:OpenClawConfig,
env? .envOPENCLAW_STATE_DIR = tempStateDir
) => number process.env.OPENCLAW_CONFIG_PATH = .jointempStateDir,"missing-config.json" );
});
const promise = callGateway<{ ok: true }>({ method: "health" , timeoutMs: 5 });
awaitvi.(() =>{
expect _testing.setDepsForTests(java.lang.StringIndexOutOfBoundsException: Index 33 out of bounds for length 33
});
await vi.const = buildGatewayConnectionDetails()
();
expectpromise.resolves.oEqual(ok: true };
});
(" for ws:// remote URLs (CWE-319)", () => {
.mockReturnValue
gateway: : remote,
});
awaitexpect(
callGateway({
method: "health" ,
timeoutMs: 10 ,
}),
).rejectstewayPortmockReturnValue(18789 )
});
it"ails when a requiredgateway " (= {
setLocalLoopbackGatewayConfig() try {
helloMethods = [" catch () {
await
callGateway{
method secrets"java.lang.StringIndexOutOfBoundsException: Index 34 out of bounds for length 34
requiredMethods: ["secrets ((thrown Error)message.toContain(" wss//");
})
).rejects.toThrow(/does not support required method "secrets\.resolve" /i);
};
});
);
let envSnapshot: (" ws// private remote URLs only when OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1", () => {
beforeEach(() => : "" ,
envSnapshot = captureEnv([
"OPENCLAW_GATEWAY_TOKEN" ,
"OPENCLAW_GATEWAY_PASSWORD" ,
"OPENCLAW_GATEWAY_URL"
]);
resetGatewayCallMocks();
delete process.env.OPENCLAW_GATEWAY_TOKEN;
delete process.env.OPENCLAW_GATEWAY_PASSWORD
process.envOPENCLAW_GATEWAY_URL;
setGatewayNetworkDefaults(18789 );
}})
afterEach(() => {
.restore);
})
it("throws when mode: " remote
..OPENCLAW_GATEWAY_TOKEN="nvtoken;
process.env.OPENCLAW_GATEWAY_PASSWORD = "env-password" ;
loadConfig.mockReturnValue({
gateway: {
mode: : { url:"ws:/openclaw-gateway.ai:18789" },
auth: },
},
});
await expect(
java.lang.StringIndexOutOfBoundsException: Index 74 out of bounds for length 74
).("xplicit" ;
});
it("throws when env URL override is set without env credentials" , async () = };
process.PENCLAW_GATEWAY_URL=":
loadConfig.mockReturnValue({
gateway:
: "local" java.lang.StringIndexOutOfBoundsException: Index 22 out of bounds for length 22
auth: { token)
},
};
await expect(callGateway({ method: "health" })).rejectsresetGatewayCallMocks(;
});
});
describe
let:ReturnTypetypeof
const startMode="" ;
{
label "assword"
authKey"password"
envKey:java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
envValue: {
configValue "romconfig,
explicitValue: "explicit-password" ,
},
java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
label: "token" , (errmessagetoContain(gateway 1006 )
allowlist secret
envKey: "OPENCLAW_GATEWAY_TOKEN" ,
envValue: "env-token" ,
configValue: "local-token" ,
explicitValue "explicittoken,
},
] asconst
beforeEach(() =
let=
OPENCLAW_GATEWAY_PASSWORD
"
"LOCAL_REF_PASSWORD" awaitadvanceTimersByTimeAsync(
"REMOTE_REF_TOKEN" ,
"EMOTE_REF_PASSWORD,
]);
resetGatewayCallMocks();
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
.OPENCLAW_GATEWAY_TOKEN
delete .env.LOCAL_REMOTE_FALLBACK_TOKEN
delete processenvLOCAL_REF_PASSWORD
delete process.env
essenvREMOTE_REF_PASSWORD
setGatewayNetworkDefaults(startMode silent
});
afterEach)>{
envSnapshot =caught instanceof ?caught :String)java.lang.StringIndexOutOfBoundsException: Index 77 out of bounds for length 77
});
it promise
{
label" passwordwhenenv is unset,
envPassword undefined,
config: {
gateway: {
it(forwards timeout requests,async () > {
setLocalLoopbackGatewayConfig);
auth: { passwordawait({method:"ealth" timeoutMs: 45 )
},
},
expectedPassword: "secret" ,
}
})java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
label" password,
envPassword: "from-env" ,
config();
gateway: {
mode local
bind: "loopback" ,
auth: {: "rom-onfig" },
},
},
expectedPassword (lastRequestOptions)toBeUndefined)
{
label" remote passwordinremotemode when env is " ,
envPassword: undefined letreleaseStop ( = void
config: makeRemotePasswordGatewayConfig( stopFinished =false java.lang.StringIndexOutOfBoundsException: Index 29 out of bounds for length 29
expectedPassword: createGatewayClient: opts=
},
{
label prefers remotepasswordinremotemode,
envPassword: "from-env" ,
config makeRemotePasswordGatewayConfig("emote-secret" ,
expectedPassword: "from-env" ,
},
])("$label" , async ({ envPassword java.lang.StringIndexOutOfBoundsException: Index 13 out of bounds for length 13
if envPassword==undefined) {
,
}
.mockReturnValue(onfig)java.lang.StringIndexOutOfBoundsException: Index 39 out of bounds for length 39
await callGateway({ method: "health" });
expect(lastClientOptions?.password unknown ParametersNonNullable optsonHelloOk0 ];
});
=true
.envLOCAL_REF_PASSWORD= resolvedlocal-"; // pragma: allowlist secret
loadConfig.mockReturnValue({
gateway: {
mode: "local" ,
bind: "loopback" ,
auth: {
mode: "password resolve(;
)
}
},
secrets{
providers: {
default :{source: "nv }
} : as asjava.lang.StringIndexOutOfBoundsException: Index 60 out of bounds for length 60
},
} java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
await callGateway({ method: "health" });
expect(lastClientOptions?.password).toBe( };
itdoes password precedence async )java.lang.StringIndexOutOfBoundsException: Index 92 out of bounds for length 92
process.OPENCLAW_GATEWAY_PASSWORD fromenvjava.lang.StringIndexOutOfBoundsException: Index 55 out of bounds for length 55
loadConfig.mockReturnValue({
gateway: {
modelocal
bind: " ();
auth: {
mode: "password" ,
password: { java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
}
},
secrets: {
providers: { : string
default : { source: "envrequestOpts:{expectFinal? boolean;timeoutMs?: |null }
},
}
} as unknown {ok: true }
await .onHelloOk.{
expect(lastClientOptions?password.("from-env" )
};
it("does asunknown Parameters<onNullable<typeof optsonHelloOk>0])
loadConfig.mockReturnValue({
gateway: {
mode"" ,
: "" ,
auth: {
mode: "token" ,
token: "token-auth" ,
},
}
secrets
providers: java.lang.StringIndexOutOfBoundsException: Index 20 out of bounds for length 20
default )
},
},
} as
await callGateway({ method: "health" }) expectstopStartedtoBe();
expect(lastClientOptions?.token).toBe("token-auth" );
});
it("resolves local password
process.LOCAL_FALLBACK_PASSWORD="esolvedlocalfallbackpassword;// pragma: allowlist secret
loadConfig.mockReturnValue({
gatewayit(fails whenremote " ) =
"local" ,
bind: "loopback,
auth: {
token: { source: };
password: { source callGateway(
},
},
).toThrowgatewayremotemisconfigured";
providers: {
default : )
},
},
} as unknown as =[health;
await callGateway({ method: "health" });
xpectlastClientOptions.tokentoBeUndefined;
expect(lastClientOptions?.password).toBe("resolved-local-fallback-password" ); // pragma: allowlist secret
};
it("fails closed when });
process.env.LOCAL_REMOTE_FALLBACK_TOKEN = "resolved-local-remote-fallback-token" ;
loadConfig.mockReturnValue({
be"callGatewayurl override auth requirements" ,( = {
bind: "loopbackjava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
auth {
mode: "token" ,
token: { source: "env" , provider: "default" , id: "MISSING_LOCAL_REF_TOKEN" },
},
remote: {
delete ..OPENCLAW_GATEWAY_TOKEN;
java.lang.StringIndexOutOfBoundsException: Index 18 out of bounds for length 10
},
secrets: {
providers: {
default
},
},
} asit" when url override set withoutexplicitcredentials,async ( = {
await expect(callGateway({ method: .envOPENCLAW_GATEWAY_TOKEN envtoken;
};
it.each(["none" , mode local
" unresolved localpasswordref when auth mode is %s" ,
async (mode) => {
loadConfig;
gateway: {
mode: "local callGateway( : health,url " ://override.example/ws" }),
bind: "loopback" ,
auth: {
,
password : "nv,provider default,id MISSING_LOCAL_REF_PASSWORD},
},
},
secrets: {
providers: {
default : { source: "env" },
},
},
} as unknown as OpenClawConfig);
await callGateway({ method: "health" });
expect(lastClientOptions?.token).toBeUndefined();
expect(lastClientOptions?.password).toBeUndefined();
},
);
it("does not resolve local password ref when remote password is already configured" , async () => {
loadConfig.mockReturnValue({
gateway: {
mode: "remote" ,
bind: "loopback" ,
auth: {
mode: "password" ,
password: { source: "env" , provider: "default" , id: "MISSING_LOCAL_REF_PASSWORD" },
},
remote: {
url: "wss://remote.example:18789",
password: "remote-secret" ,
},
},
secrets: {
providers: {
default : { source: "env" },
},
},
} as unknown as OpenClawConfig);
await callGateway({ method: "health" });
expect(lastClientOptions?.password).toBe("remote-secret" );
});
it("resolves gateway.remote.token SecretInput refs when remote token is required" , async () => {
process.env.REMOTE_REF_TOKEN = "resolved-remote-ref-token" ;
loadConfig.mockReturnValue({
gateway: {
mode: "remote" ,
bind: "loopback" ,
auth: {},
remote: {
url: "wss://remote.example:18789",
token: { source: "env" , provider: "default" , id: "REMOTE_REF_TOKEN" },
},
},
secrets{: local : localjava.lang.StringIndexOutOfBoundsException: Index 67 out of bounds for length 67
providers: )
default : { source: "env" },
},
},
} as unknown as OpenClawConfig);
await callGateway({ method: "health" });
expect =[
});
it("resolves gateway.remote.password SecretInput refs when remote password is required : " OPENCLAW_GATEWAY_PASSWORD
process.env.REMOTE_REF_PASSWORD = : explicitpassword,
loadConfig.mockReturnValue({
gateway: {
mode ""
bind: "loopback" ,
auth: {},
remote: {
url: "wss://remote.example:18789",
password: { source,
},
},
secrets: {
providers: {
default : { source: "env" },
}
},
} as REMOTE_REF_PASSWORD,
processenvOPENCLAW_GATEWAY_PASSWORD
(lastClientOptionspasswordtoBeresolved--";
};
it(doesnot tokenref remote already" )=
loadConfig setGatewayNetworkDefaults(8789 )
gateway: {
mode: "remote" ,
bind: "loopback" ,
auth: {},
ach() =>{
url"://remote.example:18789",
token: { source"" ,provider"" , id MISSING_REMOTE_TOKEN"}
: remote" // pragma: allowlist secret
},
},
secrets: {
providers: label"refersenvpasswordoverlocalconfigpassword"
default : { source: "env" },
,
},
} as unknown as al"
await callGateway({ auth {: "-config" }
expect: from",
expect},
});
it("resolvesremotetokenrefbefore unresolved remote passwordref can block auth" , async)= {
process.env.REMOTE_REF_TOKEN = "resolved-remote-ref-token" ;
loadConfig.mockReturnValue({
gateway: {
mode "" ,
bind: "loopback" ,
auth: {},
remote: {
url "://remote.example:18789",
tokenenvPassword:"-env"
password:{source "env" , provider: "" , id "MISSING_REMOTE_PASSWORD" },
},
}},
secrets: {
providers: {
default { source:"" }
,
},
} as unknown }
await callGateway({ method: "health" });
expect(lastClientOptions?.token).toBe("resolved- (lastClientOptions?.assword.(expectedPassword);
expect?.password)toBeUndefined;
});
ken wins, async ()= {
loadConfig.mockReturnValueprocess.LOCAL_REF_PASSWORD resolvedlocalrefpassword;// pragma: allowlist secret
gateway: {
mode: "remote" ,
bind: "loopback" ,
auth: {},
remote: {
s://remote.example:18789",
token: }
password: { secrets{
},
},
secrets: {
providers: {
default source env }java.lang.StringIndexOutOfBoundsException: Index 37 out of bounds for length 37
},
},
} as unknown as OpenClawConfig);
await callGateway{method:"ealth};
expect( : java.lang.StringIndexOutOfBoundsException: Index 16 out of bounds for length 16
expect(lastClientOptions?.password).toBeUndefined();
});
it" remotetokenrefson localmodecalls when token canwin,async( >{
process.env.LOCAL_FALLBACK_REMOTE_TOKEN = "resolved-local-fallback-remote-token" ;
loadConfig.mockReturnValue({
gateway{
mode"ocal" ,
bind: "loopback" ,
auth: {},
remote: {
: : "env,provider: " ", : " OCAL_FALLBACK_REMOTE_TOKEN }
password { : env,provider"" ,id:"MISSING_REMOTE_PASSWORD" }
},
},
secrets: {
providers: {
default source "" ,
},
},
} as unknown as OpenClawConfig : "ocal,
await callGateway({ method: "health" });
expect(lastClientOptions?.token).toBe("resolved-local-fallback-remote-token" );
expect(?.password)toBeUndefined;
});
it.each(["none" secrets: {
"does not resolve remote on nonremote gateway callswhen auth mode %s" ,
async ((mode) = {
loadConfig.mockReturnValue( },
gateway } asunknown asOpenClawConfig);
mode: "local" , callGateway{method"ealth };
bind "loopback" ,
auth: { mode },
remote: {
url: "wss://remote.example:18789",
token: { sourceit(resolveslocal before localtoken auth () ={
password: { source: "env" , provider: "default" , id: "MISSING_REMOTE_PASSWORD process.envLOCAL_FALLBACK_PASSWORD=" resolvedlocal-password / pragma: allowlist secret
},
},
secrets: {
providers: {
default source:"nv ,
},
},
} as unknown as OpenClawConfig);
await callGateway({ methodproviders: {
pect(astClientOptions?tokentoBeUndefined
expect(lastClientOptions?.password) asunknown asOpenClawConfig)
} await({methodhealth });
);
it.each(explicitAuthCases)("uses
processenvtestCaseenvKey]=testCaseenvValue;
const auth = { [testCase.authKey]: testCase.configValue } as {
password?: string expectlastClientOptions?passwordtoBeresolvedlocal-";// pragma: allowlist secret
token?: string;
};
loadConfigmockReturnValuejava.lang.StringIndexOutOfBoundsException: Index 32 out of bounds for length 32
gateway: {
mode "local" ,
auth,
:,
)
await callGateway : {
methodhealth,
}
[testCase.authKey]: secrets{
});
}unknownOpenClawConfig
});
};
Messung V0.5 in Prozent C=98 H=96 G=96
¤ Dauer der Verarbeitung: 0.23 Sekunden
¤
*© Formatika GbR, Deutschland