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
This commit is contained in:
Jeremy 2026-04-06 17:44:34 -05:00
parent d553455732
commit 9d1e343e41
2 changed files with 99 additions and 1 deletions

View file

@ -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");
});
});

View file

@ -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 });
});
});