Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 174 additions & 7 deletions src/__tests__/unit/services/swarm/api/nodes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ beforeEach(() => {
global.fetch = mockFetch;
});

const { addNode, addEdge } = await import("@/services/swarm/api/nodes");
const { addNode, addEdge, addEdgeBulk } = await import("@/services/swarm/api/nodes");

const config = {
jarvisUrl: "https://test-swarm.sphinx.chat:8444",
Expand Down Expand Up @@ -41,7 +41,7 @@ describe("addNode", () => {
expect(result).toEqual({ success: true, ref_id: "node-abc-123" });

expect(mockFetch).toHaveBeenCalledWith(
"https://test-swarm.sphinx.chat:8444/v2/nodes",
"https://test-swarm.sphinx.chat:8444/node",
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
Expand Down Expand Up @@ -215,7 +215,7 @@ describe("addNode", () => {
);

const calledUrl = mockFetch.mock.calls[0][0] as string;
expect(calledUrl).toBe("https://test.sphinx.chat:8444/v2/nodes");
expect(calledUrl).toBe("https://test.sphinx.chat:8444/node");
});
});
});
Expand All @@ -232,12 +232,12 @@ describe("addEdge", () => {
};

describe("Success cases", () => {
test("calls POST /v2/edges with correct body and headers", async () => {
test("calls POST /node/edge with correct body and headers", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
status: "success",
status: "Success",
status_messages: [],
}),
});
Expand All @@ -247,7 +247,7 @@ describe("addEdge", () => {
expect(result).toEqual({ success: true });

expect(mockFetch).toHaveBeenCalledWith(
"https://test-swarm.sphinx.chat:8444/v2/edges",
"https://test-swarm.sphinx.chat:8444/node/edge",
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
Expand All @@ -258,11 +258,23 @@ describe("addEdge", () => {
);
});

test("returns success with capital-S 'Success' status from /node/edge", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ status: "Success", status_messages: [] }),
});

const result = await addEdge(config, edgePayload);

expect(result).toEqual({ success: true });
});

test("works with EVAL_RUN edge type (no edge_data)", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ status: "success", status_messages: [] }),
json: async () => ({ status: "Success", status_messages: [] }),
});

const result = await addEdge(config, {
Expand Down Expand Up @@ -332,3 +344,158 @@ describe("addEdge", () => {
});
});
});

// ---------------------------------------------------------------------------
// addEdgeBulk
// ---------------------------------------------------------------------------

describe("addEdgeBulk", () => {
const edgeList = [
{
edge: { edge_type: "HAS_REQUIREMENT" },
source: { ref_id: "eval-set-1" },
target: { ref_id: "req-1" },
},
{
edge: { edge_type: "HAS_REQUIREMENT" },
source: { ref_id: "eval-set-1" },
target: { ref_id: "req-2" },
},
];

describe("Success cases", () => {
test("calls POST /node/edge/bulk with correct body and headers", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
status: "Success",
status_messages: [],
}),
});

const result = await addEdgeBulk(config, edgeList);

expect(result).toEqual({ success: true, errors: [] });

expect(mockFetch).toHaveBeenCalledWith(
"https://test-swarm.sphinx.chat:8444/node/edge/bulk",
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
"x-api-token": "test-api-key",
"Content-Type": "application/json",
}),
body: JSON.stringify({ edge_list: edgeList }),
}),
);
});

test("returns success with no errors when status_messages is empty", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ status: "Success", status_messages: [] }),
});

const result = await addEdgeBulk(config, edgeList);

expect(result.success).toBe(true);
expect(result.errors).toHaveLength(0);
});

test("returns success:true with Warning status when no error messages", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
status: "Warning",
status_messages: ["Edge already exists"],
}),
});

const result = await addEdgeBulk(config, edgeList);

expect(result.success).toBe(false); // Warning is not "success"
expect(result.errors).toHaveLength(0); // no "error" prefixed messages
});
});

describe("Partial errors surfaced from status_messages", () => {
test("collects error-prefixed status_messages as errors", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
status: "Success",
status_messages: [
"Error: invalid ref_id for item 2",
"Edge created successfully",
"error: missing source node",
],
}),
});

const result = await addEdgeBulk(config, edgeList);

expect(result.success).toBe(true);
expect(result.errors).toEqual([
"Error: invalid ref_id for item 2",
"error: missing source node",
]);
});

test("ignores non-error status_messages", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
status: "Success",
status_messages: ["Edge created", "Already exists (skipped)"],
}),
});

const result = await addEdgeBulk(config, edgeList);

expect(result.errors).toHaveLength(0);
});
});

describe("Failure cases", () => {
test("returns failure when HTTP response is not ok", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
text: async () => "Internal server error",
});

const result = await addEdgeBulk(config, edgeList);

expect(result.success).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toContain("500");
});

test("returns failure when fetch throws", async () => {
mockFetch.mockRejectedValueOnce(new Error("Network timeout"));

const result = await addEdgeBulk(config, edgeList);

expect(result.success).toBe(false);
expect(result.errors[0]).toBe("Network timeout");
});

test("handles empty edge list gracefully", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ status: "Success", status_messages: [] }),
});

const result = await addEdgeBulk(config, []);

expect(result.success).toBe(true);
expect(result.errors).toHaveLength(0);
});
});
});
42 changes: 39 additions & 3 deletions src/services/swarm/api/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export async function addNode(
): Promise<{ success: boolean; ref_id?: string; error?: string }> {
const result = await jarvisRequest({
config,
endpoint: "/v2/nodes",
endpoint: "/node",
method: "POST",
data: payload,
});
Expand Down Expand Up @@ -117,7 +117,7 @@ export async function addEdge(
): Promise<{ success: boolean; error?: string }> {
const result = await jarvisRequest({
config,
endpoint: "/v2/edges",
endpoint: "/node/edge",
method: "POST",
data: payload,
});
Expand All @@ -137,13 +137,49 @@ export async function addEdge(
m.toLowerCase().includes("already exists"),
);

if (body?.status === "success" || isAlreadyExists) {
if (body?.status?.toLowerCase() === "success" || isAlreadyExists) {
return { success: true };
}

return { success: false, error: "Edge creation returned unexpected status" };
}

export async function addEdgeBulk(
config: JarvisConnectionConfig,
edgeList: Array<{
edge: { edge_type: string; weight?: number; edge_data?: Record<string, unknown> };
source: { ref_id: string };
target: { ref_id: string };
}>,
): Promise<{ success: boolean; errors: string[] }> {
const result = await jarvisRequest({
config,
endpoint: "/node/edge/bulk",
method: "POST",
data: { edge_list: edgeList },
});

if (!result.ok) {
return {
success: false,
errors: [result.error || `Failed to create edges (status: ${result.status})`],
};
}

const body = result.body as
| { status?: string; status_messages?: string[] }
| undefined;

const errors = (body?.status_messages ?? []).filter((m) =>
m.toLowerCase().startsWith("error"),
);

return {
success: body?.status?.toLowerCase() === "success",
errors,
};
}

export async function updateNode(
config: JarvisConnectionConfig,
request: UpdateNodeRequest,
Expand Down
Loading