diff --git a/src/__tests__/integration/api/workspaces/evals/evals.test.ts b/src/__tests__/integration/api/workspaces/evals/evals.test.ts index 8e38f01baa..ce30422440 100644 --- a/src/__tests__/integration/api/workspaces/evals/evals.test.ts +++ b/src/__tests__/integration/api/workspaces/evals/evals.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect, beforeEach, vi } from "vitest"; import { GET as getEvalSets, POST as createEvalSet } from "@/app/api/workspaces/[slug]/evals/route"; -import { POST as createRequirement } from "@/app/api/workspaces/[slug]/evals/[evalSetId]/requirements/route"; +import { GET as getRequirements, POST as createRequirement } from "@/app/api/workspaces/[slug]/evals/[evalSetId]/requirements/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 { @@ -360,6 +360,187 @@ describe("Evals API — Integration Tests", () => { }); }); + // --------------------------------------------------------------------------- + // GET /api/workspaces/[slug]/evals/[evalSetId]/requirements + // --------------------------------------------------------------------------- + describe("GET /api/workspaces/[slug]/evals/[evalSetId]/requirements", () => { + describe("Success", () => { + test("returns requirements with order merged from edges", 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 mockNodes = [ + { ref_id: "req-1", node_type: "EvalRequirement", properties: { name: "Req A" } }, + { ref_id: "req-2", node_type: "EvalRequirement", properties: { name: "Req B" } }, + ]; + const mockEdges = [ + { target_ref_id: "req-1", properties: { order: 0 } }, + { target_ref_id: "req-2", properties: { order: 1 } }, + ]; + + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: async () => ({ nodes: mockNodes, edges: mockEdges }), + } as any); + + const request = createAuthenticatedGetRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-set-1/requirements`, + owner, + ); + + const response = await getRequirements(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-set-1" }), + }); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.success).toBe(true); + expect(data.data.nodes).toHaveLength(2); + expect(data.data.total).toBe(2); + expect(data.data.nodes[0].properties.order).toBe(0); + expect(data.data.nodes[1].properties.order).toBe(1); + }); + + test("returns empty array when eval set has no requirements", 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" }); + + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: async () => ({ nodes: [], edges: [] }), + } as any); + + const request = createAuthenticatedGetRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-set-empty/requirements`, + owner, + ); + + const response = await getRequirements(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-set-empty" }), + }); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.success).toBe(true); + expect(data.data.nodes).toEqual([]); + expect(data.data.total).toBe(0); + }); + + test("calls Jarvis with correct URL including Python list literal params", 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 fetchMock = vi.fn().mockResolvedValueOnce({ + ok: true, + json: async () => ({ nodes: [], edges: [] }), + } as any); + global.fetch = fetchMock; + + const request = createAuthenticatedGetRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/my-eval-set/requirements`, + owner, + ); + + await getRequirements(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "my-eval-set" }), + }); + + const calledUrl: string = fetchMock.mock.calls[0][0]; + expect(calledUrl).toContain("/v2/nodes/my-eval-set"); + expect(calledUrl).toContain("expand=edges"); + expect(calledUrl).toContain(encodeURIComponent("['HAS_REQUIREMENT']")); + expect(calledUrl).toContain(encodeURIComponent("['EvalRequirement']")); + expect(calledUrl).toContain("depth=1"); + }); + }); + + describe("Auth failures", () => { + test("rejects unauthenticated requests", async () => { + const owner = await createTestUser(); + const workspace = await createTestWorkspace({ ownerId: owner.id }); + + const request = createGetRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-set-1/requirements`, + ); + + const response = await getRequirements(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-set-1" }), + }); + + await expectUnauthorized(response); + }); + + 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 = createAuthenticatedGetRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-set-1/requirements`, + nonMember, + ); + + const response = await getRequirements(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-set-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 = createAuthenticatedGetRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-set-1/requirements`, + owner, + ); + + const response = await getRequirements(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-set-1" }), + }); + + await expectError(response, "Swarm not configured", 400); + }); + }); + + describe("Upstream failure", () => { + test("returns 502 when Jarvis returns non-ok", 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" }); + + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + status: 503, + text: async () => "Service Unavailable", + } as any); + + const request = createAuthenticatedGetRequest( + `http://localhost:3000/api/workspaces/${workspace.slug}/evals/eval-set-1/requirements`, + owner, + ); + + const response = await getRequirements(request, { + params: Promise.resolve({ slug: workspace.slug, evalSetId: "eval-set-1" }), + }); + + expect(response.status).toBe(502); + }); + }); + }); + // --------------------------------------------------------------------------- // POST /api/workspaces/[slug]/evals/[evalSetId]/requirements // --------------------------------------------------------------------------- diff --git a/src/__tests__/unit/api/mock/evals-requirements.test.ts b/src/__tests__/unit/api/mock/evals-requirements.test.ts new file mode 100644 index 0000000000..58fb8cce16 --- /dev/null +++ b/src/__tests__/unit/api/mock/evals-requirements.test.ts @@ -0,0 +1,103 @@ +import { describe, test, expect } from "vitest"; +import { GET, POST } from "@/app/api/mock/evals/[evalSetId]/requirements/route"; +import { NextRequest } from "next/server"; + +function makeGetRequest(evalSetId: string): NextRequest { + return new NextRequest( + `http://localhost:3000/api/mock/evals/${evalSetId}/requirements`, + { method: "GET" }, + ); +} + +function makePostRequest(evalSetId: string, body: object): NextRequest { + return new NextRequest( + `http://localhost:3000/api/mock/evals/${evalSetId}/requirements`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }, + ); +} + +describe("GET /api/mock/evals/[evalSetId]/requirements", () => { + test("returns seeded requirements for eval-set-1", async () => { + const request = makeGetRequest("eval-set-1"); + const response = await GET(request, { params: Promise.resolve({ evalSetId: "eval-set-1" }) }); + const data = await response.json(); + + expect(data.success).toBe(true); + expect(data.data.nodes.length).toBeGreaterThan(0); + expect(data.data.total).toBe(data.data.nodes.length); + + // All nodes belong to EvalRequirement type + for (const node of data.data.nodes) { + expect(node.node_type).toBe("EvalRequirement"); + expect(node.ref_id).toBeDefined(); + expect(node.properties.name).toBeDefined(); + expect(node.properties.prompt_snippet).toBeDefined(); + expect(Array.isArray(node.properties.positive_cases)).toBe(true); + expect(Array.isArray(node.properties.negative_cases)).toBe(true); + expect(typeof node.properties.order).toBe("number"); + } + }); + + test("returns seeded requirements for eval-set-2", async () => { + const request = makeGetRequest("eval-set-2"); + const response = await GET(request, { params: Promise.resolve({ evalSetId: "eval-set-2" }) }); + const data = await response.json(); + + expect(data.success).toBe(true); + expect(data.data.nodes.length).toBeGreaterThan(0); + }); + + test("eval-set-1 and eval-set-2 return different requirements", async () => { + const res1 = await GET(makeGetRequest("eval-set-1"), { params: Promise.resolve({ evalSetId: "eval-set-1" }) }); + const res2 = await GET(makeGetRequest("eval-set-2"), { params: Promise.resolve({ evalSetId: "eval-set-2" }) }); + + const data1 = await res1.json(); + const data2 = await res2.json(); + + const ids1 = data1.data.nodes.map((n: { ref_id: string }) => n.ref_id); + const ids2 = data2.data.nodes.map((n: { ref_id: string }) => n.ref_id); + + // No overlap between the two sets + const overlap = ids1.filter((id: string) => ids2.includes(id)); + expect(overlap).toHaveLength(0); + }); + + test("returns empty array for unknown eval set id", async () => { + const request = makeGetRequest("unknown-eval-set-xyz"); + const response = await GET(request, { params: Promise.resolve({ evalSetId: "unknown-eval-set-xyz" }) }); + const data = await response.json(); + + expect(data.success).toBe(true); + expect(data.data.nodes).toEqual([]); + expect(data.data.total).toBe(0); + }); + + test("returns 200 status", async () => { + const request = makeGetRequest("eval-set-1"); + const response = await GET(request, { params: Promise.resolve({ evalSetId: "eval-set-1" }) }); + expect(response.status).toBe(200); + }); +}); + +describe("POST /api/mock/evals/[evalSetId]/requirements", () => { + test("creates a new requirement node and returns its ref_id", async () => { + const body = { + name: "Test Req", + description: "A description", + prompt_snippet: "When asked to do X", + positive_cases: ["Does X correctly"], + negative_cases: ["Fails silently"], + }; + const request = makePostRequest("eval-set-1", body); + const response = await POST(request); + const data = await response.json(); + + expect(data.success).toBe(true); + expect(typeof data.data.ref_id).toBe("string"); + expect(data.data.ref_id.length).toBeGreaterThan(0); + }); +}); diff --git a/src/__tests__/unit/components/evals/EvalSetDetail.test.tsx b/src/__tests__/unit/components/evals/EvalSetDetail.test.tsx new file mode 100644 index 0000000000..b666cd063e --- /dev/null +++ b/src/__tests__/unit/components/evals/EvalSetDetail.test.tsx @@ -0,0 +1,169 @@ +/** + * @vitest-environment jsdom + */ +import React from "react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; + +globalThis.React = React; + +vi.mock("@/hooks/useWorkspace", () => ({ + useWorkspace: () => ({ slug: "test-ws" }), +})); + +vi.mock("sonner", () => ({ + toast: { error: vi.fn(), success: vi.fn() }, +})); + +vi.mock("@/components/evals/CreateRequirementModal", () => ({ + CreateRequirementModal: () => null, +})); + +vi.mock("@/components/evals/LinkRunModal", () => ({ + LinkRunModal: () => null, +})); + +vi.mock("@/components/ui/button", () => ({ + Button: ({ children, onClick, ...props }: any) => ( + + ), +})); + +vi.mock("@/components/ui/badge", () => ({ + Badge: ({ children }: any) => {children}, +})); + +vi.mock("@/components/ui/skeleton", () => ({ + Skeleton: () =>
, +})); + +vi.mock("lucide-react", () => ({ + ArrowLeft: () => , + Link2: () => 🔗, + Plus: () => +, +})); + +import { EvalSetDetail } from "@/components/evals/EvalSetDetail"; + +const EVAL_SET = { + ref_id: "eval-set-1", + node_type: "EvalSet", + properties: { name: "My Eval Set", description: "Test suite" }, +}; + +const MOCK_REQUIREMENTS = [ + { + ref_id: "req-1", + node_type: "EvalRequirement", + properties: { + name: "Req Alpha", + description: "First requirement", + prompt_snippet: "When asked to...", + positive_cases: ["Does A", "Does B"], + negative_cases: ["Does not C"], + order: 0, + }, + }, + { + ref_id: "req-2", + node_type: "EvalRequirement", + properties: { + name: "Req Beta", + description: "Second requirement", + prompt_snippet: "When instructed to...", + positive_cases: ["Does X"], + negative_cases: ["Does not Y"], + order: 1, + }, + }, +]; + +describe("EvalSetDetail", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("fetches from the correct requirements endpoint (not the list-all-evals endpoint)", async () => { + const fetchMock = vi.fn().mockResolvedValue({ + json: async () => ({ data: { nodes: [], total: 0 } }), + }); + global.fetch = fetchMock as any; + + render( {}} />); + + await waitFor(() => { + expect(fetchMock).toHaveBeenCalled(); + }); + + const calledUrl: string = fetchMock.mock.calls[0][0]; + // Must call the requirements endpoint + expect(calledUrl).toBe("/api/workspaces/test-ws/evals/eval-set-1/requirements"); + // Must NOT call the list-all endpoint + expect(calledUrl).not.toContain("?evalSetId="); + }); + + it("renders skeleton while loading", () => { + global.fetch = vi.fn(() => new Promise(() => {})) as any; + + render( {}} />); + + expect(screen.getAllByTestId("skeleton").length).toBeGreaterThan(0); + }); + + it("renders empty state when no requirements are returned", async () => { + global.fetch = vi.fn().mockResolvedValue({ + json: async () => ({ data: { nodes: [], total: 0 } }), + }) as any; + + render( {}} />); + + await waitFor(() => { + expect(screen.getByText(/No requirements yet/)).toBeTruthy(); + }); + }); + + it("renders requirement rows when requirements are returned", async () => { + global.fetch = vi.fn().mockResolvedValue({ + json: async () => ({ data: { nodes: MOCK_REQUIREMENTS, total: 2 } }), + }) as any; + + render( {}} />); + + await waitFor(() => { + expect(screen.getAllByTestId("requirement-row")).toHaveLength(2); + }); + + expect(screen.getByText("Req Alpha")).toBeTruthy(); + expect(screen.getByText("Req Beta")).toBeTruthy(); + }); + + it("renders requirements sorted by order property", async () => { + // Return in reverse order — component should sort by order + const reversedNodes = [...MOCK_REQUIREMENTS].reverse(); + global.fetch = vi.fn().mockResolvedValue({ + json: async () => ({ data: { nodes: reversedNodes, total: 2 } }), + }) as any; + + render( {}} />); + + await waitFor(() => { + expect(screen.getAllByTestId("requirement-row")).toHaveLength(2); + }); + + const rows = screen.getAllByTestId("requirement-row"); + expect(rows[0].textContent).toContain("Req Alpha"); + expect(rows[1].textContent).toContain("Req Beta"); + }); + + it("shows eval set name in header", async () => { + global.fetch = vi.fn().mockResolvedValue({ + json: async () => ({ data: { nodes: [], total: 0 } }), + }) as any; + + render( {}} />); + + await waitFor(() => { + expect(screen.getByText("My Eval Set")).toBeTruthy(); + }); + }); +}); diff --git a/src/app/api/mock/evals/[evalSetId]/requirements/route.ts b/src/app/api/mock/evals/[evalSetId]/requirements/route.ts index c1aabbbdc9..53c140b11f 100644 --- a/src/app/api/mock/evals/[evalSetId]/requirements/route.ts +++ b/src/app/api/mock/evals/[evalSetId]/requirements/route.ts @@ -3,6 +3,93 @@ import type { JarvisNode } from "@/types/jarvis"; export const runtime = "nodejs"; +type RequirementNode = JarvisNode & { + properties: { + name: string; + description: string; + prompt_snippet: string; + positive_cases: string[]; + negative_cases: string[]; + order: number; + }; +}; + +const SEED_REQUIREMENTS: Record = { + "eval-set-1": [ + { + ref_id: "req-1-1", + node_type: "EvalRequirement", + properties: { + name: "Basic greeting response", + description: "Agent should greet the user appropriately", + prompt_snippet: "Say hello to the user", + positive_cases: ["Response includes a greeting", "Response is polite"], + negative_cases: ["Response is rude", "Response ignores the user"], + order: 0, + }, + }, + { + ref_id: "req-1-2", + node_type: "EvalRequirement", + properties: { + name: "Code generation accuracy", + description: "Agent should generate syntactically correct code", + prompt_snippet: "Write a function that adds two numbers", + positive_cases: ["Output is valid JavaScript", "Function accepts two arguments", "Returns the sum"], + negative_cases: ["Syntax errors present", "Wrong return value"], + order: 1, + }, + }, + { + ref_id: "req-1-3", + node_type: "EvalRequirement", + properties: { + name: "Error handling explanation", + description: "Agent should explain errors clearly", + prompt_snippet: "Explain what a null pointer exception is", + positive_cases: ["Explains the concept clearly", "Provides an example"], + negative_cases: ["Response is too technical", "No explanation given"], + order: 2, + }, + }, + ], + "eval-set-2": [ + { + ref_id: "req-2-1", + node_type: "EvalRequirement", + properties: { + name: "Security vulnerability detection", + description: "Agent should identify SQL injection risks", + prompt_snippet: "Review this query for security issues", + positive_cases: ["Identifies injection risk", "Suggests parameterized queries"], + negative_cases: ["Misses the vulnerability", "No remediation suggested"], + order: 0, + }, + }, + { + ref_id: "req-2-2", + node_type: "EvalRequirement", + properties: { + name: "Refactor suggestion quality", + description: "Agent should suggest meaningful refactors", + prompt_snippet: "How can I improve this function?", + positive_cases: ["Suggestions improve readability", "Performance considered"], + negative_cases: ["Suggestions break functionality", "No explanation provided"], + order: 1, + }, + }, + ], +}; + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ evalSetId: string }> }, +) { + const { evalSetId } = await params; + const nodes = SEED_REQUIREMENTS[evalSetId] ?? []; + return NextResponse.json({ success: true, data: { nodes, total: nodes.length } }); +} + export async function POST(request: NextRequest) { const body = await request.json().catch(() => ({})); const { name, description, prompt_snippet, positive_cases, negative_cases } = diff --git a/src/app/api/workspaces/[slug]/evals/[evalSetId]/requirements/route.ts b/src/app/api/workspaces/[slug]/evals/[evalSetId]/requirements/route.ts index eca958439e..ee111f157b 100644 --- a/src/app/api/workspaces/[slug]/evals/[evalSetId]/requirements/route.ts +++ b/src/app/api/workspaces/[slug]/evals/[evalSetId]/requirements/route.ts @@ -3,6 +3,7 @@ import { getMiddlewareContext, requireAuth } from "@/lib/middleware/utils"; import { getJarvisUrl } from "@/lib/utils/swarm"; import { getWorkspaceSwarmAccess } from "@/lib/helpers/swarm-access"; import { addNode, addEdge } from "@/services/swarm/api/nodes"; +import type { JarvisNode } from "@/types/jarvis"; type RouteParams = { params: Promise<{ slug: string; evalSetId: string }> }; @@ -20,6 +21,74 @@ function handleSwarmAccessError(error: { type: string }) { return NextResponse.json({ error: errorInfo.message }, { status: errorInfo.status }); } +export async function GET(request: NextRequest, { params }: RouteParams) { + try { + const context = getMiddlewareContext(request); + const userOrResponse = requireAuth(context); + if (userOrResponse instanceof NextResponse) return userOrResponse; + + const { slug, evalSetId } = await params; + console.log(`[Evals Requirements GET] slug=${slug}, evalSetId=${evalSetId}, userId=${userOrResponse.id}`); + + const swarmAccessResult = await getWorkspaceSwarmAccess(slug, userOrResponse.id); + if (!swarmAccessResult.success) { + console.warn(`[Evals Requirements GET] Swarm access denied: ${swarmAccessResult.error.type}`); + return handleSwarmAccessError(swarmAccessResult.error); + } + + if (process.env.USE_MOCKS === "true") { + console.log(`[Evals Requirements GET] USE_MOCKS=true, routing to mock endpoint`); + const mockResponse = await fetch( + `${request.nextUrl.origin}/api/mock/evals/${evalSetId}/requirements`, + { method: "GET" }, + ); + return NextResponse.json(await mockResponse.json()); + } + + const { swarmName, swarmApiKey } = swarmAccessResult.data; + const jarvisUrl = getJarvisUrl(swarmName); + console.log(`[Evals Requirements GET] Jarvis URL: ${jarvisUrl}`); + + const edgeType = encodeURIComponent("['HAS_REQUIREMENT']"); + const nodeType = encodeURIComponent("['EvalRequirement']"); + const url = `${jarvisUrl}/v2/nodes/${evalSetId}?expand=edges&edge_type=${edgeType}&node_type=${nodeType}&depth=1`; + + const jarvisRes = await fetch(url, { + headers: { "x-api-token": swarmApiKey }, + }); + + if (!jarvisRes.ok) { + const text = await jarvisRes.text().catch(() => ""); + console.error(`[Evals Requirements GET] Jarvis error ${jarvisRes.status}: ${text}`); + return NextResponse.json( + { error: "Failed to fetch requirements from Jarvis" }, + { status: 502 }, + ); + } + + const jarvisData = await jarvisRes.json(); + const nodes: JarvisNode[] = jarvisData?.nodes ?? []; + const edges: Array<{ target_ref_id: string; properties?: { order?: number }; edge_data?: { order?: number } }> = + jarvisData?.edges ?? []; + + // Merge edge order into each node's properties + for (const node of nodes) { + const edge = edges.find((e) => e.target_ref_id === node.ref_id); + if (edge) { + const order = edge.properties?.order ?? edge.edge_data?.order; + if (order !== undefined) { + node.properties = { ...node.properties, order }; + } + } + } + + return NextResponse.json({ success: true, data: { nodes, total: nodes.length } }); + } catch (error) { + console.error("[Evals/Requirements] GET error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + export async function POST(request: NextRequest, { params }: RouteParams) { try { const context = getMiddlewareContext(request); diff --git a/src/components/evals/EvalSetDetail.tsx b/src/components/evals/EvalSetDetail.tsx index 4e4c831451..00fca0841a 100644 --- a/src/components/evals/EvalSetDetail.tsx +++ b/src/components/evals/EvalSetDetail.tsx @@ -38,7 +38,7 @@ export function EvalSetDetail({ evalSet, onBack }: EvalSetDetailProps) { setLoading(true); try { const res = await fetch( - `/api/workspaces/${slug}/evals?evalSetId=${evalSet.ref_id}`, + `/api/workspaces/${slug}/evals/${evalSet.ref_id}/requirements`, ); const data = await res.json(); const nodes: RequirementNode[] = (data?.data?.nodes ?? []).sort(