import { test } from 'node:test'; import assert from 'node:assert/strict'; import { dismissNudge, NUDGE_GRACE_MS, NUDGE_SNOOZE_MS, nudgeFor, parseNudgeRecord, SecurityState, shouldShowNudge, pickNudge, staleSessions, staleSessionsText, } from './securityNudge'; const healthy: SecurityState = { crossSigning: true, deviceVerified: true, backupOnServer: true, backupActive: true, }; test('the #123 table: which nudge for which state', () => { // E: healthy → nothing, ever. assert.equal(nudgeFor(healthy), undefined); // B: cross-signing, this device unverified, backup on server → verify first. assert.equal(nudgeFor({ ...healthy, deviceVerified: false }), 'verify-device'); // C: cross-signing, unverified, no backup → still verify first. assert.equal( nudgeFor({ ...healthy, deviceVerified: false, backupOnServer: false }), 'verify-device', ); // D: verified, no backup → #110's set-up nudge. assert.equal(nudgeFor({ ...healthy, backupOnServer: false }), 'setup-backup'); // #110 state 2: backup exists, this device not using it. assert.equal(nudgeFor({ ...healthy, backupActive: false }), 'connect-backup'); // A: never set up cross-signing, no backup → set up (the flow does both). assert.equal( nudgeFor({ crossSigning: false, deviceVerified: false, backupOnServer: false, backupActive: false, }), 'setup-backup', ); }); test('nothing while crypto is still loading (F)', () => { assert.equal(nudgeFor({ ...healthy, deviceVerified: undefined }), undefined); assert.equal(nudgeFor({ ...healthy, backupOnServer: undefined }), undefined); assert.equal(nudgeFor({ ...healthy, backupActive: undefined }), undefined); // Unknown backup connection doesn't hide a known "no backup at all". assert.equal( nudgeFor({ ...healthy, backupOnServer: false, backupActive: undefined }), 'setup-backup', ); }); test('timing: 24 h grace, 7-day snooze, stop after 3 dismissals', () => { const t0 = 1_700_000_000_000; let record = parseNudgeRecord(null, t0); assert.equal(shouldShowNudge('verify-device', record, t0), false, 'first launch'); assert.equal(shouldShowNudge('verify-device', record, t0 + NUDGE_GRACE_MS - 1), false); let now = t0 + NUDGE_GRACE_MS; assert.equal(shouldShowNudge('verify-device', record, now), true, 'after 24 h'); for (let i = 1; i <= 3; i += 1) { record = dismissNudge('verify-device', record, now); assert.equal(shouldShowNudge('verify-device', record, now), false, `snoozed after #${i}`); now += NUDGE_SNOOZE_MS; assert.equal(shouldShowNudge('verify-device', record, now), i < 3, `after 7 days (#${i})`); } // Dismissing one nudge doesn't hide another. assert.equal(shouldShowNudge('setup-backup', record, now), true); }); test('stored record: round trip, and garbage starts a fresh grace period', () => { const t0 = 1_700_000_000_000; const r = dismissNudge('setup-backup', parseNudgeRecord(null, t0 - 5), t0); assert.deepEqual(parseNudgeRecord(JSON.stringify(r), t0 + 1), r); for (const bad of ['{', 'null', '"x"', '{"firstSeen":"yesterday"}', '{"firstSeen":1}']) { assert.deepEqual(parseNudgeRecord(bad, t0), { firstSeen: t0, dismissals: {} }, bad); } }); test('[#113] stale sessions: 90+ days unused, never the current one', () => { const now = Date.UTC(2026, 9, 25); const day = 86_400_000; const devices = [ { device_id: 'ME', last_seen_ts: now - 400 * day }, { device_id: 'OLD', last_seen_ts: now - 91 * day }, { device_id: 'EDGE', last_seen_ts: now - 90 * day }, { device_id: 'RECENT', last_seen_ts: now - 89 * day }, { device_id: 'UNKNOWN' }, { device_id: 'NULL', last_seen_ts: null }, ]; assert.deepEqual( staleSessions(devices, 'ME', now).map((d) => d.device_id), ['OLD', 'EDGE'], ); }); test('[#113] wording follows the count', () => { assert.match( staleSessionsText(1), /^You have a session you haven't used in 3 months\. Signing it out/, ); assert.match( staleSessionsText(3), /^You have 3 sessions you haven't used in 3 months\. Signing them out/, ); }); test('[#113] pickNudge: security first, stale sessions when that one is snoozed or absent', () => { const now = 10 * 86_400_000; const fresh = { firstSeen: 0, dismissals: {} }; assert.equal(pickNudge('verify-device', 2, fresh, now), 'verify-device'); assert.equal(pickNudge(undefined, 2, fresh, now), 'stale-sessions'); assert.equal(pickNudge(undefined, 0, fresh, now), undefined); const snoozed = dismissNudge('verify-device', fresh, now); assert.equal(pickNudge('verify-device', 2, snoozed, now), 'stale-sessions'); const bothSnoozed = dismissNudge('stale-sessions', snoozed, now); assert.equal(pickNudge('verify-device', 2, bothSnoozed, now), undefined); // inside the 24 h grace period nothing shows assert.equal(pickNudge(undefined, 2, { firstSeen: now, dismissals: {} }, now), undefined); });