From 09a56d290f280fb01425a74e4b81d0ba40102d90 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Fri, 5 Jun 2026 17:29:58 +0200 Subject: [PATCH 1/3] test: accept platform auto-abort in test_actor_charge_limit Hitting max_total_charge_usd makes the platform auto-abort the run, which races with the Actor's clean exit, so the terminal status is non-deterministically SUCCEEDED or ABORTED; accept both. --- tests/e2e/test_actor_charge.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/e2e/test_actor_charge.py b/tests/e2e/test_actor_charge.py index 664c32ae..53c08ed2 100644 --- a/tests/e2e/test_actor_charge.py +++ b/tests/e2e/test_actor_charge.py @@ -142,15 +142,20 @@ async def test_actor_charge_limit( ) -> None: run = await run_actor(ppe_actor, max_total_charge_usd=Decimal('0.2')) + # Reaching `max_total_charge_usd` makes the platform abort the run automatically, and that abort races with the + # Actor's own clean exit — so the terminal status is either SUCCEEDED or ABORTED. Both are valid here; the + # behavior under test is that the charge limit capped the run at exactly 2 of the 4 attempted events. + terminal_statuses = {ActorJobStatus.SUCCEEDED, ActorJobStatus.ABORTED} + # Refetch until the charged event counts propagate on the platform. run = await poll_until_condition( partial(_get_run, apify_client_async, run.id), - lambda r: r.status == ActorJobStatus.SUCCEEDED and r.charged_event_counts == {'foobar': 2}, + lambda r: r.status in terminal_statuses and r.charged_event_counts == {'foobar': 2}, timeout=30, poll_interval=1, ) - assert run.status == ActorJobStatus.SUCCEEDED + assert run.status in terminal_statuses assert run.charged_event_counts == {'foobar': 2} From e2e50c5949767b2ee5e441a48cbb2b41bab4c5f9 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Fri, 5 Jun 2026 17:32:20 +0200 Subject: [PATCH 2/3] test: tighten flake comment in test_actor_charge_limit --- tests/e2e/test_actor_charge.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/e2e/test_actor_charge.py b/tests/e2e/test_actor_charge.py index 53c08ed2..1cd818b6 100644 --- a/tests/e2e/test_actor_charge.py +++ b/tests/e2e/test_actor_charge.py @@ -142,9 +142,8 @@ async def test_actor_charge_limit( ) -> None: run = await run_actor(ppe_actor, max_total_charge_usd=Decimal('0.2')) - # Reaching `max_total_charge_usd` makes the platform abort the run automatically, and that abort races with the - # Actor's own clean exit — so the terminal status is either SUCCEEDED or ABORTED. Both are valid here; the - # behavior under test is that the charge limit capped the run at exactly 2 of the 4 attempted events. + # Reaching `max_total_charge_usd` makes the platform auto-abort the run, racing with the Actor's clean exit, so + # the terminal status is either SUCCEEDED or ABORTED. What matters is that the limit capped it at 2 events. terminal_statuses = {ActorJobStatus.SUCCEEDED, ActorJobStatus.ABORTED} # Refetch until the charged event counts propagate on the platform. From 97e00fe2bd3d1f4176fe79ae5d53fdb510211bd3 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Mon, 8 Jun 2026 13:56:02 +0200 Subject: [PATCH 3/3] test: make ppe_actor block on charge limit so run aborts deterministically --- tests/e2e/test_actor_charge.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/e2e/test_actor_charge.py b/tests/e2e/test_actor_charge.py index 1cd818b6..96e70eff 100644 --- a/tests/e2e/test_actor_charge.py +++ b/tests/e2e/test_actor_charge.py @@ -74,6 +74,7 @@ async def ppe_push_data_actor( @pytest_asyncio.fixture(scope='module', loop_scope='module') async def ppe_actor_build(make_actor: MakeActorFunction) -> str: async def main() -> None: + import asyncio from dataclasses import asdict async with Actor: @@ -83,6 +84,12 @@ async def main() -> None: ) Actor.log.info('Charged', extra=asdict(charge_result)) + # When the charge limit is reached, the platform auto-aborts this run. That abort races with the + # Actor's own clean exit, making the terminal status non-deterministic (SUCCEEDED or ABORTED). Block + # here so the abort always wins and the run ends deterministically as ABORTED. + if charge_result.event_charge_limit_reached: + await asyncio.Event().wait() + actor_client = await make_actor('ppe', main_func=main) await actor_client.update( @@ -142,19 +149,17 @@ async def test_actor_charge_limit( ) -> None: run = await run_actor(ppe_actor, max_total_charge_usd=Decimal('0.2')) - # Reaching `max_total_charge_usd` makes the platform auto-abort the run, racing with the Actor's clean exit, so - # the terminal status is either SUCCEEDED or ABORTED. What matters is that the limit capped it at 2 events. - terminal_statuses = {ActorJobStatus.SUCCEEDED, ActorJobStatus.ABORTED} - + # Reaching `max_total_charge_usd` makes the platform auto-abort the run. The Actor blocks after hitting the + # limit (see `ppe_actor_build`) so the abort always wins the race against its clean exit, hence ABORTED. # Refetch until the charged event counts propagate on the platform. run = await poll_until_condition( partial(_get_run, apify_client_async, run.id), - lambda r: r.status in terminal_statuses and r.charged_event_counts == {'foobar': 2}, + lambda r: r.status == ActorJobStatus.ABORTED and r.charged_event_counts == {'foobar': 2}, timeout=30, poll_interval=1, ) - assert run.status in terminal_statuses + assert run.status == ActorJobStatus.ABORTED assert run.charged_event_counts == {'foobar': 2}