Skip to content

[ADR-127] Address code review: learning mode guard, timeout, markdown rendering, service fixes#125

Draft
Copilot wants to merge 3 commits intodev/jodavis/ADR-128-spec-programmable-ir-commandsfrom
copilot/sub-pr-96-again
Draft

[ADR-127] Address code review: learning mode guard, timeout, markdown rendering, service fixes#125
Copilot wants to merge 3 commits intodev/jodavis/ADR-128-spec-programmable-ir-commandsfrom
copilot/sub-pr-96-again

Conversation

Copy link
Contributor

Copilot AI commented Mar 20, 2026

Addresses review feedback on the ADR-127 programmable IR commands implementation. Issues fixed across learning mode lifecycle, Broadlink polling, modal message rendering, and test infrastructure.

Concurrent learning cycle guard (BroadlinkCommandService)

A second ProgramAsync call while a learning cycle is already in progress is now blocked at the service level via a SemaphoreSlim(1,1). The call logs LearningAlreadyInProgress and returns immediately without interacting with the device. The semaphore is released in a try-finally wrapping the entire ShowMessageAsync call, preventing deadlock if ShowMessageAsync throws before executing the body. New unit test (BroadlinkCommandService_ProgramAsync_WhileLearningAlreadyInProgress_ReturnsImmediately) validates this.

Learning mode re-entry guard (LifecycleViewController)

EnterLearningMode returns the existing token if already in learning mode, preventing a new CTS from being created while a learning operation is in progress.

Overall learning timeout (BroadlinkCommandService + BroadlinkSettings)

