Live after the second commit (vav :046010f0): a monetary case naming bereishit 1:1 convened three judges and came back "accepted" — the interpret seat said "resolved (5 roots, 0 verses)" with reading(mitzvot_count(0),verse_count(0),grounded_in([],[])), the other two seats said teiku, and one accept beat two abstentions (the reply showed majority 1, minority 2). Three defects the tests could not see: - The engine names roots by letter (bet_resh_aleph; json_bridge reads "bet-resh-aleph") and the pipeline sent it the Hebrew, which its verse facts never match. BereishitCycle.engine_root_name/1 builds the engine's name from the Shoresh Engine's letter names or, for the L1 fallback, from the Hebrew with finals folded; the router sends that. (The cycle's own Talmudic routing still sends the Hebrew — recorded, not changed here: it alters /peah/ask and needs its own verification.) - judge.erl: "resolved" with no verse and no mitzvah is a reading coupled to nothing (avodah_zarah.pl Form 2); the judge abstains and says "resolved on an empty trace; nothing to stand on". - panel.erl: the majority is the verdict with the most votes, abstentions included; an abstaining plurality or an accept/reject tie is teiku. EUnit 57/0 (plurality, tie, empty trace). Elixir 632/0 (engine_root_name from names, from Hebrew, nil on nothing; the no-engine case is an abstaining majority of one, teiku). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
632 lines
24 KiB
Erlang
632 lines
24 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @doc Sanhedrin test suite — EUnit tests.
|
|
%%%
|
|
%%% Architecture §6.1 Day 4 — Sanhedrin inauguration:
|
|
%%% - Convene test panels at every size (1, 3, 23, 71)
|
|
%%% - Verify unanimous guilty on capital case triggers
|
|
%%% suspicion-of-consensus acquittal
|
|
%%% - Verify minority opinions are stored
|
|
%%% - Verify deliberation completes within time budgets
|
|
%%%
|
|
%%% Architecture §6.2 — Integration Tests:
|
|
%%% - case → panel → votes → decision with minority preserved
|
|
%%% @end
|
|
%%%-------------------------------------------------------------------
|
|
-module(sanhedrin_SUITE).
|
|
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
-include("sanhedrin.hrl").
|
|
|
|
%% The consult callback the tablet-55 tests register (judge:consult reads {M,F}).
|
|
-export([fake_consult/5]).
|
|
|
|
%% ============================================================
|
|
%% Test fixture — start/stop the application for each test
|
|
%% ============================================================
|
|
setup() ->
|
|
application:ensure_all_started(sanhedrin),
|
|
ok.
|
|
|
|
cleanup(_) ->
|
|
application:stop(sanhedrin),
|
|
ok.
|
|
|
|
%% ============================================================
|
|
%% Escalation tests
|
|
%% ============================================================
|
|
|
|
escalation_assess_simple_test() ->
|
|
Case = make_case(simple, <<"simple question">>),
|
|
?assertEqual({panel, 1}, escalation:assess(Case)).
|
|
|
|
escalation_assess_monetary_test() ->
|
|
Case = make_case(monetary, <<"monetary question">>),
|
|
?assertEqual({panel, 3}, escalation:assess(Case)).
|
|
|
|
escalation_assess_capital_test() ->
|
|
Case = make_case(capital, <<"capital question">>),
|
|
?assertEqual({panel, 23}, escalation:assess(Case)).
|
|
|
|
escalation_assess_novel_test() ->
|
|
Case = make_case(novel, <<"novel question">>),
|
|
?assertEqual({panel, 71}, escalation:assess(Case)).
|
|
|
|
%% ============================================================
|
|
%% Halt monitor tests
|
|
%% ============================================================
|
|
|
|
halt_clear_test() ->
|
|
setup(),
|
|
Case = make_case(simple, <<"legitimate question">>),
|
|
?assertEqual(clear, halt_monitor:check_case(Case)),
|
|
cleanup(ok).
|
|
|
|
%% Tablet 55: a non-map context is refused, and the monitor survives it.
|
|
halt_malformed_context_test() ->
|
|
{ok, _} = application:ensure_all_started(sanhedrin),
|
|
Sides = [#{<<"position">> => <<"a">>, <<"evidence">> => [<<"x">>]}],
|
|
Case = make_case_with_context(simple, <<"question">>, Sides),
|
|
?assertMatch({halt, malformed_case, _}, halt_monitor:check_case(Case)),
|
|
%% still standing, and still judging
|
|
?assertEqual(clear, halt_monitor:check_case(make_case(simple, <<"q">>))),
|
|
ok.
|
|
|
|
halt_avodah_zarah_test() ->
|
|
setup(),
|
|
Case = make_case_with_context(simple, <<"question">>,
|
|
#{authority_source => claude_model}),
|
|
Result = halt_monitor:check_case(Case),
|
|
?assertMatch({halt, avodah_zarah, _}, Result),
|
|
cleanup(ok).
|
|
|
|
halt_torah_authority_passes_test() ->
|
|
setup(),
|
|
Case = make_case_with_context(simple, <<"question">>,
|
|
#{authority_source => torah}),
|
|
?assertEqual(clear, halt_monitor:check_case(Case)),
|
|
cleanup(ok).
|
|
|
|
halt_l1_authority_passes_test() ->
|
|
setup(),
|
|
Case = make_case_with_context(simple, <<"question">>,
|
|
#{authority_source => l1}),
|
|
?assertEqual(clear, halt_monitor:check_case(Case)),
|
|
cleanup(ok).
|
|
|
|
halt_specification_authority_passes_test() ->
|
|
setup(),
|
|
Case = make_case_with_context(simple, <<"question">>,
|
|
#{authority_source => specification}),
|
|
?assertEqual(clear, halt_monitor:check_case(Case)),
|
|
cleanup(ok).
|
|
|
|
halt_sanhedrin_authority_passes_test() ->
|
|
setup(),
|
|
Case = make_case_with_context(simple, <<"question">>,
|
|
#{authority_source => sanhedrin_deliberation}),
|
|
?assertEqual(clear, halt_monitor:check_case(Case)),
|
|
cleanup(ok).
|
|
|
|
halt_shfichut_damim_delete_test() ->
|
|
setup(),
|
|
Case = make_case_with_context(simple, <<"question">>,
|
|
#{action => delete}),
|
|
Result = halt_monitor:check_case(Case),
|
|
?assertMatch({halt, shfichut_damim, _}, Result),
|
|
cleanup(ok).
|
|
|
|
halt_shfichut_damim_destroy_test() ->
|
|
setup(),
|
|
Case = make_case_with_context(simple, <<"question">>,
|
|
#{action => destroy}),
|
|
Result = halt_monitor:check_case(Case),
|
|
?assertMatch({halt, shfichut_damim, _}, Result),
|
|
cleanup(ok).
|
|
|
|
halt_shfichut_damim_overwrite_test() ->
|
|
setup(),
|
|
Case = make_case_with_context(simple, <<"question">>,
|
|
#{action => overwrite}),
|
|
Result = halt_monitor:check_case(Case),
|
|
?assertMatch({halt, shfichut_damim, _}, Result),
|
|
cleanup(ok).
|
|
|
|
halt_shfichut_damim_read_passes_test() ->
|
|
setup(),
|
|
Case = make_case_with_context(simple, <<"question">>,
|
|
#{action => read}),
|
|
?assertEqual(clear, halt_monitor:check_case(Case)),
|
|
cleanup(ok).
|
|
|
|
halt_gilui_arayot_test() ->
|
|
setup(),
|
|
Case = make_case_with_context(simple, <<"question">>,
|
|
#{boundary_violation => {spec_interp_merge, <<"collapsed">>}}),
|
|
Result = halt_monitor:check_case(Case),
|
|
?assertMatch({halt, gilui_arayot, _}, Result),
|
|
cleanup(ok).
|
|
|
|
halt_gilui_arayot_none_passes_test() ->
|
|
setup(),
|
|
Case = make_case_with_context(simple, <<"question">>,
|
|
#{boundary_violation => none}),
|
|
?assertEqual(clear, halt_monitor:check_case(Case)),
|
|
cleanup(ok).
|
|
|
|
halt_halted_case_rejected_test() ->
|
|
setup(),
|
|
Case = make_case_with_context(simple, <<"question">>,
|
|
#{authority_source => external_model}),
|
|
Result = escalation:submit_case(Case),
|
|
?assertMatch({error, {halted, avodah_zarah, _}}, Result),
|
|
cleanup(ok).
|
|
|
|
%% ============================================================
|
|
%% Panel tests — convene and deliberate
|
|
%% ============================================================
|
|
|
|
panel_convene_single_test() ->
|
|
setup(),
|
|
Case = make_case(simple, <<"simple case">>),
|
|
{ok, PanelPid} = panel_sup:start_panel(Case, 1),
|
|
State = panel:get_state(PanelPid),
|
|
?assertEqual(1, maps:get(size, State)),
|
|
?assertEqual(convened, maps:get(status, State)),
|
|
cleanup(ok).
|
|
|
|
panel_convene_monetary_test() ->
|
|
setup(),
|
|
Case = make_case(monetary, <<"monetary case">>),
|
|
{ok, PanelPid} = panel_sup:start_panel(Case, 3),
|
|
State = panel:get_state(PanelPid),
|
|
?assertEqual(3, maps:get(size, State)),
|
|
cleanup(ok).
|
|
|
|
panel_deliberate_single_test() ->
|
|
setup(),
|
|
Case = make_case(simple, <<"Is this action permitted?">>),
|
|
{ok, PanelPid} = panel_sup:start_panel(Case, 1),
|
|
panel:deliberate_async(PanelPid, 5000),
|
|
%% Wait for deliberation to complete
|
|
timer:sleep(500),
|
|
%% Panel should have terminated (decided)
|
|
?assertEqual(false, is_process_alive(PanelPid)),
|
|
cleanup(ok).
|
|
|
|
panel_deliberate_monetary_test() ->
|
|
setup(),
|
|
Case = make_case(monetary, <<"Should this monetary action proceed?">>),
|
|
{ok, PanelPid} = panel_sup:start_panel(Case, 3),
|
|
panel:deliberate_async(PanelPid, 5000),
|
|
timer:sleep(500),
|
|
?assertEqual(false, is_process_alive(PanelPid)),
|
|
cleanup(ok).
|
|
|
|
%% ============================================================
|
|
%% Clerk tests — opinion storage
|
|
%% ============================================================
|
|
|
|
clerk_initially_empty_test() ->
|
|
setup(),
|
|
?assertEqual(0, clerk:count_decisions()),
|
|
?assertEqual(0, clerk:count_opinions()),
|
|
cleanup(ok).
|
|
|
|
clerk_store_decision_test() ->
|
|
setup(),
|
|
Decision = #decision{
|
|
case_ref = <<"test-case-1">>,
|
|
outcome = accepted,
|
|
majority = #{position => <<"accept">>, vote_count => 2},
|
|
minority = #{position => <<"reject">>, vote_count => 1},
|
|
panel_size = 3,
|
|
deliberation_time_ms = 1000,
|
|
unanimous_check = passed
|
|
},
|
|
clerk:store_decision(<<"test-case-1">>, Decision),
|
|
timer:sleep(50), %% cast is async
|
|
?assertEqual(1, clerk:count_decisions()),
|
|
{ok, Retrieved} = clerk:get_decision(<<"test-case-1">>),
|
|
?assertEqual(accepted, Retrieved#decision.outcome),
|
|
cleanup(ok).
|
|
|
|
clerk_store_opinion_test() ->
|
|
setup(),
|
|
Opinion = #opinion{
|
|
case_ref = <<"test-case-2">>,
|
|
judge_id = make_ref(),
|
|
position = <<"The roots indicate acceptance">>,
|
|
reasoning = <<"Root analysis">>,
|
|
torah_refs = [#torah_ref{book = bereishit, chapter = 1, verse = 1}],
|
|
verdict = accept,
|
|
is_majority = true,
|
|
stored_at = erlang:system_time(millisecond)
|
|
},
|
|
clerk:store_opinion(Opinion),
|
|
timer:sleep(50),
|
|
Opinions = clerk:get_opinions(<<"test-case-2">>),
|
|
?assertEqual(1, length(Opinions)),
|
|
cleanup(ok).
|
|
|
|
clerk_minority_opinions_preserved_test() ->
|
|
setup(),
|
|
%% Store majority and minority opinions
|
|
MajOpinion = #opinion{
|
|
case_ref = <<"test-case-3">>,
|
|
judge_id = make_ref(),
|
|
position = <<"majority position">>,
|
|
reasoning = <<"majority reasoning">>,
|
|
torah_refs = [],
|
|
verdict = accept,
|
|
is_majority = true,
|
|
stored_at = erlang:system_time(millisecond)
|
|
},
|
|
MinOpinion = #opinion{
|
|
case_ref = <<"test-case-3">>,
|
|
judge_id = make_ref(),
|
|
position = <<"minority position">>,
|
|
reasoning = <<"minority reasoning">>,
|
|
torah_refs = [],
|
|
verdict = reject,
|
|
is_majority = false,
|
|
stored_at = erlang:system_time(millisecond)
|
|
},
|
|
clerk:store_opinion(MajOpinion),
|
|
clerk:store_opinion(MinOpinion),
|
|
timer:sleep(50),
|
|
AllOpinions = clerk:get_opinions(<<"test-case-3">>),
|
|
?assertEqual(2, length(AllOpinions)),
|
|
MinorityOpinions = clerk:get_minority_opinions(<<"test-case-3">>),
|
|
?assertEqual(1, length(MinorityOpinions)),
|
|
[Min] = MinorityOpinions,
|
|
?assertEqual(<<"minority position">>, Min#opinion.position),
|
|
cleanup(ok).
|
|
|
|
clerk_not_found_test() ->
|
|
setup(),
|
|
?assertEqual({error, not_found}, clerk:get_decision(<<"nonexistent">>)),
|
|
cleanup(ok).
|
|
|
|
%% ============================================================
|
|
%% Unanimous check tests — THE CRITICAL CHECK
|
|
%% ============================================================
|
|
|
|
%% "If the Sanhedrin unanimously finds guilty, the accused is acquitted."
|
|
%% — Sanhedrin 17a
|
|
unanimous_capital_all_reject_test() ->
|
|
%% 23 judges all voting reject → suspicion of consensus
|
|
Votes = [make_vote(reject) || _ <- lists:seq(1, 23)],
|
|
Result = panel:check_unanimous(23, Votes),
|
|
?assertEqual({failed, suspicion_of_consensus}, Result).
|
|
|
|
unanimous_capital_mixed_passes_test() ->
|
|
%% 23 judges, some accept some reject → no suspicion
|
|
Accepts = [make_vote(accept) || _ <- lists:seq(1, 15)],
|
|
Rejects = [make_vote(reject) || _ <- lists:seq(1, 8)],
|
|
Votes = Accepts ++ Rejects,
|
|
?assertEqual(passed, panel:check_unanimous(23, Votes)).
|
|
|
|
unanimous_capital_all_accept_passes_test() ->
|
|
%% Unanimous acceptance is fine — suspicion only applies to conviction
|
|
Votes = [make_vote(accept) || _ <- lists:seq(1, 23)],
|
|
?assertEqual(passed, panel:check_unanimous(23, Votes)).
|
|
|
|
unanimous_monetary_not_checked_test() ->
|
|
%% Monetary panels (3 judges) do not have unanimous check
|
|
Votes = [make_vote(reject) || _ <- lists:seq(1, 3)],
|
|
?assertEqual(passed, panel:check_unanimous(3, Votes)).
|
|
|
|
unanimous_novel_all_reject_test() ->
|
|
%% 71 judges all voting reject → suspicion of consensus
|
|
Votes = [make_vote(reject) || _ <- lists:seq(1, 71)],
|
|
Result = panel:check_unanimous(71, Votes),
|
|
?assertEqual({failed, suspicion_of_consensus}, Result).
|
|
|
|
%% ============================================================
|
|
%% Integration test — full case flow
|
|
%% ============================================================
|
|
|
|
full_case_flow_simple_test() ->
|
|
setup(),
|
|
Case = make_case(simple, <<"Does root ברא indicate creation?">>),
|
|
{ok, CaseRef} = escalation:submit_case(Case),
|
|
?assertMatch(<<_/binary>>, CaseRef),
|
|
%% Wait for deliberation
|
|
timer:sleep(1000),
|
|
%% Opinions should have been stored
|
|
Opinions = clerk:get_opinions(CaseRef),
|
|
?assert(length(Opinions) >= 1),
|
|
cleanup(ok).
|
|
|
|
full_case_flow_monetary_test() ->
|
|
setup(),
|
|
Case = make_case(monetary, <<"Should resources be allocated for this interpretation?">>),
|
|
{ok, CaseRef} = escalation:submit_case(Case),
|
|
timer:sleep(1000),
|
|
Opinions = clerk:get_opinions(CaseRef),
|
|
?assert(length(Opinions) >= 3),
|
|
cleanup(ok).
|
|
|
|
full_case_flow_capital_test() ->
|
|
setup(),
|
|
Case = make_case(capital, <<"Should this high-stakes action proceed?">>),
|
|
{ok, CaseRef} = escalation:submit_case(Case),
|
|
timer:sleep(3000),
|
|
Opinions = clerk:get_opinions(CaseRef),
|
|
?assert(length(Opinions) >= 23),
|
|
cleanup(ok).
|
|
|
|
%% ============================================================
|
|
%% Judge tests
|
|
%% ============================================================
|
|
|
|
judge_start_and_stop_test() ->
|
|
Case = make_case(simple, <<"test">>),
|
|
{ok, JudgePid} = judge:start_link(Case),
|
|
?assert(is_process_alive(JudgePid)),
|
|
judge:stop(JudgePid),
|
|
timer:sleep(50),
|
|
?assertEqual(false, is_process_alive(JudgePid)).
|
|
|
|
judge_deliberate_sends_vote_test() ->
|
|
Case = make_case(simple, <<"test deliberation">>),
|
|
{ok, JudgePid} = judge:start_link(Case),
|
|
judge:deliberate(JudgePid, self()),
|
|
receive
|
|
{vote, Vote} ->
|
|
?assertMatch(#vote{}, Vote),
|
|
?assert(is_binary(Vote#vote.position)),
|
|
?assert(is_binary(Vote#vote.reasoning)),
|
|
?assert(Vote#vote.verdict == accept orelse
|
|
Vote#vote.verdict == reject orelse
|
|
Vote#vote.verdict == abstain)
|
|
after 5000 ->
|
|
?assert(false) %% timeout — judge did not vote
|
|
end,
|
|
judge:stop(JudgePid).
|
|
|
|
%% ============================================================
|
|
%% Tablet 55 — the judge consults; it does not roll dice
|
|
%% ============================================================
|
|
|
|
judge_seats_cycle_the_dialectic_test() ->
|
|
?assertEqual(interpret, judge:step_for_seat(1)),
|
|
?assertEqual(challenge, judge:step_for_seat(2)),
|
|
?assertEqual(resolve, judge:step_for_seat(3)),
|
|
?assertEqual(interpret, judge:step_for_seat(4)),
|
|
?assertEqual(challenge, judge:step_for_seat(23)),
|
|
?assertEqual(challenge, judge:step_for_seat(71)).
|
|
|
|
judge_without_consult_abstains_test() ->
|
|
application:unset_env(sanhedrin, consult),
|
|
Case = make_case_with_context(simple, <<"q">>, #{roots => [<<"br-a">>]}),
|
|
{ok, JudgePid} = judge:start_link(Case, 1),
|
|
judge:deliberate(JudgePid, self()),
|
|
receive
|
|
{vote, Vote} ->
|
|
?assertEqual(abstain, Vote#vote.verdict),
|
|
?assertEqual([], Vote#vote.torah_refs),
|
|
?assertMatch({_, _}, binary:match(Vote#vote.reasoning, <<"no consultation">>))
|
|
after 5000 -> ?assert(false)
|
|
end,
|
|
judge:stop(JudgePid).
|
|
|
|
judge_consult_resolved_accepts_with_verses_test() ->
|
|
application:set_env(sanhedrin, consult, {?MODULE, fake_consult}),
|
|
Case = make_case_with_context(simple, <<"q">>, #{roots => [<<"br-a">>], fake => resolved}),
|
|
{ok, JudgePid} = judge:start_link(Case, 1),
|
|
judge:deliberate(JudgePid, self()),
|
|
receive
|
|
{vote, Vote} ->
|
|
?assertEqual(accept, Vote#vote.verdict),
|
|
?assertEqual(<<"the reading">>, Vote#vote.position),
|
|
?assertMatch([#torah_ref{book = bereishit, chapter = 1, verse = 1}], Vote#vote.torah_refs),
|
|
?assertMatch({_, _}, binary:match(Vote#vote.reasoning, <<"interpret via the Talmudic Engine">>))
|
|
after 5000 -> ?assert(false)
|
|
end,
|
|
judge:stop(JudgePid),
|
|
application:unset_env(sanhedrin, consult).
|
|
|
|
judge_consult_raises_abstains_and_stands_test() ->
|
|
application:set_env(sanhedrin, consult, {?MODULE, fake_consult}),
|
|
Case = make_case_with_context(simple, <<"q">>, #{fake => raise}),
|
|
{ok, JudgePid} = judge:start_link(Case, 2),
|
|
judge:deliberate(JudgePid, self()),
|
|
receive
|
|
{vote, Vote} -> ?assertEqual(abstain, Vote#vote.verdict)
|
|
after 5000 -> ?assert(false)
|
|
end,
|
|
?assert(is_process_alive(JudgePid)),
|
|
judge:stop(JudgePid),
|
|
application:unset_env(sanhedrin, consult).
|
|
|
|
%% A panel of three where the dialectic finds a kushya at seat 2:
|
|
%% interpret → resolved, challenge → challenged, resolve → resolved.
|
|
%% Majority 2 accept, minority 1 reject — the minority preserved.
|
|
panel_of_three_dissent_from_the_dialectic_test() ->
|
|
setup(),
|
|
application:set_env(sanhedrin, consult, {?MODULE, fake_consult}),
|
|
Case = make_case_with_context(monetary, <<"q">>, #{roots => [<<"br-a">>], fake => dialectic}),
|
|
{ok, CaseRef} = escalation:submit_case(Case),
|
|
timer:sleep(600),
|
|
{ok, Decision} = clerk:get_decision(CaseRef),
|
|
?assertEqual(3, Decision#decision.panel_size),
|
|
?assertEqual(accepted, Decision#decision.outcome),
|
|
?assertEqual(2, maps:get(vote_count, Decision#decision.majority)),
|
|
?assertEqual(1, maps:get(vote_count, Decision#decision.minority)),
|
|
?assertEqual(<<"but the verse says otherwise">>, maps:get(position, Decision#decision.minority)),
|
|
application:unset_env(sanhedrin, consult),
|
|
cleanup(ok).
|
|
|
|
%% The fake consultation the tests register. Step-aware for `dialectic`.
|
|
fake_consult(_Step, _Ref, _Roots, _Q, #{fake := raise}) ->
|
|
erlang:error(engine_exploded);
|
|
fake_consult(_Step, _Ref, _Roots, _Q, #{fake := resolved}) ->
|
|
{ok, #{status => <<"resolved">>, text => <<"the reading">>, verses => [{bereishit, 1, 1}]}};
|
|
fake_consult(challenge, _Ref, _Roots, _Q, #{fake := dialectic}) ->
|
|
{ok, #{status => <<"challenged">>, text => <<>>, kushya => <<"but the verse says otherwise">>,
|
|
verses => [{devarim, 6, 4}]}};
|
|
fake_consult(_Step, _Ref, _Roots, _Q, #{fake := dialectic}) ->
|
|
{ok, #{status => <<"resolved">>, text => <<"the reading">>, verses => [{bereishit, 1, 1}]}};
|
|
fake_consult(_Step, _Ref, _Roots, _Q, _Ctx) ->
|
|
{error, no_fake}.
|
|
|
|
%% ============================================================
|
|
%% Panel majority/minority computation tests
|
|
%% ============================================================
|
|
|
|
majority_minority_empty_test() ->
|
|
{Maj, Min} = panel:compute_majority_minority([]),
|
|
?assertEqual(0, maps:get(vote_count, Maj)),
|
|
?assertEqual(0, maps:get(vote_count, Min)).
|
|
|
|
majority_minority_all_accept_test() ->
|
|
Votes = [make_vote(accept) || _ <- lists:seq(1, 3)],
|
|
{Maj, Min} = panel:compute_majority_minority(Votes),
|
|
?assertEqual(3, maps:get(vote_count, Maj)),
|
|
?assertEqual(0, maps:get(vote_count, Min)).
|
|
|
|
majority_minority_mixed_test() ->
|
|
Accepts = [make_vote(accept) || _ <- lists:seq(1, 2)],
|
|
Rejects = [make_vote(reject)],
|
|
Votes = Accepts ++ Rejects,
|
|
{Maj, Min} = panel:compute_majority_minority(Votes),
|
|
?assertEqual(2, maps:get(vote_count, Maj)),
|
|
?assertEqual(1, maps:get(vote_count, Min)).
|
|
|
|
majority_verdict_accept_test() ->
|
|
Votes = [make_vote(accept) || _ <- lists:seq(1, 3)],
|
|
{Maj, _Min} = panel:compute_majority_minority(Votes),
|
|
?assertEqual(accept, maps:get(verdict, Maj)).
|
|
|
|
majority_verdict_reject_test() ->
|
|
Rejects = [make_vote(reject) || _ <- lists:seq(1, 2)],
|
|
Accepts = [make_vote(accept)],
|
|
Votes = Rejects ++ Accepts,
|
|
{Maj, _Min} = panel:compute_majority_minority(Votes),
|
|
?assertEqual(reject, maps:get(verdict, Maj)).
|
|
|
|
%% Tablet 55: abstentions count. One accept against two abstentions is
|
|
%% not an accepting majority — it is teiku.
|
|
majority_abstaining_plurality_is_teiku_test() ->
|
|
Votes = [make_vote(accept), make_vote(abstain), make_vote(abstain)],
|
|
{Maj, Min} = panel:compute_majority_minority(Votes),
|
|
?assertEqual(abstain, maps:get(verdict, Maj)),
|
|
?assertEqual(2, maps:get(vote_count, Maj)),
|
|
?assertEqual(1, maps:get(vote_count, Min)),
|
|
?assertEqual(teiku, panel:determine_outcome(Maj, Min, passed)).
|
|
|
|
majority_accept_reject_tie_is_teiku_test() ->
|
|
Votes = [make_vote(accept), make_vote(reject)],
|
|
{Maj, _Min} = panel:compute_majority_minority(Votes),
|
|
?assertEqual(abstain, maps:get(verdict, Maj)),
|
|
?assertEqual(0, maps:get(vote_count, Maj)).
|
|
|
|
%% Tablet 55: "resolved" over an empty trace — no verse, no mitzvah —
|
|
%% is nothing to stand on; the judge abstains rather than accept.
|
|
judge_resolved_on_empty_trace_abstains_test() ->
|
|
{Pos, Reason, Refs, Verdict} =
|
|
judge:vote_from_consultation(interpret,
|
|
{ok, #{status => <<"resolved">>, text => <<"reading(0,0)">>, verses => [], grounding => []}}, 5),
|
|
?assertEqual(abstain, Verdict),
|
|
?assertEqual([], Refs),
|
|
?assertMatch({_, _}, binary:match(Pos, <<"Teiku">>)),
|
|
?assertMatch({_, _}, binary:match(Reason, <<"empty trace">>)),
|
|
%% a mitzvah without a verse is still ground
|
|
{_, _, _, V2} = judge:vote_from_consultation(interpret,
|
|
{ok, #{status => <<"resolved">>, text => <<"r">>, verses => [], grounding => [<<"mitzvah(5)">>]}}, 1),
|
|
?assertEqual(accept, V2).
|
|
|
|
%% ============================================================
|
|
%% determine_outcome tests — accepted, rejected, teiku reachable
|
|
%% ============================================================
|
|
|
|
determine_outcome_accepted_test() ->
|
|
Maj = #{vote_count => 2, verdict => accept},
|
|
Min = #{vote_count => 1, verdict => reject},
|
|
?assertEqual(accepted, panel:determine_outcome(Maj, Min, passed)).
|
|
|
|
determine_outcome_rejected_test() ->
|
|
Maj = #{vote_count => 2, verdict => reject},
|
|
Min = #{vote_count => 1, verdict => accept},
|
|
?assertEqual(rejected, panel:determine_outcome(Maj, Min, passed)).
|
|
|
|
determine_outcome_teiku_no_votes_test() ->
|
|
Maj = #{vote_count => 0, verdict => abstain},
|
|
Min = #{vote_count => 0, verdict => abstain},
|
|
?assertEqual(teiku, panel:determine_outcome(Maj, Min, passed)).
|
|
|
|
determine_outcome_teiku_all_abstain_test() ->
|
|
Maj = #{vote_count => 3, verdict => abstain},
|
|
Min = #{vote_count => 0, verdict => abstain},
|
|
?assertEqual(teiku, panel:determine_outcome(Maj, Min, passed)).
|
|
|
|
determine_outcome_suspicion_overrides_test() ->
|
|
%% Even if majority rejected, unanimous capital → acquit (accepted)
|
|
Maj = #{vote_count => 23, verdict => reject},
|
|
Min = #{vote_count => 0, verdict => abstain},
|
|
?assertEqual(accepted, panel:determine_outcome(Maj, Min, {failed, suspicion_of_consensus})).
|
|
|
|
%% ============================================================
|
|
%% Record definition tests
|
|
%% ============================================================
|
|
|
|
record_sanhedrin_case_test() ->
|
|
Case = make_case(monetary, <<"test">>),
|
|
?assertEqual(monetary, Case#sanhedrin_case.complexity),
|
|
?assertEqual(<<"test">>, Case#sanhedrin_case.question).
|
|
|
|
record_decision_test() ->
|
|
D = #decision{
|
|
case_ref = <<"ref">>,
|
|
outcome = teiku,
|
|
majority = #{},
|
|
minority = #{},
|
|
panel_size = 3,
|
|
deliberation_time_ms = 500,
|
|
unanimous_check = passed
|
|
},
|
|
?assertEqual(teiku, D#decision.outcome),
|
|
?assertEqual(passed, D#decision.unanimous_check).
|
|
|
|
%% ============================================================
|
|
%% Helper functions
|
|
%% ============================================================
|
|
|
|
make_case(Complexity, Question) ->
|
|
#sanhedrin_case{
|
|
case_ref = generate_id(),
|
|
complexity = Complexity,
|
|
question = Question,
|
|
context = #{
|
|
roots => [#{shoresh => [bet, resh, aleph], domain => creation}],
|
|
torah_refs => [{bereishit, 1, 1}],
|
|
prior_interpretations => []
|
|
},
|
|
requested_by = <<"test">>,
|
|
submitted_at = erlang:system_time(millisecond)
|
|
}.
|
|
|
|
make_case_with_context(Complexity, Question, Context) ->
|
|
#sanhedrin_case{
|
|
case_ref = generate_id(),
|
|
complexity = Complexity,
|
|
question = Question,
|
|
context = Context,
|
|
requested_by = <<"test">>,
|
|
submitted_at = erlang:system_time(millisecond)
|
|
}.
|
|
|
|
make_vote(Verdict) ->
|
|
#vote{
|
|
judge_id = make_ref(),
|
|
position = <<"test position">>,
|
|
reasoning = <<"test reasoning">>,
|
|
torah_refs = [#torah_ref{book = bereishit, chapter = 1, verse = 1}],
|
|
verdict = Verdict,
|
|
cast_at = erlang:system_time(millisecond)
|
|
}.
|
|
|
|
generate_id() ->
|
|
base64:encode(crypto:strong_rand_bytes(8)).
|