wake-mode.test.ts
1 import assert from 'node:assert/strict' 2 import { describe, it } from 'node:test' 3 4 import { 5 computeWakePriority, 6 createJobContext, 7 resolveRunAt, 8 sourceToWakeMode, 9 wakeModeToSource, 10 } from '@/lib/server/runtime/wake-mode' 11 import type { WakeModeRequest } from '@/lib/server/runtime/wake-mode' 12 13 describe('WakeMode', () => { 14 describe('computeWakePriority', () => { 15 it('returns mode-based default priority when none specified', () => { 16 assert.equal(computeWakePriority({ mode: 'immediate' }), 80) 17 assert.equal(computeWakePriority({ mode: 'next_heartbeat' }), 40) 18 assert.equal(computeWakePriority({ mode: 'scheduled' }), 60) 19 }) 20 21 it('uses explicit priority when provided', () => { 22 assert.equal(computeWakePriority({ mode: 'immediate', priority: 95 }), 95) 23 assert.equal(computeWakePriority({ mode: 'next_heartbeat', priority: 10 }), 10) 24 }) 25 26 it('clamps priority to [0, 100]', () => { 27 assert.equal(computeWakePriority({ mode: 'immediate', priority: 150 }), 100) 28 assert.equal(computeWakePriority({ mode: 'immediate', priority: -5 }), 0) 29 }) 30 31 it('ignores non-finite priority values', () => { 32 assert.equal(computeWakePriority({ mode: 'immediate', priority: NaN }), 80) 33 assert.equal(computeWakePriority({ mode: 'immediate', priority: Infinity }), 80) 34 }) 35 }) 36 37 describe('resolveRunAt', () => { 38 const NOW = 1_700_000_000_000 39 40 it('returns now for immediate mode', () => { 41 assert.equal(resolveRunAt({ mode: 'immediate' }, NOW), NOW) 42 }) 43 44 it('returns null for next_heartbeat mode (deferred)', () => { 45 assert.equal(resolveRunAt({ mode: 'next_heartbeat' }, NOW), null) 46 }) 47 48 it('returns absolute runAt for scheduled mode', () => { 49 const target = NOW + 60_000 50 assert.equal(resolveRunAt({ mode: 'scheduled', runAt: target }, NOW), target) 51 }) 52 53 it('computes runAt from delayMs for scheduled mode', () => { 54 assert.equal(resolveRunAt({ mode: 'scheduled', delayMs: 5_000 }, NOW), NOW + 5_000) 55 }) 56 57 it('clamps scheduled runAt to at least now', () => { 58 const pastTime = NOW - 10_000 59 assert.equal(resolveRunAt({ mode: 'scheduled', runAt: pastTime }, NOW), NOW) 60 }) 61 62 it('falls back to now for scheduled mode without runAt or delayMs', () => { 63 assert.equal(resolveRunAt({ mode: 'scheduled' }, NOW), NOW) 64 }) 65 }) 66 67 describe('wakeModeToSource (backward compat)', () => { 68 it('maps immediate to heartbeat-wake', () => { 69 assert.equal(wakeModeToSource('immediate'), 'heartbeat-wake') 70 }) 71 72 it('maps next_heartbeat to heartbeat', () => { 73 assert.equal(wakeModeToSource('next_heartbeat'), 'heartbeat') 74 }) 75 76 it('maps scheduled to heartbeat-wake', () => { 77 assert.equal(wakeModeToSource('scheduled'), 'heartbeat-wake') 78 }) 79 }) 80 81 describe('sourceToWakeMode (legacy migration)', () => { 82 it('infers next_heartbeat from heartbeat source', () => { 83 assert.equal(sourceToWakeMode('heartbeat'), 'next_heartbeat') 84 }) 85 86 it('infers immediate from heartbeat-wake source', () => { 87 assert.equal(sourceToWakeMode('heartbeat-wake'), 'immediate') 88 }) 89 90 it('infers scheduled from schedule-prefixed source', () => { 91 assert.equal(sourceToWakeMode('schedule:nightly'), 'scheduled') 92 }) 93 94 it('defaults to immediate for unknown sources', () => { 95 assert.equal(sourceToWakeMode('connector:slack'), 'immediate') 96 }) 97 }) 98 99 describe('createJobContext', () => { 100 it('creates an isolated context with scratchpad', () => { 101 const controller = new AbortController() 102 const ctx = createJobContext({ 103 jobId: 'job-1', 104 sessionId: 'sess-1', 105 agentId: 'agent-1', 106 mode: 'immediate', 107 signal: controller.signal, 108 source: 'connector:slack', 109 reason: 'New message arrived', 110 }) 111 112 assert.equal(ctx.jobId, 'job-1') 113 assert.equal(ctx.sessionId, 'sess-1') 114 assert.equal(ctx.agentId, 'agent-1') 115 assert.equal(ctx.mode, 'immediate') 116 assert.equal(ctx.source, 'connector:slack') 117 assert.equal(ctx.reason, 'New message arrived') 118 assert.ok(ctx.createdAt > 0) 119 assert.equal(ctx.startedAt, undefined) 120 assert.equal(ctx.endedAt, undefined) 121 assert.ok(ctx.scratchpad instanceof Map) 122 assert.equal(ctx.scratchpad.size, 0) 123 }) 124 125 it('scratchpad isolates state between jobs', () => { 126 const controller = new AbortController() 127 const ctx1 = createJobContext({ 128 jobId: 'job-a', 129 sessionId: 'sess-1', 130 mode: 'immediate', 131 signal: controller.signal, 132 }) 133 const ctx2 = createJobContext({ 134 jobId: 'job-b', 135 sessionId: 'sess-1', 136 mode: 'next_heartbeat', 137 signal: controller.signal, 138 }) 139 140 ctx1.scratchpad.set('key', 'value-a') 141 ctx2.scratchpad.set('key', 'value-b') 142 143 assert.equal(ctx1.scratchpad.get('key'), 'value-a') 144 assert.equal(ctx2.scratchpad.get('key'), 'value-b') 145 }) 146 147 it('captures heartbeat snapshot for isolation', () => { 148 const controller = new AbortController() 149 const snapshot = '# Heartbeat Tasks\n## Active\n- [ ] Send report' 150 const ctx = createJobContext({ 151 jobId: 'job-snap', 152 sessionId: 'sess-1', 153 mode: 'next_heartbeat', 154 signal: controller.signal, 155 heartbeatSnapshot: snapshot, 156 }) 157 158 assert.equal(ctx.heartbeatSnapshot, snapshot) 159 }) 160 }) 161 })