From 9d1e343e41f58bbbea10c7e2901b95c0142faa78 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 6 Apr 2026 17:44:34 -0500 Subject: [PATCH] test(gsd): add overlay backdrop and notification lock safety tests - Overlay layout: verify backdrop dims base lines, no dim without flag, overlay composites on top of dimmed background - Notification store: verify markAllRead and clearNotifications do not delete a foreign process's lock file --- .../src/__tests__/overlay-layout.test.ts | 65 +++++++++++++++++++ .../gsd/tests/notification-store.test.ts | 35 +++++++++- 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 packages/pi-tui/src/__tests__/overlay-layout.test.ts diff --git a/packages/pi-tui/src/__tests__/overlay-layout.test.ts b/packages/pi-tui/src/__tests__/overlay-layout.test.ts new file mode 100644 index 000000000..4f7d7817c --- /dev/null +++ b/packages/pi-tui/src/__tests__/overlay-layout.test.ts @@ -0,0 +1,65 @@ +// pi-tui — Overlay Layout Tests (backdrop dimming) + +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { compositeOverlays, type OverlayEntry } from "../overlay-layout.js"; + +function makeEntry( + lines: string[], + options?: OverlayEntry["options"], +): OverlayEntry { + return { + component: { render: () => lines }, + options, + hidden: false, + focusOrder: 1, + }; +} + +describe("compositeOverlays — backdrop", () => { + it("dims base lines when backdrop is true", () => { + const base = ["hello world", "second line"]; + const overlay = makeEntry(["OVERLAY"], { + width: 7, + anchor: "top-left", + backdrop: true, + }); + + const result = compositeOverlays(base, [overlay], 20, 20, 2); + + // All base lines in viewport should contain dim escape (\x1b[2m) + // The overlay line itself is composited on top, but underlying lines get dimmed + const dimmedLine = result.find((l) => l.includes("second line")); + assert.ok(dimmedLine, "should have a line containing 'second line'"); + assert.ok(dimmedLine.includes("\x1b[2m"), "base line should be dimmed"); + }); + + it("does not dim when backdrop is false/absent", () => { + const base = ["hello world", "second line"]; + const overlay = makeEntry(["OVERLAY"], { + width: 7, + anchor: "top-left", + }); + + const result = compositeOverlays(base, [overlay], 20, 20, 2); + + // Lines not covered by overlay should remain undimmed + const secondLine = result.find((l) => l.includes("second line")); + assert.ok(secondLine, "should have a line containing 'second line'"); + assert.ok(!secondLine.includes("\x1b[2m"), "base line should not be dimmed"); + }); + + it("overlay content renders on top of dimmed background", () => { + const base = ["aaaaaaaaaa"]; + const overlay = makeEntry(["XX"], { + width: 2, + anchor: "top-left", + backdrop: true, + }); + + const result = compositeOverlays(base, [overlay], 10, 10, 1); + + // The first line should contain the overlay text + assert.ok(result[0].includes("XX"), "overlay text should be composited"); + }); +}); diff --git a/src/resources/extensions/gsd/tests/notification-store.test.ts b/src/resources/extensions/gsd/tests/notification-store.test.ts index 2d8dd105a..8f13fb873 100644 --- a/src/resources/extensions/gsd/tests/notification-store.test.ts +++ b/src/resources/extensions/gsd/tests/notification-store.test.ts @@ -2,7 +2,7 @@ import { describe, test, beforeEach, afterEach } from "node:test"; import assert from "node:assert/strict"; -import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync } from "node:fs"; +import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; @@ -246,4 +246,37 @@ describe("notification-store", () => { assert.equal(getUnreadCount(), 0); assert.equal(getLineCount(), 0); }); + + test("markAllRead does not delete a foreign lock file", () => { + initNotificationStore(tmp); + appendNotification("msg1", "info"); + + // Simulate another process holding the lock + const lockPath = join(tmp, ".gsd", "notifications.lock"); + writeFileSync(lockPath, String(Date.now()), "utf-8"); + + // markAllRead should still work (best-effort) but not delete the foreign lock + markAllRead(); + + assert.ok(existsSync(lockPath), "foreign lock file should not be deleted"); + + // Clean up the lock so afterEach doesn't leave artifacts + rmSync(lockPath, { force: true }); + }); + + test("clearNotifications does not delete a foreign lock file", () => { + initNotificationStore(tmp); + appendNotification("msg1", "info"); + + // Simulate another process holding the lock + const lockPath = join(tmp, ".gsd", "notifications.lock"); + writeFileSync(lockPath, String(Date.now()), "utf-8"); + + // clearNotifications should still work but not delete the foreign lock + clearNotifications(); + + assert.ok(existsSync(lockPath), "foreign lock file should not be deleted"); + + rmSync(lockPath, { force: true }); + }); });