Skip to content

Add unit tests and extract startup form logic into testable services#10

Merged
oising merged 2 commits intoaspirifyfrom
copilot/sub-pr-8-again
Feb 11, 2026
Merged

Add unit tests and extract startup form logic into testable services#10
oising merged 2 commits intoaspirifyfrom
copilot/sub-pr-8-again

Conversation

Copy link

Copilot AI commented Feb 11, 2026

Addresses review feedback to add unit test coverage for startup form functionality and refactor logic into DI-injectable services for easier testing.

Changes

  • New test project: ProjectCommander.UnitTests with xUnit 2.9.3 (v3 not yet stable) covering:

    • Startup form state management and completion
    • WaitForStartupFormAsync blocking behavior
    • Form validation and error handling
    • Resource name parsing (e.g., "datagenerator-abc123""datagenerator")
  • Service extraction:

    • IStartupFormService: Encapsulates startup form state and operations
    • IResourceNameParser: Handles resource name string parsing logic
  • DI integration: Updated AspireProjectCommanderClientWorker and ProjectCommanderHub to consume services via constructor injection

Example

// Before: Logic embedded in worker
_isStartupFormRequired = Environment.GetEnvironmentVariable("...") == "true";

// After: Service-based approach
startupFormService.SetStartupFormRequired(isRequired);

All 17 tests pass. No Aspire integration testing used.


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

…to services

Co-authored-by: oising <1844001+oising@users.noreply.github.com>
Copilot AI changed the title [WIP] Add xUnit 3 test project and refactor for easier testing Add unit tests and extract startup form logic into testable services Feb 11, 2026
Copilot AI requested a review from oising February 11, 2026 15:58
@oising oising marked this pull request as ready for review February 11, 2026 17:43
Copilot AI review requested due to automatic review settings February 11, 2026 17:43
@oising oising merged commit c64f688 into aspirify Feb 11, 2026
1 check passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors startup form handling and resource-name parsing into injectable services to improve testability, and adds a new xUnit unit test project to cover the extracted logic.

Changes:

  • Introduces IStartupFormService + StartupFormService and wires it into the client worker via DI.
  • Introduces IResourceNameParser + ResourceNameParser and injects it into ProjectCommanderHub.
  • Adds ProjectCommander.UnitTests with unit tests for startup form service behavior and resource name parsing.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
Src/Nivot.Aspire.ProjectCommander/StartupFormService.cs New service encapsulating startup form state + async wait behavior.
Src/Nivot.Aspire.ProjectCommander/IStartupFormService.cs Public interface for startup form operations/state.
Src/Nivot.Aspire.ProjectCommander/AspireProjectCommanderClientWorker.cs Replaces embedded startup-form logic with injected service.
Src/Nivot.Aspire.ProjectCommander/ServiceCollectionAspireProjectCommanderExtensions.cs Registers IStartupFormService in DI.
Src/Nivot.Aspire.ProjectCommander/Nivot.Aspire.ProjectCommander.csproj Adds InternalsVisibleTo for unit testing and mocking.
Src/Nivot.Aspire.Hosting.ProjectCommander/ResourceNameParser.cs New parser service for deriving “base” resource names.
Src/Nivot.Aspire.Hosting.ProjectCommander/IResourceNameParser.cs Public interface for resource name parsing.
Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHub.cs Injects and uses IResourceNameParser instead of inline splitting.
Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHubResource.cs Registers IResourceNameParser in the hub host DI container.
Src/Nivot.Aspire.Hosting.ProjectCommander/Nivot.Aspire.Hosting.ProjectCommander.csproj Adds InternalsVisibleTo for unit testing and mocking.
ProjectCommander.UnitTests/StartupFormServiceTests.cs Adds unit tests for startup-form state and wait/cancel behavior.
ProjectCommander.UnitTests/ResourceNameParserTests.cs Adds unit tests for resource name parsing behavior.
ProjectCommander.UnitTests/ProjectCommander.UnitTests.csproj New unit test project with xUnit + Moq dependencies and project refs.
ProjectCommander.sln Adds the new unit test project to the solution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +11 to +15
private readonly TaskCompletionSource<Dictionary<string, string?>> _completionSource = new();

