/** * Additive schedule column on notification_suppression_rules. */ import { describe, it, expect, afterEach } from 'vitest'; import fs from 'fs'; import os from 'os'; import path from 'path'; import Database from 'better-sqlite3'; import { DatabaseService } from '../services/DatabaseService'; import { scheduleAllowsSuppression } from '../helpers/notificationSchedule'; function resetDatabaseSingleton(): void { const holder = DatabaseService as unknown as { instance?: DatabaseService }; const existing = holder.instance; if (existing) { try { existing.getDb().close(); } catch { // already closed } holder.instance = undefined; } } describe('notification suppression schedule column migration', () => { let scratchDir: string | null = null; let prevDataDir: string | undefined; afterEach(() => { resetDatabaseSingleton(); if (prevDataDir === undefined) { delete process.env.DATA_DIR; } else { process.env.DATA_DIR = prevDataDir; } if (scratchDir) { try { fs.rmSync(scratchDir, { recursive: true, force: true }); } catch { // best-effort } scratchDir = null; } }); it('adds schedule via DatabaseService startup; legacy rows load as null', { timeout: 60_000 }, () => { scratchDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sencho-supp-sched-mig-')); const dbPath = path.join(scratchDir, 'sencho.db'); const seed = new Database(dbPath); try { seed.exec(` CREATE TABLE notification_suppression_rules ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, node_id INTEGER NULL, stack_patterns TEXT NOT NULL, label_ids TEXT NULL, categories TEXT NULL, levels TEXT NULL, applies_to TEXT NOT NULL, enabled INTEGER DEFAULT 1, expires_at INTEGER NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); INSERT INTO notification_suppression_rules (name, node_id, stack_patterns, label_ids, categories, levels, applies_to, enabled, expires_at, created_at, updated_at) VALUES ('Legacy mute', NULL, '[]', NULL, NULL, NULL, 'both', 1, NULL, 1, 1); `); } finally { seed.close(); } prevDataDir = process.env.DATA_DIR; process.env.DATA_DIR = scratchDir; resetDatabaseSingleton(); const db = DatabaseService.getInstance(); const cols = db.getDb().prepare('PRAGMA table_info(notification_suppression_rules)').all() as Array<{ name: string }>; expect(cols.filter((c) => c.name === 'schedule')).toHaveLength(1); const rule = db.getNotificationSuppressionRules().find((r) => r.name === 'Legacy mute'); expect(rule).toBeDefined(); expect(rule!.schedule).toBeNull(); expect(rule!.scheduleInvalid).toBe(false); resetDatabaseSingleton(); process.env.DATA_DIR = scratchDir; const db2 = DatabaseService.getInstance(); const cols2 = db2.getDb().prepare('PRAGMA table_info(notification_suppression_rules)').all() as Array<{ name: string }>; expect(cols2.filter((c) => c.name === 'schedule')).toHaveLength(1); }); it('empty-string schedule column is invalid and does not suppress via enabled load', { timeout: 60_000 }, () => { scratchDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sencho-supp-sched-empty-')); const dbPath = path.join(scratchDir, 'sencho.db'); const seed = new Database(dbPath); try { seed.exec(` CREATE TABLE notification_suppression_rules ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, node_id INTEGER NULL, stack_patterns TEXT NOT NULL, label_ids TEXT NULL, categories TEXT NULL, levels TEXT NULL, applies_to TEXT NOT NULL, enabled INTEGER DEFAULT 1, expires_at INTEGER NULL, schedule TEXT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); INSERT INTO notification_suppression_rules (name, node_id, stack_patterns, label_ids, categories, levels, applies_to, enabled, expires_at, schedule, created_at, updated_at) VALUES ('Empty schedule mute', NULL, '[]', NULL, '["monitor_alert"]', NULL, 'both', 1, NULL, '', 1, 1); `); } finally { seed.close(); } prevDataDir = process.env.DATA_DIR; process.env.DATA_DIR = scratchDir; resetDatabaseSingleton(); const db = DatabaseService.getInstance(); const rule = db.getEnabledNotificationSuppressionRules().find((r) => r.name === 'Empty schedule mute'); expect(rule).toBeDefined(); expect(rule!.schedule).toBeNull(); expect(rule!.scheduleInvalid).toBe(true); expect(scheduleAllowsSuppression(rule!.schedule, rule!.scheduleInvalid, Date.now())).toBe(false); }); });