mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 08:04:05 +00:00
241 lines
7.4 KiB
TypeScript
241 lines
7.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { ExternalAutomationsHandler } from './external-automations-handler'
|
|
import type { RelayDispatcher } from './dispatcher'
|
|
|
|
const execFileMock = vi.hoisted(() =>
|
|
vi.fn((...args: unknown[]) => {
|
|
const callback = args.at(-1)
|
|
if (typeof callback === 'function') {
|
|
const execCallback = callback as (error: Error | null, stdout: string, stderr: string) => void
|
|
execCallback(null, '', '')
|
|
}
|
|
})
|
|
)
|
|
|
|
vi.mock('child_process', () => ({ execFile: execFileMock }))
|
|
|
|
type CapturedHandler = (params?: Record<string, unknown>) => Promise<unknown>
|
|
|
|
function createHandlerHarness(): {
|
|
handler: ExternalAutomationsHandler
|
|
requestHandlers: Map<string, CapturedHandler>
|
|
} {
|
|
const requestHandlers = new Map<string, CapturedHandler>()
|
|
const dispatcher = {
|
|
onRequest(method: string, handler: CapturedHandler): void {
|
|
requestHandlers.set(method, handler)
|
|
}
|
|
}
|
|
const handler = new ExternalAutomationsHandler(dispatcher as unknown as RelayDispatcher)
|
|
return { handler, requestHandlers }
|
|
}
|
|
|
|
beforeEach(() => {
|
|
execFileMock.mockClear()
|
|
})
|
|
|
|
describe('ExternalAutomationsHandler', () => {
|
|
it('runs external lifecycle actions without shell wrapping', async () => {
|
|
const { requestHandlers } = createHandlerHarness()
|
|
|
|
await requestHandlers.get('externalAutomations.act')?.({
|
|
provider: 'hermes',
|
|
action: 'run',
|
|
jobId: 'job-1'
|
|
})
|
|
|
|
expect(execFileMock).toHaveBeenCalledWith(
|
|
'hermes',
|
|
['cron', 'run', 'job-1'],
|
|
{ encoding: 'utf-8', timeout: 30_000 },
|
|
expect.any(Function)
|
|
)
|
|
})
|
|
|
|
it('paginates remote Hermes run history after ref lookup', async () => {
|
|
const { handler, requestHandlers } = createHandlerHarness()
|
|
const handlerInternals = handler as unknown as {
|
|
readHermesRunRefs: (jobId: string) => Promise<{ id: string; run_at: string }[]>
|
|
hydrateHermesRunRef: (
|
|
jobId: string,
|
|
ref: { id: string; run_at: string }
|
|
) => Promise<{ id: string; run_at: string }>
|
|
}
|
|
handlerInternals.readHermesRunRefs = vi.fn().mockResolvedValue([
|
|
{
|
|
id: 'cron_job-1_20260516_090000',
|
|
run_at: '2026-05-16T09:00:00'
|
|
},
|
|
{
|
|
id: 'job-1:2026-05-15_09-00-00.md',
|
|
run_at: '2026-05-15T09:00:00'
|
|
},
|
|
{
|
|
id: 'job-1:2026-05-14_09-00-00.md',
|
|
run_at: '2026-05-14T09:00:00'
|
|
}
|
|
])
|
|
handlerInternals.hydrateHermesRunRef = vi.fn(async (_jobId, ref) => ref)
|
|
|
|
const result = (await requestHandlers.get('externalAutomations.runs')?.({
|
|
provider: 'hermes',
|
|
jobId: 'job-1',
|
|
page: 1,
|
|
pageSize: 2
|
|
})) as { total: number; runs: { id: string }[] }
|
|
|
|
expect(result.total).toBe(3)
|
|
expect(result.runs.map((run) => run.id)).toEqual([
|
|
'cron_job-1_20260516_090000',
|
|
'job-1:2026-05-15_09-00-00.md'
|
|
])
|
|
})
|
|
|
|
it('uses a count-only path for remote Hermes manager listing run counts', async () => {
|
|
const { handler, requestHandlers } = createHandlerHarness()
|
|
const handlerInternals = handler as unknown as {
|
|
readHermesRunCount: (jobId: string) => Promise<number>
|
|
}
|
|
handlerInternals.readHermesRunCount = vi.fn().mockResolvedValue(42)
|
|
|
|
const result = (await requestHandlers.get('externalAutomations.runs')?.({
|
|
provider: 'hermes',
|
|
jobId: 'job-1',
|
|
page: 1,
|
|
pageSize: 0
|
|
})) as { total: number; runs: unknown[] }
|
|
|
|
expect(result).toEqual({ total: 42, runs: [] })
|
|
})
|
|
|
|
it('deduplicates concurrent remote Hermes count reads', async () => {
|
|
const { handler, requestHandlers } = createHandlerHarness()
|
|
let resolveRefs: (refs: { id: string; run_at: string }[]) => void = () => {}
|
|
const readHermesRunRefs = vi.fn(
|
|
() =>
|
|
new Promise<{ id: string; run_at: string }[]>((resolve) => {
|
|
resolveRefs = resolve
|
|
})
|
|
)
|
|
const handlerInternals = handler as unknown as {
|
|
readHermesRunRefs: typeof readHermesRunRefs
|
|
}
|
|
handlerInternals.readHermesRunRefs = readHermesRunRefs
|
|
|
|
const first = requestHandlers.get('externalAutomations.runs')?.({
|
|
provider: 'hermes',
|
|
jobId: 'job-1',
|
|
page: 1,
|
|
pageSize: 0
|
|
})
|
|
const second = requestHandlers.get('externalAutomations.runs')?.({
|
|
provider: 'hermes',
|
|
jobId: 'job-1',
|
|
page: 1,
|
|
pageSize: 0
|
|
})
|
|
|
|
expect(readHermesRunRefs).toHaveBeenCalledTimes(1)
|
|
resolveRefs([
|
|
{ id: 'job-1:2026-05-15_09-00-00.md', run_at: '2026-05-15T09:00:00' },
|
|
{ id: 'job-1:2026-05-16_09-00-00.md', run_at: '2026-05-16T09:00:00' }
|
|
])
|
|
|
|
await expect(Promise.all([first, second])).resolves.toEqual([
|
|
{ total: 2, runs: [] },
|
|
{ total: 2, runs: [] }
|
|
])
|
|
})
|
|
|
|
it('clears the remote Hermes count cache after lifecycle actions', async () => {
|
|
const { handler, requestHandlers } = createHandlerHarness()
|
|
const readHermesRunRefs = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([
|
|
{ id: 'job-1:2026-05-15_09-00-00.md', run_at: '2026-05-15T09:00:00' }
|
|
])
|
|
.mockResolvedValueOnce([
|
|
{ id: 'job-1:2026-05-15_09-00-00.md', run_at: '2026-05-15T09:00:00' },
|
|
{ id: 'job-1:2026-05-16_09-00-00.md', run_at: '2026-05-16T09:00:00' }
|
|
])
|
|
const handlerInternals = handler as unknown as {
|
|
readHermesRunRefs: typeof readHermesRunRefs
|
|
}
|
|
handlerInternals.readHermesRunRefs = readHermesRunRefs
|
|
|
|
await expect(
|
|
requestHandlers.get('externalAutomations.runs')?.({
|
|
provider: 'hermes',
|
|
jobId: 'job-1',
|
|
page: 1,
|
|
pageSize: 0
|
|
})
|
|
).resolves.toEqual({ total: 1, runs: [] })
|
|
await expect(
|
|
requestHandlers.get('externalAutomations.runs')?.({
|
|
provider: 'hermes',
|
|
jobId: 'job-1',
|
|
page: 1,
|
|
pageSize: 0
|
|
})
|
|
).resolves.toEqual({ total: 1, runs: [] })
|
|
|
|
await requestHandlers.get('externalAutomations.act')?.({
|
|
provider: 'hermes',
|
|
action: 'run',
|
|
jobId: 'job-1'
|
|
})
|
|
await expect(
|
|
requestHandlers.get('externalAutomations.runs')?.({
|
|
provider: 'hermes',
|
|
jobId: 'job-1',
|
|
page: 1,
|
|
pageSize: 0
|
|
})
|
|
).resolves.toEqual({ total: 2, runs: [] })
|
|
expect(readHermesRunRefs).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('evicts oldest remote Hermes count cache entries when many job ids are observed', async () => {
|
|
const { handler, requestHandlers } = createHandlerHarness()
|
|
const readHermesRunRefs = vi.fn(async (jobId: string) =>
|
|
jobId === 'job-0'
|
|
? [{ id: 'job-0:2026-05-15_09-00-00.md', run_at: '2026-05-15T09:00:00' }]
|
|
: []
|
|
)
|
|
const handlerInternals = handler as unknown as {
|
|
readHermesRunRefs: typeof readHermesRunRefs
|
|
}
|
|
handlerInternals.readHermesRunRefs = readHermesRunRefs
|
|
|
|
await expect(
|
|
requestHandlers.get('externalAutomations.runs')?.({
|
|
provider: 'hermes',
|
|
jobId: 'job-0',
|
|
page: 1,
|
|
pageSize: 0
|
|
})
|
|
).resolves.toEqual({ total: 1, runs: [] })
|
|
|
|
for (let i = 1; i <= 200; i += 1) {
|
|
await requestHandlers.get('externalAutomations.runs')?.({
|
|
provider: 'hermes',
|
|
jobId: `job-${i}`,
|
|
page: 1,
|
|
pageSize: 0
|
|
})
|
|
}
|
|
|
|
await expect(
|
|
requestHandlers.get('externalAutomations.runs')?.({
|
|
provider: 'hermes',
|
|
jobId: 'job-0',
|
|
page: 1,
|
|
pageSize: 0
|
|
})
|
|
).resolves.toEqual({ total: 1, runs: [] })
|
|
|
|
expect(readHermesRunRefs).toHaveBeenCalledTimes(202)
|
|
})
|
|
})
|