private bool _isStartupFormRequired;
private bool _isStartupFormCompleted;
private Dictionary<string, string?>? _startupFormData;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

StartupFormService is registered as a singleton and its state (_isStartupFormRequired, _isStartupFormCompleted, _startupFormData) can be read/written from different threads (SignalR callbacks vs. callers awaiting). Consider making the state updates thread-safe (e.g., locking or using Volatile/Interlocked) and creating the TaskCompletionSource with TaskCreationOptions.RunContinuationsAsynchronously to avoid running continuations inline on the completing thread.

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +78
// Wait for the form to be completed
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

var completedTask = await Task.WhenAny(
_completionSource.Task,
Task.Delay(Timeout.Infinite, cts.Token));

if (completedTask == _completionSource.Task)
{
return await _completionSource.Task;
}

// Cancelled
cancellationToken.ThrowIfCancellationRequested();
return null;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

The Task.Delay(Timeout.Infinite, ...) + Task.WhenAny(...) pattern is heavier than needed and leaves an extra task pending until cancellation. Since this targets modern .NET, consider awaiting the completion task with cancellation directly (e.g., WaitAsync(cancellationToken)) to reduce allocations and simplify the flow.

Suggested change
// Wait for the form to be completed
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var completedTask = await Task.WhenAny(
_completionSource.Task,
Task.Delay(Timeout.Infinite, cts.Token));
if (completedTask == _completionSource.Task)
{
return await _completionSource.Task;
}
// Cancelled
cancellationToken.ThrowIfCancellationRequested();
return null;
// Wait for the form to be completed with cancellation support
return await _completionSource.Task.WaitAsync(cancellationToken);

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +19
// Split on first hyphen to extract base name
// Example: "datagenerator-abc123" -> "datagenerator"
var parts = resourceName.Split('-', 2);
return parts[0];
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

GetBaseResourceName splits on the first hyphen, which breaks resource lookup when the base Aspire resource name itself contains hyphens. For example, a project named eventhub-datagenerator will report itself as eventhub-datagenerator-<suffix> and this implementation would incorrectly return eventhub. Consider stripping only the replica suffix by splitting on the last hyphen (or otherwise detecting/removing the OTEL suffix) so base names with hyphens remain intact.

Suggested change
// Split on first hyphen to extract base name
// Example: "datagenerator-abc123" -> "datagenerator"
var parts = resourceName.Split('-', 2);
return parts[0];
// Strip only the replica/OTEL suffix by removing the segment after the last hyphen.
// Examples:
// "datagenerator-abc123" -> "datagenerator"
// "eventhub-datagenerator-abc123" -> "eventhub-datagenerator"
var lastHyphenIndex = resourceName.LastIndexOf('-');
if (lastHyphenIndex <= 0)
{
// No hyphen or hyphen at the start; treat the whole string as the base name.
return resourceName;
}
return resourceName[..lastHyphenIndex];

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +19
[InlineData("my-service-12345", "my")]
[InlineData("singlename", "singlename")]
[InlineData("resource-with-multiple-hyphens-123", "resource")]
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

These test expectations lock in the current “split on first hyphen” behavior, which will fail for valid base resource names containing hyphens (e.g., my-service-12345 would parse to my). Once the parser is updated to strip only the replica suffix (typically after the last hyphen), update these InlineData expectations accordingly.

Suggested change
[InlineData("my-service-12345", "my")]
[InlineData("singlename", "singlename")]
[InlineData("resource-with-multiple-hyphens-123", "resource")]
[InlineData("my-service-12345", "my-service")]
[InlineData("singlename", "singlename")]
[InlineData("resource-with-multiple-hyphens-123", "resource-with-multiple-hyphens")]

Copilot uses AI. Check for mistakes.
{
// Arrange
_service.SetStartupFormRequired(true);
var cts = new CancellationTokenSource();
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

Disposable 'CancellationTokenSource' is created but not disposed.

Suggested change
var cts = new CancellationTokenSource();
using var cts = new CancellationTokenSource();

Copilot uses AI. Check for mistakes.
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