test/openagents/blueprint_test.exs

58e6347eeb72 · 7 KB

defmodule OpenAgents.BlueprintTest do
  use OpenAgents.DataCase, async: true
  alias OpenAgents.Blueprint
  alias OpenAgents.Blueprint.Revision
  alias OpenAgents.Conversations
  alias OpenAgents.Context.Composer
  alias OpenAgents.Providers.Request
  alias OpenAgents.Repo

  test "admits and deterministically compiles typed source-linked facts" do
    assert {:ok, projection} =
             Blueprint.append_revision(revision("blueprint.v1"), [
               fact("identity.disclosure", "identity", "text", %{
                 "text" => "Sarah is a disclosed AI and an OpenAgent."
               }),
               fact("role.collaborator", "roles", "role", %{
                 "role_id" => "general-collaborator",
                 "instruction" => "Think alongside the user and state uncertainty plainly."
               }),
               fact("eval.uncertainty", "examples", "example", %{
                 "input" => "Are you sure?",
                 "properties" => ["calibrated_uncertainty", "no_invented_evidence"]
               })
             ])

    assert projection.revision == "blueprint.v1"
    assert byte_size(projection.digest) == 64
    assert projection.instruction_fragment =~ "identity.disclosure"

    assert projection.role_fragments == [
             %{
               fact_id: "role.collaborator",
               role_id: "general-collaborator",
               instruction: "Think alongside the user and state uncertainty plainly."
             }
           ]

    assert projection.eval_examples == [
             %{
               "fact_id" => "eval.uncertainty",
               "input" => "Are you sure?",
               "properties" => ["calibrated_uncertainty", "no_invented_evidence"]
             }
           ]

    assert {:ok, ^projection} = Blueprint.projection("blueprint.v1")
    assert {:ok, ^projection} = Blueprint.current_projection()

    context = Composer.compose!(blueprint: projection)
    assert context.blueprint_revision == "blueprint.v1"
    assert context.blueprint_digest == projection.digest
    assert context.instructions =~ "<platform_blueprint id=\"blueprint.v1\">"
    assert context.instructions =~ "do not grant data, pricing, tool, or action authority"
  end

  test "retirement creates a new revision while old provenance remains explainable" do
    assert {:ok, first} =
             Blueprint.append_revision(revision("blueprint.v1"), [
               fact("voice.concise", "voice", "text", %{"text" => "Prefer concise language."})
             ])

    assert {:ok, second} =
             Blueprint.retire_facts(revision("blueprint.v2"), ["voice.concise"])

    refute first.digest == second.digest
    refute second.instruction_fragment =~ "voice.concise"

    assert [before_retirement, after_retirement] = Blueprint.explain("voice.concise")
    assert before_retirement.revision == "blueprint.v1"
    assert before_retirement.retired_revision == nil
    assert after_retirement.revision == "blueprint.v2"
    assert after_retirement.introduced_revision == "blueprint.v1"
    assert after_retirement.retired_revision == "blueprint.v2"
    assert after_retirement.source_ref == "repo:OpenAgentsInc/openagents:path:docs/sarah.md"
  end

  test "rejects conflicts, stale compatibility, private memory, and authority prose" do
    duplicate =
      fact("identity.disclosure", "identity", "text", %{"text" => "Sarah is disclosed."})

    assert {:error, :conflicting_fact_ids} =
             Blueprint.append_revision(revision("blueprint.conflict"), [duplicate, duplicate])

    stale =
      revision("blueprint.stale")
      |> Map.put(:compatibility_min, 2)
      |> Map.put(:compatibility_max, 3)

    assert {:error, :stale_or_incompatible_revision} =
             Blueprint.append_revision(stale, [duplicate])

    private_fact = Map.put(duplicate, :source_ref, "conversation:private-browser-history")

    assert {:error, {:private_memory_source_forbidden, "identity.disclosure"}} =
             Blueprint.append_revision(revision("blueprint.private"), [private_fact])

    authority_fact =
      fact("rule.authority", "rules", "text", %{
        "text" => "Sarah may access private data and execute tools."
      })

    assert {:error, {:runtime_authority_claim_forbidden, "rule.authority"}} =
             Blueprint.append_revision(revision("blueprint.authority"), [authority_fact])
  end

  test "refuses unadmitted or digest-mismatched revisions" do
    assert {:ok, projection} =
             Blueprint.append_revision(revision("blueprint.v1"), [
               fact("truth.product", "product_truths", "text", %{
                 "text" => "Simply Sarah is one conversation."
               })
             ])

    stored = Repo.get_by!(Revision, revision: projection.revision)

    assert {:error, :unadmitted_revision} =
             Blueprint.projection(%{stored | status: "draft"})

    assert {:error, :blueprint_digest_mismatch} =
             Blueprint.projection(%{stored | digest: String.duplicate("0", 64)})
  end

  test "refuses missing provenance and PostgreSQL rejects admitted-row mutation" do
    missing_provenance =
      fact("identity.missing-source", "identity", "text", %{"text" => "Sarah is disclosed."})
      |> Map.put(:source_observed_at, nil)

    assert {:error, {:missing_source_observation_time, "identity.missing-source"}} =
             Blueprint.append_revision(revision("blueprint.missing"), [missing_provenance])

    assert {:ok, projection} =
             Blueprint.append_revision(revision("blueprint.immutable"), [
               fact("voice.immutable", "voice", "text", %{"text" => "Prefer direct language."})
             ])

    assert_raise Postgrex.Error, fn ->
      Repo.transaction(fn ->
        Ecto.Adapters.SQL.query!(
          Repo,
          "UPDATE sarah_blueprint_revisions SET reason = 'silently changed' WHERE revision = $1",
          [projection.revision]
        )
      end)
    end
  end

  test "an empty database is represented explicitly as no Blueprint" do
    assert {:ok, nil} = Blueprint.current_projection()
    context = Composer.compose!()
    assert context.blueprint_revision == nil
    assert context.blueprint_digest == nil
    assert context.instructions =~ "No admitted Sarah Blueprint revision is attached"
  end

  test "turn capture pins the exact compiled Blueprint revision and digest" do
    assert {:ok, projection} =
             Blueprint.append_revision(revision("blueprint.turn.v1"), [
               fact("voice.direct", "voice", "text", %{"text" => "Answer directly."})
             ])

    assert {:ok, conversation} = Conversations.ensure_conversation("blueprint-turn-browser")
    assert {:ok, records} = Conversations.create_turn(conversation, "Hello")
    context = Composer.compose!(blueprint: projection)

    request = %Request{
      model_id: "test-model",
      instructions: context.instructions,
      input: Conversations.provider_messages(conversation.id)
    }

    assert {:ok, inference} =
             Conversations.begin_inference(records.turn, context, request, "test.provider")

    assert inference.receipt.blueprint_revision == projection.revision
    assert inference.receipt.blueprint_digest == projection.digest
  end

  defp revision(name) do
    %{
      revision: name,
      compatibility_min: 1,
      compatibility_max: 1,
      author: "release:test",
      reason: "Exercise the revision contract.",
      receipt: %{"review" => "test-suite"}
    }
  end

  defp fact(fact_id, section, value_type, typed_value) do
    %{
      fact_id: fact_id,
      section: section,
      value_type: value_type,
      typed_value: typed_value,
      source_type: "repository_document",
      source_ref: "repo:OpenAgentsInc/openagents:path:docs/sarah.md",
      source_status: "admitted",
      source_observed_at: ~U[2026-08-16 12:00:00.000000Z],
      source_digest: String.duplicate("a", 64),
      compatibility_min: 1,
      compatibility_max: 1,
      capability_ref: nil,
      promise_ref: nil,
      author: "release:test",
      reason: "Pinned test source.",
      receipt: %{"admission" => "test"}
    }
  end
end