141 lines
3.2 KiB
TypeScript
141 lines
3.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
executePipelineGraph,
|
|
GraphCancellationError,
|
|
type GraphExecutorState,
|
|
} from '@/lib/run-runtime/graph-executor'
|
|
|
|
const { createCheckpointMock, getRunByIdMock } = vi.hoisted(() => ({
|
|
createCheckpointMock: vi.fn(),
|
|
getRunByIdMock: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/lib/run-runtime/service', () => ({
|
|
buildLeanState: vi.fn((value: unknown) => value),
|
|
createCheckpoint: createCheckpointMock,
|
|
getRunById: getRunByIdMock,
|
|
}))
|
|
|
|
describe('graph executor', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
getRunByIdMock.mockResolvedValue({
|
|
id: 'run_1',
|
|
userId: 'user_1',
|
|
status: 'running',
|
|
})
|
|
})
|
|
|
|
it('retries retryable node error and writes checkpoint once success', async () => {
|
|
const state: GraphExecutorState = {
|
|
refs: {},
|
|
meta: {},
|
|
}
|
|
|
|
const runMock = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new TypeError('fetch failed sending request'))
|
|
.mockResolvedValueOnce({
|
|
output: { ok: true },
|
|
})
|
|
|
|
await executePipelineGraph({
|
|
runId: 'run_1',
|
|
projectId: 'project_1',
|
|
userId: 'user_1',
|
|
state,
|
|
nodes: [
|
|
{
|
|
key: 'node_a',
|
|
title: 'Node A',
|
|
maxAttempts: 2,
|
|
run: runMock,
|
|
},
|
|
],
|
|
})
|
|
|
|
expect(runMock).toHaveBeenCalledTimes(2)
|
|
expect(createCheckpointMock).toHaveBeenCalledTimes(1)
|
|
expect(createCheckpointMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
runId: 'run_1',
|
|
nodeKey: 'node_a',
|
|
version: 2,
|
|
}))
|
|
})
|
|
|
|
it('throws cancellation error when run status is canceling', async () => {
|
|
getRunByIdMock.mockResolvedValue({
|
|
id: 'run_1',
|
|
userId: 'user_1',
|
|
status: 'canceling',
|
|
})
|
|
|
|
await expect(
|
|
executePipelineGraph({
|
|
runId: 'run_1',
|
|
projectId: 'project_1',
|
|
userId: 'user_1',
|
|
state: {
|
|
refs: {},
|
|
meta: {},
|
|
},
|
|
nodes: [
|
|
{
|
|
key: 'node_a',
|
|
title: 'Node A',
|
|
run: async () => ({ output: { ok: true } }),
|
|
},
|
|
],
|
|
}),
|
|
).rejects.toBeInstanceOf(GraphCancellationError)
|
|
})
|
|
|
|
it('merges refs into state and persists lean checkpoint', async () => {
|
|
const state: GraphExecutorState = {
|
|
refs: {
|
|
scriptId: 'script_1',
|
|
},
|
|
meta: {
|
|
tag: 'v1',
|
|
},
|
|
}
|
|
|
|
await executePipelineGraph({
|
|
runId: 'run_1',
|
|
projectId: 'project_1',
|
|
userId: 'user_1',
|
|
state,
|
|
nodes: [
|
|
{
|
|
key: 'node_b',
|
|
title: 'Node B',
|
|
run: async () => ({
|
|
checkpointRefs: {
|
|
storyboardId: 'storyboard_1',
|
|
},
|
|
checkpointMeta: {
|
|
done: true,
|
|
},
|
|
}),
|
|
},
|
|
],
|
|
})
|
|
|
|
expect(state.refs).toEqual({
|
|
scriptId: 'script_1',
|
|
storyboardId: 'storyboard_1',
|
|
voiceLineBatchId: undefined,
|
|
versionHash: undefined,
|
|
cursor: undefined,
|
|
})
|
|
expect(createCheckpointMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
state: expect.objectContaining({
|
|
refs: expect.objectContaining({
|
|
scriptId: 'script_1',
|
|
storyboardId: 'storyboard_1',
|
|
}),
|
|
}),
|
|
}))
|
|
})
|
|
})
|