Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .agents/skills/gui-e2e/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: gui-e2e
description: "Run and extend the automated GUI end-to-end lane (FlaUI over the real OpenClaw tray, fake or real gateway, Azure Windows via Crabbox). Use when a change touches tray, Settings, onboarding, chat, permissions, sandbox, connection UI, or when the user asks for GUI E2E coverage."
---

# GUI end-to-end lane

Status: **planned, not yet implemented.** The design lives in
`tests\OpenClaw.GuiE2ETests\PLAN.md`; verified facts for implementers are in
`tests\OpenClaw.GuiE2ETests\IMPLEMENTATION_NOTES.md`; the Phase 0 starting
prompt is `tests\OpenClaw.GuiE2ETests\KICKOFF_PROMPT.md`. Fill in the sections
below as each phase lands and remove this status line in Phase 3.

## When to use

- A change touches any hub page, the setup wizard, tray menu, chat surface,
dialogs, or connection/pairing UI.
- A PR declares the `windows-winui-interactive` or `windows-wsl-gateway-e2e`
proof pool and the automated `run-gui-e2e` command exists.
- The user asks to add or run a GUI scenario.

## Run locally (Phase 1+)

```powershell
$env:OPENCLAW_REPO_ROOT = (Get-Location).Path
.\scripts\gui-e2e\Invoke-GuiE2E.ps1 -Tier Fake
```

