defmodule VavPipeline.DoorpostTest do @moduledoc """ Tests for the doorpost-text identity verification (vehaya.md §3.3, §6 item 3). Devarim 4:2's lo tosif enforced as a computable property: any addition or subtraction across the category-grade specification files produces a hash mismatch detectable at /kodesh/specification_identity. These tests verify behavior at three layers: 1. The pure functions (compute_identity, manifest, files) 2. The verify_identity gate against a known-good hash 3. Drift detection against a synthetic spec set in a temp dir (uses application env override for repo_root) """ use ExUnit.Case, async: false alias VavPipeline.Doorpost describe "files/0" do test "returns the full list of category-grade spec files" do files = Doorpost.files() assert is_list(files) # Sanity: includes the load-bearing artifacts assert "BUILD_AZAZEL.md" in files assert "BUILD_KORBAN.md" in files assert "talmudic_engine/src/halt/avodah_zarah.pl" in files assert "talmudic_engine/src/halt/gilui_arayot.pl" in files assert "talmudic_engine/src/halt/shfichut_damim.pl" in files assert "specs/eish_zarah_reading.md" in files end test "explicitly excludes operations files (pekudei, terumah)" do files = Doorpost.files() refute "specs/shorashim_pekudei.md" in files refute "specs/shorashim_terumah.md" in files end test "list is sorted (canonical order independent of filesystem)" do files = Doorpost.files() assert files == Enum.sort(files) end test "no file appears twice" do files = Doorpost.files() assert length(files) == length(Enum.uniq(files)) end end describe "manifest/0" do test "returns one entry per file in @spec_files" do manifest = Doorpost.manifest() assert length(manifest) == length(Doorpost.files()) # Each entry is %{path, hash} Enum.each(manifest, fn entry -> assert Map.has_key?(entry, :path) assert Map.has_key?(entry, :hash) end) end test "manifest paths match @spec_files order" do manifest_paths = Enum.map(Doorpost.manifest(), & &1.path) assert manifest_paths == Doorpost.files() end test "each per-file hash is a 64-character hex SHA-256 (or :missing)" do Enum.each(Doorpost.manifest(), fn entry -> case entry.hash do :missing -> :ok hash when is_binary(hash) -> assert String.length(hash) == 64 assert String.match?(hash, ~r/^[0-9a-f]{64}$/) end end) end end describe "compute_identity/0 against the live repo" do test "produces a stable 64-char hex hash" do hash = Doorpost.compute_identity() assert is_binary(hash), "compute_identity returned: #{inspect(hash)}" assert String.length(hash) == 64 assert String.match?(hash, ~r/^[0-9a-f]{64}$/) end test "is deterministic — two calls return the same hash" do hash1 = Doorpost.compute_identity() hash2 = Doorpost.compute_identity() assert hash1 == hash2 end end describe "verify_identity/0 against the live repo" do test "returns :ok when the live spec set matches the baked hash" do assert :ok = Doorpost.verify_identity() end end # ============================================================ # Drift-detection tests against a synthetic spec set # ============================================================ # These tests build a fake spec set in a temp dir, point Doorpost # at it via the application env, and verify drift detection works # end-to-end. We can't override @spec_files (compile-time list); # instead we set up a temp dir that has THE SAME files as the # live spec set and verify the drift behaviors against THAT. describe "drift detection (synthetic spec set in temp dir)" do setup do # Build a temp dir mirror of the spec files temp = System.tmp_dir!() |> Path.join("doorpost-test-#{:erlang.unique_integer([:positive])}") File.mkdir_p!(temp) original_root = Application.get_env(:vav_pipeline, :doorpost_repo_root) Enum.each(Doorpost.files(), fn rel -> src = Path.join(Doorpost.repo_root(), rel) dst = Path.join(temp, rel) File.mkdir_p!(Path.dirname(dst)) if File.exists?(src), do: File.cp!(src, dst) end) on_exit(fn -> File.rm_rf!(temp) # NOTE: cannot restore Application env override because # @repo_root is read at compile time, not runtime. We # work around by computing identity directly via a # helper fn pointed at the temp dir — see below. if original_root do Application.put_env(:vav_pipeline, :doorpost_repo_root, original_root) end end) %{temp: temp} end # Direct-compute helper that mirrors Doorpost.compute_identity # but takes an explicit root. Used because @repo_root is baked # at compile time. defp compute_at(temp_root) do items = Enum.map(Doorpost.files(), fn rel -> full_path = Path.join(temp_root, rel) case File.read(full_path) do {:ok, contents} -> {rel, :crypto.hash(:sha256, contents) |> Base.encode16(case: :lower)} {:error, _} -> {rel, :missing} end end) case Enum.find(items, fn {_, h} -> h == :missing end) do nil -> combined = items |> Enum.map(fn {p, h} -> "#{p}\t#{h}" end) |> Enum.join("\n") :crypto.hash(:sha256, combined) |> Base.encode16(case: :lower) {missing, _} -> {:error, "missing spec file: #{missing}"} end end test "fresh mirror of the spec set produces the same hash as live", %{temp: temp} do assert compute_at(temp) == Doorpost.compute_identity() end test "modifying ANY one file changes the hash", %{temp: temp} do original = compute_at(temp) # Append a single byte to one file target = Path.join(temp, "talmudic_engine/src/halt/avodah_zarah.pl") File.write!(target, File.read!(target) <> "\n") drifted = compute_at(temp) refute original == drifted end test "removing any one file fails compute_identity", %{temp: temp} do target = Path.join(temp, "talmudic_engine/src/halt/avodah_zarah.pl") File.rm!(target) assert {:error, "missing spec file: " <> _} = compute_at(temp) end test "modifying a different file produces a different hash than modifying the first", %{ temp: temp } do File.write!( Path.join(temp, "talmudic_engine/src/halt/avodah_zarah.pl"), File.read!(Path.join(temp, "talmudic_engine/src/halt/avodah_zarah.pl")) <> "\n" ) hash_a = compute_at(temp) # Restore, then drift a different file target_b = Path.join(temp, "specs/eish_zarah_reading.md") File.write!(target_b, File.read!(target_b) <> "\n") hash_b = compute_at(temp) # Two distinct drifts → two distinct hashes refute hash_a == hash_b end end describe "/kodesh/specification_identity HTTP endpoint" do # Plug.Test integration. The endpoint is under /kodesh/* but is # currently NOT auth-gated (the kodesh tier is mTLS in production # via an upstream gateway, not in the application). import Plug.Test alias VavPipeline.BigdeiKodesh.Router @opts Router.init([]) test "returns 200 when verify_identity is :ok" do conn = conn(:get, "/kodesh/specification_identity") |> Router.call(@opts) assert conn.status == 200 body = Jason.decode!(conn.resp_body) assert body["status"] == "verified" assert is_binary(body["computed_hash"]) assert is_binary(body["expected_hash"]) assert body["computed_hash"] == body["expected_hash"] assert is_integer(body["file_count"]) assert body["file_count"] > 0 assert is_list(body["manifest"]) assert is_list(body["torah_basis"]) assert "Devarim 4:2 (lo tosif)" in body["torah_basis"] end test "manifest in response carries per-file hashes" do conn = conn(:get, "/kodesh/specification_identity") |> Router.call(@opts) body = Jason.decode!(conn.resp_body) manifest = body["manifest"] assert length(manifest) == length(Doorpost.files()) Enum.each(manifest, fn entry -> assert Map.has_key?(entry, "path") assert Map.has_key?(entry, "hash") end) end end end