first commit

This commit is contained in:
wsq
2026-05-13 21:58:19 +08:00
commit 0167c66cb7
1475 changed files with 233414 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
import { describe, expect, it } from 'vitest'
import { RUN_STATUS } from '@/lib/run-runtime/types'
import { isRecoverableRunRecord, selectRecoverableRun } from '@/lib/run-runtime/recovery'
describe('run recovery', () => {
it('treats queued runs as recoverable', () => {
expect(isRecoverableRunRecord({
id: 'run-1',
status: RUN_STATUS.QUEUED,
})).toBe(true)
})
it('rejects running runs with expired lease and no heartbeat extension', () => {
const now = Date.parse('2026-03-30T10:00:00.000Z')
expect(isRecoverableRunRecord({
id: 'run-1',
status: RUN_STATUS.RUNNING,
leaseExpiresAt: '2026-03-30T09:59:00.000Z',
heartbeatAt: '2026-03-30T09:58:30.000Z',
}, now)).toBe(false)
})
it('prefers the latest recoverable run and skips stale leased runs', () => {
const now = Date.parse('2026-03-30T10:00:00.000Z')
const decision = selectRecoverableRun([
{
id: 'run-stale',
status: RUN_STATUS.RUNNING,
createdAt: '2026-03-30T09:00:00.000Z',
updatedAt: '2026-03-30T09:30:00.000Z',
leaseExpiresAt: '2026-03-30T09:59:00.000Z',
heartbeatAt: '2026-03-30T09:58:30.000Z',
},
{
id: 'run-fresh',
status: RUN_STATUS.RUNNING,
createdAt: '2026-03-30T09:50:00.000Z',
updatedAt: '2026-03-30T09:59:30.000Z',
leaseExpiresAt: '2026-03-30T10:02:00.000Z',
heartbeatAt: '2026-03-30T09:59:30.000Z',
},
], now)
expect(decision).toEqual({
runId: 'run-fresh',
reason: 'latest_active',
})
})
it('skips a newer stale active run when an older recoverable run still exists', () => {
const now = Date.parse('2026-03-30T10:00:00.000Z')
const decision = selectRecoverableRun([
{
id: 'run-new-stale',
status: RUN_STATUS.RUNNING,
createdAt: '2026-03-30T09:58:00.000Z',
updatedAt: '2026-03-30T09:59:50.000Z',
leaseExpiresAt: '2026-03-30T09:59:00.000Z',
heartbeatAt: '2026-03-30T09:58:30.000Z',
},
{
id: 'run-older-fresh',
status: RUN_STATUS.RUNNING,
createdAt: '2026-03-30T09:50:00.000Z',
updatedAt: '2026-03-30T09:59:30.000Z',
leaseExpiresAt: '2026-03-30T10:02:00.000Z',
heartbeatAt: '2026-03-30T09:59:30.000Z',
},
], now)
expect(decision).toEqual({
runId: 'run-older-fresh',
reason: 'latest_active',
})
})
})
+133
View File
@@ -0,0 +1,133 @@
import { describe, expect, it } from 'vitest'
import { mapTaskSSEEventToRunEvents } from '@/lib/run-runtime/task-bridge'
import { RUN_EVENT_TYPE } from '@/lib/run-runtime/types'
import { TASK_EVENT_TYPE, TASK_SSE_EVENT_TYPE, type SSEEvent } from '@/lib/task/types'
function buildEvent(input: Partial<SSEEvent>): SSEEvent {
return {
id: input.id || '1',
type: input.type || TASK_SSE_EVENT_TYPE.LIFECYCLE,
taskId: input.taskId || 'task_1',
projectId: input.projectId || 'project_1',
userId: input.userId || 'user_1',
ts: input.ts || new Date().toISOString(),
payload: input.payload || {},
taskType: input.taskType || null,
targetType: input.targetType || null,
targetId: input.targetId || null,
episodeId: input.episodeId || null,
}
}
describe('task->run event bridge', () => {
it('maps task.stream to step.chunk and normalizes lane by kind', () => {
const event = buildEvent({
type: TASK_SSE_EVENT_TYPE.STREAM,
payload: {
runId: 'run_1',
stepId: 'step_a',
stream: {
kind: 'reasoning',
delta: 'abc',
seq: 1,
},
},
})
const mapped = mapTaskSSEEventToRunEvents(event)
expect(mapped).toHaveLength(1)
expect(mapped[0]).toMatchObject({
runId: 'run_1',
eventType: RUN_EVENT_TYPE.STEP_CHUNK,
stepKey: 'step_a',
lane: 'reasoning',
})
})
it('uses taskType-based fallback stepKey for stream when stepId missing', () => {
const event = buildEvent({
type: TASK_SSE_EVENT_TYPE.STREAM,
taskType: 'story_to_script_run',
payload: {
runId: 'run_1',
stream: {
kind: 'text',
delta: 'hello',
seq: 1,
},
},
})
const mapped = mapTaskSSEEventToRunEvents(event)
expect(mapped).toHaveLength(1)
expect(mapped[0]).toMatchObject({
eventType: RUN_EVENT_TYPE.STEP_CHUNK,
stepKey: 'step:story_to_script_run',
lane: 'text',
})
})
it('maps task.processing + done=true to step.start then step.complete', () => {
const event = buildEvent({
payload: {
runId: 'run_2',
stepId: 'step_b',
lifecycleType: TASK_EVENT_TYPE.PROCESSING,
done: true,
},
})
const mapped = mapTaskSSEEventToRunEvents(event)
expect(mapped).toHaveLength(2)
expect(mapped[0]?.eventType).toBe(RUN_EVENT_TYPE.STEP_START)
expect(mapped[1]?.eventType).toBe(RUN_EVENT_TYPE.STEP_COMPLETE)
})
it('maps processing error stage to step.error', () => {
const event = buildEvent({
payload: {
meta: { runId: 'run_3' },
stepId: 'step_c',
lifecycleType: TASK_EVENT_TYPE.PROCESSING,
stage: 'worker_llm_error',
error: {
message: 'boom',
},
},
})
const mapped = mapTaskSSEEventToRunEvents(event)
expect(mapped).toHaveLength(2)
expect(mapped[0]?.eventType).toBe(RUN_EVENT_TYPE.STEP_START)
expect(mapped[1]).toMatchObject({
eventType: RUN_EVENT_TYPE.STEP_ERROR,
runId: 'run_3',
stepKey: 'step_c',
})
})
it('maps task.completed to step.complete and run.complete', () => {
const event = buildEvent({
payload: {
runId: 'run_4',
stepId: 'step_d',
lifecycleType: TASK_EVENT_TYPE.COMPLETED,
},
})
const mapped = mapTaskSSEEventToRunEvents(event)
expect(mapped).toHaveLength(2)
expect(mapped[0]?.eventType).toBe(RUN_EVENT_TYPE.STEP_COMPLETE)
expect(mapped[1]?.eventType).toBe(RUN_EVENT_TYPE.RUN_COMPLETE)
})
it('returns empty when runId is missing', () => {
const event = buildEvent({
payload: {
stepId: 'step_x',
lifecycleType: TASK_EVENT_TYPE.PROCESSING,
},
})
expect(mapTaskSSEEventToRunEvents(event)).toEqual([])
})
})