test/openagents_web/controllers/trace_controller_test.exs

58e6347eeb72 · 3 KB

defmodule OpenAgentsWeb.TraceControllerTest do
  @moduledoc """
  Accept ATIF v1 trace uploads at `POST /api/v1/traces`.
  """

  use OpenAgentsWeb.ConnCase, async: false

  alias OpenAgents.Repo
  alias OpenAgents.Traces.Trace

  describe "POST /api/v1/traces" do
    test "returns the trace id and url for a valid ATIF v1.7 document", %{conn: conn} do
      body =
        conn
        |> put_chat_api_token("trace-valid")
        |> post(~p"/api/v1/traces", %{
          "schema_version" => "ATIF/1.7",
          "trace" => %{"events" => [%{"type" => "step"}]}
        })
        |> json_response(201)

      assert %{"id" => id, "url" => url} = body
      assert is_binary(id)
      assert url =~ "/api/v1/traces/#{id}"
      assert body["visibility"] == "dark"
      assert is_integer(body["byte_size"])
      assert is_binary(body["digest"])
      assert String.starts_with?(body["digest"], "sha256:")
    end

    test "returns the existing trace when the same owner uploads the same document again", %{
      conn: conn
    } do
      document = %{
        "schema_version" => "ATIF/1.7",
        "trace" => %{"events" => [%{"type" => "step"}]}
      }

      first =
        conn
        |> put_chat_api_token("trace-dedup")
        |> post(~p"/api/v1/traces", document)
        |> json_response(201)

      second =
        conn
        |> put_chat_api_token("trace-dedup")
        |> post(~p"/api/v1/traces", document)
        |> json_response(200)

      assert second["id"] == first["id"]
      assert second["digest"] == first["digest"]
      assert second["byte_size"] == first["byte_size"]
      assert Repo.aggregate(Trace, :count) == 1
    end

    test "refuses a body that exceeds the size ceiling", %{conn: conn} do
      big = String.duplicate("x", 10_485_761)

      body =
        conn
        |> put_chat_api_token("trace-oversize")
        |> post(~p"/api/v1/traces", %{
          "schema_version" => "ATIF/1.7",
          "data" => big
        })
        |> json_response(413)

      assert body["code"] == "trace_body_too_large"
    end

    test "rejects an unauthenticated call", %{conn: conn} do
      conn
      |> post(~p"/api/v1/traces", %{"schema_version" => "ATIF/1.7"})
      |> assert_api_error(401, "unauthenticated")
    end

    test "rejects an invalid ATIF schema_version", %{conn: conn} do
      conn =
        conn
        |> put_chat_api_token("trace-invalid")
        |> post(~p"/api/v1/traces", %{"schema_version" => "ATIF/2.0"})

      assert_api_error(conn, 422, "validation_failed",
        errors: %{"document" => ["The document is not a valid ATIF v1 object."]}
      )
    end

    test "rejects a document with no schema_version", %{conn: conn} do
      conn =
        conn
        |> put_chat_api_token("trace-no-version")
        |> post(~p"/api/v1/traces", %{"trace" => %{}})

      assert_api_error(conn, 422, "validation_failed")
    end

    test "uses dark visibility by default and allows an explicit tier", %{conn: conn} do
      body =
        conn
        |> put_chat_api_token("trace-visibility")
        |> post("/api/v1/traces?visibility=ledger", %{
          "schema_version" => "ATIF/1.7",
          "trace" => %{}
        })
        |> json_response(201)

      assert body["visibility"] == "ledger"
    end

    test "different owners uploading the same document get separate traces", %{conn: conn} do
      document = %{
        "schema_version" => "ATIF/1.7",
        "trace" => %{"events" => [%{"type" => "step"}]}
      }

      first =
        conn
        |> put_chat_api_token("trace-owner-one")
        |> post(~p"/api/v1/traces", document)
        |> json_response(201)

      second =
        conn
        |> put_chat_api_token("trace-owner-two")
        |> post(~p"/api/v1/traces", document)
        |> json_response(201)

      refute first["id"] == second["id"]
    end
  end
end