spanDetail.test.tsx5.1 KBView on GitHub
/**
 * The detail panel. Three things have to hold:
 *   - a tool-call span shows its name and both payloads,
 *   - a 200KB payload truncates behind an explicit expand instead of freezing the tab,
 *   - an execution with no tool calls says so — that is a finding, not a spinner.
 */

import { fireEvent, render, screen, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router';
import type { ExecutionTree } from '@zero/server/execution-tree';
import { SpanDetail } from '@/modules/agentExecutions/components/SpanDetail';
import { buildTraceFixture } from './traceFixture';

function renderDetail(spanId: string, tree: ExecutionTree = buildTraceFixture()) {
  const onSelectSpan = jest.fn();
  render(
    <MemoryRouter>
      <SpanDetail tree={tree} spanId={spanId} onSelectSpan={onSelectSpan} />
    </MemoryRouter>,
  );
  return { onSelectSpan };
}

/** The fixture with an argument blob big enough to matter. */
function treeWithOversizedPayload(): ExecutionTree {
  const tree = buildTraceFixture();
  const node = tree.nodes.find((candidate) => candidate.runId === 'subagent-b')!;
  node.toolCalls[0]!.arguments = { blob: 'x'.repeat(50_000) };
  return tree;
}

describe('SpanDetail — tool-call span', () => {
  it('shows the tool name, arguments and result', () => {
    renderDetail('t-b1');

    expect(screen.getByTestId('span-detail-tool-name')).toHaveTextContent('search-crm');
    expect(
      within(screen.getByTestId('payload-Arguments')).getByText(/conversationId/),
    ).toBeInTheDocument();
    expect(
      within(screen.getByTestId('payload-Result')).getByText(/CRM timed out/),
    ).toBeInTheDocument();
  });

  it('says an unmeasured duration was never recorded instead of printing a confident 0ms', () => {
    renderDetail('t-b1');

    expect(screen.getByTestId('span-detail-duration')).toHaveTextContent('duration never recorded');
    expect(screen.getByTestId('span-detail-point-note')).toHaveTextContent(
      /no start time was recorded|never measured/i,
    );
    // The only timestamp such a row has is when it ENDED — say that, not "started".
    expect(screen.getByTestId('span-detail-point-note')).toHaveTextContent('Finished at +900ms');
  });

  it('shows the real elapsed time for a measured tool call', () => {
    renderDetail('t-c1');

    expect(screen.getByTestId('span-detail-duration')).toHaveTextContent('900ms');
    expect(screen.getByTestId('span-detail-tool-duration')).toHaveTextContent('900ms');
    // A measured row has a genuine start, so the header dates it from there.
    expect(screen.getByTestId('span-detail')).toHaveTextContent('started +5.6s');
    expect(screen.queryByTestId('span-detail-point-note')).not.toBeInTheDocument();
  });

  it('reports a measured 0ms call as 0ms, not as unrecorded', () => {
    renderDetail('t-c4');

    // The discriminator again: `durationMs === 0` is a real answer here.
    expect(screen.getByTestId('span-detail-duration')).toHaveTextContent('0ms');
    expect(screen.getByTestId('span-detail-duration')).not.toHaveTextContent('never recorded');
    expect(screen.queryByTestId('span-detail-point-note')).not.toBeInTheDocument();
  });

  it('truncates an oversized payload behind an explicit expand', () => {
    renderDetail('t-b1', treeWithOversizedPayload());

    const payload = screen.getByTestId('payload-Arguments');
    const notice = screen.getByTestId('payload-truncated-Arguments');
    expect(notice).toHaveTextContent(/Truncated/);

    const rendered = within(payload).getByText(/xxxx/);
    expect(rendered.textContent!.length).toBeLessThan(10_000);

    fireEvent.click(within(notice).getByRole('button', { name: /show full payload/i }));
    expect(screen.queryByTestId('payload-truncated-Arguments')).not.toBeInTheDocument();
    expect(within(payload).getByText(/xxxx/).textContent!.length).toBeGreaterThan(49_000);
  });
});

describe('SpanDetail — execution span', () => {
  it('shows agent, status, conversation link, and jumps to children', () => {
    const { onSelectSpan } = renderDetail('review-a');

    expect(screen.getByTestId('span-detail-agent-name')).toHaveTextContent('review-conversation-a');
    expect(screen.getByTestId('span-detail-status')).toHaveTextContent('completed');
    expect(screen.getByTestId('span-detail-conversation-link')).toHaveAttribute(
      'href',
      '/conversations/c/conv-a',
    );

    fireEvent.click(screen.getByRole('button', { name: /draft-email/ }));
    expect(onSelectSpan).toHaveBeenCalledWith('subagent-a');
  });

  it('shows the empty state for an execution that recorded no tool calls', () => {
    renderDetail('review-a');

    const empty = screen.getByTestId('span-detail-no-tool-calls');
    expect(empty).toHaveTextContent('No tool calls recorded');
    expect(screen.queryByRole('status')).not.toBeInTheDocument();
  });

  it('prompts to pick a span when nothing is selected', () => {
    const onSelectSpan = jest.fn();
    render(
      <MemoryRouter>
        <SpanDetail tree={buildTraceFixture()} spanId={null} onSelectSpan={onSelectSpan} />
      </MemoryRouter>,
    );
    expect(screen.getByTestId('span-detail-empty-selection')).toBeInTheDocument();
  });
});