From 30f608c53e78b877ac211d30c00caf03162f644d Mon Sep 17 00:00:00 2001 From: Eneko Retolaza <73554201+enekoreto@users.noreply.github.com> Date: Sun, 1 Nov 2026 20:48:14 +0100 Subject: [PATCH 1/2] fix(core): start at the default entrypoint when resume_at_next_action is False --- burr/core/application.py | 5 +-- tests/core/test_application.py | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/burr/core/application.py b/burr/core/application.py index 91ef8b1b6..cb19fe95f 100644 --- a/burr/core/application.py +++ b/burr/core/application.py @@ -2707,8 +2707,9 @@ def _init_state_from_persister( self.start = last_position self.reset_to_entrypoint() else: - # self.start is already set to the default. We don't need to do anything. - pass + # self.start is already set to the default, but the loaded state still has the + # prior step, which would otherwise decide the next action. Clear it. + self.reset_to_entrypoint() def _load_from_sync_persister(self): """Loads from the set sync persister and into this current object. diff --git a/tests/core/test_application.py b/tests/core/test_application.py index efb7d205b..ba4d95a58 100644 --- a/tests/core/test_application.py +++ b/tests/core/test_application.py @@ -67,6 +67,7 @@ BaseStateLoader, BaseStatePersister, DevNullPersister, + InMemoryPersister, PersistedStateData, PersisterHookAsync, SQLLitePersister, @@ -4454,3 +4455,59 @@ def noop(state: State) -> State: app = builder.build() assert app.state["x"] == 100 + + +@pytest.mark.parametrize( + "resume_at_next_action,expected_next_action", + [(True, "result"), (False, "counter")], +) +def test_initialize_from_resume_at_next_action(resume_at_next_action, expected_next_action): + """resume_at_next_action=False should start at the default entrypoint, keeping the loaded state""" + persister = InMemoryPersister() + + def build(**initialize_kwargs): + return ( + ApplicationBuilder() + .with_actions(counter=base_counter_action, result=Result("count")) + .with_transitions(("counter", "result", default)) + .initialize_from( + persister, default_state={}, default_entrypoint="counter", **initialize_kwargs + ) + .with_identifiers(app_id="app", partition_key="pk") + .with_state_persister(persister) + .build() + ) + + build(resume_at_next_action=True).step() + app = build(resume_at_next_action=resume_at_next_action) + assert app.get_next_action().name == expected_next_action + assert app.state["count"] == 1 + + +@pytest.mark.parametrize( + "resume_at_next_action,expected_next_action", + [(True, "result"), (False, "counter")], +) +async def test_initialize_from_resume_at_next_action_async( + resume_at_next_action, expected_next_action +): + """Same as above, for the async builder""" + persister = AsyncInMemoryPersister() + + async def abuild(**initialize_kwargs): + return await ( + ApplicationBuilder() + .with_actions(counter=base_counter_action_async, result=Result("count")) + .with_transitions(("counter", "result", default)) + .initialize_from( + persister, default_state={}, default_entrypoint="counter", **initialize_kwargs + ) + .with_identifiers(app_id="app", partition_key="pk") + .with_state_persister(persister) + .abuild() + ) + + await (await abuild(resume_at_next_action=True)).astep() + app = await abuild(resume_at_next_action=resume_at_next_action) + assert app.get_next_action().name == expected_next_action + assert app.state["count"] == 1 From 7efef1068dc1c9d4256bce8da470e77e3ddb59c6 Mon Sep 17 00:00:00 2001 From: Eneko Retolaza <73554201+enekoreto@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:32:30 +0100 Subject: [PATCH 2/2] test(core): cover a failed last step in the resume_at_next_action tests --- tests/core/test_application.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/tests/core/test_application.py b/tests/core/test_application.py index ba4d95a58..8f2d7c81d 100644 --- a/tests/core/test_application.py +++ b/tests/core/test_application.py @@ -4457,18 +4457,23 @@ def noop(state: State) -> State: assert app.state["x"] == 100 +@pytest.mark.parametrize("last_step_failed", [False, True]) @pytest.mark.parametrize( "resume_at_next_action,expected_next_action", [(True, "result"), (False, "counter")], ) -def test_initialize_from_resume_at_next_action(resume_at_next_action, expected_next_action): - """resume_at_next_action=False should start at the default entrypoint, keeping the loaded state""" +def test_initialize_from_resume_at_next_action( + resume_at_next_action, expected_next_action, last_step_failed +): + """resume_at_next_action=False should start at the default entrypoint, keeping the loaded state, + whether the last saved step completed or failed""" persister = InMemoryPersister() + result = base_broken_action if last_step_failed else Result("count") def build(**initialize_kwargs): return ( ApplicationBuilder() - .with_actions(counter=base_counter_action, result=Result("count")) + .with_actions(counter=base_counter_action, result=result) .with_transitions(("counter", "result", default)) .initialize_from( persister, default_state={}, default_entrypoint="counter", **initialize_kwargs @@ -4479,25 +4484,32 @@ def build(**initialize_kwargs): ) build(resume_at_next_action=True).step() + if last_step_failed: + with pytest.raises(BrokenStepException): + build(resume_at_next_action=True).step() app = build(resume_at_next_action=resume_at_next_action) assert app.get_next_action().name == expected_next_action assert app.state["count"] == 1 + if not resume_at_next_action: + assert PRIOR_STEP not in app.state.get_all() +@pytest.mark.parametrize("last_step_failed", [False, True]) @pytest.mark.parametrize( "resume_at_next_action,expected_next_action", [(True, "result"), (False, "counter")], ) async def test_initialize_from_resume_at_next_action_async( - resume_at_next_action, expected_next_action + resume_at_next_action, expected_next_action, last_step_failed ): """Same as above, for the async builder""" persister = AsyncInMemoryPersister() + result = base_broken_action_async if last_step_failed else Result("count") async def abuild(**initialize_kwargs): return await ( ApplicationBuilder() - .with_actions(counter=base_counter_action_async, result=Result("count")) + .with_actions(counter=base_counter_action_async, result=result) .with_transitions(("counter", "result", default)) .initialize_from( persister, default_state={}, default_entrypoint="counter", **initialize_kwargs @@ -4508,6 +4520,11 @@ async def abuild(**initialize_kwargs): ) await (await abuild(resume_at_next_action=True)).astep() + if last_step_failed: + with pytest.raises(BrokenStepException): + await (await abuild(resume_at_next_action=True)).astep() app = await abuild(resume_at_next_action=resume_at_next_action) assert app.get_next_action().name == expected_next_action assert app.state["count"] == 1 + if not resume_at_next_action: + assert PRIOR_STEP not in app.state.get_all()