@delorenj/claude-notifications
Version:
Delightful Notification for Claude Code
82 lines (67 loc) • 3.08 kB
JavaScript
;
const test = require("node:test");
const assert = require("node:assert/strict");
const path = require("node:path");
const {
MARKER_SOURCE,
makeMarker,
readJsonSafe,
upsertByMarker,
} = require("../../lib/adapters");
const ccAdapter = require("../../lib/adapters/claude-code");
const { createFakeFs } = require("../helpers/fake-fs");
const HOME = "/home/tester";
const MODERN = path.join(HOME, ".claude", "settings.json");
function makeDeps(files = {}) {
return {
fs: createFakeFs(files),
homedir: () => HOME,
which: async () => `${HOME}/.local/bin/claude`,
};
}
// --- Patch P6: makeMarker cannot be hijacked by caller-supplied source/version ---
test("makeMarker forces source/version regardless of caller extras", () => {
const m = makeMarker({ source: "evil", version: "9.9.9", matcher: "" });
assert.equal(m.source, MARKER_SOURCE);
assert.notEqual(m.version, "9.9.9");
assert.equal(m.matcher, "");
});
// --- Patch E1: marker version change alone does not trigger rewrite ---
test("upsertByMarker treats version-only diff as unchanged", () => {
const old = { source: MARKER_SOURCE, version: "1.0.0", matcher: "", hooks: [{ type: "command", command: "claude-notify" }] };
const fresh = { source: MARKER_SOURCE, version: "2.0.0", matcher: "", hooks: [{ type: "command", command: "claude-notify" }] };
const arr = [JSON.parse(JSON.stringify(old))];
const res = upsertByMarker(arr, fresh);
assert.equal(res.changed, false);
assert.equal(res.alreadyInstalled, true);
});
// --- Patch E3: BOM-prefixed JSON is still readable ---
test("readJsonSafe strips UTF-8 BOM before parsing", () => {
const deps = { fs: createFakeFs({ "/x.json": "\uFEFF{\"a\":1}" }) };
const parsed = readJsonSafe("/x.json", deps);
assert.deepEqual(parsed, { a: 1 });
});
// --- Patch E4: root-level array is rejected loudly, not silently mutated ---
test("readJsonSafe throws ENOTOBJECT on root-level array", () => {
const deps = { fs: createFakeFs({ "/x.json": "[]" }) };
assert.throws(() => readJsonSafe("/x.json", deps), { code: "ENOTOBJECT" });
});
test("readJsonSafe throws ENOTOBJECT on scalar root", () => {
const deps = { fs: createFakeFs({ "/x.json": "42" }) };
assert.throws(() => readJsonSafe("/x.json", deps), { code: "ENOTOBJECT" });
});
// --- Patch B7: modern settings.json wins over legacy config.json when both exist ---
test("claude-code prefers modern settings.json over legacy config.json", async () => {
const legacy = path.join(HOME, ".claude", "config.json");
const modern = MODERN;
const deps = makeDeps({
[modern]: JSON.stringify({}, null, 2),
[legacy]: JSON.stringify({ hooks: { Stop: [] } }, null, 2),
});
await ccAdapter.install({ notifyCommand: "claude-notify", deps });
const written = JSON.parse(deps.fs.readFileSync(modern));
assert.ok(written.hooks.Notification.some((e) => e.source === MARKER_SOURCE));
// Legacy config should be untouched.
const legacyAfter = JSON.parse(deps.fs.readFileSync(legacy));
assert.deepEqual(legacyAfter, { hooks: { Stop: [] } });
});