huskies: merge 770
This commit is contained in:
@@ -132,38 +132,6 @@ describe("agentsApi", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("listAgents", () => {
|
||||
it("sends GET to /agents and returns agent list", async () => {
|
||||
mockFetch.mockResolvedValueOnce(okResponse([sampleAgent]));
|
||||
|
||||
const result = await agentsApi.listAgents();
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
"/api/agents",
|
||||
expect.objectContaining({}),
|
||||
);
|
||||
expect(result).toEqual([sampleAgent]);
|
||||
});
|
||||
|
||||
it("returns empty array when no agents running", async () => {
|
||||
mockFetch.mockResolvedValueOnce(okResponse([]));
|
||||
|
||||
const result = await agentsApi.listAgents();
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("uses custom baseUrl when provided", async () => {
|
||||
mockFetch.mockResolvedValueOnce(okResponse([]));
|
||||
|
||||
await agentsApi.listAgents("http://localhost:3002/api");
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
"http://localhost:3002/api/agents",
|
||||
expect.objectContaining({}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAgentConfig", () => {
|
||||
it("sends GET to /agents/config and returns config list", async () => {
|
||||
mockFetch.mockResolvedValueOnce(okResponse([sampleConfig]));
|
||||
@@ -216,15 +184,17 @@ describe("agentsApi", () => {
|
||||
|
||||
describe("error handling", () => {
|
||||
it("throws on non-ok response with body text", async () => {
|
||||
mockFetch.mockResolvedValueOnce(errorResponse(404, "agent not found"));
|
||||
mockFetch.mockResolvedValueOnce(errorResponse(404, "config not found"));
|
||||
|
||||
await expect(agentsApi.listAgents()).rejects.toThrow("agent not found");
|
||||
await expect(agentsApi.getAgentConfig()).rejects.toThrow(
|
||||
"config not found",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws with status code when no body", async () => {
|
||||
mockFetch.mockResolvedValueOnce(errorResponse(500, ""));
|
||||
|
||||
await expect(agentsApi.listAgents()).rejects.toThrow(
|
||||
await expect(agentsApi.getAgentConfig()).rejects.toThrow(
|
||||
"Request failed (500)",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { rpcCall } from "./rpc";
|
||||
|
||||
export type AgentStatusValue = "pending" | "running" | "completed" | "failed";
|
||||
|
||||
export interface AgentInfo {
|
||||
@@ -94,8 +96,8 @@ export const agentsApi = {
|
||||
);
|
||||
},
|
||||
|
||||
listAgents(baseUrl?: string) {
|
||||
return requestJson<AgentInfo[]>("/agents", {}, baseUrl);
|
||||
listAgents(_baseUrl?: string) {
|
||||
return rpcCall<AgentInfo[]>("active_agents.list");
|
||||
},
|
||||
|
||||
getAgentConfig(baseUrl?: string) {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Lightweight read-RPC client over the `/ws` WebSocket.
|
||||
*
|
||||
* Opens a short-lived WebSocket, sends an `rpc_request` frame, waits for the
|
||||
* matching `rpc_response`, then closes the connection.
|
||||
*/
|
||||
|
||||
let correlationCounter = 0;
|
||||
|
||||
function nextCorrelationId(): string {
|
||||
return `rpc-${Date.now()}-${++correlationCounter}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the WebSocket URL for the `/ws` endpoint, deriving the protocol
|
||||
* (ws/wss) and host from the current page location.
|
||||
*/
|
||||
function buildWsUrl(): string {
|
||||
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return `${proto}//${window.location.host}/ws`;
|
||||
}
|
||||
|
||||
export interface RpcResponse<T = unknown> {
|
||||
ok: boolean;
|
||||
result?: T;
|
||||
error?: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a read-RPC request over a temporary WebSocket connection and return
|
||||
* the result. Rejects if the server responds with `ok: false` or if the
|
||||
* connection times out.
|
||||
*/
|
||||
export function rpcCall<T = unknown>(
|
||||
method: string,
|
||||
params: Record<string, unknown> = {},
|
||||
timeoutMs = 5000,
|
||||
): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const correlationId = nextCorrelationId();
|
||||
const ws = new WebSocket(buildWsUrl());
|
||||
let settled = false;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
ws.close();
|
||||
reject(new Error(`RPC timeout for ${method}`));
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
kind: "rpc_request",
|
||||
version: 1,
|
||||
correlation_id: correlationId,
|
||||
ttl_ms: timeoutMs,
|
||||
method,
|
||||
params,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
// Only process rpc_response frames matching our correlation ID.
|
||||
if (
|
||||
data.kind === "rpc_response" &&
|
||||
data.correlation_id === correlationId
|
||||
) {
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
ws.close();
|
||||
if (data.ok) {
|
||||
resolve(data.result as T);
|
||||
} else {
|
||||
reject(
|
||||
new Error(data.error || `RPC error: ${data.code || "UNKNOWN"}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Ignore other messages (pipeline_state, onboarding_status, etc.)
|
||||
} catch {
|
||||
// Ignore non-JSON or unparseable messages
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
reject(new Error(`WebSocket error during RPC call to ${method}`));
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
reject(new Error(`WebSocket closed before RPC response for ${method}`));
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user