fix: clean up Hermes-visible OpenClaw leftovers (#97)
* cleanup openclaw session leftovers - hermes can breathe now * fix: load hermes adapter env from .env * fix: redact secrets from hermes adapter error output * addressed review findings * address luke findings #2
This commit is contained in:
+11
-2
@@ -3,10 +3,17 @@ NEXT_PUBLIC_GATEWAY_URL=ws://localhost:18789
|
|||||||
|
|
||||||
# Runtime gateway URL — takes effect on restart without a rebuild.
|
# Runtime gateway URL — takes effect on restart without a rebuild.
|
||||||
# Use this instead of NEXT_PUBLIC_GATEWAY_URL when you want to change the
|
# Use this instead of NEXT_PUBLIC_GATEWAY_URL when you want to change the
|
||||||
# gateway endpoint without re-running `npm run build`. Also used as a
|
# gateway endpoint without re-running `npm run build`.
|
||||||
# fallback when openclaw.json is not present.
|
|
||||||
# CLAW3D_GATEWAY_URL=ws://localhost:18789
|
# CLAW3D_GATEWAY_URL=ws://localhost:18789
|
||||||
# CLAW3D_GATEWAY_TOKEN=
|
# CLAW3D_GATEWAY_TOKEN=
|
||||||
|
# Optional: tell Studio which backend that runtime gateway URL represents.
|
||||||
|
# Valid values: openclaw, hermes, demo, custom
|
||||||
|
# CLAW3D_GATEWAY_ADAPTER_TYPE=openclaw
|
||||||
|
|
||||||
|
|
||||||
|
# HERMES_API_URL=http://localhost:8642
|
||||||
|
# HERMES_API_KEY=change-me-local-dev
|
||||||
|
# HERMES_MODEL=hermes-agent
|
||||||
|
|
||||||
# Debug UI
|
# Debug UI
|
||||||
DEBUG=true
|
DEBUG=true
|
||||||
@@ -30,10 +37,12 @@ DEBUG=true
|
|||||||
# HERMES_ADAPTER_PORT=18789
|
# HERMES_ADAPTER_PORT=18789
|
||||||
# HERMES_MODEL=hermes
|
# HERMES_MODEL=hermes
|
||||||
# HERMES_AGENT_NAME=Hermes
|
# HERMES_AGENT_NAME=Hermes
|
||||||
|
# If CLAW3D_GATEWAY_URL is unset, Studio can still detect this local adapter port.
|
||||||
|
|
||||||
# Demo gateway (no OpenClaw or Hermes required)
|
# Demo gateway (no OpenClaw or Hermes required)
|
||||||
# Run `npm run demo-gateway` and connect Claw3D to ws://localhost:18789
|
# Run `npm run demo-gateway` and connect Claw3D to ws://localhost:18789
|
||||||
# DEMO_ADAPTER_PORT=18789
|
# DEMO_ADAPTER_PORT=18789
|
||||||
|
# If CLAW3D_GATEWAY_URL is unset, Studio can still detect this local adapter port.
|
||||||
|
|
||||||
# Optional: voice features
|
# Optional: voice features
|
||||||
# ELEVENLABS_API_KEY=
|
# ELEVENLABS_API_KEY=
|
||||||
|
|||||||
@@ -230,7 +230,10 @@ Common environment variables:
|
|||||||
- `UPSTREAM_ALLOWLIST` restricts which upstream gateway hosts Studio may proxy to. Set this in production.
|
- `UPSTREAM_ALLOWLIST` restricts which upstream gateway hosts Studio may proxy to. Set this in production.
|
||||||
- `CUSTOM_RUNTIME_ALLOWLIST` restricts which hosts `/api/runtime/custom` may fetch. If unset, it falls back to `UPSTREAM_ALLOWLIST`.
|
- `CUSTOM_RUNTIME_ALLOWLIST` restricts which hosts `/api/runtime/custom` may fetch. If unset, it falls back to `UPSTREAM_ALLOWLIST`.
|
||||||
- `NEXT_PUBLIC_GATEWAY_URL` provides the default upstream gateway URL when Studio settings are empty. **Note:** this is a build-time variable — changes require `npm run build` to take effect.
|
- `NEXT_PUBLIC_GATEWAY_URL` provides the default upstream gateway URL when Studio settings are empty. **Note:** this is a build-time variable — changes require `npm run build` to take effect.
|
||||||
- `CLAW3D_GATEWAY_URL` and `CLAW3D_GATEWAY_TOKEN` provide a runtime alternative to `NEXT_PUBLIC_GATEWAY_URL` that takes effect on server restart without a rebuild. These are also used as a fallback when `openclaw.json` is not present.
|
- `CLAW3D_GATEWAY_URL` and `CLAW3D_GATEWAY_TOKEN` provide a runtime alternative to `NEXT_PUBLIC_GATEWAY_URL` that takes effect on server restart without a rebuild.
|
||||||
|
- `CLAW3D_GATEWAY_ADAPTER_TYPE` can pair with `CLAW3D_GATEWAY_URL` to mark those runtime defaults as `openclaw`, `hermes`, `demo`, or `custom`.
|
||||||
|
- If `CLAW3D_GATEWAY_URL` is not set, Studio can still surface local Hermes or demo adapter defaults from `HERMES_ADAPTER_PORT` / `DEMO_ADAPTER_PORT`.
|
||||||
|
- OpenClaw file defaults still come from `~/.openclaw/openclaw.json` when present.
|
||||||
- `OPENCLAW_STATE_DIR` and `OPENCLAW_CONFIG_PATH` override the default OpenClaw paths.
|
- `OPENCLAW_STATE_DIR` and `OPENCLAW_CONFIG_PATH` override the default OpenClaw paths.
|
||||||
- `OPENCLAW_GATEWAY_SSH_TARGET`, `OPENCLAW_GATEWAY_SSH_USER`, `OPENCLAW_GATEWAY_SSH_PORT`, and `OPENCLAW_GATEWAY_SSH_STRICT_HOST_KEY_CHECKING` support advanced gateway-host operations over SSH when needed.
|
- `OPENCLAW_GATEWAY_SSH_TARGET`, `OPENCLAW_GATEWAY_SSH_USER`, `OPENCLAW_GATEWAY_SSH_PORT`, and `OPENCLAW_GATEWAY_SSH_STRICT_HOST_KEY_CHECKING` support advanced gateway-host operations over SSH when needed.
|
||||||
- `ELEVENLABS_API_KEY`, `ELEVENLABS_VOICE_ID`, and `ELEVENLABS_MODEL_ID` enable voice reply integration.
|
- `ELEVENLABS_API_KEY`, `ELEVENLABS_VOICE_ID`, and `ELEVENLABS_MODEL_ID` enable voice reply integration.
|
||||||
|
|||||||
@@ -27,6 +27,35 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { WebSocketServer } = require("ws");
|
const { WebSocketServer } = require("ws");
|
||||||
|
|
||||||
|
function loadDotenvFile(filePath) {
|
||||||
|
if (!fs.existsSync(filePath)) return;
|
||||||
|
const content = fs.readFileSync(filePath, "utf8");
|
||||||
|
for (const rawLine of content.split(/\r?\n/)) {
|
||||||
|
const line = rawLine.trim();
|
||||||
|
if (!line || line.startsWith("#")) continue;
|
||||||
|
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
||||||
|
if (!match) continue;
|
||||||
|
const [, key, rawValue] = match;
|
||||||
|
if (process.env[key] !== undefined) continue;
|
||||||
|
let value = rawValue.trim();
|
||||||
|
if (
|
||||||
|
(value.startsWith('"') && value.endsWith('"')) ||
|
||||||
|
(value.startsWith("'") && value.endsWith("'"))
|
||||||
|
) {
|
||||||
|
value = value.slice(1, -1);
|
||||||
|
}
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadRuntimeEnv() {
|
||||||
|
const cwd = process.cwd();
|
||||||
|
loadDotenvFile(path.join(cwd, ".env.local"));
|
||||||
|
loadDotenvFile(path.join(cwd, ".env"));
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRuntimeEnv();
|
||||||
|
|
||||||
const HERMES_API_URL = (process.env.HERMES_API_URL || "http://localhost:8642").replace(/\/$/, "");
|
const HERMES_API_URL = (process.env.HERMES_API_URL || "http://localhost:8642").replace(/\/$/, "");
|
||||||
const HERMES_API_KEY = process.env.HERMES_API_KEY || "";
|
const HERMES_API_KEY = process.env.HERMES_API_KEY || "";
|
||||||
const ADAPTER_PORT = parseInt(process.env.HERMES_ADAPTER_PORT || "18789", 10);
|
const ADAPTER_PORT = parseInt(process.env.HERMES_ADAPTER_PORT || "18789", 10);
|
||||||
@@ -227,7 +256,7 @@ function loadHistoryFromDisk() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[hermes-adapter] Could not load history:", err.message);
|
console.warn("[hermes-adapter] Could not load history:", sanitizeErrorMessage(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +271,7 @@ function saveHistoryToDisk() {
|
|||||||
fs.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
|
fs.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
|
||||||
fs.writeFileSync(HISTORY_FILE, JSON.stringify(data, null, 2), "utf8");
|
fs.writeFileSync(HISTORY_FILE, JSON.stringify(data, null, 2), "utf8");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[hermes-adapter] Could not save history:", err.message);
|
console.warn("[hermes-adapter] Could not save history:", sanitizeErrorMessage(err));
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
@@ -261,6 +290,17 @@ function randomId() {
|
|||||||
return require("crypto").randomBytes(8).toString("hex");
|
return require("crypto").randomBytes(8).toString("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function redactSecrets(value) {
|
||||||
|
if (typeof value !== "string" || !value) return value;
|
||||||
|
let redacted = value;
|
||||||
|
if (HERMES_API_KEY) {
|
||||||
|
redacted = redacted.split(HERMES_API_KEY).join("[REDACTED]");
|
||||||
|
}
|
||||||
|
redacted = redacted.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]");
|
||||||
|
redacted = redacted.replace(/\b\d{8,12}:[A-Za-z0-9_-]{20,}\b/g, "[REDACTED]");
|
||||||
|
return redacted;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Hermes HTTP API helpers
|
// Hermes HTTP API helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -311,6 +351,12 @@ async function readJsonBody(res) {
|
|||||||
return JSON.parse(raw);
|
return JSON.parse(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeErrorMessage(error) {
|
||||||
|
if (!error) return "Unknown error";
|
||||||
|
if (typeof error === "string") return redactSecrets(error);
|
||||||
|
return redactSecrets(error.message || String(error));
|
||||||
|
}
|
||||||
|
|
||||||
function extractOpenAiStyleError(payload, fallbackMessage) {
|
function extractOpenAiStyleError(payload, fallbackMessage) {
|
||||||
if (payload && typeof payload === "object") {
|
if (payload && typeof payload === "object") {
|
||||||
const message =
|
const message =
|
||||||
@@ -607,8 +653,9 @@ async function execDelegateTask(args) {
|
|||||||
byAgent: [{ agentId: targetId, recent: [{ key: sessionKey, updatedAt: Date.now() }] }] } },
|
byAgent: [{ agentId: targetId, recent: [{ key: sessionKey, updatedAt: Date.now() }] }] } },
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
emitSub("error", { errorMessage: err.message });
|
const message = sanitizeErrorMessage(err);
|
||||||
return JSON.stringify({ ok: false, error: err.message });
|
emitSub("error", { errorMessage: message });
|
||||||
|
return JSON.stringify({ ok: false, error: message });
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.stringify({ ok: true, agent_id: targetId, response: responseText });
|
return JSON.stringify({ ok: true, agent_id: targetId, response: responseText });
|
||||||
@@ -962,7 +1009,7 @@ async function handleMethod(method, params, id, sendEvent) {
|
|||||||
byAgent: [{ agentId: sessionAgentId, recent: [{ key: sessionKey, updatedAt: Date.now() }] }] } } });
|
byAgent: [{ agentId: sessionAgentId, recent: [{ key: sessionKey, updatedAt: Date.now() }] }] } } });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!aborted) emitChat("error", { errorMessage: err.message || "Hermes API error" });
|
if (!aborted) emitChat("error", { errorMessage: sanitizeErrorMessage(err) || "Hermes API error" });
|
||||||
else emitChat("aborted", {});
|
else emitChat("aborted", {});
|
||||||
} finally {
|
} finally {
|
||||||
activeRuns.delete(runId);
|
activeRuns.delete(runId);
|
||||||
@@ -1134,7 +1181,7 @@ function startAdapter() {
|
|||||||
|
|
||||||
const wss = new WebSocketServer({ server: httpServer });
|
const wss = new WebSocketServer({ server: httpServer });
|
||||||
wss.on("error", (err) => {
|
wss.on("error", (err) => {
|
||||||
if (err.code !== "EADDRINUSE") console.error("[hermes-adapter] Server error:", err.message);
|
if (err.code !== "EADDRINUSE") console.error("[hermes-adapter] Server error:", sanitizeErrorMessage(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
wss.on("connection", (ws) => {
|
wss.on("connection", (ws) => {
|
||||||
@@ -1144,7 +1191,7 @@ function startAdapter() {
|
|||||||
const send = (frame) => {
|
const send = (frame) => {
|
||||||
if (ws.readyState === ws.OPEN) {
|
if (ws.readyState === ws.OPEN) {
|
||||||
try { ws.send(JSON.stringify(frame)); }
|
try { ws.send(JSON.stringify(frame)); }
|
||||||
catch (e) { console.error("[hermes-adapter] send error:", e.message); }
|
catch (e) { console.error("[hermes-adapter] send error:", sanitizeErrorMessage(e)); }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1197,14 +1244,15 @@ function startAdapter() {
|
|||||||
const response = await handleMethod(method, params, id, sendEventFn);
|
const response = await handleMethod(method, params, id, sendEventFn);
|
||||||
send(response);
|
send(response);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[hermes-adapter] Error handling ${method}:`, err.message);
|
const message = sanitizeErrorMessage(err);
|
||||||
send(resErr(id, "internal_error", err.message || "Internal error"));
|
console.error(`[hermes-adapter] Error handling ${method}:`, message);
|
||||||
|
send(resErr(id, "internal_error", message || "Internal error"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("close", () => activeSendEventFns.delete(sendEventFn));
|
ws.on("close", () => activeSendEventFns.delete(sendEventFn));
|
||||||
ws.on("error", (err) => {
|
ws.on("error", (err) => {
|
||||||
console.error("[hermes-adapter] WebSocket error:", err.message);
|
console.error("[hermes-adapter] WebSocket error:", sanitizeErrorMessage(err));
|
||||||
activeSendEventFns.delete(sendEventFn);
|
activeSendEventFns.delete(sendEventFn);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1221,7 +1269,7 @@ function startAdapter() {
|
|||||||
if (err.code === "EADDRINUSE") {
|
if (err.code === "EADDRINUSE") {
|
||||||
console.error(`[hermes-adapter] Port ${ADAPTER_PORT} in use. Set HERMES_ADAPTER_PORT to change it.`);
|
console.error(`[hermes-adapter] Port ${ADAPTER_PORT} in use. Set HERMES_ADAPTER_PORT to change it.`);
|
||||||
} else {
|
} else {
|
||||||
console.error("[hermes-adapter] Server error:", err.message);
|
console.error("[hermes-adapter] Server error:", sanitizeErrorMessage(err));
|
||||||
}
|
}
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import type { AgentState } from "@/features/agents/state/store";
|
|||||||
import type { CronCreateDraft, CronCreateTemplateId } from "@/lib/cron/createPayloadBuilder";
|
import type { CronCreateDraft, CronCreateTemplateId } from "@/lib/cron/createPayloadBuilder";
|
||||||
import { formatCronPayload, formatCronSchedule, type CronJobSummary } from "@/lib/cron/types";
|
import { formatCronPayload, formatCronSchedule, type CronJobSummary } from "@/lib/cron/types";
|
||||||
import type { SkillStatusReport } from "@/lib/skills/types";
|
import type { SkillStatusReport } from "@/lib/skills/types";
|
||||||
|
import type { StudioGatewayAdapterType } from "@/lib/studio/settings";
|
||||||
|
|
||||||
export type AgentSettingsPanelProps = {
|
export type AgentSettingsPanelProps = {
|
||||||
agent: AgentState;
|
agent: AgentState;
|
||||||
@@ -47,6 +48,7 @@ export type AgentSettingsPanelProps = {
|
|||||||
cronCreateBusy?: boolean;
|
cronCreateBusy?: boolean;
|
||||||
onCreateCronJob?: (draft: CronCreateDraft) => Promise<void> | void;
|
onCreateCronJob?: (draft: CronCreateDraft) => Promise<void> | void;
|
||||||
controlUiUrl?: string | null;
|
controlUiUrl?: string | null;
|
||||||
|
adapterType?: StudioGatewayAdapterType | null;
|
||||||
skillsReport?: SkillStatusReport | null;
|
skillsReport?: SkillStatusReport | null;
|
||||||
skillsLoading?: boolean;
|
skillsLoading?: boolean;
|
||||||
skillsError?: string | null;
|
skillsError?: string | null;
|
||||||
@@ -248,6 +250,7 @@ export const AgentSettingsPanel = ({
|
|||||||
cronCreateBusy = false,
|
cronCreateBusy = false,
|
||||||
onCreateCronJob = () => {},
|
onCreateCronJob = () => {},
|
||||||
controlUiUrl = null,
|
controlUiUrl = null,
|
||||||
|
adapterType = "openclaw",
|
||||||
skillsReport = null,
|
skillsReport = null,
|
||||||
skillsLoading = false,
|
skillsLoading = false,
|
||||||
skillsError = null,
|
skillsError = null,
|
||||||
@@ -267,6 +270,7 @@ export const AgentSettingsPanel = ({
|
|||||||
onSkillApiKeyChange = () => {},
|
onSkillApiKeyChange = () => {},
|
||||||
onSaveSkillApiKey = () => {},
|
onSaveSkillApiKey = () => {},
|
||||||
}: AgentSettingsPanelProps) => {
|
}: AgentSettingsPanelProps) => {
|
||||||
|
const isOpenClawRuntime = adapterType === "openclaw";
|
||||||
const initialPermissionsDraft =
|
const initialPermissionsDraft =
|
||||||
permissionsDraft ?? resolvePresetDefaultsForRole(resolveExecutionRoleFromAgent(agent));
|
permissionsDraft ?? resolvePresetDefaultsForRole(resolveExecutionRoleFromAgent(agent));
|
||||||
const [permissionsBaselineValue, setPermissionsBaselineValue] =
|
const [permissionsBaselineValue, setPermissionsBaselineValue] =
|
||||||
@@ -785,16 +789,18 @@ export const AgentSettingsPanel = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<section className="sidebar-section" data-testid="agent-settings-heartbeat-coming-soon">
|
{isOpenClawRuntime ? (
|
||||||
<h3 className="sidebar-section-title">Heartbeats</h3>
|
<section className="sidebar-section" data-testid="agent-settings-heartbeat-coming-soon">
|
||||||
<div className="mt-3 text-[11px] text-muted-foreground">
|
<h3 className="sidebar-section-title">Heartbeats</h3>
|
||||||
Heartbeat automation controls are coming soon.
|
<div className="mt-3 text-[11px] text-muted-foreground">
|
||||||
</div>
|
Heartbeat automation controls are coming soon.
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{mode === "advanced" ? (
|
{mode === "advanced" && isOpenClawRuntime ? (
|
||||||
<>
|
<>
|
||||||
<section className="sidebar-section mt-8" data-testid="agent-settings-control-ui">
|
<section className="sidebar-section mt-8" data-testid="agent-settings-control-ui">
|
||||||
<h3 className="sidebar-section-title ui-text-danger">Danger Zone</h3>
|
<h3 className="sidebar-section-title ui-text-danger">Danger Zone</h3>
|
||||||
|
|||||||
@@ -1691,7 +1691,8 @@ const AgentsPageScreen = () => {
|
|||||||
onDeleteCronJob={(jobId) =>
|
onDeleteCronJob={(jobId) =>
|
||||||
settingsMutationController.handleDeleteCronJob(inspectSidebarAgent.agentId, jobId)
|
settingsMutationController.handleDeleteCronJob(inspectSidebarAgent.agentId, jobId)
|
||||||
}
|
}
|
||||||
controlUiUrl={controlUiUrl}
|
controlUiUrl={selectedAdapterType === "openclaw" ? controlUiUrl : null}
|
||||||
|
adapterType={selectedAdapterType}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -727,7 +727,8 @@ export const useGatewayConnection = (
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
// When the user has no saved gateway URL, prefer the runtime
|
// When the user has no saved gateway URL, prefer the runtime
|
||||||
// localGatewayDefaults (from openclaw.json / CLAW3D_GATEWAY_URL)
|
// localGatewayDefaults (from openclaw.json, CLAW3D_GATEWAY_URL,
|
||||||
|
// or detected local Hermes/demo adapter ports)
|
||||||
// over the build-time NEXT_PUBLIC_GATEWAY_URL which may be stale
|
// over the build-time NEXT_PUBLIC_GATEWAY_URL which may be stale
|
||||||
// or empty if the operator forgot to rebuild after .env changes.
|
// or empty if the operator forgot to rebuild after .env changes.
|
||||||
const hasSavedUrl = Boolean(gateway?.url?.trim());
|
const hasSavedUrl = Boolean(gateway?.url?.trim());
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import {
|
|||||||
defaultStudioSettings,
|
defaultStudioSettings,
|
||||||
mergeStudioSettings,
|
mergeStudioSettings,
|
||||||
normalizeStudioSettings,
|
normalizeStudioSettings,
|
||||||
|
type StudioGatewayAdapterType,
|
||||||
|
type StudioGatewayProfile,
|
||||||
|
type StudioGatewaySettings,
|
||||||
type StudioSettings,
|
type StudioSettings,
|
||||||
type StudioSettingsPatch,
|
type StudioSettingsPatch,
|
||||||
} from "@/lib/studio/settings";
|
} from "@/lib/studio/settings";
|
||||||
@@ -16,6 +19,7 @@ import {
|
|||||||
const SETTINGS_DIRNAME = "claw3d";
|
const SETTINGS_DIRNAME = "claw3d";
|
||||||
const SETTINGS_FILENAME = "settings.json";
|
const SETTINGS_FILENAME = "settings.json";
|
||||||
const OPENCLAW_CONFIG_FILENAME = "openclaw.json";
|
const OPENCLAW_CONFIG_FILENAME = "openclaw.json";
|
||||||
|
const DEFAULT_LOCAL_GATEWAY_PORT = 18789;
|
||||||
|
|
||||||
export const resolveStudioSettingsPath = () =>
|
export const resolveStudioSettingsPath = () =>
|
||||||
path.join(resolveStateDir(), SETTINGS_DIRNAME, SETTINGS_FILENAME);
|
path.join(resolveStateDir(), SETTINGS_DIRNAME, SETTINGS_FILENAME);
|
||||||
@@ -23,11 +27,21 @@ export const resolveStudioSettingsPath = () =>
|
|||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||||
Boolean(value && typeof value === "object");
|
Boolean(value && typeof value === "object");
|
||||||
|
|
||||||
const readOpenclawGatewayDefaults = (): {
|
const buildGatewaySettings = (params: {
|
||||||
|
adapterType: StudioGatewayAdapterType;
|
||||||
url: string;
|
url: string;
|
||||||
token: string;
|
token?: string;
|
||||||
adapterType: "openclaw";
|
profiles?: Partial<Record<StudioGatewayAdapterType, StudioGatewayProfile>>;
|
||||||
} | null => {
|
}): StudioGatewaySettings => ({
|
||||||
|
url: params.url,
|
||||||
|
token: params.token ?? "",
|
||||||
|
adapterType: params.adapterType,
|
||||||
|
...(params.profiles ? { profiles: params.profiles } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildLocalProfile = (url: string, token = ""): StudioGatewayProfile => ({ url, token });
|
||||||
|
|
||||||
|
const readOpenclawGatewayDefaults = (): StudioGatewaySettings | null => {
|
||||||
try {
|
try {
|
||||||
const configPath = path.join(resolveStateDir(), OPENCLAW_CONFIG_FILENAME);
|
const configPath = path.join(resolveStateDir(), OPENCLAW_CONFIG_FILENAME);
|
||||||
if (!fs.existsSync(configPath)) return null;
|
if (!fs.existsSync(configPath)) return null;
|
||||||
@@ -40,29 +54,110 @@ const readOpenclawGatewayDefaults = (): {
|
|||||||
const token = typeof auth?.token === "string" ? auth.token.trim() : "";
|
const token = typeof auth?.token === "string" ? auth.token.trim() : "";
|
||||||
const port = typeof gateway.port === "number" && Number.isFinite(gateway.port) ? gateway.port : null;
|
const port = typeof gateway.port === "number" && Number.isFinite(gateway.port) ? gateway.port : null;
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
const url = port ? `ws://localhost:${port}` : "";
|
const url = port ? `ws://localhost:${port}` : `ws://localhost:${DEFAULT_LOCAL_GATEWAY_PORT}`;
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
return { url, token, adapterType: "openclaw" };
|
return buildGatewaySettings({
|
||||||
|
adapterType: "openclaw",
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
profiles: {
|
||||||
|
openclaw: buildLocalProfile(url, token),
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadLocalGatewayDefaults = (): {
|
const normalizeAdapterType = (value: string | undefined): StudioGatewayAdapterType | null => {
|
||||||
url: string;
|
const normalized = value?.trim().toLowerCase();
|
||||||
token: string;
|
if (normalized === "openclaw" || normalized === "hermes" || normalized === "demo" || normalized === "custom") {
|
||||||
adapterType: "openclaw";
|
return normalized;
|
||||||
} | null => {
|
}
|
||||||
const fromFile = readOpenclawGatewayDefaults();
|
|
||||||
if (fromFile) return fromFile;
|
|
||||||
// Fall back to env vars so operators can configure the gateway URL at
|
|
||||||
// runtime without openclaw.json and without a rebuild.
|
|
||||||
const envUrl = process.env.CLAW3D_GATEWAY_URL?.trim();
|
|
||||||
const envToken = process.env.CLAW3D_GATEWAY_TOKEN?.trim();
|
|
||||||
if (envUrl) return { url: envUrl, token: envToken ?? "", adapterType: "openclaw" };
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readPortBasedGatewayProfile = (
|
||||||
|
adapterType: Extract<StudioGatewayAdapterType, "hermes" | "demo">,
|
||||||
|
envKey: "HERMES_ADAPTER_PORT" | "DEMO_ADAPTER_PORT"
|
||||||
|
): StudioGatewayProfile | null => {
|
||||||
|
const rawPort = process.env[envKey]?.trim();
|
||||||
|
if (!rawPort) return null;
|
||||||
|
const port = Number.parseInt(rawPort, 10);
|
||||||
|
if (!Number.isFinite(port) || port <= 0) return null;
|
||||||
|
return buildLocalProfile(`ws://localhost:${port}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildEnvGatewayDefaults = (): StudioGatewaySettings | null => {
|
||||||
|
const envUrl = process.env.CLAW3D_GATEWAY_URL?.trim();
|
||||||
|
const envToken = process.env.CLAW3D_GATEWAY_TOKEN?.trim() ?? "";
|
||||||
|
const envAdapterType =
|
||||||
|
normalizeAdapterType(process.env.CLAW3D_GATEWAY_ADAPTER_TYPE) ?? "openclaw";
|
||||||
|
|
||||||
|
const hermesProfile = readPortBasedGatewayProfile("hermes", "HERMES_ADAPTER_PORT");
|
||||||
|
const demoProfile = readPortBasedGatewayProfile("demo", "DEMO_ADAPTER_PORT");
|
||||||
|
|
||||||
|
const profiles: Partial<Record<StudioGatewayAdapterType, StudioGatewayProfile>> = {};
|
||||||
|
if (hermesProfile) profiles.hermes = hermesProfile;
|
||||||
|
if (demoProfile) profiles.demo = demoProfile;
|
||||||
|
|
||||||
|
if (envUrl) {
|
||||||
|
profiles[envAdapterType] = buildLocalProfile(envUrl, envToken);
|
||||||
|
return buildGatewaySettings({
|
||||||
|
adapterType: envAdapterType,
|
||||||
|
url: envUrl,
|
||||||
|
token: envToken,
|
||||||
|
profiles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackProfile = profiles.hermes ?? profiles.demo ?? null;
|
||||||
|
if (!fallbackProfile) return null;
|
||||||
|
const fallbackAdapterType = profiles.hermes ? "hermes" : "demo";
|
||||||
|
return buildGatewaySettings({
|
||||||
|
adapterType: fallbackAdapterType,
|
||||||
|
url: fallbackProfile.url,
|
||||||
|
token: fallbackProfile.token,
|
||||||
|
profiles,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergeGatewayProfiles = (
|
||||||
|
base: StudioGatewaySettings,
|
||||||
|
extra: StudioGatewaySettings | null
|
||||||
|
): StudioGatewaySettings => {
|
||||||
|
if (!extra?.profiles) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
const mergedProfiles: Partial<Record<StudioGatewayAdapterType, StudioGatewayProfile>> = {
|
||||||
|
...(base.profiles ?? {}),
|
||||||
|
};
|
||||||
|
for (const [adapterType, profile] of Object.entries(extra.profiles) as Array<
|
||||||
|
[StudioGatewayAdapterType, StudioGatewayProfile | undefined]
|
||||||
|
>) {
|
||||||
|
if (!profile || mergedProfiles[adapterType]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mergedProfiles[adapterType] = profile;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
profiles: mergedProfiles,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadLocalGatewayDefaults = (): StudioGatewaySettings | null => {
|
||||||
|
const fromFile = readOpenclawGatewayDefaults();
|
||||||
|
const fromEnv = buildEnvGatewayDefaults();
|
||||||
|
if (fromFile) {
|
||||||
|
return mergeGatewayProfiles(fromFile, fromEnv);
|
||||||
|
}
|
||||||
|
// Fall back to env vars so operators can configure the gateway URL at
|
||||||
|
// runtime without openclaw.json and without a rebuild. If no explicit
|
||||||
|
// URL is provided, also expose local Hermes/Demo adapter ports when set.
|
||||||
|
return fromEnv;
|
||||||
|
};
|
||||||
|
|
||||||
export const loadStudioSettings = (): StudioSettings => {
|
export const loadStudioSettings = (): StudioSettings => {
|
||||||
const settingsPath = resolveStudioSettingsPath();
|
const settingsPath = resolveStudioSettingsPath();
|
||||||
if (!fs.existsSync(settingsPath)) {
|
if (!fs.existsSync(settingsPath)) {
|
||||||
|
|||||||
@@ -1110,6 +1110,7 @@ describe("AgentSettingsPanel", () => {
|
|||||||
cronDeleteBusyJobId: null,
|
cronDeleteBusyJobId: null,
|
||||||
onRunCronJob: vi.fn(),
|
onRunCronJob: vi.fn(),
|
||||||
onDeleteCronJob: vi.fn(),
|
onDeleteCronJob: vi.fn(),
|
||||||
|
adapterType: "openclaw",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1117,6 +1118,29 @@ describe("AgentSettingsPanel", () => {
|
|||||||
expect(screen.getByText("Heartbeat automation controls are coming soon.")).toBeInTheDocument();
|
expect(screen.getByText("Heartbeat automation controls are coming soon.")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("hides_heartbeat_coming_soon_for_hermes", () => {
|
||||||
|
render(
|
||||||
|
createElement(AgentSettingsPanel, {
|
||||||
|
agent: createAgent(),
|
||||||
|
mode: "automations",
|
||||||
|
onClose: vi.fn(),
|
||||||
|
onDelete: vi.fn(),
|
||||||
|
onToolCallingToggle: vi.fn(),
|
||||||
|
onThinkingTracesToggle: vi.fn(),
|
||||||
|
cronJobs: [createCronJob("job-1")],
|
||||||
|
cronLoading: false,
|
||||||
|
cronError: null,
|
||||||
|
cronRunBusyJobId: null,
|
||||||
|
cronDeleteBusyJobId: null,
|
||||||
|
onRunCronJob: vi.fn(),
|
||||||
|
onDeleteCronJob: vi.fn(),
|
||||||
|
adapterType: "hermes",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("agent-settings-heartbeat-coming-soon")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("shows_control_ui_section_in_advanced_mode", () => {
|
it("shows_control_ui_section_in_advanced_mode", () => {
|
||||||
render(
|
render(
|
||||||
createElement(AgentSettingsPanel, {
|
createElement(AgentSettingsPanel, {
|
||||||
@@ -1133,6 +1157,7 @@ describe("AgentSettingsPanel", () => {
|
|||||||
cronDeleteBusyJobId: null,
|
cronDeleteBusyJobId: null,
|
||||||
onRunCronJob: vi.fn(),
|
onRunCronJob: vi.fn(),
|
||||||
onDeleteCronJob: vi.fn(),
|
onDeleteCronJob: vi.fn(),
|
||||||
|
adapterType: "openclaw",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1140,6 +1165,30 @@ describe("AgentSettingsPanel", () => {
|
|||||||
expect(screen.getByRole("button", { name: "Open Full Control UI" })).toBeDisabled();
|
expect(screen.getByRole("button", { name: "Open Full Control UI" })).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("hides_control_ui_section_for_hermes", () => {
|
||||||
|
render(
|
||||||
|
createElement(AgentSettingsPanel, {
|
||||||
|
agent: createAgent(),
|
||||||
|
mode: "advanced",
|
||||||
|
onClose: vi.fn(),
|
||||||
|
onDelete: vi.fn(),
|
||||||
|
onToolCallingToggle: vi.fn(),
|
||||||
|
onThinkingTracesToggle: vi.fn(),
|
||||||
|
cronJobs: [],
|
||||||
|
cronLoading: false,
|
||||||
|
cronError: null,
|
||||||
|
cronRunBusyJobId: null,
|
||||||
|
cronDeleteBusyJobId: null,
|
||||||
|
onRunCronJob: vi.fn(),
|
||||||
|
onDeleteCronJob: vi.fn(),
|
||||||
|
adapterType: "hermes",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("agent-settings-control-ui")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button", { name: "Open Full Control UI" })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders_enabled_control_ui_link_when_available", () => {
|
it("renders_enabled_control_ui_link_when_available", () => {
|
||||||
render(
|
render(
|
||||||
createElement(AgentSettingsPanel, {
|
createElement(AgentSettingsPanel, {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
describe("loadLocalGatewayDefaults with CLAW3D_GATEWAY_URL", () => {
|
describe("loadLocalGatewayDefaults with CLAW3D_GATEWAY_URL", () => {
|
||||||
const originalEnv = { ...process.env };
|
const originalEnv = { ...process.env };
|
||||||
@@ -17,7 +20,14 @@ describe("loadLocalGatewayDefaults with CLAW3D_GATEWAY_URL", () => {
|
|||||||
"../../src/lib/studio/settings-store"
|
"../../src/lib/studio/settings-store"
|
||||||
);
|
);
|
||||||
const result = loadLocalGatewayDefaults();
|
const result = loadLocalGatewayDefaults();
|
||||||
expect(result).toEqual({ url: "ws://my-gateway:18789", token: "my-token" });
|
expect(result).toEqual({
|
||||||
|
url: "ws://my-gateway:18789",
|
||||||
|
token: "my-token",
|
||||||
|
adapterType: "openclaw",
|
||||||
|
profiles: {
|
||||||
|
openclaw: { url: "ws://my-gateway:18789", token: "my-token" },
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns env-based defaults with empty token when only URL is set", async () => {
|
it("returns env-based defaults with empty token when only URL is set", async () => {
|
||||||
@@ -28,7 +38,14 @@ describe("loadLocalGatewayDefaults with CLAW3D_GATEWAY_URL", () => {
|
|||||||
"../../src/lib/studio/settings-store"
|
"../../src/lib/studio/settings-store"
|
||||||
);
|
);
|
||||||
const result = loadLocalGatewayDefaults();
|
const result = loadLocalGatewayDefaults();
|
||||||
expect(result).toEqual({ url: "ws://my-gateway:18789", token: "" });
|
expect(result).toEqual({
|
||||||
|
url: "ws://my-gateway:18789",
|
||||||
|
token: "",
|
||||||
|
adapterType: "openclaw",
|
||||||
|
profiles: {
|
||||||
|
openclaw: { url: "ws://my-gateway:18789", token: "" },
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null when no env var and no openclaw.json", async () => {
|
it("returns null when no env var and no openclaw.json", async () => {
|
||||||
@@ -57,4 +74,110 @@ describe("loadLocalGatewayDefaults with CLAW3D_GATEWAY_URL", () => {
|
|||||||
}
|
}
|
||||||
// If no file exists in CI, it falls back to env — that's also correct
|
// If no file exists in CI, it falls back to env — that's also correct
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses CLAW3D_GATEWAY_ADAPTER_TYPE for Hermes env defaults", async () => {
|
||||||
|
process.env.CLAW3D_GATEWAY_URL = "ws://my-hermes:18789";
|
||||||
|
process.env.CLAW3D_GATEWAY_ADAPTER_TYPE = "hermes";
|
||||||
|
delete process.env.CLAW3D_GATEWAY_TOKEN;
|
||||||
|
process.env.OPENCLAW_STATE_DIR = "/tmp/claw3d-test-nonexistent-" + Date.now();
|
||||||
|
const { loadLocalGatewayDefaults } = await import(
|
||||||
|
"../../src/lib/studio/settings-store"
|
||||||
|
);
|
||||||
|
const result = loadLocalGatewayDefaults();
|
||||||
|
expect(result).toEqual({
|
||||||
|
url: "ws://my-hermes:18789",
|
||||||
|
token: "",
|
||||||
|
adapterType: "hermes",
|
||||||
|
profiles: {
|
||||||
|
hermes: { url: "ws://my-hermes:18789", token: "" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes local Hermes adapter defaults when only HERMES_ADAPTER_PORT is set", async () => {
|
||||||
|
delete process.env.CLAW3D_GATEWAY_URL;
|
||||||
|
delete process.env.CLAW3D_GATEWAY_TOKEN;
|
||||||
|
process.env.HERMES_ADAPTER_PORT = "19444";
|
||||||
|
process.env.OPENCLAW_STATE_DIR = "/tmp/claw3d-test-nonexistent-" + Date.now();
|
||||||
|
const { loadLocalGatewayDefaults } = await import(
|
||||||
|
"../../src/lib/studio/settings-store"
|
||||||
|
);
|
||||||
|
const result = loadLocalGatewayDefaults();
|
||||||
|
expect(result).toEqual({
|
||||||
|
url: "ws://localhost:19444",
|
||||||
|
token: "",
|
||||||
|
adapterType: "hermes",
|
||||||
|
profiles: {
|
||||||
|
hermes: { url: "ws://localhost:19444", token: "" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("merges Hermes adapter defaults into file-backed OpenClaw defaults", async () => {
|
||||||
|
delete process.env.CLAW3D_GATEWAY_URL;
|
||||||
|
delete process.env.CLAW3D_GATEWAY_TOKEN;
|
||||||
|
delete process.env.CLAW3D_GATEWAY_ADAPTER_TYPE;
|
||||||
|
process.env.HERMES_ADAPTER_PORT = "19444";
|
||||||
|
|
||||||
|
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "claw3d-gateway-defaults-"));
|
||||||
|
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(stateDir, "openclaw.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
gateway: {
|
||||||
|
port: 18789,
|
||||||
|
auth: { token: "file-token" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { loadLocalGatewayDefaults } = await import(
|
||||||
|
"../../src/lib/studio/settings-store"
|
||||||
|
);
|
||||||
|
const result = loadLocalGatewayDefaults();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
url: "ws://localhost:18789",
|
||||||
|
token: "file-token",
|
||||||
|
adapterType: "openclaw",
|
||||||
|
profiles: {
|
||||||
|
openclaw: { url: "ws://localhost:18789", token: "file-token" },
|
||||||
|
hermes: { url: "ws://localhost:19444", token: "" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps file-backed openclaw profile when CLAW3D_GATEWAY_URL is also set", async () => {
|
||||||
|
process.env.CLAW3D_GATEWAY_URL = "ws://env-gateway:19999";
|
||||||
|
process.env.CLAW3D_GATEWAY_TOKEN = "env-token";
|
||||||
|
delete process.env.CLAW3D_GATEWAY_ADAPTER_TYPE;
|
||||||
|
|
||||||
|
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "claw3d-gateway-defaults-"));
|
||||||
|
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(stateDir, "openclaw.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
gateway: {
|
||||||
|
port: 18789,
|
||||||
|
auth: { token: "file-token" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { loadLocalGatewayDefaults } = await import(
|
||||||
|
"../../src/lib/studio/settings-store"
|
||||||
|
);
|
||||||
|
const result = loadLocalGatewayDefaults();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
url: "ws://localhost:18789",
|
||||||
|
token: "file-token",
|
||||||
|
adapterType: "openclaw",
|
||||||
|
profiles: {
|
||||||
|
openclaw: { url: "ws://localhost:18789", token: "file-token" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,8 +47,20 @@ describe("studio settings route", () => {
|
|||||||
|
|
||||||
const response = await GET();
|
const response = await GET();
|
||||||
const body = (await response.json()) as {
|
const body = (await response.json()) as {
|
||||||
settings?: { gateway?: { url?: string; tokenConfigured?: boolean } | null };
|
settings?: {
|
||||||
localGatewayDefaults?: { url?: string; tokenConfigured?: boolean } | null;
|
gateway?: {
|
||||||
|
url?: string;
|
||||||
|
tokenConfigured?: boolean;
|
||||||
|
adapterType?: string;
|
||||||
|
profiles?: Record<string, { url?: string; tokenConfigured?: boolean }>;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
localGatewayDefaults?: {
|
||||||
|
url?: string;
|
||||||
|
tokenConfigured?: boolean;
|
||||||
|
adapterType?: string;
|
||||||
|
profiles?: Record<string, { url?: string; tokenConfigured?: boolean }>;
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -56,11 +68,23 @@ describe("studio settings route", () => {
|
|||||||
url: "ws://localhost:18791",
|
url: "ws://localhost:18791",
|
||||||
tokenConfigured: true,
|
tokenConfigured: true,
|
||||||
adapterType: "openclaw",
|
adapterType: "openclaw",
|
||||||
|
profiles: {
|
||||||
|
openclaw: {
|
||||||
|
url: "ws://localhost:18791",
|
||||||
|
tokenConfigured: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(body.settings?.gateway).toEqual({
|
expect(body.settings?.gateway).toEqual({
|
||||||
url: "ws://localhost:18791",
|
url: "ws://localhost:18791",
|
||||||
tokenConfigured: true,
|
tokenConfigured: true,
|
||||||
adapterType: "openclaw",
|
adapterType: "openclaw",
|
||||||
|
profiles: {
|
||||||
|
openclaw: {
|
||||||
|
url: "ws://localhost:18791",
|
||||||
|
tokenConfigured: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user