Skip to content

fix(testing): handle workflow failure states correctly - #682

Open
hln33 wants to merge 1 commit into
mainfrom
fix/656-emulator-null-invocation-error-causes-successful-execution
Open

fix(testing): handle workflow failure states correctly#682
hln33 wants to merge 1 commit into
mainfrom
fix/656-emulator-null-invocation-error-causes-successful-execution

Conversation

@hln33

@hln33 hln33 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Issue #, if available:
#656

Description of changes:

  • Modify handling of failed invocations to authoratively call self._fail_workflow() instead of self._complete_workflow() so that executions fail despite not having an error payload
    • Confirmed against the real backend service that failed invocations without error payloads should result in a failed execution
    • Prior to this change, failed invocations with no error payloads would result in a successful execution
  • Add tests covering the following behavior (requested in [Bug]: FAILED invocation without ErrorObject is completed as SUCCEEDED by local emulator #656):
    • A failed invocation results in a failed execution
    • Caught Child context failures do not cause execution failure

Testing:

  • Ran all tests in package via hatch run test:all
  • Invoked SAM CLI with local emulator changes and a reproducing Java durable function. This confirms failed invocations with empty error payloads result in failed execution:
sam local invoke WaitForConditionChildFailureJavaFunction

...

Execution Summary:
=========================
ARN:      31d25a2e-2e37-4bea-b67e-02441a5fa80c/55c89648-9aca-4b8e-8147-f01658dabf0a                                                                           Name:     N/A                                                                  Duration: 6.58s
Status:   FAILED ❌
Input:    {}

Commands you can use next
=========================
[*] Get execution details: sam local execution get 31d25a2e-2e37-4bea-b67e-02441a5fa80c/55c89648-9aca-4b8e-8147-f01658dabf0a
[*] View execution history: sam local execution history 31d25a2e-2e37-4bea-b67e-02441a5fa80c/55c89648-9aca-4b8e-8147-f01658dabf0a

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@hln33
hln33 deployed to ai-pr-review-runtime August 25, 2026 22:56 — with GitHub Actions Active
@hln33
hln33 deployed to ai-pr-review-runtime August 25, 2026 22:56 — with GitHub Actions Active
@hln33
hln33 deployed to ai-pr-review-runtime August 25, 2026 22:56 — with GitHub Actions Active
@hln33
hln33 marked this pull request as draft August 25, 2026 22:56
@hln33
hln33 force-pushed the fix/656-emulator-null-invocation-error-causes-successful-execution branch from f4129a2 to b6a604a Compare August 25, 2026 22:59
@hln33
hln33 force-pushed the fix/656-emulator-null-invocation-error-causes-successful-execution branch from b6a604a to 861756a Compare August 25, 2026 23:20
@hln33
hln33 force-pushed the fix/656-emulator-null-invocation-error-causes-successful-execution branch from 861756a to 09acaf6 Compare August 25, 2026 23:22
@hln33
hln33 deployed to ai-pr-review August 25, 2026 23:22 — with GitHub Actions Active
@hln33
hln33 had a problem deploying to ai-pr-review-runtime August 25, 2026 23:35 — with GitHub Actions Failure
@hln33
hln33 deployed to ai-pr-review-runtime August 25, 2026 23:35 — with GitHub Actions Active
self._complete_workflow(
execution_arn, result=None, error=response.error
)
self._fail_workflow(execution_arn, response.error)

This comment was marked as outdated.

@hln33 hln33 Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one small obstacle to doing that. There currently isn't anything in the public API of DurableFunctionTestRunner or DurableFunctionTestResult that exposes an execution's status in the emulator. The public runner result only exposes the returned durable invocation status/result, not execution.

We could use private internals, like DurableFunctionTestRunner._executor, to get execution information, but that feels hacky to me for an e2e style test.

If we want to add an e2e test for this case, then I think we should first add a small public runner accessor for execution details/status, then use that in the test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should expose a method for the overall execution status on the test runner here. Do we have this in the TS testing library? Thinking something like result.execution_status?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice idea @bchampp :-)

result.execution_status sourced from close_status

@github-actions

This comment has been minimized.

@hln33
hln33 marked this pull request as ready for review August 26, 2026 00:24
@hln33
hln33 deployed to ai-pr-review-runtime August 26, 2026 00:24 — with GitHub Actions Active
@hln33
hln33 deployed to ai-pr-review-runtime August 26, 2026 00:24 — with GitHub Actions Active
@hln33
hln33 had a problem deploying to ai-pr-review-runtime August 26, 2026 00:24 — with GitHub Actions Failure
@github-actions

Copy link
Copy Markdown
Contributor

Codex AI review

No actionable findings. Tests were not run per review constraints; residual risk is limited to unexecuted runtime integration coverage.

Reviewed commit 09acaf6e6ed15e233c6a5deb0bcbda15034fe888. Workflow run

self._complete_workflow(
execution_arn, result=None, error=response.error
)
self._fail_workflow(execution_arn, response.error)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should expose a method for the overall execution status on the test runner here. Do we have this in the TS testing library? Thinking something like result.execution_status?

@yaythomas yaythomas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice, thank you! just some nits.

stored = store.load(execution.durable_execution_arn)
assert stored.is_complete is True
assert stored.close_status is not None
assert stored.close_status.value == "FAILED"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can compare enums directly

assert stored.close_status is ExecutionStatus.FAILED

assert stored.close_status is not None
assert stored.close_status.value == "FAILED"
assert stored.result is not None
assert stored.result.status == InvocationStatus.FAILED

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert stored.result.status is InvocationStatus.FAILED

assert result.result == json.dumps("handled")

child_op: ContextOperation = result.get_context("failing-child")
assert child_op.status == OperationStatus.FAILED

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

child_op.status is OperationStatus.FAILED

execution.complete_fail(None)

assert execution.is_complete is True
assert execution.result.status == InvocationStatus.FAILED

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execution.result.status is InvocationStatus.FAILED

self._complete_workflow(
execution_arn, result=None, error=response.error
)
self._fail_workflow(execution_arn, response.error)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice idea @bchampp :-)

result.execution_status sourced from close_status

self.complete_execution(execution_arn, result)

def on_failed(self, execution_arn: str, error: ErrorObject) -> None:
def on_failed(self, execution_arn: str, error: ErrorObject | None) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type widening is right, and it exposes a pre-existing emulator divergence... Common typing W.

The checkpoint EXECUTION FAIL processor (checkpoint/processors/execution.py) synthesizes a generic ErrorObject when the update has no error, but it should instead preserve null there.

Proposal: remove the synthetic fallback, and widen ExecutionNotifier.notify_failed + ExecutionObserver.on_failed to ErrorObject | None to match the widened null signature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants