-
Notifications
You must be signed in to change notification settings - Fork 3.7k
feat(voice): expose a counted SpeechHandle interruption hold #7203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bharatnpti
wants to merge
2
commits into
livekit:main
Choose a base branch
from
bharatnpti:feat/hold-interruptions-public
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| """``SpeechHandle.hold_interruptions()``: the counted, public interruption hold. | ||
|
|
||
| The count is kept beside the assigned ``allow_interruptions`` value rather than | ||
| overwriting it. Overwriting it is what the private hold used to do, and it lost | ||
| information in both directions: an assignment during a hold defeated the hold, and | ||
| the last release discarded whatever had been assigned since. Both are covered below. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import pytest | ||
|
|
||
| from livekit.agents.voice.speech_handle import SpeechHandle | ||
|
|
||
| pytestmark = pytest.mark.unit | ||
|
|
||
|
|
||
| def _handle(*, allow_interruptions: bool = True) -> SpeechHandle: | ||
| return SpeechHandle.create(allow_interruptions=allow_interruptions) | ||
|
|
||
|
|
||
| async def test_a_hold_makes_the_speech_uninterruptible_and_restores_it() -> None: | ||
| handle = _handle() | ||
|
|
||
| with handle.hold_interruptions() as held: | ||
| assert held is handle | ||
| assert handle.allow_interruptions is False | ||
| assert handle.interruptions_held is True | ||
| with pytest.raises(RuntimeError, match="does not allow interruptions"): | ||
| handle.interrupt() | ||
|
|
||
| assert handle.allow_interruptions is True | ||
| assert handle.interruptions_held is False | ||
| handle.interrupt() | ||
| assert handle.interrupted | ||
|
|
||
|
|
||
| async def test_overlapping_holds_release_only_on_the_last_one() -> None: | ||
| handle = _handle() | ||
|
|
||
| with handle.hold_interruptions(): | ||
| with handle.hold_interruptions(): | ||
| assert handle.allow_interruptions is False | ||
| assert handle.allow_interruptions is False, "the outer holder still holds" | ||
|
|
||
| assert handle.allow_interruptions is True | ||
| handle._mark_done() | ||
|
|
||
|
|
||
| async def test_a_hold_does_not_make_an_uninterruptible_speech_interruptible() -> None: | ||
| """The release restores nothing -- it only stops holding.""" | ||
| handle = _handle(allow_interruptions=False) | ||
|
|
||
| with handle.hold_interruptions(): | ||
| assert handle.allow_interruptions is False | ||
|
|
||
| assert handle.allow_interruptions is False | ||
| handle._mark_done() | ||
|
|
||
|
|
||
| async def test_an_assignment_during_a_hold_cannot_defeat_the_hold() -> None: | ||
| """The regression. The hold used to overwrite ``allow_interruptions``, so assigning | ||
| it back to True during a hold made the speech interruptible again -- the hold was | ||
| silently gone while its holder still believed it was protected.""" | ||
| handle = _handle() | ||
|
|
||
| with handle.hold_interruptions(): | ||
| handle.allow_interruptions = True | ||
|
|
||
| assert handle.allow_interruptions is False, "still held" | ||
| with pytest.raises(RuntimeError, match="does not allow interruptions"): | ||
| handle.interrupt() | ||
|
|
||
| handle._mark_done() | ||
|
|
||
|
|
||
| async def test_an_assignment_during_a_hold_survives_the_release() -> None: | ||
| """And the other direction: the release must not discard it.""" | ||
| handle = _handle() | ||
|
|
||
| with handle.hold_interruptions(): | ||
| handle.allow_interruptions = False | ||
|
|
||
| assert handle.allow_interruptions is False, "the assignment outlives the hold" | ||
| handle._mark_done() | ||
|
|
||
|
|
||
| async def test_a_forced_interrupt_lands_through_a_hold() -> None: | ||
| """A hold survives barge-in; it does not make a speech impossible to stop.""" | ||
| handle = _handle() | ||
|
|
||
| with handle.hold_interruptions(): | ||
| handle.interrupt(force=True) | ||
| assert handle.interrupted | ||
|
|
||
|
|
||
| async def test_a_release_after_a_forced_interrupt_does_not_raise() -> None: | ||
| handle = _handle() | ||
|
|
||
| with handle.hold_interruptions(): | ||
| handle.interrupt(force=True) | ||
|
|
||
| assert handle.interrupted | ||
| assert handle.interruptions_held is False | ||
|
|
||
|
|
||
| async def test_an_already_interrupted_speech_refuses_to_be_held() -> None: | ||
| """Holding a speech that has already been cut off would report protection that | ||
| cannot exist, and leave the holder waiting on a speech that is not playing.""" | ||
| handle = _handle() | ||
| handle.interrupt() | ||
|
|
||
| with pytest.raises(RuntimeError, match="already interrupted"): | ||
| with handle.hold_interruptions(): | ||
| pass | ||
|
|
||
| assert handle.interruptions_held is False, "the failed hold left the count balanced" | ||
| handle._mark_done() | ||
|
|
||
|
|
||
| async def test_a_hold_released_by_an_exception_still_releases() -> None: | ||
| handle = _handle() | ||
|
|
||
| with pytest.raises(ValueError, match="boom"): | ||
| with handle.hold_interruptions(): | ||
| raise ValueError("boom") | ||
|
|
||
| assert handle.interruptions_held is False | ||
| assert handle.allow_interruptions is True | ||
| handle._mark_done() | ||
|
|
||
|
|
||
| async def test_repeated_hold_cycles_do_not_accumulate() -> None: | ||
| handle = _handle() | ||
|
|
||
| for _ in range(3): | ||
| with handle.hold_interruptions(): | ||
| assert handle.allow_interruptions is False | ||
| assert handle.allow_interruptions is True | ||
|
|
||
| assert handle.interruptions_held is False | ||
| handle._mark_done() | ||
|
|
||
|
|
||
| async def test_interruptions_held_is_narrower_than_allow_interruptions() -> None: | ||
| """A speech configured uninterruptible is not a *held* speech, and the realtime | ||
| barge-in path tells them apart to decide whether a refusal is expected.""" | ||
| handle = _handle(allow_interruptions=False) | ||
|
|
||
| assert handle.allow_interruptions is False | ||
| assert handle.interruptions_held is False | ||
| handle._mark_done() |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.