The polling loop had no upper bound. Added LearnTimeout (default 120 s — intentionally much longer than the device's own hardware timeout). User cancellation propagates as OperationCanceledException; timeout faults with TimeoutException.

Markdown rendering in ModalMessageUI

Modal messages are rendered as HTML via Markdig. The markdown-to-HTML conversion is encapsulated in a CurrentMessageHtml property (null-safe: returns null when CurrentMessage is null, making it easy to set breakpoints and inspect translations in the debugger). Component implements IDisposable to properly unsubscribe PropertyChanged. E2E feature assertions updated to check rendered HTML markup via Gherkin docstrings:

Then I should see a modal message containing
    """
    <h1 id="programming-down">Programming 'Down'</h1>
    """

A new GetInnerHtmlFromElementWithCssClassAsync Playwright method, WaitForModalMessageMarkupContaining extension, and docstring step binding support these assertions.

ModalMessageService.ShowMessageAsyncWriteAsync over TryWrite

TryWrite silently dropped messages on a closed channel. Replaced with WriteAsync so ChannelClosedException propagates to callers after Dispose().

Smaller fixes

  • PlaywrightButtonTestObject.cs: removed spurious using static Google.Protobuf… import
  • PlaywrightUITestService.cs: corrected exception message (IBrowserProviderIBrowserUIAccess)
  • _spec_ProgrammableCommands.md: replaced UTF-8 replacement character with em dash
  • _doc_ProgrammaticSettings.md: corrected section/key format — keys under [IRData] are plain command names (e.g. Power, VolumeUp) without the IRData: prefix

Tests

  • LifecycleViewControllerTests: 17 tests covering the entire class — constructor, SetPhase (all phases), StartTask, SetFatalError, and all learning mode methods
  • BroadlinkCommandServiceTests: concurrent learning rejection, timeout vs. cancellation distinction
  • ModalMessageServiceTests: post-dispose fault

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

…wn rendering, and service fixes

Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
Agent-Logs-Url: https://github.com/jodavis/AdaptiveRemote/sessions/e12bbbf8-d0f1-4410-9fbf-90b4a7360994
Copilot AI changed the title [WIP] [ADR-127] Programmable IR commands [ADR-127] Address code review: learning mode guard, timeout, markdown rendering, service fixes Mar 20, 2026
Copilot AI requested a review from jodavis March 20, 2026 19:47
VolumeUp = AABgA6gDAwQFBgcICQoLDA0ODw==
VolumeDown = AABgA6gDAwQFBgcICQoLDA0ODw==
[IRData]
IRData:Power = AABgA6gDAwQFBgcICQoLDA0ODw==
Copy link
Owner

Choose a reason for hiding this comment

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

The IRData prefix is not required. Having it in the IRData section gives it the prefix when reading settings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in fe0ca00. The example now shows plain command names (Power, VolumeUp, VolumeDown) under [IRData] without the IRData: prefix. Also removed the prefix mention from the Notes section.

namespace AdaptiveRemote.Services.Lifecycle;

[TestClass]
public class LifecycleViewControllerTests
Copy link
Owner

Choose a reason for hiding this comment

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

Were there not any existing LifecycleViewController tests? If not, the whole class needs to have unit tests written. But I thought there were.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There were no pre-existing tests for LifecycleViewController. fe0ca00 adds comprehensive coverage for the whole class: constructor (ShowError/CloseError commands), SetPhase (all 6 phases via [DataTestMethod]), StartTask (single activity, multiple/ordering behavior, description update, disposal), SetFatalError (phase transition, ShowBrowser/ShowButtonPanel, error via activity), and the full EnterLearningMode/ExitLearningModeAsync suite. Total: 17 tests.

And I should see the 'Volume Down' button is not programmed
When I click on the 'Volume Down' button
Then I should see a modal message containing "# Programming 'Down'"
Then I should see a modal message containing "Programming 'Down'"
Copy link
Owner

Choose a reason for hiding this comment

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

Modify this to check for the expected rendered markup for the message.

Then I should see a modal message containing
    """
    <h1>Programming 'Down</h1>
    """

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in fe0ca00. The feature file now uses Gherkin docstrings to check for rendered HTML, e.g.:

Then I should see a modal message containing
    """
    <h1 id="programming-down">Programming 'Down'</h1>
    """

A new GetInnerHtmlFromElementWithCssClassAsync Playwright method and WaitForModalMessageMarkupContaining extension power this check via a new Then I should see a modal message containing docstring step definition.

Comment on lines +41 to +45
CancellationTokenSource? existing = _learningCts;
if (existing is not null)
{
return existing.Token;
}
Copy link
Owner

Choose a reason for hiding this comment

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

This isn't sufficient, because it will prevent a new token from being created, but it won't stop a new learning cycle from starting. The learning cycle in BroadlinkCommandService must be prevented if there is already one in progress.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in fe0ca00. BroadlinkCommandService.CreateProgramHandler now uses a SemaphoreSlim(1,1) guard: await _learningSemaphore.WaitAsync(0, cancellationToken) is called before entering any learning work. If the semaphore can't be acquired (another learning operation is in progress), the method logs LearningAlreadyInProgress and returns immediately without calling ShowMessageAsync or interacting with the device. The semaphore release is in a try-finally wrapping the entire ShowMessageAsync call (not just the body) so it's always released even if ShowMessageAsync throws. A new unit test (BroadlinkCommandService_ProgramAsync_WhileLearningAlreadyInProgress_ReturnsImmediately) validates this behavior.

@if (CurrentMessage is not null)
{
<div class="conversation-speaking-message"><span>"@CurrentMessage"</span></div>
<div class="conversation-speaking-message">@((MarkupString)RenderMarkdown(CurrentMessage))</div>
Copy link
Owner

Choose a reason for hiding this comment

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

What happens if CurrentMessage is null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When CurrentMessage is null, the new CurrentMessageHtml property returns null (it's CurrentMessage is not null ? RenderMarkdown(CurrentMessage) : null). The @if (CurrentMessageHtml is not null) guard then prevents the <div> from rendering at all.

@if (CurrentMessage is not null)
{
<div class="conversation-speaking-message"><span>"@CurrentMessage"</span></div>
<div class="conversation-speaking-message">@((MarkupString)RenderMarkdown(CurrentMessage))</div>
Copy link
Owner

Choose a reason for hiding this comment

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

I would like to have the markdown translation encapsulated in another property on the class. It's easier to set breakpoints and view translation in the debugger that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in fe0ca00. The markdown conversion is now in CurrentMessageHtml:

private string? CurrentMessageHtml => CurrentMessage is not null ? RenderMarkdown(CurrentMessage) : null;

The template uses @if (CurrentMessageHtml is not null) and renders @((MarkupString)CurrentMessageHtml).

…ViewController tests, HTML modal assertion

Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
Agent-Logs-Url: https://github.com/jodavis/AdaptiveRemote/sessions/90db648e-1f4f-467b-87db-6eb1af63fb53
Copilot AI requested a review from jodavis March 21, 2026 04:29
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.

2 participants