%%%------------------------------------------------------------------- %%% @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"). %% ============================================================ %% 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). 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). %% ============================================================ %% 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)). %% ============================================================ %% 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)).