test/openagents/tools/conversation_execution_context_test.exs

main at 58e6347eeb72 · 4 KB

defmodule OpenAgents.Tools.ConversationExecutionContextTest do
  use OpenAgents.DataCase, async: false

  alias OpenAgents.AccountsFixtures
  alias OpenAgents.Conversations
  alias OpenAgents.Machines
  alias OpenAgents.Modules.{Router, RoutingPolicy, SurfacePolicy}
  alias OpenAgents.Provenance.Canonical
  alias OpenAgents.Tools.{ConversationExecutionContext, Registry, ScvDeploy}

  setup do
    original_admin_ids = Application.get_env(:openagents, :admin_github_ids, [])

    on_exit(fn ->
      Application.put_env(:openagents, :admin_github_ids, original_admin_ids)
    end)

    :ok
  end

  test "text and voice share conversation authorities and operator receipts" do
    user = AccountsFixtures.repository_user_fixture("conversation-context-operator")
    {:ok, conversation} = Conversations.ensure_conversation(user)

    Application.put_env(:openagents, :admin_github_ids, [user.github_id])

    text = build("text", conversation, user)
    voice = build("voice", conversation, user)

    assert text.authorities == voice.authorities
    assert text.approval_receipts == voice.approval_receipts
    assert "scv.deploy" in text.authorities
    assert "repository.read" in text.authorities
    assert text.owner_user_id == user.id
    assert voice.owner_user_id == user.id

    assert Enum.any?(text.approval_receipts, fn receipt ->
             receipt["module_id"] == "sarah.tool.scv_deploy.v1" and
               receipt["scope_ref"] == "conversation:#{conversation.id}" and
               receipt["receipt_ref"] == "operator:#{user.id}"
           end)

    assert {:ok, snapshot} = Registry.build([ScvDeploy])
    assert {:ok, artifact} = Registry.module_for_tool(snapshot, "scv_deploy", 1)
    assert :ok = SurfacePolicy.authorize_execution(artifact, voice)

    assert %RoutingPolicy{id: "sarah.routing.policy.operator.v1"} =
             policy = ConversationExecutionContext.routing_policy(user.id)

    proposal = %{
      "module_id" => artifact.module_id,
      "version" => artifact.version,
      "artifact_digest" => artifact.artifact_digest,
      "registry_digest" => snapshot.digest
    }

    route_input = %{
      intent_digest: Canonical.sha256("deploy an SCV"),
      required_capability: "scv.deploy",
      required_side_effect: "external_effect",
      surface: "text",
      data_scope: "browser_conversation",
      authorities: text.authorities,
      proposal: proposal,
      exact_proposal: true
    }

    assert {:ok, decision} = Router.route(snapshot, policy, route_input)
    assert decision.status == "selected"
    assert {:ok, ^artifact} = Router.revalidate(decision, snapshot, policy, route_input)
  end

  test "non-operators receive the shared authorities without an SCV receipt" do
    user = AccountsFixtures.repository_user_fixture("conversation-context-member")
    {:ok, conversation} = Conversations.ensure_conversation(user)

    context = build("voice", conversation, user)

    assert "scv.deploy" in context.authorities

    refute Enum.any?(context.approval_receipts, fn receipt ->
             receipt["module_id"] == "sarah.tool.scv_deploy.v1"
           end)

    assert {:ok, snapshot} = Registry.build([ScvDeploy])
    assert {:ok, artifact} = Registry.module_for_tool(snapshot, "scv_deploy", 1)

    assert {:error, :module_approval_required} =
             SurfacePolicy.authorize_execution(artifact, context)

    assert %RoutingPolicy{id: "sarah.routing.policy.default.v1"} =
             ConversationExecutionContext.routing_policy(user.id)
  end

  test "an active paired machine retains the paired-machine routing policy" do
    user = AccountsFixtures.repository_user_fixture("conversation-context-machine")

    {:ok, %{code: code}} =
      Machines.start_pairing(%{
        "name" => "context-machine",
        "tier" => "probe",
        "platform" => "linux-x64",
        "agent_version" => "0.1.0",
        "roots" => ["/workspace"]
      })

    assert {:ok, _machine} = Machines.approve_pairing(user, code)

    assert %RoutingPolicy{id: "sarah.routing.policy.paired-machine.v1"} =
             ConversationExecutionContext.routing_policy(user.id)
  end

  defp build(surface, conversation, user) do
    ConversationExecutionContext.build(%{
      surface: surface,
      conversation_id: conversation.id,
      current_user_message_id: Ecto.UUID.generate(),
      owner_visitor_id: conversation.visitor_id,
      owner_user_id: user.id,
      memory_snapshot_ref: "memory-snapshot:test",
      profile_memory_snapshot_ref: "profile-memory-snapshot:test",
      module_registry_snapshot: :snapshot
    })
  end
end