diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3350350..3e9c1ac 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -51,6 +51,20 @@ jobs:
dotnet new blazor -o SmokeApp --no-restore
cd SmokeApp
shellui init --tailwind standalone --yes
+
+ # Assert init produced a working host: App.razor patched with render mode,
+ # theme bootstrap, and shellui.js script tag.
+ grep -q 'HeadOutlet @rendermode="InteractiveServer"' Components/App.razor || (echo "init did not patch HeadOutlet @rendermode"; exit 1)
+ grep -q 'Routes @rendermode="InteractiveServer"' Components/App.razor || (echo "init did not patch Routes @rendermode"; exit 1)
+ grep -q 'ShellUI theme bootstrap' Components/App.razor || (echo "init did not inject theme bootstrap"; exit 1)
+ grep -q '' Components/App.razor || (echo "init did not inject shellui.js script tag"; exit 1)
+ grep -q 'shellui-sidebar.js' Components/App.razor && (echo "init incorrectly injected shellui-sidebar.js script tag (sidebar JS is dynamically imported)"; exit 1) || true
+
+ # Assert input.css has the full theme, not just @import "tailwindcss";
+ grep -q '@theme inline' wwwroot/input.css || (echo "init did not write full theme to input.css"; exit 1)
+ grep -q ':root' wwwroot/input.css || (echo "init did not write :root variables to input.css"; exit 1)
+ grep -q '\.dark' wwwroot/input.css || (echo "init did not write .dark variables to input.css"; exit 1)
+
shellui add chart pie-chart dashboard-02 --force || true
# Chart components reference ApexCharts.* types but the CLI does not yet
# auto-add the runtime NuGet dep. Drop this line once `shellui add chart`
diff --git a/ShellUI.Tests/InitBootstrapTests.cs b/ShellUI.Tests/InitBootstrapTests.cs
new file mode 100644
index 0000000..b78de84
--- /dev/null
+++ b/ShellUI.Tests/InitBootstrapTests.cs
@@ -0,0 +1,134 @@
+using ShellUI.CLI.Services;
+using Xunit;
+
+namespace ShellUI.Tests;
+
+public class InitBootstrapTests
+{
+ // Mirrors the App.razor that `dotnet new blazor` (net9) produces, including the
+ // @Assets[] asset-fingerprinting wrapper around the Blazor script. The wrapper
+ // is what tripped the smoke test on the first CI run of this branch.
+ private const string FreshAppRazor = @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+";
+
+ [Fact]
+ public void RewriteAppRazor_AddsRenderModeToHeadOutletAndRoutes()
+ {
+ var result = InitService.RewriteAppRazor(FreshAppRazor);
+
+ Assert.Contains(@"", result);
+ Assert.Contains(@"", result);
+ }
+
+ [Fact]
+ public void RewriteAppRazor_InjectsThemeBootstrapInHead()
+ {
+ var result = InitService.RewriteAppRazor(FreshAppRazor);
+
+ Assert.Contains("ShellUI theme bootstrap", result);
+ Assert.Contains("classList.add('dark')", result);
+ // Theme script must be inside , before .
+ var themeIdx = result.IndexOf("ShellUI theme bootstrap");
+ var headCloseIdx = result.IndexOf("");
+ Assert.True(themeIdx > 0 && themeIdx < headCloseIdx);
+ }
+
+ [Fact]
+ public void RewriteAppRazor_InjectsShelluiJsBeforeBlazorScript()
+ {
+ var result = InitService.RewriteAppRazor(FreshAppRazor);
+
+ var shelluiIdx = result.IndexOf(@"");
+ var blazorIdx = result.IndexOf("blazor.web.js");
+
+ Assert.True(shelluiIdx > 0, "shellui.js script tag was not injected");
+ Assert.True(shelluiIdx < blazorIdx, "shellui.js must precede blazor.web.js so window.ShellUI.* is defined before Blazor calls into it");
+ }
+
+ [Fact]
+ public void RewriteAppRazor_HandlesBareBlazorScriptTag()
+ {
+ // Older templates ship the bare form without @Assets[]. The patcher must handle both.
+ const string bare = @"";
+
+ var result = InitService.RewriteAppRazor(bare);
+
+ Assert.Contains(@"", result);
+ Assert.True(result.IndexOf(@"shellui.js") < result.IndexOf(@"blazor.web.js"));
+ }
+
+ [Fact]
+ public void RewriteAppRazor_IsIdempotent()
+ {
+ var once = InitService.RewriteAppRazor(FreshAppRazor);
+ var twice = InitService.RewriteAppRazor(once);
+
+ Assert.Equal(once, twice);
+ }
+
+ [Fact]
+ public void RewriteAppRazor_PreservesExistingRenderMode()
+ {
+ // If the user (or another tool) already set a different render mode (e.g. Auto),
+ // we must not overwrite it.
+ const string custom =
+ @"" + "\n" +
+ @"";
+
+ var result = InitService.RewriteAppRazor(custom);
+
+ Assert.Contains(@"", result);
+ Assert.Contains(@"", result);
+ Assert.DoesNotContain(@"InteractiveServer", result);
+ }
+
+ [Fact]
+ public void RewriteWasmIndexHtml_InjectsThemeAndShelluiJs()
+ {
+ const string indexHtml = @"
+
+
+ App
+
+
+
+
+
+";
+
+ var result = InitService.RewriteWasmIndexHtml(indexHtml);
+
+ Assert.Contains("ShellUI theme bootstrap", result);
+ var shelluiIdx = result.IndexOf(@"");
+ var blazorIdx = result.IndexOf(@"