From 3df27382800d80a3d38d521fa61e2fd4649d106e Mon Sep 17 00:00:00 2001 From: Ljy-0827 Date: Wed, 19 Aug 2026 14:04:33 +0800 Subject: [PATCH] fix(vom): fix CI --- .../src/observation.ts | 9 ++--- .../tests/observation.test.ts | 36 ++++++++++++++++--- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/dsh-plugin-browserskill/src/observation.ts b/packages/dsh-plugin-browserskill/src/observation.ts index 78991b6..e38412c 100644 --- a/packages/dsh-plugin-browserskill/src/observation.ts +++ b/packages/dsh-plugin-browserskill/src/observation.ts @@ -12,7 +12,7 @@ * events, and never move the registry's current pointer. */ -import { createHash } from "node:crypto"; +import { createHash, randomUUID } from "node:crypto"; import { readFile, unlink } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -78,6 +78,7 @@ const FAILURE_BACKOFF_THRESHOLD = 3; const FRAME_RING_SIZE = 2; export class ObservationService { + private readonly scratchNamespace = randomUUID(); private readonly observations = new Map(); private readonly listeners = new Set<(event: ObservationEvent) => void>(); private readonly captureTimers = new Map(); @@ -304,9 +305,9 @@ export class ObservationService { if (current === undefined || current.dead === true) return; if (this.captureInFlight.has(sessionId)) return; this.captureInFlight.add(sessionId); - // One fixed scratch path per session (overwritten each frame) and always - // unlinked after the bytes are read — /tmp must not grow without bound. - const outPath = join(tmpdir(), `bsk-obs-${sessionId}.png`); + // Stable within this service, isolated from captures owned by other instances. + const sessionKey = createHash("sha256").update(sessionId).digest("hex").slice(0, 16); + const outPath = join(tmpdir(), `bsk-obs-${this.scratchNamespace}-${sessionKey}.png`); let writtenPath = outPath; try { const result = await this.deps.queue.run(sessionId, () => diff --git a/packages/dsh-plugin-browserskill/tests/observation.test.ts b/packages/dsh-plugin-browserskill/tests/observation.test.ts index e54a15d..7acd93d 100644 --- a/packages/dsh-plugin-browserskill/tests/observation.test.ts +++ b/packages/dsh-plugin-browserskill/tests/observation.test.ts @@ -5,7 +5,7 @@ import { existsSync, mkdtempSync, writeFileSync } from "node:fs"; import type { IncomingMessage, ServerResponse } from "node:http"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { basename, dirname } from "node:path"; import { describe, expect, it } from "vitest"; import { actionForLabel, @@ -548,8 +548,12 @@ describe("scratch files and thumbnail references", () => { ); const firstOut = runner.calls.filter((c) => c.args[0] === "screenshot").at(-1)?.args; const pathOf = (args: string[]) => args[args.indexOf("--out") + 1]; - expect(pathOf(firstOut ?? [])).toBe(join(tmpdir(), "bsk-obs-s1.png")); - expect(existsSync(pathOf(firstOut ?? []))).toBe(false); + const firstPath = pathOf(firstOut ?? []); + expect(dirname(firstPath)).toBe(tmpdir()); + expect(basename(firstPath)).toMatch( + /^bsk-obs-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-[0-9a-f]{16}\.png$/, + ); + expect(existsSync(firstPath)).toBe(false); // second frame: same path, deleted again service.endAction("s1"); scheduler.runNext(); @@ -559,11 +563,33 @@ describe("scratch files and thumbnail references", () => { scheduler.pending().length === 1, ); const secondOut = runner.calls.filter((c) => c.args[0] === "screenshot").at(-1)?.args; - expect(pathOf(secondOut ?? [])).toBe(join(tmpdir(), "bsk-obs-s1.png")); - await waitFor(() => !existsSync(pathOf(secondOut ?? []))); + const secondPath = pathOf(secondOut ?? []); + expect(secondPath).toBe(firstPath); + await waitFor(() => !existsSync(secondPath)); service.dispose(); }); + it("isolates scratch paths between service instances", async () => { + const first = setup({ scheduler: fakeScheduler(), runner: fakeRunner() }); + const second = setup({ scheduler: fakeScheduler(), runner: fakeRunner() }); + first.service.addSession("s1"); + second.service.addSession("s1"); + first.scheduler.runNext(); + second.scheduler.runNext(); + await waitFor( + () => + first.service.getState()[0]?.thumbnailAttachmentId !== undefined && + second.service.getState()[0]?.thumbnailAttachmentId !== undefined, + ); + const pathOf = (runner: FakeRunner) => { + const args = runner.calls.find((call) => call.args[0] === "screenshot")?.args ?? []; + return args[args.indexOf("--out") + 1]; + }; + expect(pathOf(first.runner)).not.toBe(pathOf(second.runner)); + first.service.dispose(); + second.service.dispose(); + }); + it("keeps the last two distinct frames and evicts the third", async () => { let n = 0; const runner = fakeRunner({