Results: `TestResults\ProofPools\gui-e2e-fake\gui-e2e-fake.trx`, screenshots and
redacted logs under `TestResults\GuiE2E\<run>\`. The script refuses zero-test runs.

## Run on Azure via Crabbox (Phase 2+)

Requires the Crabbox CLI and Azure auth as described in
`.agents\skills\crabbox\SKILL.md`. UI tests need a desktop lease
(`warmup --desktop`); the script launches the suite inside the interactive
session through a scheduled task and polls for completion.

```powershell
.\scripts\gui-e2e\Invoke-GuiE2E-Crabbox.ps1 -Tier Fake
```

Always report the provider and lease id, and confirm the lease was stopped.

## Add a scenario (Phase 1+)

1. Add or verify AutomationIds in `src` (PascalCase `<Surface><Element>[Action|Toggle|Input|Marker]`).
2. Add or extend a page object in `tests\OpenClaw.GuiE2ETests\Pages\`.
3. Write the test in `Scenarios\<Workflow>Scenarios.cs` with `[GuiE2EFact]` and `Tier`, `Workflow`, `Pool` traits.
4. Add the entry to `Catalog\gui-e2e-catalog.json`; run `dotnet test --filter FullyQualifiedName~Catalog`.
5. Regenerate the docs table with `.\scripts\gui-e2e\Export-GuiE2ECatalog.ps1`.

## Rules

- Never run against real `%APPDATA%\OpenClawTray`; the harness always isolates state.
- Capture app windows only; never copy `settings.json`, `gateways.json`, or device key files into artifacts.
- A skipped or quarantined scenario is not proof. Report blockers explicitly.
34 changes: 25 additions & 9 deletions src/OpenClaw.Connection/LocalAi/LlamaServerRouterConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static LlamaServerRouterLaunchPlan Build(
LocalModelInfo model = LocalModelCatalog.Find(manifest.ModelCatalogId)
?? throw new InvalidDataException("The managed local AI model is no longer qualified.");

ValidateQualifiedReceipt(manifest, runtime, model);
LocalInferenceRunProfile profile = ValidateQualifiedReceipt(manifest, runtime, model);

string presetPath = paths.ResolveContainedPath(
Path.GetRelativePath(paths.RootDirectory, paths.RouterPresetPath),
Expand All @@ -58,11 +58,11 @@ public static LlamaServerRouterLaunchPlan Build(
.WithComparers(StringComparer.OrdinalIgnoreCase)
.Add("CUDA_VISIBLE_DEVICES", manifest.SelectedGpuId),
presetPath,
BuildPreset(model, install.ModelPath),
BuildPreset(model, profile, install.ModelPath),
model.Id);
}

private static void ValidateQualifiedReceipt(
private static LocalInferenceRunProfile ValidateQualifiedReceipt(
LocalAiInstallManifest manifest,
LlamaRuntimeVariant runtime,
LocalModelInfo model)
Expand All @@ -78,12 +78,21 @@ private static void ValidateQualifiedReceipt(
throw new InvalidDataException("The managed local AI architecture and runtime receipt do not match.");
}
if (!string.Equals(manifest.EngineVersion, LlamaRuntimeCatalog.ReleaseTag, StringComparison.Ordinal) ||
!string.Equals(manifest.ModelAlias, model.Id, StringComparison.Ordinal) ||
manifest.ContextLength != model.Recipe.ContextTokens)
!string.Equals(manifest.ModelAlias, model.Id, StringComparison.Ordinal))
{
throw new InvalidDataException("The managed local AI model recipe receipt does not match the qualified catalog.");
}

LocalInferenceRunProfile profile = LocalModelCatalog.FindProfile(
model,
manifest.ContextLength,
manifest.KeyCachePrecision,
manifest.ValueCachePrecision,
manifest.DraftKeyCachePrecision,
manifest.DraftValueCachePrecision)
?? throw new InvalidDataException(
"The managed local AI context and KV cache receipt do not match a qualified catalog profile.");

if (manifest.RuntimeAssets.Length != runtime.Artifacts.Count ||
runtime.Artifacts.Any(artifact => !manifest.RuntimeAssets.Any(receipt =>
string.Equals(receipt.FileName, Path.GetFileName(artifact.RelativePath), StringComparison.Ordinal) &&
Expand All @@ -103,9 +112,14 @@ private static void ValidateQualifiedReceipt(
{
throw new InvalidDataException("The managed model artifact receipt does not match the qualified catalog.");
}

return profile;
}

private static string BuildPreset(LocalModelInfo model, string modelPath)
private static string BuildPreset(
LocalModelInfo model,
LocalInferenceRunProfile profile,
string modelPath)
{
if (modelPath.IndexOfAny(['\r', '\n']) >= 0)
throw new InvalidDataException("The managed model path cannot be represented safely in a llama-server preset.");
Expand All @@ -118,11 +132,13 @@ private static string BuildPreset(LocalModelInfo model, string modelPath)
preset.Append('[').Append(model.Id).AppendLine("]");
preset.Append("model = ").AppendLine(modelPath);
preset.AppendLine("load-on-startup = false");
preset.Append("ctx-size = ").AppendLine(Invariant(recipe.ContextTokens));
preset.Append("ctx-size = ").AppendLine(Invariant(profile.ContextTokens));
preset.Append("n-predict = ").AppendLine(Invariant(LocalAiGatewayProviderDefinition.MaximumOutputTokens));
preset.Append("parallel = ").AppendLine(Invariant(recipe.ParallelRequests));
preset.AppendLine("cache-type-k = f16");
preset.AppendLine("cache-type-v = f16");
preset.Append("cache-type-k = ").AppendLine(LocalModelCatalog.ToLlamaServerCacheType(profile.KeyCachePrecision));
preset.Append("cache-type-v = ").AppendLine(LocalModelCatalog.ToLlamaServerCacheType(profile.ValueCachePrecision));
preset.Append("cache-type-k-draft = ").AppendLine(LocalModelCatalog.ToLlamaServerCacheType(profile.DraftKeyCachePrecision));
preset.Append("cache-type-v-draft = ").AppendLine(LocalModelCatalog.ToLlamaServerCacheType(profile.DraftValueCachePrecision));
preset.Append("batch-size = ").AppendLine(Invariant(recipe.BatchTokens));
preset.Append("ubatch-size = ").AppendLine(Invariant(recipe.MicroBatchTokens));
preset.AppendLine("flash-attn = on");
Expand Down
7 changes: 6 additions & 1 deletion src/OpenClaw.Connection/LocalAi/LlamaServerRuntimeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,12 @@ private LocalAiRuntimeSnapshot Publish(
processId,
processStartedAtUtc,
detail,
now);
now,
_install?.Manifest.ContextLength,
_install?.Manifest.KeyCachePrecision,
_install?.Manifest.ValueCachePrecision,
_install?.Manifest.DraftKeyCachePrecision,
_install?.Manifest.DraftValueCachePrecision);
lock (_snapshotGate)
_snapshot = value;

Expand Down
27 changes: 23 additions & 4 deletions src/OpenClaw.Connection/LocalAi/LocalAiManifest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Serialization;
using OpenClaw.Shared.Inference.Catalog;

namespace OpenClaw.Connection.LocalAi;

Expand Down Expand Up @@ -158,6 +159,10 @@ public sealed record LocalAiInstallManifest
/// </summary>
public string? GatewayFallbackModel { get; init; }
public required int ContextLength { get; init; }
public KvCachePrecision KeyCachePrecision { get; init; } = KvCachePrecision.F16;
public KvCachePrecision ValueCachePrecision { get; init; } = KvCachePrecision.F16;
public KvCachePrecision DraftKeyCachePrecision { get; init; } = KvCachePrecision.F16;
public KvCachePrecision DraftValueCachePrecision { get; init; } = KvCachePrecision.F16;
public DateTimeOffset InstalledAtUtc { get; init; } = DateTimeOffset.UtcNow;
}

Expand Down Expand Up @@ -213,11 +218,18 @@ public static void ValidateFallbackModel(string? model)
/// <summary>Persists the installation manifest with same-directory atomic replacement.</summary>
public sealed class LocalAiManifestStore
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions();

private static JsonSerializerOptions CreateJsonOptions()
{
WriteIndented = true,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
};
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = true,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
};
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower, allowIntegerValues: false));
return options;
}

private readonly LocalAiPaths _paths;

Expand Down Expand Up @@ -330,6 +342,13 @@ public LocalAiResolvedInstall ResolveAndValidate(LocalAiInstallManifest manifest
}
if (manifest.ContextLength <= 0)
throw new InvalidDataException("The local AI manifest context length must be positive.");
if (!Enum.IsDefined(manifest.KeyCachePrecision) ||
!Enum.IsDefined(manifest.ValueCachePrecision) ||
!Enum.IsDefined(manifest.DraftKeyCachePrecision) ||
!Enum.IsDefined(manifest.DraftValueCachePrecision))
{
throw new InvalidDataException("The local AI manifest KV cache precision is unsupported.");
}

if (manifest.RuntimeAssets.IsDefaultOrEmpty)
throw new InvalidDataException("The local AI manifest must record at least one runtime asset receipt.");
Expand Down
9 changes: 8 additions & 1 deletion src/OpenClaw.Connection/LocalAi/LocalAiRuntimeModels.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using OpenClaw.Shared.Inference.Catalog;

namespace OpenClaw.Connection.LocalAi;

public enum LocalAiRuntimeState
Expand Down Expand Up @@ -85,7 +87,12 @@ public sealed record LocalAiRuntimeSnapshot(
int? ProcessId,
DateTimeOffset? ProcessStartedAtUtc,
string? Detail,
DateTimeOffset UpdatedAtUtc)
DateTimeOffset UpdatedAtUtc,
int? ContextLength = null,
KvCachePrecision? KeyCachePrecision = null,
KvCachePrecision? ValueCachePrecision = null,
KvCachePrecision? DraftKeyCachePrecision = null,
KvCachePrecision? DraftValueCachePrecision = null)
{
public static LocalAiRuntimeSnapshot Initial(Uri endpoint, DateTimeOffset now) =>
new(
Expand Down
18 changes: 3 additions & 15 deletions src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,25 +188,13 @@
<TextBlock x:Name="LocalAiModelDetailText"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" TextWrapping="Wrap" />
<TextBlock x:Name="LocalAiSettingsDetailText"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" TextWrapping="Wrap" />
<InfoBar x:Name="LocalAiNetworkingConsentPanel"
AutomationProperties.AutomationId="LocalAiNetworkingConsentPanel"
AutomationProperties.LiveSetting="Assertive"
IsOpen="True" IsClosable="False" Severity="Warning"
Title="WSL networking change required" Visibility="Collapsed">
<StackPanel Spacing="6" Margin="0,0,0,12">
<TextBlock Text="Local AI needs mirrored WSL networking. Setup will update your global .wslconfig and stop all running WSL distributions once. No distributions are deleted. Save any work in WSL first."
Style="{StaticResource CaptionTextBlockStyle}" TextWrapping="Wrap" />
<CheckBox x:Name="LocalAiNetworkingConsentCheckBox"
AutomationProperties.AutomationId="LocalAiNetworkingConsentCheckBox"
AutomationProperties.Name="Allow the global WSL networking change and one-time WSL shutdown"
Content="I understand and allow this global WSL change and one-time shutdown."
Checked="LocalAiNetworkingConsent_Changed"
Unchecked="LocalAiNetworkingConsent_Changed" />
</StackPanel>
</InfoBar>
Title="WSL networking change required"
Message="Setup will enable mirrored WSL networking and stop all running WSL distributions once; save any WSL work first."
Visibility="Collapsed" />
<InfoBar x:Name="LocalAiNetworkingInspectionError"
AutomationProperties.AutomationId="LocalAiNetworkingInspectionError"
AutomationProperties.LiveSetting="Assertive"
Expand Down
Loading
Loading