Files
2026-10-31 20:05:39 +08:00

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() }
})