Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/test_notif_panel.c
T
MeowOS 82b77d64b4 通知中心 ⑥⑦: 电源菜单chip + 勿扰面板内开关 + 单条左滑删除(agent-132)
- ⑥ 快捷行 5chip: WiFi/蓝牙/亮度/主题/电源菜单; 电源菜单chip点击=唤起 show_power_menu(真弹) + after_modal 还原
- 勿扰=面板内开关(notify.dnd bool, 零旁路), ncp_draw_list 按 urgency>=2 过滤; NCP_LIST_TOP 308
- ⑦ 单条通知行左滑删除: 跟手(mnp_swipe_clamp)+半程可回(未过阈值回弹), 过阈值滑出删除(mnp_swipe_delete/step)
- 手势区分: 行内左滑=删该条, 竖向=滚动, meow_edge_back(左缘<24) 优先; 局部刷该行带
- meow_ui.a 零改动(d249c6e6); launcher=115e2b1d; 宿主147断言PASS; 新增0warn
- SYSTEM_SETTINGS §1 加 notify.dnd; NOTIFICATION_SYSTEM §9 增 ⑥/⑦ 定案
2026-08-30 17:23:04 +08:00

425 lines
23 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* test_notif_panel.c — 宿主单测:阶段B 通知中心面板(纯逻辑 + 面板读取的存储契约)。
*
* 直接 #include "meow_notif.c" + "meow_settings.c" 于同一 TU(同 test_notif_tf.c 手法),
* 以「真实 meow_notif 存储 + 真实 meow_settings 后端」验证面板真正读取的字段契约;
* 再测 meow_notif_panel.h 的纯逻辑(滑条映射/亮度 clamp/顶部下滑判定/时间显示/行命中/滚动钳位)。
* 不依赖 launcher 其它 static / fb / 触摸。
*
* 运行:gcc -D_GNU_SOURCE -o test_notif_panel test_notif_panel.c && ./test_notif_panel
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <time.h>
/* stub:meow_notif.c 的 read_quota() 经 meow_settings_get_int 读 notify.* 配额。
* <0 => 键缺失 => 用默认(MEOW_NOTIF_CATS_DEF=4 / CAT_QUOTA_DEF=4 / MEDIA_QUOTA_DEF=1)。
* meow_notif.c 对 meow_settings 的唯一依赖就是这一个函数。 */
int meow_settings_get_int(const char *key, int *out)
{
(void)key; (void)out;
return -2; /* MEOW_ERR_NOENT */
}
/* 先把 tf 转储路径指向临时目录,避免污染真实卡路径(meow_notif_init 会 clear tf)*/
static char g_tflog[600];
#include "meow_notif.c" /* 真实通知存储:post/list/count/dismiss/update_progress/... */
#include "meow_notif_panel.h" /* 面板纯逻辑(header-only) */
static int g_fail = 0;
#define CHECK(cond, msg) do { \
if (!(cond)) { fprintf(stderr, "FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fail++; } \
else fprintf(stderr, "ok: %s\n", msg); \
} while (0)
/* 收集列表到固定槽(回调式,模拟面板 ncp_collect_cb 的字段读取) */
#define MAXG 16
typedef struct { uint32_t id; char summary[96]; char body[96]; char icon[64];
char app[48]; char tgt[48]; int progress; uint8_t urgency; uint64_t ms; } gslot_t;
static gslot_t g_got[MAXG]; static int g_nslots = 0;
static int list_grab_cb(const meow_notif_t *n, void *ctx)
{
(void)ctx;
if (g_nslots >= MAXG) return 1;
gslot_t *s = &g_got[g_nslots++];
memset(s, 0, sizeof *s);
s->id = n->id;
snprintf(s->summary, sizeof s->summary, "%s", n->summary);
snprintf(s->body, sizeof s->body, "%s", n->body);
snprintf(s->icon, sizeof s->icon, "%s", n->app_icon);
snprintf(s->app, sizeof s->app, "%s", n->app_id);
snprintf(s->tgt, sizeof s->tgt, "%s", n->target_activity);
s->progress = n->progress;
s->urgency = n->urgency;
s->ms = n->created_ms;
return 0;
}
static int grab_list(void)
{
g_nslots = 0;
meow_notif_list(NULL, list_grab_cb, NULL);
return g_nslots;
}
static gslot_t *find_slot(const char *summary)
{
for (int i = 0; i < g_nslots; i++)
if (!strcmp(g_got[i].summary, summary)) return &g_got[i];
return NULL;
}
static uint64_t wall_now_ms(void)
{
struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts);
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
}
static void test_pure_logic(void)
{
fprintf(stderr, "--- P1 pure logic ---\n");
/* mnp_top_swipe_down */
CHECK(mnp_top_swipe_down(40, 5, 3, 90, 28, 65) == 1, "top hotzone down-dominant -> open");
CHECK(mnp_top_swipe_down(10, 5, 3, 90, 28, 65) == 0, "left-edge (down_x<24) reserved -> no open");
CHECK(mnp_top_swipe_down(40, 30, 3, 90, 28, 65) == 0, "origin below hotzone -> no open");
CHECK(mnp_top_swipe_down(40, 5, 3, 40, 28, 65) == 0, "below swipe threshold -> no open");
CHECK(mnp_top_swipe_down(40, 5, 100, 70, 28, 65) == 0, "horizontal-dominant -> no open");
CHECK(mnp_top_swipe_down(40, 5, -3, 90, 28, 65) == 1, "down with minor left drift still opens");
/* mnp_slider_val_from_x */
CHECK(mnp_slider_val_from_x(20, 20, 440, 1, 100) == 1, "slider x=left -> min");
CHECK(mnp_slider_val_from_x(460, 20, 440, 1, 100) == 100, "slider x=right -> max");
int mid = mnp_slider_val_from_x(240, 20, 440, 1, 100);
CHECK(mid >= 1 && mid <= 100, "slider x=mid within [min,max]");
CHECK(mnp_slider_val_from_x(0, 20, 440, 1, 100) == 1, "slider x out-left clamped to min");
CHECK(mnp_slider_val_from_x(999, 20, 440, 1, 100) == 100, "slider x out-right clamped to max");
/* mnp_step_round:跳步吸附(音量滑条去抖/减少逐值写 DAC) */
CHECK(mnp_step_round(37, 1) == 37, "step_round step=1 -> unchanged (continuous, same as settings slider)");
CHECK(mnp_step_round(0, 1) == 0, "step_round step=1 -> 0 kept");
CHECK(mnp_step_round(37, 5) == 35, "step_round 37 step=5 -> 35 (nearest multiple of 5)");
CHECK(mnp_step_round(38, 5) == 40, "step_round 38 step=5 -> 40 (rounds up to nearest)");
CHECK(mnp_step_round(50, 5) == 50, "step_round 50 step=5 -> exactly on step");
CHECK(mnp_step_round(0, 5) == 0, "step_round 0 step=5 -> 0");
CHECK(mnp_step_round(100, 5) == 100, "step_round 100 step=5 -> 100");
CHECK(mnp_step_round(99, 5) == 100, "step_round 99 step=5 -> 100 (rounds up to max)");
CHECK(mnp_step_round(2, 4) == 4, "step_round 2 step=4 -> 4 (round to nearest multiple)");
CHECK(mnp_step_round(-3, 5) == 0, "step_round negative clamps to 0 (value domain >=0)");
/* mnp_brightness_clamp */
CHECK(mnp_brightness_clamp(0, 1) == 1, "brightness clamp below min -> min");
CHECK(mnp_brightness_clamp(150, 1) == 100, "brightness clamp above 100 -> 100");
CHECK(mnp_brightness_clamp(50, 1) == 50, "brightness 50 kept");
CHECK(mnp_brightness_clamp(3, 5) == 5, "brightness below custom min -> custom min");
CHECK(mnp_brightness_clamp(-7, -3) == 1, "brightness clamp forces vmin>=1");
/* mnp_time_ago */
uint64_t now = wall_now_ms();
char tb[32];
mnp_time_ago(now - 5000, now, tb, sizeof tb);
CHECK(!strcmp(tb, "刚刚"), "time_ago <60s -> 刚刚");
mnp_time_ago(now - 5 * 60000u, now, tb, sizeof tb);
CHECK(!strcmp(tb, "5分钟前"), "time_ago 5min -> 5分钟前");
mnp_time_ago(now - 3 * 3600000u, now, tb, sizeof tb);
CHECK(strlen(tb) == 5 && tb[2] == ':', "time_ago >1h -> HH:MM");
/* mnp_row_at */
CHECK(mnp_row_at(250, 252, 696, 92) == -1, "row_at above list -> -1");
CHECK(mnp_row_at(300, 252, 696, 92) == 0, "row_at first row -> 0");
CHECK(mnp_row_at(400, 252, 696, 92) == 1, "row_at second row -> 1");
CHECK(mnp_row_at(700, 252, 696, 92) == -1, "row_at below list -> -1");
/* mnp_scroll_clamp */
CHECK(mnp_scroll_clamp(0, 2000, 444) == 0, "scroll clamp lower -> 0");
CHECK(mnp_scroll_clamp(9999, 2000, 444) == 1556, "scroll clamp upper -> total-view");
CHECK(mnp_scroll_clamp(-5, 2000, 444) == 0, "scroll clamp negative -> 0");
CHECK(mnp_scroll_clamp(5, 400, 444) == 0, "content fits view -> 0");
}
/* P4 通知列表滚动:方向映射(input.swipe_invert)+ 回弹判定(纯逻辑,header-only)。
* 语义与设置页列表一致:invert=0 手指上滑(d<0) → target 增大(内容上滚看后面项);
* invert=1(反向)手指上滑 → target 减小。进滚动锁 base 保证触发瞬间不跳变。 */
static void test_scroll_pure(void)
{
fprintf(stderr, "--- P4 notification list scroll (swipe_invert direction + rebound) ---\n");
int total = 2000, view = 444, max_scroll = total - view; /* 1556 */
/* mnp_scroll_max */
CHECK(mnp_scroll_max(2000, 444) == 1556, "scroll_max overflow -> total-view");
CHECK(mnp_scroll_max(400, 444) == 0, "scroll_max content fits -> 0");
CHECK(mnp_scroll_max(444, 444) == 0, "scroll_max equal -> 0");
CHECK(mnp_scroll_max(445, 444) == 1, "scroll_max 1px over -> 1");
/* mnp_scroll_base:进滚动锁基准(invert=0 同设置页 `scroll + d`;invert=1 反向) */
CHECK(mnp_scroll_base(100, -45, 0) == 55, "base invert=0 finger-up -> scroll+d");
CHECK(mnp_scroll_base(100, -45, 1) == 145, "base invert=1 finger-up -> scroll-d");
/* mnp_scroll_target:触发瞬间 target 回到原 scroll(连续跟手,不跳变) */
CHECK(mnp_scroll_target(55, -45, 0) == 100, "target invert=0 at engage -> back to scroll");
CHECK(mnp_scroll_target(145, -45, 1) == 100, "target invert=1 at engage -> back to scroll");
/* 方向映射:invert=0 手指持续上滑(更负) → target 增大;invert=1 → target 减小 */
CHECK(mnp_scroll_target(55, -200, 0) == 255, "target invert=0 finger-up more -> increases");
CHECK(mnp_scroll_target(145, -200, 1) == -55, "target invert=1 finger-up more -> decreases");
CHECK(mnp_scroll_target(55, 200, 0) == -145, "target invert=0 finger-down -> decreases");
CHECK(mnp_scroll_target(145, 200, 1) == 345, "target invert=1 finger-down -> increases");
/* clamp 落位:进滚动 + 拖到边界 → 钳到 [0,max_scroll] */
CHECK(mnp_scroll_clamp(mnp_scroll_target(55, -200, 0), total, view) == 255, "clamp invert=0 up -> in range");
CHECK(mnp_scroll_clamp(mnp_scroll_target(145, -200, 1), total, view) == 0, "clamp invert=1 up -> bottom(0)");
CHECK(mnp_scroll_clamp(mnp_scroll_target(55, 200, 0), total, view) == 0, "clamp invert=0 down -> top(0)");
/* mnp_scroll_rebound:边内容差 tol=48;界内不触发,越过才触发 */
CHECK(mnp_scroll_rebound(0, max_scroll, 48) == 0, "rebound at top(0) in-tol -> 0");
CHECK(mnp_scroll_rebound(-47, max_scroll, 48) == 0, "rebound top overshoot 47<48 -> 0");
CHECK(mnp_scroll_rebound(-49, max_scroll, 48) == 1, "rebound top overshoot 49>48 -> 1");
CHECK(mnp_scroll_rebound(max_scroll, max_scroll, 48) == 0, "rebound at bottom in-tol -> 0");
CHECK(mnp_scroll_rebound(max_scroll + 47, max_scroll, 48) == 0, "rebound bottom overshoot 47<48 -> 0");
CHECK(mnp_scroll_rebound(max_scroll + 49, max_scroll, 48) == 1, "rebound bottom overshoot 49>48 -> 1");
CHECK(mnp_scroll_rebound(500, max_scroll, 48) == 0, "rebound mid-range -> 0");
}
/* P3 开关平移动画状态机:p 推进/翻转/中断、offset 计算、裁切边界、关闭到 0 才退出。
* 用任意 fix/ph 参数化(默认 1024/692,通知中心面板 PH=720-28=692);纯函数不依赖 fb。 */
static void test_anim_pure(void)
{
fprintf(stderr, "--- P3 open/close slide animation state machine ---\n");
enum { FIX = 1024, PH = 692 };
/* mnp_anim_off:p→offset 线性映射(∈[-PH,0];p=0 全隐藏 / p=FIX 到位) */
CHECK(mnp_anim_off(0, FIX, PH) == -PH, "anim_off p=0 -> -PH (fully hidden above)");
CHECK(mnp_anim_off(FIX, FIX, PH) == 0, "anim_off p=FIX -> 0 (settled open)");
CHECK(mnp_anim_off(FIX / 2, FIX, PH) == -(PH / 2), "anim_off mid -> -PH/2");
/* mnp_anim_step:打开推进(0→FIX) */
CHECK(mnp_anim_step(0, FIX, FIX, 180, 180) == FIX, "step dt=dur -> reaches FIX in one tick");
CHECK(mnp_anim_step(0, FIX, FIX, 90, 180) == FIX / 2, "step dt=dur/2 -> half progress");
CHECK(mnp_anim_step(0, FIX, FIX, 10, 180) >= 1, "step small dt -> at least 1 (monotonic, no stall)");
CHECK(mnp_anim_step(0, FIX, FIX, 0, 180) == 0, "step dt=0 -> no move");
CHECK(mnp_anim_step(0, FIX, FIX, 100000, 180) == FIX, "step huge dt -> clamped at FIX");
/* 反向推进(关闭:FIX→0,线性步进率 = dt*fix/dur,非剩余比例) */
CHECK(mnp_anim_step(FIX, 0, FIX, 90, 180) == FIX / 2, "step close dt=dur/2 -> half back");
CHECK(mnp_anim_step(FIX / 2, 0, FIX, 45, 180) == FIX / 4, "step reversal mid -> toward 0 linearly");
CHECK(mnp_anim_step(FIX / 4, 0, FIX, 100000, 180) == 0, "step reversal huge dt -> clamped at 0");
/* 打断(打开中→关闭→再打开):平滑转向,不跳变到目标 */
int p = mnp_anim_step(0, FIX, FIX, 90, 180); /* 打开到半程 (512) */
int p_before = p;
p = mnp_anim_step(p, 0, FIX, 30, 180); /* 打断:转为关闭,小步推进 */
CHECK(p < p_before && p > 0, "interrupt open->close: heading to 0 (not target), mid value ok");
int s2 = 0;
while (s2++ < 1000 && p != 0) p = mnp_anim_step(p, 0, FIX, 30, 180);
CHECK(p == 0, "interrupt close completes at 0");
p = mnp_anim_step(p, FIX, FIX, 180, 180); /* 关闭到位后再打开 */
CHECK(p == FIX, "reopen after interrupt -> settled open");
/* mnp_anim_covered_bot:裁切边界(off∈[-PH,0] → bot∈[top, top+PH]) */
CHECK(mnp_anim_covered_bot(28, PH, -PH) == 28, "covered_bot fully hidden -> top (statusbar bottom)");
CHECK(mnp_anim_covered_bot(28, PH, 0) == 28 + PH, "covered_bot settled -> top+PH");
CHECK(mnp_anim_covered_bot(28, PH, -(PH / 2)) == 28 + PH / 2, "covered_bot halfway -> top+PH/2");
CHECK(mnp_anim_covered_bot(28, PH, 0) <= 720 && mnp_anim_covered_bot(28, PH, -PH) >= 28,
"covered_bot within [28,720] — 不盖状态栏、不出屏底");
/* 完整关闭:多次 tick 才到 0;单调不增;且只在 p==0 才算退出 */
p = FIX;
int steps = 0, prev = p, monotonic = 1;
while (steps++ < 100000 && p != 0) {
int np = mnp_anim_step(p, 0, FIX, 10, 180);
if (np > prev) monotonic = 0; /* 关闭方向必须单调减小 */
if (np < 0 || np > FIX) monotonic = 0;
p = np; prev = np;
}
CHECK(p == 0, "close animation terminates at p=0 (exit condition)");
CHECK(steps > 1, "close takes multiple ticks (smooth sliding, not a jump)");
CHECK(monotonic, "close progress monotonic non-increasing");
CHECK(mnp_anim_step(0, 0, FIX, 10, 180) == 0, "at p==0 with target 0 stays put (would break)");
}
/* P5 ⑥ 勿扰过滤 + ⑦ 行左滑删除(纯逻辑,header-only):
* - mnp_dnd_should_show:dnd=0 全显示;dnd=1 只 urgency>=2(HIGH/CRITICAL)。
* - mnp_swipe_clamp:跟手 clamp 到 [-row_w,0]。
* - mnp_swipe_delete:松手阈值(行宽百分比 / 绝对像素,取大者)。
* - mnp_swipe_step:落位动画线性步进(回弹/滑出,正反向,到底停,半程取消)。
* 另加一段 mini 状态机:手指拖 → 松手过阈值=滑出删除 / 未过=回弹复原(跟手、半程可回)。 */
static void test_dnd_swipe_pure(void)
{
fprintf(stderr, "--- P5 DND filter + row left-swipe ---\n");
/* mnp_dnd_should_show */
CHECK(mnp_dnd_should_show(0, 0) == 1, "dnd off -> LOW(0) shows");
CHECK(mnp_dnd_should_show(1, 0) == 1, "dnd off -> NORMAL(1) shows");
CHECK(mnp_dnd_should_show(2, 0) == 1, "dnd off -> HIGH(2) shows");
CHECK(mnp_dnd_should_show(3, 0) == 1, "dnd off -> CRITICAL(3) shows");
CHECK(mnp_dnd_should_show(0, 1) == 0, "dnd on -> LOW(0) hidden");
CHECK(mnp_dnd_should_show(1, 1) == 0, "dnd on -> NORMAL(1) hidden");
CHECK(mnp_dnd_should_show(2, 1) == 1, "dnd on -> HIGH(2) shows");
CHECK(mnp_dnd_should_show(3, 1) == 1, "dnd on -> CRITICAL(3) shows");
/* mnp_swipe_clamp(跟手 offset clamp:off∈[-row_w,0]) */
CHECK(mnp_swipe_clamp(-200, 456) == -200, "swipe_clamp in-range kept");
CHECK(mnp_swipe_clamp(30, 456) == 0, "swipe_clamp rightward -> 0");
CHECK(mnp_swipe_clamp(-500, 456) == -456, "swipe_clamp past left -> -row_w");
CHECK(mnp_swipe_clamp(-456, 456) == -456, "swipe_clamp exactly -row_w kept");
CHECK(mnp_swipe_clamp(0, 456) == 0, "swipe_clamp 0 kept");
/* mnp_swipe_delete(row_w=456, frac_pct=40 → 40%=182.4;min_px=110;OR 语义 → 有效阈值=110px) */
CHECK(mnp_swipe_delete(-80, 456, 40, 110) == 0, "delete -80 (<110 & <182.4) -> cancel");
CHECK(mnp_swipe_delete(-110, 456, 40, 110) == 1, "delete -110 (>=min_px) -> delete");
CHECK(mnp_swipe_delete(-150, 456, 40, 110) == 1, "delete -150 (>=min_px, <40%) -> delete");
CHECK(mnp_swipe_delete(-183, 456, 40, 110) == 1, "delete -183 (>=40%) -> delete");
CHECK(mnp_swipe_delete(-456, 456, 40, 110) == 1, "delete full slide -> delete");
CHECK(mnp_swipe_delete(0, 456, 40, 110) == 0, "delete 0 -> cancel");
/* 窄行:40%=24px(row_w=60),min_px=110 不起作用 → 阈值=24px */
CHECK(mnp_swipe_delete(-25, 60, 40, 110) == 1, "delete narrow row -25 (>=40%) -> delete");
CHECK(mnp_swipe_delete(-20, 60, 40, 110) == 0, "delete narrow row -20 (<40%) -> cancel");
/* mnp_swipe_step(线性步进:off 向 target 走 dt*rate,可正/反向,到底停) */
CHECK(mnp_swipe_step(-100, 0, 10, 3) == -70, "step springback +30px/tick");
CHECK(mnp_swipe_step(-100, 0, 0, 3) == -100, "step dt=0 -> unchanged");
CHECK(mnp_swipe_step(-100, 0, 1000, 3) == 0, "step huge dt -> clamp at 0 (springback done)");
CHECK(mnp_swipe_step(-50, -456, 10, 3) == -80, "step slide-out leftward");
CHECK(mnp_swipe_step(-50, -456, 1000, 3) == -456, "step huge dt -> clamp at -row_w (deleted)");
CHECK(mnp_swipe_step(-30, -30, 10, 3) == -30, "step at target -> stays");
CHECK(mnp_swipe_step(0, 0, 10, 3) == 0, "step 0 target 0 -> stays");
CHECK(mnp_swipe_step(-1000, 0, 5, 3) == -985, "step linear rate not exponential (small dt)");
/* mini 状态机:跟手拖 → 松手判定 → 动画到目标(半程取消 vs 删除)。 */
int row_w = 456, off = 0;
/* 拖 80px 左(低于阈值 110)→ 松手回弹复原 */
off = mnp_swipe_clamp(-80, row_w);
CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 0, "mini: drag -80 -> cancel (springback)");
int target = mnp_swipe_delete(off, row_w, 40, 110) ? -row_w : 0;
off = mnp_swipe_step(off, target, 1000, 3);
CHECK(off == 0, "mini: springback reaches 0 (row restored)");
/* 拖 150px 左(>=110)→ 松手滑出删除 */
off = mnp_swipe_clamp(-150, row_w);
CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 1, "mini: drag -150 -> delete");
target = mnp_swipe_delete(off, row_w, 40, 110) ? -row_w : 0;
off = mnp_swipe_step(off, target, 1000, 3);
CHECK(off == -row_w, "mini: slide-out reaches -row_w (dismiss)");
/* 半程可反向:拖 200 左再反向回摄(dx 变 +30)→ off 回 0(跟手可回、松手在途取消) */
off = mnp_swipe_clamp(-200, row_w);
CHECK(off == -200, "mini: drag -200 follows finger");
off = mnp_swipe_clamp(30, row_w); /* 反向拖回原点左侧以右 → clamp 0 */
CHECK(off == 0, "mini: reverse drag -> row slides back to 0 (halfway cancel)");
/* 半程取消:拖 150 左(未达 182.4 的 40% 但达 110px 阈值)→ 松手删;若再拖回 <110 → 回弹 */
off = mnp_swipe_clamp(-150, row_w);
CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 1, "mini: -150 passed threshold -> delete");
off = mnp_swipe_clamp(-100, row_w); /* 又拖回 100 <110 */
CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 0, "mini: -100 back under threshold -> cancel");
}
static void test_storage_contract(void)
{
fprintf(stderr, "--- P0 storage contract (panel reads) ---\n");
/* meow_notif_init:应清内存表 + 清 tf 文件(路径已指临时);配额用默认(stub 返回 NOENT) */
CHECK(meow_notif_init() == NOTIF_OK, "meow_notif_init ok");
CHECK(meow_notif_count(NULL) == 0, "init clears memory table");
CHECK(g_max_cats == 4 && g_cat_quota == 4 && g_media_quota == 1,
"quota defaults from meow_settings backend (stub NOENT -> defaults)");
meow_notif_t n; memset(&n, 0, sizeof n);
snprintf(n.app_id, sizeof n.app_id, "os.meow.hello");
snprintf(n.category, sizeof n.category, "general");
snprintf(n.summary, sizeof n.summary, "你好世界");
snprintf(n.body, sizeof n.body, "这是正文第二行");
snprintf(n.app_icon, sizeof n.app_icon, "apps/os.meow.hello/icon.png");
snprintf(n.target_activity, sizeof n.target_activity, "activity_main");
n.expire_timeout = 0; n.progress = -1; n.urgency = 1;
uint32_t id1 = 0;
CHECK(meow_notif_post(&n, &id1) == NOTIF_OK, "post general ok");
CHECK(id1 != 0, "post assigns id");
/* 媒体通知:ongoing + 进度 */
memset(&n, 0, sizeof n);
snprintf(n.app_id, sizeof n.app_id, "os.meow.hello");
snprintf(n.category, sizeof n.category, "media");
snprintf(n.summary, sizeof n.summary, "正在播放");
n.expire_timeout = 0xFFFFFFFFu; n.ongoing = 1; n.progress = 20; n.urgency = 2;
uint32_t id2 = 0;
CHECK(meow_notif_post(&n, &id2) == NOTIF_OK, "post media ok");
/* 高紧急 + 进度 */
memset(&n, 0, sizeof n);
snprintf(n.app_id, sizeof n.app_id, "system");
snprintf(n.category, sizeof n.category, "event");
snprintf(n.summary, sizeof n.summary, "系统提醒");
snprintf(n.body, sizeof n.body, "电量低");
n.progress = 50; n.urgency = 3;
uint32_t id3 = 0;
CHECK(meow_notif_post(&n, &id3) == NOTIF_OK, "post event(high) ok");
CHECK(meow_notif_count(NULL) == 3, "count == 3 after posts");
/* 遍历(面板 ncp_collect_cb 同款字段读取)*/
int got = grab_list();
CHECK(got == 3, "list returns 3 active notifs");
gslot_t *s1 = find_slot("你好世界");
CHECK(s1 != NULL, "panel sees summary 你好世界");
if (s1) {
CHECK(!strcmp(s1->app, "os.meow.hello"), "app_id");
CHECK(!strcmp(s1->body, "这是正文第二行"), "body");
CHECK(!strcmp(s1->icon, "apps/os.meow.hello/icon.png"), "app_icon");
CHECK(!strcmp(s1->tgt, "activity_main"), "target_activity");
CHECK(s1->progress == -1, "general progress=-1 (none)");
CHECK(s1->urgency == 1, "general urgency=NORMAL(1)");
CHECK(s1->ms > 0, "created_ms set");
}
gslot_t *s2 = find_slot("正在播放");
CHECK(s2 != NULL && s2->progress == 20 && s2->urgency == 2, "media progress=20 urgency=HIGH(2)");
gslot_t *s3 = find_slot("系统提醒");
CHECK(s3 != NULL && s3->progress == 50 && s3->urgency == 3, "event progress=50 urgency=CRITICAL(3)");
/* dismiss → 面板列表减一(点通知=阅读)*/
CHECK(meow_notif_dismiss(id1) == NOTIF_OK, "dismiss general ok");
CHECK(meow_notif_count(NULL) == 2, "count == 2 after dismiss");
grab_list();
CHECK(find_slot("你好世界") == NULL, "dismissed notif no longer listed");
/* 里程碑 100% → 释放(非 ONGOING):面板列表再减一 */
CHECK(meow_notif_update_progress(id3, 100) == NOTIF_OK, "update_progress(100) ok");
CHECK(meow_notif_count(NULL) == 1, "milestone released event slot -> count 1");
grab_list();
CHECK(find_slot("系统提醒") == NULL, "100% milestone notif no longer listed");
/* 剩余媒体通知仍活跃(ONGOING 常驻)*/
CHECK(find_slot("正在播放") != NULL, "ongoing media stays listed");
/* 配额:单 app 媒体单例(media_quota=1)——再发一条 media 顶掉旧媒体 */
memset(&n, 0, sizeof n);
snprintf(n.app_id, sizeof n.app_id, "os.meow.hello");
snprintf(n.category, sizeof n.category, "media");
snprintf(n.summary, sizeof n.summary, "下一首");
n.expire_timeout = 0xFFFFFFFFu; n.ongoing = 1; n.progress = 60;
uint32_t id4 = 0;
CHECK(meow_notif_post(&n, &id4) == NOTIF_OK, "post second media ok");
CHECK(meow_notif_count(NULL) == 1, "media quota=1 -> old media replaced, count stays 1");
grab_list();
CHECK(find_slot("下一首") != NULL && find_slot("正在播放") == NULL,
"media singleton replaced oldest");
}
int main(void)
{
char tmp[] = "/tmp/meow_notif_panel_XXXXXX";
char *dir = mkdtemp(tmp);
if (!dir) { perror("mkdtemp"); return 2; }
snprintf(g_tflog, sizeof g_tflog, "%s/notif.log", dir);
meow_notif_set_tf_path(g_tflog);
meow_notif_set_tf_max(1024 * 1024);
test_pure_logic();
test_scroll_pure();
test_anim_pure();
test_dnd_swipe_pure();
test_storage_contract();
unlink(g_tflog); rmdir(dir);
fprintf(stderr, "\n%s (failures=%d)\n", g_fail ? "== TEST FAILED ==" : "== TEST PASSED ==", g_fail);
return g_fail ? 1 : 0;
}