test/openagents/do_not_build_register_test.exs

58e6347eeb72 · 3 KB

defmodule OpenAgents.DoNotBuildRegisterTest do
  use ExUnit.Case, async: false

  alias OpenAgents.DoNotBuildRegister

  defmodule AnalyticsSink do
    def capture(event, distinct_id, properties) do
      send(Application.fetch_env!(:openagents, :do_not_build_test_pid), {
        :captured,
        event,
        distinct_id,
        properties
      })
    end
  end

  setup do
    original_token = Application.get_env(:openagents, :posthog_project_token)
    original_sink = Application.get_env(:openagents, :analytics_sink)
    original_pid = Application.get_env(:openagents, :do_not_build_test_pid)

    Application.put_env(:openagents, :posthog_project_token, "phc_test")
    Application.put_env(:openagents, :analytics_sink, AnalyticsSink)
    Application.put_env(:openagents, :do_not_build_test_pid, self())

    on_exit(fn ->
      restore_env(:posthog_project_token, original_token)
      restore_env(:analytics_sink, original_sink)
      restore_env(:do_not_build_test_pid, original_pid)
    end)

    :ok
  end

  test "the committed register is valid, complete, and history preserving" do
    assert {:ok, register} = DoNotBuildRegister.load()
    assert length(register["entries"]) == 9

    assert register["entries"]
           |> Enum.map(& &1["current"]["state"])
           |> Enum.uniq()
           |> Enum.sort() == ~w(deferred rejected retired superseded)

    assert Enum.all?(register["entries"], fn entry ->
             List.last(entry["history"]) == entry["current"]
           end)

    [entry | rest] = register["entries"]

    invalid =
      put_in(register, ["entries"], [put_in(entry, ["current", "state"], "retired") | rest])

    assert {:error, :invalid_entry} = DoNotBuildRegister.validate(invalid)
  end

  test "matching uses explicit phrases rather than broad product keywords" do
    assert %{"id" => "DNB-003"} =
             DoNotBuildRegister.match("Replace npm with Bun in production")

    assert %{"id" => "DNB-008"} =
             DoNotBuildRegister.match(%{
               title: "Split Sarah into a Rust service",
               body: "Prepare the runtime boundary."
             })

    refute DoNotBuildRegister.match("Use Bun for a disposable local benchmark")
    refute DoNotBuildRegister.match("Improve the Spark wallet activity table")
    refute DoNotBuildRegister.match("Document optional Claude Code adapters")
  end

  test "FastFollow suppresses a match and records only bounded metadata" do
    proposal = "Add Copilot as an executor for delegated work"

    assert {:suppressed, %{"id" => "DNB-006"}} =
             DoNotBuildRegister.screen_fast_follow(proposal)

    assert_receive {:captured, "fast_follow_proposal_suppressed", "system_fast_follow",
                    properties}

    assert properties["register_id"] == "DNB-006"
    assert properties["decision_state"] == "deferred"
    assert String.length(properties["proposal_fingerprint"]) == 64
    refute inspect(properties) =~ proposal
  end

  test "new evidence and a decision record require review instead of bypassing the register" do
    proposal = "Build a hosted Spark wallet"

    assert {:suppressed, %{"id" => "DNB-001"}} =
             DoNotBuildRegister.screen_fast_follow(proposal, new_evidence: "custody changed")

    assert {:review_required, %{"id" => "DNB-001"}} =
             DoNotBuildRegister.screen_fast_follow(proposal,
               new_evidence: "A self-custodial implementation shipped.",
               decision_record: "docs/decisions/0010-spark-wallet.md"
             )

    assert :allow = DoNotBuildRegister.screen_fast_follow("Add project dependency labels")
  end

  defp restore_env(key, nil), do: Application.delete_env(:openagents, key)
  defp restore_env(key, value), do: Application.put_env(:openagents, key, value)
end