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
106 changes: 106 additions & 0 deletions packages/worker/src/package-runtime/package-workflows.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,112 @@ test('DynamicCallableWorkflowBase restores attached remote connectors for packag
)
})

test('DynamicCallableWorkflowBase marks package export error responses as workflow errors', async () => {
const binding = createStatefulWorkflowBinding()
const db = createWorkflowRunsDatabase()
const env = {
APP_DB: db,
DYNAMIC_CALLABLE_WORKFLOWS: binding.workflow,
APP_BASE_URL: 'https://app.example.com',
} as Env
const created = await createDynamicCallableWorkflow({
env,
userId: 'user-1',
packageContext: null,
body: {
packageId: 'pkg-1',
exportName: './workflow-run-event',
runAt: '2026-05-03T12:34:56.000Z',
idempotencyKey: 'package-error-response-smoke',
params: { key: 'west-sensitive-reopen' },
},
})
const queued = binding.instances.get(created.id)
if (!queued?.params) throw new Error('Expected queued workflow payload.')
invocationMocks.invokePackageExport.mockReset()
invocationMocks.invokePackageExport.mockResolvedValueOnce({
status: 500,
body: {
ok: false,
error: {
message:
'Shade workflow event failed: Tool "home_default_bond_shade_set_position" not found',
},
},
})

const workflow = new DynamicCallableWorkflowBase({} as ExecutionContext, env)
const stepDo = vi.fn(
async (_name: string, _config: unknown, callback: () => unknown) =>
await callback(),
)
await expect(
workflow.run(
{
payload: queued.params as never,
timestamp: new Date(),
instanceId: created.id,
},
{ sleepUntil: vi.fn(), do: stepDo } as unknown as WorkflowStep,
),
).rejects.toThrow('home_default_bond_shade_set_position')
expect(db.workflowRuns.get(created.id)).toMatchObject({
status: 'errored',
completed_at: expect.any(String),
last_error: expect.stringContaining('home_default_bond_shade_set_position'),
})
})

test('DynamicCallableWorkflowBase rejects package export redirect responses', async () => {
const binding = createStatefulWorkflowBinding()
const db = createWorkflowRunsDatabase()
const env = {
APP_DB: db,
DYNAMIC_CALLABLE_WORKFLOWS: binding.workflow,
APP_BASE_URL: 'https://app.example.com',
} as Env
const created = await createDynamicCallableWorkflow({
env,
userId: 'user-1',
packageContext: null,
body: {
packageId: 'pkg-1',
exportName: './workflow-run-event',
runAt: '2026-05-03T12:34:56.000Z',
idempotencyKey: 'package-redirect-response-smoke',
params: { key: 'west-sensitive-reopen' },
},
})
const queued = binding.instances.get(created.id)
if (!queued?.params) throw new Error('Expected queued workflow payload.')
invocationMocks.invokePackageExport.mockReset()
invocationMocks.invokePackageExport.mockResolvedValueOnce({
status: 302,
body: { ok: false },
})

const workflow = new DynamicCallableWorkflowBase({} as ExecutionContext, env)
const stepDo = vi.fn(
async (_name: string, _config: unknown, callback: () => unknown) =>
await callback(),
)
await expect(
workflow.run(
{
payload: queued.params as never,
timestamp: new Date(),
instanceId: created.id,
},
{ sleepUntil: vi.fn(), do: stepDo } as unknown as WorkflowStep,
),
).rejects.toThrow('Package workflow export failed with HTTP 302.')
expect(db.workflowRuns.get(created.id)).toMatchObject({
status: 'errored',
completed_at: expect.any(String),
last_error: 'Package workflow export failed with HTTP 302.',
})
})

test('createDynamicCallableWorkflow verifies package ownership before queueing package exports', async () => {
const binding = createWorkflowBinding({ existing: null })
const db = createWorkflowRunsDatabase()
Expand Down
21 changes: 21 additions & 0 deletions packages/worker/src/package-runtime/package-workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,24 @@ function toSerializableJson(value: unknown): JsonValue {
}
}

function getWorkflowInvocationErrorMessage(response: {
status: number
body: unknown
}) {
const body = response.body
const error =
body && typeof body === 'object'
? (body as Record<string, unknown>)['error']
: null
const message =
error && typeof error === 'object'
? (error as Record<string, unknown>)['message']
: null
return typeof message === 'string' && message.trim()
? message
: `Package workflow export failed with HTTP ${response.status}.`
}

function normalizeNonEmptyString(value: string, fieldName: string) {
const trimmed = value.trim()
if (!trimmed) {
Expand Down Expand Up @@ -992,6 +1010,9 @@ export class DynamicCallableWorkflowBase extends WorkflowEntrypoint<
topic: payload.workflowName,
},
})
if (response.status < 200 || response.status >= 300) {
throw new Error(getWorkflowInvocationErrorMessage(response))
}
return {
status: response.status,
body: toSerializableJson(response.body),
Expand Down
6 changes: 3 additions & 3 deletions packages/worker/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"assets": {
"directory": "./public",
"binding": "ASSETS",
"run_worker_first": ["/mcp-apps/*", "/styles.css"],
"run_worker_first": ["/connectors/*", "/mcp-apps/*", "/styles.css"],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
"durable_objects": {
"bindings": [
Expand Down Expand Up @@ -183,7 +183,7 @@
"assets": {
"directory": "./public",
"binding": "ASSETS",
"run_worker_first": ["/mcp-apps/*", "/styles.css"],
"run_worker_first": ["/connectors/*", "/mcp-apps/*", "/styles.css"],
},
"durable_objects": {
"bindings": [
Expand Down Expand Up @@ -267,7 +267,7 @@
"assets": {
"directory": "./public",
"binding": "ASSETS",
"run_worker_first": ["/mcp-apps/*", "/styles.css"],
"run_worker_first": ["/connectors/*", "/mcp-apps/*", "/styles.css"],
},
"durable_objects": {
"bindings": [
Expand Down
Loading