mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(daily): send the morning briefing push with AI analysis off
The push refused outright while the `insights` module was off, which now means only "AI analysis". The digest it reads already drops the model-written lead and top signal in that case, so the push goes out with the deterministic line (the score, or the day's items) and a digest with nothing to say is still skipped. The `module-off` outcome and its counter are gone. Refs #975
This commit is contained in:
1 parent
c509afeb2e
commit
7df260d62e
4 files changed
+23
-26
No files matched your search
@@ -82,7 +82,6 @@ function makeDeps(
|
||||
return {
|
||||
dispatch,
|
||||
loadDigest: async () => makeDigest(),
|
||||
isModuleEnabled: async () => true,
|
||||
...over,
|
||||
} as DailyBriefingDispatchDeps & { dispatch: ReturnType<typeof vi.fn> };
|
||||
}
|
||||
@@ -253,17 +252,27 @@ describe("maybeDispatchDailyBriefing", () => {
|
||||
expect(deps.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("insights module off → no dispatch", async () => {
|
||||
it("AI analysis off: the push still goes out with the deterministic line", async () => {
|
||||
// The digest the loader returns when the briefing capability is
|
||||
// unavailable: no lead, no top signal, the score floor as the line.
|
||||
const prisma = makePrisma({});
|
||||
const deps = makeDeps({ isModuleEnabled: async () => false });
|
||||
const deps = makeDeps({
|
||||
loadDigest: async () =>
|
||||
makeDigest({
|
||||
briefingLead: null,
|
||||
topSignal: null,
|
||||
line: "Your health score today is 82.",
|
||||
}),
|
||||
});
|
||||
const result = await maybeDispatchDailyBriefing(
|
||||
prisma,
|
||||
"u1",
|
||||
IN_WINDOW,
|
||||
deps,
|
||||
);
|
||||
expect(result).toBe("module-off");
|
||||
expect(deps.dispatch).not.toHaveBeenCalled();
|
||||
expect(result).toBe("sent");
|
||||
const payload = JSON.stringify(deps.dispatch.mock.calls[0]);
|
||||
expect(payload).toContain("Your health score today is 82.");
|
||||
});
|
||||
|
||||
it("frequency cap: an ok ledger row earlier the SAME local day suppresses the second push", async () => {
|
||||
|
||||
@@ -32,7 +32,6 @@ import type { Locale } from "@/lib/i18n/config";
|
||||
import { resolveJobLocale } from "@/lib/i18n/job-locale";
|
||||
import { getServerTranslator } from "@/lib/i18n/server-translator";
|
||||
import type { ServerTranslator } from "@/lib/i18n/server-translator";
|
||||
import { isModuleEnabled } from "@/lib/modules/gate";
|
||||
import { wallClockInTz } from "@/lib/tz/wall-clock";
|
||||
import { userDayKey } from "@/lib/tz/format";
|
||||
import { dispatchNotification } from "@/lib/notifications/dispatcher";
|
||||
@@ -72,7 +71,6 @@ export type DailyBriefingDispatchResult =
|
||||
| "suppressed-frequency"
|
||||
| "no-digest"
|
||||
| "opted-out"
|
||||
| "module-off"
|
||||
| "outside-window"
|
||||
| "no-channel"
|
||||
| "missing-user"
|
||||
@@ -81,7 +79,6 @@ export type DailyBriefingDispatchResult =
|
||||
export interface DailyBriefingDispatchDeps {
|
||||
dispatch?: typeof dispatchNotification;
|
||||
loadDigest?: (user: User, now: Date, locale: Locale) => Promise<DailyDigest>;
|
||||
isModuleEnabled?: typeof isModuleEnabled;
|
||||
}
|
||||
|
||||
function loadDailyBriefingDigest(
|
||||
@@ -137,10 +134,15 @@ export function buildDailyBriefingPush(
|
||||
* 1. user missing → `missing-user`
|
||||
* 2. no opt-in (default OFF) → `opted-out` (no enabled DAILY_BRIEFING pref)
|
||||
* 3. outside morning window → `outside-window`
|
||||
* 4. insights module off → `module-off`
|
||||
* 5. already pushed today → `suppressed-frequency` (push_attempts ledger)
|
||||
* 6. digest not substantive → `no-digest`
|
||||
* 7. dispatch; no channel → `no-channel`; else `sent`.
|
||||
* 4. already pushed today → `suppressed-frequency` (push_attempts ledger)
|
||||
* 5. digest not substantive → `no-digest`
|
||||
* 6. dispatch; no channel → `no-channel`; else `sent`.
|
||||
*
|
||||
* No module gate. The `insights` module is the AI analysis opt-out: with it
|
||||
* off (or AI unavailable for any other reason) the digest carries no briefing
|
||||
* lead or top signal, and the push body falls to the digest's deterministic
|
||||
* line (the score, or the all-clear). A digest with nothing deterministic to
|
||||
* say is skipped as `no-digest`, like any other.
|
||||
*/
|
||||
export async function maybeDispatchDailyBriefing(
|
||||
prisma: PrismaClient,
|
||||
@@ -150,7 +152,6 @@ export async function maybeDispatchDailyBriefing(
|
||||
): Promise<DailyBriefingDispatchResult> {
|
||||
const dispatch = deps.dispatch ?? dispatchNotification;
|
||||
const loadDigest = deps.loadDigest ?? loadDailyBriefingDigest;
|
||||
const moduleGate = deps.isModuleEnabled ?? isModuleEnabled;
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||
@@ -187,13 +188,6 @@ export async function maybeDispatchDailyBriefing(
|
||||
return "outside-window";
|
||||
}
|
||||
|
||||
// Insights is the digest's home module; a user who turned it off gets no
|
||||
// digest surfaces at all, so no morning push either.
|
||||
if (!(await moduleGate(userId, "insights"))) {
|
||||
annotate({ action: { name: "daily.briefing_push.module_off" } });
|
||||
return "module-off";
|
||||
}
|
||||
|
||||
// One per user per LOCAL day. Anchored on the `push_attempts` ledger the
|
||||
// senders already write (`ok` on a successful delivery), exactly like the
|
||||
// coach-nudge cap: whichever trigger fired first this morning left an `ok`
|
||||
|
||||
@@ -40,7 +40,6 @@ export interface DailyBriefingTickSummary {
|
||||
suppressedFrequency: number;
|
||||
noDigest: number;
|
||||
optedOut: number;
|
||||
moduleOff: number;
|
||||
noChannel: number;
|
||||
outsideWindow: number;
|
||||
failed: number;
|
||||
@@ -74,7 +73,6 @@ export async function runDailyBriefingTick(
|
||||
suppressedFrequency: 0,
|
||||
noDigest: 0,
|
||||
optedOut: 0,
|
||||
moduleOff: 0,
|
||||
noChannel: 0,
|
||||
outsideWindow: 0,
|
||||
failed: 0,
|
||||
@@ -123,9 +121,6 @@ export async function runDailyBriefingTick(
|
||||
case "opted-out":
|
||||
summary.optedOut += 1;
|
||||
break;
|
||||
case "module-off":
|
||||
summary.moduleOff += 1;
|
||||
break;
|
||||
case "no-channel":
|
||||
summary.noChannel += 1;
|
||||
break;
|
||||
|
||||
@@ -663,7 +663,6 @@ export async function registerStatusQueues(
|
||||
suppressed_frequency: summary.suppressedFrequency,
|
||||
no_digest: summary.noDigest,
|
||||
opted_out: summary.optedOut,
|
||||
module_off: summary.moduleOff,
|
||||
no_channel: summary.noChannel,
|
||||
outside_window: summary.outsideWindow,
|
||||
failed: summary.failed,
|
||||
|
||||
Reference in new issue
Block a user