Ctrl K
ComunidadGuías…

tests/e2e/flows-api-runs.spec.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { expect, test } from "@playwright/test";
import {
  user1Headers,
  seedState,
  resetState,
} from "./helpers/flows-api-fixtures";

test.beforeEach(async ({ request }) => {
  await resetState(request);
});

test("GET /api/flows/:id/runs returns flow-scoped runs, dispatch context, and node runs", async ({
  request,
}) => {
  await seedState(request, {
    flows: [
      {
        id: "flow-1",
        user_id: "user-1",
        installation_id: 101,
        name: "Review Flow",
        description: null,
        notes: null,
        source_kind: "github",
        status: "active",
        draft_graph: {
          nodes: [],
          edges: [],
          viewport: { x: 0, y: 0, zoom: 1 },
        },
        published_version_id: "version-1",
        created_at: "2026-03-28T10:00:00.000Z",
        updated_at: "2026-03-28T10:00:00.000Z",
      },
    ],
    flowVersions: [
      {
        id: "version-1",
        flow_id: "flow-1",
        version_number: 1,
        graph: { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } },
        created_at: "2026-03-28T10:00:00.000Z",
      },
    ],
    jobRuns: [
      {
        id: "job-1",
        flow_id: "flow-1",
        flow_version_id: "version-1",
        status: "failed",
        error: "lint failed",
        created_at: "2026-03-28T11:00:00.000Z",
        started_at: "2026-03-28T11:01:00.000Z",
        completed_at: "2026-03-28T11:02:00.000Z",
        duration_ms: 60000,
        start_attempts: 2,
        last_start_attempt_at: "2026-03-28T11:01:00.000Z",
        last_start_error: "lint failed",
        last_start_source: "webhook",
        runtime_provider: "trigger",
        runtime_run_id: "run_123",
        metadata: {
          repo_id: "repo-1",
          repo_full_name: "webrenew/blackbox",
          source_type: "pr_opened",
        },
      },
    ],
    flowNodeRuns: [
      {
        id: "node-run-1",
        user_id: "user-1",
        job_run_id: "job-1",
        flow_id: "flow-1",
        flow_version_id: "version-1",
        node_id: "agent-1",
        node_type: "agent",
        node_label: "Reviewer",
        status: "failed",
        started_at: "2026-03-28T11:01:00.000Z",
        completed_at: "2026-03-28T11:02:00.000Z",
        duration_ms: 60000,
        error: "lint failed",
        output: null,
        created_at: "2026-03-28T11:01:00.000Z",
      },
    ],
    dispatchEvents: [
      {
        id: "dispatch-1",
        job_run_id: "job-1",
        trigger_id: null,
        outcome: "start_failed",
        reason: "RUNTIME_HANDLE_PERSIST_FAILED",
        created_at: "2026-03-28T11:01:30.000Z",
      },
    ],
  });

  const response = await request.get("/api/flows/flow-1/runs?limit=5", {
    headers: user1Headers,
  });
  expect(response.ok()).toBeTruthy();

  const payload = await response.json();
  expect(payload.runs).toHaveLength(1);
  expect(payload.runs[0].id).toBe("job-1");
  expect(payload.runs[0].source_kind).toBe("flow");
  expect(payload.runs[0].latest_dispatch_event.reason).toBe(
    "RUNTIME_HANDLE_PERSIST_FAILED"
  );
  expect(payload.runs[0].node_runs).toHaveLength(1);
  expect(payload.runs[0].node_runs[0].node_label).toBe("Reviewer");
});

