From b600503f42c280fff8837fa5fbcbc8d9e6aa62b9 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 20 May 2026 14:37:57 +0000 Subject: [PATCH 1/4] fix(code): coalesce writeLocalLogs per taskRunId to stop main-thread storm Extract local NDJSON cache handling into a singleton LocalLogsService and single-flight writes per taskRunId with latest-wins coalescing. If a write is already in flight when another arrives for the same run, the new content replaces any queued content rather than spawning a parallel fs.promises.writeFile. When the renderer's gap-reconcile loop fires on every SSE snapshot (which happens whenever parseLogContent silently drops corrupted lines and the processedLineCount never catches up to the server's expectedCount), the old fire-and-forget writeLocalLogs would pile fs.promises.writeFile continuations onto the main thread, producing the FileHandle::CloseReq::Resolve hang signature we saw at app launch. Generated-By: PostHog Code Task-Id: 0270afde-6140-4867-bd57-52ae8a767a4d --- apps/code/src/main/di/container.ts | 2 + apps/code/src/main/di/tokens.ts | 1 + .../main/services/local-logs/service.test.ts | 209 ++++++++++++++++++ .../src/main/services/local-logs/service.ts | 98 ++++++++ apps/code/src/main/trpc/routers/logs.ts | 45 +--- 5 files changed, 319 insertions(+), 36 deletions(-) create mode 100644 apps/code/src/main/services/local-logs/service.test.ts create mode 100644 apps/code/src/main/services/local-logs/service.ts diff --git a/apps/code/src/main/di/container.ts b/apps/code/src/main/di/container.ts index b5b0d0461..5d6a8d508 100644 --- a/apps/code/src/main/di/container.ts +++ b/apps/code/src/main/di/container.ts @@ -49,6 +49,7 @@ import { HandoffService } from "../services/handoff/service"; import { InboxLinkService } from "../services/inbox-link/service"; import { LinearIntegrationService } from "../services/linear-integration/service"; import { LlmGatewayService } from "../services/llm-gateway/service"; +import { LocalLogsService } from "../services/local-logs/service"; import { McpAppsService } from "../services/mcp-apps/service"; import { McpCallbackService } from "../services/mcp-callback/service"; import { McpProxyService } from "../services/mcp-proxy/service"; @@ -134,6 +135,7 @@ container.bind(MAIN_TOKENS.HandoffService).to(HandoffService); container .bind(MAIN_TOKENS.LinearIntegrationService) .to(LinearIntegrationService); +container.bind(MAIN_TOKENS.LocalLogsService).to(LocalLogsService); container.bind(MAIN_TOKENS.McpCallbackService).to(McpCallbackService); container.bind(MAIN_TOKENS.NotificationService).to(NotificationService); container.bind(MAIN_TOKENS.OAuthService).to(OAuthService); diff --git a/apps/code/src/main/di/tokens.ts b/apps/code/src/main/di/tokens.ts index 92d9a1287..aeade0e77 100644 --- a/apps/code/src/main/di/tokens.ts +++ b/apps/code/src/main/di/tokens.ts @@ -64,6 +64,7 @@ export const MAIN_TOKENS = Object.freeze({ GitHubIntegrationService: Symbol.for("Main.GitHubIntegrationService"), LinearIntegrationService: Symbol.for("Main.LinearIntegrationService"), SlackIntegrationService: Symbol.for("Main.SlackIntegrationService"), + LocalLogsService: Symbol.for("Main.LocalLogsService"), DeepLinkService: Symbol.for("Main.DeepLinkService"), NotificationService: Symbol.for("Main.NotificationService"), McpCallbackService: Symbol.for("Main.McpCallbackService"), diff --git a/apps/code/src/main/services/local-logs/service.test.ts b/apps/code/src/main/services/local-logs/service.test.ts new file mode 100644 index 000000000..3c40fbc61 --- /dev/null +++ b/apps/code/src/main/services/local-logs/service.test.ts @@ -0,0 +1,209 @@ +import os from "node:os"; +import path from "node:path"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { mockMkdir, mockWriteFile, mockReadFile } = vi.hoisted(() => ({ + mockMkdir: vi.fn(), + mockWriteFile: vi.fn(), + mockReadFile: vi.fn(), +})); + +vi.mock("node:fs", () => ({ + default: { + promises: { + mkdir: mockMkdir, + writeFile: mockWriteFile, + readFile: mockReadFile, + }, + }, +})); + +vi.mock("../../utils/logger.js", () => ({ + logger: { + scope: () => ({ + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + }), + }, +})); + +import { LocalLogsService } from "./service"; + +const RUN_ID = "run-abc"; +const expectedPath = path.join( + os.homedir(), + ".posthog-code", + "sessions", + RUN_ID, + "logs.ndjson", +); + +function deferred(): { + promise: Promise; + resolve: (value: T) => void; + reject: (err: unknown) => void; +} { + let resolve!: (value: T) => void; + let reject!: (err: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + +async function flushMicrotasks(): Promise { + for (let i = 0; i < 5; i++) await Promise.resolve(); +} + +describe("LocalLogsService", () => { + beforeEach(() => { + mockMkdir.mockReset().mockResolvedValue(undefined); + mockWriteFile.mockReset().mockResolvedValue(undefined); + mockReadFile.mockReset(); + }); + + describe("readLocalLogs", () => { + it("returns file contents", async () => { + mockReadFile.mockResolvedValue("hello"); + const service = new LocalLogsService(); + await expect(service.readLocalLogs(RUN_ID)).resolves.toBe("hello"); + expect(mockReadFile).toHaveBeenCalledWith(expectedPath, "utf-8"); + }); + + it("returns null when the file is missing", async () => { + const err = Object.assign(new Error("nope"), { code: "ENOENT" }); + mockReadFile.mockRejectedValue(err); + const service = new LocalLogsService(); + await expect(service.readLocalLogs(RUN_ID)).resolves.toBeNull(); + }); + + it("returns null on other read errors", async () => { + mockReadFile.mockRejectedValue(new Error("boom")); + const service = new LocalLogsService(); + await expect(service.readLocalLogs(RUN_ID)).resolves.toBeNull(); + }); + }); + + describe("writeLocalLogs", () => { + it("writes content to the run's NDJSON path", async () => { + const service = new LocalLogsService(); + await service.writeLocalLogs(RUN_ID, "line1\n"); + expect(mockMkdir).toHaveBeenCalledWith(path.dirname(expectedPath), { + recursive: true, + }); + expect(mockWriteFile).toHaveBeenCalledWith( + expectedPath, + "line1\n", + "utf-8", + ); + }); + + it("collapses many concurrent writes to one in-flight + one queued", async () => { + const firstWrite = deferred(); + mockWriteFile.mockImplementationOnce(() => firstWrite.promise); + + const service = new LocalLogsService(); + + const a = service.writeLocalLogs(RUN_ID, "A"); + const b = service.writeLocalLogs(RUN_ID, "B"); + const c = service.writeLocalLogs(RUN_ID, "C"); + const d = service.writeLocalLogs(RUN_ID, "D"); + + await flushMicrotasks(); + expect(mockWriteFile).toHaveBeenCalledTimes(1); + expect(mockWriteFile).toHaveBeenCalledWith(expectedPath, "A", "utf-8"); + + firstWrite.resolve(); + await Promise.all([a, b, c, d]); + + expect(mockWriteFile).toHaveBeenCalledTimes(2); + expect(mockWriteFile).toHaveBeenNthCalledWith( + 2, + expectedPath, + "D", + "utf-8", + ); + }); + + it("all coalesced callers see resolution when drain completes", async () => { + const firstWrite = deferred(); + mockWriteFile.mockImplementationOnce(() => firstWrite.promise); + + const service = new LocalLogsService(); + const a = service.writeLocalLogs(RUN_ID, "A"); + const b = service.writeLocalLogs(RUN_ID, "B"); + + let aResolved = false; + let bResolved = false; + void a.then(() => { + aResolved = true; + }); + void b.then(() => { + bResolved = true; + }); + + await Promise.resolve(); + expect(aResolved).toBe(false); + expect(bResolved).toBe(false); + + firstWrite.resolve(); + await Promise.all([a, b]); + expect(aResolved).toBe(true); + expect(bResolved).toBe(true); + }); + + it("keeps writes for different taskRunIds independent", async () => { + const writeA = deferred(); + const writeB = deferred(); + mockWriteFile + .mockImplementationOnce(() => writeA.promise) + .mockImplementationOnce(() => writeB.promise); + + const service = new LocalLogsService(); + const a = service.writeLocalLogs("run-a", "AAA"); + const b = service.writeLocalLogs("run-b", "BBB"); + + await flushMicrotasks(); + expect(mockWriteFile).toHaveBeenCalledTimes(2); + writeA.resolve(); + writeB.resolve(); + await Promise.all([a, b]); + }); + + it("starts fresh after the queue drains", async () => { + const service = new LocalLogsService(); + await service.writeLocalLogs(RUN_ID, "first"); + await service.writeLocalLogs(RUN_ID, "second"); + expect(mockWriteFile).toHaveBeenCalledTimes(2); + expect(mockWriteFile).toHaveBeenNthCalledWith( + 2, + expectedPath, + "second", + "utf-8", + ); + }); + + it("continues draining queued content even if a write rejects", async () => { + const firstWrite = deferred(); + mockWriteFile.mockImplementationOnce(() => firstWrite.promise); + + const service = new LocalLogsService(); + const a = service.writeLocalLogs(RUN_ID, "A"); + const b = service.writeLocalLogs(RUN_ID, "B"); + + firstWrite.reject(new Error("disk full")); + await Promise.all([a, b]); + + expect(mockWriteFile).toHaveBeenCalledTimes(2); + expect(mockWriteFile).toHaveBeenNthCalledWith( + 2, + expectedPath, + "B", + "utf-8", + ); + }); + }); +}); diff --git a/apps/code/src/main/services/local-logs/service.ts b/apps/code/src/main/services/local-logs/service.ts new file mode 100644 index 000000000..64b8558f9 --- /dev/null +++ b/apps/code/src/main/services/local-logs/service.ts @@ -0,0 +1,98 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { injectable } from "inversify"; +import { logger } from "../../utils/logger"; + +const log = logger.scope("local-logs"); + +interface WriteState { + inFlight: Promise; + pending: string | undefined; +} + +/** + * Owns the per-run NDJSON cache at `~/.posthog-code/sessions/{taskRunId}/logs.ndjson`. + * + * `writeLocalLogs` is single-flight per `taskRunId` with latest-wins coalescing: + * if a write is already in flight when a new one arrives, the new content replaces + * any queued content rather than spawning a parallel `fs.promises.writeFile`. This + * prevents a storm of full-file overwrites when the renderer's gap-reconcile loop + * fires `writeLocalLogs` per SSE snapshot — that storm pegs the main thread on + * `FileHandle::CloseReq::Resolve` continuations and is what tipped a user's app + * into an 81-second hang on launch. + */ +@injectable() +export class LocalLogsService { + private writes = new Map(); + + async readLocalLogs(taskRunId: string): Promise { + const logPath = this.getLocalLogPath(taskRunId); + try { + return await fs.promises.readFile(logPath, "utf-8"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return null; + } + log.warn("Failed to read local logs:", error); + return null; + } + } + + writeLocalLogs(taskRunId: string, content: string): Promise { + const existing = this.writes.get(taskRunId); + if (existing) { + existing.pending = content; + return existing.inFlight; + } + + const entry: WriteState = { + inFlight: undefined as unknown as Promise, + pending: undefined, + }; + + entry.inFlight = this.drain(taskRunId, content, entry); + this.writes.set(taskRunId, entry); + return entry.inFlight; + } + + private async drain( + taskRunId: string, + initialContent: string, + entry: WriteState, + ): Promise { + let next: string | undefined = initialContent; + while (next !== undefined) { + const current = next; + next = undefined; + await this.doWrite(taskRunId, current); + if (entry.pending !== undefined) { + next = entry.pending; + entry.pending = undefined; + } + } + this.writes.delete(taskRunId); + } + + private async doWrite(taskRunId: string, content: string): Promise { + const logPath = this.getLocalLogPath(taskRunId); + const logDir = path.dirname(logPath); + try { + await fs.promises.mkdir(logDir, { recursive: true }); + await fs.promises.writeFile(logPath, content, "utf-8"); + } catch (error) { + log.warn("Failed to write local logs:", error); + } + } + + private getLocalLogPath(taskRunId: string): string { + return path.join( + os.homedir(), + ".posthog-code", + "sessions", + taskRunId, + "logs.ndjson", + ); + } +} diff --git a/apps/code/src/main/trpc/routers/logs.ts b/apps/code/src/main/trpc/routers/logs.ts index 23b578854..bd0e80a67 100644 --- a/apps/code/src/main/trpc/routers/logs.ts +++ b/apps/code/src/main/trpc/routers/logs.ts @@ -1,22 +1,14 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - import { z } from "zod"; +import { container } from "../../di/container"; +import { MAIN_TOKENS } from "../../di/tokens"; +import type { LocalLogsService } from "../../services/local-logs/service"; import { logger } from "../../utils/logger"; import { publicProcedure, router } from "../trpc"; const log = logger.scope("logsRouter"); -function getLocalLogPath(taskRunId: string): string { - return path.join( - os.homedir(), - ".posthog-code", - "sessions", - taskRunId, - "logs.ndjson", - ); -} +const getLocalLogsService = (): LocalLogsService => + container.get(MAIN_TOKENS.LocalLogsService); export const logsRouter = router({ fetchS3Logs: publicProcedure @@ -47,30 +39,11 @@ export const logsRouter = router({ readLocalLogs: publicProcedure .input(z.object({ taskRunId: z.string() })) - .query(async ({ input }) => { - const logPath = getLocalLogPath(input.taskRunId); - try { - return await fs.promises.readFile(logPath, "utf-8"); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") { - return null; - } - log.warn("Failed to read local logs:", error); - return null; - } - }), + .query(({ input }) => getLocalLogsService().readLocalLogs(input.taskRunId)), writeLocalLogs: publicProcedure .input(z.object({ taskRunId: z.string(), content: z.string() })) - .mutation(async ({ input }) => { - const logPath = getLocalLogPath(input.taskRunId); - const logDir = path.dirname(logPath); - - try { - await fs.promises.mkdir(logDir, { recursive: true }); - await fs.promises.writeFile(logPath, input.content, "utf-8"); - } catch (error) { - log.warn("Failed to write local logs:", error); - } - }), + .mutation(({ input }) => + getLocalLogsService().writeLocalLogs(input.taskRunId, input.content), + ), }); From cdc3304d63238ca11f10adefd7a4f8f4edbbef8b Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 21 May 2026 13:31:59 +0100 Subject: [PATCH 2/4] refactor(code): trim LocalLogsService docblock and dedup mkdir + same-content writes Tighten WriteState shape (drop the placeholder cast), skip mkdir after the first successful write per drain, skip writeFile when the coalesced content matches the last written payload, and use the DATA_DIR constant. Generated-By: PostHog Code Task-Id: 65171e71-4765-479c-b0c8-e4d0dd5c5bc7 --- .../main/services/local-logs/service.test.ts | 29 ++++++++ .../src/main/services/local-logs/service.ts | 69 +++++++++++-------- 2 files changed, 69 insertions(+), 29 deletions(-) diff --git a/apps/code/src/main/services/local-logs/service.test.ts b/apps/code/src/main/services/local-logs/service.test.ts index 3c40fbc61..0e21bc0cd 100644 --- a/apps/code/src/main/services/local-logs/service.test.ts +++ b/apps/code/src/main/services/local-logs/service.test.ts @@ -205,5 +205,34 @@ describe("LocalLogsService", () => { "utf-8", ); }); + + it("skips writeFile when coalesced content matches the last write", async () => { + const firstWrite = deferred(); + mockWriteFile.mockImplementationOnce(() => firstWrite.promise); + + const service = new LocalLogsService(); + const a = service.writeLocalLogs(RUN_ID, "SAME"); + const b = service.writeLocalLogs(RUN_ID, "SAME"); + + firstWrite.resolve(); + await Promise.all([a, b]); + + expect(mockWriteFile).toHaveBeenCalledTimes(1); + }); + + it("only mkdirs once per drain", async () => { + const firstWrite = deferred(); + mockWriteFile.mockImplementationOnce(() => firstWrite.promise); + + const service = new LocalLogsService(); + const a = service.writeLocalLogs(RUN_ID, "A"); + const b = service.writeLocalLogs(RUN_ID, "B"); + + firstWrite.resolve(); + await Promise.all([a, b]); + + expect(mockWriteFile).toHaveBeenCalledTimes(2); + expect(mockMkdir).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/apps/code/src/main/services/local-logs/service.ts b/apps/code/src/main/services/local-logs/service.ts index 64b8558f9..375196685 100644 --- a/apps/code/src/main/services/local-logs/service.ts +++ b/apps/code/src/main/services/local-logs/service.ts @@ -3,29 +3,28 @@ import os from "node:os"; import path from "node:path"; import { injectable } from "inversify"; +import { DATA_DIR } from "../../../shared/constants"; import { logger } from "../../utils/logger"; const log = logger.scope("local-logs"); interface WriteState { - inFlight: Promise; pending: string | undefined; + lastWritten: string | undefined; + dirReady: boolean; } /** * Owns the per-run NDJSON cache at `~/.posthog-code/sessions/{taskRunId}/logs.ndjson`. * * `writeLocalLogs` is single-flight per `taskRunId` with latest-wins coalescing: - * if a write is already in flight when a new one arrives, the new content replaces - * any queued content rather than spawning a parallel `fs.promises.writeFile`. This - * prevents a storm of full-file overwrites when the renderer's gap-reconcile loop - * fires `writeLocalLogs` per SSE snapshot — that storm pegs the main thread on - * `FileHandle::CloseReq::Resolve` continuations and is what tipped a user's app - * into an 81-second hang on launch. + * if a write is already in flight, the new content replaces any queued content + * rather than spawning a parallel `fs.promises.writeFile`. Identical consecutive + * payloads are skipped — gap-reconcile re-emits the same NDJSON on every snapshot. */ @injectable() export class LocalLogsService { - private writes = new Map(); + private writes = new Map }>(); async readLocalLogs(taskRunId: string): Promise { const logPath = this.getLocalLogPath(taskRunId); @@ -43,43 +42,55 @@ export class LocalLogsService { writeLocalLogs(taskRunId: string, content: string): Promise { const existing = this.writes.get(taskRunId); if (existing) { - existing.pending = content; + existing.state.pending = content; return existing.inFlight; } - const entry: WriteState = { - inFlight: undefined as unknown as Promise, + const state: WriteState = { pending: undefined, + lastWritten: undefined, + dirReady: false, }; - - entry.inFlight = this.drain(taskRunId, content, entry); - this.writes.set(taskRunId, entry); - return entry.inFlight; + const inFlight = this.drain(taskRunId, content, state); + this.writes.set(taskRunId, { state, inFlight }); + return inFlight; } private async drain( taskRunId: string, initialContent: string, - entry: WriteState, + state: WriteState, ): Promise { - let next: string | undefined = initialContent; - while (next !== undefined) { - const current = next; - next = undefined; - await this.doWrite(taskRunId, current); - if (entry.pending !== undefined) { - next = entry.pending; - entry.pending = undefined; + try { + let next: string | undefined = initialContent; + while (next !== undefined) { + const current = next; + next = undefined; + if (current !== state.lastWritten) { + await this.doWrite(taskRunId, current, state); + state.lastWritten = current; + } + if (state.pending !== undefined) { + next = state.pending; + state.pending = undefined; + } } + } finally { + this.writes.delete(taskRunId); } - this.writes.delete(taskRunId); } - private async doWrite(taskRunId: string, content: string): Promise { + private async doWrite( + taskRunId: string, + content: string, + state: WriteState, + ): Promise { const logPath = this.getLocalLogPath(taskRunId); - const logDir = path.dirname(logPath); try { - await fs.promises.mkdir(logDir, { recursive: true }); + if (!state.dirReady) { + await fs.promises.mkdir(path.dirname(logPath), { recursive: true }); + state.dirReady = true; + } await fs.promises.writeFile(logPath, content, "utf-8"); } catch (error) { log.warn("Failed to write local logs:", error); @@ -89,7 +100,7 @@ export class LocalLogsService { private getLocalLogPath(taskRunId: string): string { return path.join( os.homedir(), - ".posthog-code", + DATA_DIR, "sessions", taskRunId, "logs.ndjson", From 307c2622d466b221cd3388a3ad4117d1a3cfbbef Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 21 May 2026 14:39:06 +0100 Subject: [PATCH 3/4] chore(code): dedupe duplicate INBOX_VIEWED analytics declarations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The duplicate `INBOX_VIEWED` entries in `ANALYTICS_EVENTS` (one before PR #2228 added it as a typed event, and the second inside the proper Inbox section) were tripping `tsc`. Remove the legacy untyped placeholder plus the no-props `track(INBOX_VIEWED)` call in `navigateToInbox` — the properly-typed event already fires from `InboxSignalsTab` with the full report counts. Also trim the LocalLogsService docblock to focus on the non-obvious single-flight rationale. Generated-By: PostHog Code Task-Id: 65171e71-4765-479c-b0c8-e4d0dd5c5bc7 --- apps/code/src/main/services/local-logs/service.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/code/src/main/services/local-logs/service.ts b/apps/code/src/main/services/local-logs/service.ts index 375196685..4c4281bf2 100644 --- a/apps/code/src/main/services/local-logs/service.ts +++ b/apps/code/src/main/services/local-logs/service.ts @@ -15,16 +15,15 @@ interface WriteState { } /** - * Owns the per-run NDJSON cache at `~/.posthog-code/sessions/{taskRunId}/logs.ndjson`. - * - * `writeLocalLogs` is single-flight per `taskRunId` with latest-wins coalescing: - * if a write is already in flight, the new content replaces any queued content - * rather than spawning a parallel `fs.promises.writeFile`. Identical consecutive - * payloads are skipped — gap-reconcile re-emits the same NDJSON on every snapshot. + * Single-flight per `taskRunId` with latest-wins coalescing. Prevents the + * gap-reconcile loop from spawning parallel writeFile of the same NDJSON. */ @injectable() export class LocalLogsService { - private writes = new Map }>(); + private writes = new Map< + string, + { state: WriteState; inFlight: Promise } + >(); async readLocalLogs(taskRunId: string): Promise { const logPath = this.getLocalLogPath(taskRunId); From d7b92a8422832ed3f37c744f87218b6473e66cda Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Thu, 21 May 2026 15:33:27 +0100 Subject: [PATCH 4/4] test(code): parameterize readLocalLogs null-return cases Generated-By: PostHog Code Task-Id: 65171e71-4765-479c-b0c8-e4d0dd5c5bc7 --- .../src/main/services/local-logs/service.test.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/code/src/main/services/local-logs/service.test.ts b/apps/code/src/main/services/local-logs/service.test.ts index 0e21bc0cd..80b735e73 100644 --- a/apps/code/src/main/services/local-logs/service.test.ts +++ b/apps/code/src/main/services/local-logs/service.test.ts @@ -73,18 +73,14 @@ describe("LocalLogsService", () => { expect(mockReadFile).toHaveBeenCalledWith(expectedPath, "utf-8"); }); - it("returns null when the file is missing", async () => { - const err = Object.assign(new Error("nope"), { code: "ENOENT" }); + it.each([ + ["file is missing", Object.assign(new Error("nope"), { code: "ENOENT" })], + ["other read errors", new Error("boom")], + ])("returns null when %s", async (_label, err) => { mockReadFile.mockRejectedValue(err); const service = new LocalLogsService(); await expect(service.readLocalLogs(RUN_ID)).resolves.toBeNull(); }); - - it("returns null on other read errors", async () => { - mockReadFile.mockRejectedValue(new Error("boom")); - const service = new LocalLogsService(); - await expect(service.readLocalLogs(RUN_ID)).resolves.toBeNull(); - }); }); describe("writeLocalLogs", () => {