refactor(remote): extract shared registerWithCloud() helper
Registration HTTP logic was duplicated three times across listen.ts (initial + re-register) and listen.tsx. Extracted into listen-register.ts with proper error handling for non-JSON responses, response shape validation, and 5 focused tests. Removes ~110 lines of duplication. 🐛 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta <noreply@letta.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { hostname } from "node:os";
|
||||
import { getServerUrl } from "../../agent/client";
|
||||
import { settingsManager } from "../../settings-manager";
|
||||
import { getErrorMessage } from "../../utils/error";
|
||||
import { registerWithCloud } from "../../websocket/listen-register";
|
||||
import type { Buffers, Line } from "../helpers/accumulator";
|
||||
|
||||
// tiny helper for unique ids
|
||||
@@ -221,44 +222,14 @@ export async function handleListen(
|
||||
throw new Error("Missing LETTA_API_KEY");
|
||||
}
|
||||
|
||||
// Call register endpoint
|
||||
const registerUrl = `${serverUrl}/v1/environments/register`;
|
||||
const registerResponse = await fetch(registerUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"X-Letta-Source": "letta-code",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
deviceId,
|
||||
connectionName,
|
||||
}),
|
||||
// Register with cloud
|
||||
const { connectionId, wsUrl } = await registerWithCloud({
|
||||
serverUrl,
|
||||
apiKey,
|
||||
deviceId,
|
||||
connectionName,
|
||||
});
|
||||
|
||||
if (!registerResponse.ok) {
|
||||
let errorMessage = `Registration failed (HTTP ${registerResponse.status})`;
|
||||
try {
|
||||
const error = (await registerResponse.json()) as { message?: string };
|
||||
if (error.message) errorMessage = error.message;
|
||||
} catch {
|
||||
// Response body is not JSON (e.g. HTML error page from proxy)
|
||||
const text = await registerResponse.text().catch(() => "");
|
||||
if (text) errorMessage += `: ${text.slice(0, 200)}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
let registerBody: { connectionId: string; wsUrl: string };
|
||||
try {
|
||||
registerBody = (await registerResponse.json()) as typeof registerBody;
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Registration endpoint returned non-JSON response — is the server running?",
|
||||
);
|
||||
}
|
||||
const { connectionId, wsUrl } = registerBody;
|
||||
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
@@ -352,48 +323,17 @@ export async function handleListen(
|
||||
);
|
||||
|
||||
try {
|
||||
// Re-register to get new connectionId
|
||||
const reregisterResponse = await fetch(registerUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"X-Letta-Source": "letta-code",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
deviceId,
|
||||
connectionName,
|
||||
}),
|
||||
const reregisterResult = await registerWithCloud({
|
||||
serverUrl,
|
||||
apiKey,
|
||||
deviceId,
|
||||
connectionName,
|
||||
});
|
||||
|
||||
if (!reregisterResponse.ok) {
|
||||
let errorMessage = `Re-registration failed (HTTP ${reregisterResponse.status})`;
|
||||
try {
|
||||
const error = (await reregisterResponse.json()) as {
|
||||
message?: string;
|
||||
};
|
||||
if (error.message) errorMessage = error.message;
|
||||
} catch {
|
||||
const text = await reregisterResponse.text().catch(() => "");
|
||||
if (text) errorMessage += `: ${text.slice(0, 200)}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
let reregisterData: { connectionId: string; wsUrl: string };
|
||||
try {
|
||||
reregisterData =
|
||||
(await reregisterResponse.json()) as typeof reregisterData;
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Re-registration endpoint returned non-JSON response — is the server running?",
|
||||
);
|
||||
}
|
||||
|
||||
// Restart client with new connectionId
|
||||
await startClient(
|
||||
reregisterData.connectionId,
|
||||
reregisterData.wsUrl,
|
||||
reregisterResult.connectionId,
|
||||
reregisterResult.wsUrl,
|
||||
);
|
||||
} catch (error) {
|
||||
updateCommandResult(
|
||||
|
||||
@@ -11,6 +11,7 @@ import type React from "react";
|
||||
import { useState } from "react";
|
||||
import { getServerUrl } from "../../agent/client";
|
||||
import { settingsManager } from "../../settings-manager";
|
||||
import { registerWithCloud } from "../../websocket/listen-register";
|
||||
import { ListenerStatusUI } from "../components/ListenerStatusUI";
|
||||
|
||||
/**
|
||||
@@ -154,50 +155,22 @@ export async function runListenSubcommand(argv: string[]): Promise<number> {
|
||||
|
||||
// Register with cloud
|
||||
const serverUrl = getServerUrl();
|
||||
const registerUrl = `${serverUrl}/v1/environments/register`;
|
||||
|
||||
if (debugMode) {
|
||||
console.log(`[${formatTimestamp()}] Registering with ${registerUrl}`);
|
||||
console.log(
|
||||
`[${formatTimestamp()}] Registering with ${serverUrl}/v1/environments/register`,
|
||||
);
|
||||
console.log(`[${formatTimestamp()}] deviceId: ${deviceId}`);
|
||||
console.log(`[${formatTimestamp()}] connectionName: ${connectionName}`);
|
||||
}
|
||||
|
||||
const registerResponse = await fetch(registerUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"X-Letta-Source": "letta-code",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
deviceId,
|
||||
connectionName,
|
||||
}),
|
||||
const { connectionId, wsUrl } = await registerWithCloud({
|
||||
serverUrl,
|
||||
apiKey,
|
||||
deviceId,
|
||||
connectionName,
|
||||
});
|
||||
|
||||
if (!registerResponse.ok) {
|
||||
let errorMessage = `Registration failed (HTTP ${registerResponse.status})`;
|
||||
try {
|
||||
const error = (await registerResponse.json()) as { message?: string };
|
||||
if (error.message) errorMessage = error.message;
|
||||
} catch {
|
||||
const text = await registerResponse.text().catch(() => "");
|
||||
if (text) errorMessage += `: ${text.slice(0, 200)}`;
|
||||
}
|
||||
console.error(errorMessage);
|
||||
return 1;
|
||||
}
|
||||
|
||||
let registerBody: { connectionId: string; wsUrl: string };
|
||||
try {
|
||||
registerBody = (await registerResponse.json()) as typeof registerBody;
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Registration endpoint returned non-JSON response — is the server running?",
|
||||
);
|
||||
}
|
||||
const { connectionId, wsUrl } = registerBody;
|
||||
|
||||
if (debugMode) {
|
||||
console.log(`[${formatTimestamp()}] Registered successfully`);
|
||||
console.log(`[${formatTimestamp()}] connectionId: ${connectionId}`);
|
||||
|
||||
99
src/tests/websocket/listen-register.test.ts
Normal file
99
src/tests/websocket/listen-register.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { beforeEach, describe, expect, it, mock } from "bun:test";
|
||||
import { registerWithCloud } from "../../websocket/listen-register";
|
||||
|
||||
const defaultOpts = {
|
||||
serverUrl: "https://api.example.com",
|
||||
apiKey: "sk-test-key",
|
||||
deviceId: "device-123",
|
||||
connectionName: "test-machine",
|
||||
};
|
||||
|
||||
const mockFetch = mock(() => {
|
||||
throw new Error("fetch not mocked for this test");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetch.mockReset();
|
||||
globalThis.fetch = mockFetch as unknown as typeof globalThis.fetch;
|
||||
});
|
||||
|
||||
describe("registerWithCloud", () => {
|
||||
it("returns connectionId and wsUrl on successful JSON response", async () => {
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({ connectionId: "conn-1", wsUrl: "wss://example.com" }),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
),
|
||||
);
|
||||
|
||||
const result = await registerWithCloud(defaultOpts);
|
||||
|
||||
expect(result).toEqual({
|
||||
connectionId: "conn-1",
|
||||
wsUrl: "wss://example.com",
|
||||
});
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [url, init] = mockFetch.mock.calls[0] as unknown as [
|
||||
string,
|
||||
RequestInit,
|
||||
];
|
||||
expect(url).toBe("https://api.example.com/v1/environments/register");
|
||||
expect(init.method).toBe("POST");
|
||||
expect((init.headers as Record<string, string>).Authorization).toBe(
|
||||
"Bearer sk-test-key",
|
||||
);
|
||||
expect((init.headers as Record<string, string>)["X-Letta-Source"]).toBe(
|
||||
"letta-code",
|
||||
);
|
||||
const body = JSON.parse(init.body as string);
|
||||
expect(body).toEqual({
|
||||
deviceId: "device-123",
|
||||
connectionName: "test-machine",
|
||||
});
|
||||
});
|
||||
|
||||
it("throws with body message on non-OK response with JSON error", async () => {
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ message: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(registerWithCloud(defaultOpts)).rejects.toThrow(
|
||||
"Unauthorized",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws with HTTP status and truncated body on non-OK non-JSON response", async () => {
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
new Response("<html>Bad Gateway</html>", { status: 502 }),
|
||||
);
|
||||
|
||||
await expect(registerWithCloud(defaultOpts)).rejects.toThrow(
|
||||
"HTTP 502: <html>Bad Gateway</html>",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws actionable message on 200 with non-JSON body", async () => {
|
||||
mockFetch.mockResolvedValueOnce(new Response("OK", { status: 200 }));
|
||||
|
||||
await expect(registerWithCloud(defaultOpts)).rejects.toThrow(
|
||||
"is the server running?",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws on unexpected response shape (missing fields)", async () => {
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ connectionId: "conn-1" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(registerWithCloud(defaultOpts)).rejects.toThrow(
|
||||
"missing connectionId or wsUrl",
|
||||
);
|
||||
});
|
||||
});
|
||||
81
src/websocket/listen-register.ts
Normal file
81
src/websocket/listen-register.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Shared registration helper for letta remote / /remote command.
|
||||
* Owns the HTTP request contract and error handling; callers own UX strings and logging.
|
||||
*/
|
||||
|
||||
export interface RegisterResult {
|
||||
connectionId: string;
|
||||
wsUrl: string;
|
||||
}
|
||||
|
||||
export interface RegisterOptions {
|
||||
serverUrl: string;
|
||||
apiKey: string;
|
||||
deviceId: string;
|
||||
connectionName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this device with the Letta Cloud environments endpoint.
|
||||
* Throws on any failure with an error message suitable for wrapping in caller-specific context.
|
||||
*/
|
||||
export async function registerWithCloud(
|
||||
opts: RegisterOptions,
|
||||
): Promise<RegisterResult> {
|
||||
const registerUrl = `${opts.serverUrl}/v1/environments/register`;
|
||||
|
||||
const response = await fetch(registerUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${opts.apiKey}`,
|
||||
"X-Letta-Source": "letta-code",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
deviceId: opts.deviceId,
|
||||
connectionName: opts.connectionName,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let detail = `HTTP ${response.status}`;
|
||||
const text = await response.text().catch(() => "");
|
||||
if (text) {
|
||||
try {
|
||||
const parsed = JSON.parse(text) as { message?: string };
|
||||
if (parsed.message) {
|
||||
detail = parsed.message;
|
||||
} else {
|
||||
detail += `: ${text.slice(0, 200)}`;
|
||||
}
|
||||
} catch {
|
||||
detail += `: ${text.slice(0, 200)}`;
|
||||
}
|
||||
}
|
||||
throw new Error(detail);
|
||||
}
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await response.json();
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Server returned non-JSON response — is the server running?",
|
||||
);
|
||||
}
|
||||
|
||||
const result = body as Record<string, unknown>;
|
||||
if (
|
||||
typeof result.connectionId !== "string" ||
|
||||
typeof result.wsUrl !== "string"
|
||||
) {
|
||||
throw new Error(
|
||||
"Server returned unexpected response shape (missing connectionId or wsUrl)",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
connectionId: result.connectionId,
|
||||
wsUrl: result.wsUrl,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user