openpetswithchatandmcp/plugins/official/openpets.reminders/test.js
2026-06-11 18:13:56 +02:00

226 lines
7 KiB
JavaScript

// Golden test for openpets.reminders.
//
// Runs two ways:
// * `node test.js` (via scripts/test-plugins.mjs) — pure-helper unit checks
// plus the harness-driven golden test.
// * authored against `@open-pets/plugin-sdk/testing`; when that bare
// specifier isn't resolvable from this directory we fall back to the
// built workspace dist so the test still runs standalone.
import assert from "node:assert/strict";
import {
cleanMessage,
durationMs,
summary,
register,
MAX_REMINDERS,
} from "./index.js";
let createTestHarness;
try {
({ createTestHarness } = await import("@open-pets/plugin-sdk/testing"));
} catch {
({ createTestHarness } = await import(
new URL("../../../packages/sdk/dist/testing.js", import.meta.url)
));
}
// --- pure helper unit checks --------------------------------------------
assert.equal(cleanMessage(" hello\nthere "), "hello there");
assert.equal(cleanMessage("", "fallback"), "fallback");
assert.equal(cleanMessage("x".repeat(500)).length, 140);
assert.equal(durationMs({ hours: 1, minutes: 30 }), 90 * 60_000);
assert.equal(durationMs({ minutes: 15 }), 15 * 60_000);
assert.throws(() => durationMs({ hours: 0, minutes: 0 }));
// Out-of-range fields are clamped, not rejected: 25h -> 23h is still valid.
assert.equal(durationMs({ hours: 25, minutes: 0 }), 23 * 60 * 60_000);
assert.equal(summary([]), "No active reminders.");
assert.equal(
summary([{ id: "a", message: "tea", dueAt: 1_000 + 5 * 60_000 }], 1_000),
"5 min: tea",
);
// --- golden harness test -------------------------------------------------
const PERMISSIONS = [
"pet:speak",
"pet:interact",
"audio",
"schedule",
"storage",
"commands",
"status",
"notify",
];
const LOCALES = {
en: JSON.parse(
await (await import("node:fs/promises")).readFile(
new URL("./locales/en.json", import.meta.url),
"utf8",
),
),
};
// 1) Setting a reminder via the form schedules it and stores it.
{
const h = createTestHarness(register, {
permissions: PERMISSIONS,
config: { soundEnabled: true, osNotification: true, customSound: "gong" },
locales: LOCALES,
nowMs: 1_000_000,
});
await h.start();
await h.runCommand("set-reminder", { message: "Drink water", hours: 0, minutes: 30 });
h.expectStored("reminders", (v) => Array.isArray(v) && v.length === 1 && v[0].message === "Drink water");
h.expectSpoke(/30 min/);
assert.equal(h.calls.schedules.size, 1, "expected one scheduled reminder");
// Advancing past the due time fires the acknowledge-pattern delivery.
await h.clock.advance("31m");
h.expectBubble({
indicator: {
icon: { kind: "icon", name: "reminder" },
label: "Reminder",
tone: "info",
color: "#7c3aed",
background: "#ede9fe",
borderColor: "#c4b5fd",
},
tone: "info",
sticky: true,
priority: "high",
});
h.expectBubble({ textMatch: /Drink water/ });
h.expectNotified(/Drink water/);
assert.equal(h.calls.alerts.length, 1, "expected ctx.ui.alert delivery");
assert.ok(h.calls.sounds.some((s) => s.sound === "gong"), "expected the custom alert sound to play");
// Fired reminder is removed from storage.
h.expectStored("reminders", (v) => Array.isArray(v) && v.length === 0);
h.expectNoErrors();
}
// 2) A preset reminder fires and the Snooze action reschedules +5m.
{
const h = createTestHarness(register, {
permissions: PERMISSIONS,
config: { soundEnabled: false, osNotification: false },
locales: LOCALES,
nowMs: 2_000_000,
});
await h.start();
await h.runCommand("reminder-15");
h.expectStored("reminders", (v) => v.length === 1);
assert.equal(h.calls.sounds.length, 0, "sound disabled — nothing should play");
await h.clock.advance("16m");
const bubble = h.calls.bubbles[h.calls.bubbles.length - 1];
assert.ok(bubble, "expected a delivery bubble");
assert.deepEqual(
bubble.spec.actions?.map((a) => a.id),
["done", "snooze"],
"expected Done + Snooze actions",
);
assert.equal(h.calls.alerts.length, 1, "preset delivery should use ctx.ui.alert");
assert.equal(h.calls.notifications.length, 0, "osNotification disabled — no notification");
// Snooze: reschedules a fresh reminder ~5 minutes out.
await h.fireBubbleAction(bubble.handle.id, "snooze");
h.expectStored("reminders", (v) => v.length === 1 && v[0].dueAt > h.clock.now());
h.expectNoErrors();
}
// 3) view-reminders lists pending items with a per-item cancel.
{
const h = createTestHarness(register, {
permissions: PERMISSIONS,
locales: LOCALES,
nowMs: 3_000_000,
});
await h.start();
await h.runCommand("reminder-30");
await h.runCommand("reminder-60");
await h.runCommand("view-reminders");
assert.equal(h.calls.menuItems.length, 2, "expected two pending menu items");
// Selecting an item's cancel removes that reminder.
await h.calls.menuItems[0].onSelect();
h.expectStored("reminders", (v) => v.length === 1);
h.expectNoErrors();
}
// 4) reconcile() on start fires overdue reminders as "missed".
{
const h = createTestHarness(register, {
permissions: PERMISSIONS,
config: { soundEnabled: true, osNotification: true },
locales: LOCALES,
nowMs: 4_000_000,
});
// Seed storage with an already-overdue reminder before start().
await h.ctx.storage.set("reminders", [
{ id: "reminder-old", message: "Stand up", dueAt: 4_000_000 - 60_000 },
]);
await h.start();
h.expectBubble({ textMatch: /Stand up/ });
h.expectBubble({
indicator: {
icon: { kind: "icon", name: "reminder" },
label: "Missed reminder",
tone: "warning",
color: "#d97706",
background: "#fef3c7",
borderColor: "#fbbf24",
},
});
h.expectNotified(/Stand up/);
h.expectStored("reminders", (v) => Array.isArray(v) && v.length === 0);
h.expectNoErrors();
}
// 5) clear-reminders cancels everything.
{
const h = createTestHarness(register, { permissions: PERMISSIONS, locales: LOCALES });
await h.start();
await h.runCommand("reminder-15");
await h.runCommand("clear-reminders");
h.expectStored("reminders", (v) => Array.isArray(v) && v.length === 0);
assert.equal(h.calls.schedules.size, 0, "expected no scheduled reminders after clear");
h.expectSpoke(/cleared/i);
h.expectNoErrors();
}
// 6) test-reminder previews the alert without storing a reminder.
{
const h = createTestHarness(register, {
permissions: PERMISSIONS,
config: { soundEnabled: false, osNotification: false },
locales: LOCALES,
nowMs: 5_000_000,
});
await h.start();
await h.runCommand("test-reminder");
h.expectBubble({ textMatch: /test reminder/i });
h.expectBubble({
indicator: {
icon: { kind: "icon", name: "reminder" },
label: "Reminder",
tone: "info",
color: "#7c3aed",
background: "#ede9fe",
borderColor: "#c4b5fd",
},
sticky: true,
priority: "high",
});
h.expectStored("reminders", (v) => Array.isArray(v) && v.length === 0);
h.expectNoErrors();
}
// 7) MAX_REMINDERS guard.
assert.equal(MAX_REMINDERS, 10);
console.log("openpets.reminders: all checks passed.");