refactor: analysis workflow architecture

fix: NEXTAUTH_URL

fix: prevent project model edits from affecting default model
This commit is contained in:
saturn
2026-03-16 21:48:57 +08:00
parent ecbd183a77
commit 9aff44e37a
58 changed files with 2753 additions and 7985 deletions

View File

@@ -1,140 +0,0 @@
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',
}),
}),
}))
})
})

View File

@@ -1,136 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { GraphExecutorState } from '@/lib/run-runtime/graph-executor'
const executePipelineGraphMock = vi.hoisted(() =>
vi.fn(async (input: {
runId: string
projectId: string
userId: string
state: GraphExecutorState
nodes: Array<{
key: string
run: (context: {
runId: string
projectId: string
userId: string
nodeKey: string
attempt: number
state: GraphExecutorState
}) => Promise<unknown>
}>
}) => {
for (const node of input.nodes) {
await node.run({
runId: input.runId,
projectId: input.projectId,
userId: input.userId,
nodeKey: node.key,
attempt: 1,
state: input.state,
})
}
return input.state
}),
)
vi.mock('@/lib/run-runtime/graph-executor', () => ({
executePipelineGraph: executePipelineGraphMock,
}))
import { runLangGraphPipeline } from '@/lib/run-runtime/langgraph-pipeline'
type TestState = GraphExecutorState & {
order: string[]
}
describe('langgraph pipeline adapter', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('runs nodes in declared order through langgraph', async () => {
const state: TestState = {
refs: {},
meta: {},
order: [],
}
const result = await runLangGraphPipeline({
runId: 'run-1',
projectId: 'project-1',
userId: 'user-1',
state,
nodes: [
{
key: 'node_a',
title: 'Node A',
run: async (context) => {
const typedState = context.state as TestState
typedState.order.push('node_a')
return { output: { ok: true } }
},
},
{
key: 'node_b',
title: 'Node B',
run: async (context) => {
const typedState = context.state as TestState
typedState.order.push('node_b')
return { output: { ok: true } }
},
},
],
})
expect(result.order).toEqual(['node_a', 'node_b'])
expect(executePipelineGraphMock).toHaveBeenCalledTimes(2)
})
it('returns input state when graph has no nodes', async () => {
const state: TestState = {
refs: {},
meta: {},
order: [],
}
const result = await runLangGraphPipeline({
runId: 'run-1',
projectId: 'project-1',
userId: 'user-1',
state,
nodes: [],
})
expect(result).toBe(state)
expect(executePipelineGraphMock).not.toHaveBeenCalled()
})
it('fails explicitly on duplicate node keys', async () => {
const state: TestState = {
refs: {},
meta: {},
order: [],
}
await expect(
runLangGraphPipeline({
runId: 'run-1',
projectId: 'project-1',
userId: 'user-1',
state,
nodes: [
{
key: 'dup',
title: 'Dup 1',
run: async () => ({ output: { ok: true } }),
},
{
key: 'dup',
title: 'Dup 2',
run: async () => ({ output: { ok: true } }),
},
],
}),
).rejects.toThrow('LANGGRAPH_NODE_KEY_DUPLICATE: dup')
})
})