Headers X-Bikoret-Key / -Rebuke / -Verdict / -Bedikah; the `bikoret` object and the "— bikoret …" fringe on a gate reply; environment variables BIKORET_*; the image user and paths (/home/bikoret, /tmp/bikoret, /var/lib/bikoret/manna, /etc/bikoret/secrets, /opt/bikoret/spec); the tmp-bikoret pod volume; local databases bikoret_dev/_test. Kubernetes resource names, the shorashim_app role and the domain are unchanged (Tamid §1.1, "the names that still carry the old word"). Doorpost c963c4fe…. Elixir 604/0, L3 10/0, korban 295, Prolog bedikah 44/44. Gitea repo renamed heaviness510/bikoret (old URL redirects). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
254 lines
8.4 KiB
Elixir
254 lines
8.4 KiB
Elixir
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/bikoret_pekudei.md" in files
|
|
refute "specs/bikoret_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
|