Skip to content
Open
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
7 changes: 7 additions & 0 deletions doc/contributing/4_running_tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ CI workers. These are deadlock backstops, not expected runtimes. Production dead
are unchanged. Deadline and cancellation tests use explicit controls and keep their
separate timing assertions.

Async correctness tests can use `TEST_WATCHDOG_SECONDS` from `tests/unit/async_utils.py`
for a finite 300-second deadlock backstop. Tests that start background tasks must
release any test-controlled waits and join their tasks before removing mocks or
disposing fixtures, including when a watchdog expires. When waiting for a task to
signal readiness, also watch for that task finishing so an early exception is not
hidden by the readiness timeout.

The discovery provider-state test and Copilot cancellation-test startup use the
shared `TEST_WATCHDOG_SECONDS` backstop in `tests/unit/async_utils.py`. Discovery
timeout coverage expires the real cancellation path only after the request starts.
Expand Down
31 changes: 26 additions & 5 deletions tests/unit/backend/test_conversation_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from pyrit.models.target.target_capabilities import TargetCapabilities
from pyrit.prompt_target import OpenAIResponseTarget
from pyrit.score.scorer_prompt_validator import ScorerPromptValidator
from unit.async_utils import TEST_WATCHDOG_SECONDS, wait_for_completion_async
from unit.backend.mocks import _settle_send_async, message_send_lifecycle_async
from unit.mocks import MockPromptTarget, get_mock_target_identifier, run_memory_session_async

Expand Down Expand Up @@ -1305,11 +1306,31 @@ async def interrupted_write_async(*args: Any, **kwargs: Any) -> None:

with patch.object(storage, "write_file_async", side_effect=interrupted_write_async):
task = asyncio.create_task(AttackService().save_conversation_async(request=request))
await asyncio.wait_for(started.wait(), timeout=5)
if cancel:
task.cancel()
started_task = asyncio.create_task(started.wait())
try:
done, _ = await asyncio.wait(
{task, started_task},
timeout=TEST_WATCHDOG_SECONDS,
return_when=asyncio.FIRST_COMPLETED,
)
if not done:
raise TimeoutError("The media write did not start before the test watchdog expired.")
if not started.is_set():
await task
pytest.fail("The conversation save finished without writing media.")
if cancel:
task.cancel()
finish.set()
with pytest.raises(asyncio.CancelledError if cancel else OSError):
await wait_for_completion_async(future=task)
finally:
if not task.done():
task.cancel()
# The shielded writer must finish before the save task can clean up.
finish.set()
with pytest.raises(asyncio.CancelledError if cancel else OSError):
await task
started_task.cancel()
await wait_for_completion_async(
future=asyncio.gather(task, started_task, return_exceptions=True),
)
assert not list(Path(sqlite_instance.results_path).rglob("*.txt"))
assert (await sqlite_instance.get_attack_results_async()) == []
Loading