fix: resolve gateway URL at runtime via /api/studio fallback (#66)
Fixes #57 — NEXT_PUBLIC_GATEWAY_URL is a build-time variable that gets baked into the client bundle. Changing it in .env and restarting has no effect without a rebuild. - normalizeLocalGatewayDefaults now accepts the sanitized public form ({url, tokenConfigured}) from /api/studio - When no saved gateway URL exists, prefer runtime localGatewayDefaults (from openclaw.json or CLAW3D_GATEWAY_URL env var) over the potentially stale build-time NEXT_PUBLIC_GATEWAY_URL - loadLocalGatewayDefaults falls back to CLAW3D_GATEWAY_URL/TOKEN env vars when openclaw.json is absent - Added runtime env vars documentation to .env.example and README Co-authored-by: robotica4us-collab <neo@openclaw.ai> Made-with: Cursor
This commit is contained in:
committed by
iamlukethedev
parent
456cfae771
commit
e71b62444c
@@ -0,0 +1,60 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("loadLocalGatewayDefaults with CLAW3D_GATEWAY_URL", () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("returns env-based defaults when CLAW3D_GATEWAY_URL is set and no openclaw.json exists", async () => {
|
||||
process.env.CLAW3D_GATEWAY_URL = "ws://my-gateway:18789";
|
||||
process.env.CLAW3D_GATEWAY_TOKEN = "my-token";
|
||||
// Point state dir to a non-existent location so openclaw.json is not found
|
||||
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-gateway:18789", token: "my-token" });
|
||||
});
|
||||
|
||||
it("returns env-based defaults with empty token when only URL is set", async () => {
|
||||
process.env.CLAW3D_GATEWAY_URL = "ws://my-gateway:18789";
|
||||
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-gateway:18789", token: "" });
|
||||
});
|
||||
|
||||
it("returns null when no env var and no openclaw.json", async () => {
|
||||
delete process.env.CLAW3D_GATEWAY_URL;
|
||||
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).toBeNull();
|
||||
});
|
||||
|
||||
it("prefers openclaw.json over env vars when both exist", async () => {
|
||||
process.env.CLAW3D_GATEWAY_URL = "ws://env-gateway:18789";
|
||||
process.env.CLAW3D_GATEWAY_TOKEN = "env-token";
|
||||
// Use real state dir which has openclaw.json
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
const { loadLocalGatewayDefaults } = await import(
|
||||
"../../src/lib/studio/settings-store"
|
||||
);
|
||||
const result = loadLocalGatewayDefaults();
|
||||
// Should return the file-based defaults, not the env vars
|
||||
if (result) {
|
||||
expect(result.url).not.toBe("ws://env-gateway:18789");
|
||||
}
|
||||
// If no file exists in CI, it falls back to env — that's also correct
|
||||
});
|
||||
});
|
||||
@@ -154,6 +154,56 @@ describe("useGatewayConnection", () => {
|
||||
expect(captured.authScopeKey).toBe("ws://localhost:18789");
|
||||
});
|
||||
|
||||
it("auto_applies_runtime_local_defaults_when_no_saved_gateway_and_build_time_empty", async () => {
|
||||
// Simulates #57: NEXT_PUBLIC_GATEWAY_URL was never rebuilt, but
|
||||
// CLAW3D_GATEWAY_URL is set on the server so localGatewayDefaults
|
||||
// comes through in the sanitized (public) form with tokenConfigured.
|
||||
const { useGatewayConnection } = await setupAndImportHook("");
|
||||
const coordinator = {
|
||||
loadSettings: async () => null,
|
||||
loadSettingsEnvelope: async () => ({
|
||||
settings: {
|
||||
version: 1,
|
||||
gateway: null, // no saved gateway settings
|
||||
focused: {},
|
||||
avatars: {},
|
||||
analytics: {},
|
||||
voiceReplies: {},
|
||||
office: {},
|
||||
deskAssignments: {},
|
||||
standup: {},
|
||||
},
|
||||
// Sanitized public form — token is replaced with tokenConfigured
|
||||
localGatewayDefaults: { url: "ws://my-server:18789", tokenConfigured: true },
|
||||
}),
|
||||
schedulePatch: () => {},
|
||||
flushPending: async () => {},
|
||||
};
|
||||
|
||||
const Probe = () => {
|
||||
const state = useGatewayConnection(coordinator);
|
||||
return createElement(
|
||||
"div",
|
||||
null,
|
||||
createElement("div", { "data-testid": "gatewayUrl" }, state.gatewayUrl),
|
||||
createElement(
|
||||
"div",
|
||||
{ "data-testid": "localDefaultsUrl" },
|
||||
state.localGatewayDefaults?.url ?? ""
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
render(createElement(Probe));
|
||||
|
||||
// The runtime local defaults should be auto-applied since there are
|
||||
// no saved settings and the build-time default is empty.
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("gatewayUrl")).toHaveTextContent("ws://my-server:18789");
|
||||
});
|
||||
expect(screen.getByTestId("localDefaultsUrl")).toHaveTextContent("ws://my-server:18789");
|
||||
});
|
||||
|
||||
it("applies_local_defaults_from_settings_envelope", async () => {
|
||||
const { useGatewayConnection } = await setupAndImportHook(null);
|
||||
const coordinator = {
|
||||
|
||||
Reference in New Issue
Block a user