#!/bin/bash set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" run_node_test <<'JS' const fs = require('fs') const notifications = requireFromRoot('shell/plugins/notifications/NotificationLogic.js') assert(notifications.isChromiumDerived('Brave Browser', ''), 'notifications detect chromium-derived apps by name') assert(notifications.isChromiumDerived('', 'microsoft-edge'), 'notifications detect chromium-derived apps by icon') assert(!notifications.isChromiumDerived('Slack', ''), 'notifications do not treat unrelated apps as chromium-derived') assertEqual( notifications.sanitizeBody('Hello', 'Slack', ''), 'Hello', 'notifications strip inline image tags' ) assertEqual( notifications.sanitizeBody('example.com Message body', 'Chromium', ''), 'Message body', 'notifications strip chromium leading origin links' ) assertEqual( notifications.sanitizeBody('https://example.com/path Message body', 'Chromium', ''), 'Message body', 'notifications strip chromium leading origin text' ) assertEqual( notifications.sanitizeBody('https://example.com/path Message body', 'Slack', ''), 'https://example.com/path Message body', 'notifications keep non-browser leading origin text' ) assert(notifications.summaryStartsWithGlyph('󰂚 Silenced'), 'notifications detect glyph-prefixed summaries') assert(!notifications.summaryStartsWithGlyph('Normal summary'), 'notifications ignore normal summaries as glyph-prefixed') assert(notifications.shouldRenderCompactGlyph('K', '', true), 'notifications render glyph-only single-line toasts compactly') assert(!notifications.shouldRenderCompactGlyph('K', '', false), 'notifications give glyph hints with bodies the large icon slot') assert(!notifications.shouldRenderCompactGlyph('K', 'file:///tmp/image.png', true), 'notifications keep image-backed glyph hints in the icon slot') assert(notifications.shouldBypassDnd({ appName: 'omarchy-action', urgency: 1 }, 2), 'omarchy action toasts bypass DND') assert(notifications.shouldBypassDnd({ appName: 'notify-send', urgency: 2 }, 2), 'critical notify-send bypasses DND') assert(!notifications.shouldBypassDnd({ appName: 'notify-send', urgency: 1 }, 2), 'normal notify-send does not bypass DND') assert(!notifications.shouldBypassDnd({ appName: 'Slack', urgency: 2 }, 2), 'critical app notifications do not bypass DND') assert(!notifications.shouldBypassDnd({ appName: 'omarchy-menu-keybindings', urgency: 1 }, 2), 'omarchy command app names do not bypass DND') assert(!notifications.isEphemeralApp('omarchy-menu-keybindings'), 'notifications treat omarchy command app names as normal apps') assertDeepEqual( notifications.popupPlacement('top', 32, 6), { anchors: { top: true, bottom: false, left: false, right: true }, margins: { top: 32, bottom: 6, left: 6, right: 6 } }, 'notifications clear a top bar while staying anchored top-right' ) assertDeepEqual( notifications.popupPlacement('right', 32, 6), { anchors: { top: true, bottom: false, left: false, right: true }, margins: { top: 6, bottom: 6, left: 6, right: 32 } }, 'notifications clear a right bar while staying anchored top-right' ) assertDeepEqual( notifications.popupPlacement('bottom', 32, 6), { anchors: { top: true, bottom: false, left: false, right: true }, margins: { top: 6, bottom: 6, left: 6, right: 6 } }, 'notifications ignore a bottom bar for popup placement' ) assertDeepEqual( notifications.popupPlacement('left', 32, 6), { anchors: { top: true, bottom: false, left: false, right: true }, margins: { top: 6, bottom: 6, left: 6, right: 6 } }, 'notifications ignore a left bar for popup placement' ) const notification = { id: 12, appName: 'Mail', appIcon: 'mail', summary: 42, body: 'Body', image: 'file:///tmp/mail.png', hints: { 'omarchy-glyph': '!' }, urgency: 1, expireTimeout: 1.5 } const snapshot = notifications.snapshotOf(notification, 12345) assertDeepEqual( { id: snapshot.id, originalId: snapshot.originalId, app: snapshot.app, appIcon: snapshot.appIcon, summary: snapshot.summary, body: snapshot.body, image: snapshot.image, glyph: snapshot.glyph, urgency: snapshot.urgency, expireTimeout: snapshot.expireTimeout, timestamp: snapshot.timestamp }, { id: 12, originalId: 12, app: 'Mail', appIcon: 'mail', summary: '42', body: 'Body', image: 'file:///tmp/mail.png', glyph: '!', urgency: 1, expireTimeout: 1.5, timestamp: 12345 }, 'notifications create stable snapshots' ) // An in-place update keeps the popup's identity — the file name it was // persisted under — and takes everything the card draws from the new content. const replacement = notifications.replacementSnapshot( { id: 12, appName: 'Slack', summary: 'Thread v2', body: 'message 2', image: 'file:///tmp/new.png', hints: { 'omarchy-glyph': '!' }, urgency: 2, expireTimeout: 4000 }, 12, 12345 ) assertDeepEqual( { id: replacement.id, originalId: replacement.originalId, timestamp: replacement.timestamp, summary: replacement.summary, body: replacement.body, image: replacement.image, glyph: replacement.glyph, urgency: replacement.urgency, expireTimeout: replacement.expireTimeout }, { id: 12, originalId: 12, timestamp: 12345, summary: 'Thread v2', body: 'message 2', image: 'file:///tmp/new.png', glyph: '!', urgency: 2, expireTimeout: 4000 }, 'notifications take updated content without moving the popup it replaces' ) assertEqual( notifications.popupFileName(replacement), '12345-12.json', 'notifications keep the persisted file name across an in-place update' ) assert( !notifications.popupRowChanged(replacement, replacement), 'notifications skip a refresh that matches the row it would write' ) assert( notifications.popupRowChanged(replacement, Object.assign({}, replacement, { body: 'message 3' })), 'notifications refresh a row whose content moved on' ) assert( !notifications.popupRowChanged(replacement, Object.assign({}, replacement, { timestamp: 999 })), 'notifications ignore identity fields when deciding whether a refresh has work' ) const settings = notifications.parseSettings(JSON.stringify({ version: 3, dnd: true })) assertEqual(settings.dnd, true, 'notifications parse the persisted DND state') assertEqual(settings.legacy, false, 'notifications do not flag a current settings file as legacy') assertEqual(notifications.parseSettings('').dnd, null, 'notifications leave DND unset without a settings file') assertEqual( notifications.parseSettings(JSON.stringify({ dnd: false, pending: [], past: [] })).legacy, true, 'notifications flag a settings file still carrying the retired history rows' ) assert(notifications.parseSettings('{').error, 'notifications flag invalid settings JSON') // History is the notification files moved into the history dir, read back // exactly like live popup files. const archived = [ notifications.serializePopup({ id: 1, originalId: 1, summary: 'oldest', timestamp: 100 }, 1), notifications.serializePopup({ id: 2, originalId: 2, summary: 'newest', timestamp: 900, expireTimeout: 30000, deadline: 5000 }, 1), notifications.serializePopup({ id: 3, originalId: 3, summary: 'middle', timestamp: 500 }, 1) ].join('\n') const historyReplay = notifications.historyRows(archived, [], 1, 2) assertDeepEqual( historyReplay.map(row => row.summary), ['newest', 'middle'], 'notifications replay the newest history rows up to the limit' ) assertEqual(historyReplay[0].expireTimeout, 0, 'notifications replay history rows with the standard toast lifetime') assertEqual('deadline' in historyReplay[0], false, 'notifications drop the restore deadline from replayed history rows') assertDeepEqual(notifications.historyRows('', [], 1, 10), [], 'notifications replay nothing from an empty history dir') // A toast still on screen is the newest notification there is, and its move // into the history dir races the read, so the replay takes it from memory. assertDeepEqual( notifications.historyRows(archived, [{ id: 4, originalId: 4, summary: 'on screen', timestamp: 1500 }], 1, 10) .map(row => row.summary), ['on screen', 'newest', 'middle', 'oldest'], 'notifications replay the toasts still on screen alongside the archived ones' ) assertDeepEqual( notifications.historyRows(archived, [{ id: 2, originalId: 2, summary: 'newest', timestamp: 900 }], 1, 10) .map(row => row.summary), ['newest', 'middle', 'oldest'], 'notifications replay a toast once when its archived file already landed' ) assertDeepEqual( notifications.historyRows('', [{ id: 4, originalId: 4, summary: 'on screen', timestamp: 1500 }], 1, 10) .map(row => row.summary), ['on screen'], 'notifications replay an on-screen toast even when nothing is archived yet' ) const popup = { id: 7, originalId: 7, app: 'Mail', appIcon: 'mail', summary: 'New message', body: 'Body', image: '', glyph: '', urgency: 2, expireTimeout: 2500, timestamp: 1000 } assertEqual(notifications.popupFileName(popup), '1000-7.json', 'notifications name popup files by timestamp and id') assertEqual( notifications.serializePopup(popup, 1).indexOf('\n'), -1, 'notifications serialize popups to a single line' ) assertEqual( notifications.popupEntry({ id: 1, timestamp: 5 }, 1).urgency, 1, 'notifications default popup urgency to normal' ) assertEqual( notifications.popupEntry({ id: 1, timestamp: 5, expireTimeout: 4000 }, 1).expireTimeout, 4000, 'notifications preserve popup expire timeouts unlike history rows' ) const popupFiles = notifications.parsePopupFiles( [ notifications.serializePopup({ id: 1, originalId: 1, summary: 'old-generation', urgency: 2, timestamp: 100 }, 1), notifications.serializePopup({ id: 1, originalId: 1, summary: 'new-generation', urgency: 1, timestamp: 300 }, 1), notifications.serializePopup({ id: 2, originalId: 2, summary: 'critical', urgency: 2, timestamp: 200 }, 1), '{ torn write' ].join('\n'), 1 ) assertDeepEqual( popupFiles.map(row => row.summary), ['new-generation', 'critical', 'old-generation'], 'notifications restore every persisted popup newest-first, never deduping ids across server generations' ) assertDeepEqual( notifications.parsePopupFiles('', 1), [], 'notifications restore nothing from an empty popup dir' ) assert(!notifications.popupExpired({ timestamp: 0 }, 0, 999999), 'critical popups never expire on restore') assert(!notifications.popupExpired({ timestamp: 1000 }, 8000, 5000), 'popups within their lifetime are restored') assert(notifications.popupExpired({ timestamp: 1000 }, 8000, 9000), 'popups past their lifetime are not restored') assert( !notifications.popupExpired({ timestamp: 1000, deadline: 20000 }, 8000, 15000), 'a restore-reset deadline outranks the original popup timestamp' ) assert( notifications.popupExpired({ timestamp: 1000, deadline: 20000 }, 8000, 20000), 'popups past their reset deadline are not restored' ) assertEqual( notifications.popupEntry(JSON.parse(notifications.serializePopup({ id: 1, originalId: 1, timestamp: 5, deadline: 9000 }, 1)), 1).deadline, 9000, 'notifications round-trip reset deadlines through popup files' ) assertEqual( 'deadline' in notifications.popupEntry({ id: 1, timestamp: 5 }, 1), false, 'notifications omit the deadline field until a restore sets it' ) // A click action carried as a command is the only kind that survives a shell // restart: a libnotify action leaves its sender waiting on an id from a server // generation that no longer exists. assertEqual( notifications.snapshotOf({ id: 3, hints: { 'omarchy-exec': 'omarchy-menu-keybindings' } }, 1).exec, 'omarchy-menu-keybindings', 'notifications capture the click command from the exec hint' ) assertEqual( notifications.snapshotOf({ id: 3, hints: { 'omarchy-glyph': '!' } }, 1).exec, '', 'notifications leave the click command empty without an exec hint' ) assertEqual( notifications.popupEntry( JSON.parse(notifications.serializePopup({ id: 1, originalId: 1, timestamp: 5, exec: "mpv '/tmp/a b.mp4'" }, 1)), 1 ).exec, "mpv '/tmp/a b.mp4'", 'notifications round-trip the click command through popup files' ) assertEqual( notifications.popupEntry({ id: 1, originalId: 1, timestamp: 5 }, 1).exec, '', 'notifications restore an empty click command for popups without one' ) assertEqual( notifications.historyEntry({ id: 1, exec: 'xdg-open /tmp/received' }, 1).exec, 'xdg-open /tmp/received', 'notifications keep the click command on history rows' ) const serviceQml = fs.readFileSync(path.join(root, 'shell/plugins/notifications/Service.qml'), 'utf8') assert( /readonly property int historyLimit: 10/.test(serviceQml), 'notifications service keeps the last ten notifications in history' ) assert( /function showHistory\(\): string \{\s*return service\.showRecentHistory\(\)\s*\}/.test(serviceQml), 'notifications history IPC replays recent notifications' ) assert( /readonly property string popupStateDir: stateDir \+ "notifications\/"/.test(serviceQml), 'notifications service persists popups under the omarchy state dir' ) assert( /readonly property string historyDir: popupStateDir \+ "history\/"/.test(serviceQml), 'notifications service keeps history in a subdirectory of the popup state dir' ) assert( /if \(entry\) \{\s*\n\s*archivePopupFileFor\(entry\)[\s\S]{0,200}?popupModel\.remove\(index\)/.test(serviceQml), 'notifications service archives the popup file when a popup leaves the screen' ) assert( /mv -f \\"\$4\/\$3\\" \\"\$1\/\$3\\"/.test(serviceQml), 'notifications service archives by moving the popup file into the history dir' ) assert( /head -n \\"-\$2\\"/.test(serviceQml), 'notifications service trims history to the newest entries in the same job' ) assert( /if \(!isEphemeral\(notification\)\) writeHistoryFile\(snapshot\)/.test(serviceQml), 'notifications service records DND-silenced notifications straight into history' ) assert( /service\.replayCarryOver = liveRowsForReplay\(\)/.test(serviceQml), 'notifications service carries the toasts still on screen into the replay' ) assert( /watchForUpdates\(notification, snapshot\)/.test(serviceQml), 'notifications service watches a shown notification for in-place updates' ) assert( /if \(signal && typeof signal\.connect === "function"\) signal\.connect\(refresh\)/.test(serviceQml), 'notifications service refreshes the popup from every property the card draws' ) assert( /popupModel\.setProperty\(i, roles\[r\], updated\[roles\[r\]\]\)[\s\S]{0,600}?persistPopupFile\(updated\)/.test(serviceQml), 'notifications service rewrites both the row and its file when a notification is updated in place' ) assert( /if \(!NotificationLogic\.popupRowChanged\(row, updated\)\) return/.test(serviceQml), 'notifications service leaves the row and its file alone when a refresh finds nothing changed' ) assert( /popupModel\.insert\(0, snapshot\)[\s\S]{0,300}?service\.refreshPopup\(notification, snapshot\.originalId, snapshot\.timestamp\)/.test(serviceQml), 'notifications service catches up on an update that beat the deferred row insert' ) assert( /function showRecentHistory\(\)[\s\S]{0,300}?enqueueHistoryRead\(\)/.test(serviceQml), 'notifications service reads history from its place in the file queue' ) assert( /if \(job\.read\) \{\s*\n\s*startHistoryRead\(\)/.test(serviceQml), 'notifications service runs the queued read when its turn comes' ) assert( /function runNextPopupFileJob\(\) \{\s*\n\s*if \(readHistoryProc\.running \|\| popupFileProc\.running\) return/.test(serviceQml), 'notifications service holds queued file work until a history read finishes' ) assert( /id: readHistoryProc[\s\S]{0,300}?onExited: service\.runNextPopupFileJob\(\)/.test(serviceQml), 'notifications service releases the file queue even when a history read comes back empty' ) assert( /onSummaryChanged: cardSlot\.remainingLifetime = 1\.0/.test(serviceQml), 'notifications service restarts the countdown when a toast is updated under it' ) assert( /awk 1 \\"\$1\\"\/\*\.json 2>\/dev\/null \|\| true", "--", historyDir/.test(serviceQml), 'notifications service replays history by reading the archived files' ) assert( /restorePopupsProc\.running = true/.test(serviceQml), 'notifications service restores persisted popups on startup' ) assert( /if \(isRestoredRow\(row\)\) continue/.test(serviceQml), 'notifications service protects restored popups from new-generation id collisions' ) assert( /var ref = !restored && originalId >= 0 \? liveRefs\[originalId\] : null/.test(serviceQml), 'notifications service never resolves a restored popup to a live server object' ) assert( /service\.restoredPopups\[NotificationLogic\.popupFileName\(rows\[i\]\)\] = true/.test(serviceQml), 'notifications service treats replayed history rows as restored, never as live notifications' ) assert( /popupFileName\(row\) !== keepFileName/.test(serviceQml), 'notifications service keeps a same-millisecond replacement popup file' ) assert( /awk 1 \\"\$1\\"\/\*\.json/.test(serviceQml), 'notifications service delimits every popup file during restore' ) assert( /var command = entry \? String\(entry\.exec \|\| ""\) : ""[\s\S]{0,300}?Util\.execDetached\(command\)/.test(serviceQml), 'notifications service runs the popup click command itself instead of a libnotify action' ) assert( /function clear\(\): string \{\s*service\.clearHistory\(\)/.test(serviceQml), 'notifications clear IPC forgets the recorded history' ) assert( !/pendingModel|pastModel/.test(serviceQml), 'notifications service keeps no in-memory history models' ) JS