From bd8eb884c276e79a61e573463942ac121de6a78a Mon Sep 17 00:00:00 2001 From: Astro-Han Date: Sun, 1 Mar 2026 23:23:01 +0800 Subject: [PATCH] Fix P0 docs and expand core behavior tests --- README.md | 8 +++- test/plugin.test.js | 110 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8426383..ab0f565 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Privacy-friendly by design: no system notifications, no message content, no exte ```json { "$schema": "https://opencode.ai/config.json", - "plugin": ["opencode-bell@0.1.0"] + "plugin": ["opencode-bell@0.1.1"] } ``` @@ -32,6 +32,12 @@ The plugin triggers a terminal bell for these events: - `session.idle` - `session.error` +Behavior details: + +- Rings only when `process.stdout.isTTY` is true. +- Debounces repeated rings for the same key within 1200ms. +- Uses per-session keys when `event.properties.sessionID` is present. + ## Verify - Trigger a permission request (e.g., any tool that requires approval). diff --git a/test/plugin.test.js b/test/plugin.test.js index 849e98f..a541b6d 100644 --- a/test/plugin.test.js +++ b/test/plugin.test.js @@ -3,6 +3,47 @@ import assert from "node:assert/strict" import { OpencodeBellPlugin } from "../index.js" +const withStdoutStub = async ({ isTTY = true }, run) => { + const originalWrite = process.stdout.write + const originalDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY") + const writes = [] + + process.stdout.write = (chunk) => { + writes.push(chunk) + return true + } + Object.defineProperty(process.stdout, "isTTY", { + configurable: true, + value: isTTY + }) + + try { + await run(writes) + } finally { + process.stdout.write = originalWrite + if (originalDescriptor) { + Object.defineProperty(process.stdout, "isTTY", originalDescriptor) + } + } +} + +const withMockedNow = async (times, run) => { + const originalNow = Date.now + let index = 0 + + Date.now = () => { + const value = times[index] + index += 1 + return value + } + + try { + await run() + } finally { + Date.now = originalNow + } +} + test("does not throw when event.properties is missing", async () => { const plugin = await OpencodeBellPlugin() await assert.doesNotReject( @@ -11,3 +52,72 @@ test("does not throw when event.properties is missing", async () => { }) ) }) + +test("rings once for each supported event type", async () => { + const plugin = await OpencodeBellPlugin() + + await withStdoutStub({ isTTY: true }, async (writes) => { + await plugin.event({ event: { type: "permission.asked" } }) + await plugin.event({ event: { type: "question.asked" } }) + await plugin.event({ event: { type: "session.idle" } }) + await plugin.event({ event: { type: "session.error" } }) + + assert.equal(writes.length, 4) + assert.deepEqual(writes, ["\x07", "\x07", "\x07", "\x07"]) + }) +}) + +test("does not ring for unsupported event types", async () => { + const plugin = await OpencodeBellPlugin() + + await withStdoutStub({ isTTY: true }, async (writes) => { + await plugin.event({ event: { type: "session.started" } }) + assert.equal(writes.length, 0) + }) +}) + +test("does not ring when stdout is not a TTY", async () => { + const plugin = await OpencodeBellPlugin() + + await withStdoutStub({ isTTY: false }, async (writes) => { + await plugin.event({ event: { type: "session.idle" } }) + assert.equal(writes.length, 0) + }) +}) + +test("debounces repeated events within 1200ms for the same key", async () => { + const plugin = await OpencodeBellPlugin() + + await withStdoutStub({ isTTY: true }, async (writes) => { + await withMockedNow([2000, 2500, 3301], async () => { + await plugin.event({ event: { type: "permission.asked" } }) + await plugin.event({ event: { type: "permission.asked" } }) + await plugin.event({ event: { type: "permission.asked" } }) + }) + + assert.equal(writes.length, 2) + }) +}) + +test("uses session-specific debounce keys", async () => { + const plugin = await OpencodeBellPlugin() + + await withStdoutStub({ isTTY: true }, async (writes) => { + await withMockedNow([2000, 2100], async () => { + await plugin.event({ + event: { + type: "permission.asked", + properties: { sessionID: "A" } + } + }) + await plugin.event({ + event: { + type: "permission.asked", + properties: { sessionID: "B" } + } + }) + }) + + assert.equal(writes.length, 2) + }) +})