import { createArgReader, createGatewayWsClient, resolveGatewayUrl } from "./gateway-ws-client.ts" ;
function writeStdoutLine(message = "" ): void {
process.stdout.write(`${message}\n`);
}
function writeStdoutJson(value: unknown): void {
process.stdout.write(`${JSON.stringify(value, null , 2 )}\n`);
}
function writeStderrLine(message: string): void {
process.stderr.write(`${message}\n`);
}
type NodeListPayload = {
ts?: number;
nodes?: Array<{
nodeId: string;
displayName?: string;
platform?: string;
connected?: boolean ;
paired?: boolean ;
commands?: string[];
permissions?: unknown;
}>;
};
type NodeListNode = NonNullable<NodeListPayload["nodes" ]>[number];
const { get: getArg, has: hasFlag } = createArgReader();
const urlRaw = getArg("--url" ) ?? process.env.OPENCLAW_GATEWAY_URL;
const token = getArg("--token" ) ?? process.env.OPENCLAW_GATEWAY_TOKEN;
const nodeHint = getArg("--node" );
const dangerous = hasFlag("--dangerous" ) || process.env.OPENCLAW_RUN_DANGEROUS === "1" ;
const jsonOut = hasFlag("--json" );
if (!urlRaw || !token) {
writeStderrLine(
"Usage: bun scripts/dev/ios-node-e2e.ts --url <wss://host[:port]> --token <gateway.auth.token> [--node <id|name-substring>] [--dangerous] [--json]\n" +
"Or set env: OPENCLAW_GATEWAY_URL / OPENCLAW_GATEWAY_TOKEN" ,
);
process.exit(1 );
}
const url = resolveGatewayUrl(urlRaw);
const isoNow = () => new Date().toISOString();
const isoMinusMs = (ms: number) => new Date(Date.now() - ms).toISOString();
type TestCase = {
id: string;
command: string;
params?: unknown;
timeoutMs?: number;
dangerous?: boolean ;
};
function formatErr(err: unknown): string {
if (!err) {
return "error" ;
}
if (typeof err === "string" ) {
return err;
}
if (err instanceof Error) {
return err.message || String(err);
}
try {
return JSON.stringify(err);
} catch {
return Object.prototype.toString.call(err);
}
}
function pickIosNode(list: NodeListPayload, hint?: string): NodeListNode | null {
const nodes = (list.nodes ?? []).filter((n) => n && n.connected);
const ios = nodes.filter((n) => (n.platform ?? "" ).toLowerCase().includes("ios" ));
if (ios.length === 0 ) {
return null ;
}
if (!hint) {
return ios[0 ] ?? null ;
}
const h = hint.toLowerCase();
return (
ios.find((n) => n.nodeId.toLowerCase() === h) ??
ios.find((n) => (n.displayName ?? "" ).toLowerCase().includes(h)) ??
ios.find((n) => n.nodeId.toLowerCase().includes(h)) ??
ios[0 ] ??
null
);
}
async function main() {
const { request, waitOpen, close } = createGatewayWsClient({ url: url.toString() });
await waitOpen();
const connectRes = await request("connect" , {
minProtocol: 3 ,
maxProtocol: 3 ,
client: {
id: "cli" ,
displayName: "openclaw ios node e2e" ,
version: "dev" ,
platform: "dev" ,
mode: "cli" ,
instanceId: "openclaw-dev-ios-node-e2e" ,
},
locale: "en-US" ,
userAgent: "ios-node-e2e" ,
role: "operator" ,
scopes: ["operator.read" , "operator.write" , "operator.admin" ],
caps: [],
auth: { token },
});
if (!connectRes.ok) {
writeStderrLine(`connect failed: ${String(connectRes.error)}`);
close();
process.exit(2 );
}
const healthRes = await request("health" );
if (!healthRes.ok) {
writeStderrLine(`health failed: ${String(healthRes.error)}`);
close();
process.exit(3 );
}
const nodesRes = await request("node.list" );
if (!nodesRes.ok) {
writeStderrLine(`node.list failed: ${String(nodesRes.error)}`);
close();
process.exit(4 );
}
const listPayload = (nodesRes.payload ?? {}) as NodeListPayload;
let node = pickIosNode(listPayload, nodeHint);
if (!node) {
const waitSeconds = Number.parseInt(getArg("--wait-seconds" ) ?? "25" , 10 );
const deadline = Date.now() + Math.max(1 , waitSeconds) * 1000 ;
while (!node && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 1000 ));
const res = await request("node.list" ).catch (() => null );
if (!res?.ok) {
continue ;
}
node = pickIosNode((res.payload ?? {}) as NodeListPayload, nodeHint);
}
}
if (!node) {
writeStderrLine("No connected iOS nodes found. (Is the iOS app connected to the gateway?)" );
close();
process.exit(5 );
}
const tests: TestCase[] = [
{ id: "device.info" , command: "device.info" },
{ id: "device.status" , command: "device.status" },
{
id: "system.notify" ,
command: "system.notify" ,
params: { title: "OpenClaw E2E" , body: `ios-node-e2e @ ${isoNow()}`, delivery: "system" },
},
{
id: "contacts.search" ,
command: "contacts.search" ,
params: { query: null , limit: 5 },
},
{
id: "calendar.events" ,
command: "calendar.events" ,
params: { startISO: isoMinusMs(6 * 60 * 60 * 1000 ), endISO: isoNow(), limit: 10 },
},
{
id: "reminders.list" ,
command: "reminders.list" ,
params: { status: "incomplete" , limit: 10 },
},
{
id: "motion.pedometer" ,
command: "motion.pedometer" ,
params: { startISO: isoMinusMs(60 * 60 * 1000 ), endISO: isoNow() },
},
{
id: "photos.latest" ,
command: "photos.latest" ,
params: { limit: 1 , maxWidth: 512 , quality: 0 .7 },
},
{
id: "camera.snap" ,
command: "camera.snap" ,
params: { facing: "back" , maxWidth: 768 , quality: 0 .7 , format: "jpeg" },
dangerous: true ,
timeoutMs: 20 _000 ,
},
{
id: "screen.record" ,
command: "screen.record" ,
params: { durationMs: 2 _000 , fps: 15 , includeAudio: false },
dangerous: true ,
timeoutMs: 30 _000 ,
},
];
const run = tests.filter((t) => dangerous || !t.dangerous);
const results: Array<{
id: string;
ok: boolean ;
error?: unknown;
payload?: unknown;
}> = [];
for (const t of run) {
const invokeRes = await request(
"node.invoke" ,
{
nodeId: node.nodeId,
command: t.command,
params: t.params,
timeoutMs: t.timeoutMs ?? 12 _000 ,
idempotencyKey: randomUUID(),
},
(t.timeoutMs ?? 12 _000 ) + 2 _000 ,
).catch ((err) => {
results.push({ id: t.id, ok: false , error: formatErr(err) });
return null ;
});
if (!invokeRes) {
continue ;
}
if (!invokeRes.ok) {
results.push({ id: t.id, ok: false , error: invokeRes.error });
continue ;
}
results.push({ id: t.id, ok: true , payload: invokeRes.payload });
}
if (jsonOut) {
writeStdoutJson({
gateway: url.toString(),
node: {
nodeId: node.nodeId,
displayName: node.displayName,
platform: node.platform,
},
dangerous,
results,
});
} else {
const pad = (s: string, n: number) => (s.length >= n ? s : s + " " .repeat(n - s.length));
const rows = results.map((r) => ({
cmd: r.id,
ok: r.ok ? "ok" : "fail" ,
note: r.ok ? "" : formatErr(r.error ?? "error" ),
}));
const width = Math.min(64 , Math.max(12 , ...rows.map((r) => r.cmd.length)));
writeStdoutLine(`node: ${node.displayName ?? node.nodeId} (${node.platform ?? "unknown" })`);
writeStdoutLine(`dangerous: ${dangerous ? "on" : "off" }`);
writeStdoutLine();
for (const r of rows) {
writeStdoutLine(`${pad(r.cmd, width)} ${pad(r.ok, 4 )} ${r.note}`);
}
}
const failed = results.filter((r) => !r.ok);
close();
if (failed.length > 0 ) {
process.exit(10 );
}
}
await main();
Messung V0.5 in Prozent C=95 H=95 G=94
¤ Dauer der Verarbeitung: 0.3 Sekunden
¤
*© Formatika GbR, Deutschland