npx skills add ...
npx skills add microsoft/aspire --skill cli-e2e-testing
Use when creating, modifying, debugging, or reviewing Aspire CLI end-to-end tests that use Hex1b terminal automation under tests/Aspire.Cli.EndToEnd.Tests/.
npx skills add microsoft/aspire --skill cli-e2e-testing
This skill provides patterns and practices for writing end-to-end tests for the Aspire CLI using the Hex1b terminal automation library.
CLI E2E tests use the Hex1b library to automate terminal sessions, simulating real user interactions with the Aspire CLI. Tests run in CI with asciinema recordings for debugging.
Location: tests/Aspire.Cli.EndToEnd.Tests/
Supported Platforms: Linux only. Hex1b requires a Linux terminal environment. Tests are configured to skip on Windows and macOS in CI.
Hex1bTerminal: The main terminal class from the Hex1b library for terminal automationHex1bTerminalAutomator: Async/await API for driving a Hex1bTerminal — the preferred approach for new testsHex1bAutomatorTestHelpers (shared helpers): Async extension methods on Hex1bTerminalAutomator (WaitForSuccessPromptAsync, AspireNewAsync, etc.)CliE2EAutomatorHelpers (Helpers/CliE2EAutomatorHelpers.cs): CLI-specific async extension methods on Hex1bTerminalAutomator (PrepareDockerEnvironmentAsync, InstallAspireCliAsync, etc.)CellPatternSearcher: Pattern matching for terminal cell contentSequenceCounter (Helpers/SequenceCounter.cs): Tracks command execution count for deterministic prompt detectionCliE2ETestHelpers (Helpers/CliE2ETestHelpers.cs): Environment variable helpers and terminal factory methodsTemporaryWorkspace: Creates isolated temporary directories for test executionHex1bTerminalInputSequenceBuilder (legacy): Fluent builder API for building sequences of terminal input/output operations. Prefer Hex1bTerminalAutomator for new tests.Each test:
TemporaryWorkspace for isolationHex1bTerminal with headless mode and asciinema recordingHex1bTerminalAutomator wrapping the terminalAlways use CliE2ETestHelpers.StartRun to wrap the terminal run. This returns a TerminalRun (implements IAsyncDisposable) that automatically:
CaptureAspireDiagnosticsAsync (best effort)exit and presses Enter to close the terminalThis eliminates the need for manual exit/await pendingRun at the end of every test and ensures diagnostics are always captured, even when tests fail.
CLI E2E tests run inside Docker containers on Linux. The workflow is: build a portable archive with localhive, then point the tests at it. This is the primary way to iterate on E2E tests during development.
./restore.sh or .\restore.cmd)The archive must match the Docker container's architecture:
| Host | Docker Desktop | RID |
|---|---|---|
| Apple Silicon Mac | Linux arm64 containers | linux-arm64 |
| Intel Mac | Linux x64 containers | linux-x64 |
| Windows (any) | WSL2 Linux x64 | linux-x64 |
| Linux x64 | Native | linux-x64 |
| Linux arm64 | Native | linux-arm64 |
The typical loop when writing or debugging E2E tests:
The CliInstallStrategy class auto-detects how to install the CLI in the test container. You can override via environment variables:
| Env Var | Mode | Example |
|---|---|---|
ASPIRE_E2E_ARCHIVE | LocalHive — extract archive into container | /tmp/aspire-e2e.tar.gz |
ASPIRE_E2E_QUALITY | Install script with quality | dev, staging, release |
ASPIRE_E2E_VERSION | Install script with version | 13.2.1 |
| (none, in CI) | PullRequest — install from PR artifacts | Auto-detected |
| (none, locally) | InstallScript (latest GA) | Auto-detected |
LocalHive (via ASPIRE_E2E_ARCHIVE) is the recommended mode for local development — it uses your locally-built CLI, packages, and bundle so you test exactly what you've changed.
Useful for verifying tests pass against shipped versions or catching regressions:
A set of tests validates the CLI identity sidecar — the ability to make a locally built CLI
emulate a different channel/version via ASPIRE_CLI_* env vars. They form an AppHost-language ×
channel-emulation matrix (one test per language because C# and TypeScript scaffold through different
code paths and have diverged before):
| Class | Channel emulated | Aspire* source | NuGet.config dropped? |
|---|---|---|---|
EmulatedReleasedBuildTests | stable (latest shipped) | nuget.org | No (C# and TS) |
EmulatedStagingBuildTests | staging (latest darc build) | darc-pub-... feed | Yes — darc feed pin |
EmulatedLocalReleaseBuildTests | stable (future, local-only) | local hive via ASPIRE_CLI_PACKAGES | No (C# and TS) |
EmulatedLocalReleaseBuildTests is the all-local "future release" row: it emulates a version
(e.g. 13.5.0) that exists only in a locally built hive, so a successful resolve proves the CLI
consulted ASPIRE_CLI_PACKAGES rather than nuget.org. Run it by building a stable-shaped archive
with localhive --version:
These tests skip unless the CLI was installed from a LocalHive archive and that archive is
stable-shaped (no prerelease suffix). In default CI the archive is a prerelease LocalArchive, so they
skip and add zero CI cost — CI relies on --ignore-exit-code 8 (set in eng/Testing.props
MtpBaseArgs) so an all-skipped class job still passes. The test also registers the hive as an
ambient NuGet source (dotnet nuget add source) because MSBuild resolves the apphost's
Aspire.AppHost.Sdk before restore from nuget.config sources only, ignoring ASPIRE_CLI_PACKAGES.
⚠️ Local rebuilds: isolate the NuGet global cache. This only affects local iteration of a stable-shaped emulation (the E2E tests run in fresh Docker containers, so CI is immune). NuGet's global packages folder (
~/.nuget/packages/<id>/<version>/) caches extracted packages keyed by version. When you emulate a fixed stable version (e.g.13.5.0) and rebuild it, a stale13.5.0in that shared cache silently shadows the freshly built one — same version, different content — so restore drifts (the stale AppHost SDK injects a prerelease floor and you getNU1603warnings binding the graph to a stray13.5.0-pr.…). Fix: pointNUGET_PACKAGESat a per-emulation directory (export NUGET_PACKAGES=/tmp/aspire-localrelease/.nuget-packages).localhive … -o DIR's generatedactivate.sh/activate.ps1already sets this up. See the cache hazard note in.agents/skills/cli-channel-debugging/SKILL.md(Scenario 7c) for the full mechanism.
The SequenceCounter class tracks the number of shell commands executed. This enables deterministic waiting for command completion via a custom shell prompt.
PrepareDockerEnvironmentAsync() configures the shell with a custom prompt: [N OK] $ or [N ERR:code] $ WaitForSuccessPromptAsync(counter) waits for a prompt showing the current count with OKThis approach is more reliable than arbitrary timeouts because it deterministically waits for each command to complete.
Use CellPatternSearcher to find text patterns in terminal output:
Find(string): Literal string matching. Use this for most cases.FindPattern(string): Regex pattern matching. Use only when you need regex features like wildcards.Important: If your search string contains regex special characters like (, ), /, ., *, +, ?, [, ], {, }, ^, $, |, or \, use Find() instead of FindPattern() to avoid regex interpretation.
| Method | Description |
|---|---|
WaitForSuccessPromptAsync(counter, timeout?) | Waits for [N OK] $ prompt, fails immediately if error prompt appears, and increments counter |
WaitForAnyPromptAsync(counter, timeout?) | Waits for any prompt (OK or ERR) and increments counter |
WaitForErrorPromptAsync(counter, timeout?) | Waits for [N ERR:code] $ prompt and increments counter |
RunCommandAsync(command, counter, timeout?) | Types a command, presses Enter, and waits for success prompt (fails fast on error) |
DeclineAgentInitPromptAsync() | Declines the aspire agent init prompt if it appears |
AspireNewAsync(projectName, counter, template?, useRedisCache?) | Runs aspire new interactively, handling template selection, project name, output path, URLs, Redis, and test project prompts |
See AspireNew Helper below for detailed usage.
| Method | Description |
|---|---|
PrepareDockerEnvironmentAsync(counter, workspace) | Sets up Docker container environment with custom prompt and command tracking |
InstallAspireCliAsync(installMode, counter) | Installs the Aspire CLI inside the Docker container |
ClearScreenAsync(counter) | Clears the terminal screen and waits for prompt |
| Method | Description |
|---|---|
IncrementSequence(counter) | Manually increments the counter |
The following extensions on Hex1bTerminalInputSequenceBuilder are still available but should not be used in new tests:
| Method | Description |
|---|---|
WaitForSuccessPrompt(counter, timeout?) | (legacy) Waits for [N OK] $ prompt and increments counter |
PrepareEnvironment(workspace, counter) | (legacy) Sets up custom prompt with command tracking |
SourceAspireBundleEnvironment(counter) | (legacy) Sources bundle PATH environment variables |
Wait for specific output patterns rather than arbitrary delays:
After running shell commands, use WaitForSuccessPromptAsync() to wait for the command to complete:
The AspireNew extension method centralizes the multi-step aspire new interactive flow. Use it instead of manually building the prompt sequence.
| Value | Template | Arrow Keys |
|---|---|---|
Starter (default) | Starter App (Blazor) | None (first option) |
JsReact | Starter App (ASP.NET Core/React) | Down ×1 |
PythonReact | Starter App (FastAPI/React) | Down ×2 |
ExpressReact | Starter App (Express/React) | Down ×3 |
EmptyAppHost | Empty AppHost | Down ×4 |
| Parameter | Default | Description |
|---|---|---|
projectName | (required) | Project name typed at the prompt |
counter | (required) | SequenceCounter for prompt tracking |
template | AspireTemplate.Starter | Which template to select |
useRedisCache | true | Accept Redis (Enter) or decline (Down+Enter). Only applies to Starter, JsReact, PythonReact. |
For aspire new, use the AspireNewAsync helper instead of manually building the prompt sequence:
For other interactive CLI commands, wait for each prompt before responding:
For processes like aspire run that don't exit on their own:
Some operations only apply in CI (like installing CLI from PR artifacts):
Use CliE2ETestHelpers for CI environment variables:
description: on WaitUntilAsyncEvery WaitUntilAsync call requires a named description: parameter. This description appears in logs and asciinema recordings to make debugging easier when a wait times out.
ExecuteCallback Was UsedThe old builder API used ExecuteCallback() to run synchronous operations mid-sequence. With the automator API, simply inline the code directly — no special wrapper is needed.
Use WaitUntilAsync() with specific output patterns instead of arbitrary delays:
Don't hard-code the sequence numbers in WaitForSuccessPromptAsync calls. Use the counter:
The counter automatically tracks which command you're waiting for, even if command sequences change.
When writing new CLI E2E tests, use the Hex1b MCP server to interactively explore what terminal output to expect. The MCP server provides tools to start terminal sessions, send commands, and capture screenshots—helping you discover the exact strings and prompts to use in CellPatternSearcher.
aspire new or aspire run) and observe the outputCellPatternSearcher patternsaspire newAsk the MCP server to:
aspire new interactivelyThis reveals the exact strings like:
"> Starter App" for template selection"Enter the project name" for name input"Press Ctrl+C to stop..." for run completionCellPatternSearcher isn't matching, capture current terminal state to compareCapture Terminal Text to get plain text for pattern matchingCapture Terminal Screenshot (SVG) for visual debuggingWait for Terminal Text tool works similarly to WaitUntil in testsWhen adding new CLI operations as extension methods, define them on Hex1bTerminalAutomator:
Key points:
Hex1bTerminalAutomatorSequenceCounter parameter for prompt trackingCellPatternSearcher for output detectiondescription: on WaitUntilAsync callsWaitForSuccessPromptAsync(counter) after command completionTask (no fluent chaining needed with async/await)Environment variables set in CI:
GITHUB_PR_NUMBER: PR number for downloading CLI artifactsGITHUB_PR_HEAD_SHA: PR head commit SHA for version verification (not the merge commit)GH_TOKEN: GitHub token for API accessGITHUB_WORKSPACE: Workspace root for artifact pathsEach test class runs as a separate CI job via the unified TestEnumerationRunsheetBuilder infrastructure (using SplitTestsOnCI=true) for parallel execution.
When CLI E2E tests fail in CI, follow these steps to diagnose the issue:
Flaky test investigation: for recurring/intermittent failures, see
troubleshooting.mdfor a catalog of known flake classes (Y/n input race, prompt-counter desync, etc.) and the recipes to identify them from.castrecordings.
For VS Code extension behavior or extension/CLI integration issues, strongly prefer adding or
updating a reproducible test under extension/src/test-e2e/. Use agent-driven Playwright/VS Code UI
driving only as exploratory diagnosis when the E2E scenario is not clear yet; convert any successful
manual reproduction into an E2E test before fixing the bug unless there is a strong, explicit reason
not to.
When running extension E2E tests against an older published CLI for compatibility validation, set
ASPIRE_EXTENSION_E2E_SKIP_CURRENT_CLI_REGRESSIONS=true to skip tests that intentionally cover bugs
fixed only by the current repo-built CLI.
VS Code extension E2E jobs upload shard-specific diagnostics as extension-e2e-diagnostics-<rid>-<shard>-attempt<N> artifacts. Linux shards include .mp4 display recordings from Xvfb by default; Windows shards do not record video and instead rely on screenshots, VS Code logs, state files, and workspace diagnostics.
Important paths inside the downloaded shard:
The workflow keeps Linux recordings by default with ASPIRE_EXTENSION_E2E_RECORDING_MODE=always. Use failure to keep only failed-run videos or off to disable recording for local runs.
The fastest way to debug a CLI E2E test failure is to download and play the asciinema recording.
Using the helper scripts (recommended):
Manual download steps:
Job names follow the pattern: Tests / Cli E2E Linux (<TestClass>) / <TestClass> (ubuntu-latest)
Artifact names follow the pattern: logs-<TestClass>-ubuntu-latest
Downloaded artifacts contain:
Tests annotated with [CaptureWorkspaceOnFailure] automatically copy the generated project workspace into the test artifacts when a test fails. This is invaluable for debugging template generation or aspire run failures — you can inspect the exact generated files including the SDK output in .aspire/modules/aspire.js.
To add workspace capture to a new test:
| Symptom | Likely Cause | Solution |
|---|---|---|
| Timeout waiting for prompt | Command failed or hung | Check recording to see terminal output at timeout |
[N ERR:code] $ in prompt | Previous command exited with non-zero | Check recording to see which command failed |
| Pattern not found | Output format changed | Update CellPatternSearcher patterns |
| Pattern not found but text is visible | Using FindPattern with regex special chars | Use Find() instead of FindPattern() for literal strings containing (, ), /, etc. |
| Test hangs indefinitely | Waiting for wrong prompt number | Verify SequenceCounter usage matches commands |
| Timeout waiting for dashboard URL | Project failed to build/run | Check recording for build errors |