/ src / lib / server / runtime / wake-mode.test.ts
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  })