test("GET /api/flows/:id/runs/:runId returns observability detail for a flow run", async ({
  request,
}) => {
  await seedState(request, {
    flows: [
      {
        id: "flow-1",
        user_id: "user-1",
        installation_id: 101,
        name: "Review Flow",
        description: null,
        notes: null,
        source_kind: "github",
        status: "active",
        draft_graph: {
          nodes: [],
          edges: [],
          viewport: { x: 0, y: 0, zoom: 1 },
        },
        published_version_id: "version-1",
        created_at: "2026-03-28T10:00:00.000Z",
        updated_at: "2026-03-28T10:00:00.000Z",
      },
    ],
    flowVersions: [
      {
        id: "version-1",
        flow_id: "flow-1",
        version_number: 1,
        graph: { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } },
        created_at: "2026-03-28T10:00:00.000Z",
      },
    ],
    jobRuns: [
      {
        id: "job-1",
        flow_id: "flow-1",
        flow_version_id: "version-1",
        status: "success",
        error: null,
        created_at: "2026-03-28T11:00:00.000Z",
        started_at: "2026-03-28T11:01:00.000Z",
        completed_at: "2026-03-28T11:02:00.000Z",
        duration_ms: 60000,
        input_tokens: 120,
        output_tokens: 80,
        start_attempts: 1,
        last_start_attempt_at: "2026-03-28T11:01:00.000Z",
        last_start_error: null,
        last_start_source: "webhook",
        runtime_provider: "trigger",
        runtime_run_id: "run_123",
        metadata: {
          repo_id: "repo-1",
          repo_full_name: "webrenew/blackbox",
          source_type: "pr_opened",
        },
      },
    ],
    flowNodeRuns: [
      {
        id: "node-run-1",
        user_id: "user-1",
        job_run_id: "job-1",
        flow_id: "flow-1",
        flow_version_id: "version-1",
        node_id: "agent-1",
        node_type: "agent",
        node_label: "Reviewer",
        status: "success",
        started_at: "2026-03-28T11:01:00.000Z",
        completed_at: "2026-03-28T11:02:00.000Z",
        duration_ms: 60000,
        error: null,
        output: { summary: "LGTM" },
        created_at: "2026-03-28T11:01:00.000Z",
      },
    ],
    dispatchEvents: [
      {
        id: "dispatch-1",
        job_run_id: "job-1",
        trigger_id: null,
        flow_id: "flow-1",
        flow_version_id: "version-1",
        event_kind: "enqueue",
        outcome: "queued",
        reason: null,
        metadata: { source: "webhook" },
        created_at: "2026-03-28T11:00:30.000Z",
      },
      {
        id: "dispatch-2",
        job_run_id: "job-1",
        trigger_id: null,
        flow_id: "flow-1",
        flow_version_id: "version-1",
        event_kind: "start",
        outcome: "started",
        reason: null,
        metadata: { runtime_run_id: "run_123" },
        created_at: "2026-03-28T11:01:00.000Z",
      },
    ],
    aiCalls: [
      {
        id: "call-1",
        user_id: "user-1",
        type: "agent",
        model: "minimax/minimax-m2.5",
        input_tokens: 120,
        output_tokens: 80,
        total_tokens: 200,
        duration_ms: 45000,
        started_at: "2026-03-28T11:01:05.000Z",
        completed_at: "2026-03-28T11:01:50.000Z",
        status: "success",
        error: null,
        conversation_id: null,
        job_run_id: "job-1",
        repo_id: "repo-1",
        limit_claim_id: null,
        cancel_requested_at: null,
        control_state: "active",
        runtime_command_id: "cmd_123",
        tool_calls_count: 1,
        tool_calls: [
          {
            name: "postComment",
            input_preview: "Looks good",
            output_preview: "commented",
          },
        ],
        metadata: { agent_id: "agent-1" },
      },
    ],
    aiCallEvents: [
      {
        id: "call-event-1",
        ai_call_id: "call-1",
        user_id: "user-1",
        conversation_id: null,
        repo_id: "repo-1",
        event_type: "started",
        tool_name: null,
        message: "Agent started",
        payload: {},
        created_at: "2026-03-28T11:01:05.000Z",
      },
      {
        id: "call-event-2",
        ai_call_id: "call-1",
        user_id: "user-1",
        conversation_id: null,
        repo_id: "repo-1",
        event_type: "finished",
        tool_name: null,
        message: "Agent finished",
        payload: { finishReason: "stop" },
        created_at: "2026-03-28T11:01:50.000Z",
      },
    ],
  });

  const response = await request.get("/api/flows/flow-1/runs/job-1", {
    headers: user1Headers,
  });
  expect(response.ok()).toBeTruthy();

  const payload = await response.json();
  expect(payload.run.id).toBe("job-1");
  expect(payload.run.dispatch_events).toHaveLength(2);
  expect(payload.run.dispatch_events[1].event_kind).toBe("start");
  expect(payload.run.ai_calls).toHaveLength(1);
  expect(payload.run.ai_calls[0].tool_calls).toHaveLength(1);
  expect(payload.run.ai_calls[0].events).toHaveLength(2);
  expect(payload.run.node_runs[0].output.summary).toBe("LGTM");
});

