chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@sim/db', () => dbChainMock)
|
||||
|
||||
import {
|
||||
PauseResumeManager,
|
||||
updateResumeOutputInAggregationBuffers,
|
||||
} from '@/lib/workflows/executor/human-in-the-loop-manager'
|
||||
import type { SerializableExecutionState } from '@/executor/execution/types'
|
||||
import type { PausePoint, SerializedSnapshot } from '@/executor/types'
|
||||
|
||||
function createExecutionState(): SerializableExecutionState {
|
||||
return {
|
||||
blockStates: {},
|
||||
executedBlocks: [],
|
||||
blockLogs: [],
|
||||
decisions: { router: {}, condition: {} },
|
||||
completedLoops: [],
|
||||
activeExecutionPath: [],
|
||||
}
|
||||
}
|
||||
|
||||
describe('updateResumeOutputInAggregationBuffers', () => {
|
||||
it('replaces a paused parallel branch placeholder with the resumed HITL output', () => {
|
||||
const pausedOutput = {
|
||||
response: { status: 'paused' },
|
||||
_pauseMetadata: {
|
||||
contextId: 'pause-context-1',
|
||||
blockId: 'hitl₍1₎',
|
||||
},
|
||||
}
|
||||
const siblingOutput = { value: 'already-complete' }
|
||||
const mergedOutput = {
|
||||
response: { data: { submission: { approved: true } } },
|
||||
submission: { approved: true },
|
||||
_resumed: true,
|
||||
}
|
||||
const state = createExecutionState()
|
||||
state.parallelExecutions = {
|
||||
'parallel-1': {
|
||||
branchOutputs: {
|
||||
0: [siblingOutput],
|
||||
1: [pausedOutput],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updateResumeOutputInAggregationBuffers(
|
||||
state,
|
||||
'hitl₍1₎',
|
||||
'hitl',
|
||||
'pause-context-1',
|
||||
mergedOutput
|
||||
)
|
||||
|
||||
expect(state.parallelExecutions['parallel-1'].branchOutputs).toEqual({
|
||||
0: [siblingOutput],
|
||||
1: [mergedOutput],
|
||||
})
|
||||
})
|
||||
|
||||
it('does not replace unrelated paused parallel branch outputs', () => {
|
||||
const unrelatedPausedOutput = {
|
||||
response: { status: 'paused' },
|
||||
_pauseMetadata: {
|
||||
contextId: 'different-context',
|
||||
blockId: 'hitl₍1₎',
|
||||
},
|
||||
}
|
||||
const mergedOutput = {
|
||||
response: { data: { submission: { approved: true } } },
|
||||
submission: { approved: true },
|
||||
_resumed: true,
|
||||
}
|
||||
const state = createExecutionState()
|
||||
state.parallelExecutions = {
|
||||
'parallel-1': {
|
||||
branchOutputs: {
|
||||
1: [unrelatedPausedOutput],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updateResumeOutputInAggregationBuffers(
|
||||
state,
|
||||
'hitl₍1₎',
|
||||
'hitl',
|
||||
'pause-context-1',
|
||||
mergedOutput
|
||||
)
|
||||
|
||||
expect(state.parallelExecutions['parallel-1'].branchOutputs).toEqual({
|
||||
1: [unrelatedPausedOutput],
|
||||
})
|
||||
})
|
||||
|
||||
it('replaces paused loop iteration outputs using the resumed state block key', () => {
|
||||
const pausedOutput = {
|
||||
response: { status: 'paused' },
|
||||
_pauseMetadata: {
|
||||
contextId: 'pause-context-1',
|
||||
blockId: 'hitl',
|
||||
},
|
||||
}
|
||||
const unrelatedPausedOutput = {
|
||||
response: { status: 'paused' },
|
||||
_pauseMetadata: {
|
||||
contextId: 'different-context',
|
||||
blockId: 'hitl',
|
||||
},
|
||||
}
|
||||
const siblingOutput = { value: 'already-complete' }
|
||||
const mergedOutput = {
|
||||
response: { data: { submission: { approved: true } } },
|
||||
submission: { approved: true },
|
||||
_resumed: true,
|
||||
}
|
||||
const state = createExecutionState()
|
||||
state.loopExecutions = {
|
||||
'loop-1': {
|
||||
currentIterationOutputs: {
|
||||
hitl: pausedOutput,
|
||||
sibling: siblingOutput,
|
||||
},
|
||||
},
|
||||
'loop-2': {
|
||||
currentIterationOutputs: {
|
||||
hitl: unrelatedPausedOutput,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updateResumeOutputInAggregationBuffers(
|
||||
state,
|
||||
'hitl₍1₎',
|
||||
'hitl',
|
||||
'pause-context-1',
|
||||
mergedOutput
|
||||
)
|
||||
|
||||
expect(state.loopExecutions['loop-1'].currentIterationOutputs).toEqual({
|
||||
'hitl₍1₎': mergedOutput,
|
||||
sibling: siblingOutput,
|
||||
})
|
||||
expect(state.loopExecutions['loop-2'].currentIterationOutputs).toEqual({
|
||||
hitl: unrelatedPausedOutput,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('PauseResumeManager.getPauseContextDetail', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
resetDbChainMock()
|
||||
})
|
||||
|
||||
it('does not duplicate a pause point large response payload between pausePoint and execution.pausePoints', async () => {
|
||||
const largeDisplayValue = 'x'.repeat(50_000)
|
||||
|
||||
const row = {
|
||||
id: 'paused-exec-1',
|
||||
workflowId: 'workflow-1',
|
||||
executionId: 'execution-1',
|
||||
status: 'paused',
|
||||
pausedAt: null,
|
||||
updatedAt: null,
|
||||
expiresAt: null,
|
||||
metadata: {},
|
||||
executionSnapshot: { triggerIds: [] },
|
||||
pausePoints: {
|
||||
'ctx-1': {
|
||||
contextId: 'ctx-1',
|
||||
blockId: 'hitl-1',
|
||||
resumeStatus: 'paused',
|
||||
snapshotReady: true,
|
||||
pauseKind: 'human',
|
||||
registeredAt: '2026-07-02T00:00:00.000Z',
|
||||
response: {
|
||||
data: {
|
||||
operation: 'human',
|
||||
inputFormat: [{ id: 'field_0', name: 'approved', type: 'boolean', required: false }],
|
||||
submission: null,
|
||||
responseStructure: [
|
||||
{ name: 'ai_analysis', type: 'string', value: largeDisplayValue },
|
||||
],
|
||||
},
|
||||
status: 200,
|
||||
headers: {},
|
||||
},
|
||||
},
|
||||
'ctx-2': {
|
||||
contextId: 'ctx-2',
|
||||
blockId: 'hitl-2',
|
||||
resumeStatus: 'paused',
|
||||
snapshotReady: true,
|
||||
pauseKind: 'human',
|
||||
registeredAt: '2026-07-02T00:00:00.000Z',
|
||||
response: {
|
||||
data: { operation: 'human', inputFormat: [], submission: null },
|
||||
status: 200,
|
||||
headers: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dbChainMockFns.limit.mockResolvedValueOnce([row])
|
||||
dbChainMockFns.orderBy.mockResolvedValueOnce([])
|
||||
|
||||
const detail = await PauseResumeManager.getPauseContextDetail({
|
||||
workflowId: 'workflow-1',
|
||||
executionId: 'execution-1',
|
||||
contextId: 'ctx-1',
|
||||
})
|
||||
|
||||
expect(detail).not.toBeNull()
|
||||
// The requested pause point keeps its full response payload.
|
||||
expect(detail!.pausePoint.response.data.responseStructure[0].value).toBe(largeDisplayValue)
|
||||
expect(detail!.pausePoint.contextId).toBe('ctx-1')
|
||||
|
||||
// `execution.pausePoints` must not re-embed the (potentially large)
|
||||
// response payload — it's already available via `pausePoint` above.
|
||||
for (const point of detail!.execution.pausePoints) {
|
||||
expect(point.response?.data).toBeUndefined()
|
||||
}
|
||||
// Non-payload fields are still present on the execution's pause points.
|
||||
expect(detail!.execution.pausePoints.map((p) => p.contextId).sort()).toEqual(['ctx-1', 'ctx-2'])
|
||||
expect(detail!.execution.pausePoints.find((p) => p.contextId === 'ctx-1')?.resumeStatus).toBe(
|
||||
'paused'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns null when the pause context no longer exists', async () => {
|
||||
dbChainMockFns.limit.mockResolvedValueOnce([
|
||||
{
|
||||
id: 'paused-exec-1',
|
||||
workflowId: 'workflow-1',
|
||||
executionId: 'execution-1',
|
||||
status: 'paused',
|
||||
pausedAt: null,
|
||||
updatedAt: null,
|
||||
expiresAt: null,
|
||||
metadata: {},
|
||||
executionSnapshot: { triggerIds: [] },
|
||||
pausePoints: {
|
||||
'ctx-1': {
|
||||
contextId: 'ctx-1',
|
||||
blockId: 'hitl-1',
|
||||
resumeStatus: 'paused',
|
||||
snapshotReady: true,
|
||||
pauseKind: 'human',
|
||||
registeredAt: '2026-07-02T00:00:00.000Z',
|
||||
response: { data: { operation: 'human' }, status: 200, headers: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
dbChainMockFns.orderBy.mockResolvedValueOnce([])
|
||||
|
||||
const detail = await PauseResumeManager.getPauseContextDetail({
|
||||
workflowId: 'workflow-1',
|
||||
executionId: 'execution-1',
|
||||
contextId: 'missing-ctx',
|
||||
})
|
||||
|
||||
expect(detail).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('PauseResumeManager.persistPauseResult metadata merge on re-pause', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
resetDbChainMock()
|
||||
})
|
||||
|
||||
it('preserves the stashed cellContext when an existing paused row re-pauses (chained waits)', async () => {
|
||||
const cellContext = {
|
||||
tableId: 'table-1',
|
||||
rowId: 'row-1',
|
||||
workspaceId: 'workspace-1',
|
||||
groupId: 'group-1',
|
||||
workflowId: 'workflow-1',
|
||||
}
|
||||
const existingRow = {
|
||||
id: 'paused-exec-1',
|
||||
workflowId: 'workflow-1',
|
||||
executionId: 'execution-1',
|
||||
status: 'partially_resumed',
|
||||
pausePoints: {
|
||||
'ctx-wait-1': { contextId: 'ctx-wait-1', blockId: 'wait1', resumeStatus: 'resuming' },
|
||||
},
|
||||
metadata: {
|
||||
pauseScope: 'execution',
|
||||
triggerIds: ['start'],
|
||||
executorUserId: 'user-1',
|
||||
cellContext,
|
||||
},
|
||||
}
|
||||
|
||||
// First `.limit(1)` resolves the select-for-update to the existing row,
|
||||
// forcing persistPauseResult down the update (not insert) branch.
|
||||
dbChainMockFns.limit.mockResolvedValueOnce([existingRow])
|
||||
|
||||
const snapshotSeed: SerializedSnapshot = { snapshot: '{}', triggerIds: [] }
|
||||
const pausePoints: PausePoint[] = [
|
||||
{
|
||||
contextId: 'ctx-wait-2',
|
||||
blockId: 'wait2',
|
||||
pauseKind: 'time',
|
||||
resumeAt: new Date(Date.now() + 60_000).toISOString(),
|
||||
resumeStatus: 'paused',
|
||||
} as PausePoint,
|
||||
]
|
||||
|
||||
await PauseResumeManager.persistPauseResult({
|
||||
workflowId: 'workflow-1',
|
||||
executionId: 'execution-1',
|
||||
pausePoints,
|
||||
snapshotSeed,
|
||||
executorUserId: 'user-1',
|
||||
})
|
||||
|
||||
const updateSetCall = dbChainMockFns.set.mock.calls.find(
|
||||
([arg]) => arg && typeof arg === 'object' && 'metadata' in (arg as Record<string, unknown>)
|
||||
)
|
||||
expect(updateSetCall).toBeDefined()
|
||||
|
||||
const updatedMetadata = (updateSetCall![0] as { metadata: Record<string, unknown> }).metadata
|
||||
expect(updatedMetadata.cellContext).toEqual(cellContext)
|
||||
expect(updatedMetadata.pauseScope).toBe('execution')
|
||||
expect(updatedMetadata.executorUserId).toBe('user-1')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user