fix(notifications): persist canonicalized config on load — legacy keys no longer stale on disk (DC-097) [glm-grade=B]
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s

_loadConfig canonicalized legacy spellings in memory only (DC-092); the
on-disk notifications.json kept email user/pass, camelCase event keys and
string secure until the next explicit UI save — i.e. forever on installs
that never open the settings page.

- _persistCanonicalForm(): after the defaults merge, re-serialize and write
  back only when the bytes differ; idempotent on subsequent loads.
- Best-effort: write failures (read-only mount, EACCES) warn and continue —
  the in-memory config is already correct; constructor never throws.
- No secrets in new log lines; JSON.stringify(this.config) same as saveConfig.

Judge notes (non-blocking, GLM-5.3 cold read): unknown top-level keys are
now dropped from disk at boot (pre-existing merge-drop semantics, previously
deferred to next UI save); write is non-atomic, matching saveConfig.
Verdict: urn:ump:rchawiu427idev5mrettlxkyw2rcnwqpegiolqmzbc5ygc277u5q

Tests: +5 (__tests__/notification-config-writeback-dc097.test.js) —
canonical rewrite, idempotence, clean-file-untouched, EACCES no-throw,
fresh-install no-write. Full suite 119 suites / 2740 tests green.
This commit is contained in:
Hermes
2026-08-22 21:52:19 -07:00
parent bb59595d6d
commit 3ccd00d1a1
2 changed files with 186 additions and 1 deletions
@@ -98,15 +98,41 @@ class NotificationManager extends EventEmitter {
_loadConfig() {
try {
if (fs.existsSync(this.NOTIFICATIONS_FILE)) {
const data = JSON.parse(fs.readFileSync(this.NOTIFICATIONS_FILE, 'utf8'));
const raw = fs.readFileSync(this.NOTIFICATIONS_FILE, 'utf8');
const data = JSON.parse(raw);
this._canonicalizeLegacyKeys(data);
this.config = this._mergeConfig(DEFAULT_CONFIG, data);
this._persistCanonicalForm(raw);
}
} catch (error) {
this.log.error('notification', error, null, { note: 'Failed to load config' });
}
}
/**
* DC-097: _canonicalizeLegacyKeys only fixed the file in memory — the
* on-disk file kept its legacy spellings (email user/pass, camelCase
* event keys, string `secure`) until the next explicit UI save, so any
* pre-DC-092 file stayed stale forever on installs that never touch the
* settings page. After the defaults merge, persist the canonical form
* whenever it differs from what is on disk. Best-effort: the config is
* already correct in memory, so a write failure (read-only mount, EACCES)
* must never block startup — warn and continue. Idempotent: once written,
* the re-serialized form matches the file byte-for-byte and no further
* writes happen on subsequent loads.
*/
_persistCanonicalForm(rawFileContents) {
try {
const canonical = JSON.stringify(this.config, null, 2);
if (canonical !== rawFileContents) {
fs.writeFileSync(this.NOTIFICATIONS_FILE, canonical);
this.log.info?.('notification', 'Notification config canonicalized on disk (legacy keys normalized)', {});
}
} catch (writeError) {
this.log.warn?.('notification', 'Failed to persist canonicalized notification config; continuing with in-memory config', { error: writeError?.message || String(writeError) });
}
}
/**
* DC-092: configs saved by older clients may contain the legacy spellings
* the old POST /config merged verbatim — email.user/email.pass instead of