test("flow run endpoints keep historical source_type after a flow is republished", async ({
  request,
}) => {
  await seedState(request, {
    flows: [
      {
        id: "flow-1",
        user_id: "user-1",
        installation_id: 101,
        name: "Review Flow",
        description: null,
        notes: null,
        source_kind: "github",
        status: "active",
        draft_graph: {
          nodes: [],
          edges: [],
          viewport: { x: 0, y: 0, zoom: 1 },
        },
        published_version_id: "version-2",
        created_at: "2026-03-28T10:00:00.000Z",
        updated_at: "2026-03-28T10:00:00.000Z",
      },
    ],
    flowVersions: [
      {
        id: "version-1",
        flow_id: "flow-1",
        version_number: 1,
        graph: {
          nodes: [
            {
              id: "start",
              type: "start",
              position: { x: 0, y: 0 },
              data: { label: "PR opened", event: "pr_opened" },
            },
            {
              id: "agent-1",
              type: "agent",
              position: { x: 100, y: 0 },
              data: { label: "Reviewer", agentId: "agent-1" },
            },
            {
              id: "end",
              type: "end",
              position: { x: 200, y: 0 },
              data: { label: "Done" },
            },
          ],
          edges: [
            { id: "e1", source: "start", target: "agent-1" },
            { id: "e2", source: "agent-1", target: "end" },
          ],
          viewport: { x: 0, y: 0, zoom: 1 },
        },
        created_at: "2026-03-28T10:00:00.000Z",
      },
      {
        id: "version-2",
        flow_id: "flow-1",
        version_number: 2,
        graph: {
          nodes: [
            {
              id: "start",
              type: "start",
              position: { x: 0, y: 0 },
              data: { label: "Issue opened", event: "issue_opened" },
            },
            {
              id: "agent-2",
              type: "agent",
              position: { x: 100, y: 0 },
              data: { label: "Triage", agentId: "agent-2" },
            },
            {
              id: "end",
              type: "end",
              position: { x: 200, y: 0 },
              data: { label: "Done" },
            },
          ],
          edges: [
            { id: "e1", source: "start", target: "agent-2" },
            { id: "e2", source: "agent-2", target: "end" },
          ],
          viewport: { x: 0, y: 0, zoom: 1 },
        },
        created_at: "2026-03-28T10:05:00.000Z",
      },
    ],
    jobRuns: [
      {
        id: "job-1",
        flow_id: "flow-1",
        flow_version_id: "version-1",
        status: "success",
        error: null,
        created_at: "2026-03-28T11:00:00.000Z",
        started_at: "2026-03-28T11:01:00.000Z",
        completed_at: "2026-03-28T11:02:00.000Z",
        duration_ms: 60000,
        start_attempts: 1,
        last_start_attempt_at: "2026-03-28T11:01:00.000Z",
        last_start_error: null,
        last_start_source: "webhook",
        runtime_provider: "trigger",
        runtime_run_id: "run_123",
        metadata: {
          repo_id: "repo-1",
          repo_full_name: "webrenew/blackbox",
        },
      },
    ],
  });

  const runsResponse = await request.get("/api/flows/flow-1/runs?limit=5", {
    headers: user1Headers,
  });
  expect(runsResponse.ok()).toBeTruthy();
  const runsPayload = await runsResponse.json();
  expect(runsPayload.runs[0].source_kind).toBe("flow");
  expect(runsPayload.runs[0].source_type).toBe("pr_opened");

  const detailResponse = await request.get("/api/flows/flow-1/runs/job-1", {
    headers: user1Headers,
  });
  expect(detailResponse.ok()).toBeTruthy();
  const detailPayload = await detailResponse.json();
  expect(detailPayload.run.source_kind).toBe("flow");
  expect(detailPayload.run.source_type).toBe("pr_opened");
});