/* The app's tsconfig carries only jest types, so read sources through jest's own real-module loader rather than widening the compiler config for a test. */ declare const __dirname: string; const nodeFs = jest.requireActual('fs') as { readFileSync(path: string, encoding: string): string; }; const nodePath = jest.requireActual('path') as { join(...parts: string[]): string }; /** * Source-level contract checks for the native alarm delivery path. * * Kotlin behaviour cannot be executed in this Jest run (no Android runtime, no * device), so these assertions pin the *structure* the design depends on: * delivery goes through `AlarmManager.setAlarmClock`, there is no * notification-based delivery path, the standing entry is cancelled when the * device stops following an alert, and the boot receiver re-arms it. * * They are deliberately narrow. They prove the calls exist; they cannot prove * that Android accepts them, nor that scheduling lifts the full-screen-intent * restriction. Those are listed as unverified in the report. */ const NATIVE = nodePath.join(__dirname, '..', 'android', 'app', 'src', 'main', 'java', 'com', 'dcntrlized', 'alarm'); const MANIFEST = nodePath.join(__dirname, '..', 'android', 'app', 'src', 'main', 'AndroidManifest.xml'); function native(file: string): string { return nodeFs.readFileSync(nodePath.join(NATIVE, file), 'utf8'); } describe('native alarm delivery contract', () => { const scheduler = native('AlarmScheduler.kt'); const service = native('AlarmForegroundService.kt'); const notifier = native('AlarmPlayer.kt'); const receiver = native('AlarmWakeReceiver.kt'); const bridge = native('AlarmModule.kt'); const player = native('AlarmPlayer.kt'); const manifest = nodeFs.readFileSync(MANIFEST, 'utf8'); it('schedules a real alarm clock with AlarmManager.setAlarmClock', () => { expect(scheduler).toContain('setAlarmClock('); expect(scheduler).toContain('AlarmClockInfo('); expect(scheduler).toContain('canScheduleExactAlarms'); }); it('never makes the alarm clock entry launch an activity', () => { // Measured on Android 15: a background activity launch from the alarm // clock's PendingIntent is refused (BAL_BLOCK) and then *deferred*, so the // window surfaced later — after the alarm had been acknowledged — and the // siren came back for a delivered alert. The screen is raised by the // full-screen-intent notification; the entry itself must only wake the // process. const fireAt = scheduler.indexOf('fun fireNow('); const fire = scheduler .slice(fireAt, fireAt + 2000) .split('\n') .filter(line => !line.trimStart().startsWith('//')) .join('\n'); expect(fire).toContain('PendingIntent.getBroadcast('); expect(fire).toContain('AlarmWakeReceiver::class.java'); expect(fire).not.toContain('PendingIntent.getActivity('); expect(fire).not.toContain('AlarmActivity.intent('); // The one activity PendingIntent left is the alarm-clock *show* intent, // which only the user can invoke by tapping the status-bar alarm icon. const showAt = scheduler.indexOf('private fun showIntent('); const show = scheduler.slice(showAt); expect(show).toContain('PendingIntent.getActivity('); const code = scheduler .split('\n') .filter(line => !line.trimStart().startsWith('//')) .join('\n'); expect(code.split('PendingIntent.getActivity(').length - 1).toBe(1); // The receiver must ignore an entry that outlived the arming. expect(receiver).toContain('AlarmScheduler.ACTION_FIRE'); expect(receiver).toContain('armed?.alertId != alertId'); }); it('fires the alarm from the trigger frame through the scheduler, not startActivity', () => { expect(service).toContain('AlarmScheduler.fireNow('); expect(service).not.toMatch(/startActivity\(AlarmActivity\.intent/); // A failed schedule is recorded, never replaced by a notification. expect(service).toContain('markDeliveryFailed'); }); it('holds a standing alarm-clock entry while armed and re-arms after reboot', () => { expect(service).toContain('AlarmScheduler.holdStanding('); expect(receiver).toContain('ACTION_BOOT_COMPLETED'); expect(receiver).toContain('rearmAfterBoot'); expect(receiver).toContain('ACTION_STANDING'); expect(receiver).toContain('AlarmScheduler.holdStanding('); }); it('cancels the entries and forgets the arm record on stop', () => { expect(service).toContain('AlarmScheduler.cancel('); expect(service).toContain('AlarmArmStore.clear('); }); it('keeps the standing entry across a disarm — only the firing entry dies', () => { const disarmAt = service.indexOf('"disarm"'); expect(disarmAt).toBeGreaterThan(-1); const disarm = service.slice(disarmAt, disarmAt + 500); expect(disarm).toContain('AlarmScheduler.cancelFire('); // A disarm ends the ring, not the subscription: cancelling the standing // entry here would drop the "armed" indicator and the re-arm point. expect(disarm).not.toContain('AlarmScheduler.cancel('); // cancelFire must not touch the standing entry either. const cancelFireAt = scheduler.indexOf('fun cancelFire('); const cancelFireBody = scheduler.slice(cancelFireAt, cancelFireAt + 800); expect(cancelFireBody).not.toContain('REQ_STANDING'); expect(cancelFireBody).toContain('REQ_FIRE'); }); // The old contract said "no notification ever announces an alarm" — that was // falsified on an Android 15 emulator: the alarm's own PendingIntent cannot // start the takeover from the background (`BAL_BLOCK`), so the ringing alarm // needs the one notification the platform does launch for you, a full-screen // intent. Delivery is still the alarm clock + the service's own audio stream; // the notification is only the launch path for the screen. it('has three channels, and exactly one full-screen-intent notification', () => { const channels = notifier.split('= NotificationChannel(').length - 1; expect(channels).toBe(3); expect(notifier).toContain('CHANNEL_VOLUME'); expect(notifier).toContain('CHANNEL_SERVICE'); expect(notifier).toContain('CHANNEL_FIRING'); const fsi = notifier.split('setFullScreenIntent').length - 1; expect(fsi).toBe(1); // The firing channel is silent: the siren is our own audio stream, and a // channel sound on top of it would double the alarm. const firingAt = notifier.indexOf('CHANNEL_FIRING,'); expect(notifier.slice(firingAt, firingAt + 400)).toContain('setSound(null, null)'); }); it('keeps the volume warning informational and never arms from a notification', () => { expect(notifier).toContain('Alarm volume is at '); expect(notifier).toContain('CATEGORY_RECOMMENDATION'); // The volume warning must not carry the takeover: only the firing // notification may take over the screen. const warningAt = notifier.indexOf('fun showVolumeWarning'); expect(notifier.slice(warningAt, warningAt + 1200)).not.toContain('setFullScreenIntent'); }); it('refuses to arm a device that cannot deliver a real alarm', () => { expect(bridge).toContain('exact-alarms-not-granted'); expect(bridge).toContain('full-screen-intent-not-granted'); }); it('uses the specialUse foreground-service type with a real subtype', () => { // dataSync's 6h/24h cumulative cap is why the primary type changed; the // subtype text is the reason shown for a specialUse service. expect(manifest).toContain('android:foregroundServiceType="dataSync|specialUse"'); expect(manifest).toContain('PROPERTY_SPECIAL_USE_FGS_SUBTYPE'); expect(manifest).toContain('android.permission.FOREGROUND_SERVICE_SPECIAL_USE'); expect(manifest).not.toContain('FOREGROUND_SERVICE_MEDIA_PLAYBACK'); expect(service).toContain('FOREGROUND_SERVICE_TYPE_SPECIAL_USE'); // Kept as defence in depth even though specialUse has no time limit today. expect(service).toContain('override fun onTimeout('); }); it('schedules close to now instead of rounding up to a future bucket', () => { // The answer to "31st second or next minute" is the 31st second: the // trigger time is now + a small margin, never a rounded tick. const match = /FIRE_DELAY_MS\s*=\s*(\d+)L/.exec(scheduler); expect(match).not.toBeNull(); const fireDelay = Number(match ? match[1] : NaN); expect(fireDelay).toBeGreaterThan(0); expect(fireDelay).toBeLessThanOrEqual(1000); expect(scheduler).toContain('System.currentTimeMillis() + FIRE_DELAY_MS'); }); it('contains no repeating, inexact or delayed scheduling', () => { // No bucket quantization anywhere in the firing path. The socket reconnect // backoff in the service is the only Handler use and is not scheduling an // alarm (asserted below by scoping this to the scheduler/player/activity). const activity = native('AlarmActivity.kt'); for (const src of [scheduler, player, activity]) { expect(src).not.toContain('setRepeating'); expect(src).not.toContain('setInexactRepeating'); expect(src).not.toContain('setWindow'); expect(src).not.toContain('postDelayed'); expect(src).not.toMatch(/Handler\(/); } }); it('rings until the device user dismisses it — no auto-stop anywhere', () => { // User ruling: a lost owner must not mean an alarm that expires on a timer. for (const file of [ 'AlarmScheduler.kt', 'AlarmPlayer.kt', 'AlarmActivity.kt', 'AlarmForegroundService.kt', 'AlarmModule.kt', ]) { expect(native(file)).not.toMatch(/autoStop/i); } expect(player).not.toMatch(/stopTimer|expire|countdown/i); }); it('declares the exact-alarm and boot permissions it relies on', () => { expect(manifest).toContain('android.permission.USE_EXACT_ALARM'); expect(manifest).toContain('android.permission.SCHEDULE_EXACT_ALARM'); expect(manifest).toContain('android.permission.USE_FULL_SCREEN_INTENT'); expect(manifest).toContain('android.permission.RECEIVE_BOOT_COMPLETED'); expect(manifest).toContain('.AlarmWakeReceiver'); }); });