import { afterEach, beforeEach, expect, test, vi } from "vitest"; import { Scheduler } from "~/server/core/scheduler"; import { serverEvents } from "~/server/core/events"; import { config } from "~/server/core/config"; import { db } from "~/server/db/db"; import { backupsService } from "~/server/modules/backups/backups.service"; import { repositoriesService } from "~/server/modules/repositories/repositories.service"; import { notificationsService } from "~/server/modules/notifications/notifications.service"; import { volumeService } from "~/server/modules/volumes/volume.service"; import * as provisioningModule from "~/server/modules/provisioning/provisioning"; import { taskStore } from "~/server/modules/tasks/tasks.store"; import { createTestBackupSchedule } from "~/test/helpers/backup"; import { createTestRepository } from "~/test/helpers/repository"; import { createTestVolume } from "~/test/helpers/volume"; import { TEST_ORG_ID } from "~/test/helpers/organization"; const loadStartupModule = async () => { const moduleUrl = new URL("../startup.ts", import.meta.url); moduleUrl.searchParams.set("test", crypto.randomUUID()); return import(moduleUrl.href); }; let originalEnableLocalAgent: boolean; beforeEach(() => { originalEnableLocalAgent = config.flags.enableLocalAgent; config.flags.enableLocalAgent = true; vi.spyOn(Scheduler, "start").mockResolvedValue(); vi.spyOn(Scheduler, "clear").mockResolvedValue(); vi.spyOn(Scheduler, "build").mockImplementation(() => ({ schedule: vi.fn() })); vi.spyOn(provisioningModule, "syncProvisionedResources").mockResolvedValue(); vi.spyOn(backupsService, "cleanupOrphanedSchedules").mockResolvedValue({ deletedSchedules: 0 }); vi.spyOn(volumeService, "updateVolume").mockResolvedValue(undefined as never); vi.spyOn(repositoriesService, "updateRepository").mockResolvedValue(undefined as never); vi.spyOn(notificationsService, "updateDestination").mockResolvedValue(undefined as never); }); afterEach(() => { config.flags.enableLocalAgent = originalEnableLocalAgent; vi.restoreAllMocks(); }); test("marks active tasks stale and keeps stuck backup schedule recovery silent", async () => { const emitSpy = vi.spyOn(serverEvents, "emit"); const notificationSpy = vi.spyOn(notificationsService, "sendBackupNotification").mockResolvedValue(); const volume = await createTestVolume(); const repository = await createTestRepository(); const schedule = await createTestBackupSchedule({ volumeId: volume.id, repositoryId: repository.id, lastBackupStatus: "in_progress", }); const task = taskStore.create({ id: "task-startup-active", organizationId: TEST_ORG_ID, resourceType: "backup_schedule", resourceId: String(schedule.id), targetAgentId: "local", input: { kind: "backup", scheduleId: schedule.id, scheduleShortId: schedule.shortId, manual: false, }, }); taskStore.markRunning(task.id); const { startup } = await loadStartupModule(); await startup(); const updatedTask = await db.query.tasksTable.findFirst({ where: { id: task.id } }); const updatedSchedule = await db.query.backupSchedulesTable.findFirst({ where: { id: schedule.id } }); expect(updatedTask?.status).toBe("stale"); expect(updatedTask?.error).toBe("Zerobyte was restarted before this task completed"); expect(updatedTask?.finishedAt).toEqual(expect.any(Number)); expect(updatedSchedule?.lastBackupStatus).toBe("warning"); expect(updatedSchedule?.lastBackupError).toBe("Zerobyte was restarted during the last scheduled backup"); expect(emitSpy).not.toHaveBeenCalledWith("backup:completed", expect.anything()); expect(notificationSpy).not.toHaveBeenCalled(); });