test/openagents/artifact_catalog_test.exs

58e6347eeb72 · 8 KB

defmodule OpenAgents.ArtifactCatalogTest do
  use OpenAgents.DataCase, async: true

  import OpenAgents.ArtifactCatalogFixtures

  alias OpenAgents.ArtifactCatalog
  alias OpenAgents.ArtifactCatalog.Listing
  alias OpenAgents.ArtifactCatalog.Receipt
  alias OpenAgents.Repo

  test "publishes and discovers an exact safe projection" do
    attributes = listing_attributes()
    assert {:ok, listing} = ArtifactCatalog.publish_listing(attributes)

    assert [^listing] = ArtifactCatalog.list_public_listings()
    assert byte_size(listing.license_digest) == 64
    assert byte_size(listing.listing_digest) == 64

    projection = Listing.public_projection(listing)
    refute Map.has_key?(projection, "source_ref")
    refute inspect(projection) =~ attributes.source_ref
    assert projection["artifact_digest"] == attributes.artifact_digest
    assert projection["provenance"]["digest"] == attributes.provenance_digest
    assert projection["buyer"]["name"] == "OpenAgents continual-learning program"
    assert projection["verification_policy"]["policy_ref"] == "verification:artifact-v1"
  end

  test "searches compatible listings by text, type, buyer class, and digest" do
    matching =
      publish_listing!(%{
        artifact_type: "trace",
        owner_description: "Browser navigation trace",
        buyer_class: "openagents_evaluation"
      })

    _other = publish_listing!(%{owner_description: "Database query dataset"})

    assert [^matching] = ArtifactCatalog.list_public_listings(%{"q" => "navigation"})
    assert [^matching] = ArtifactCatalog.list_public_listings(%{"artifact_type" => "trace"})

    assert [^matching] =
             ArtifactCatalog.list_public_listings(%{"buyer_class" => "openagents_evaluation"})

    assert [^matching] =
             ArtifactCatalog.list_public_listings(%{
               "q" => binary_part(matching.artifact_digest, 0, 20)
             })
  end

  test "requires explicit opt-in and rejects private source metadata" do
    no_opt_in =
      listing_attributes(%{
        license_terms: %{"opt_in" => false, "allowed_uses" => ["evaluation"]}
      })

    assert {:error, changeset} = ArtifactCatalog.publish_listing(no_opt_in)
    assert "must record explicit opt-in" in errors_on(changeset).license_terms

    private_projection =
      listing_attributes(%{
        coverage: %{"domains" => ["tool selection"], "source_uri" => "s3://private/source"}
      })

    assert {:error, changeset} = ArtifactCatalog.publish_listing(private_projection)
    assert "contains private source metadata" in errors_on(changeset).coverage
  end

  test "binds a supplied listing identity to canonical license and listing digests" do
    assert {:error, changeset} =
             listing_attributes(%{license_digest: String.duplicate("f", 64)})
             |> ArtifactCatalog.publish_listing()

    assert "does not match the canonical identity" in errors_on(changeset).license_digest

    assert {:error, changeset} =
             listing_attributes(%{listing_digest: String.duplicate("e", 64)})
             |> ArtifactCatalog.publish_listing()

    assert "does not match the canonical identity" in errors_on(changeset).listing_digest
  end

  test "admits source access only after an accepted delivery or evaluation flow" do
    listing = publish_listing!()

    assert {:error, :not_authorized} =
             ArtifactCatalog.authorize_source_access(listing.id, %{
               purpose: "evaluation",
               buyer_ref: "buyer:openagents",
               acceptance_ref: "missing"
             })

    assert {:ok, offer} =
             ArtifactCatalog.record_transaction(
               listing.id,
               "offer",
               transaction_attributes(listing, listing.publication_receipt_ref)
             )

    assert {:ok, acceptance} =
             ArtifactCatalog.record_transaction(
               listing.id,
               "acceptance",
               transaction_attributes(listing, offer.receipt_ref)
             )

    assert {:error, :not_authorized} =
             ArtifactCatalog.authorize_source_access(listing.id, %{
               purpose: "evaluation",
               buyer_ref: "buyer:other",
               acceptance_ref: acceptance.receipt_ref
             })

    assert {:ok, authorization} =
             ArtifactCatalog.authorize_source_access(listing.id, %{
               purpose: "evaluation",
               buyer_ref: "buyer:openagents",
               acceptance_ref: acceptance.receipt_ref
             })

    assert authorization.source_ref == listing.source_ref
    assert authorization.artifact_digest == listing.artifact_digest
  end

  test "rejects delivery when any accepted digest changes" do
    listing = publish_listing!()

    assert {:ok, offer} =
             ArtifactCatalog.record_transaction(
               listing.id,
               "offer",
               transaction_attributes(listing, listing.publication_receipt_ref)
             )

    assert {:ok, acceptance} =
             ArtifactCatalog.record_transaction(
               listing.id,
               "acceptance",
               transaction_attributes(listing, offer.receipt_ref)
             )

    mismatched =
      transaction_attributes(listing, acceptance.receipt_ref, %{
        artifact_digest: String.duplicate("0", 64)
      })

    assert {:error, {:digest_mismatch, :artifact_digest}} =
             ArtifactCatalog.record_transaction(listing.id, "delivery", mismatched)

    refute Repo.get_by(Receipt, listing_id: listing.id, action: "delivery")
  end

  test "records the bounded transaction chain without settling funds" do
    listing = publish_listing!()

    receipts =
      Enum.reduce(
        ~w(offer acceptance delivery verification settlement),
        {listing.publication_receipt_ref, []},
        fn action, {predecessor_ref, receipts} ->
          assert {:ok, receipt} =
                   ArtifactCatalog.record_transaction(
                     listing.id,
                     action,
                     transaction_attributes(listing, predecessor_ref)
                   )

          {receipt.receipt_ref, [receipt | receipts]}
        end
      )
      |> elem(1)
      |> Enum.reverse()

    assert Enum.map(receipts, & &1.action) ==
             ~w(offer acceptance delivery verification settlement)

    settlement = List.last(receipts)
    assert settlement.status == "settled"
    refute Map.has_key?(settlement.metadata, "amount")
  end

  test "requires an external settlement receipt reference" do
    listing = publish_listing!()

    predecessor_ref =
      Enum.reduce(~w(offer acceptance delivery verification), listing.publication_receipt_ref, fn
        action, predecessor_ref ->
          assert {:ok, receipt} =
                   ArtifactCatalog.record_transaction(
                     listing.id,
                     action,
                     transaction_attributes(listing, predecessor_ref)
                   )

          receipt.receipt_ref
      end)

    assert {:error, :missing_settlement_reference} =
             ArtifactCatalog.record_transaction(
               listing.id,
               "settlement",
               transaction_attributes(listing, predecessor_ref, %{external_ref: nil})
             )
  end

  test "removal blocks discovery and new transactions while preserving receipts" do
    listing = publish_listing!()

    assert {:ok, offer} =
             ArtifactCatalog.record_transaction(
               listing.id,
               "offer",
               transaction_attributes(listing, listing.publication_receipt_ref)
             )

    assert {:ok, removed} =
             ArtifactCatalog.remove_listing(listing.id, %{
               reason: "Owner withdrew this artifact",
               receipt_ref: "artifact-removal:test",
               actor_ref: "operator:test"
             })

    assert removed.state == "removed"
    assert {:error, :not_found} = ArtifactCatalog.get_public_listing(listing.id)
    assert ArtifactCatalog.list_public_listings() == []

    assert {:error, :listing_removed} =
             ArtifactCatalog.record_transaction(
               listing.id,
               "acceptance",
               transaction_attributes(listing, offer.receipt_ref)
             )

    assert {:ok, history} = ArtifactCatalog.export_listing_history(listing.id)
    assert history["state"] == "removed"

    assert Enum.map(history["receipts"], & &1["action"]) ==
             ~w(publication offer removal)
  end

  test "a stale license blocks discovery and new transactions" do
    listing = publish_listing!()

    listing
    |> Ecto.Changeset.change(license_expires_at: DateTime.add(DateTime.utc_now(), -1, :second))
    |> Repo.update!()

    assert {:error, :not_found} = ArtifactCatalog.get_public_listing(listing.id)
    assert ArtifactCatalog.list_public_listings() == []

    assert {:error, :stale_license} =
             ArtifactCatalog.record_transaction(
               listing.id,
               "offer",
               transaction_attributes(listing, listing.publication_receipt_ref)
             )
  end
end