From d6b634bc302a202c74ca2e6a18bfc8f190708b26 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 11:40:02 +0100 Subject: [PATCH] test: harden gateway talk and config drift coverage --- src/gateway/call.test.ts | 30 +++-- src/gateway/server.talk-runtime.test.ts | 167 +++++++++++------------- 2 files changed, 101 insertions(+), 96 deletions(-) diff --git a/src/gateway/call.test.ts b/src/gateway/call.test.ts index 1a8d64e6dae..f09b33a5d61 100644 --- a/src/gateway/call.test.ts +++ b/src/gateway/call.test.ts @@ -1,3 +1,6 @@ +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"; @@ -174,15 +177,19 @@ function makeRemotePasswordGatewayConfig(remotePassword: string, localPassword = describe("callGateway url resolution", () => { const envSnapshot = captureEnv([ "OPENCLAW_ALLOW_INSECURE_PRIVATE_WS", + "OPENCLAW_CONFIG_PATH", "OPENCLAW_GATEWAY_URL", "OPENCLAW_GATEWAY_TOKEN", + "OPENCLAW_STATE_DIR", ]); beforeEach(() => { envSnapshot.restore(); delete process.env.OPENCLAW_ALLOW_INSECURE_PRIVATE_WS; + delete process.env.OPENCLAW_CONFIG_PATH; delete process.env.OPENCLAW_GATEWAY_URL; delete process.env.OPENCLAW_GATEWAY_TOKEN; + delete process.env.OPENCLAW_STATE_DIR; resetGatewayCallMocks(); }); @@ -597,16 +604,23 @@ describe("buildGatewayConnectionDetails", () => { }); it("falls back to the default config loader when test deps drift", () => { - loadConfig.mockReturnValue({ gateway: { mode: "local", bind: "loopback" } }); - resolveGatewayPort.mockReturnValue(18800); - __testing.setDepsForTests({ - loadConfig: {} as never, - }); + const tempStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-gateway-call-")); + process.env.OPENCLAW_STATE_DIR = tempStateDir; + process.env.OPENCLAW_CONFIG_PATH = path.join(tempStateDir, "missing-config.json"); + try { + loadConfig.mockReturnValue({ gateway: { mode: "local", bind: "loopback" } }); + resolveGatewayPort.mockReturnValue(18800); + __testing.setDepsForTests({ + loadConfig: {} as never, + }); - const details = buildGatewayConnectionDetails(); + const details = buildGatewayConnectionDetails(); - expect(details.url).toBe("ws://127.0.0.1:18789"); - expect(details.urlSource).toBe("local loopback"); + expect(details.url).toBe("ws://127.0.0.1:18789"); + expect(details.urlSource).toBe("local loopback"); + } finally { + fs.rmSync(tempStateDir, { recursive: true, force: true }); + } }); it("throws for insecure ws:// remote URLs (CWE-319)", () => { diff --git a/src/gateway/server.talk-runtime.test.ts b/src/gateway/server.talk-runtime.test.ts index 2a8d767a00d..5d0133c6fdc 100644 --- a/src/gateway/server.talk-runtime.test.ts +++ b/src/gateway/server.talk-runtime.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest"; import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js"; import { talkHandlers } from "./server-methods/talk.js"; -import { withServer } from "./test-with-server.js"; type TalkSpeakPayload = { audioBase64?: string; @@ -60,51 +59,47 @@ describe("gateway talk runtime", () => { }, }); - await withServer(async () => { - await withSpeechProviders( - [ - { - pluginId: "acme-plugin", - source: "test", - provider: { - id: "acme", - label: "Acme Speech", - isConfigured: () => true, - synthesize: async () => ({ - audioBuffer: Buffer.from([7, 8, 9]), - outputFormat: "mp3", - fileExtension: ".mp3", - voiceCompatible: false, - }), - }, + await withSpeechProviders( + [ + { + pluginId: "acme-plugin", + source: "test", + provider: { + id: "acme", + label: "Acme Speech", + isConfigured: () => true, + synthesize: async () => ({ + audioBuffer: Buffer.from([7, 8, 9]), + outputFormat: "mp3", + fileExtension: ".mp3", + voiceCompatible: false, + }), }, - ], - async () => { - const res = await invokeTalkSpeakDirect({ - text: "Hello from talk mode.", - }); - expect(res?.ok, JSON.stringify(res?.error)).toBe(true); - expect((res?.payload as TalkSpeakPayload | undefined)?.provider).toBe("acme"); - expect((res?.payload as TalkSpeakPayload | undefined)?.audioBase64).toBe( - Buffer.from([7, 8, 9]).toString("base64"), - ); }, - ); - }); + ], + async () => { + const res = await invokeTalkSpeakDirect({ + text: "Hello from talk mode.", + }); + expect(res?.ok, JSON.stringify(res?.error)).toBe(true); + expect((res?.payload as TalkSpeakPayload | undefined)?.provider).toBe("acme"); + expect((res?.payload as TalkSpeakPayload | undefined)?.audioBase64).toBe( + Buffer.from([7, 8, 9]).toString("base64"), + ); + }, + ); }); it("returns fallback-eligible details when talk provider is not configured", async () => { const { writeConfigFile } = await import("../config/config.js"); await writeConfigFile({ talk: {} }); - await withServer(async () => { - const res = await invokeTalkSpeakDirect({ text: "Hello from talk mode." }); - expect(res?.ok).toBe(false); - expect(res?.error?.message).toContain("talk provider not configured"); - expect((res?.error as { details?: unknown } | undefined)?.details).toEqual({ - reason: "talk_unconfigured", - fallbackEligible: true, - }); + const res = await invokeTalkSpeakDirect({ text: "Hello from talk mode." }); + expect(res?.ok).toBe(false); + expect(res?.error?.message).toContain("talk provider not configured"); + expect((res?.error as { details?: unknown } | undefined)?.details).toEqual({ + reason: "talk_unconfigured", + fallbackEligible: true, }); }); @@ -121,32 +116,30 @@ describe("gateway talk runtime", () => { }, }); - await withServer(async () => { - await withSpeechProviders( - [ - { - pluginId: "acme-plugin", - source: "test", - provider: { - id: "acme", - label: "Acme Speech", - isConfigured: () => true, - synthesize: async () => { - throw new Error("provider failed"); - }, + await withSpeechProviders( + [ + { + pluginId: "acme-plugin", + source: "test", + provider: { + id: "acme", + label: "Acme Speech", + isConfigured: () => true, + synthesize: async () => { + throw new Error("provider failed"); }, }, - ], - async () => { - const res = await invokeTalkSpeakDirect({ text: "Hello from talk mode." }); - expect(res?.ok).toBe(false); - expect(res?.error?.details).toEqual({ - reason: "synthesis_failed", - fallbackEligible: false, - }); }, - ); - }); + ], + async () => { + const res = await invokeTalkSpeakDirect({ text: "Hello from talk mode." }); + expect(res?.ok).toBe(false); + expect(res?.error?.details).toEqual({ + reason: "synthesis_failed", + fallbackEligible: false, + }); + }, + ); }); it("rejects empty audio results as invalid_audio_result", async () => { @@ -162,34 +155,32 @@ describe("gateway talk runtime", () => { }, }); - await withServer(async () => { - await withSpeechProviders( - [ - { - pluginId: "acme-plugin", - source: "test", - provider: { - id: "acme", - label: "Acme Speech", - isConfigured: () => true, - synthesize: async () => ({ - audioBuffer: Buffer.alloc(0), - outputFormat: "mp3", - fileExtension: ".mp3", - voiceCompatible: false, - }), - }, + await withSpeechProviders( + [ + { + pluginId: "acme-plugin", + source: "test", + provider: { + id: "acme", + label: "Acme Speech", + isConfigured: () => true, + synthesize: async () => ({ + audioBuffer: Buffer.alloc(0), + outputFormat: "mp3", + fileExtension: ".mp3", + voiceCompatible: false, + }), }, - ], - async () => { - const res = await invokeTalkSpeakDirect({ text: "Hello from talk mode." }); - expect(res?.ok).toBe(false); - expect(res?.error?.details).toEqual({ - reason: "invalid_audio_result", - fallbackEligible: false, - }); }, - ); - }); + ], + async () => { + const res = await invokeTalkSpeakDirect({ text: "Hello from talk mode." }); + expect(res?.ok).toBe(false); + expect(res?.error?.details).toEqual({ + reason: "invalid_audio_result", + fallbackEligible: false, + }); + }, + ); }); });