%%%------------------------------------------------------------------- %%% EUnit: пилотные метрики ИИ-подсказок (logic_ai_metrics). %%%------------------------------------------------------------------- -module(logic_ai_metrics_tests). -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). -define(TABLES, [ai_hint_metric]). setup() -> catch ets:delete(eventhub_ai_metrics_rl), eh_test_support:start_mnesia(), eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> eh_test_support:delete_tables(?TABLES), eh_test_support:stop_mnesia(), catch ets:delete(eventhub_ai_metrics_rl), ok. logic_ai_metrics_test_() -> {foreach, fun setup/0, fun cleanup/1, [ {"invalid body rejected", fun test_invalid_body/0}, {"unknown kind rejected", fun test_unknown_kind/0}, {"counters increment per key", fun test_increment/0}, {"batch over limit rejected", fun test_batch_limit/0}, {"summary counts kinds and gate ratio", fun test_summary/0} ]}. event(Kind, Pattern) -> #{<<"kind">> => Kind, <<"pattern">> => Pattern}. test_invalid_body() -> ?assertEqual({error, invalid_body}, logic_ai_metrics:record_events(<<"u1">>, [])), ?assertEqual({error, invalid_body}, logic_ai_metrics:record_events(<<"u1">>, not_a_list)), ?assertEqual({error, invalid_body}, logic_ai_metrics:record_events(<<"u1">>, [#{<<"kind">> => <<"shown">>}])). test_unknown_kind() -> ?assertEqual({error, invalid_body}, logic_ai_metrics:record_events(<<"u1">>, [event(<<"click">>, <<"p">>)])), ?assertEqual({error, invalid_body}, logic_ai_metrics:record_events(<<"u1">>, [event(<<"shown">>, <<>>)])). test_increment() -> {ok, 2} = logic_ai_metrics:record_events(<<"u1">>, [event(<<"shown">>, <<"cal|2|10">>), event(<<"confirm">>, <<"cal|2|10">>)]), {ok, 1} = logic_ai_metrics:record_events(<<"u1">>, [event(<<"shown">>, <<"cal|2|10">>)]), [Row] = mnesia:dirty_match_object(#ai_hint_metric{ id = {<<"u1">>, '_', <<"cal|2|10">>, <<"shown">>}, _ = '_'}), ?assertEqual(2, Row#ai_hint_metric.count), [_] = mnesia:dirty_match_object(#ai_hint_metric{ id = {<<"u1">>, '_', <<"cal|2|10">>, <<"confirm">>}, _ = '_'}). test_batch_limit() -> Events = [event(<<"shown">>, iolist_to_binary(["p", integer_to_list(N)])) || N <- lists:seq(1, 21)], ?assertEqual({error, invalid_body}, logic_ai_metrics:record_events(<<"u1">>, Events)). test_summary() -> {ok, _} = logic_ai_metrics:record_events(<<"u1">>, [event(<<"shown">>, <<"a">>), event(<<"confirm">>, <<"a">>), event(<<"confirm">>, <<"a">>), event(<<"dismiss">>, <<"b">>)]), {ok, _} = logic_ai_metrics:record_events(<<"u2">>, [event(<<"shown">>, <<"a">>), event(<<"confirm">>, <<"a">>)]), S = logic_ai_metrics:summary(), ?assertEqual(#{<<"shown">> => 2, <<"confirm">> => 3, <<"dismiss">> => 1}, maps:from_list(maps:to_list(maps:get(<<"by_kind">>, S)))), ?assertEqual(2, maps:get(<<"active_users_30d">>, S)), ?assertEqual(1, maps:get(<<"users_with_confirms_ge2_30d">>, S)), ?assertEqual(0.5, maps:get(<<"gate_ratio_30d">>, S)), [Top | _] = maps:get(<<"top_patterns_by_confirm">>, S), ?assertEqual(<<"a">>, maps:get(<<"pattern">>, Top)), ?assertEqual(3, maps:get(<<"confirms">>, Top)).