Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions apps/docs/openapi-v2-workflows.json
Original file line number Diff line number Diff line change
Expand Up @@ -4107,8 +4107,15 @@
"type": "object",
"properties": {
"contextId": {
"type": "string",
"description": "Resume context identifier for the earliest active pause point."
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Resume context identifier, or null while every pause point is mid-resume."
},
"pausedAt": {
"type": "string",
Expand Down
21 changes: 21 additions & 0 deletions apps/sim/app/api/credentials/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,25 @@ describe('POST /api/credentials', () => {
auditMetadata: { principalKind: 'tenant', principalId: 'acct_123' },
principal: { kind: 'tenant', id: 'acct_123' },
})
queueTableRows(credential, [])
queueTableRows(credential, [])
queueTableRows(credential, [
{
id: 'credential-1',
workspaceId: WORKSPACE_ID,
type: 'service_account',
displayName: 'Zoom account acct_123',
description: null,
providerId: 'zoom-service-account',
accountId: null,
envKey: null,
envOwnerUserId: null,
encryptedServiceAccountKey: 'encrypted-blob',
createdBy: 'user-1',
createdAt: new Date('2026-08-11T00:00:00.000Z'),
updatedAt: new Date('2026-08-11T00:00:00.000Z'),
},
])

const req = createMockRequest('POST', {
workspaceId: WORKSPACE_ID,
Expand All @@ -154,8 +173,10 @@ describe('POST /api/credentials', () => {
})

const response = await POST(req)
const body = await response.json()

expect(response.status).toBe(201)
expect(body.credential).not.toHaveProperty('encryptedServiceAccountKey')
expect(mockVerifyAndBuildServiceAccountSecret).toHaveBeenCalledTimes(1)
expect(mockVerifyAndBuildServiceAccountSecret).toHaveBeenCalledWith(
'zoom-service-account',
Expand Down
17 changes: 13 additions & 4 deletions apps/sim/app/api/credentials/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,18 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}

if (!result.credential) {
throw new Error('Credential creation succeeded without a credential')
}

const responseBody = createWorkspaceCredentialContract.response.schema.parse({
credential: {
...result.credential,
createdAt: result.credential.createdAt.toISOString(),
updatedAt: result.credential.updatedAt.toISOString(),
},
})

// An existing credential matched the source: an idempotent replay, not a create.
return NextResponse.json(
{ credential: result.credential },
{ status: result.created ? 201 : 200 }
)
return NextResponse.json(responseBody, { status: result.created ? 201 : 200 })
})
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ function resolveContentProvenance(
request: NextRequest,
principal: Principal,
payload: unknown,
workspaceId: string,
workspaceId: string | undefined,
includeContent: boolean
) {
const resolved = resolveKnowledgeWriteSecretProvenance({
request,
payload,
authType: internalKnowledgeAuthType(principal),
userId: internalKnowledgeActorUserId(principal),
workspaceId,
...(workspaceId ? { workspaceId } : {}),
selectionKeys: includeContent ? ['chunk-content'] : [],
})
if (!resolved.success) {
Expand Down Expand Up @@ -93,7 +93,7 @@ export const PUT = defineInternalJsonRoute({
chunkId: params.chunkId,
content: body.content,
enabled: body.enabled,
resolveContentProvenance: ({ workspaceId }: { workspaceId: string }) =>
resolveContentProvenance: ({ workspaceId }: { workspaceId?: string }) =>
resolveContentProvenance(request, principal, body, workspaceId, body.content !== undefined),
}),
useCase: updateKnowledgeChunk,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ function resolveContentProvenance(
request: NextRequest,
principal: Principal,
payload: unknown,
workspaceId: string,
workspaceId: string | undefined,
includeContent: boolean
) {
const resolved = resolveKnowledgeWriteSecretProvenance({
request,
payload,
authType: internalKnowledgeAuthType(principal),
userId: internalKnowledgeActorUserId(principal),
workspaceId,
...(workspaceId ? { workspaceId } : {}),
selectionKeys: includeContent ? ['chunk-content'] : [],
})
if (!resolved.success) {
Expand Down Expand Up @@ -95,7 +95,7 @@ export const POST = defineInternalJsonRoute({
documentId: params.documentId,
content: body.content,
enabled: body.enabled,
resolveContentProvenance: ({ workspaceId }: { workspaceId: string }) =>
resolveContentProvenance: ({ workspaceId }: { workspaceId?: string }) =>
resolveContentProvenance(request, principal, body, workspaceId, true),
}),
useCase: createKnowledgeChunk,
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/knowledge/[id]/documents/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const POST = defineInternalJsonRoute({
rateLimit: internalRateLimits.none({
reason: 'Preserve existing internal document-create behavior',
}),
errorPolicy: internalKnowledgeErrorPolicies.documents,
errorPolicy: internalKnowledgeErrorPolicies.uploads,
mapInput: ({ params, body }, { principal, request }) => {
const documents = body.bulk ? body.documents : [body]
return {
Expand Down
43 changes: 43 additions & 0 deletions apps/sim/app/api/knowledge/migrated-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,15 @@ vi.mock('@/lib/core/telemetry', () => ({

vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: mocks.capture }))

import { OrchestrationError } from '@/lib/core/orchestration/types'
import { KnowledgeUsageLimitExceededError } from '@/lib/knowledge/application/billing'
import {
GET as listConnectorDocuments,
PATCH as updateConnectorDocuments,
} from '@/app/api/knowledge/[id]/connectors/[connectorId]/documents/route'
import { PUT as updateDocument } from '@/app/api/knowledge/[id]/documents/[documentId]/route'
import {
PATCH as bulkDocuments,
POST as createDocuments,
GET as listDocuments,
} from '@/app/api/knowledge/[id]/documents/route'
Expand Down Expand Up @@ -360,6 +363,46 @@ describe('migrated internal Knowledge routes', () => {
)
})

it('preserves payment-required for document usage admission', async () => {
mocks.createDocuments.mockRejectedValueOnce(
new KnowledgeUsageLimitExceededError('Usage limit exceeded')
)

const response = await createDocuments(
createMockRequest('POST', {
bulk: false,
filename: document.filename,
fileUrl: document.fileUrl,
fileSize: document.fileSize,
mimeType: document.mimeType,
}),
{ params: Promise.resolve({ id: 'knowledge-1' }) }
)

expect(response.status).toBe(402)
await expect(response.json()).resolves.toEqual({ error: 'Usage limit exceeded' })
expect(mocks.capture).not.toHaveBeenCalled()
})

it('returns not found when a bulk document selection has no active matches', async () => {
mocks.bulkDocuments.mockRejectedValueOnce(
new OrchestrationError('not_found', 'No valid documents found to update')
)

const response = await bulkDocuments(
createMockRequest('PATCH', {
operation: 'disable',
documentIds: ['document-1'],
}),
{ params: Promise.resolve({ id: 'knowledge-1' }) }
)

expect(response.status).toBe(404)
await expect(response.json()).resolves.toEqual({
error: 'No valid documents found to update',
})
})

it('rejects oversized document-create arrays at the contract boundary', async () => {
const response = await createDocuments(
createMockRequest('POST', {
Expand Down
43 changes: 43 additions & 0 deletions apps/sim/app/api/mcp/serve/[serverId]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,49 @@ describe('MCP Serve Route', () => {
expect(body.result.isError).toBe(false)
})

it('reports a human-in-the-loop pause as a successful tool result', async () => {
dbChainMockFns.limit
.mockResolvedValueOnce([
{
id: 'server-1',
name: 'Public Server',
workspaceId: 'ws-1',
isPublic: true,
createdBy: 'owner-1',
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])

mockExecuteWorkflowService.mockResolvedValueOnce({
ok: true,
executionId: 'exec-paused',
workflowId: 'wf-1',
status: 'paused',
aborted: null,
output: { approvalRequired: true },
error: null,
hasResponseBlock: false,
resolvedSecretTraceProvenance: createResolvedSecretTraceProvenance('owner-1'),
})

const req = new NextRequest('http://localhost:3000/api/mcp/serve/server-1', {
method: 'POST',
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: { name: 'tool_a', arguments: { q: 'test' } },
}),
})
const response = await POST(req, { params: Promise.resolve({ serverId: 'server-1' }) })
const body = await response.json()

expect(response.status).toBe(200)
expect(body.result.isError).toBe(false)
expect(body.result.content[0].text).toContain('approvalRequired')
})

it('serializes failed runs with the structured error and child executionId', async () => {
dbChainMockFns.limit
.mockResolvedValueOnce([
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/mcp/serve/[serverId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,7 @@ async function handleToolsCall(
)
}

const isError = serviceResult.status !== 'completed'
const isError = serviceResult.status === 'failed' || serviceResult.status === 'cancelled'
const toolOutput = isError
? {
success: false,
Expand Down
5 changes: 2 additions & 3 deletions apps/sim/app/api/table/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { describe, expect, it } from 'vitest'
import { OrchestrationError } from '@/lib/core/orchestration/types'
import { TableRowLimitError } from '@/lib/table/billing'
import { TableRowNotFoundError } from '@/lib/table/rows/errors'
import type { ColumnDefinition } from '@/lib/table/types'
import {
orchestrationErrorResponse,
Expand Down Expand Up @@ -54,9 +55,7 @@ describe('orchestrationErrorResponse', () => {
})

it('answers the code the failure carries, not one derived from its wording', () => {
expect(
orchestrationErrorResponse(new OrchestrationError('not_found', 'Row not found'))?.status
).toBe(404)
expect(orchestrationErrorResponse(new TableRowNotFoundError())?.status).toBe(404)
// The phrase that used to force a 400 no longer decides anything.
expect(
orchestrationErrorResponse(new OrchestrationError('conflict', 'Row 3: must be unique'))
Expand Down
Loading
Loading