diff --git a/eng/restore-toolset.ps1 b/eng/restore-toolset.ps1
new file mode 100644
index 0000000000..b12b38332e
--- /dev/null
+++ b/eng/restore-toolset.ps1
@@ -0,0 +1,46 @@
+# Installs .NET SDK workloads needed by projects in this repository.
+#
+# This script is dot-sourced by eng/common/build.ps1's InitializeCustomToolset
+# AFTER InitializeDotNetCli has bootstrapped the repo-local .dotnet/ SDK, so
+# $RepoRoot and the helpers from eng/common/tools.ps1 are in scope.
+#
+# Currently required by samples/WasiPlayground/WasiPlayground.csproj which
+# targets net10.0 with true
+# and would otherwise fail with NETSDK1147 in CI.
+
+$RequiredWorkloads = @('wasi-experimental-net10')
+
+# Note: `wasm-tools-net10` is documented in samples/WasiPlayground/README.md
+# as a prerequisite for `dotnet publish`, but it is not needed by the repo's
+# `dotnet build` so we keep the CI install minimal.
+
+$dotnetRoot = if (-not [string]::IsNullOrEmpty($env:DOTNET_INSTALL_DIR)) {
+ $env:DOTNET_INSTALL_DIR
+} else {
+ Join-Path $RepoRoot '.dotnet'
+}
+
+$dotnet = Join-Path $dotnetRoot (GetExecutableFileName 'dotnet')
+
+if (-not (Test-Path $dotnet)) {
+ Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "restore-toolset.ps1: dotnet executable not found at '$dotnet'."
+ ExitWithExitCode 1
+}
+
+# Cheap, network-free probe: parse `dotnet workload list` text to skip already-installed workloads.
+$listOutput = & $dotnet workload list 2>&1 | Out-String
+$missing = @($RequiredWorkloads | Where-Object {
+ -not ($listOutput -match "(?m)^\s*$([regex]::Escape($_))\s")
+})
+
+if ($missing.Count -eq 0) {
+ Write-Host "All required workloads already installed: $($RequiredWorkloads -join ', ')"
+ return
+}
+
+Write-Host "Installing .NET SDK workloads: $($missing -join ', ')"
+& $dotnet workload install @missing
+if ($LASTEXITCODE -ne 0) {
+ Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Failed to install workloads '$($missing -join ', ')' (dotnet workload install exit code $LASTEXITCODE)."
+ ExitWithExitCode $LASTEXITCODE
+}
diff --git a/eng/restore-toolset.sh b/eng/restore-toolset.sh
new file mode 100644
index 0000000000..4542399044
--- /dev/null
+++ b/eng/restore-toolset.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+
+# Installs .NET SDK workloads needed by projects in this repository.
+#
+# This script is dot-sourced by eng/common/build.sh's InitializeCustomToolset
+# AFTER InitializeDotNetCli has bootstrapped the repo-local .dotnet/ SDK, so
+# $repo_root and the helpers from eng/common/tools.sh are in scope.
+#
+# Currently required by samples/WasiPlayground/WasiPlayground.csproj which
+# targets net10.0 with true
+# and would otherwise fail with NETSDK1147 in CI.
+
+required_workloads=('wasi-experimental-net10')
+
+# Note: `wasm-tools-net10` is documented in samples/WasiPlayground/README.md
+# as a prerequisite for `dotnet publish`, but it is not needed by the repo's
+# `dotnet build` so we keep the CI install minimal.
+
+if [[ -n "${DOTNET_INSTALL_DIR:-}" ]]; then
+ dotnet_root="$DOTNET_INSTALL_DIR"
+else
+ dotnet_root="${repo_root}.dotnet"
+fi
+
+dotnet_exe="$dotnet_root/dotnet"
+
+if [[ ! -x "$dotnet_exe" ]]; then
+ Write-PipelineTelemetryError -category 'InitializeToolset' "restore-toolset.sh: dotnet executable not found at '$dotnet_exe'."
+ ExitWithExitCode 1
+fi
+
+# Cheap, network-free probe: parse `dotnet workload list` text to skip already-installed workloads.
+list_output=$("$dotnet_exe" workload list 2>&1)
+missing=()
+for workload in "${required_workloads[@]}"; do
+ if ! grep -qE "^[[:space:]]*${workload}[[:space:]]" <<<"$list_output"; then
+ missing+=("$workload")
+ fi
+done
+
+if [[ ${#missing[@]} -eq 0 ]]; then
+ echo "All required workloads already installed: ${required_workloads[*]}"
+ return 0
+fi
+
+echo "Installing .NET SDK workloads: ${missing[*]}"
+"$dotnet_exe" workload install "${missing[@]}"
+workload_install_exit_code=$?
+if [[ $workload_install_exit_code -ne 0 ]]; then
+ Write-PipelineTelemetryError -category 'InitializeToolset' "Failed to install workloads '${missing[*]}' (dotnet workload install exit code $workload_install_exit_code)."
+ ExitWithExitCode $workload_install_exit_code
+fi
diff --git a/samples/WasiPlayground/Program.cs b/samples/WasiPlayground/Program.cs
index 049b9e41d4..2e02978391 100644
--- a/samples/WasiPlayground/Program.cs
+++ b/samples/WasiPlayground/Program.cs
@@ -14,7 +14,8 @@
testApplicationBuilder.AddTrxReportProvider();
testApplicationBuilder.AddAppInsightsTelemetryProvider();
testApplicationBuilder.AddCrashDumpProvider();
-testApplicationBuilder.AddHangDumpProvider();
+// AddHangDumpProvider is intentionally not registered: hang dumps rely on
+// System.Diagnostics.Process which is unsupported on wasi (see #8557).
testApplicationBuilder.AddAzureDevOpsProvider();
using ITestApplication testApplication = await testApplicationBuilder.BuildAsync();
return await testApplication.RunAsync();
diff --git a/samples/WasiPlayground/README.md b/samples/WasiPlayground/README.md
index fb6789b966..22038453db 100644
--- a/samples/WasiPlayground/README.md
+++ b/samples/WasiPlayground/README.md
@@ -1,21 +1,85 @@
# WasiPlayground
-To run this project:
+Demonstrates hosting [Microsoft.Testing.Platform](https://aka.ms/testingplatform/) on the WebAssembly System Interface (WASI).
-1. Run `dotnet workload install wasi-experimental`
-2. Run `dotnet build`
-3. Install wasmtime. See docs at .
-4. Open command-line in AppBundle directory (`artifacts\bin\WasiPlayground\Debug\net10.0\wasi-wasm\AppBundle`)
-5. Run `wasmtime run --wasi http --dir . -- dotnet.wasm WasiPlayground`
+## Build & run
+
+Prerequisites:
+
+- The repo-local .NET SDK at `.dotnet\dotnet.exe`. Bootstrap it once by running
+ `.\build.cmd` (Windows) or `./build.sh` (Linux/macOS) from the repo root;
+ this also installs the `wasi-experimental-net10` workload required to build
+ the sample (see [`eng/restore-toolset.ps1`](../../eng/restore-toolset.ps1)
+ / [`eng/restore-toolset.sh`](../../eng/restore-toolset.sh)).
+- The `wasm-tools-net10` workload, which is only needed for `dotnet publish`
+ (not by the repo's `dotnet build`), so install it manually:
+
+ ```cmd
+ .\.dotnet\dotnet.exe workload install wasm-tools-net10
+ ```
+
+- [wasmtime](https://docs.wasmtime.dev/cli-install.html) on `PATH`.
+
+> All commands below use `.\.dotnet\dotnet.exe` so that the `.dotnet\packs`
+> lookup in step 2 resolves. If you prefer a machine-installed SDK, swap
+> `dotnet` in and replace `.dotnet\packs` with the corresponding `packs`
+> folder reported by `dotnet --info` (look for *.NET SDKs installed*).
+
+Then:
+
+1. From the repo root, publish the sample:
+
+ ```cmd
+ .\.dotnet\dotnet.exe publish samples\WasiPlayground\WasiPlayground.csproj -c Debug -f net10.0
+ ```
+
+2. The pre-built `dotnet.wasm` does not embed the ICU data file, so copy it
+ next to the bundle. First list the installed runtime-pack version (a
+ single folder name such as `10.0.8`), then substitute it into the copy
+ command:
+
+ ```cmd
+ dir /b .dotnet\packs\Microsoft.NETCore.App.Runtime.Mono.wasi-wasm
+
+ copy .dotnet\packs\Microsoft.NETCore.App.Runtime.Mono.wasi-wasm\\runtimes\wasi-wasm\native\icudt.dat ^
+ artifacts\bin\WasiPlayground\Debug\net10.0\wasi-wasm\AppBundle\
+ ```
+
+3. Switch to the bundle directory and invoke `wasmtime` (the `-S http` flag is required because the runtime imports `wasi:http`):
+
+ ```cmd
+ cd artifacts\bin\WasiPlayground\Debug\net10.0\wasi-wasm\AppBundle
+ wasmtime run -S http --dir . -- dotnet.wasm WasiPlayground
+ ```
## Status
-As of today, this will produce this exception (it's not yet supported by MTP).
+After publishing, Microsoft.Testing.Platform now boots on `wasi-wasm` (the
+banner reads `[wasi-wasm - net10.0]`) thanks to [#7137](https://github.com/microsoft/testfx/pull/7137).
+Test execution itself currently fails with:
```console
+Microsoft.Testing.Platform v2.3.0-dev (UTC ...) [wasi-wasm - net10.0]
+...
Unhandled Exception:
System.PlatformNotSupportedException: Arg_PlatformNotSupported
- at System.Threading.Tasks.Task.InternalWaitCore(Int32 millisecondsTimeout, CancellationToken cancellationToken)
- at System.Threading.Tasks.Task.InternalWait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
+ at System.Threading.Tasks.Task.InternalWaitCore(...)
+ at System.Threading.Tasks.Task.InternalWait(...)
at Program.(String[] args)
```
+
+The exception comes from the C# compiler's synthetic `Main` wrapper for
+`async Task Main`, which calls `Task.GetAwaiter().GetResult()` →
+`Task.Wait()`. On single-threaded `wasi-wasm` (no thread pool) this throws
+`PlatformNotSupportedException`. Tracked in [#5366](https://github.com/microsoft/testfx/issues/5366)
+so this sample can act as the canonical repro.
+
+## Build configuration notes
+
+`WasiPlayground.csproj` ships with a few non-obvious switches:
+
+| Property | Reason |
+| --- | --- |
+| `UsingWasiRuntimeWorkload=true` | Workaround for an SDK manifest bug in `11.0.100-preview.5` where `$(UsingWasiRuntimeWorkload)` never resolves to `true` for net10.0 projects, so the WASI Sdk targets are never imported and no `dotnet.wasm` is produced. |
+| `WasmSingleFileBundle=false` | Single-file bundling requires the [wasi-sdk](https://github.com/WebAssembly/wasi-sdk) (clang) toolchain to relink the native runtime. Keeping the managed assemblies on disk avoids that requirement. |
+| `InvariantGlobalization=true` | Prevents the trimmer from crashing during publish, but the pre-built `dotnet.wasm` still loads ICU at runtime, so `icudt.dat` must still be staged next to the bundle (see step 2). |
diff --git a/samples/WasiPlayground/WasiPlayground.csproj b/samples/WasiPlayground/WasiPlayground.csproj
index ed0972d7ce..035401e937 100644
--- a/samples/WasiPlayground/WasiPlayground.csproj
+++ b/samples/WasiPlayground/WasiPlayground.csproj
@@ -5,10 +5,29 @@
net10.0
wasi-wasm
true
+
+ false
+ true
+
+
+ true
enable
- Exe
- true
+
+ true