From f254117f6eee09184e3cf5409092d2c618fb361f Mon Sep 17 00:00:00 2001 From: Dhruv Sharma Date: Sun, 10 May 2026 17:09:32 +0530 Subject: [PATCH] fix(core): cover single-session recovery failures --- .../src/__tests__/recovery-manager.test.ts | 51 ++++++++++++++++++- packages/core/src/metadata.ts | 2 +- packages/core/src/recovery/manager.ts | 50 +++++++++++------- 3 files changed, 82 insertions(+), 21 deletions(-) diff --git a/packages/core/src/__tests__/recovery-manager.test.ts b/packages/core/src/__tests__/recovery-manager.test.ts index 8e092c41c..80647dc29 100644 --- a/packages/core/src/__tests__/recovery-manager.test.ts +++ b/packages/core/src/__tests__/recovery-manager.test.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { randomUUID } from "node:crypto"; -import { runRecovery } from "../recovery/manager.js"; +import { recoverSessionById, runRecovery } from "../recovery/manager.js"; import { recordActivityEvent } from "../activity-events.js"; import { getProjectDir, getProjectSessionsDir } from "../paths.js"; import { @@ -188,6 +188,55 @@ describe("runRecovery activity events", () => { ); }); + it("emits recovery.session_failed when single-session recovery fails", async () => { + vi.spyOn(validatorModule, "validateSession").mockImplementation(async (scanned) => + makeAssessment(scanned.sessionId), + ); + + const failedResult: RecoveryResult = { + success: false, + sessionId: "app-2", + action: "recover", + error: "agent process missing", + }; + + vi.spyOn(actionsModule, "executeAction").mockResolvedValueOnce(failedResult); + + const config = makeConfig(rootDir); + const registry = makeRegistry(); + + const result = await recoverSessionById("app-2", { + config, + registry, + recoveryConfig: { + ...DEFAULT_RECOVERY_CONFIG, + logPath: join(rootDir, "recovery.log"), + }, + }); + + expect(result).toEqual(failedResult); + + const emitCalls = vi + .mocked(recordActivityEvent) + .mock.calls.map((c) => c[0]) + .filter((e) => e.kind === "recovery.session_failed"); + + expect(emitCalls).toHaveLength(1); + expect(emitCalls[0]).toEqual( + expect.objectContaining({ + sessionId: "app-2", + projectId: PROJECT_ID, + source: "recovery", + kind: "recovery.session_failed", + level: "error", + data: expect.objectContaining({ + action: "recover", + errorMessage: "agent process missing", + }), + }), + ); + }); + it("does not emit recovery.session_failed when every session recovers cleanly", async () => { vi.spyOn(validatorModule, "validateSession").mockImplementation(async (scanned) => makeAssessment(scanned.sessionId), diff --git a/packages/core/src/metadata.ts b/packages/core/src/metadata.ts index f3fa958a5..1d1bef8e4 100644 --- a/packages/core/src/metadata.ts +++ b/packages/core/src/metadata.ts @@ -372,7 +372,7 @@ export function mutateMetadata( // Truncate the bad-JSON sample to 200 chars (B11 invariant — full file // could be 16KB+ and would be dropped by the sanitizer cap). const contentSample = - content.length > 200 ? `${content.slice(0, 200)}` : content; + content.length > 200 ? content.slice(0, 200) : content; // dataDir is `.../projects/{projectId}/sessions`; recover projectId for filtering. const inferredProjectId = basename(dirname(dataDir)); const summary = renamed diff --git a/packages/core/src/recovery/manager.ts b/packages/core/src/recovery/manager.ts index 3666adea6..330922e7f 100644 --- a/packages/core/src/recovery/manager.ts +++ b/packages/core/src/recovery/manager.ts @@ -107,29 +107,11 @@ export async function runRecovery(options: RecoveryManagerOptions): Promise