From e69681b251551ce769ef0494ec68e49c00bb849a Mon Sep 17 00:00:00 2001 From: tomsmith8 Date: Tue, 26 May 2026 11:06:42 +0000 Subject: [PATCH] Generated with Hive: Add CRUD service helpers, API routes, and mock endpoints for eval sets and requirements --- .../api/workspaces/evals/evals.test.ts | 433 ++++++++++++++++++ .../api/workspaces/nodes/update-node.test.ts | 84 ++-- .../unit/services/swarm/api/nodes.test.ts | 253 +++++++++- .../[evalSetId]/requirements/[reqId]/route.ts | 15 + src/app/api/mock/evals/[evalSetId]/route.ts | 15 + .../[evalSetId]/requirements/[reqId]/route.ts | 136 ++++++ .../[slug]/evals/[evalSetId]/route.ts | 113 +++++ .../workspaces/[slug]/nodes/[nodeId]/route.ts | 12 +- src/services/swarm/api/nodes.ts | 75 ++- src/types/jarvis.ts | 3 +- 10 files changed, 1103 insertions(+), 36 deletions(-) create mode 100644 src/app/api/mock/evals/[evalSetId]/requirements/[reqId]/route.ts create mode 100644 src/app/api/mock/evals/[evalSetId]/route.ts create mode 100644 src/app/api/workspaces/[slug]/evals/[evalSetId]/requirements/[reqId]/route.ts create mode 100644 src/app/api/workspaces/[slug]/evals/[evalSetId]/route.ts diff --git a/src/__tests__/integration/api/workspaces/evals/evals.test.ts b/src/__tests__/integration/api/workspaces/evals/evals.test.ts index 8e38f01baa..0f36ef08ca 100644 --- a/src/__tests__/integration/api/workspaces/evals/evals.test.ts +++ b/src/__tests__/integration/api/workspaces/evals/evals.test.ts @@ -1,6 +1,8 @@ import { describe, test, expect, beforeEach, vi } from "vitest"; import { GET as getEvalSets, POST as createEvalSet } from "@/app/api/workspaces/[slug]/evals/route"; +import { PUT as updateEvalSet, DELETE as deleteEvalSet } from "@/app/api/workspaces/[slug]/evals/[evalSetId]/route"; import { POST as createRequirement } from "@/app/api/workspaces/[slug]/evals/[evalSetId]/requirements/route"; +import { PUT as updateRequirement, DELETE as deleteRequirement } from "@/app/api/workspaces/[slug]/evals/[evalSetId]/requirements/[reqId]/route"; import { POST as linkRuns } from "@/app/api/workspaces/[slug]/evals/[evalSetId]/requirements/[reqId]/runs/route"; import { GET as getSessions } from "@/app/api/workspaces/[slug]/evals/sessions/route"; import { @@ -12,8 +14,11 @@ import { import { createAuthenticatedGetRequest, createAuthenticatedPostRequest, + createAuthenticatedPutRequest, + createAuthenticatedDeleteRequest, createGetRequest, createPostRequest, + createDeleteRequest, } from "@/__tests__/support/helpers/request-builders"; import { expectSuccess, @@ -890,4 +895,432 @@ describe("Evals API — Integration Tests", () => { }); }); }); + + // --------------------------------------------------------------------------- + // PUT /api/workspaces/[slug]/evals/[evalSetId] + // --------------------------------------------------------------------------- + describe("PUT /api/workspaces/[slug]/evals/[evalSetId]", () => { + describe("Success", () => { + test("updates eval set with valid name and description", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestMembership({ workspaceId: workspace.id, userId: owner.id, role: "OWNER" }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + vi.mocked(nodesService.updateNode).mockResolvedValueOnce({ success: true }); + + const request = createAuthenticatedPutRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-1`, + owner, + { name: "Updated Name", description: "Updated desc" }, + ); + + const response = await updateEvalSet(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-1" }), + }); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.success).toBe(true); + expect(nodesService.updateNode).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + ref_id: "eval-1", + node_type: "EvalSet", + node_data: expect.objectContaining({ name: "Updated Name" }), + }), + ); + }); + }); + + describe("Validation", () => { + test("returns 400 when name is missing", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestMembership({ workspaceId: workspace.id, userId: owner.id, role: "OWNER" }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + const request = createAuthenticatedPutRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-1`, + owner, + { description: "No name provided" }, + ); + + const response = await updateEvalSet(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-1" }), + }); + + await expectError(response, "name is required", 400); + }); + + test("returns 400 when name is empty string", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestMembership({ workspaceId: workspace.id, userId: owner.id, role: "OWNER" }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + const request = createAuthenticatedPutRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-1`, + owner, + { name: " " }, + ); + + const response = await updateEvalSet(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-1" }), + }); + + await expectError(response, "name is required", 400); + }); + }); + + describe("Auth failures", () => { + test("rejects unauthenticated requests", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + + const request = createAuthenticatedPutRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-1`, + owner, + { name: "Test" }, + ); + // Simulate unauthenticated by creating a plain PUT + const unauthRequest = createDeleteRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-1`, + ); + // Use a user not in the workspace + const nonMember = await createTestUser(); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + const authRequest = createAuthenticatedPutRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-1`, + nonMember, + { name: "Test" }, + ); + + const response = await updateEvalSet(authRequest, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-1" }), + }); + + await expectForbidden(response, "Access denied"); + }); + }); + + describe("Service failures", () => { + test("returns 502 when updateNode fails", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestMembership({ workspaceId: workspace.id, userId: owner.id, role: "OWNER" }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + vi.mocked(nodesService.updateNode).mockResolvedValueOnce({ + success: false, + error: "Jarvis unavailable", + }); + + const request = createAuthenticatedPutRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-1`, + owner, + { name: "Updated" }, + ); + + const response = await updateEvalSet(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-1" }), + }); + + expect(response.status).toBe(502); + }); + }); + }); + + // --------------------------------------------------------------------------- + // DELETE /api/workspaces/[slug]/evals/[evalSetId] + // --------------------------------------------------------------------------- + describe("DELETE /api/workspaces/[slug]/evals/[evalSetId]", () => { + describe("Success", () => { + test("deletes eval set successfully", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestMembership({ workspaceId: workspace.id, userId: owner.id, role: "OWNER" }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + vi.mocked(nodesService.deleteNode).mockResolvedValueOnce({ success: true }); + + const request = createAuthenticatedDeleteRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-1`, + owner, + ); + + const response = await deleteEvalSet(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-1" }), + }); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.success).toBe(true); + expect(nodesService.deleteNode).toHaveBeenCalledWith(expect.any(Object), "eval-1"); + }); + }); + + describe("Auth failures", () => { + test("rejects non-member", async () => { + const owner = await createTestUser(); + const nonMember = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + const request = createAuthenticatedDeleteRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-1`, + nonMember, + ); + + const response = await deleteEvalSet(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-1" }), + }); + + await expectForbidden(response, "Access denied"); + }); + }); + + describe("Service failures", () => { + test("returns 502 when deleteNode fails", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestMembership({ workspaceId: workspace.id, userId: owner.id, role: "OWNER" }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + vi.mocked(nodesService.deleteNode).mockResolvedValueOnce({ + success: false, + error: "Delete failed", + }); + + const request = createAuthenticatedDeleteRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-1`, + owner, + ); + + const response = await deleteEvalSet(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-1" }), + }); + + expect(response.status).toBe(502); + }); + }); + }); + + // --------------------------------------------------------------------------- + // PUT /api/workspaces/[slug]/evals/[evalSetId]/requirements/[reqId] + // --------------------------------------------------------------------------- + describe("PUT /api/workspaces/[slug]/evals/[evalSetId]/requirements/[reqId]", () => { + const validReqBody = { + name: "Check output", + description: "Verifies correct output", + prompt_snippet: "Summarize this text", + positive_cases: ["Good summary"], + negative_cases: ["Bad summary"], + }; + + describe("Success", () => { + test("updates requirement with valid fields", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestMembership({ workspaceId: workspace.id, userId: owner.id, role: "OWNER" }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + vi.mocked(nodesService.updateNode).mockResolvedValueOnce({ success: true }); + + const request = createAuthenticatedPutRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/set-1/requirements/req-1`, + owner, + validReqBody, + ); + + const response = await updateRequirement(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "set-1", reqId: "req-1" }), + }); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.success).toBe(true); + expect(nodesService.updateNode).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + ref_id: "req-1", + node_type: "EvalRequirement", + node_data: expect.objectContaining({ + name: "Check output", + prompt_snippet: "Summarize this text", + }), + }), + ); + }); + }); + + describe("Validation", () => { + test("returns 400 when name is missing", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestMembership({ workspaceId: workspace.id, userId: owner.id, role: "OWNER" }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + const request = createAuthenticatedPutRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/set-1/requirements/req-1`, + owner, + { ...validReqBody, name: "" }, + ); + + const response = await updateRequirement(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "set-1", reqId: "req-1" }), + }); + + await expectError(response, "name is required", 400); + }); + + test("returns 400 when prompt_snippet is missing", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestMembership({ workspaceId: workspace.id, userId: owner.id, role: "OWNER" }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + const request = createAuthenticatedPutRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/set-1/requirements/req-1`, + owner, + { ...validReqBody, prompt_snippet: "" }, + ); + + const response = await updateRequirement(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "set-1", reqId: "req-1" }), + }); + + await expectError(response, "prompt_snippet is required", 400); + }); + + test("returns 400 when positive_cases is empty", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestMembership({ workspaceId: workspace.id, userId: owner.id, role: "OWNER" }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + const request = createAuthenticatedPutRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/set-1/requirements/req-1`, + owner, + { ...validReqBody, positive_cases: [] }, + ); + + const response = await updateRequirement(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "set-1", reqId: "req-1" }), + }); + + await expectError(response, "positive_cases must be a non-empty array", 400); + }); + + test("returns 400 when negative_cases is empty", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestMembership({ workspaceId: workspace.id, userId: owner.id, role: "OWNER" }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + const request = createAuthenticatedPutRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/set-1/requirements/req-1`, + owner, + { ...validReqBody, negative_cases: [] }, + ); + + const response = await updateRequirement(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "set-1", reqId: "req-1" }), + }); + + await expectError(response, "negative_cases must be a non-empty array", 400); + }); + }); + + describe("Auth failures", () => { + test("rejects non-member", async () => { + const owner = await createTestUser(); + const nonMember = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + const request = createAuthenticatedPutRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/set-1/requirements/req-1`, + nonMember, + validReqBody, + ); + + const response = await updateRequirement(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "set-1", reqId: "req-1" }), + }); + + await expectForbidden(response, "Access denied"); + }); + }); + }); + + // --------------------------------------------------------------------------- + // DELETE /api/workspaces/[slug]/evals/[evalSetId]/requirements/[reqId] + // --------------------------------------------------------------------------- + describe("DELETE /api/workspaces/[slug]/evals/[evalSetId]/requirements/[reqId]", () => { + describe("Success", () => { + test("deletes requirement successfully", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestMembership({ workspaceId: workspace.id, userId: owner.id, role: "OWNER" }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + vi.mocked(nodesService.deleteNode).mockResolvedValueOnce({ success: true }); + + const request = createAuthenticatedDeleteRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/set-1/requirements/req-1`, + owner, + ); + + const response = await deleteRequirement(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "set-1", reqId: "req-1" }), + }); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.success).toBe(true); + expect(nodesService.deleteNode).toHaveBeenCalledWith(expect.any(Object), "req-1"); + }); + }); + + describe("Auth failures", () => { + test("rejects non-member", async () => { + const owner = await createTestUser(); + const nonMember = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-key" }); + + const request = createAuthenticatedDeleteRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/set-1/requirements/req-1`, + nonMember, + ); + + const response = await deleteRequirement(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "set-1", reqId: "req-1" }), + }); + + await expectForbidden(response, "Access denied"); + }); + }); + + describe("Swarm not configured", () => { + test("returns 400 when workspace has no swarm", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + await createTestMembership({ workspaceId: workspace.id, userId: owner.id, role: "OWNER" }); + + const request = createAuthenticatedDeleteRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/set-1/requirements/req-1`, + owner, + ); + + const response = await deleteRequirement(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "set-1", reqId: "req-1" }), + }); + + await expectError(response, "Swarm not configured", 400); + }); + }); + }); }); diff --git a/src/__tests__/integration/api/workspaces/nodes/update-node.test.ts b/src/__tests__/integration/api/workspaces/nodes/update-node.test.ts index 46cef37770..a6e00b5afe 100644 --- a/src/__tests__/integration/api/workspaces/nodes/update-node.test.ts +++ b/src/__tests__/integration/api/workspaces/nodes/update-node.test.ts @@ -46,7 +46,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-123"; const updateData = { - properties: { + node_type: "TestNode", + node_data: { name: "Updated Node", description: "Updated description", }, @@ -81,7 +82,8 @@ describe("Node Update API - Integration Tests", () => { }), expect.objectContaining({ ref_id: nodeId, - properties: updateData.properties, + node_type: updateData.node_type, + node_data: updateData.node_data, }) ); }); @@ -99,7 +101,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-456"; const updateData = { - properties: { + node_type: "TestNode", + node_data: { status: "active", metadata: { version: "2.0" }, }, @@ -137,7 +140,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-789"; const updateData = { - properties: { + node_type: "TestNode", + node_data: { name: "Complete Node", type: "service", config: { @@ -179,7 +183,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-dev-123"; const updateData = { - properties: { name: "Developer Update" }, + node_type: "TestNode", + node_data: { name: "Developer Update" }, }; vi.mocked(nodesService.updateNode).mockResolvedValue({ success: true }); @@ -211,7 +216,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-viewer-123"; const updateData = { - properties: { name: "Viewer Update" }, + node_type: "TestNode", + node_data: { name: "Viewer Update" }, }; vi.mocked(nodesService.updateNode).mockResolvedValue({ success: true }); @@ -243,7 +249,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-pm-123"; const updateData = { - properties: { name: "PM Update" }, + node_type: "TestNode", + node_data: { name: "PM Update" }, }; vi.mocked(nodesService.updateNode).mockResolvedValue({ success: true }); @@ -270,7 +277,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-non-member-123"; const updateData = { - properties: { name: "Unauthorized Update" }, + node_type: "TestNode", + node_data: { name: "Unauthorized Update" }, }; const request = createAuthenticatedPutRequest( @@ -291,7 +299,8 @@ describe("Node Update API - Integration Tests", () => { const user = await createTestUser(); const nodeId = "node-404"; const updateData = { - properties: { name: "Update" }, + node_type: "TestNode", + node_data: { name: "Update" }, }; const request = createAuthenticatedPutRequest( @@ -328,7 +337,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-deleted-ws"; const updateData = { - properties: { name: "Update" }, + node_type: "TestNode", + node_data: { name: "Update" }, }; const request = createAuthenticatedPutRequest( @@ -359,7 +369,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-left-member"; const updateData = { - properties: { name: "Update" }, + node_type: "TestNode", + node_data: { name: "Update" }, }; const request = createAuthenticatedPutRequest( @@ -381,7 +392,8 @@ describe("Node Update API - Integration Tests", () => { const workspace = await createTestWorkspace({ ownerId: owner.id }); const nodeId = "node-unauth"; const updateData = { - properties: { name: "Update" }, + node_type: "TestNode", + node_data: { name: "Update" }, }; const request = createPutRequest( @@ -399,7 +411,7 @@ describe("Node Update API - Integration Tests", () => { }); describe("Validation Failures", () => { - test("rejects request with missing properties field", async () => { + test("rejects request with missing node_type field", async () => { const owner = await createTestUser(); const workspace = await createTestWorkspace({ ownerId: owner.id }); await createTestMembership({ @@ -410,7 +422,7 @@ describe("Node Update API - Integration Tests", () => { await createTestSwarm({ workspaceId: workspace.id, swarmApiKey: "test-api-key" }); const nodeId = "node-no-props"; - const updateData = {}; // Missing properties + const updateData = { node_data: { name: "Update" } }; // Missing node_type const request = createAuthenticatedPutRequest( `http://localhost:3000/api/workspaces/${workspace.slug}/nodes/${nodeId}`, @@ -422,11 +434,11 @@ describe("Node Update API - Integration Tests", () => { params: Promise.resolve({ slug: workspace.slug, nodeId }), }); - await expectError(response, "properties object is required", 400); + await expectError(response, "node_type string is required", 400); expect(nodesService.updateNode).not.toHaveBeenCalled(); }); - test("rejects request with properties field as non-object", async () => { + test("rejects request with missing node_data field", async () => { const owner = await createTestUser(); const workspace = await createTestWorkspace({ ownerId: owner.id }); await createTestMembership({ @@ -438,7 +450,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-invalid-props-type"; const updateData = { - properties: "invalid-string" as any, // Should be object + node_type: "TestNode", + // Missing node_data }; const request = createAuthenticatedPutRequest( @@ -451,11 +464,11 @@ describe("Node Update API - Integration Tests", () => { params: Promise.resolve({ slug: workspace.slug, nodeId }), }); - await expectError(response, "properties object is required", 400); + await expectError(response, "node_data object is required", 400); expect(nodesService.updateNode).not.toHaveBeenCalled(); }); - test("allows array as properties (typeof array === 'object')", async () => { + test("allows array as node_data (typeof array === 'object')", async () => { const owner = await createTestUser(); const workspace = await createTestWorkspace({ ownerId: owner.id }); await createTestMembership({ @@ -467,7 +480,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-array-props"; const updateData = { - properties: ["item1", "item2"] as any, // Arrays pass typeof check + node_type: "TestNode", + node_data: ["item1", "item2"] as any, // Arrays pass typeof check }; vi.mocked(nodesService.updateNode).mockResolvedValue({ success: true }); @@ -501,7 +515,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-no-swarm"; const updateData = { - properties: { name: "Update" }, + node_type: "TestNode", + node_data: { name: "Update" }, }; const request = createAuthenticatedPutRequest( @@ -532,7 +547,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-error"; const updateData = { - properties: { name: "Update" }, + node_type: "TestNode", + node_data: { name: "Update" }, }; // Mock Jarvis API error - updateNode returns error via result object @@ -568,7 +584,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-exception"; const updateData = { - properties: { name: "Update" }, + node_type: "TestNode", + node_data: { name: "Update" }, }; // Mock unexpected exception @@ -605,10 +622,12 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-concurrent"; const updateData1 = { - properties: { name: "Update 1" }, + node_type: "TestNode", + node_data: { name: "Update 1" }, }; const updateData2 = { - properties: { name: "Update 2" }, + node_type: "TestNode", + node_data: { name: "Update 2" }, }; vi.mocked(nodesService.updateNode) @@ -653,7 +672,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-large-props"; const largeString = "x".repeat(10000); const updateData = { - properties: { + node_type: "TestNode", + node_data: { name: "Large Node", config: largeString, }, @@ -686,7 +706,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-special-chars"; const updateData = { - properties: { + node_type: "TestNode", + node_data: { "property-with-dashes": "value", "property.with.dots": "value", "property:with:colons": "value", @@ -723,7 +744,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-null-values"; const updateData = { - properties: { + node_type: "TestNode", + node_data: { name: "Node with nulls", optionalField: null, nestedObject: { @@ -760,7 +782,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-nested"; const updateData = { - properties: { + node_type: "TestNode", + node_data: { name: "Nested Node", config: { level1: { @@ -806,7 +829,8 @@ describe("Node Update API - Integration Tests", () => { const nodeId = "node-preserve-ws"; const updateData = { - properties: { name: "Update" }, + node_type: "TestNode", + node_data: { name: "Update" }, }; vi.mocked(nodesService.updateNode).mockResolvedValue({ success: true }); diff --git a/src/__tests__/unit/services/swarm/api/nodes.test.ts b/src/__tests__/unit/services/swarm/api/nodes.test.ts index aa5688cd75..ed7f212005 100644 --- a/src/__tests__/unit/services/swarm/api/nodes.test.ts +++ b/src/__tests__/unit/services/swarm/api/nodes.test.ts @@ -9,7 +9,7 @@ beforeEach(() => { global.fetch = mockFetch; }); -const { addNode, addEdge, addEdgeBulk } = await import("@/services/swarm/api/nodes"); +const { addNode, addEdge, addEdgeBulk, updateNode, deleteNode, deleteEdge } = await import("@/services/swarm/api/nodes"); const config = { jarvisUrl: "https://test-swarm.sphinx.chat:8444", @@ -499,3 +499,254 @@ describe("addEdgeBulk", () => { }); }); }); + +// --------------------------------------------------------------------------- +// updateNode +// --------------------------------------------------------------------------- + +describe("updateNode", () => { + describe("Success cases", () => { + test("calls PUT /node with node_data (not properties) in request body", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ status: "success" }), + }); + + const result = await updateNode(config, { + ref_id: "eval-set-1", + node_type: "EvalSet", + node_data: { name: "Updated Name", description: "Updated desc" }, + }); + + expect(result).toEqual({ success: true }); + + expect(mockFetch).toHaveBeenCalledWith( + "https://test-swarm.sphinx.chat:8444/node", + expect.objectContaining({ + method: "PUT", + headers: expect.objectContaining({ + "x-api-token": "test-api-key", + "Content-Type": "application/json", + }), + body: JSON.stringify({ + ref_id: "eval-set-1", + node_type: "EvalSet", + node_data: { name: "Updated Name", description: "Updated desc" }, + }), + }), + ); + + // Verify that legacy 'properties' key is NOT sent + const sentBody = JSON.parse(mockFetch.mock.calls[0][1].body as string); + expect(sentBody).not.toHaveProperty("properties"); + }); + + test("returns success for EvalRequirement update", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ status: "success" }), + }); + + const result = await updateNode(config, { + ref_id: "req-1", + node_type: "EvalRequirement", + node_data: { + name: "Check output", + prompt_snippet: "Summarize this", + positive_cases: ["Good"], + negative_cases: ["Bad"], + }, + }); + + expect(result.success).toBe(true); + }); + }); + + describe("Failure cases", () => { + test("returns failure when HTTP response is not ok", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + text: async () => "Server error", + }); + + const result = await updateNode(config, { + ref_id: "eval-1", + node_type: "EvalSet", + node_data: { name: "Fail" }, + }); + + expect(result.success).toBe(false); + expect(result.error).toContain("500"); + }); + + test("returns failure when fetch throws", async () => { + mockFetch.mockRejectedValueOnce(new Error("Network error")); + + const result = await updateNode(config, { + ref_id: "eval-1", + node_type: "EvalSet", + node_data: { name: "Throw" }, + }); + + expect(result.success).toBe(false); + expect(result.error).toBe("Network error"); + }); + }); +}); + +// --------------------------------------------------------------------------- +// deleteNode +// --------------------------------------------------------------------------- + +describe("deleteNode", () => { + describe("Success cases", () => { + test("calls DELETE /node/{refId} with X-Is-Admin: true header", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ status: "success" }), + }); + + const result = await deleteNode(config, "eval-set-abc"); + + expect(result).toEqual({ success: true }); + + expect(mockFetch).toHaveBeenCalledWith( + "https://test-swarm.sphinx.chat:8444/node/eval-set-abc", + expect.objectContaining({ + method: "DELETE", + headers: expect.objectContaining({ + "x-api-token": "test-api-key", + "X-Is-Admin": "true", + "Content-Type": "application/json", + }), + }), + ); + }); + + test("returns success even when response body has no status field", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 204, + json: async () => { throw new Error("No body"); }, + }); + + const result = await deleteNode(config, "node-1"); + + expect(result.success).toBe(true); + }); + + test("URL encodes the refId", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ status: "success" }), + }); + + await deleteNode(config, "node with spaces"); + + const calledUrl = mockFetch.mock.calls[0][0] as string; + expect(calledUrl).toBe("https://test-swarm.sphinx.chat:8444/node/node%20with%20spaces"); + }); + }); + + describe("Failure cases", () => { + test("returns failure when HTTP response is not ok", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + text: async () => "Not found", + }); + + const result = await deleteNode(config, "missing-node"); + + expect(result.success).toBe(false); + expect(result.error).toContain("404"); + }); + + test("returns failure when fetch throws", async () => { + mockFetch.mockRejectedValueOnce(new Error("Connection refused")); + + const result = await deleteNode(config, "node-1"); + + expect(result.success).toBe(false); + expect(result.error).toBe("Connection refused"); + }); + }); +}); + +// --------------------------------------------------------------------------- +// deleteEdge +// --------------------------------------------------------------------------- + +describe("deleteEdge", () => { + describe("Success cases", () => { + test("calls DELETE /node/edge/{refId} without X-Is-Admin header", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ status: "success" }), + }); + + const result = await deleteEdge(config, "edge-ref-123"); + + expect(result).toEqual({ success: true }); + + expect(mockFetch).toHaveBeenCalledWith( + "https://test-swarm.sphinx.chat:8444/node/edge/edge-ref-123", + expect.objectContaining({ + method: "DELETE", + headers: expect.objectContaining({ + "x-api-token": "test-api-key", + }), + }), + ); + + // Verify X-Is-Admin is NOT present + const calledHeaders = mockFetch.mock.calls[0][1].headers as Record; + expect(calledHeaders).not.toHaveProperty("X-Is-Admin"); + }); + + test("URL encodes the edgeRefId", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ status: "success" }), + }); + + await deleteEdge(config, "edge/with/slashes"); + + const calledUrl = mockFetch.mock.calls[0][0] as string; + expect(calledUrl).toBe( + "https://test-swarm.sphinx.chat:8444/node/edge/edge%2Fwith%2Fslashes", + ); + }); + }); + + describe("Failure cases", () => { + test("returns failure when HTTP response is not ok", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + text: async () => "Server error", + }); + + const result = await deleteEdge(config, "edge-1"); + + expect(result.success).toBe(false); + expect(result.error).toContain("500"); + }); + + test("returns failure when fetch throws", async () => { + mockFetch.mockRejectedValueOnce(new Error("Timeout")); + + const result = await deleteEdge(config, "edge-1"); + + expect(result.success).toBe(false); + expect(result.error).toBe("Timeout"); + }); + }); +}); diff --git a/src/app/api/mock/evals/[evalSetId]/requirements/[reqId]/route.ts b/src/app/api/mock/evals/[evalSetId]/requirements/[reqId]/route.ts new file mode 100644 index 0000000000..a53a9ab935 --- /dev/null +++ b/src/app/api/mock/evals/[evalSetId]/requirements/[reqId]/route.ts @@ -0,0 +1,15 @@ +import { NextRequest, NextResponse } from "next/server"; + +export const runtime = "nodejs"; + +type RouteParams = { params: Promise<{ evalSetId: string; reqId: string }> }; + +export async function PUT(request: NextRequest, { params }: RouteParams) { + const { evalSetId, reqId } = await params; + const body = await request.json().catch(() => ({})); + return NextResponse.json({ success: true, data: { ref_id: reqId, evalSetId, ...body } }); +} + +export async function DELETE(_request: NextRequest, _ctx: RouteParams) { + return NextResponse.json({ success: true }); +} diff --git a/src/app/api/mock/evals/[evalSetId]/route.ts b/src/app/api/mock/evals/[evalSetId]/route.ts new file mode 100644 index 0000000000..8636a0e77e --- /dev/null +++ b/src/app/api/mock/evals/[evalSetId]/route.ts @@ -0,0 +1,15 @@ +import { NextRequest, NextResponse } from "next/server"; + +export const runtime = "nodejs"; + +type RouteParams = { params: Promise<{ evalSetId: string }> }; + +export async function PUT(request: NextRequest, { params }: RouteParams) { + const { evalSetId } = await params; + const body = await request.json().catch(() => ({})); + return NextResponse.json({ success: true, data: { ref_id: evalSetId, ...body } }); +} + +export async function DELETE(_request: NextRequest, _ctx: RouteParams) { + return NextResponse.json({ success: true }); +} diff --git a/src/app/api/workspaces/[slug]/evals/[evalSetId]/requirements/[reqId]/route.ts b/src/app/api/workspaces/[slug]/evals/[evalSetId]/requirements/[reqId]/route.ts new file mode 100644 index 0000000000..ed12955cee --- /dev/null +++ b/src/app/api/workspaces/[slug]/evals/[evalSetId]/requirements/[reqId]/route.ts @@ -0,0 +1,136 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getMiddlewareContext, requireAuth } from "@/lib/middleware/utils"; +import { getJarvisUrl } from "@/lib/utils/swarm"; +import { getWorkspaceSwarmAccess } from "@/lib/helpers/swarm-access"; +import { updateNode, deleteNode } from "@/services/swarm/api/nodes"; + +type RouteParams = { + params: Promise<{ slug: string; evalSetId: string; reqId: string }>; +}; + +function handleSwarmAccessError(error: { type: string }) { + const errorMap: Record = { + WORKSPACE_NOT_FOUND: { message: "Workspace not found", status: 404 }, + ACCESS_DENIED: { message: "Access denied", status: 403 }, + SWARM_NOT_ACTIVE: { message: "Swarm not active", status: 400 }, + SWARM_NAME_MISSING: { message: "Swarm name not found", status: 400 }, + SWARM_API_KEY_MISSING: { message: "Swarm API key not configured", status: 400 }, + SWARM_NOT_CONFIGURED: { message: "Swarm not configured", status: 400 }, + }; + const errorInfo = errorMap[error.type] || { message: "Unknown error", status: 500 }; + return NextResponse.json({ error: errorInfo.message }, { status: errorInfo.status }); +} + +export async function PUT(request: NextRequest, { params }: RouteParams) { + try { + const context = getMiddlewareContext(request); + const userOrResponse = requireAuth(context); + if (userOrResponse instanceof NextResponse) return userOrResponse; + + const { slug, evalSetId, reqId } = await params; + + const body = await request.json(); + const { name, description, prompt_snippet, positive_cases, negative_cases } = body ?? {}; + + if (!name || typeof name !== "string" || !name.trim()) { + return NextResponse.json({ error: "name is required" }, { status: 400 }); + } + if (!prompt_snippet || typeof prompt_snippet !== "string" || !prompt_snippet.trim()) { + return NextResponse.json({ error: "prompt_snippet is required" }, { status: 400 }); + } + if (!Array.isArray(positive_cases) || positive_cases.length === 0) { + return NextResponse.json( + { error: "positive_cases must be a non-empty array" }, + { status: 400 }, + ); + } + if (!Array.isArray(negative_cases) || negative_cases.length === 0) { + return NextResponse.json( + { error: "negative_cases must be a non-empty array" }, + { status: 400 }, + ); + } + + const swarmAccessResult = await getWorkspaceSwarmAccess(slug, userOrResponse.id); + if (!swarmAccessResult.success) { + console.warn(`[Evals Requirements PUT] Swarm access denied: ${swarmAccessResult.error.type}`); + return handleSwarmAccessError(swarmAccessResult.error); + } + + if (process.env.USE_MOCKS === "true") { + const mockResponse = await fetch( + `${request.nextUrl.origin}/api/mock/evals/${evalSetId}/requirements/${reqId}`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }, + ); + return NextResponse.json(await mockResponse.json()); + } + + const { swarmName, swarmApiKey } = swarmAccessResult.data; + const jarvisUrl = getJarvisUrl(swarmName); + const config = { jarvisUrl, apiKey: swarmApiKey }; + + const result = await updateNode(config, { + ref_id: reqId, + node_type: "EvalRequirement", + node_data: { + name: name.trim(), + description, + prompt_snippet: prompt_snippet.trim(), + positive_cases, + negative_cases, + }, + }); + + if (!result.success) { + return NextResponse.json({ error: result.error }, { status: 502 }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("[Evals/Requirements] PUT error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: RouteParams) { + try { + const context = getMiddlewareContext(request); + const userOrResponse = requireAuth(context); + if (userOrResponse instanceof NextResponse) return userOrResponse; + + const { slug, evalSetId, reqId } = await params; + + const swarmAccessResult = await getWorkspaceSwarmAccess(slug, userOrResponse.id); + if (!swarmAccessResult.success) { + console.warn(`[Evals Requirements DELETE] Swarm access denied: ${swarmAccessResult.error.type}`); + return handleSwarmAccessError(swarmAccessResult.error); + } + + if (process.env.USE_MOCKS === "true") { + const mockResponse = await fetch( + `${request.nextUrl.origin}/api/mock/evals/${evalSetId}/requirements/${reqId}`, + { method: "DELETE" }, + ); + return NextResponse.json(await mockResponse.json()); + } + + const { swarmName, swarmApiKey } = swarmAccessResult.data; + const jarvisUrl = getJarvisUrl(swarmName); + const config = { jarvisUrl, apiKey: swarmApiKey }; + + const result = await deleteNode(config, reqId); + + if (!result.success) { + return NextResponse.json({ error: result.error }, { status: 502 }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("[Evals/Requirements] DELETE error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/src/app/api/workspaces/[slug]/evals/[evalSetId]/route.ts b/src/app/api/workspaces/[slug]/evals/[evalSetId]/route.ts new file mode 100644 index 0000000000..08de8e537f --- /dev/null +++ b/src/app/api/workspaces/[slug]/evals/[evalSetId]/route.ts @@ -0,0 +1,113 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getMiddlewareContext, requireAuth } from "@/lib/middleware/utils"; +import { getJarvisUrl } from "@/lib/utils/swarm"; +import { getWorkspaceSwarmAccess } from "@/lib/helpers/swarm-access"; +import { updateNode, deleteNode } from "@/services/swarm/api/nodes"; + +type RouteParams = { params: Promise<{ slug: string; evalSetId: string }> }; + +function handleSwarmAccessError(error: { type: string }) { + const errorMap: Record = { + WORKSPACE_NOT_FOUND: { message: "Workspace not found", status: 404 }, + ACCESS_DENIED: { message: "Access denied", status: 403 }, + SWARM_NOT_ACTIVE: { message: "Swarm not active", status: 400 }, + SWARM_NAME_MISSING: { message: "Swarm name not found", status: 400 }, + SWARM_API_KEY_MISSING: { message: "Swarm API key not configured", status: 400 }, + SWARM_NOT_CONFIGURED: { message: "Swarm not configured", status: 400 }, + }; + const errorInfo = errorMap[error.type] || { message: "Unknown error", status: 500 }; + return NextResponse.json({ error: errorInfo.message }, { status: errorInfo.status }); +} + +export async function PUT(request: NextRequest, { params }: RouteParams) { + try { + const context = getMiddlewareContext(request); + const userOrResponse = requireAuth(context); + if (userOrResponse instanceof NextResponse) return userOrResponse; + + const { slug, evalSetId } = await params; + + const body = await request.json(); + const { name, description } = body ?? {}; + + if (!name || typeof name !== "string" || !name.trim()) { + return NextResponse.json({ error: "name is required" }, { status: 400 }); + } + + const swarmAccessResult = await getWorkspaceSwarmAccess(slug, userOrResponse.id); + if (!swarmAccessResult.success) { + console.warn(`[Evals PUT] Swarm access denied: ${swarmAccessResult.error.type}`); + return handleSwarmAccessError(swarmAccessResult.error); + } + + if (process.env.USE_MOCKS === "true") { + const mockResponse = await fetch( + `${request.nextUrl.origin}/api/mock/evals/${evalSetId}`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, description }), + }, + ); + return NextResponse.json(await mockResponse.json()); + } + + const { swarmName, swarmApiKey } = swarmAccessResult.data; + const jarvisUrl = getJarvisUrl(swarmName); + const config = { jarvisUrl, apiKey: swarmApiKey }; + + const result = await updateNode(config, { + ref_id: evalSetId, + node_type: "EvalSet", + node_data: { name: name.trim(), description }, + }); + + if (!result.success) { + return NextResponse.json({ error: result.error }, { status: 502 }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("[Evals] PUT error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: RouteParams) { + try { + const context = getMiddlewareContext(request); + const userOrResponse = requireAuth(context); + if (userOrResponse instanceof NextResponse) return userOrResponse; + + const { slug, evalSetId } = await params; + + const swarmAccessResult = await getWorkspaceSwarmAccess(slug, userOrResponse.id); + if (!swarmAccessResult.success) { + console.warn(`[Evals DELETE] Swarm access denied: ${swarmAccessResult.error.type}`); + return handleSwarmAccessError(swarmAccessResult.error); + } + + if (process.env.USE_MOCKS === "true") { + const mockResponse = await fetch( + `${request.nextUrl.origin}/api/mock/evals/${evalSetId}`, + { method: "DELETE" }, + ); + return NextResponse.json(await mockResponse.json()); + } + + const { swarmName, swarmApiKey } = swarmAccessResult.data; + const jarvisUrl = getJarvisUrl(swarmName); + const config = { jarvisUrl, apiKey: swarmApiKey }; + + const result = await deleteNode(config, evalSetId); + + if (!result.success) { + return NextResponse.json({ error: result.error }, { status: 502 }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("[Evals] DELETE error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/src/app/api/workspaces/[slug]/nodes/[nodeId]/route.ts b/src/app/api/workspaces/[slug]/nodes/[nodeId]/route.ts index dd1b2923ad..c34930c881 100644 --- a/src/app/api/workspaces/[slug]/nodes/[nodeId]/route.ts +++ b/src/app/api/workspaces/[slug]/nodes/[nodeId]/route.ts @@ -29,9 +29,15 @@ export async function PUT(request: NextRequest, { params }: RouteParams) { const { slug, nodeId } = await params; const body = await request.json(); - if (!body.properties || typeof body.properties !== "object") { + if (!body.node_type || typeof body.node_type !== "string") { return NextResponse.json( - { error: "properties object is required" }, + { error: "node_type string is required" }, + { status: 400 }, + ); + } + if (!body.node_data || typeof body.node_data !== "object") { + return NextResponse.json( + { error: "node_data object is required" }, { status: 400 }, ); } @@ -46,7 +52,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) { const result = await updateNode( { jarvisUrl, apiKey: swarmApiKey }, - { ref_id: nodeId, properties: body.properties }, + { ref_id: nodeId, node_type: body.node_type, node_data: body.node_data }, ); if (!result.success) { diff --git a/src/services/swarm/api/nodes.ts b/src/services/swarm/api/nodes.ts index 6ddc30b067..8329a69aae 100644 --- a/src/services/swarm/api/nodes.ts +++ b/src/services/swarm/api/nodes.ts @@ -190,7 +190,8 @@ export async function updateNode( method: "PUT", data: { ref_id: request.ref_id, - properties: request.properties, + node_type: request.node_type, + node_data: request.node_data, }, }); @@ -203,3 +204,75 @@ export async function updateNode( return { success: true }; } + +export async function deleteNode( + config: JarvisConnectionConfig, + refId: string, +): Promise<{ success: boolean; error?: string }> { + try { + const url = `${config.jarvisUrl.replace(/\/$/, "")}/node/${encodeURIComponent(refId)}`; + const response = await fetch(url, { + method: "DELETE", + headers: { + "x-api-token": config.apiKey, + "X-Is-Admin": "true", + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const responseText = await response.text(); + console.error("[Jarvis Nodes] deleteNode failed:", response.status, responseText); + return { + success: false, + error: `Request failed with status ${response.status}`, + }; + } + + const body = await response.json().catch(() => ({})) as { status?: string }; + if (body?.status === "success") { + return { success: true }; + } + + return { success: true }; + } catch (error) { + console.error("[Jarvis Nodes] deleteNode error:", error); + return { + success: false, + error: error instanceof Error ? error.message : "Request failed", + }; + } +} + +export async function deleteEdge( + config: JarvisConnectionConfig, + edgeRefId: string, +): Promise<{ success: boolean; error?: string }> { + try { + const url = `${config.jarvisUrl.replace(/\/$/, "")}/node/edge/${encodeURIComponent(edgeRefId)}`; + const response = await fetch(url, { + method: "DELETE", + headers: { + "x-api-token": config.apiKey, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const responseText = await response.text(); + console.error("[Jarvis Nodes] deleteEdge failed:", response.status, responseText); + return { + success: false, + error: `Request failed with status ${response.status}`, + }; + } + + return { success: true }; + } catch (error) { + console.error("[Jarvis Nodes] deleteEdge error:", error); + return { + success: false, + error: error instanceof Error ? error.message : "Request failed", + }; + } +} diff --git a/src/types/jarvis.ts b/src/types/jarvis.ts index f95b3b9d15..cd810d7d1b 100644 --- a/src/types/jarvis.ts +++ b/src/types/jarvis.ts @@ -18,7 +18,8 @@ export interface JarvisResponse { export interface UpdateNodeRequest { ref_id: string; - properties: Record; + node_type: string; + node_data: Record; } export interface JarvisConnectionConfig {