mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
fix(recovery): readiness leaves accounts pending deletion out
The weekly and off-host passes skip an account pending deletion on purpose, and the readiness read counted it anyway: within days of any deletion the weekly copy read failing and the off-host copies stale, every admin was pushed a problem and then a recovery after the purge, and a real failure hid behind a signal already red. The read now filters with ACTIVE_ACCOUNT, and the enumeration guard covers the file.
This commit is contained in:
1 parent
be78517500
commit
763cb48273
3 files changed
+84
No files matched your search
@@ -49,6 +49,9 @@ const SRC = join(process.cwd(), "src");
|
||||
const EXTRA_FILES = [
|
||||
"lib/measurements/consolidation-base.ts",
|
||||
"lib/integrations/watchdog.ts",
|
||||
// Read by the hourly readiness evaluation, which judges every account's
|
||||
// copies.
|
||||
"lib/recovery/readiness.ts",
|
||||
];
|
||||
|
||||
function backgroundFiles(): string[] {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* The readiness read leaves accounts pending deletion out, as the weekly and
|
||||
* off-host passes do. Their copies stop on purpose; counted, one deletion
|
||||
* turned "weekly copy" failing and "off-host copies" stale, and every admin
|
||||
* was pushed about a backup that was not broken.
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const NOW = new Date("2026-11-10T12:00:00Z");
|
||||
const DAY = 86_400_000;
|
||||
const ago = (ms: number) => new Date(NOW.getTime() - ms);
|
||||
|
||||
const healthy = {
|
||||
createdAt: ago(400 * DAY),
|
||||
dataBackups: [{ createdAt: ago(2 * DAY) }],
|
||||
offhostBackupState: {
|
||||
lastAttemptAt: ago(0.4 * DAY),
|
||||
lastSuccessAt: ago(0.4 * DAY),
|
||||
},
|
||||
};
|
||||
// Pending deletion for ten days: both passes have skipped it since.
|
||||
const pending = {
|
||||
createdAt: ago(400 * DAY),
|
||||
dataBackups: [{ createdAt: ago(15 * DAY) }],
|
||||
offhostBackupState: {
|
||||
lastAttemptAt: ago(10.4 * DAY),
|
||||
lastSuccessAt: ago(10.4 * DAY),
|
||||
},
|
||||
deletionRequestedAt: ago(10 * DAY),
|
||||
};
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
prisma: {
|
||||
user: {
|
||||
findFirst: vi.fn(async () => ({ createdAt: ago(400 * DAY) })),
|
||||
findMany: vi.fn(
|
||||
async (args: { where?: { deletionRequestedAt?: null } }) =>
|
||||
[healthy, pending].filter(
|
||||
(a) =>
|
||||
!(args.where && "deletionRequestedAt" in args.where) ||
|
||||
!("deletionRequestedAt" in a),
|
||||
),
|
||||
),
|
||||
},
|
||||
restoreDrillOutcome: { findUnique: vi.fn(async () => null) },
|
||||
recoveryReadinessNotice: { findUnique: vi.fn(async () => null) },
|
||||
},
|
||||
}));
|
||||
vi.mock("@/lib/jobs/offhost-backup", () => ({
|
||||
offhostBackupConfigured: () => true,
|
||||
probeOffhostLifecycle: async () => ({
|
||||
database: { state: "configured", expirationDays: 30 },
|
||||
}),
|
||||
}));
|
||||
vi.mock("@/lib/recovery/kit", () => ({
|
||||
readRecoveryKit: async () => ({
|
||||
confirmedAt: NOW.toISOString(),
|
||||
unconfirmed: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
const { readRecoveryReadiness } = await import("@/lib/recovery/readiness");
|
||||
|
||||
describe("readiness and accounts pending deletion", () => {
|
||||
it("judges the weekly and off-host copies of active accounts only", async () => {
|
||||
const r = await readRecoveryReadiness({
|
||||
now: NOW,
|
||||
dumpHealth: async () => null,
|
||||
});
|
||||
const byId = Object.fromEntries(
|
||||
r.signals.map((s) => [s.id, `${s.state}/${s.reason}`]),
|
||||
);
|
||||
expect(byId.weekly_copy).toMatch(/^ok\//);
|
||||
expect(byId.offhost_copies).toMatch(/^ok\//);
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@
|
||||
* - the kit: the same assessment the kit card shows;
|
||||
* - the dump: whatever the dump module reports, through a provider.
|
||||
*/
|
||||
import { ACTIVE_ACCOUNT } from "@/lib/account-deletion/active-account";
|
||||
import { prisma } from "@/lib/db";
|
||||
import {
|
||||
offhostBackupConfigured,
|
||||
@@ -53,7 +54,11 @@ export async function readReadinessInputs(
|
||||
orderBy: { createdAt: "asc" },
|
||||
select: { createdAt: true },
|
||||
}),
|
||||
// The weekly and off-host passes skip an account pending deletion on
|
||||
// purpose, so its copies age without anything being wrong; counted
|
||||
// here, every deletion turned both signals red and pushed the admins.
|
||||
prisma.user.findMany({
|
||||
where: ACTIVE_ACCOUNT,
|
||||
select: {
|
||||
createdAt: true,
|
||||
dataBackups: {
|
||||
|
||||
Reference in new issue
Block a user