57 lines
2.6 KiB
TypeScript
57 lines
2.6 KiB
TypeScript
/** Pass scheduling, coalescing, and runtime health reporting. */
|
|
import { expect, it, vi } from 'vitest'
|
|
import { Passes } from '../src/app/passes.ts'
|
|
import type { Trigger } from '../src/core/index.ts'
|
|
import { HealthBoard } from '../src/infra/health.ts'
|
|
|
|
function passes(closing = () => false) {
|
|
const health = new HealthBoard().claim('runtime', { state: 'starting', detail: '' })
|
|
const engine = { receive: vi.fn(() => 'stored' as const), evaluateDue: vi.fn() }
|
|
const effects = { kick: vi.fn(), idle: vi.fn(async () => {}) }
|
|
const value = new Passes({ engine, effects, health, lifetime: new AbortController().signal, configured: () => true, closing })
|
|
return { passes: value, engine, effects, health }
|
|
}
|
|
const trigger = { id: 't1', source: 'feishu', threads: [{ source: 'feishu', id: 'oc_1:om_1' }], actor: { source: 'feishu', id: 'ou_a' }, human: true,
|
|
at: '2026-10-01T00:00:00.000Z', text: 'hi', meta: {} } as Trigger
|
|
|
|
it('reports a failed pass as an error of the runtime, which the next pass that succeeds clears', async () => {
|
|
const h = passes()
|
|
h.engine.evaluateDue.mockImplementationOnce(() => { throw new Error('database is locked') })
|
|
h.passes.request()
|
|
await h.passes.idle()
|
|
expect(h.health.get()).toMatchObject({ state: 'error', detail: 'A pass failed: Error: database is locked' })
|
|
h.passes.request()
|
|
await h.passes.idle()
|
|
expect(h.health.get()).toMatchObject({ state: 'ready', detail: 'Orchestrator is ready' })
|
|
})
|
|
|
|
it('runs the passes asked for during a pass as one pass after it', async () => {
|
|
const h = passes()
|
|
await Promise.all([h.passes.pump(), h.passes.pump(), h.passes.pump(), h.passes.pump()])
|
|
expect([h.engine.evaluateDue.mock.calls.length, h.effects.kick.mock.calls.length]).toEqual([2, 2])
|
|
await h.passes.pump()
|
|
expect(h.engine.evaluateDue).toHaveBeenCalledTimes(3)
|
|
})
|
|
|
|
it('refuses a Trigger while dsh-superagent stops, before the Engine stores anything, and starts a pass for one it stored', async () => {
|
|
let closing = true
|
|
const h = passes(() => closing)
|
|
expect(() => h.passes.receive(trigger)).toThrow('Orchestrator is stopping')
|
|
expect(h.engine.receive).not.toHaveBeenCalled()
|
|
closing = false
|
|
expect(h.passes.receive(trigger)).toBe('stored')
|
|
await h.passes.idle()
|
|
expect(h.engine.evaluateDue).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('starts no timer once stopped, as a start-up that ends after a stop asks', async () => {
|
|
vi.useFakeTimers({ toFake: ['setInterval', 'clearInterval'] })
|
|
try {
|
|
const h = passes()
|
|
await h.passes.stop()
|
|
h.passes.start()
|
|
await vi.advanceTimersByTimeAsync(10_000)
|
|
expect(h.engine.evaluateDue).not.toHaveBeenCalled()
|
|
} finally { vi.useRealTimers() }
|
|
})
|