diff --git a/.env.test b/.env.test index 131beb69..d6805656 100644 --- a/.env.test +++ b/.env.test @@ -1,3 +1,4 @@ ZEROBYTE_DATABASE_URL=:memory: APP_SECRET=8b9acd4456dd5db0a4a3c4f4e1240b2c3ae08bb59690167197425e4a25dd9a69 BASE_URL=http://localhost:4096 +ENABLE_LOCAL_AGENT=true diff --git a/app/server/modules/agents/__tests__/agents-manager.test.ts b/app/server/modules/agents/__tests__/agents-manager.test.ts index dd9310f2..c55d222d 100644 --- a/app/server/modules/agents/__tests__/agents-manager.test.ts +++ b/app/server/modules/agents/__tests__/agents-manager.test.ts @@ -1,6 +1,7 @@ import { EventEmitter } from "node:events"; import { PassThrough } from "node:stream"; -import { afterEach, expect, test, vi } from "vitest"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; +import type { ProcessWithAgentRuntime } from "../helpers/runtime-state.dev"; const spawnMock = vi.fn(); @@ -8,7 +9,19 @@ vi.mock("node:child_process", async () => { return { spawn: spawnMock }; }); -const { spawnLocalAgent, stopLocalAgent } = await import("../agents-manager"); +let spawnLocalAgent: (typeof import("../agents-manager"))["spawnLocalAgent"]; +let stopLocalAgent: (typeof import("../agents-manager"))["stopLocalAgent"]; + +const processWithAgentRuntime = process as ProcessWithAgentRuntime; + +const setAgentRuntime = () => { + processWithAgentRuntime.__zerobyteAgentRuntime = { + agentManager: null, + localAgent: null, + isStoppingLocalAgent: false, + localAgentRestartTimeout: null, + }; +}; type FakeChildProcess = EventEmitter & { stdout: PassThrough; @@ -34,8 +47,15 @@ const createFakeChild = () => { return child; }; +beforeEach(async () => { + vi.resetModules(); + setAgentRuntime(); + ({ spawnLocalAgent, stopLocalAgent } = await import("../agents-manager")); +}); + afterEach(async () => { await stopLocalAgent(); + delete processWithAgentRuntime.__zerobyteAgentRuntime; spawnMock.mockReset(); vi.restoreAllMocks(); vi.useRealTimers(); diff --git a/app/server/modules/agents/agents-manager.ts b/app/server/modules/agents/agents-manager.ts index 5680abbf..6782dcad 100644 --- a/app/server/modules/agents/agents-manager.ts +++ b/app/server/modules/agents/agents-manager.ts @@ -152,6 +152,7 @@ export const startAgentRuntime = async () => { if (runtime.agentManager) { await runtime.agentManager.stop(); + runtime.agentManager = null; } const { createAgentManagerRuntime } = await import("./controller/server"); diff --git a/app/server/modules/agents/controller/session.ts b/app/server/modules/agents/controller/session.ts index f010f5a5..999cec41 100644 --- a/app/server/modules/agents/controller/session.ts +++ b/app/server/modules/agents/controller/session.ts @@ -147,8 +147,8 @@ export const createControllerAgentSession = ( yield* Effect.sync(() => { try { const sendResult = socket.send(message); - if (sendResult <= 0) { - handleSendFailure(sendResult === 0 ? "connection issue" : "backpressure"); + if (sendResult === 0) { + handleSendFailure("connection issue"); } } catch (error) { handleSendFailure(toMessage(error)); diff --git a/app/server/modules/backups/__tests__/backups.service.execution.test.ts b/app/server/modules/backups/__tests__/backups.service.execution.test.ts index c9b236b9..cdc4bd4d 100644 --- a/app/server/modules/backups/__tests__/backups.service.execution.test.ts +++ b/app/server/modules/backups/__tests__/backups.service.execution.test.ts @@ -283,6 +283,29 @@ describe("stop backup", () => { ); }); + test("should settle and mark the backup as failed when the backup process throws", async () => { + const { resticBackupMock } = setup(); + const volume = await createTestVolume(); + const repository = await createTestRepository(); + const schedule = await createTestBackupSchedule({ + volumeId: volume.id, + repositoryId: repository.id, + }); + + resticBackupMock.mockImplementationOnce(() => Promise.reject(new Error("restic crashed"))); + + const result = await Promise.race([ + backupsService.executeBackup(schedule.id).then(() => "settled"), + new Promise((resolve) => setTimeout(() => resolve("timed-out"), 100)), + ]); + + expect(result).toBe("settled"); + + const updatedSchedule = await getScheduleByIdOrShortId(schedule.id); + expect(updatedSchedule.lastBackupStatus).toBe("error"); + expect(updatedSchedule.lastBackupError).toBe("Error: restic crashed"); + }); + test("should block forget on the same repository until the active backup completes", async () => { const { resticBackupMock, resticForgetMock, runBackupMock } = setup(); const volume = await createTestVolume(); diff --git a/app/test/helpers/agent-mock.ts b/app/test/helpers/agent-mock.ts index 1f00b14f..4fbf20e5 100644 --- a/app/test/helpers/agent-mock.ts +++ b/app/test/helpers/agent-mock.ts @@ -74,7 +74,10 @@ export const createAgentBackupMocks = ( status: "failed", error: stderrLines.join("\n") || resultWithStderr.stderr || result.error, }); - })().catch(() => {}); + })().catch((err) => { + runningBackups.delete(request.scheduleId); + resolve({ status: "failed", error: String(err) }); + }); }); }, );