Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (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 / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

361 lines
12 KiB
TypeScript

/**
* @vitest-environment node
*/
import { loggerMock } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { BillingAttributionSnapshot } from '@/lib/billing/core/billing-attribution'
import type { ExecutionSnapshot } from '@/executor/execution/snapshot'
const {
captureServerEventMock,
executeWorkflowCoreMock,
handlePostExecutionPauseStateMock,
loggingSessionConstructorMock,
projectDiagnosticErrorMock,
safeStartMock,
waitForPostExecutionMock,
setTrustedExecutionCorrelationMock,
} = vi.hoisted(() => ({
captureServerEventMock: vi.fn(),
executeWorkflowCoreMock: vi.fn(),
handlePostExecutionPauseStateMock: vi.fn(),
loggingSessionConstructorMock: vi.fn(),
projectDiagnosticErrorMock: vi.fn(),
safeStartMock: vi.fn(),
waitForPostExecutionMock: vi.fn(),
setTrustedExecutionCorrelationMock: vi.fn(),
}))
vi.mock('@sim/utils/id', () => ({
generateId: () => 'execution-1',
}))
vi.mock('@/lib/logs/execution/logging-session', () => ({
LoggingSession: class {
projectDiagnosticError = projectDiagnosticErrorMock
safeStart = safeStartMock
waitForPostExecution = waitForPostExecutionMock
setTrustedExecutionCorrelation = setTrustedExecutionCorrelationMock
constructor(...args: unknown[]) {
loggingSessionConstructorMock(...args)
}
},
}))
vi.mock('@/lib/posthog/server', () => ({
captureServerEvent: captureServerEventMock,
}))
vi.mock('@/lib/workflows/executor/execution-core', () => ({
executeWorkflowCore: executeWorkflowCoreMock,
}))
vi.mock('@/lib/workflows/executor/pause-persistence', () => ({
handlePostExecutionPauseState: handlePostExecutionPauseStateMock,
}))
import { executeWorkflow } from '@/lib/workflows/executor/execute-workflow'
const workflowExecutionLoggerCallIndex = loggerMock.createLogger.mock.calls.findIndex(
([name]) => name === 'WorkflowExecution'
)
const workflowExecutionLogger =
loggerMock.createLogger.mock.results[workflowExecutionLoggerCallIndex]?.value
if (!workflowExecutionLogger) throw new Error('WorkflowExecution logger mock was not initialized')
const billingAttribution: BillingAttributionSnapshot = {
actorUserId: 'actor-1',
workspaceId: 'workspace-1',
organizationId: 'org-1',
billedAccountUserId: 'owner-1',
billingEntity: { type: 'organization', id: 'org-1' },
billingPeriod: {
start: '2026-07-01T00:00:00.000Z',
end: '2026-08-01T00:00:00.000Z',
},
payerSubscription: {
id: 'subscription-1',
referenceId: 'org-1',
plan: 'team',
status: 'active',
seats: 5,
periodStart: '2026-07-01T00:00:00.000Z',
periodEnd: '2026-08-01T00:00:00.000Z',
},
}
const workflow = {
id: 'workflow-1',
userId: 'owner-1',
workspaceId: 'workspace-1',
variables: {},
}
describe('executeWorkflow', () => {
beforeEach(() => {
vi.clearAllMocks()
safeStartMock.mockResolvedValue(true)
waitForPostExecutionMock.mockResolvedValue(undefined)
projectDiagnosticErrorMock.mockImplementation(
(error: unknown, details: Record<string, unknown> = {}) => ({
...details,
errorType: error instanceof Error ? 'error' : typeof error,
hasStack: error instanceof Error && typeof error.stack === 'string',
})
)
handlePostExecutionPauseStateMock.mockResolvedValue(undefined)
executeWorkflowCoreMock.mockImplementation(
async (params: {
snapshot: ExecutionSnapshot
loggingSession: { safeStart: (startParams: unknown) => Promise<boolean> }
}) => {
await params.loggingSession.safeStart({
userId: params.snapshot.metadata.userId,
billingAttribution: params.snapshot.metadata.billingAttribution,
workspaceId: params.snapshot.metadata.workspaceId,
})
return {
success: true,
output: { ok: true },
logs: [],
metadata: { duration: 10 },
status: 'completed',
}
}
)
})
it('rejects workspace execution without immutable billing attribution', async () => {
await expect(
executeWorkflow(workflow, 'request-1', undefined, 'actor-1', {
enabled: true,
})
).rejects.toThrow('Billing attribution is required for workspace execution')
expect(executeWorkflowCoreMock).not.toHaveBeenCalled()
expect(safeStartMock).not.toHaveBeenCalled()
})
it.each([
['actor', { ...billingAttribution, actorUserId: 'other-actor' }],
['workspace', { ...billingAttribution, workspaceId: 'other-workspace' }],
])('rejects a billing attribution %s mismatch', async (_scope, mismatchedAttribution) => {
await expect(
executeWorkflow(workflow, 'request-1', undefined, 'actor-1', {
enabled: true,
billingAttribution: mismatchedAttribution,
})
).rejects.toThrow('Workflow billing attribution does not match its actor and workspace')
expect(executeWorkflowCoreMock).not.toHaveBeenCalled()
expect(safeStartMock).not.toHaveBeenCalled()
})
it('asserts the billing attribution snapshot before execution', async () => {
const malformedAttribution = {
...billingAttribution,
billingPeriod: undefined,
} as unknown as BillingAttributionSnapshot
await expect(
executeWorkflow(workflow, 'request-1', undefined, 'actor-1', {
enabled: true,
billingAttribution: malformedAttribution,
})
).rejects.toThrow('Billing attribution snapshot is missing its billing period')
expect(executeWorkflowCoreMock).not.toHaveBeenCalled()
})
it('propagates validated attribution through execution metadata to logger startup', async () => {
await executeWorkflow(workflow, 'request-1', { prompt: 'hello' }, 'actor-1', {
enabled: true,
workflowTriggerType: 'copilot',
billingAttribution,
})
const coreParams = executeWorkflowCoreMock.mock.calls[0]?.[0] as {
snapshot: ExecutionSnapshot
}
expect(coreParams.snapshot.metadata.billingAttribution).toEqual(billingAttribution)
expect(Object.isFrozen(coreParams.snapshot.metadata.billingAttribution)).toBe(true)
expect(safeStartMock).toHaveBeenCalledWith({
userId: 'actor-1',
billingAttribution,
workspaceId: 'workspace-1',
})
expect(loggingSessionConstructorMock).toHaveBeenCalledWith(
'workflow-1',
'execution-1',
'copilot',
'request-1'
)
})
it('forwards trusted initial trace-secret provenance to the execution core', async () => {
const provenance = {
version: 1 as const,
complete: true,
entries: [{ name: 'API_KEY', encryptedValue: 'encrypted-secret' }],
scope: { userId: 'actor-1', workspaceId: 'workspace-1' },
}
await executeWorkflow(workflow, 'request-1', { prompt: 'hello' }, 'actor-1', {
enabled: true,
billingAttribution,
trustedInitialResolvedSecretTraceProvenance: provenance,
})
expect(executeWorkflowCoreMock).toHaveBeenCalledWith(
expect.objectContaining({ trustedInitialResolvedSecretTraceProvenance: provenance })
)
})
it('waits for post-execution persistence before resolving', async () => {
let resolvePostExecution!: () => void
waitForPostExecutionMock.mockReturnValueOnce(
new Promise<void>((resolve) => {
resolvePostExecution = resolve
})
)
let executionSettled = false
const executionPromise = executeWorkflow(
workflow,
'request-1',
{ prompt: 'hello' },
'actor-1',
{
enabled: true,
billingAttribution,
}
).then((result) => {
executionSettled = true
return result
})
await vi.waitFor(() => expect(waitForPostExecutionMock).toHaveBeenCalledOnce())
expect(executionSettled).toBe(false)
resolvePostExecution()
await executionPromise
expect(executionSettled).toBe(true)
})
it('waits for post-execution persistence before rejecting', async () => {
const executionError = new Error('Request body size limit exceeded (10MB)')
executeWorkflowCoreMock.mockRejectedValueOnce(executionError)
let resolvePostExecution!: () => void
waitForPostExecutionMock.mockReturnValueOnce(
new Promise<void>((resolve) => {
resolvePostExecution = resolve
})
)
let executionSettled = false
const executionPromise = executeWorkflow(workflow, 'request-1', undefined, 'actor-1', {
enabled: true,
billingAttribution,
}).catch((error: unknown) => {
executionSettled = true
throw error
})
await vi.waitFor(() => expect(waitForPostExecutionMock).toHaveBeenCalledOnce())
expect(executionSettled).toBe(false)
resolvePostExecution()
await expect(executionPromise).rejects.toBe(executionError)
expect(executionSettled).toBe(true)
})
it('transfers post-execution ownership with successful streaming metadata', async () => {
const result = await executeWorkflow(workflow, 'request-1', undefined, 'actor-1', {
enabled: true,
skipLoggingComplete: true,
billingAttribution,
})
expect(waitForPostExecutionMock).not.toHaveBeenCalled()
expect(result._streamingMetadata?.loggingSession).toBeDefined()
})
it('retains post-execution ownership when streaming execution rejects', async () => {
const executionError = new Error('Streaming execution failed')
executeWorkflowCoreMock.mockRejectedValueOnce(executionError)
await expect(
executeWorkflow(workflow, 'request-1', undefined, 'actor-1', {
enabled: true,
skipLoggingComplete: true,
billingAttribution,
})
).rejects.toBe(executionError)
expect(waitForPostExecutionMock).toHaveBeenCalledOnce()
})
it('persists server-issued workflow-group correlation in execution metadata', async () => {
const correlation = {
executionId: 'execution-1',
requestId: 'wfgrp-execution-1',
source: 'workflow_group' as const,
workflowId: 'workflow-1',
triggerType: 'table',
tableId: 'table-1',
rowId: 'row-1',
groupId: 'group-1',
}
await executeWorkflow(workflow, 'request-1', { rowId: 'row-1' }, 'actor-1', {
enabled: true,
workflowTriggerType: 'table',
billingAttribution,
trustedExecutionCorrelation: correlation,
})
const coreParams = executeWorkflowCoreMock.mock.calls[0]?.[0] as {
snapshot: ExecutionSnapshot
}
expect(coreParams.snapshot.metadata.correlation).toEqual(correlation)
expect(setTrustedExecutionCorrelationMock).toHaveBeenCalledWith(correlation)
})
it('uses the shared diagnostic projection for operational logs and failure telemetry', async () => {
const secret = 'workflow-telemetry-secret-7f3a91'
const error = new Error(`failed ${secret} __var_API_KEY __sim_code_1_binding_0`)
const projectedError = 'failed {{API_KEY}} {{API_KEY}} [RUNTIME_BINDING]'
executeWorkflowCoreMock.mockRejectedValueOnce(error)
projectDiagnosticErrorMock.mockReturnValueOnce({ error: projectedError, errorName: 'Error' })
await expect(
executeWorkflow(workflow, 'request-1', undefined, 'actor-1', {
enabled: true,
billingAttribution,
})
).rejects.toBe(error)
expect(workflowExecutionLogger.error).toHaveBeenCalledWith(
'[request-1] Workflow execution failed',
{ error: projectedError, errorName: 'Error' }
)
expect(captureServerEventMock).toHaveBeenCalledWith(
'actor-1',
'workflow_execution_failed',
expect.objectContaining({ error_message: projectedError }),
expect.anything()
)
const observabilityPayload = JSON.stringify({
logger: workflowExecutionLogger.error.mock.calls,
telemetry: captureServerEventMock.mock.calls,
})
expect(observabilityPayload).not.toContain(secret)
expect(observabilityPayload).not.toContain('__var_')
expect(observabilityPayload).not.toContain('__sim_')
expect(error.message).toContain(secret)
})
})