diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 0b537e2c..fad6f5f8 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,5 +1,5 @@ name: Bug report -description: Report a bug in a skill, tool, or the agent +description: Report a bug in a skill, plugin, or agent guidance title: "[Bug]: " labels: ["bug"] body: @@ -8,6 +8,8 @@ body: value: | Thanks for taking the time to file a bug report! + **WinApp CLI or WinUI analyzer bug?** Please [open an issue in microsoft/winappCli](https://github.com/microsoft/winappCli/issues/new/choose) for analyzer diagnostics/package issues or tool behavior in `find-api`, `run`, packaging, and Windows Sandbox. Use this form for incorrect skill/agent guidance or plugin integration. + πŸ’‘ **The single most useful thing you can attach** is a `session-report.md` from the **`winui-session-report`** skill β€” it captures the agent's tool calls, build output, and timing in a structured way. - type: textarea id: description @@ -31,7 +33,6 @@ body: - "skill: winui-packaging" - "skill: winui-session-report" - "skill: winui-wpf-migration" - - "tool: winui-analyzer (Roslyn)" - "area: plugin / install" - "area: docs" validations: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 1ec2bfdc..7344cc6e 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,8 @@ blank_issues_enabled: false contact_links: + - name: WinApp CLI or WinUI analyzer bug + url: https://github.com/microsoft/winappCli/issues/new/choose + about: Report analyzer diagnostics/package issues and find-api, run, packaging, or Windows Sandbox tool bugs in the WinApp repository. - name: Question / discussion url: https://github.com/microsoft/win-dev-skills/discussions about: Have a question about how to use a skill, or want to discuss an idea? Start a Discussion. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7ea7423d..0725fda8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -31,7 +31,7 @@ The `pr-target-policy` CI check enforces this. - [ ] Agent (`plugins/winui/agents/`, `plugins/winui/agent-plugin/com.github.copilot/agents/`) - [ ] Skill: -- [ ] Tool: +- [ ] Script: - [ ] Plugin metadata (`plugin.json`, `plugins/winui/`) - [ ] Repo-level docs / governance @@ -41,7 +41,6 @@ The `pr-target-policy` CI check enforces this. - [ ] Tested locally on Windows (build + agent invocation if applicable) - [ ] If a skill changed: `SKILL.md` frontmatter still valid; cross-references to other skills still resolve -- [ ] If `Microsoft.WindowsAppSDK.Analyzers` source changed: rebuilt the DLL and committed it (`plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.dll`) β€” provenance check in CI will fail otherwise - [ ] If a `.ps1` script changed: tested under default `RemoteSigned` execution policy - [ ] If a CLI command or agent invocation changed: `README.md` updated - [ ] New tests added for new functionality (if applicable) diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index 19e1619d..ea2abd77 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -1,8 +1,2 @@ paths: - - src - .github/workflows - - plugins/winui -paths-ignore: - - '**/*Tests/*.cs' - - '**/bin/**' - - '**/obj/**' diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6db38f4e..8e391d60 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,32 +15,5 @@ updates: prefix: "ci" include: "scope" - # Microsoft.WindowsAppSDK.Analyzers (Roslyn analyzer) cooldown: default-days: 7 - - package-ecosystem: "nuget" - directory: "/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers" - schedule: - interval: "weekly" - day: "monday" - open-pull-requests-limit: 5 - labels: - - "dependencies" - - "tool: winui-analyzer" - commit-message: - prefix: "deps" - include: "scope" - - # Microsoft.WindowsAppSDK.Analyzers tests (xUnit + Roslyn test deps) - - package-ecosystem: "nuget" - directory: "/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests" - schedule: - interval: "weekly" - day: "monday" - open-pull-requests-limit: 5 - labels: - - "dependencies" - - "tool: winui-analyzer" - commit-message: - prefix: "deps" - include: "scope" diff --git a/.github/skills/pr-review/SKILL.md b/.github/skills/pr-review/SKILL.md index 43bda508..e0166ca3 100644 --- a/.github/skills/pr-review/SKILL.md +++ b/.github/skills/pr-review/SKILL.md @@ -1,6 +1,6 @@ --- name: pr-review -description: Multi-dimensional review of a PR or feature branch in microsoft/win-dev-skills. Activate on "review my PR / changes / branch", "vet before pushing", "PR review", "is this ready to merge". Fans out parallel sub-agents over skill content, the skill-vs-tool boundary (solution hierarchy), tool correctness, payload/provenance/tests, docs & manifest sync, plus a multi-model cross-check. Reports findings to stdout. Does NOT apply fixes. +description: Multi-dimensional review of a PR or feature branch in microsoft/win-dev-skills. Activate on "review my PR / changes / branch", "vet before pushing", "PR review", "is this ready to merge". Fans out parallel sub-agents over skill content, the skill-vs-tool boundary (solution hierarchy), tool correctness, dependency integration/tests, docs & manifest sync, plus a multi-model cross-check. Reports findings to stdout. Does NOT apply fixes. infer: true --- @@ -9,23 +9,22 @@ Your job is to give a contributor a thorough, high-signal review of their in-progress branch before they push, by fanning out parallel sub-agents and consolidating their findings. -This repo is **not a regular C# product**. It ships: +This repo is a **content plugin, not a C# product**. Its review surfaces are: - A portable **Agent Plugins package** under `plugins/winui/agent-plugin/` plus the containing Claude/Codex/OpenClaw compatibility package β€” agent prompt + skill prompts (`SKILL.md` files). These are **Tier 3 instructions** that agents frequently ignore (see `dimensions/skill-tool-boundary.md`). Adding prose here is the *last resort*, not the first response to any problem. -- One **in-repo C# tool** under `src/tools/` β€” the WinUI 3 Roslyn analyzer. - This is **Tier 1 enforcement** and the preferred place - to land behavior changes that belong in this repository. -- **Committed analyzer payloads** (DLL and - `Microsoft.WindowsAppSDK.Analyzers.targets`) inside - `plugins/winui/agent-plugin/skills/` - that must stay in sync with their sources. CI provenance jobs will fail - the PR if they drift, but it's better to flag the drift in review. - -The reviewer's job is to keep these three layers honest, lean, and in sync β€” +- **PowerShell helpers and regression checks** for session reporting and + repository workflows. There is no local analyzer or metadata CLI source. +- **External Tier 1 enforcement:** WinApp CLI 0.7+ owns build/run, packaging, + API discovery, and Sandbox automation. Projects consume + `Microsoft.Windows.SDK.BuildTools.WinUIAnalyzer` from NuGet. Its active + source and publication live in `microsoft/winappCli`; do not request + committed DLL refreshes or recreate a local build/run wrapper. + +The reviewer's job is to keep these surfaces honest, lean, and in sync β€” and to push back on changes that bloat the skills with content that should have been a tool change. @@ -113,16 +112,13 @@ focus. Common buckets in this repo: |-------------|--------------| | `plugins/winui/agent-plugin/skills//SKILL.md` | skill-content, skill-tool-boundary | | `plugins/winui/agent-plugin/skills//references/` | skill-content (references discipline) | -| `plugins/winui/agent-plugin/skills//*.ps1` (e.g. `BuildAndRun.ps1`, `Analyze-Session.ps1`) | tool-correctness, payloads-and-tests | -| `plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/` | payloads-and-tests (committed analyzer payload) | +| `plugins/winui/agent-plugin/skills//*.ps1` (e.g. `Analyze-Session.ps1`) | tool-correctness, payloads-and-tests | | `plugins/winui/{agents,agent-plugin/com.github.copilot/agents}/winui-dev.agent.md` | skill-content, docs-and-manifests | | `plugins/winui/agent-plugin/plugin.json` | docs-and-manifests | | `.github/plugin/marketplace.json` | docs-and-manifests | -| `src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers/` | tool-correctness, payloads-and-tests | -| `src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/` | payloads-and-tests | -| `src/tools/winui-analyzer/RULES.md` / `CHANGELOG.md` | docs-and-manifests | -| `scripts/build-tools.ps1` | payloads-and-tests | -| `.github/workflows/` | docs-and-manifests (CI), payloads-and-tests (provenance) | +| `scripts/tests/` | tool-correctness, payloads-and-tests | +| `scripts/open-release-pr.ps1` | tool-correctness, docs-and-manifests | +| `.github/workflows/` | docs-and-manifests (CI), payloads-and-tests (regressions) | | `README.md`, `SECURITY.md`, `SUPPORT.md` | docs-and-manifests | ### 3. Fan out parallel sub-agents @@ -131,8 +127,8 @@ Launch all 5 specialist sub-agents in **the same response** using the `task` tool, mode `"sync"`. Pick the agent type per the table below β€” `code-review` is the right default for `tool-correctness` because that built-in agent already specializes in bug/security review of C# and PowerShell, which lets -the dimension fragment focus on the *repo-specific* deltas (analyzer ID -immutability, AOT constraints, payload-script behavior). Each prompt must +the dimension fragment focus on the *repo-specific* deltas (external command +contracts, target scoping, shipped-script behavior). Each prompt must be self-contained: include the diff, the base/head refs, the file classification, and the contents of the corresponding `dimensions/.md` plus the shared contract. @@ -143,8 +139,8 @@ The 6 dimensions and their fragment files: |---|-----------|----------|---------------| | 1 | skill content quality | `dimensions/skill-content.md` | general-purpose | | 2 | skill ↔ tool boundary (solution hierarchy) | `dimensions/skill-tool-boundary.md` | general-purpose | -| 3 | tool correctness (C# / PowerShell in `src/tools/` and shipped scripts) | `dimensions/tool-correctness.md` | code-review | -| 4 | payloads, provenance, analyzer tests | `dimensions/payloads-and-tests.md` | general-purpose | +| 3 | PowerShell correctness and external command contracts | `dimensions/tool-correctness.md` | code-review | +| 4 | dependency integration and script regressions | `dimensions/payloads-and-tests.md` | general-purpose | | 5 | docs & manifests sync | `dimensions/docs-and-manifests.md` | explore | | 6 | multi-model cross-check | `dimensions/multi-model.md` | general-purpose, with `model` override | @@ -232,10 +228,9 @@ verdict. - **No fix application.** Even if findings are obvious, do not edit code. - **No file output.** Stdout only, unless the user explicitly asked for a file. -- **No build/test execution.** Flag staleness (analyzer DLL not refreshed, - `RULES.md` not updated) but do not run - `scripts/build-tools.ps1` or `dotnet test` yourself β€” they are slow and - the contributor will run them. +- **No test execution.** Flag specific missing regression cases or unsupported + external contracts, but do not run workflow tests yourself; the contributor + and CI run the checks. - **Signal-to-noise.** Reject sub-agent findings that are pure style nits, formatting, things the compiler / analyzer already catches, context inflation without evidence, or scenario-specific patches that don't @@ -270,7 +265,7 @@ parameter on the `task` call to a different model family than yourself. ``` 1. collect-diff.ps1 -Scope auto β†’ JSON: 7 files, +220/-40, status=ok -2. Map files to areas β†’ 1 SKILL.md + analyzer rule + RULES.md + tests + payload +2. Map files to areas β†’ 1 SKILL.md + helper script + regression tests 3. Fan out 5 task() calls in parallel β†’ wait for all 4. Fan out task() #6 with model override β†’ wait 5. Dedupe, sort, ID, mark multi-model status diff --git a/.github/skills/pr-review/dimensions/_shared-contract.md b/.github/skills/pr-review/dimensions/_shared-contract.md index f905fa9e..6dbe1cce 100644 --- a/.github/skills/pr-review/dimensions/_shared-contract.md +++ b/.github/skills/pr-review/dimensions/_shared-contract.md @@ -48,7 +48,7 @@ After the findings (or in place of them when there are zero), include: ## What I checked - - -- +- ``` This appears in the orchestrator's `Coverage notes` section so the @@ -81,8 +81,8 @@ high. The bar for adding tooling enforcement is lower. three other situations where this change would help, it is too narrow. - **Redundant enforcement.** Suggesting skill text for something the - C# compiler, the WinUI analyzer, the `winapp` CLI, `BuildAndRun.ps1`, - or the CI provenance jobs already catch. Name the existing + C# compiler, the WinUI analyzer NuGet package, the `winapp` CLI, + or existing CI checks already catch. Name the existing enforcement instead of duplicating it. - **Action bias.** Feeling obliged to flag every diff hunk. "No finding" is a valid verdict for a clean change. @@ -90,27 +90,26 @@ high. The bar for adding tooling enforcement is lower. ### Keep these -- Bugs, logic errors, races, missed edge cases (in tool C# code or in - shipped PowerShell scripts). +- Bugs, logic errors, races, missed edge cases in PowerShell helpers or + documented executable examples. - Security issues β€” never suppressed, even at low confidence. - Skill content that is **measurably bloated** with content that duplicates other skills, would have been better as a tool change, or is too scenario-specific. - Trigger-phrasing problems in `description:` frontmatter that would cause the wrong agent activation. -- Stale committed analyzer payloads (DLL or - `Microsoft.WindowsAppSDK.Analyzers.targets`) that will fail CI - provenance. +- External command/package contracts that the required released version + does not support, or analyzer installation guidance that omits XAML inputs. - Manifest / version / agent-file drift that ships broken artifacts to end users. -- Missing analyzer xUnit tests for new or changed rules. +- Missing PowerShell regression cases for changed helper behavior. ## Severity guide | Severity | Meaning | |----------|---------| -| critical | Will ship broken behavior to end users (plugin install fails, manifest invalid, analyzer crashes on real code) or block release. Must fix before merge. | -| high | Real bug in tool code, real provenance drift CI will reject, real skill bloat that meaningfully harms agent quality, or missing tests for a new analyzer rule. Should fix before merge. | +| critical | Will ship broken behavior to end users (plugin install fails, manifest invalid, helper unusable) or block release. Must fix before merge. | +| high | Real bug in helper code, broken external dependency integration, real skill bloat that meaningfully harms agent quality, or missing regression coverage. Should fix before merge. | | medium | Worth fixing but not a blocker; may be deferred with a note. | | low | Minor improvement; only emit if the recommendation is concrete and actionable AND the finding survives the Team Lead Test. | @@ -125,7 +124,7 @@ high. The bar for adding tooling enforcement is lower. verifiable. Security findings (in the `tool-correctness` dimension when reviewing -shipped PowerShell or analyzer code) are **never** suppressed by low +PowerShell helpers or executable examples) are **never** suppressed by low confidence β€” emit them anyway. ## The Solution Hierarchy (cross-cutting) @@ -136,14 +135,14 @@ change should land. Cite the tier on every `skill-content` and | Tier | Type | Reliability | Examples in this repo | |------|------|-------------|------------------------| -| **0** | Environment / harness defaults | Highest β€” agent never sees it | `winapp new` template choice, `BuildAndRun.ps1` defaults, prerequisite checks in `winui-setup` | -| **1** | Tooling enforcement | High β€” produces errors/warnings the agent must address | `Microsoft.WindowsAppSDK.Analyzers` rules, `winapp find-ui` query results, `winapp find-api` API verification, `winapp` CLI exit codes | +| **0** | Environment / harness defaults | Highest β€” agent never sees it | `winapp new` template choice, project analyzer references, prerequisite checks in `winui-setup` | +| **1** | Tooling enforcement | High β€” produces errors/warnings the agent must address | WinUI analyzer NuGet rules, `winapp find-ui` / `find-api`, `winapp` CLI exit codes | | **2** | Templates / scaffolding | Medium β€” structural, applied at creation time | `Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`, starter project files | | **3** | Instructions / skills | Lowest β€” advisory, frequently ignored | `SKILL.md` content, `winui-dev.agent.md` rules, `references/*.md` | ### Upstream alternatives (when this repo isn't the right place at all) -Two of the most leveraged Tier 0/1/2 surfaces this plugin depends on +The most leveraged Tier 0/1/2 surfaces this plugin depends on live in **other repositories**. When a finding's recommendation lands naturally on one of them, name the upstream surface explicitly so the contributor can decide whether to file an issue there instead of @@ -152,6 +151,7 @@ working around it locally: | Upstream | Lives in | Right for | |----------|----------|-----------| | **`winapp` CLI** ([`microsoft/winappcli`](https://github.com/microsoft/winappcli)) | external repo, installed via `winget install Microsoft.WinAppCLI` | New install/run/sign/package/automate behavior; better error messages from `winapp run`, `winapp ui`, `winapp manifest`, `winapp pack`; new subcommands the skill currently scripts around. The skills are co-developed with this CLI in lockstep, so "land it in `winapp`" is often the highest-leverage option. | +| **WinUI analyzer NuGet** (`Microsoft.Windows.SDK.BuildTools.WinUIAnalyzer`) | `microsoft/winappCli/src/winapp-Analyzer` | Analyzer rules, tests, and package targets. All implementation changes belong upstream; this repo documents package consumption only. | | **WinUI 3 .NET templates** (`Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`) | shipped on [NuGet](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.WinUI.CSharp.Templates) by the WinAppSDK team | New "every WinUI 3 app should start with X" defaults β€” pre-wired dependencies, default `app.manifest` settings, baseline MVVM scaffolding, default analyzer references. Anything the agent re-types into `dotnet new` output every time is a template request. | **Rule:** When a finding recommends *adding* something to a `SKILL.md` diff --git a/.github/skills/pr-review/dimensions/docs-and-manifests.md b/.github/skills/pr-review/dimensions/docs-and-manifests.md index 3cc331ee..f363a3ef 100644 --- a/.github/skills/pr-review/dimensions/docs-and-manifests.md +++ b/.github/skills/pr-review/dimensions/docs-and-manifests.md @@ -13,7 +13,7 @@ The repo's user-facing surface and its install/discovery metadata. When code or skills change, these need to keep up: - `README.md` β€” top-level pitch, install instructions, the "8 skills" - table, the "in-repo tools" table. + table, external prerequisites, and upstream tool ownership. - `plugins/winui/agent-plugin/plugin.json` β€” portable Agent Plugins manifest (identity, metadata, and extension declarations). - `.github/plugin/marketplace.json` β€” marketplace registry pointing @@ -25,10 +25,6 @@ When code or skills change, these need to keep up: `plugins/winui/agent-plugin/com.github.copilot/agents/winui-dev.agent.md` β€” compatibility and Copilot orchestrator prompts; mention specific skills by name and list default-loaded skills. -- `src/tools/winui-analyzer/RULES.md` β€” rule catalog (per-rule entry - required for every shipped diagnostic; IDs are immutable). -- `src/tools/winui-analyzer/CHANGELOG.md` β€” analyzer-scoped changelog. -- Per-tool READMEs: `src/tools/winui-analyzer/README.md`. - `SECURITY.md`, `SUPPORT.md`, `THIRD_PARTY_NOTICES.md`, `cgmanifest.json` β€” only relevant when dependencies or contact surfaces change. @@ -54,30 +50,15 @@ When code or skills change, these need to keep up: don't need a manifest edit, but if the glob ever narrows or a new skill lives outside `plugins/winui/agent-plugin/skills/`, flag it. -### New / renamed / removed analyzer rule +### External contracts and helper scripts -- **New rule shipped without a `RULES.md` entry.** `RULES.md` is the - single source of truth ("Adding, removing, or changing the - severity of a rule requires updating this file in the same PR"). - β†’ **high**. -- **Rule severity changed in code without `RULES.md` update** β†’ - **high**. -- **Rule removed but `RULES.md` row deleted.** Repo policy: removed - rules stay listed with a "removed in vX.Y" note. Deleting the row - silently breaks the immutability contract. β†’ **high**. -- **`CHANGELOG.md` not updated** for a user-visible analyzer change - (new rule, severity change, false-positive fix) β†’ **medium**. -- **Per-tool `README.md` rule-category table** out of sync with new - rule's category β†’ **medium**. - -### New / renamed / removed in-repo tool - -- **New tool under `src/tools/`** without a row in `README.md`'s - "in-repo tools" table β†’ **high**. -- **New tool without its own `README.md`** β†’ **medium**. -- **Tool's distribution path renamed** (e.g. exe moves out of - `winui-design/` into a different skill) without README update and - CI workflow update β†’ **high**. +- Changes to the required analyzer package or CLI surface must update setup + and consumption guidance together. Reference upstream rule documentation, + not a local duplicate catalog. +- A new or removed shipped helper needs a README update explaining its + purpose, execution scope, and any privacy implications. +- A helper path renamed without updating its skill references and CI + regression paths is a **high** finding. ### Version bumps @@ -85,9 +66,8 @@ When code or skills change, these need to keep up: `.github/plugin/marketplace.json` `metadata.version` and `plugins[].version` should match. Diff that bumps one but not the others β†’ **high**. -- A user-visible plugin change (new skill, removed skill, new tool) - with no version bump β†’ **medium** (judgment call; preview repo, - but bumps help downstream). +- Feature PRs must not bump version fields. User-facing changes belong in + CHANGELOG `[Unreleased]`; only release/hotfix PRs change versions. ### Agent-file currency @@ -102,8 +82,7 @@ When code or skills change, these need to keep up: - `.github/workflows/pr-validation.yml` `validate-skill-frontmatter` walks `find plugins/winui/agent-plugin/skills -type f -name SKILL.md`. New skills outside this glob won't be validated β†’ **medium**. -- Any CI step's hardcoded file path - (e.g. `plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.dll`) +- Any CI step's hardcoded source or test file path changed in the diff but not in the workflow β†’ **high**. ### Other docs @@ -122,21 +101,15 @@ When code or skills change, these need to keep up: - Asking to update docs for behavior that didn't change. - Asking to update `THIRD_PARTY_NOTICES.md` when no dependency changed. -- Auto-generated artifacts that the build refreshes β€” flag the - build, not the artifact. -- "Bump the version" suggestions for diffs that aren't user-visible - (internal refactor, test-only change). +- "Bump the version" suggestions for feature PRs (see `CONTRIBUTING.md`). ## Severity guide for this dimension -- New skill / new tool missing from README β†’ **high**. -- New analyzer rule missing from `RULES.md` β†’ **high**. +- New skill / shipped helper missing from README β†’ **high**. - Skill rename not propagated to `winui-dev.agent.md` β†’ **high**. -- Removed analyzer rule row deleted (instead of marked removed) β†’ - **high**. - `plugin.json` and `marketplace.json` versions out of sync β†’ **high**. - Per-tool README out of date β†’ **medium**. -- Missing CHANGELOG entry for user-visible analyzer change β†’ +- Missing CHANGELOG entry for user-visible integration change β†’ **medium**. - Polish (typo, link target moved) β†’ **low** (only with concrete fix). diff --git a/.github/skills/pr-review/dimensions/multi-model.md b/.github/skills/pr-review/dimensions/multi-model.md index 39106f1f..b34be05b 100644 --- a/.github/skills/pr-review/dimensions/multi-model.md +++ b/.github/skills/pr-review/dimensions/multi-model.md @@ -32,7 +32,7 @@ For each critical/high input finding, independently verify: hallucinated references. 2. **Is the cause-and-effect chain real?** Re-trace the input β†’ sink path yourself. For `payloads-and-tests` findings, check whether - the source-vs-payload diff really shows drift. For + the dependency contract or missing regression is actually broken. For `skill-content` / `skill-tool-boundary` findings, re-read the cited prose in context. 3. **Is the severity reasonable?** If you would set it lower, say diff --git a/.github/skills/pr-review/dimensions/payloads-and-tests.md b/.github/skills/pr-review/dimensions/payloads-and-tests.md index b9ce51dc..72909dcc 100644 --- a/.github/skills/pr-review/dimensions/payloads-and-tests.md +++ b/.github/skills/pr-review/dimensions/payloads-and-tests.md @@ -1,4 +1,4 @@ -# Payloads, provenance & analyzer tests review +# Dependency integration & regression tests review You are the `payloads-and-tests` sub-agent for the win-dev-skills PR review skill. Apply the shared output contract in `_shared-contract.md`. @@ -6,68 +6,30 @@ Set `Domain: payloads-and-tests` on every finding. ## What this dimension owns -This repo commits the analyzer's **prebuilt payloads** alongside source, -and the CI `pr-validation` workflow has provenance jobs that fail the PR -if those payloads drift from source. This dimension's job is to flag -drift *before* the contributor pushes. - -The payloads: - -| Payload (committed) | Source | CI job that catches drift | -|---|---|---| -| `plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.dll` | `src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers/` | `analyzer-provenance` (sha256 + size delta) | -| `plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.targets` | `src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers/Microsoft.WindowsAppSDK.Analyzers.targets` | `analyzer-targets-sync` (byte-identical) | - -Refresh command: `./scripts/build-tools.ps1` (no flags) rebuilds the -analyzer and refreshes its payloads. The contributor will run this β€” -you only flag drift. +The plugin consumes the upstream WinUI analyzer NuGet package and WinApp CLI, +not committed analyzer binaries or a local metadata CLI. This dimension owns +dependency integration and PowerShell regressions. Analyzer implementation +and tests belong upstream; there is no native-tool build in this repository. ## What to look for -### Payload freshness - -- **Analyzer source touched, DLL not refreshed.** Diff includes any - file under - `src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers/` *but - not* - `plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.dll` - β†’ **high**. CI `analyzer-provenance` will fail with a size-delta - > 256 bytes; small toolchain drift is tolerated below that bar but - any deliberate source change will exceed it. -- **`.targets` source touched, payload `.targets` not refreshed.** - Same pattern but for - `Microsoft.WindowsAppSDK.Analyzers.targets`. CI - `analyzer-targets-sync` requires byte-identical files. β†’ **high**. -- **Payload-only change (no source).** The inverse β€” committed DLL - or `.targets` updated without a corresponding source diff. Either the - source was already on `main` (fine β€” this is a refresh PR) or - someone hand-edited the binary (red flag). β†’ **medium**, ask the - contributor to confirm. +### External dependency integration -### Analyzer test coverage +- **Wrong analyzer package ID or inaccurate coverage claims.** The package + is `Microsoft.Windows.SDK.BuildTools.WinUIAnalyzer`; its assembly remains + `Microsoft.WindowsAppSDK.Analyzers`. Neither `winapp run` nor CLI scaffolding + automatically injects it. Preserve its imported XAML targets and use + `PrivateAssets="all"` when installed. Recommend the latest package, but + continue with a notice if unavailable; its absence is not a task blocker. +- **Unpublished prerequisites treated as available.** Source merged upstream + does not establish a consumable CLI or NuGet release. A draft can carry an + explicit CLI release gate. Validate the analyzer-installed and + analyzer-unavailable paths without claiming missing checks ran. +- **Local implementation reintroduced.** Analyzer rules, targets, and CLI + implementation belong in `microsoft/winappCli`; do not request source or + binary refreshes here. -- **New `DiagnosticAnalyzer` / new rule, no test.** Every new rule - in `Microsoft.WindowsAppSDK.Analyzers/Rules/` should have at least - one matching test class in `Microsoft.WindowsAppSDK.Analyzers - .Tests/` covering: (a) a positive case that fires, (b) a negative - case that doesn't, (c) a `#pragma warning disable WUIxxxx` - suppression test (the suite has a `SuppressionTests` convention - β€” see `Microsoft.WindowsAppSDK.Analyzers.Tests` directory). β†’ **high**. -- **Allowlist change without a regression test.** Edits to - `Allowlists.cs` should be paired with a test asserting the - formerly-flagged code is now silent. -- **`ApiMappings.g.cs` / `FeatureMappings.g.cs` regenerated by hand.** - These are data-driven from Microsoft Learn (per `RULES.md`) and - should be refreshed by the documented generation step, not edited - in place. Ad-hoc edits β†’ **medium**, recommend the regen path. -- **Test-project compile failure pattern.** New tests that don't - follow the `xUnit` naming used in `SuppressionTests` (e.g. - `Suppress_Wui4101`) β€” the test csproj opts out of CA1707 - precisely so these names work. New tests using a different - naming style are fine, but tests that try to assert on disabled - CA1707 in the test project β†’ **medium** (will be fragile). - -### `BuildAndRun.ps1` & `Analyze-Session.ps1` +### `Analyze-Session.ps1` and documented test scripts These are also "tool" payloads (PowerShell scripts ship inside the skill folders). Treat changes the same way as `tool-correctness` @@ -76,48 +38,34 @@ findings, but additionally: - **Behavior change with no documented rationale.** These scripts ship to end users via the plugin install. New flags or changed defaults should be obvious from the script's own comment block. -- **Cross-contamination with skill prose.** If new prose in - `winui-dev-workflow/SKILL.md` describes a `BuildAndRun.ps1` - feature that the script doesn't actually implement (or vice +- **Cross-contamination with skill prose.** If new prose describes a script + feature that the implementation doesn't actually provide (or vice versa), β†’ **high** (drift between Tier 1 and Tier 3). - -### `scripts/build-tools.ps1` - -- **New tool added under `src/tools/`** without a corresponding - build/publish step in `build-tools.ps1` β†’ **medium** (the new - tool will fall out of the one-verb-build contract). -- **Payload destination renamed** in source (e.g. moving where the - DLL goes) without updating `build-tools.ps1` and the matching - CI provenance job paths β†’ **high** (CI will silently start - comparing the wrong files). +- **False passes or target leakage.** UI test scripts must fail on CLI errors + and empty/malformed inspection results, preserve Sandbox scope for every + command, and retain screenshot evidence. Changed session command detection + needs regression cases for build/publish, no-build, project versus folder + packaging, and historical wrapper commands. ### `.github/workflows/pr-validation.yml` -- New CI step added that needs a corresponding contributor-side - command in `build-tools.ps1` (so contributors can reproduce - locally) β€” flag if the workflow gets ahead of the script. -- Path constants (`COMMITTED`, `SRC`, `DIST` in the workflow yaml) - changed without updating the matching path in `build-tools.ps1` - β†’ **high**. +- New regression checks need a corresponding contributor-side command in + `CONTRIBUTING.md`, so contributors can reproduce CI locally. +- Removed jobs still named as required checks in the release playbook need + an explicit maintainer migration step; otherwise PRs can wait indefinitely. ## What to drop -- Asking for tests on auto-generated code (`*.g.cs`). - Asking for "more coverage" without naming a specific uncovered branch. -- Asking the contributor to *run* `build-tools.ps1` β€” they will, and - CI will catch them if not. Just flag the staleness. -- Suggesting the analyzer payload move out of the skill folder. The - long-term plan in `README.md` is to publish as a NuGet package; - until then the in-repo payload is the documented design. +- Asking the contributor to run every check without identifying a missing + regression. CI runs the checks; flag the uncovered behavior instead. +- Asking for a committed analyzer DLL or duplicate targets; the package + supplies them. ## Severity guide for this dimension -- Source touched but committed payload not refreshed (DLL or .targets) - β†’ **high** (CI provenance will fail). -- New analyzer rule with no test β†’ **high**. -- New tool not wired into `build-tools.ps1` β†’ **medium**. -- Allowlist change without regression test β†’ **medium**. -- Hand-edited `*.g.cs` mapping file β†’ **medium**. -- Payload-only change with no source diff (clarification needed) β†’ - **medium**. +- Broken package integration, false passing tests, or host input from a + guest-scoped test β†’ **high**. +- Changed helper behavior without a regression case β†’ **high**. +- A new regression check not wired into CI β†’ **medium**. diff --git a/.github/skills/pr-review/dimensions/skill-tool-boundary.md b/.github/skills/pr-review/dimensions/skill-tool-boundary.md index 790c741b..099a945e 100644 --- a/.github/skills/pr-review/dimensions/skill-tool-boundary.md +++ b/.github/skills/pr-review/dimensions/skill-tool-boundary.md @@ -14,18 +14,18 @@ the agent reads the prompt or not. ## The Solution Hierarchy in this repo Re-stated for emphasis. Every finding in this dimension cites a tier. -**In-repo** options come first; upstream options are listed in the -`_shared-contract.md` "Upstream alternatives" section and you must -consider them before defaulting to Tier 3 prose. +Choose the owning tool first, including upstream options listed in +`_shared-contract.md`, before defaulting to Tier 3 prose. Analyzer and CLI +implementation live in `microsoft/winappCli`, not in this content repository. | Tier | Type | Reliability | In-repo examples | |------|------|-------------|-------------------| -| **0** | Environment / harness defaults | Highest β€” agent never sees it | `winapp new` template choice, `BuildAndRun.ps1` defaults, `winui-setup` prerequisite checks | -| **1** | Tooling enforcement | High β€” produces diagnostics agent must address | `Microsoft.WindowsAppSDK.Analyzers` rules (WUI0xxx-WUI4xxx), `winapp find-ui` queries, `winapp find-api` API verification, `winapp` CLI exit codes | +| **0** | Environment / harness defaults | Highest β€” agent never sees it | `winapp new` template choice, project analyzer references, `winui-setup` prerequisite checks | +| **1** | Tooling enforcement | High β€” produces diagnostics agent must address | WinUI analyzer NuGet rules (WUI0xxx-WUI4xxx), `winapp find-ui` / `find-api`, `winapp` CLI exit codes | | **2** | Templates / scaffolding | Medium β€” structural, applied once | `Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`, starter `.csproj` defaults | | **3** | Instructions / skills | Lowest β€” advisory, frequently ignored | `SKILL.md` content, `winui-dev.agent.md` rules, `references/*.md` | -**Always also consider the two upstream surfaces** documented in the +**Always consider the upstream surfaces** documented in the shared contract: - **`winapp` CLI** (`microsoft/winappcli`) β€” for any new install / run / @@ -36,6 +36,8 @@ shared contract: - **`Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`** β€” for any "every new WinUI 3 app should start with X" guidance the agent currently re-types into `dotnet new` output. +- **`Microsoft.Windows.SDK.BuildTools.WinUIAnalyzer`** in `microsoft/winappCli` + for published diagnostics and project targets. ## What to look for @@ -57,7 +59,7 @@ This is the most common drift. Symptoms: query* the tool, not embed the catalogue. - A new bullet that says **"after building, do X"**. General build/run behavior belongs in the upstream `winapp` command so every caller gets it; - reserve `BuildAndRun.ps1` for analyzer injection and diagnostic defaults. + do not reintroduce a plugin wrapper for analyzer injection or diagnostics. - A new "common error" entry that boils down to a missing prerequisite β€” that belongs in `winui-setup` (Tier 0/1). @@ -67,8 +69,8 @@ For each such finding: - Recommendation: name the specific Tier 1 hook β€” "Add an analyzer rule under `WUI20xx` (runtime/layout/XAML pitfalls)", "Improve the upstream `winapp find-ui` corpus", or "Improve the relevant upstream - `winapp` subcommand". Recommend `BuildAndRun.ps1` only for analyzer or - diagnostic-default behavior. + `winapp` subcommand". Analyzer configuration belongs in its NuGet targets + and the project's package reference. ### Skill prose that should be a Tier 2 (template) change @@ -83,8 +85,7 @@ For each such finding: - A new "before doing anything, verify Z" instruction. If Z is a prerequisite, `winui-setup` (which is `user-invocable: true`) - should check it; or `BuildAndRun.ps1` should fail fast with a - helpful message. + should check it; or the owning CLI should fail fast with a helpful message. ### Skill prose that should be a `winapp` CLI change (upstream Tier 1) @@ -180,8 +181,7 @@ justification, the addition is misplaced β€” emit a finding. - New skill prose duplicating an existing analyzer rule β†’ **medium** (Tier 3, recommend cite-the-rule-instead). - New skill prose that should clearly have been a new analyzer rule, - an upstream `winapp find-ui` improvement, or a new `BuildAndRun.ps1` - step β†’ **high** + an upstream `winapp find-ui` improvement, or a `winapp run` change β†’ **high** if the change is large and the Tier 1 path is straightforward; **medium** otherwise. - New skill prose that should clearly have been a `winapp` CLI diff --git a/.github/skills/pr-review/dimensions/tool-correctness.md b/.github/skills/pr-review/dimensions/tool-correctness.md index a5178829..04254ec9 100644 --- a/.github/skills/pr-review/dimensions/tool-correctness.md +++ b/.github/skills/pr-review/dimensions/tool-correctness.md @@ -3,7 +3,7 @@ You are the `tool-correctness` sub-agent for the win-dev-skills PR review skill. The orchestrator runs you with the `code-review` agent type, which already specializes in generic bug, security, and -correctness review of C# and PowerShell. **Do not re-implement that +correctness review of PowerShell. **Do not re-implement that job.** Your role is to enforce the repo-specific rules below *on top of* the standard code-review pass, and to consolidate everything into the shared output contract. @@ -14,13 +14,10 @@ optional here (these changes are inherently Tier 1). ## Scope -C# / PowerShell code under: +PowerShell code under: -- `src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers/` β€” Roslyn - analyzer (netstandard2.0). -- `plugins/winui/agent-plugin/skills/winui-dev-workflow/BuildAndRun.ps1` - `plugins/winui/agent-plugin/skills/winui-session-report/Analyze-Session.ps1` -- `scripts/build-tools.ps1` +- `scripts/` β€” release helper and workflow regression checks - `.github/skills/*/collect-diff.ps1` and similar repo-internal helpers These ship and run on contributors' or end-user machines. Bugs here @@ -28,46 +25,31 @@ directly break agent sessions. ## Repo-specific rules (the deltas the built-in code-review won't know) -### WinUI analyzer (Microsoft.WindowsAppSDK.Analyzers) - -- **Severity ceiling.** Repo policy: no rule ships at `Error` by - default; ceiling is `Warning`. New `DiagnosticDescriptor` entries - with `DiagnosticSeverity.Error` violate this β€” flag as **high**. -- **Missing `helpLinkUri`.** Every diagnostic must include one (see - `HelpLinks.cs`). New rules without it β†’ **medium**. -- **ID immutability.** `RULES.md` is the source of truth and IDs are - immutable. New code that reuses a removed ID, or changes the ID of - an existing rule, β†’ **critical**. -- **Category alignment.** New rules must land in the right `WUIcXxx` - range (0=compat, 1=migration, 2=runtime/XAML, 3=MVVM, 4=interop). - Misclassified IDs β†’ **medium**. -- **False-positive guards.** Primary audience is external developers; - FPs erode trust. New rules should consult `ProjectContext` if - UWP-vs-greenfield-sensitive, respect `Allowlists.cs` carve-outs - where applicable, and match symbols via `INamedTypeSymbol` / - fully-qualified names rather than string-name comparison (which - catches user types that happen to share a name). -- **Roslyn API discipline.** Use `SymbolEqualityComparer.Default` - explicitly. Avoid `SyntaxNode.ToString()` for identity matching - (use structural compare). No analyzer state across `Compilation` - boundaries β€” that causes IDE-vs-build inconsistency. -- **Performance.** Analyzers run on every keystroke in the IDE. New - rules doing heavy LINQ or recursive tree walks per node β†’ - **medium**; cache lookups via `RegisterCompilationStartAction` - instead. +### External tool contracts + +- **AOT versus normal builds.** Release alone does not make `winapp run` + execute native output; `run --aot` requires effective `PublishAot=true`. + Project packaging uses publish properties, not a `package --aot` flag. +- **Sandbox scope.** Guest PIDs and HWNDs must retain `--on sandbox`. + Prefer Windows Sandbox when available; announced local execution is valid + when unavailable, unless the user explicitly requested Windows Sandbox. + Never silently redirect guest IDs or failed tests to the host. +- **Analyzer delivery.** The NuGet ID is + `Microsoft.Windows.SDK.BuildTools.WinUIAnalyzer`, not its assembly name. + Recommend the latest version. If unavailable, continue with a coverage + notice; do not require it as a task prerequisite or claim it ran. ### Repo-specific PowerShell rules The built-in code-review will catch generic PowerShell issues. The deltas to enforce here: -- **`BuildAndRun.ps1` ↔ skill prose drift.** Behavior changes in the - script must match what `winui-dev-workflow/SKILL.md` advertises (and +- **Shipped script ↔ skill prose drift.** Behavior changes in a + script must match what its `SKILL.md` advertises (and vice versa). Drift between Tier 1 (script) and Tier 3 (skill) is a **high** finding β€” call out which side is wrong, don't just note the mismatch. -- **Temp-file cleanup pattern.** `BuildAndRun.ps1` writes a temporary - `Directory.Build.props` and removes it on exit. New scripts that +- **Temp-file cleanup.** New scripts that drop temp files, install temp packages, or register temp appx packages without `try/finally` cleanup β†’ **high** (CI / contributor machine pollution). @@ -78,27 +60,17 @@ deltas to enforce here: ## What to drop (in addition to the shared Team Lead Test) -- Style suggestions covered by `EnforceCodeStyleInBuild=true` (the - analyzer subtree's `Directory.Build.props` enables it). -- Generic "missing `using`", "use `var`", "expression-bodied member" - bikeshedding β€” code-review's own filtering already handles this. +- Style-only suggestions; code-review's own filtering already handles them. - Anything CodeQL (`.github/workflows/codeql.yml`) already catches. ## Severity guide for repo-specific deltas -- AOT-incompat reflection / `BinaryFormatter` β†’ **critical** (will - crash users at runtime). -- Reused or changed analyzer rule ID β†’ **critical**. -- New analyzer rule shipping at `Error` severity β†’ **high**. -- `BuildAndRun.ps1` ↔ skill prose drift β†’ **high**. +- Shipped script ↔ skill prose drift or incorrect target routing β†’ **high**. - Temp-file cleanup gap in shipped scripts β†’ **high**. -- Missing `helpLinkUri` on a new analyzer rule β†’ **medium**. -- Misclassified analyzer rule ID range β†’ **medium**. -- Analyzer perf concern with concrete trigger β†’ **medium**. +- Unsupported external command/package contract β†’ **high**. For generic bug, security, async, disposal, path-traversal, exit-code, and process-launch issues, defer to the built-in code-review pass β€” emit findings only when the issue is also tied to one of the -repo-specific rules above (e.g. a path traversal *in* an analyzer -rule's IO, where the consequences are amplified by the analyzer's -trust position). +repo-specific rules above (e.g. a helper accidentally directing +guest-scoped UI input to the host desktop). diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 55be8195..82c17d01 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,8 +29,6 @@ jobs: include: - language: actions build-mode: none - - language: csharp - build-mode: none steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index fc6048f0..56d7796d 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -11,92 +11,24 @@ permissions: contents: read jobs: - build-tools: - name: Build C# tools - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Setup .NET - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 - with: - dotnet-version: | - 8.0.x - 10.0.x - - - name: Build Microsoft.WindowsAppSDK.Analyzers (Roslyn analyzer) - # No -warnaserror flag here: the analyzer subtree's Directory.Build.props - # already turns it on for production code, and the test csproj opts out. - # The CLI flag would force it on for tests too, blocking xUnit naming - # conventions like Suppress_Wui4101 (CA1707). - run: dotnet build src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.slnx -c Release - - - name: Test Microsoft.WindowsAppSDK.Analyzers - run: dotnet test src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Microsoft.WindowsAppSDK.Analyzers.Tests.csproj -c Release --no-build --logger "console;verbosity=normal" - - - name: Upload analyzer DLL artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: Microsoft.WindowsAppSDK.Analyzers-built - path: src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers/bin/Release/netstandard2.0/Microsoft.WindowsAppSDK.Analyzers.dll - if-no-files-found: error - - analyzer-provenance: - name: Analyzer DLL provenance - needs: build-tools - runs-on: ubuntu-latest + powershell-tests: + name: PowerShell workflow regressions + runs-on: windows-latest steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Download CI-built analyzer DLL - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: Microsoft.WindowsAppSDK.Analyzers-built - path: ci-built/ + - name: Test session build classification + shell: pwsh + run: ./scripts/tests/Test-SessionBuildClassification.ps1 - - name: Compare CI-built vs committed analyzer DLL - shell: bash - run: | - set -euo pipefail + - name: Test Sandbox UI script contracts + shell: pwsh + run: ./scripts/tests/Test-WinuiUiTestingSandbox.ps1 - COMMITTED="plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.dll" - CI_BUILT="ci-built/Microsoft.WindowsAppSDK.Analyzers.dll" - - if [[ ! -f "$COMMITTED" ]]; then - echo "::error::Committed analyzer DLL not found at $COMMITTED" - exit 1 - fi - - COMMITTED_HASH=$(sha256sum "$COMMITTED" | awk '{print $1}') - CI_HASH=$(sha256sum "$CI_BUILT" | awk '{print $1}') - COMMITTED_SIZE=$(stat -c %s "$COMMITTED") - CI_SIZE=$(stat -c %s "$CI_BUILT") - - echo "Committed: $COMMITTED ($COMMITTED_SIZE bytes, sha256=$COMMITTED_HASH)" - echo "CI-built: $CI_BUILT ($CI_SIZE bytes, sha256=$CI_HASH)" - - if [[ "$COMMITTED_HASH" == "$CI_HASH" ]]; then - echo "::notice::Analyzer DLL hash matches β€” provenance verified." - exit 0 - fi - - # Hashes differ. Distinguish "source changed without rebuild" (real - # problem) from "deterministic-build drift" (toolchain / SDK delta; - # less alarming) by comparing sizes as a coarse proxy. - SIZE_DELTA=$(( CI_SIZE - COMMITTED_SIZE )) - ABS_DELTA=${SIZE_DELTA#-} - - if [[ "$ABS_DELTA" -gt 256 ]]; then - echo "::error::Analyzer DLL hash mismatch and size differs by $SIZE_DELTA bytes." - echo "::error::This usually means src/tools/winui-analyzer/ was changed without rebuilding and recommitting Microsoft.WindowsAppSDK.Analyzers.dll." - echo "::error::Run: dotnet build src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers/Microsoft.WindowsAppSDK.Analyzers.csproj -c Release" - echo "::error::Then copy bin/Release/netstandard2.0/Microsoft.WindowsAppSDK.Analyzers.dll into $COMMITTED and commit." - exit 1 - else - echo "::warning::Analyzer DLL hash differs but size delta is small ($SIZE_DELTA bytes) β€” likely deterministic-build drift across SDK versions, not a source change. Investigate before merging." - fi + - name: Test setup version detection + shell: pwsh + run: ./scripts/tests/Test-SetupVersionDetection.ps1 validate-plugin-manifest: name: Validate Agent Plugins package @@ -185,6 +117,19 @@ jobs: errors.append("plugin.json extensions must map namespaces to objects") skills_root = plugin_root / "skills" + retired_paths = ( + skills_root / "winui-dev-workflow/BuildAndRun.ps1", + skills_root / "winui-dev-workflow/analyzer", + Path("src/tools/winmd-cli"), + Path("src/tools/winui-analyzer"), + Path("scripts/build-tools.ps1"), + ) + for retired_path in retired_paths: + if retired_path.exists(): + errors.append( + f"Retired local tooling must not be redistributed: {retired_path}" + ) + if not skills_root.is_dir(): errors.append(f"Portable skills directory not found at {skills_root}") else: @@ -356,41 +301,3 @@ jobs: while IFS= read -r -d '' skill; do skills-ref validate "$(dirname "$skill")" done < <(find plugins/winui/agent-plugin/skills -mindepth 2 -maxdepth 2 -type f -name SKILL.md -print0) - - analyzer-targets-sync: - name: Analyzer .targets in sync - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Compare duplicated analyzer .targets files - shell: bash - run: | - set -euo pipefail - - # The analyzer .targets file is currently committed in two places: - # - source-of-truth in the source tree - # - distribution copy under the skill payload - # They MUST be byte-identical until the analyzer is published as a - # NuGet package and the skill payload copy goes away - # (see launch tracker Β§12.3 / P1-NEW-D). - - SRC="src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers/Microsoft.WindowsAppSDK.Analyzers.targets" - DIST="plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.targets" - - for f in "$SRC" "$DIST"; do - if [[ ! -f "$f" ]]; then - echo "::error::Expected analyzer .targets at $f" - exit 1 - fi - done - - if ! diff -q "$SRC" "$DIST" >/dev/null; then - echo "::error::$SRC and $DIST have drifted." - echo "::error::These files must be byte-identical. Update both copies in the same commit." - diff -u "$SRC" "$DIST" || true - exit 1 - fi - - echo "Analyzer .targets files are in sync." diff --git a/.pipelines/ci.yml b/.pipelines/ci.yml index 78a71c33..b8de04e8 100644 --- a/.pipelines/ci.yml +++ b/.pipelines/ci.yml @@ -18,8 +18,8 @@ resources: extends: template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines parameters: - # SDL/compliance (PoliCheck, CredScan, Roslyn, BinSkim, etc.) is provided - # by the 1ES template. Source analysis runs on this pool. + # Keep source compliance scanning for the content plugin. Native tooling + # builds and binary analysis belong to microsoft/winappCli. sdl: policheck: enabled: true @@ -28,9 +28,9 @@ extends: image: windows-latest os: windows stages: - - stage: Build + - stage: Compliance jobs: - - job: Build + - job: SourceChecks pool: name: Azure-Pipelines-1ESPT-ExDShared image: windows-latest @@ -38,13 +38,3 @@ extends: hostArchitecture: amd64 steps: - checkout: self - - task: UseDotNet@2 - displayName: Setup .NET 10 - inputs: - version: 10.0.x - # The 1ES pool blocks public api.nuget.org. Swap in a nuget.config - # that points at the internal pde-oss_Internal upstream mirror feed. - - script: move /Y $(Build.SourcesDirectory)\.pipelines\release-nuget.config $(Build.SourcesDirectory)\nuget.config - displayName: Use internal NuGet feed - - task: NuGetAuthenticate@1 - - template: ./.pipelines/templates/build.yaml@self diff --git a/.pipelines/release-nuget.config b/.pipelines/release-nuget.config deleted file mode 100644 index 5756ca12..00000000 --- a/.pipelines/release-nuget.config +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - diff --git a/.pipelines/templates/build.yaml b/.pipelines/templates/build.yaml deleted file mode 100644 index c2ca9517..00000000 --- a/.pipelines/templates/build.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Builds the C# tools in this repo so SDL/compliance scanners (PoliCheck, -# CredScan, Roslyn, BinSkim, etc.) injected by the 1ES template have source -# and binaries to analyze. This pipeline is compliance-only: it does not -# publish artifacts, sign anything, or produce releases. PR validation lives -# in .github/workflows/pr-validation.yml. - -steps: -- task: PowerShell@2 - displayName: Build C# tools (analyzer) - inputs: - pwsh: true - filePath: $(System.DefaultWorkingDirectory)\scripts\build-tools.ps1 - arguments: '-SkipPayloadRefresh' diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e0f6ff5..3a697fd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ strict SemVer. ## [Unreleased] + - + ``` +In a page/window, `x:Bind` resolves against code-behind (e.g., its `Vm` property), not `DataContext`. Use `x:DataType` on typed **DataTemplates**, not on `Page` to set a VM. Runtime `{Binding}`/`DisplayMemberPath` can be appropriate; for AOT, their source classes may need `partial` plus `[WinRT.GeneratedBindableCustomProperty]`. See [source-generator patterns](../winui-packaging/references/sourcegen-patterns.md) instead of treating all runtime binding as unsupported. + ### `TextBox` two-way needs `UpdateSourceTrigger=PropertyChanged` ```xml diff --git a/plugins/winui/agent-plugin/skills/winui-dev-workflow/BuildAndRun.ps1 b/plugins/winui/agent-plugin/skills/winui-dev-workflow/BuildAndRun.ps1 deleted file mode 100644 index e39fd8ed..00000000 --- a/plugins/winui/agent-plugin/skills/winui-dev-workflow/BuildAndRun.ps1 +++ /dev/null @@ -1,178 +0,0 @@ -<# -.SYNOPSIS -Runs a WinUI 3 project with WinApp CLI 0.7+ and the bundled analyzer. - -.DESCRIPTION -WinApp CLI owns input resolution, restore, build, architecture selection, -output discovery, runtime setup, package registration, and launch. This thin -wrapper forwards its arguments unchanged, injects -Microsoft.WindowsAppSDK.Analyzers through MSBuild, and enables ---debug-output by default for attached runs. - -Use WinApp's `--args ""` form for application arguments; PowerShell -consumes an unquoted `--` delimiter when it invokes another PowerShell script. - -.EXAMPLE -.\BuildAndRun.ps1 -.\BuildAndRun.ps1 MyApp.csproj -c Release --arch arm64 -.\BuildAndRun.ps1 . --detach --json -.\BuildAndRun.ps1 . --symbols -#> - -$ErrorActionPreference = 'Stop' -$minimumWinAppVersion = [version]'0.7.0' -$arguments = @($args) - -$winapp = Get-Command winapp -ErrorAction SilentlyContinue -if (-not $winapp) { - Write-Error "WinApp CLI 0.7 or later is required. Run /winui-setup, then retry." - exit 1 -} - -$installedWinAppVersion = $null -foreach ($line in @(& winapp --version 2>$null)) { - $match = [regex]::Match( - [string]$line, - '^\s*v?(?\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\s*$' - ) - if ($match.Success) { - $installedWinAppVersion = [version]$match.Groups['version'].Value - } -} -if (-not $installedWinAppVersion) { - Write-Error "Could not determine the WinApp CLI version. Run /winui-setup, then retry." - exit 1 -} -if ($installedWinAppVersion -lt $minimumWinAppVersion) { - Write-Error "WinApp CLI $minimumWinAppVersion or later is required; found $installedWinAppVersion. Run /winui-setup to upgrade." - exit 1 -} - -$analyzerDir = Join-Path $PSScriptRoot 'analyzer' -$analyzerDll = Join-Path $analyzerDir 'Microsoft.WindowsAppSDK.Analyzers.dll' -$analyzerTargets = Join-Path $analyzerDir 'Microsoft.WindowsAppSDK.Analyzers.targets' -if (-not (Test-Path -LiteralPath $analyzerDll -PathType Leaf) -or - -not (Test-Path -LiteralPath $analyzerTargets -PathType Leaf)) { - Write-Error "The bundled WinUI analyzer payload is incomplete. Reinstall the winui plugin." - exit 1 -} - -$valueOptions = @( - '--manifest', '--output-appx-directory', '--args', '--exe', '--executable', - '-c', '--configuration', '--arch', '-r', '--runtime', '-f', '--framework', - '-p', '--property', '--project' -) -$debugSpecified = $false -$jsonOutput = $false -$noLaunch = $false -$detach = $false - -function Resolve-BooleanOption { - param( - [System.Text.RegularExpressions.Match]$Match, - [ref]$Index, - [object[]]$Tokens - ) - - if ($Match.Groups['value'].Success) { - return $Match.Groups['value'].Value -ieq 'true' - } - if ($Index.Value + 1 -lt $Tokens.Count -and - [string]$Tokens[$Index.Value + 1] -match '^(?i:true|false)$') { - $Index.Value++ - return [string]$Tokens[$Index.Value] -ieq 'true' - } - return $true -} - -for ($i = 0; $i -lt $arguments.Count; $i++) { - $token = [string]$arguments[$i] - - if ($token -eq '--') { - Write-Error 'PowerShell consumes an unquoted -- delimiter for script calls. Pass application arguments with --args "".' - exit 1 - } - - if ($valueOptions -icontains $token) { - if ($i + 1 -lt $arguments.Count) { - $value = [string]$arguments[++$i] - if ($token -in '-p', '--property' -and - $value -match '(?i)^CustomAfterDirectoryBuildProps\s*=') { - Write-Error "CustomAfterDirectoryBuildProps is reserved for loading the bundled WinUI analyzer." - exit 1 - } - } - continue - } - - if ($token -match '(?i)^(?:-p|--property)[:=](?.*)$') { - if ($Matches['value'] -match '(?i)^CustomAfterDirectoryBuildProps\s*=') { - Write-Error "CustomAfterDirectoryBuildProps is reserved for loading the bundled WinUI analyzer." - exit 1 - } - continue - } - - $booleanMatch = [regex]::Match($token, '(?i)^--debug-output(?:=(?true|false))?$') - if ($booleanMatch.Success) { - $debugSpecified = $true - [void](Resolve-BooleanOption -Match $booleanMatch -Index ([ref]$i) -Tokens $arguments) - continue - } - $booleanMatch = [regex]::Match($token, '(?i)^--json(?:=(?true|false))?$') - if ($booleanMatch.Success) { - $jsonOutput = Resolve-BooleanOption -Match $booleanMatch -Index ([ref]$i) -Tokens $arguments - continue - } - $booleanMatch = [regex]::Match($token, '(?i)^--no-launch(?:=(?true|false))?$') - if ($booleanMatch.Success) { - $noLaunch = Resolve-BooleanOption -Match $booleanMatch -Index ([ref]$i) -Tokens $arguments - continue - } - $booleanMatch = [regex]::Match($token, '(?i)^--detach(?:=(?true|false))?$') - if ($booleanMatch.Success) { - $detach = Resolve-BooleanOption -Match $booleanMatch -Index ([ref]$i) -Tokens $arguments - } -} - -$tempBuildProps = Join-Path ([System.IO.Path]::GetTempPath()) "winui-build-$([guid]::NewGuid().ToString('N')).props" -$escapedAnalyzerDll = [System.Security.SecurityElement]::Escape((Resolve-Path -LiteralPath $analyzerDll).Path) -$escapedAnalyzerTargets = [System.Security.SecurityElement]::Escape((Resolve-Path -LiteralPath $analyzerTargets).Path) -$propsContent = @" - - - - - - -"@ - -$runArgs = $arguments -if (-not $debugSpecified -and -not $jsonOutput -and -not $noLaunch -and -not $detach) { - $runArgs += '--debug-output' -} - -$runExitCode = 1 -$previousCustomAfterProps = $env:CustomAfterDirectoryBuildProps -try { - Set-Content -LiteralPath $tempBuildProps -Value $propsContent -Encoding utf8 - # Microsoft.Common.props accepts a semicolon-delimited import list; the .NET SDK - # composes its own UseArtifactsOutputPath.props hook the same way. - $env:CustomAfterDirectoryBuildProps = if ($previousCustomAfterProps) { - "$previousCustomAfterProps;$tempBuildProps" - } else { - $tempBuildProps - } - & winapp run @runArgs - $runExitCode = $LASTEXITCODE -} -finally { - if ($null -eq $previousCustomAfterProps) { - Remove-Item Env:\CustomAfterDirectoryBuildProps -ErrorAction SilentlyContinue - } else { - $env:CustomAfterDirectoryBuildProps = $previousCustomAfterProps - } - Remove-Item -LiteralPath $tempBuildProps -Force -ErrorAction SilentlyContinue -} - -exit $runExitCode diff --git a/plugins/winui/agent-plugin/skills/winui-dev-workflow/SKILL.md b/plugins/winui/agent-plugin/skills/winui-dev-workflow/SKILL.md index ef83b3c2..adedbaee 100644 --- a/plugins/winui/agent-plugin/skills/winui-dev-workflow/SKILL.md +++ b/plugins/winui/agent-plugin/skills/winui-dev-workflow/SKILL.md @@ -1,8 +1,10 @@ --- name: winui-dev-workflow -description: "Build and run workflow for WinUI 3 apps with WinApp CLI 0.7+ β€” project creation with winapp new, project-mode winapp run, winapp find-api verification, BuildAndRun.ps1 analyzer integration, crash diagnosis, and prerequisites. Use when creating, building, running, or fixing build errors in a WinUI 3 project." +description: "Build and run workflow for WinUI 3 apps with WinApp CLI 0.7+ β€” project creation with winapp new, per-app NuGet analyzer setup, project-mode winapp run, Native AOT publish runs, crash diagnosis, and prerequisites. Use when creating, building, running, or fixing build errors in a WinUI 3 project." --- +Requires **WinApp CLI 0.7+**. + ### Create or Open a Project **New app** β€” let WinApp CLI install/update the official templates and scaffold: @@ -17,65 +19,40 @@ Run `winapp new --list` to discover the currently installed template short names - `` versions (WindowsAppSDK, CommunityToolkit) - Project structure and established patterns -### Install Packages +### Add or Check the Per-App Analyzer +Add the latest stable analyzer to each app project; do not assume a template includes it: ```powershell -dotnet add package +dotnet add .\MyApp.csproj package Microsoft.Windows.SDK.BuildTools.WinUIAnalyzer ``` -Never specify `--version` β€” omitting it gets the latest stable and avoids outdated API mismatches. - -After a package is added and the project restores, verify the API surface you intend to use with `winapp find-api` rather than guessing β€” see [winui-design](../winui-design/SKILL.md) for the full scoping rules. A `check-property` miss is far cheaper than a build error: -```powershell -# Scope to the project you just restored; --project-dir defaults to the current directory. -winapp find-api check-property ... --project-dir . -``` +Keep `PrivateAssets="all"` on the reference. It loads in normal CLI, IDE, and CI builds; WinApp CLI does not inject it. If the package is unavailable, continue and tell the user its checks for potential runtime issues did not run. Undo only an incomplete reference added by this attempt; do not remove existing references or hide other restore failures. -### Build & Run +For other packages, prefer the latest stable unless the project has a version policy or the user requests a specific version. Before coding API assumptions, use `winapp find-api` scoped to the restored app with `--project-dir ` (or `--project ` in a solution); see [winui-design](../winui-design/SKILL.md). -WinApp CLI 0.7+ builds a `.csproj` and launches it directly: +### Build & Run (JIT Development) ```powershell -winapp run . --debug-output -winapp run .\MyApp.csproj -c Release --arch arm64 +winapp run . --detach --json ``` +For UI testing, see [winui-ui-testing](../winui-ui-testing/SKILL.md), which chooses the execution target itself. -For normal development, prefer the included `BuildAndRun.ps1` wrapper. It invokes project-mode `winapp run`, injects the bundled `Microsoft.WindowsAppSDK.Analyzers`, and turns on `--debug-output` by default: - -```powershell -.\BuildAndRun.ps1 -``` +Ordinary `winapp run` uses the build/JIT path, **even with `-c Release`**; it does not validate Native AOT. Use an explicit `.csproj` when project selection is ambiguous; see `winapp run --help` for options. -**Invoke attached runs with `mode: "async"`.** The command stays attached while the app is open, so a synchronous call blocks for the app's lifetime. The output contains the running app's PID. +**If build fails:** Read all errors, batch-fix them in one pass, then rerun the same command. -The wrapper only adds repository-specific analyzer and debug defaults. WinApp CLI handles: -1. Project restore and build -2. Configuration, architecture, runtime, and framework selection -3. Packaged versus unpackaged detection -4. Build-output and executable discovery -5. Windows App Runtime setup -6. Package registration and launch +### Native AOT Publish Runs -**Options and forwarded WinApp arguments:** -``` -.\BuildAndRun.ps1 # one top-level csproj; attached diagnostics -.\BuildAndRun.ps1 .\MyApp.csproj # explicit project -.\BuildAndRun.ps1 .\MyApp.csproj -c Release # forwarded to winapp run -.\BuildAndRun.ps1 .\MyApp.csproj --arch arm64 # forwarded to winapp run -.\BuildAndRun.ps1 . --detach --json # return after launch; emit PID as JSON -.\BuildAndRun.ps1 . --symbols # add Symbol Server-backed native symbols -.\BuildAndRun.ps1 --args "--flag value" # pass application arguments +For intended AOT deployment, set `true` in the app project (it also enables analysis during development); for a one-off trial, pass `-p PublishAot=true`. `--aot` publishes rather than builds; choose an `--arch` runnable on this machine: +```powershell +winapp run . --aot -c Release --arch --detach --json +winapp run . --aot -c Release --arch -p PublishAot=true --detach --json ``` +Fix IL/CsWinRT warnings rather than suppressing them. See [AOT/source-generator patterns](../winui-packaging/references/sourcegen-patterns.md). -The wrapper accepts the same `.csproj`, `.sln`/`.slnx`, directory, and `--project` inputs as `winapp run`. - -**If build fails:** Read all errors, batch-fix them in one pass, then rerun the same command. - -**If the app crashes on launch:** `read_powershell` the shell β€” first-chance exceptions appear in the output. See the crash-diagnosis section below for WinUI stowed-exception triage. - -### Diagnosing Crashes with `winapp run` +### Diagnosing Crashes -For WinUI apps, `--debug-output` (the wrapper default) runs a **stowed-exception triage** on crash, surfacing the real WinUI/XAML error behind an opaque `0x8000FFFF` / `E_FAIL`. The first crash downloads debugger components and can take a few minutes; point `WINAPP_DBGTOOLS_DIR` at an existing *Debugging Tools for Windows* install for offline/locked-down environments. Add `--symbols` for richer native frames. +Run attached with `--debug-output` and **invoke it with `mode: "async"`**, then read the same shell. On a WinUI crash, stowed-exception triage surfaces the real XAML error behind an opaque `0x8000FFFF` / `E_FAIL`; add `--symbols` for richer native frames. The first crash downloads debugger components; set `WINAPP_DBGTOOLS_DIR` to an existing *Debugging Tools for Windows* install when offline. `--debug-output` cannot combine with `--json` or `--no-launch`. ### Common Errors @@ -86,9 +63,9 @@ For WinUI apps, `--debug-output` (the wrapper default) runs a **stowed-exception | NETSDK1136 platform required | Target a Windows TFM (for example `net10.0-windows10.0.26100.0`); use `-f ` when the project already multi-targets | | XLS0414 XAML type not found | Add `xmlns` declaration | | XDG0062 binding path missing | Check `x:Bind` property exists on ViewModel | -| Blank window after launch | `x:Bind` defaults to `OneTime` β€” add `Mode=OneWay` | -| App silently exits | Use `winapp run`, never run the .exe directly | -| App crashes with opaque `0x8000FFFF` / `E_FAIL` | Run under `--debug-output` (BuildAndRun.ps1 default) β€” WinUI stowed-exception triage surfaces the real XAML error + symbolicated native stack. `--symbols` is optional | +| Dynamic bound value does not update | Check effective mode, including inherited `x:DefaultBindMode`; use `OneWay`/`TwoWay` and change notifications where needed | +| App silently exits | Use project-mode `winapp run`; don't bypass packaged activation by running the .exe directly | +| App crashes with opaque `0x8000FFFF` / `E_FAIL` | See **Diagnosing Crashes** | | XAML compiler crashes silently | Remove any `PresentationCore.dll` / `System.Windows` references | | MSB3073 / `XamlCompiler.exe ... exited with code 1`, no `.xaml` named | Old WindowsAppSDK XAML-compiler bug β€” update `Microsoft.WindowsAppSDK` NuGet to latest (β‰₯ 2.1.3, or β‰₯ 1.8 on the 1.x line) | | 0x80073CF6 package install failed | Check the manifest publisher and Developer Mode; apps from `winapp new` need no separate `winapp init` | @@ -97,22 +74,23 @@ For WinUI apps, `--debug-output` (the wrapper default) runs a **stowed-exception ### Prerequisites -| Requirement | Minimum | Recommended (fresh installs) | Install command | -|-------------|---------|------------------------------|-----------------| -| Windows 10 v1903+ | β€” | β€” | β€” | -| Developer Mode | enabled | enabled | Settings β†’ Advanced β†’ Developer Mode β†’ On | -| .NET SDK | 8.0.100 | 10.0 | `winget install Microsoft.DotNet.SDK.10` | -| WinApp CLI | 0.7.0 | latest | `/winui-setup` | +| Requirement | Required for this workflow | +|-------------|----------------------------| +| Windows | Windows 10 v1903+ and the app's OS requirements | +| Developer Mode | Enabled for development deployment | +| .NET SDK | 8.0.100 minimum **plus the SDK required by the app's TFM** (e.g., .NET 10 for `net10.0-windows…`) | +| WinApp CLI | 0.7+ | +| Native AOT only | MSVC C++ build tools (Visual Studio or Build Tools, **Desktop development with C++** workload, target-architecture tools); not needed for normal builds | -If `winapp`/`dotnet` is missing or too old, or Developer Mode is off, **do not install it ad hoc or work around it**. Ask the user to run `/winui-setup`, then retry. `winapp new` manages the WinUI template pack itself. +If WinApp CLI is missing or older than 0.7, install or upgrade it using [winui-setup](../winui-setup/SKILL.md) without asking (it needs no admin rights) and tell the user. Ask before installing anything that needs admin rights β€” the .NET SDK, Developer Mode, or the [Native AOT toolchain](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/); do not work around them. ### Critical Rules -- ❌ NEVER run the packaged .exe directly β€” always use project-mode `winapp run` or `BuildAndRun.ps1` -- ❌ NEVER add `None` to work around launch issues -- ❌ NEVER delete `Package.appxmanifest` +- Keep **packaged** as the default and use project-mode `winapp run` for activation. +- Only for an **explicitly requested unpackaged/debug experiment**, set `WindowsPackageType=None`; package-identity-dependent APIs may fail and runtime requirements still apply. Do not use this as a silent launch workaround. Preserve the manifest and restore the original packaged setting after the experiment. +- Do not delete `Package.appxmanifest`. - ❌ NEVER use `AnyCPU` β€” always x64 or ARM64 ### References -- `BuildAndRun.ps1` β€” included with this skill; adds the bundled analyzer and diagnostic defaults to `winapp run` +- [winui-packaging](../winui-packaging/SKILL.md) β€” release packaging directly from the project; no development registration required. diff --git a/plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.dll b/plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.dll deleted file mode 100644 index ff43456c..00000000 Binary files a/plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.dll and /dev/null differ diff --git a/plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.targets b/plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.targets deleted file mode 100644 index 7f90b961..00000000 --- a/plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.targets +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - diff --git a/plugins/winui/agent-plugin/skills/winui-packaging/SKILL.md b/plugins/winui/agent-plugin/skills/winui-packaging/SKILL.md index 6832b53c..5d1cada7 100644 --- a/plugins/winui/agent-plugin/skills/winui-packaging/SKILL.md +++ b/plugins/winui/agent-plugin/skills/winui-packaging/SKILL.md @@ -1,109 +1,87 @@ --- name: winui-packaging -description: "MSIX packaging, code signing, and distribution for WinUI 3 apps β€” build for release, certificate generation (winapp cert generate), certificate trust, code signing (winapp sign), self-contained deployment, CI/CD with GitHub Actions, and Microsoft Store submission. Use when preparing for release, creating MSIX installers, managing certificates, setting up CI/CD packaging, or publishing to the Microsoft Store." +description: "MSIX packaging, code signing, and distribution for WinUI 3 apps with WinApp CLI 0.7+ β€” SDK-native project packaging, Native AOT, certificates, self-contained deployment, CI/CD, and Microsoft Store handoff. Use when preparing for release, creating MSIX installers, managing certificates, setting up CI/CD packaging, or publishing to the Microsoft Store." --- +Requires **WinApp CLI 0.7+**. For analyzer setup, see [winui-dev-workflow](../winui-dev-workflow/SKILL.md). + ### Quick Reference | Task | Command | |------|---------| -| Build for release | `.\BuildAndRun.ps1 . -c Release --arch x64 --no-launch` | -| Package + sign | `winapp package --cert devcert.pfx` | -| Generate + sign + package | `winapp package --generate-cert --install-cert` | +| Release package + sign | `winapp package .\MyApp.csproj --arch x64 --cert .\devcert.pfx` | +| Unsigned multi-architecture bundle | `winapp package .\MyApp.csproj --arch x64 --arch arm64 --no-sign` | | Generate dev certificate | `winapp cert generate` | | Trust certificate (admin) | `winapp cert install ./devcert.pfx` | | Sign existing file | `winapp sign ./app.msix ./devcert.pfx` | -| Self-contained deployment | `winapp package --cert devcert.pfx --self-contained` | +| Bundle Windows App SDK runtime | `winapp package .\MyApp.csproj --arch x64 --cert .\devcert.pfx --self-contained` | ### End-to-End Workflow -#### Step 1: Build for Release -Build the project in Release configuration without launching it. Use the `BuildAndRun.ps1` wrapper from the `winui-dev-workflow` skill β€” plain `dotnet build` does **not** load the bundled `Microsoft.WindowsAppSDK.Analyzers`, so release builds would ship without the analyzer gate that development builds get: +#### Step 1: Check the Project and Deployment Intent -```powershell -.\BuildAndRun.ps1 . -c Release --arch x64 --no-launch -``` +- Pass the **explicit project file**, not `.` or a guessed `bin` folder. WinUI project packaging uses SDK-native `dotnet publish` packaging, defaults to **Release**, and preserves project AOT settings. +- Check manifest identity, target architectures, the SDK for the app's TFM, and release warnings. +- For Native AOT, set `true` in the project and fix IL/CsWinRT warnings; there is **no `winapp package --aot`**. AOT also needs the MSVC C++ build tools. See [source-generator patterns](references/sourcegen-patterns.md). +- Project packaging rejects `WindowsPackageType=None`; restore the packaged setting first (see [winui-dev-workflow](../winui-dev-workflow/SKILL.md) Critical Rules). -`--no-launch` builds and registers a development package without starting the app. Run `winapp unregister` before installing the signed `.msix` in Step 5, so the development registration does not conflict with the packaged identity. +Do **not** run/register/unregister a development package just to produce release artifacts. Project packaging builds without a development `winapp run --no-launch` step. For WinUI SDK-native packaging, do not pass layout overrides `--manifest`, `--executable`, or `--skip-pri`; fix the project/manifest instead. #### Step 2: Generate Certificate (one-time) ```powershell -winapp cert generate --manifest . +winapp cert generate --manifest .\Package.appxmanifest ``` -Creates `devcert.pfx` (default password: `password`). The `--manifest` flag auto-matches the `Publisher` field in `Package.appxmanifest`. +Creates a development `devcert.pfx` (default password: `password`). This **certificate command's** `--manifest` flag auto-matches the `Publisher` field in `Package.appxmanifest`. Keep PFX files/passwords out of source control; production signing needs the organization's signing policy. -#### Step 3: Trust Certificate (one-time, requires admin) +#### Step 3: Trust on the Intended Test Machine (optional, admin) ```powershell winapp cert install ./devcert.pfx ``` -Adds cert to machine Trusted Root store. Persists across reboots. +Adds the cert to the machine trust store and persists across reboots. Do this only with approval on the intended test machine; it is not required just to build/package in CI. #### Step 4: Package and Sign ```powershell -winapp package --cert ./devcert.pfx +winapp package .\MyApp.csproj --arch x64 --cert .\devcert.pfx +# Repeat --arch for a bundle +winapp package .\MyApp.csproj --arch x64 --arch arm64 --no-sign ``` -This locates `appxmanifest.xml`, stages the layout, generates `resources.pri`, creates `.msix`, and signs it. +Choose one signing policy: `--cert` takes a **PFX file**, not a certificate thumbprint; its subject must match the manifest's `Identity.Publisher`. `--no-sign` leaves an artifact for external signing. Use the artifact paths reported by the command, not a guessed output directory. -#### Step 5: Install or Distribute +For timestamped production signing, create an unsigned package then sign the resulting artifact: ```powershell -# Local install -Add-AppxPackage ./MyApp.msix +# Replace MyApp.msix with the package/bundle path returned above +winapp sign .\MyApp.msix .\prod.pfx --timestamp http://timestamp.digicert.com +``` +`--timestamp` belongs to **`winapp sign`**, not `winapp package`. Use an approved timestamp service and protect the PFX/password. -# Or double-click the .msix file +#### Step 5: Install or Distribute +When installation/testing is part of the task, choose the target per [winui-ui-testing](../winui-ui-testing/SKILL.md) Step 1, and get consent for certificate trust and dependency provisioning on that machine. Packaging alone is not permission to install an app. + +### Self-Contained Does Not Mean Single-File + +`winapp package --self-contained` sets only **`WindowsAppSDKSelfContained=true`**, not .NET `SelfContained=true`. For a fully self-contained JIT .NET app, set both in the project: +```xml + + true + true + ``` +Native AOT covers the .NET runtime but still needs the Windows App SDK choice. Native WinUI runtime files stay alongside the executable/in the package; do not promise a single-file WinUI EXE. Some APIs still require additional MSIX dependencies; see the [deployment guidance](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/self-contained-deploy/deploy-self-contained-apps). -### Key Rules - -- **Publisher must match** between certificate and manifest `Identity.Publisher` β€” use `winapp cert generate --manifest` to auto-match -- **Prefer `winapp package --cert`** over separate `winapp sign` β€” one step instead of two -- **`cert install` requires admin** β€” run terminal as Administrator -- **Default PFX password** is `password` β€” override with `--password` -- **`--timestamp`** is critical for production β€” without it, signatures expire with the cert: - ```powershell - winapp package --cert prod.pfx --timestamp http://timestamp.digicert.com - ``` -- **`--self-contained`** bundles Windows App SDK runtime β€” larger but no runtime dependency - -### CI/CD with GitHub Actions - -```yaml -name: Build and Package -on: [push] -jobs: - build: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - uses: microsoft/setup-WinAppCli@v0.1 - - - name: Build - run: dotnet build -c Release -p:Platform=x64 - - - name: Package - run: | - winapp cert generate --if-exists skip --quiet - winapp package ./bin/x64/Release/ --cert ./devcert.pfx --quiet - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: msix-package - path: "*.msix" +### CI/CD + +On a Windows runner provisioned with CLI 0.7+, the app's .NET SDK, and (for AOT) the native C++ toolchain, the packaging step can be: +```powershell +winapp package .\MyApp.csproj --arch x64 --arch arm64 --no-sign ``` -**CI/CD tips:** -- Use plain `dotnet build` on the runner, not `BuildAndRun.ps1` β€” the wrapper registers a development package and needs the plugin's local analyzer payload, neither of which belongs in CI. Enforce analyzer coverage on the dev machine instead. -- Use `--quiet` for clean output -- Use `--if-exists skip` with `cert generate` to avoid failures on re-runs -- Store production PFX as a repository secret +- Committed package references, including the analyzer, restore normally; no machine-local analyzer is needed. +- Archive the reported `.msix`/`.msixbundle` outputs using the CI system's artifact step. No UI launch or development registration is needed. +- Retrieve production signing material through approved secret storage and sign in a separate protected step. Never commit a PFX or install a development root certificate just to build. ### Store Submission -1. **Partner Center account** β€” register at [partner.microsoft.com](https://partner.microsoft.com) -2. **Age ratings** β€” complete the questionnaire in Partner Center -3. **Screenshots** β€” capture at 1366x768 minimum resolution -4. **Privacy policy** β€” required for apps that access internet or user data -5. **Submit:** upload the signed `.msix` / `.msixbundle` produced by `winapp package` via [Microsoft Partner Center](https://partner.microsoft.com/dashboard) β€” Apps and games β†’ your app β†’ Packages. Microsoft Store submission is browser-based; there is no first-party CLI submit command yet. +Associate the app with its [Partner Center](https://partner.microsoft.com/dashboard) identity and follow its current submission requirements (package validation, ratings, screenshots, privacy policy). Use the **SDK/Visual Studio packaging workflow** when Store-upload artifacts or resource-package splitting are required. `winapp package` produces package artifacts; it does not publish to the Store or replace that submission workflow. ### Troubleshooting @@ -113,12 +91,12 @@ jobs: | "Certificate not trusted" | Run `winapp cert install ./devcert.pfx` as admin | | "Access denied" | `cert install` needs admin elevation | | "Certificate file already exists" | Use `--if-exists overwrite` or `--if-exists skip` | -| "appxmanifest.xml not found" | Run `winapp init` or pass `--manifest ` | -| "Package installation failed" | Trust cert first; remove stale: `Get-AppxPackage \| Remove-AppxPackage` | -| Signature invalid after time | Re-sign with `--timestamp` | +| Manifest missing / layout override rejected | Check the explicit project and its original manifest; do not use `winapp init` or layout overrides to bypass SDK packaging | +| "Package installation failed" | Check signing/trust and the exact conflicting identity; remove a stale registration only if confirmed and authorized | +| Signature invalid after time | Sign with an approved timestamp service via `winapp sign --timestamp` | ### References | File | Read when... | |------|-------------| -| `references/sourcegen-patterns.md` | Setting up AOT/trimming, JSON source generators, NativeAOT readiness, CsWin32 | +| [references/sourcegen-patterns.md](references/sourcegen-patterns.md) | Setting up AOT/trimming, JSON source generators, Native AOT readiness, CsWin32 | diff --git a/plugins/winui/agent-plugin/skills/winui-packaging/references/sourcegen-patterns.md b/plugins/winui/agent-plugin/skills/winui-packaging/references/sourcegen-patterns.md index ea3475cb..215c1b19 100644 --- a/plugins/winui/agent-plugin/skills/winui-packaging/references/sourcegen-patterns.md +++ b/plugins/winui/agent-plugin/skills/winui-packaging/references/sourcegen-patterns.md @@ -1,49 +1,48 @@ # Source Generator Patterns β€” Detailed Reference -Detailed code patterns for AOT compilation and source generators. See [SKILL.md](../SKILL.md) for rules summary. +Patterns for Native AOT and trimming in WinUI 3. See [SKILL.md](../SKILL.md) for project packaging and [winui-dev-workflow](../../winui-dev-workflow/SKILL.md) for analyzer setup and publish runs. --- -## Trimming Configuration +## Native AOT Intent and Diagnostics + +Prefer persistent project intent, not just a publish command override: ```xml - true - full + true false + false + 2 ``` -For reflection-heavy code, annotate to preserve members: +Native AOT enables trimming. Keep IL and CsWinRT warnings enabled and fix the offending reflection, ABI, or binding pattern; do not disable analysis to obtain a clean publish. A successful JIT build/run is not evidence that the published AOT app works. -```csharp -public void LoadService([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType) - => Activator.CreateInstance(serviceType); -``` +`CsWinRTAotOptimizerEnabled` defaults to `True` (selecting **Auto** for WinUI). Do not turn it off. Types implementing projected interfaces, extending projected classes, or implementing mapped .NET interfaces and crossing the WinRT ABI must be **partial** so CsWinRT can generate their vtables. Warning level 2 also covers mapped built-in interfaces: inspect each warning and make the relevant source types partial rather than applying blanket suppressions. Dependencies must be AOT-compatible too. --- -## Trim Compatibility Testing +## Reflection and Trimming -Keep warnings enabled during development: +Prefer static references/source generation over runtime type discovery. Replace `Assembly.LoadFrom()` with compile-time dependencies and `Type.GetType("…")` with known types where possible. When reflection is necessary, express the member requirements and resolve the resulting diagnostics: +```csharp +using System.Diagnostics.CodeAnalysis; -```xml -false -false +public object? CreateService( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type serviceType) => Activator.CreateInstance(serviceType); ``` - -Patterns to eliminate: - -- `Type.GetType("MyNamespace.MyClass")` β€” use `typeof(T)` or `[DynamicDependency]` -- `Activator.CreateInstance(someType)` without `[DynamicallyAccessedMembers]` -- `Assembly.LoadFrom()` β€” use compile-time references -- Unattributed reflection: `typeof(T).GetProperties()` +Annotations preserve required members; they do not make arbitrary dynamic loading or runtime code generation AOT-compatible. Test the actual published artifact and exercise reflection-dependent paths. --- ## JSON Source Generator Setup ```csharp +using System.Text.Json; +using System.Text.Json.Serialization; + [JsonSerializable(typeof(UserProfile))] [JsonSerializable(typeof(List))] internal partial class AppJsonContext : JsonSerializerContext { } @@ -68,17 +67,30 @@ public partial class InputValidator --- -## XAML Compilation (x:Bind) +## Compiled and Runtime XAML Binding + +Prefer `x:Bind` for known source types. In a page/window, paths resolve against the code-behind instance (for example, its `ViewModel` property), **not** `DataContext`. Do not set `x:DataType` on a `Page` to declare its VM. ```xml - - - + + + + ``` -Set `x:DataType` on pages: `` +`x:Bind` defaults to `OneTime` unless an inherited `x:DefaultBindMode` changes it. Dynamic data needs an effective `OneWay`/`TwoWay` mode plus change notifications; stable command/event bindings do not need a blanket mode rewrite. + +Runtime `{Binding}` and `DisplayMemberPath` are **not categorically unsupported**. For their `ICustomPropertyProvider` path, make each source class partial and request generated bindable properties: +```csharp +[WinRT.GeneratedBindableCustomProperty] +public partial class Person +{ + public string Name { get; set; } = string.Empty; +} +``` +This generates an AOT-safe property provider for public properties; it does not supply change notifications. Set the correct `DataContext`/item source, add notifications for mutable data, and exercise the runtime binding in the published app. See the [CsWinRT guidance](https://github.com/microsoft/CsWinRT/blob/master/docs/aot-trimming.md) for scoped property generation and ABI cases the generator cannot infer. --- @@ -96,7 +108,7 @@ ShowWindow ## CommunityToolkit.Mvvm Source Generators -Use partial properties (CommunityToolkit.Mvvm 8.4+). The legacy field form emits **MVVMTK0045** in WinRT/WinUI projects. +Use partial properties (CommunityToolkit.Mvvm 8.4+ and a compiler supporting partial properties). The field form triggers **MVVMTK0045** because CsWinRT cannot generate the required WinRT marshalling support for those generated properties. Make the containing types partial; do not suppress the warning to preserve field syntax. ```csharp public partial class SettingsViewModel : ObservableObject @@ -106,16 +118,8 @@ public partial class SettingsViewModel : ObservableObject } ``` ---- - -## Single-File Publishing Configuration - -```xml - - true - true - true - -``` +## Official References -Primarily for unpackaged apps. Combine with `true`. +- [C#/WinRT AOT, trimming, and generated bindable properties](https://github.com/microsoft/CsWinRT/blob/master/docs/aot-trimming.md) +- [MVVMTK0045 and partial properties](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/errors/mvvmtk0045) +- [.NET Native AOT prerequisites and publishing](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) diff --git a/plugins/winui/agent-plugin/skills/winui-session-report/Analyze-Session.ps1 b/plugins/winui/agent-plugin/skills/winui-session-report/Analyze-Session.ps1 index 9759c1b5..ac9bbdc8 100644 --- a/plugins/winui/agent-plugin/skills/winui-session-report/Analyze-Session.ps1 +++ b/plugins/winui/agent-plugin/skills/winui-session-report/Analyze-Session.ps1 @@ -309,11 +309,61 @@ function Test-NoBuildEnabled { function Test-BuildCapableCommand { param([string]$Command) - if ($Command -match '\bdotnet build\b|\bmsbuild\b') { - return $true - } - if ($Command -match 'BuildAndRun|\bwinapp run\b') { - return -not (Test-NoBuildEnabled -Command $Command) + # Classify transcript text, not today's filesystem. Keep flags scoped to each + # invocation, and don't mistake arguments passed to the app for CLI options. + foreach ($segment in [regex]::Split($Command, ';|&&|\|\||\r?\n')) { + $invocation = ($segment -split '\s+--(?:\s|$)', 2)[0] + if ($invocation -match '(?:^|\s)(?:--help|-h|-\?)(?=\s|$)') { continue } + if ($invocation -match '\bmsbuild\b') { return $true } + if (Test-NoBuildEnabled -Command $invocation) { continue } + if ($invocation -match '\bdotnet\s+(?:build|publish)\b|BuildAndRun') { + return $true + } + $cli = [regex]::Match($invocation, '(?i)\bwinapp(?:\.exe)?\s+(?run|pack(?:age)?)\b(?.*)') + if (-not $cli.Success) { continue } + $arguments = $cli.Groups['args'].Value.Trim() + $argumentTokens = @([regex]::Matches($arguments, '(?:"[^"]*"|''[^'']*''|[^\s"''])+') | + ForEach-Object { $_.Value.Trim([char[]]@('"', "'")) }) + $valueOptions = @('--arch', '--configuration', '-c', '--framework', '-f', '--runtime', '-r', + '--project', '--on', '--manifest', '--property', '-p', '--output', '-o', '--cert') + $booleanOptions = @('--no-build', '--no-restore', '--aot', '--detach', '--json', '--no-launch', + '--no-sign', '--generate-cert', '--install-cert', '--debug-output', '--with-alias', '--clean', + '--self-contained', '--symbols') + $inputPath = '' + $knownOptions = $true + # Only skip known leading options; unknown options are ambiguous, not + # evidence that their value is a positional project input. + for ($index = 0; $index -lt $argumentTokens.Count; $index++) { + $token = $argumentTokens[$index] + if (-not $token.StartsWith('-')) { $inputPath = $token; break } + $option = $token -split '=', 2 + if ($option[0] -in $valueOptions) { + if ($option.Count -eq 1) { + if ($index + 1 -ge $argumentTokens.Count -or $argumentTokens[$index + 1].StartsWith('-')) { + $knownOptions = $false; break + } + $index++ + } + } elseif ($option[0] -in $booleanOptions) { + if ($option.Count -eq 2 -and $option[1] -notmatch '^(true|false)$') { + $knownOptions = $false; break + } + if ($option.Count -eq 1 -and $index + 1 -lt $argumentTokens.Count -and + $argumentTokens[$index + 1] -match '^(true|false)$') { $index++ } + } else { + $knownOptions = $false; break + } + } + if (-not $knownOptions) { continue } + if ($cli.Groups['verb'].Value -match '^pack') { + # Only an explicit .csproj is project packaging. Folder, bundle, and + # sparse-manifest packaging do not build, even when flags mention a project. + if ($inputPath -match '\.csproj$') { return $true } + } elseif (-not $inputPath -or $inputPath -match '^\.[\\/]?$|\.(?:csproj|slnx?|cs)$') { + # "." / omitted input is the normal project-root workflow. Other + # directory inputs are ambiguous without historical filesystem state. + return $true + } } return $false } @@ -328,7 +378,7 @@ function Get-TurnCategory { if (-not (& $shellCmd $_)) { return $false } return Test-BuildCapableCommand -Command $_.Args.command } - $hasRun = $Turn.Tools | Where-Object { (& $shellCmd $_) -and ($_.Args.command -match 'winapp run|BuildAndRun') } + $hasRun = $Turn.Tools | Where-Object { (& $shellCmd $_) -and ($_.Args.command -match '\bwinapp(?:\.exe)?\s+run\b|BuildAndRun') } $hasGit = $Turn.Tools | Where-Object { (& $shellCmd $_) -and ($_.Args.command -match '\bgit\b') } $hasBuildError = $Turn.Tools | Where-Object { $_.HasError -and (& $shellCmd $_) } $hasScaffold = $Turn.Tools | Where-Object { $_.Args.command -match 'winapp new|dotnet new|New-Item.*Directory' } @@ -817,27 +867,43 @@ $buildWorkflowSuccesses = $buildWorkflowAttempts - $buildWorkflowFailures $buildErrors = @() foreach ($t in $allTurns) { foreach ($tool in $t.Tools) { - if ($tool.HasError -and $tool.Name -in 'powershell', 'shell' -and $tool.Args.command -match '\bdotnet build\b|\bmsbuild\b|BuildAndRun\.ps1|BuildAndRun |\bwinapp run\b' -and $tool.ErrorSummary.Count -gt 0) { + if ($tool.HasError -and $tool.Name -in 'powershell', 'shell' -and + (Test-BuildCapableCommand -Command $tool.Args.command) -and $tool.ErrorSummary.Count -gt 0) { $buildErrors += @{ Turn = $t.TurnNum; Errors = $tool.ErrorSummary } } } } -$winappWorkflows = $allTurns | Where-Object { - $_.Tools | Where-Object { $_.Name -in 'powershell', 'shell' -and $_.Args.command -match 'BuildAndRun|\bwinapp run\b' } -} -$rawDotnetBuilds = $allTurns | Where-Object { - $_.Tools | Where-Object { $_.Name -in 'powershell', 'shell' -and $_.Args.command -match 'dotnet build' -and $_.Args.command -notmatch 'BuildAndRun' } -} -if ($winappWorkflows -and -not $rawDotnetBuilds) { - $projectBuildStatus = "Used winapp run / BuildAndRun.ps1 workflows" -} elseif ($winappWorkflows -and $rawDotnetBuilds) { - $projectBuildStatus = "Mixed: raw 'dotnet build' $($rawDotnetBuilds.Count)x, winapp run / BuildAndRun.ps1 $($winappWorkflows.Count)x" -} elseif ($rawDotnetBuilds) { - $projectBuildStatus = "Raw 'dotnet build' used $($rawDotnetBuilds.Count)x; no winapp run / BuildAndRun.ps1 detected" -} else { - $projectBuildStatus = "No build commands detected" +$workflowLabels = @() +foreach ($workflow in @( + @{ Pattern = '\bdotnet\s+(?:build|publish)\b'; Label = 'dotnet build/publish' } + @{ Pattern = '\bwinapp(?:\.exe)?\s+(?:run|pack(?:age)?)\b'; Label = 'winapp project build/publish/package' } + @{ Pattern = 'BuildAndRun'; Label = 'historical BuildAndRun.ps1' } + @{ Pattern = '\bmsbuild\b'; Label = 'MSBuild' } +)) { + $count = @($buildAttemptTurns | Where-Object { + $_.Tools | Where-Object { + if ($_.Name -notin 'powershell', 'shell') { return $false } + [regex]::Split($_.Args.command, ';|&&|\|\||\r?\n') | Where-Object { + $_ -match $workflow.Pattern -and (Test-BuildCapableCommand -Command $_) + } + } + }).Count + if ($count) { $workflowLabels += "$($workflow.Label): $count turn(s)" } } +$projectBuildStatus = if ($workflowLabels.Count) { $workflowLabels -join '; ' } else { 'No build commands detected' } + +$sandboxCommands = @( + foreach ($turn in $allTurns) { + foreach ($tool in $turn.Tools) { + if ($tool.Name -in 'powershell', 'shell' -and + $tool.Args.command -match '(?i)--on(?:\s+|=)sandbox\b|\bwinapp\s+target\s+\S+\s+sandbox\b') { + [pscustomobject]@{ Turn = $turn.TurnNum; HasError = $tool.HasError } + } + } + } +) +$sandboxFailures = @($sandboxCommands | Where-Object HasError).Count $skillTimeline = @() foreach ($t in $parsed.Turns) { @@ -908,7 +974,7 @@ if ($buildErrors | Where-Object { $_.Errors -match 'MSB3073' }) { } $devWorkflowEntry = $skillTimeline | Where-Object { $_.Skill -match 'winui-dev-workflow' } | Select-Object -First 1 $firstBuildTurn = ($buildAttemptTurns | Select-Object -First 1).TurnNum -if ($devWorkflowEntry -and $rawDotnetBuilds -and $devWorkflowEntry.Turn -gt $firstBuildTurn) { +if ($devWorkflowEntry -and $firstBuildTurn -and $devWorkflowEntry.Turn -gt $firstBuildTurn) { $toolingIssues += @{ Area = "Skill timing" Issue = "dev-workflow skill loaded at turn $($devWorkflowEntry.Turn) but first build was turn $firstBuildTurn" @@ -1015,9 +1081,11 @@ $md += "## Build Analysis" $md += "" $md += "- **Build-capable workflow attempts:** $buildWorkflowAttempts ($buildWorkflowSuccesses completed without a command error, $buildWorkflowFailures command failures)" $md += "- **Project build workflow:** $projectBuildStatus" +$md += "- When installed, NuGet-delivered analyzers run in ordinary dotnet build/publish and project-mode CLI workflows; direct dotnet usage is not evidence of a missing analyzer. If unavailable, report the coverage gap without treating it as a task blocker." +$md += "- Classification is based on command text: explicit project inputs and current-directory run workflows are recognized; other run directory inputs are ambiguous. Folder/manifest packaging and --no-build invocations are not builds." $md += "" if ($buildErrors.Count -gt 0) { - $md += "**Build/run command errors encountered:**" + $md += "**Build/publish workflow command errors encountered:**" $md += "" foreach ($be in $buildErrors) { $md += "Turn $($be.Turn):" @@ -1028,6 +1096,16 @@ if ($buildErrors.Count -gt 0) { $md += "" } +if ($sandboxCommands.Count) { + $md += "## Sandbox Execution" + $md += "" + $md += "- **Sandbox command calls:** $($sandboxCommands.Count) ($sandboxFailures with reported errors). Builds run on the host; deployment, launch, and UI run in the guest." + $md += "- Review failed target startup/readiness, deployment, UI, and capture calls separately from compiler failures. An error in a build-capable run command does not prove compilation failed." + $md += "- Check target scope on every UI call (including picker HWNDs), fresh-guest PID invalidation, one WINAPP_UI_WORKFLOW_ID per cooperating flow, and an unlocked host with a connected, nonminimized Sandbox client for input/capture. Tree reads alone do not prove input readiness." + $md += "- Preserve delivered host evidence and recovery paths before a consented shutdown. Local execution after explaining unavailable Windows Sandbox is valid unless the user explicitly requested Windows Sandbox. Flag silent switches, reused guest IDs, and capture claims without delivered evidence." + $md += "" +} + if ($stuckPatterns.Count -gt 0) { $md += "## Stuck Patterns" $md += "" diff --git a/plugins/winui/agent-plugin/skills/winui-session-report/SKILL.md b/plugins/winui/agent-plugin/skills/winui-session-report/SKILL.md index 8e39f27c..3f9a6ce6 100644 --- a/plugins/winui/agent-plugin/skills/winui-session-report/SKILL.md +++ b/plugins/winui/agent-plugin/skills/winui-session-report/SKILL.md @@ -35,7 +35,7 @@ If the user only wants the high-level metrics (turn counts, skill usage, build s .\Analyze-Session.ps1 -SessionId "" -OutputFile session-report.md # Or analyze a transcript file directly (format sniffed from content) -.\Analyze-Session.ps1 -EventsFile -OutputFile session-report.md +.\Analyze-Session.ps1 -EventsFile '.\transcript.jsonl' -OutputFile session-report.md # Force a specific format if auto-detection picks the wrong harness .\Analyze-Session.ps1 -Format ClaudeCode -OutputFile session-report.md @@ -64,7 +64,8 @@ Detection rules: 4. Include any tooling improvements or recommendations based on the analysis. - Are there rules that need to be added to the Roslyn analyzer to prevent common mistakes detected during the session? - - Were there bugs or issues with `winapp new`, project-mode `winapp run`, `winapp find-ui`, or the BuildAndRun.ps1 wrapper? + - Were there bugs or issues with `winapp new`, project-mode `winapp run` / `winapp package`, `dotnet build` / `dotnet publish`, or `winapp find-ui`? (`BuildAndRun.ps1` appears only in historical transcripts.) + - Review the report's **Build Analysis** and **Sandbox Execution** sections for host/guest scope, input-readiness, and evidence issues. - Are there features that could be added to lower the number of turns required to complete a task? ### What the Report Covers @@ -76,11 +77,8 @@ Detection rules: | Turn Breakdown | Turns and tokens by category (building, coding, exploring, subagent dispatch, etc.) | | Skills | Which were invoked and when, including from inside subagent transcripts | | Subagents | (Claude Code only) Per-agent breakdown of dispatched subagents and their work | -| Build Analysis | Build-capable workflow attempts, command failures/errors, and whether project-mode winapp run / BuildAndRun.ps1 was used | +| Build Analysis | Build/publish attempts and command errors (dotnet, project-mode `winapp run`/`package`, historical `BuildAndRun.ps1`) | +| Sandbox Execution | When present: Sandbox command usage/error counts and focused checks for host/guest scope, input readiness, workflow coordination, and evidence delivery | | Stuck Patterns | Build loops, repeated file reads, obj/ clean cycles | | Tooling Issues | Auto-detected improvement opportunities | | Turn Detail | Every turn with tools used and errors flagged, parent and subagent transcripts shown separately | - -### When to Use - -- When the user asks for a session report to understand what happened during an agent session. diff --git a/plugins/winui/agent-plugin/skills/winui-setup/SKILL.md b/plugins/winui/agent-plugin/skills/winui-setup/SKILL.md index 1a0eee8f..6943daaf 100644 --- a/plugins/winui/agent-plugin/skills/winui-setup/SKILL.md +++ b/plugins/winui/agent-plugin/skills/winui-setup/SKILL.md @@ -1,14 +1,14 @@ --- name: winui-setup -description: "Install and verify the prerequisites the win-dev-skills WinUI 3 toolchain depends on β€” .NET SDK 8.0.100+, WinApp CLI 0.7+, and Developer Mode. Use only when the user explicitly asks to set up or repair the toolchain. Do not invoke automatically when another skill reports a missing prerequisite; tell the user what is missing and ask them to invoke this skill." +description: "Install and verify WinUI 3 prerequisites β€” .NET SDK 8.0.100+, WinApp CLI 0.7+, and Developer Mode; identify additional Native AOT and Windows Sandbox requirements. Use when the user asks to set up or repair the toolchain, or when another WinUI skill finds a missing or outdated prerequisite." --- ### Purpose -Install and verify the prerequisites every other `winui-*` skill assumes. WinApp CLI 0.7 owns WinUI template discovery and installation through `winapp new`; **do not install the template pack separately**. +Install and verify the prerequisites every other `winui-*` skill assumes. WinApp CLI 0.7 owns WinUI template discovery and installation through `winapp new`; **do not install the template pack separately**. The project's target framework may require a newer .NET SDK than the CLI's minimum. > [!IMPORTANT] -> Run this skill only when the user explicitly asks to set up or repair the toolchain. If it is loaded without an explicit request, do not run checks or installations; explain what the skill changes and wait for confirmation. +> Install per-user prerequisites (WinApp CLI) without asking, and tell the user what changed. **Ask before anything that needs admin rights (UAC) or a large toolchain** β€” the .NET SDK, Developer Mode, and Native AOT build tools. Windows Sandbox enablement is the user's to do. This skill is idempotent: detect everything first, install or upgrade only what is needed, and print one final summary. @@ -34,24 +34,26 @@ $dotnetVersion = $dotnetSdks | Select-Object -First 1 $dotnetOk = $null -ne $dotnetVersion -# WinApp CLI β€” require 0.7+ for winapp new, find-ui, find-api, and project-mode run +# WinApp CLI β€” require released 0.7+ for the migrated workflows $winappCmd = Get-Command winapp -ErrorAction SilentlyContinue $winappVersion = $null +$winappPrerelease = $false if ($winappCmd) { foreach ($line in @(& winapp --version 2>$null)) { $match = [regex]::Match( [string]$line, - '^\s*v?(?\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\s*$' + '^\s*v?(?\d+\.\d+\.\d+)(?:-(?[0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?\s*$' ) if ($match.Success) { $parsed = $null if ([version]::TryParse($match.Groups['version'].Value, [ref]$parsed)) { $winappVersion = $parsed + $winappPrerelease = $match.Groups['prerelease'].Success } } } } -$winappOk = $winappVersion -ge $minimumWinApp +$winappOk = ($winappVersion -ge $minimumWinApp) -and -not $winappPrerelease # Developer Mode $devModeOk = ((Get-ItemProperty ` @@ -64,31 +66,36 @@ Print a one-shot status table: ```text .NET SDK >= 8.0.100 [OK] found 10.0.100 -WinApp CLI >= 0.7.0 [!] found 0.6.1 β€” will upgrade +WinApp CLI >= 0.7.0 [!] found 0.6.0 β€” will upgrade Developer Mode [X] disabled β€” needs admin to enable ``` #### Install what's missing -##### .NET SDK +##### .NET SDK (ask first) -Only when no SDK at or above `8.0.100` was found: +The SDK installer is machine-wide and triggers UAC. **Ask the user first.** When they agree and no SDK at or above `8.0.100` was found, install the recommended SDK: ```powershell winget install --id Microsoft.DotNet.SDK.10 --exact --silent --accept-package-agreements --accept-source-agreements ``` -Do not install another SDK when 8.0.100+, 9.x, or 10.x is already present. +Do not install another SDK when one already satisfies both the CLI minimum and +the requested project's target framework / `global.json`. The CLI's minimum +does not let an 8.x SDK build a `net10.0-windows` app. +If the base check passed but the requested project needs another SDK, report +that requirement and offer to install its matching SDK rather than declaring +the project ready. If the user declines, print the command for later use. -##### WinApp CLI +##### WinApp CLI (no admin needed) -If `winapp` is missing, install it. If it is present but below 0.7.0, try to upgrade it. Skip both commands when the installed version already meets the minimum: +WinApp CLI is a per-user MSIX package, so install or upgrade it **without asking** and tell the user what changed. If `winapp` is missing, install it. If it is below released 0.7.0, upgrade it. Skip both when the installed version already meets the minimum: ```powershell # When winapp is missing winget install --id Microsoft.WinAppCli --exact --silent --accept-package-agreements --accept-source-agreements -# When winapp is present but older than 0.7.0 +# When winapp does not meet the released 0.7.0 minimum winget upgrade --id Microsoft.WinAppCli --exact --silent --accept-package-agreements --accept-source-agreements ``` @@ -99,9 +106,7 @@ $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User') ``` -Run the version detection again. If the result is still below `0.7.0`, report the actual version and mark setup failed; do not continue with old command fallbacks. - -> `winapp new` installs the official `Microsoft.WindowsAppSDK.WinUI.CSharp.Templates` pack on demand and can update it with `--template-version latest`. Do not run `dotnet new install` during setup. +Run the version detection again. If a released `0.7.0` or later is still unavailable, report the actual version and mark setup blocked; do not fall back to older commands. A prerelease doesn't meet the requirement unless the user asks for preview validation. ##### Developer Mode (ask first) @@ -118,6 +123,25 @@ Start-Process powershell -Verb RunAs -ArgumentList @( If the user declines or dismisses UAC, continue to the summary and print the command for later use. +#### Additional requirements for the requested workflow + +| Workflow | Additional requirements | +|---|---| +| Native AOT publish/run | Windows native compiler/linker toolchain: Visual Studio or Build Tools with **Desktop development with C++**, including the target architecture's tools and Windows SDK. See [Native AOT prerequisites](https://learn.microsoft.com/dotnet/core/deploying/native-aot/). These are not required for normal JIT iteration. | +| Windows Sandbox app runs and UI automation (preferred when available) | WinApp's integration requires Windows 11 24H2+, hardware virtualization, and a working Windows Sandbox feature/client. Supported editions include **Pro, Enterprise, and Education; not Home**. Input/capture needs an unlocked host and a connected, non-minimized client. See [WinApp Sandbox prerequisites](https://github.com/microsoft/winappCli/blob/main/docs/sandbox-execution.md#prerequisites). | + +Report these separately from the base toolchain. `winapp target snapshot sandbox --json` inspects an existing +guest without starting or repairing it; "no target running" alone does not +mean the Windows feature is unavailable. [winui-ui-testing](../winui-ui-testing/SKILL.md) +Step 1 defines what to do when Windows Sandbox is unavailable. + +Enabling Windows Sandbox is a **user action** (admin plus a reboot): ask the user +to open **Turn Windows features on or off**, select **Windows Sandbox**, and +restart if prompted. Hardware virtualization must be enabled; a VM may also +require nested virtualization. See Microsoft's +[supported editions](https://learn.microsoft.com/windows/security/application-security/application-isolation/windows-sandbox/) +and [installation steps](https://learn.microsoft.com/windows/security/application-security/application-isolation/windows-sandbox/windows-sandbox-install). + ### Final summary Always print a single summary: @@ -129,7 +153,7 @@ WinApp CLI >= 0.7.0 [OK] upgraded to 0.7.0 Developer Mode [OK] enabled ``` -You're ready. If the current harness exposes the `winui-dev` orchestrator agent, +Only report readiness for the workflows whose requirements passed. If the current harness exposes the `winui-dev` orchestrator agent, start a fresh session with that agent and ask it to build a WinUI app. Otherwise, start a fresh session in the current harness and ask it to perform the WinUI task; it will load the relevant `winui-*` skills on demand. @@ -140,11 +164,7 @@ For GitHub Copilot CLI, for example: ### Things to NOT do -- Do not install Visual Studio; these skills build and run with `dotnet` and `winapp`. +- Do not install Visual Studio; it is optional. Native AOT needs only the MSVC C++ build tools, a separate, explicitly approved setup. - Do not install or upgrade the user's AI coding harness; this skill manages Windows/WinUI development prerequisites only. -- Do not install the WinUI template pack separately; `winapp new` owns it in 0.7+. -- Do not elevate the entire session; only the Developer Mode registry write needs admin. -- Do not skip the PATH refresh after a winget install or upgrade. -- Do not trigger UAC without asking the user first. -- Do not silently retry failed installs or accept WinApp CLI below 0.7.0. -- Do not install .NET 10 when any SDK at or above 8.0.100 is already available. +- Do not elevate the entire session; request elevation only for the specific approved setup operation. +- Do not silently retry failed installs. diff --git a/plugins/winui/agent-plugin/skills/winui-ui-testing/SKILL.md b/plugins/winui/agent-plugin/skills/winui-ui-testing/SKILL.md index f33a8ccb..bb2265cb 100644 --- a/plugins/winui/agent-plugin/skills/winui-ui-testing/SKILL.md +++ b/plugins/winui/agent-plugin/skills/winui-ui-testing/SKILL.md @@ -1,368 +1,341 @@ --- name: winui-ui-testing -description: "Automated UI testing for Windows desktop apps β€” generate a batch test script with the `winapp ui` UI Automation harness, run all tests in one pass, read results. Covers element assertions, interactions, value checking (TextBox, ComboBox, ToggleSwitch), keyboard shortcuts and typing (send-keys), hover, drag-and-drop, touch and pen input, file pickers, flyouts, dialogs, persistence, accessibility audits, and screenshot/video capture. Works on any Windows app (Win32, WPF, WinForms, WinUI 3, packaged or unpackaged)." +description: "Automated UI testing for Windows desktop apps β€” generate a batch test script with the `winapp ui` UI Automation harness (WinApp CLI 0.7+), run all tests in one pass, read results. Covers assertions, interactions, keyboard/touch/pen input, file pickers, dialogs, persistence, accessibility, and screenshot/video capture for Win32, WPF, WinForms, and WinUI 3, in Windows Sandbox or locally." --- -### Scope β€” any Windows app +### Scope and execution boundary -`winapp ui` drives Windows **UI Automation (UIA)**, the accessibility layer every Windows UI framework exposes, so the AutomationId-based approach in this skill works on **any** Windows desktop app: Win32, WPF, WinForms, and WinUI 3, packaged or unpackaged. The file-picker tests below already drive the OS's Win32 file dialog through the same verbs. For a non-WinUI app, use the same verbs and script template and skip the WinUI-specific gotchas (x:Bind `LostFocus` commit, ContentDialog selectors, MSIX relaunch). +`winapp ui` uses Windows UI Automation (UIA), so the AutomationId-based workflow works with any Windows desktop framework, packaged or unpackaged. Skip WinUI-specific binding/dialog advice for other frameworks. -### Approach +Windows Sandbox keeps synthetic input off the user's desktop; target selection is in Step 1. Discover the installed contract with `winapp run --help`, `winapp target --help`, and `winapp ui --help`. Use `--on sandbox`, not `--sandbox` or a top-level `sandbox` command. Do not enable Windows features or launch an app without the task's permission. -The goal of this skill is to validate UI and app functionality automatically, without manual interaction, by exercising the app's UI elements, verifying their state, and asserting that the app behaves as expected under test conditions. +- Prerequisites and enablement: see [winui-setup](../winui-setup/SKILL.md). +- Project builds/publishes execute on the **host**; deployment, app launch, and `ui --on sandbox` execute in the **guest**. Run the batch script below on the host, not inside `target exec` (which would double-route). +- Real input and capture require an unlocked host and a connected, nonminimized Sandbox client. Tree inspection may work while input cannot; a readable tree is not an input-readiness check. +- `winapp target snapshot sandbox --json` is a read-only readiness query: it neither starts nor reconnects a guest. Use it to diagnose readiness rather than probing the user's desktop. +- The persistent guest is shared, not isolation between mutually untrusted workflows. Coordinate with other users; there is no supported `--unique-identity` option. Use separate machines for mutually untrusted work. +- Guest `--debug-output` supports **packaged** apps only. -There are two main approaches: -1. Interactive exploration β€” manually run the app, use `winapp ui ` to explore the UI tree, find AutomationIds, verify element properties, and test functionality interactively. This is useful for discovery, but slow and expensive if repeated for every test iteration. -2. Scripted batch testing β€” generate a `ui-tests.ps1` script that exercises all UI elements and asserts expected behavior in one pass. This allows you to run the tests automatically, capture results, and iterate quickly without manually interacting with the app each time. +### Approach and command discovery -Unless the user asked for interactive exploration, or you are unfamiliar with the code/app or need to explore the UI tree to discover AutomationIds for hidden or dynamically generated elements (flyouts, dialogs, lazy-loaded content), **prefer scripted batch testing** β€” it is faster, repeatable, and produces a record of pass/fail results that can be reviewed and acted on. +Prefer a single scripted batch over a long series of interactive calls. If you wrote the app, use its XAML/source AutomationIds directly; otherwise inspect its current tree and read source for hidden/lazy flyouts and dialogs. Either way, validate a real window/tree before passing an accessibility audit. -### `winapp ui` Verbs +Core verbs: `list-windows`, `inspect`, `search`, `get-property`, `get-value`, `wait-for`; `invoke`, `click`, `set-value`, `focus`, `scroll-into-view`; `send-keys`, `hover`, `drag`, `touch`, `pen`; `screenshot`, `record`. Use each verb's `--help` for selectors and options, rather than guessing from this short reference. -- **Query:** `status`, `list-windows`, `inspect`, `search`, `get-property`, `get-value`, `get-focused`, `wait-for` -- **Interact:** `invoke`, `click`, `set-value`, `focus`, `scroll`, `scroll-into-view` -- **Advanced input:** `send-keys` (synthetic keyboard + accelerators), `hover` (tooltips/flyouts), `drag` (drag-drop, reorder, sliders), `touch` (tap/swipe/pinch/stretch), `pen` (stylus ink, pressure/tilt/eraser) -- **Capture:** `screenshot`, `record` (H.264 MP4 video) +### Step 1: Select a target, then keep the PID and target together -Run `winapp ui --cli-schema` for the complete command structure as JSON, or `winapp ui --help` for any single verb. +Prefer `sandbox` when Windows Sandbox is available; otherwise tell the user and choose `local`. **If the user explicitly requested Windows Sandbox and it is unavailable, stop** and point them to the enablement steps in [winui-setup](../winui-setup/SKILL.md). A stopped guest does not prove unavailability: `target snapshot` can report no running target while the feature is enabled. App build/test failures are not Sandbox unavailability. The same policy applies to diagnostics: unpackaged apps can't use guest `--debug-output`, so diagnose them locally unless Sandbox was explicitly requested. -### Step 1: Use the Running App +Pass the selected target to the template: it launches with `winapp run . --on sandbox --detach --json` for a guest, or omits `--on sandbox` for local execution. Reuse an already-running app only when its captured target matches the selected target and the guest has not been recreated. Never pass a guest PID to default-host `winapp ui`. If a target becomes unavailable after selection, report it and select again under the same policy; the script itself never retries in another target. -If the app is already running, use its PID. **Do NOT relaunch** β€” use the PID already captured from the build step. If the app is not running, build and launch it using the guidance in the winui-dev-workflow skill. +The run JSON includes **`ProcessId`**, **`ProcessScope`**, and **`UiTargetArgs`** (PascalCase). In Windows Sandbox, `UiTargetArgs` is **`--on sandbox -a `**: preserve both parts, not just the number. For an explicitly requested **unpackaged** guest run, the launch result has no app PID: launch separately, find the intended app by process/title with target-wide `winapp ui list-windows --on sandbox --json`, then pass its PID with `-Target sandbox -AppPid -AppScope sandbox`. Never use the containment PID. A fresh guest invalidates old PIDs/HWNDs. -### Step 2: Write the Test Script +### Step 2: Write the host-side batch -**If you wrote the code:** Skip inspect β€” you already know all the AutomationIds and control structure from the XAML and code-behind. Write tests directly from that knowledge. Inspect misses popups, flyouts, dialogs, and lazy-loaded content anyway. - -**If you're verifying code you didn't write:** Run inspect first to discover the UI: -```powershell -winapp ui inspect -a --interactive -``` -Then read the XAML files to find AutomationIds that aren't currently visible (flyout items, dialog buttons, secondary pages). - -Create a `ui-tests.ps1` file that tests all the app's requirements in one pass: +Create `ui-tests.ps1`, replace the sample AutomationIds/expected values with the app's requirements, and add the relevant examples below **inside the outer `try`**, before its `finally`. The two small command helpers only route and check CLI calls; they do not implement a target manager. Use them for **every** added command so an earlier native failure cannot be hidden by a later success. ```powershell # ui-tests.ps1 -param([Parameter(Mandatory)][int]$AppPid) -# NOTE: Do NOT name the parameter $Pid β€” it's read-only in PowerShell - -$ErrorActionPreference = 'Continue' -$pass = 0; $fail = 0; $results = @() +param( + [Parameter(Mandatory)][ValidateSet('sandbox', 'local')][string]$Target, + [int]$AppPid = 0, + [ValidateSet('sandbox', 'local')][string]$AppScope, + [ValidateNotNullOrEmpty()][string]$WorkflowId = [guid]::NewGuid().ToString('N'), + [string]$ArtifactDirectory = (Join-Path '.\ui-test-artifacts' ([guid]::NewGuid().ToString('N'))) +) + +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $false +$ScopeArgs = @(if ($Target -eq 'sandbox') { '--on'; 'sandbox' }) +$previousWorkflowId = $env:WINAPP_UI_WORKFLOW_ID +$env:WINAPP_UI_WORKFLOW_ID = $WorkflowId +$results = [Collections.Generic.List[object]]::new() +$screenshots = [Collections.Generic.List[string]]::new() +$uiStarted = $false + +function Invoke-WinAppChecked { + param([string[]]$Arguments) + $global:LASTEXITCODE = $null + $output = @(& winapp @Arguments) + $code = $global:LASTEXITCODE + if ($null -eq $code -or $code -ne 0) { + throw "winapp $($Arguments -join ' ') failed (exit $code). $($output -join "`n")" + } + $output +} -# Get main window HWND (avoids PopupHost interference with JSON parsing) -$windows = winapp ui list-windows -a $AppPid --json 2>$null | ConvertFrom-Json -$hwnd = ($windows | Where-Object { $_.title -ne "PopupHost" } | Select-Object -First 1).hwnd +function Invoke-Ui { + param([string[]]$Arguments, [string]$Window) + $selector = if ($Window) { @('-w', $Window) } else { @('-a', "$AppPid") } + Invoke-WinAppChecked (@('ui') + $Arguments + $ScopeArgs + $selector) +} function Test-UI { param([string]$Name, [scriptblock]$Script) - # IMPORTANT: Inside $Script, use 'throw' to signal failure β€” NOT 'exit 1' - # (exit terminates the entire script, not just the test) try { - $output = & $Script 2>&1 - if ($LASTEXITCODE -eq 0) { - $script:pass++; $script:results += @{ name = $Name; status = "PASS" } - } else { - $script:fail++; $script:results += @{ name = $Name; status = "FAIL"; detail = "$output" } - } + & $Script | Out-Null + $results.Add(@{ name = $Name; status = 'PASS' }) } catch { - $script:fail++; $script:results += @{ name = $Name; status = "FAIL"; detail = "$_" } + $results.Add(@{ name = $Name; status = 'FAIL'; detail = "$_" }) } } -# ─── Element Existence ─── -Test-UI "NavHome exists" { winapp ui wait-for "NavHome" -a $AppPid -t 3000 } -Test-UI "NavSettings exists" { winapp ui wait-for "NavSettings" -a $AppPid -t 3000 } - -# ─── Navigation ─── -Test-UI "Navigate to Settings" { winapp ui invoke "NavSettings" -a $AppPid } -Test-UI "Settings page loaded" { winapp ui wait-for "TxtUserName" -a $AppPid -t 3000 } - -# ─── Interactions ─── -Test-UI "Set username" { winapp ui set-value "TxtUserName" "TestUser" -a $AppPid } -Test-UI "Click Save" { winapp ui invoke "BtnSave" -a $AppPid } # commits the TextBox binding -Test-UI "Username value set" { - winapp ui wait-for "TxtUserName" -a $AppPid --value "TestUser" -t 2000 +function Save-Screenshot { + param([string]$Name) + $path = Join-Path $ArtifactDirectory $Name + if (Test-Path -LiteralPath $path) { throw "Choose a new evidence path: $path" } + Invoke-Ui @('screenshot', '--output', $path) -Window $hwnd | Out-Null + if (-not (Test-Path -LiteralPath $path -PathType Leaf) -or (Get-Item -LiteralPath $path).Length -eq 0) { + throw "Screenshot was not delivered to the host: $path" + } + $screenshots.Add($path) } -# ─── Value assertions for different control types ─── -Test-UI "Theme is System default" { - winapp ui wait-for "CmbTheme" -a $AppPid --value "System default" -t 2000 -} -Test-UI "Logging is off" { - winapp ui wait-for "TglLogging" -a $AppPid --value "Off" -t 2000 -} +try { + $ArtifactDirectory = [IO.Path]::GetFullPath($ArtifactDirectory) + New-Item -ItemType Directory -Force -Path $ArtifactDirectory | Out-Null + if ($AppPid -lt 0) { throw 'AppPid must be positive, or zero to launch.' } + if ($AppPid -gt 0) { + if ($AppScope -ne $Target) { + throw 'Reusing a PID requires matching -AppScope and -Target from the same live target.' + } + } else { + $launch = Invoke-WinAppChecked (@('run', '.') + $ScopeArgs + @('--detach', '--json')) | + ConvertFrom-Json + if ($launch.Error -or -not $launch.ProcessId -or [int]$launch.ProcessId -le 0) { + throw "No app ProcessId. For unpackaged guest runs, discover the app in that target and rerun with -AppPid and -AppScope. $($launch.Error)" + } + $AppPid = [int]$launch.ProcessId + if ($Target -eq 'sandbox' -and + ($launch.ProcessScope -ne 'sandbox' -or $launch.UiTargetArgs -ne "--on sandbox -a $AppPid")) { + throw 'Launch response is missing the expected Sandbox ProcessScope / UiTargetArgs pair.' + } + if ($Target -eq 'local' -and $launch.ProcessScope -and $launch.ProcessScope -ne 'local') { + throw 'Launch response belongs to a different target.' + } + } -# ─── Accessibility Audit ─── -# Only audit controls in the app's main window (exclude OS picker/popup controls) -$inspection = winapp ui inspect -a $AppPid --interactive --json 2>$null | ConvertFrom-Json -$allElements = @($inspection.windows | ForEach-Object { $_.elements }) -$appElements = @($allElements | Where-Object { - $_.type -match 'Button|TextBox|ComboBox|CheckBox|ToggleSwitch|TabItem|Edit' -and - $_.name -notmatch 'Minimize|Maximize|Close|System' -and # window chrome - $_.className -notmatch 'PickerHost|#32770|CabinetWClass' # OS dialogs -}) -$missingId = @($appElements | Where-Object { -not $_.automationId }) -if ($missingId.Count -eq 0) { - $pass++; $results += @{ name = "All app controls have AutomationId"; status = "PASS" } -} else { - $fail++ - $names = ($missingId | ForEach-Object { "$($_.type) '$($_.name)'" }) -join ", " - $results += @{ name = "AutomationId coverage"; status = "FAIL"; detail = "Missing: $names" } + $uiStarted = $true + $windows = @(Invoke-Ui @('list-windows', '--json') | ConvertFrom-Json) + $main = $windows | Where-Object { $_.hwnd -and $_.hwnd -ne '0' -and $_.title -ne 'PopupHost' } | + Select-Object -First 1 + if (-not $main) { throw 'No app window found in the selected target; rediscover after a fresh guest.' } + $hwnd = [string]$main.hwnd + + Test-UI 'Initial screenshot delivered' { Save-Screenshot '01-initial.png' } + Test-UI 'Home exists' { Invoke-Ui @('wait-for', 'NavHome', '-t', '3000') } + Test-UI 'Navigate to Settings' { + Invoke-Ui @('invoke', 'NavSettings') + Invoke-Ui @('wait-for', 'TxtUserName', '-t', '3000') + } + Test-UI 'Save username' { + Invoke-Ui @('set-value', 'TxtUserName', 'TestUser') + Invoke-Ui @('invoke', 'BtnSave') + Invoke-Ui @('wait-for', 'TxtUserName', '--value', 'TestUser', '-t', '2000') + } + Test-UI 'Default values' { + Invoke-Ui @('wait-for', 'CmbTheme', '--value', 'System default', '-t', '2000') + Invoke-Ui @('wait-for', 'TglLogging', '--value', 'Off', '-t', '2000') + } + Test-UI 'App controls have AutomationIds' { + $inspection = Invoke-Ui @('inspect', '--interactive', '--json') -Window $hwnd | ConvertFrom-Json + function Get-Elements($nodes) { + foreach ($node in $nodes) { + if ($null -ne $node) { $node; Get-Elements $node.children } + } + } + $allElements = @(Get-Elements @($inspection.windows | ForEach-Object { $_.elements })) + if (-not $allElements.Count) { throw 'Inspection returned no elements; not an accessibility PASS.' } + $appElements = @($allElements | Where-Object { + $_.type -match 'Button|TextBox|ComboBox|CheckBox|ToggleSwitch|TabItem|Edit' -and + $_.name -notmatch 'Minimize|Maximize|Close|System' -and + $_.className -notmatch 'PickerHost|#32770|CabinetWClass' + }) + if (-not $appElements.Count) { throw 'No app controls were audited; adjust selectors rather than passing.' } + $missingId = @($appElements | Where-Object { -not $_.automationId }) + if ($missingId.Count) { + throw "Missing AutomationIds: $(($missingId | ForEach-Object { "$($_.type) '$($_.name)'" }) -join ', ')" + } + } + Test-UI 'Final screenshot delivered' { Save-Screenshot '02-settings.png' } +} catch { + $results.Add(@{ name = 'Target setup / test execution'; status = 'FAIL'; detail = "$_" }) +} finally { + try { + if ($uiStarted) { Invoke-WinAppChecked (@('ui', 'yield') + $ScopeArgs) | Out-Null } + } catch { + $results.Add(@{ name = 'Release UI workflow'; status = 'FAIL'; detail = "$_" }) + } finally { + $env:WINAPP_UI_WORKFLOW_ID = $previousWorkflowId + } } -# ─── State Screenshots (capture each meaningful state for visual review) ─── -New-Item -ItemType Directory -Force -Path "screenshots" | Out-Null -winapp ui screenshot -a $AppPid -o "screenshots/01-initial.png" 2>$null -# ...take more screenshots after key interactions above (mode switches, dialogs opened, etc.) - -# ─── Final Screenshot ─── -winapp ui screenshot -a $AppPid -o "test-screenshot.png" 2>$null - -# ─── Results ─── -Write-Host "`nPassed: $pass | Failed: $fail" -$results | Where-Object { $_.status -eq "FAIL" } | ForEach-Object { - Write-Host " FAIL: $($_.name) β€” $($_.detail)" -ForegroundColor Red +$failed = @($results | Where-Object { $_.status -eq 'FAIL' }).Count +$report = [ordered]@{ + target = $Target; appPid = $AppPid; workflowId = $WorkflowId + passed = $results.Count - $failed; failed = $failed + results = @($results.ToArray()); screenshots = @($screenshots.ToArray()) } -$results | ConvertTo-Json | Out-File "test-results.json" -if ($fail -gt 0) { exit 1 } else { exit 0 } +$report | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath '.\test-results.json' -Encoding utf8 +Write-Host "Passed: $($report.passed) | Failed: $failed | Results: .\test-results.json" +if ($failed) { exit 1 } +exit 0 ``` -### What to Test - -Write tests for **every requirement** from the user's prompt: - -| Requirement type | Test approach | -|---|---| -| "Has a button that does X" | `search` to verify exists, `invoke` to click, `wait-for --value` to check result | -| "Text field shows value" | `wait-for "TxtName" --value "expected"` β€” works for TextBox, TextBlock, labels | -| "Status bar contains text" | `wait-for "StatusBar" --value "words" --contains` β€” substring match for dynamic content | -| "Dropdown is set to X" | `wait-for "CmbTheme" --value "Dark"` β€” reads the selected item automatically | -| "Toggle is on/off" | `wait-for "TglFeature" --value "On"` β€” reads the toggle state | -| "Navigation between pages" | `invoke` nav item, `wait-for` a page-specific element to appear | -| "Open file dialog" | `invoke` trigger, `list-windows` to find picker HWND, interact with `-w` | -| "Save file dialog" | Same as open β€” find picker with `list-windows`, `set-value` filename, `invoke` Save | -| "Right-click context menu" | `click --right` on element, `invoke` the flyout MenuItem | -| "Keyboard shortcut (Ctrl+S, etc.)" | `send-keys "ctrl+s" --via send-input` then `wait-for` the result | -| "Type into a TextBox/RichEditBox" | `send-keys "text" --target "Id" --via send-input` (real per-key input) | -| "Tooltip / hover flyout appears" | `hover` the element, then `wait-for` the tooltip/flyout | -| "Drag to reorder / resize / slider" | `drag ` then `wait-for --value` the new state | -| "Touch gesture (swipe/pinch/stretch)" | `touch -g swipe/pinch/stretch` then assert the result | -| "Capture a repro clip of a flow" | `record -a PID --duration-sec N -o clip.mp4` | -| "Confirmation dialog" | `invoke` trigger, `search` for dialog buttons, `invoke` Primary/Secondary/Close | -| "Data persists" | Set values, `invoke` a button (to commit bindings), verify data file on disk (`Get-Content` + `ConvertFrom-Json`) | -| "All controls accessible" | `inspect --interactive --json` + check all have AutomationId | - -### Step 3: Run and Read Results - -```powershell -.\ui-tests.ps1 -AppPid -``` +Within a test, **throw**, not `exit`; the script fails on CLI errors, empty inspections, and undelivered evidence. Screenshots target the known main HWND so each lands at one exact host path; use `--capture-screen` when popups must be included. -Read `test-results.json` for structured pass/fail. Only fix code if tests fail. - -### Step 3.5: Look at the Screenshots - -UIA assertions don't see clipping, overlap, wrong theming, or controls bleeding past their container β€” UIA returns `PASS` while the app is visually broken. **Capture screenshots with `winapp ui screenshot` and view each PNG.** - -Capture the initial state and any state after a major interaction (the State Screenshots block in the script template above handles this). - -**Visual checklist β€” fail the run if any item is `no`:** -- [ ] No unintended scrollbars -- [ ] No text ending in `…` that shouldn't be -- [ ] Hero elements fully visible (not sliced) -- [ ] Right-edge controls fully visible -- [ ] No overlapping rows -- [ ] Content uses the available width β€” no asymmetric dead zones (e.g. content pinned to one edge leaving empty space on the other) -- [ ] Spacing intentional β€” not cramped, not unintentionally vast -- [ ] Theming matches the user's ask (Light/Dark/HighContrast if relevant) -- [ ] Focus/hover/error states render if tested - -If the checklist fails, it's a bug β€” fix before declaring done. Window too small β†’ grow per `winui-design` Step 4. - -### Step 4: Fix and Rerun (if the user asked for it) - -If tests fail: -1. Read the failure details from `test-results.json` -2. Batch-fix all issues in one pass -3. Rebuild with `.\BuildAndRun.ps1` (blocking mode β€” shows crash info if the fix broke something) -4. Rerun `.\ui-tests.ps1 -AppPid ` (parse PID from the `launched (PID: XXXXX)` output) - -**Maximum 2 fix-and-rerun cycles.** If the same tests keep failing after 2 cycles, report them as known issues and move on β€” do not keep iterating. - -### Assertion Reference - -Use `wait-for --value` as the primary assertion β€” it uses a smart fallback chain that reads the right value for any control type: - -| Control type | `--value` reads from | Example | -|---|---|---| -| TextBlock / Label | Name property | `wait-for "LblTitle" --value "Home"` | -| TextBox / NumberBox | ValuePattern | `wait-for "TxtName" --value "John"` | -| RichEditBox | TextPattern | `wait-for "Editor" --value "Hello"` | -| ComboBox | Selected item (SelectionPattern) | `wait-for "CmbTheme" --value "Dark"` | -| ToggleSwitch | Toggle state (On/Off) | `wait-for "TglDark" --value "On"` | -| CheckBox | Toggle state (On/Off) | `wait-for "ChkAgree" --value "On"` | - -**Full assertion commands:** - -| Assertion | Command | -|---|---| -| Element exists | `winapp ui wait-for "Id" -a PID -t 3000` | -| Element has exact value | `winapp ui wait-for "Id" -a PID --value "expected" -t 3000` | -| Value contains text | `winapp ui wait-for "Id" -a PID --value "words" --contains -t 3000` | -| Element gone | `winapp ui wait-for "Id" -a PID --gone -t 3000` | -| Specific property | `winapp ui wait-for "Id" -a PID -p IsEnabled --value "True" -t 3000` | -| Button clickable | `winapp ui invoke "Id" -a PID` (exit code 0) | -| Set then verify | `winapp ui set-value "Id" "text" -a PID` then `wait-for --value` | -| Screenshot | `winapp ui screenshot -a PID -o path.png` | -| Dialog appeared | `winapp ui list-windows -a PID --json` (check window count) | -| Right-click menu | `winapp ui click "Id" -a PID --right` then `wait-for` menu item | -| Read raw property | `winapp ui get-property "Id" -a PID -p IsEnabled --json` | -| Read current value (no wait) | `(winapp ui get-value "Id" -a PID --json \| ConvertFrom-Json).text` β€” always pass `--json` when capturing into a variable (plain stdout can include advisory text like "Auto-selected HWND … from N windows"); otherwise prefer `wait-for --value` | -| Scroll item into view | `winapp ui scroll-into-view "Id" -a PID` β€” call before `wait-for` on virtualized ListView/repeater items below the fold | -| Set keyboard focus | `winapp ui focus "Id" -a PID` β€” cleaner than clicking another control to trigger a TextBox `LostFocus` commit | -| Type real keystrokes into a control | `winapp ui send-keys "text" --target "Id" -a PID --via send-input` | -| Fire a keyboard accelerator/shortcut | `winapp ui send-keys "ctrl+s" -a PID --via send-input` | -| Hover to show tooltip/flyout | `winapp ui hover "Id" -a PID` then `wait-for` | -| Drag / reorder / slider gesture | `winapp ui drag "From" "To" -a PID` | -| Touch gesture | `winapp ui touch "Id" -g swipe --direction up -a PID` | -| Pen / ink stroke | `winapp ui pen "InkCanvas" --path "x,y x,y" -a PID` | -| Record a video clip | `winapp ui record -a PID --duration-sec N -o clip.mp4` | - -### Testing File Pickers - -File/folder pickers (FileOpenPicker, FileSavePicker, FolderPicker) run in a separate `PickerHost` process but are fully interactable. The picker appears as an owned dialog window. +### Step 3: Run, read, and visually verify ```powershell -# 1. Trigger the picker -winapp ui invoke "BtnOpenFile" -a $AppPid - -# 2. Find the picker window (it's a dialog owned by the app window) -Start-Sleep 1 -$allWindows = winapp ui list-windows -a $AppPid --json 2>$null | ConvertFrom-Json -$picker = $allWindows | Where-Object { $_.title -match "Open|Save" } -$pickerHwnd = $picker.hwnd - -# 3. Interact with the picker using -w -# Type a filename: -winapp ui set-value "FileNameControlHost" "test.txt" -w $pickerHwnd -# Click Open/Save: -winapp ui invoke "Open" -w $pickerHwnd # or "Save", "Cancel" -# Or cancel: -winapp ui invoke "Cancel" -w $pickerHwnd - -# 4. Verify the app processed the file -winapp ui wait-for "StatusBar" -a $AppPid -p Name --value "opened" -t 3000 +# Preferred when Windows Sandbox is available: +.\ui-tests.ps1 -Target sandbox +if ($LASTEXITCODE -ne 0) { throw 'UI batch failed; read test-results.json.' } +$report = Get-Content -LiteralPath '.\test-results.json' -Raw | ConvertFrom-Json +$report.screenshots ``` -**Tip:** Use `winapp ui inspect -w --interactive` to discover the picker's controls β€” they include the folder tree, file list, filename textbox, and Open/Cancel buttons. +For a captured, still-live guest PID, use `.\ui-tests.ps1 -Target sandbox -AppPid 1234 -AppScope sandbox`. For a local run (see Step 1), use `.\ui-tests.ps1 -Target local`, optionally with a local PID and `-AppScope local`. Never reuse a guest PID locally or switch targets to make failing tests pass. -### Testing Context Menus and Flyouts +View **each host PNG** listed in `test-results.json` with the image-viewing tool. UIA PASS cannot detect clipping, overlap, incorrect theming, or content bleeding past its container. Fail visual review for unintended scrollbars, unintended ellipses, clipped hero/right-edge controls, overlapping rows, unbalanced whitespace, cramped/vast spacing, incorrect Light/Dark/High Contrast, or missing focus/hover/error states. Capture meaningful states immediately after their interactions, not just at the end. -MenuFlyouts and ContextFlyouts are fully testable. They appear in the UI automation tree when open. - -```powershell -# 1. Right-click to open a ContextFlyout -winapp ui click "LstItems" -a $AppPid --right -Start-Sleep 0.5 +If fixes are requested, batch-fix failures, rebuild/relaunch with the same target using the template (omit `-AppPid` to launch again), and capture the new PID. Maximum **two** fix-and-rerun cycles; then report remaining failures. Do not declare completion without the structured results **and** visual review. -# 2. The flyout MenuItems appear in the tree immediately -# Find them with inspect or search: -winapp ui inspect -a $AppPid --interactive # shows MnuCopy, MnuDelete, etc. +### Assertions and coverage -# 3. Click a flyout item -winapp ui invoke "MnuCopy" -a $AppPid +Write tests for every requested requirement. Use `wait-for --value` for TextBox/NumberBox values, RichEditBox text, ComboBox selection, toggle/checkbox `On`/`Off`, and TextBlock/label text. `--contains` matches a substring; `--gone` waits for disappearance. Use `-p IsEnabled --value True` only for a specific property. Invoke buttons/nav items, wait for page-specific elements, and test default/error/empty states. -# 4. Verify the action -winapp ui wait-for "StatusText" -a $AppPid -p Name --value "Copied" -t 2000 -``` +The following examples are additions to the template, not independent shells. `Invoke-Ui` always supplies the chosen scope plus `-a $AppPid`, or scope plus `-w $Window`. For raw reads, pass `--json` and parse it; plain stdout can include advisory messages. Use `scroll-into-view` before asserting a virtualized item below the fold. -**For MenuBar flyouts** (File, Edit, View menus): ```powershell -# Click the menu header to open -winapp ui invoke "FileMenu" -a $AppPid -Start-Sleep 0.5 -# Click the sub-item -winapp ui invoke "MenuSaveAs" -a $AppPid +Test-UI 'Status and enabled state' { + Invoke-Ui @('wait-for', 'StatusBar', '--value', 'saved', '--contains', '-t', '3000') + Invoke-Ui @('wait-for', 'BtnSave', '-p', 'IsEnabled', '--value', 'True', '-t', '3000') + $value = Invoke-Ui @('get-value', 'TxtUserName', '--json') | ConvertFrom-Json + if ($value.text -ne 'TestUser') { throw 'Wrong username.' } +} ``` -### Testing ContentDialogs +### File pickers, flyouts, and ContentDialogs -ContentDialogs are in-app controls (same window) β€” they appear directly in the UI tree when shown. +Pickers run in a separate `PickerHost` process. Discover their HWND in the **same target**; `-w` changes the window selector, never the target. Replace the filename with a file that exists **in the guest** for Sandbox testing. ```powershell -# 1. Trigger the dialog -winapp ui invoke "BtnDelete" -a $AppPid -Start-Sleep 0.5 - -# 2. The dialog buttons appear in the tree -# For a standard confirmation dialog: -winapp ui search "Primary" -a $AppPid --json # finds the primary button -winapp ui invoke "Primary" -a $AppPid # click "Yes"/"Delete"/"Save" -# Or: -winapp ui invoke "Secondary" -a $AppPid # click "No"/"Don't Save" -winapp ui invoke "Close" -a $AppPid # click "Cancel" - -# 3. Wait for dialog to dismiss -winapp ui wait-for "Primary" -a $AppPid --gone -t 3000 -``` +Test-UI 'Open file picker' { + Invoke-Ui @('invoke', 'BtnOpenFile') + Start-Sleep -Seconds 1 + $allWindows = @(Invoke-WinAppChecked (@('ui', 'list-windows', '--json') + $ScopeArgs) | ConvertFrom-Json) + $pickers = @($allWindows | Where-Object { $_.hwnd -and [string]$_.ownerHwnd -eq $hwnd -and $_.title -match 'Open|Save' }) + if ($pickers.Count -ne 1) { throw 'Expected one picker; inspect the selected target again.' } + $pickerHwnd = [string]$pickers[0].hwnd + Invoke-Ui @('inspect', '--interactive', '--json') -Window $pickerHwnd + Invoke-Ui @('set-value', 'FileNameControlHost', 'test.txt') -Window $pickerHwnd + Invoke-Ui @('invoke', 'Open') -Window $pickerHwnd + Invoke-Ui @('wait-for', 'StatusBar', '--value', 'opened', '--contains', '-t', '3000') +} -**Tip:** ContentDialog buttons often don't have custom AutomationIds β€” use `inspect` to find the actual selector (slug or text match). +Test-UI 'Copy context menu' { + Invoke-Ui @('click', 'LstItems', '--right') + Invoke-Ui @('wait-for', 'MnuCopy', '-t', '2000') + Invoke-Ui @('invoke', 'MnuCopy') + Invoke-Ui @('wait-for', 'StatusText', '--value', 'Copied', '-t', '2000') +} -### Advanced Input: keyboard, hover, drag, touch & pen +Test-UI 'Confirm deletion' { + Invoke-Ui @('invoke', 'BtnDelete') + Invoke-Ui @('wait-for', 'Primary', '-t', '2000') + Invoke-Ui @('invoke', 'Primary') + Invoke-Ui @('wait-for', 'Primary', '--gone', '-t', '3000') +} +``` -Synthetic input beyond `invoke`/`click`/`set-value`. Each verb takes `-a ` / `-w ` like the rest. +For Save/Cancel pickers, substitute the appropriate action and assertion. MenuBar headers and flyout items use `invoke` and `wait-for` similarly. ContentDialogs live in the app window; `Primary`, `Secondary`, and `Close` are common selectors, but inspect the actual open dialog because custom AutomationIds are not guaranteed. -**`send-keys` β€” real keyboard input.** Named keys (`enter`, `tab`, `f5`), combos (`ctrl+shift+t`), raw `vk=0x42`, or literal text. `--via` selects the transport: -- `post-message` (default) β€” HWND-targeted, no foreground needed; raises `TextChanged` but **not** per-character `KeyDown`. -- `send-input` β€” OS-wide; real per-character `KeyDown` + `TextChanged`. **Required for accelerators/shortcuts** (`KeyboardAccelerator`, e.g. `ctrl+t`) and for reliable typing into a WinUI 3 / WPF `TextBox`. +### Keyboard, hover, drag, touch, and pen -```powershell -winapp ui send-keys "ctrl+s" -a $AppPid --via send-input # fire a Ctrl+S accelerator -winapp ui send-keys "hello world" --target "TxtName" -a $AppPid --via send-input # focus then type -winapp ui send-keys --verbatim "down down enter" -a $AppPid # type the words, not the keys -``` -`--target` focuses first; `text=` / `--verbatim` type literally instead of interpreting key names. System combos (`win+r`, `alt+f4`) need `--allow-system-keys` + `--via send-input` (`win+l` stays blocked). +`send-keys --via send-input` is required for accelerators and reliable per-character input in WinUI/WPF text controls. `post-message` is HWND-targeted but does not produce per-character `KeyDown`. `--target` focuses first; `--verbatim` types literal key names. System shortcuts require the CLI's explicit permission flags; do not use them as a workaround for desktop-readiness errors. -**`hover` β€” tooltips, flyouts, hover states.** Dwells on the element (`--dwell-time`, default 800 ms) so hover-triggered UI appears in the tree. ```powershell -winapp ui hover "BtnInfo" -a $AppPid -winapp ui wait-for "InfoTooltip" -a $AppPid -t 2000 +Test-UI 'Keyboard save' { + Invoke-Ui @('send-keys', 'hello world', '--target', 'TxtName', '--via', 'send-input') + Invoke-Ui @('send-keys', 'ctrl+s', '--via', 'send-input') + Invoke-Ui @('wait-for', 'StatusBar', '--value', 'saved', '--contains', '-t', '2000') +} +Test-UI 'Tooltip' { + Invoke-Ui @('hover', 'BtnInfo') + Invoke-Ui @('wait-for', 'InfoTooltip', '-t', '2000') +} +Test-UI 'Reorder items' { + Invoke-Ui @('drag', 'ItemA', 'ItemB') + Invoke-Ui @('wait-for', 'StatusBar', '--value', 'reordered', '--contains', '-t', '2000') +} +Test-UI 'Touch and ink input' { + Invoke-Ui @('touch', 'LstFeed', '-g', 'swipe', '--direction', 'up', '--distance', '400') + Invoke-Ui @('pen', 'InkCanvas', '--path', '50,50 120,80 200,60', '--pressure', '0.8') + Save-Screenshot '03-ink.png' + # Add the app-specific state assertion; successful injection alone is not functional proof. +} ``` -**`drag` β€” drag-drop, reorder, resize, sliders.** ``/`` are each an element selector (its center) or screen `x,y` from `inspect`. `--hold-ms` long-presses before moving; `--dwell-ms` settles on the target before releasing (merge/latch targets). -```powershell -winapp ui drag "ItemA" "ItemB" -a $AppPid # reorder ItemA onto ItemB -winapp ui drag "SldVolume" 300,120 -a $AppPid # drag a slider thumb to a point -``` +Use `winapp ui drag --help`, `winapp ui touch --help`, and `winapp ui pen --help` for coordinates, long-press/dwell, pinch/stretch, pressure/tilt/eraser support. Read coordinates in the guest tree; host coordinates are not guest coordinates. + +### Recordings and workflow coordination -**`touch` β€” touch gestures.** `-g`: `tap` (default), `double-tap`, `long-press`, `swipe`, `pinch`, `stretch`; `--direction`/`--distance`/`--to-point` for swipes, `--fingers` for multi-touch. Needs an unlocked interactive desktop with the window foregroundable. ```powershell -winapp ui touch "LstFeed" -g swipe --direction up --distance 400 -a $AppPid -winapp ui touch "ImgPhoto" -g stretch --distance 200 -a $AppPid # pinch-to-zoom +Test-UI 'Video delivered' { + $video = Join-Path $ArtifactDirectory 'flow.mp4' + if (Test-Path -LiteralPath $video) { throw 'Choose a new video evidence path.' } + Invoke-Ui @('record', '--duration-sec', '6', '--fps', '30', '--output', $video) + if (-not (Test-Path -LiteralPath $video -PathType Leaf) -or (Get-Item -LiteralPath $video).Length -eq 0) { + throw "Recording was not delivered to the host: $video" + } +} ``` -**`pen` β€” pen/stylus.** Taps or draws ink; `--path "x,y x,y …"` for a multi-point stroke, with `--pressure`, `--tilt-x`/`--tilt-y`, `--eraser` (Win10 1809+). +Sandbox screenshot/record **`--output` paths are host paths**, automatically delivered when capture completes; do not pull them from guessed guest directories. Use fresh output paths and check the exit status and actual host files. `--capture-screen` can include popups outside the app window. For an interrupted recording, preserve `stopReason`, `partialOutput`, `recoveryHint`, and the reported partial/recovery paths; a nonempty partial file is not necessarily playable. + +WinApp CLI arbitrates desktop-changing commands automatically. Without a workflow ID, each command releases its turn immediately; with one, the four-second grace covers tight script bursts, not agent reasoning. Use the **same `WINAPP_UI_WORKFLOW_ID`** for cooperating commands, including concurrent recording and interaction; independent flows need distinct IDs. The template accepts `-WorkflowId` for this purpose, sets it in its own process, restores the previous value, and calls `winapp ui yield --on sandbox` in `finally`. Fresh shell tool calls do not inherit changes from an earlier shell: set the same ID on **every** cooperating invocation. A separate concurrent recording process must also check its exit code/delivery, use the same scoped PID, and finish before the test runner yields. Do not start an independent-ID recording that blocks the interactions it is meant to capture. After a pause, inspect again and reopen menus/dialogs another workflow may have changed. + +### Guest persistence and arbitrary file transfer + +Do not inspect **host** `$env:LOCALAPPDATA` to prove **guest** persistence. Check the actual app data file in the selected target. Replace the sample package family/path with the app's real storage location; unpackaged apps usually use an app-specific LocalAppData directory. + ```powershell -winapp ui pen "InkCanvas" --path "50,50 120,80 200,60" --pressure 0.8 -a $AppPid -winapp ui pen "InkCanvas" --path "50,50 200,60" --eraser -a $AppPid +Test-UI 'Username persisted in selected target' { + $readSettings = @' +$ErrorActionPreference = 'Stop' +$file = Join-Path $env:LOCALAPPDATA 'Packages\YourPackageFamily\LocalState\settings.json' +if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { throw "Missing settings: $file" } +Get-Content -LiteralPath $file -Raw +'@ + $json = if ($Target -eq 'sandbox') { + Invoke-WinAppChecked @('target', 'exec', 'sandbox', '--', 'powershell.exe', '-NoProfile', '-NonInteractive', '-Command', $readSettings) + } else { + & ([scriptblock]::Create($readSettings)) + } + $settings = $json | ConvertFrom-Json + if ($settings.UserName -ne 'TestUser') { throw 'Saved username does not match.' } +} ``` -### Recording a Video - -`winapp ui record` captures the target window (or an element region) to an H.264 MP4 β€” handy for a repro clip of a flow or animation. Records until stopped (newline/EOF on stdin, or Ctrl+C); `--duration-sec N` gives a fixed-length clip (simplest for scripts). The MP4 is finalized on graceful stop. +For arbitrary setup/results files, use `target exec` / `push` / `pull`, not host filesystem guesses. Transfer guest paths are **relative to snapshot `workRoot`**, normally `C:\WinApp\work`, not absolute guest paths. The following optional additions assume a host `setup.ps1` that creates `Results` under the guest work root: ```powershell -winapp ui record -a $AppPid --duration-sec 6 --fps 30 -o "flow.mp4" +Test-UI 'Guest setup and result transfer' { + if ($Target -ne 'sandbox') { throw 'This transfer test requires Sandbox.' } + $snapshot = Invoke-WinAppChecked @('target', 'snapshot', 'sandbox', '--json') | ConvertFrom-Json + if (-not $snapshot.workRoot) { throw 'No live guest workRoot; fix readiness first.' } + Invoke-WinAppChecked @('target', 'push', 'sandbox', '.\setup.ps1', 'Setup\setup.ps1') + $guestSetup = $snapshot.workRoot.TrimEnd('\') + '\Setup\setup.ps1' + Invoke-WinAppChecked @('target', 'exec', 'sandbox', '--', 'powershell.exe', '-NoProfile', '-File', $guestSetup) + Invoke-WinAppChecked @('target', 'pull', 'sandbox', 'Results', (Join-Path $ArtifactDirectory 'results')) +} ``` -`--max-edge N` downscales large windows; `--capture-screen` uses screen BitBlt to include popups/overlays outside the target window (also on `screenshot`). -### Key Gotchas +Keep the guest alive while recovering failed delivery. Preserve evidence/recovery paths and app data needed for diagnosis before any **user-consented** shutdown; never stop all Sandboxes as routine cleanup. + +### Binding and selector gotchas -- **`set-value` does NOT commit default TextBox bindings** β€” WinUI 3 `x:Bind TwoWay` on TextBox.Text updates the ViewModel on `LostFocus` by default. UIA `set-value` changes the text but doesn't trigger focus events. **Fix:** apps should use `UpdateSourceTrigger=PropertyChanged` on TextBox bindings (see design skill). If the app doesn't, `invoke` a button or `click`/`focus` another element after `set-value` to trigger `LostFocus`. -- **Set a `RichEditBox` with `send-keys`, not `set-value`** β€” WinUI 3 `RichEditBox` / WPF `RichTextBox` don't support UIA value-setting. `focus` (or `--target`), then `send-keys "…" --via send-input` β€” which also raises real per-key `KeyDown`, so use it whenever a control reacts to individual keystrokes (or a `KeyboardAccelerator`) rather than a bulk value change. -- **Verify persistence via the data file, not UI relaunch** β€” killing and relaunching a packaged app from a test script is fragile (MSIX registration timing, PID issues). Instead, check the data file on disk: `Get-Content $dataFile | ConvertFrom-Json` and verify expected values. -- **Use `$AppPid` not `$Pid`** β€” `$Pid` is a read-only automatic variable in PowerShell -- **Use `--value` without `-p`** β€” it auto-detects the right UIA pattern (TextPattern β†’ ValuePattern β†’ TogglePattern β†’ SelectionPattern β†’ Name). Only use `-p PropertyName --value` when you need a specific property like `IsEnabled` -- **File pickers need `-w `** β€” they run in a separate PickerHost process, so `-a PID` won't find them. Use `list-windows` to discover the picker HWND first -- **Flyouts need a short `Start-Sleep`** after triggering β€” the menu items appear in the tree asynchronously +- `set-value` does not commit default WinUI `x:Bind TwoWay` TextBox bindings until focus changes. Prefer `UpdateSourceTrigger=PropertyChanged` in the app; otherwise `invoke` Save or `focus` another control before checking persistence. +- RichEditBox/RichTextBox generally need `send-keys --via send-input`, not UIA `set-value`. +- Use `$AppPid`, never PowerShell's read-only automatic `$Pid`. +- A picker HWND needs `-w` **and the same target scope**. A fresh guest invalidates both HWNDs and PIDs; do not recycle either. diff --git a/plugins/winui/agent-plugin/skills/winui-wpf-migration/SKILL.md b/plugins/winui/agent-plugin/skills/winui-wpf-migration/SKILL.md index 97547b0a..73517c7a 100644 --- a/plugins/winui/agent-plugin/skills/winui-wpf-migration/SKILL.md +++ b/plugins/winui/agent-plugin/skills/winui-wpf-migration/SKILL.md @@ -5,6 +5,8 @@ description: "Migrate WPF applications to WinUI 3 β€” namespace replacement (Sys ### Migration Process +Use the **WinApp CLI 0.7+** prerequisites and per-app analyzer setup in [winui-dev-workflow](../winui-dev-workflow/SKILL.md). The normal SDK path needs .NET 8.0.100 or later **and** the SDK required by the target TFM; Native AOT additionally needs MSVC/Desktop C++ tools. Handle missing prerequisites as described there. + #### Step 1: Audit the WPF Source Before writing code, inventory WPF-specific APIs: ```powershell @@ -17,15 +19,15 @@ List: WPF controls used, custom MVVM framework, imaging APIs, threading patterns ```powershell winapp new --name --template winui-mvvm --template-version latest --use-defaults ``` -Immediately set `` in `.csproj` to match the WPF namespace. Update `x:Class` in `App.xaml`, `MainWindow.xaml` and their code-behind files. Build to verify before porting any code. - -Once the project restores, use `winapp find-api` scoped to the new WinUI project to confirm each replacement below exists with the members you expect, instead of trusting a WPF-shaped assumption. Run these from the WinUI project root β€” an unscoped call can answer for the old WPF project sitting beside it: +Immediately set `` in `.csproj` to match the WPF namespace. Update `x:Class` in `App.xaml`, `MainWindow.xaml` and their code-behind files. Add the analyzer per [winui-dev-workflow](../winui-dev-workflow/SKILL.md). Build to verify before porting any code. +Before implementing API replacements, restore the app project and check its exact references from the WinUI project directory, not the old WPF project or machine SDK: ```powershell winapp find-api DispatcherQueue --json --project-dir . -winapp find-api members DispatcherQueue --filter TryEnqueue --project-dir . -winapp find-api check-property ListView ItemsSource SelectionMode --project-dir . +winapp find-api members DispatcherQueue --filter TryEnqueue --json --project-dir . +winapp find-api check-property ListView ItemsSource SelectionMode --json --project-dir . ``` +Use `winapp find-ui ""` for usage samples; see [winui-design](../winui-design/SKILL.md) for project selection and batch API checks. #### Step 3: Replace Namespaces @@ -70,9 +72,9 @@ Get via `DispatcherQueue.GetForCurrentThread()`. No `Application.Current.Dispatc #### Step 7: Replace MVVM Framework Delete custom `ObservableObject`/`RelayCommand`/`DelegateCommand`. Use CommunityToolkit.Mvvm: -- `INotifyPropertyChanged` base β†’ `ObservableObject` with `[ObservableProperty]` partial properties +- `INotifyPropertyChanged` base β†’ `ObservableObject` with `[ObservableProperty]` partial properties (fix MVVMTK0045; don't keep fields) - Custom `RelayCommand` β†’ `[RelayCommand]` attribute -- `{Binding}` β†’ `{x:Bind Mode=OneWay}` +- Prefer `{x:Bind}` for known types; keep runtime `{Binding}`/`DisplayMemberPath` where needed. See [source-generator patterns](../winui-packaging/references/sourcegen-patterns.md) for binding modes, `x:DataType`, and AOT-safe runtime binding. - `DynamicResource` β†’ `{ThemeResource}` #### Step 8: Replace Resources @@ -83,10 +85,11 @@ Delete custom `ObservableObject`/`RelayCommand`/`DelegateCommand`. Use Community ### Critical Rules - ❌ NEVER reference `PresentationCore`, `PresentationFramework`, or `System.Windows.Controls` assemblies -- ❌ NEVER add `true` or `None` +- ❌ NEVER add `true` +- Keep packaged as the default; see [winui-dev-workflow](../winui-dev-workflow/SKILL.md) Critical Rules for unpackaged experiments. - ❌ NEVER delete `Package.appxmanifest` - ❌ NEVER overwrite `App.xaml` / `App.xaml.cs` β€” merge WPF code into the WinUI 3 boilerplate -- βœ… Always use `winapp run` to launch β€” never run the .exe directly +- βœ… Launch with project-mode `winapp run`, not the .exe directly. - βœ… Break migration into file-level tasks β€” not one massive rewrite ### Post-Migration Validation @@ -98,6 +101,11 @@ Select-String -Path (Get-ChildItem -Recurse -Filter "*.cs" | Where-Object { $_.F # Verify packaging preserved Test-Path "Package.appxmanifest" # should be True -# Build and run with the bundled analyzer -.\BuildAndRun.ps1 +# Build (the analyzer participates when installed) +dotnet build .\MyApp.csproj -p:Platform=x64 + +# Run the migrated app +winapp run .\MyApp.csproj --detach --json ``` + +For UI validation, see [winui-ui-testing](../winui-ui-testing/SKILL.md); for crash diagnostics, see [winui-dev-workflow](../winui-dev-workflow/SKILL.md). If AOT is intended, also test the published artifact via the workflow's AOT path; the run above is JIT, even in Release. diff --git a/plugins/winui/agents/winui-dev.agent.md b/plugins/winui/agents/winui-dev.agent.md index 370ad05d..04e60a6c 100644 --- a/plugins/winui/agents/winui-dev.agent.md +++ b/plugins/winui/agents/winui-dev.agent.md @@ -14,8 +14,8 @@ You build WinUI 3 desktop apps following this process: understand requirements Before continuing -1. Load the `winui-dev-workflow` skill β€” it uses WinApp CLI 0.7+ for scaffolding and project-mode build/run, with `BuildAndRun.ps1` adding the bundled analyzer -2. Load the `winui-design` skill β€” it has Fluent Design rules, control selection, XAML correctness, theming guidance, grounded `winapp find-ui` sample lookup, and project-scoped `winapp find-api` verification +1. Load the `winui-dev-workflow` skill β€” it uses WinApp CLI 0.7+ for scaffolding, direct build/run, the analyzer NuGet reference, and opt-in Native AOT +2. Load the `winui-design` skill β€” it has Fluent Design rules, XAML correctness, theming guidance, and grounded `winapp find-ui` / `winapp find-api` lookup ## Best Practices diff --git a/scripts/build-tools.ps1 b/scripts/build-tools.ps1 deleted file mode 100644 index 00d1a46f..00000000 --- a/scripts/build-tools.ps1 +++ /dev/null @@ -1,90 +0,0 @@ -#Requires -Version 7.0 -<# -.SYNOPSIS - One-shot build for the C# tooling in this repo, including the analyzer DLL - payload refresh that the winui-dev-workflow skill ships with. - -.DESCRIPTION - Builds and tests the WinUI 3 / Windows App SDK Roslyn analyzer, then - refreshes the analyzer skill payload. - - This script exists to give contributors one verb to run before opening - a PR. The pr-validation.yml workflow will rebuild everything in CI - anyway, but provenance checks (analyzer-provenance and - analyzer-targets-sync) will fail fast on the PR if the committed payload - drifts from source β€” running this script keeps it in sync. - -.PARAMETER Configuration - Build configuration. Defaults to Release. - -.PARAMETER SkipTests - Skip the analyzer xUnit test run. Default: tests run. - -.PARAMETER SkipPayloadRefresh - Don't copy the freshly built artifacts into the - plugins/winui/agent-plugin/skills/.../ payload folder. Default: the payload - is refreshed (this is what keeps CI provenance happy). - -.EXAMPLE - ./scripts/build-tools.ps1 - # Build + test everything in Release and refresh the analyzer payload. - -.EXAMPLE - ./scripts/build-tools.ps1 -SkipTests -SkipPayloadRefresh - # Quick build only β€” skip tests and payload copy. Useful while iterating. -#> - -[CmdletBinding()] -param( - [string]$Configuration = 'Release', - [switch]$SkipTests, - [switch]$SkipPayloadRefresh -) - -$ErrorActionPreference = 'Stop' -$repoRoot = Split-Path -Parent $PSScriptRoot - -function Step([string]$msg) { - Write-Host "" - Write-Host "==> $msg" -ForegroundColor Cyan -} - -function Ok([string]$msg) { Write-Host " [OK] $msg" -ForegroundColor Green } -function Warn([string]$msg) { Write-Host " [!] $msg" -ForegroundColor Yellow } - -# -------------------- Analyzer (build + tests + payload refresh) ------------ - -$analyzerDir = Join-Path $repoRoot 'src/tools/winui-analyzer' -$analyzerSlnx = Join-Path $analyzerDir 'Microsoft.WindowsAppSDK.Analyzers.slnx' -$analyzerTests = Join-Path $analyzerDir 'Microsoft.WindowsAppSDK.Analyzers.Tests/Microsoft.WindowsAppSDK.Analyzers.Tests.csproj' - -Step "Building analyzer ($Configuration)" -dotnet build $analyzerSlnx -c $Configuration --nologo -if ($LASTEXITCODE -ne 0) { throw "analyzer build failed" } -Ok "analyzer built" - -if (-not $SkipTests) { - Step "Running analyzer tests" - dotnet test $analyzerTests -c $Configuration --no-build --nologo --logger 'console;verbosity=normal' - if ($LASTEXITCODE -ne 0) { throw "analyzer tests failed" } - Ok "analyzer tests passed" -} else { - Warn "skipping analyzer tests (-SkipTests)" -} - -if (-not $SkipPayloadRefresh) { - Step "Refreshing analyzer skill payload" - $payload = Join-Path $repoRoot 'plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer' - $builtDll = Join-Path $analyzerDir "Microsoft.WindowsAppSDK.Analyzers/bin/$Configuration/netstandard2.0/Microsoft.WindowsAppSDK.Analyzers.dll" - $srcTargets = Join-Path $analyzerDir 'Microsoft.WindowsAppSDK.Analyzers/Microsoft.WindowsAppSDK.Analyzers.targets' - Copy-Item $builtDll (Join-Path $payload 'Microsoft.WindowsAppSDK.Analyzers.dll') -Force - Copy-Item $srcTargets (Join-Path $payload 'Microsoft.WindowsAppSDK.Analyzers.targets') -Force - Ok "payload refreshed: $payload" -} else { - Warn "skipping payload refresh (-SkipPayloadRefresh)" -} - -# -------------------- Done -------------------------------------------------- - -Step "All tools built successfully" -Write-Host " Analyzer payload: plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/" -ForegroundColor DarkGray diff --git a/scripts/tests/Test-SessionBuildClassification.ps1 b/scripts/tests/Test-SessionBuildClassification.ps1 new file mode 100644 index 00000000..bd0daa19 --- /dev/null +++ b/scripts/tests/Test-SessionBuildClassification.ps1 @@ -0,0 +1,150 @@ +$ErrorActionPreference = 'Stop' +$analyzer = Join-Path $PSScriptRoot '..\..\plugins\winui\agent-plugin\skills\winui-session-report\Analyze-Session.ps1' +$tokens = $null; $parseErrors = $null +$ast = [System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path $analyzer), [ref]$tokens, [ref]$parseErrors) +if ($parseErrors.Count) { throw ($parseErrors.Message -join "`n") } + +# Import only pure classification helpers; never open a user's session logs. +foreach ($name in 'Test-NoBuildEnabled', 'Test-BuildCapableCommand', 'Get-TurnCategory') { + $function = $ast.Find({ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name + }.GetNewClosure(), $true) + if (-not $function) { throw "Missing helper: $name" } + . ([scriptblock]::Create($function.Extent.Text)) +} + +$cases = @( + @{ Command = '.\BuildAndRun.ps1'; Build = $true } + @{ Command = '.\BuildAndRun.ps1 --no-build'; Build = $false } + @{ Command = 'dotnet build .\App.csproj'; Build = $true } + @{ Command = 'dotnet publish .\App.csproj -p:PublishAot=true'; Build = $true } + @{ Command = 'dotnet publish .\App.csproj --no-build'; Build = $false } + @{ Command = 'dotnet publish .\App.csproj --no-build=false'; Build = $true } + @{ Command = 'msbuild .\App.csproj'; Build = $true } + @{ Command = 'winapp run . --on sandbox --detach --json'; Build = $true } + @{ Command = 'winapp run .\App.csproj --aot --on sandbox'; Build = $true } + @{ Command = 'winapp run ".\My App\App.csproj" --aot'; Build = $true } + @{ Command = 'winapp run .\App.slnx --project App'; Build = $true } + @{ Command = 'winapp run .\App.cs'; Build = $true } + @{ Command = 'winapp run --detach'; Build = $true } + @{ Command = 'winapp run . --no-build'; Build = $false } + @{ Command = 'winapp run . --no-build=true'; Build = $false } + @{ Command = 'winapp run . --no-build true'; Build = $false } + @{ Command = 'winapp run . --no-build=false'; Build = $true } + @{ Command = 'winapp run . --no-build false'; Build = $true } + @{ Command = 'winapp run . -- --no-build'; Build = $true } + @{ Command = 'winapp run . --help'; Build = $false } + @{ Command = 'winapp run .\publish'; Build = $false } + @{ Command = 'winapp run ".\publish output"'; Build = $false } + @{ Command = 'winapp run --arch x64 .\dist'; Build = $false } + @{ Command = 'winapp run --arch=x64 ".\dist output"'; Build = $false } + @{ Command = 'winapp run --on sandbox --detach .\dist'; Build = $false } + @{ Command = 'winapp run --detach false --arch x64 .\dist'; Build = $false } + @{ Command = 'winapp run --aot true --arch x64 .\App.csproj'; Build = $true } + @{ Command = 'winapp run --no-build false --arch x64 .\App.csproj'; Build = $true } + @{ Command = 'winapp run --arch x64'; Build = $true } + @{ Command = 'winapp run --symbols --debug-output .\App.csproj'; Build = $true } + @{ Command = 'winapp run --arch'; Build = $false } + @{ Command = 'winapp run --manifest .\misleading.csproj .\dist'; Build = $false } + @{ Command = 'winapp run --property .\misleading.csproj .\dist'; Build = $false } + @{ Command = 'winapp package .\App.csproj -c Release'; Build = $true } + @{ Command = 'winapp package --arch x64 .\App.csproj'; Build = $true } + @{ Command = 'winapp package --self-contained .\App.csproj --no-sign'; Build = $true } + @{ Command = 'winapp package --arch=x64 ".\My App\App.csproj"'; Build = $true } + @{ Command = 'winapp package -c Release --arch x64 -p "Setting=My App.csproj" .\App.csproj'; Build = $true } + @{ Command = 'winapp package --no-sign .\App.csproj'; Build = $true } + @{ Command = 'winapp package --no-sign true .\App.csproj'; Build = $true } + @{ Command = 'winapp package --no-build false --arch x64 .\App.csproj'; Build = $true } + @{ Command = 'winapp package --no-build true --arch x64 .\App.csproj'; Build = $false } + @{ Command = 'winapp package --manifest .\misleading.csproj .\publish'; Build = $false } + @{ Command = 'winapp package --manifest .\misleading.csproj'; Build = $false } + @{ Command = 'winapp package --property .\misleading.csproj .\publish'; Build = $false } + @{ Command = 'winapp package --property=Setting=App.csproj .\publish'; Build = $false } + @{ Command = 'winapp package --output ".\My App.csproj" .\publish'; Build = $false } + @{ Command = 'winapp package --unknown .\App.csproj'; Build = $false } + @{ Command = 'winapp pack ".\My App\App.csproj"'; Build = $true } + @{ Command = 'winapp package .\App.csproj --no-build'; Build = $false } + @{ Command = 'winapp package .\App.csproj --no-build false'; Build = $true } + @{ Command = 'winapp package .\App.csproj --help'; Build = $false } + @{ Command = 'winapp package .\publish'; Build = $false } + @{ Command = 'winapp package .'; Build = $false } + @{ Command = 'winapp package .\publish\x64 .\publish\arm64'; Build = $false } + @{ Command = 'winapp package .\AppxManifest.xml'; Build = $false } + @{ Command = 'winapp package .\publish --output .\misleading.csproj'; Build = $false } + @{ Command = 'winapp package --help'; Build = $false } + @{ Command = 'winapp ui inspect --on sandbox -a 123 --json'; Build = $false } + @{ Command = 'dotnet publish .\App.csproj; winapp run . --no-build'; Build = $true } + @{ Command = 'winapp run . --no-build; dotnet publish .\App.csproj'; Build = $true } + @{ Command = 'winapp run . --no-build && winapp package .\App.csproj'; Build = $true } +) +$failures = @() +foreach ($case in $cases) { + $actual = Test-BuildCapableCommand $case.Command + if ($actual -ne $case.Build) { + $failures += "Expected build=$($case.Build), got ${actual}: $($case.Command)" + } + $turn = [pscustomobject]@{ + Tools = @([pscustomobject]@{ + Name = 'powershell'; Args = @{ command = $case.Command }; HasError = $false + }) + SkillInvocations = @() + } + $category = Get-TurnCategory $turn + if (($category -eq 'build-ok') -ne $case.Build) { + $failures += "Unexpected category ${category}: $($case.Command)" + } + $turn.Tools[0].HasError = $true + $category = Get-TurnCategory $turn + if (($category -eq 'build-fix') -ne $case.Build) { + $failures += "Unexpected failure category ${category}: $($case.Command)" + } +} +if ($failures.Count) { throw ($failures -join "`n") } + +# Evaluate only the aggregate-analysis statements over synthetic turns. This +# verifies report labels without invoking harness detection or reading logs. +$allTurns = @( + [pscustomobject]@{ + TurnNum = 1 + Tools = @([pscustomobject]@{ + Name = 'powershell' + Args = @{ command = 'dotnet publish .\App.csproj; winapp run . --no-build --on sandbox' } + HasError = $false + ErrorSummary = @() + }) + } + [pscustomobject]@{ + TurnNum = 2 + Tools = @([pscustomobject]@{ + Name = 'powershell' + Args = @{ command = 'winapp package .\publish --no-build' } + HasError = $true + ErrorSummary = @('CLI rejected flags on a folder') + }) + } +) +$start = $ast.EndBlock.Statements | Where-Object { $_.Extent.Text.StartsWith('$buildAttemptTurns =') } | Select-Object -First 1 +$end = $ast.EndBlock.Statements | Where-Object { $_.Extent.Text.StartsWith('$sandboxFailures =') } | Select-Object -First 1 +if (-not $start -or -not $end) { throw 'Missing aggregate analysis boundaries.' } +foreach ($statement in $ast.EndBlock.Statements | Where-Object { + $_.Extent.StartOffset -ge $start.Extent.StartOffset -and $_.Extent.EndOffset -le $end.Extent.EndOffset +}) { + . ([scriptblock]::Create($statement.Extent.Text)) +} +if ($buildWorkflowAttempts -ne 1 -or $buildWorkflowFailures -ne 0 -or $buildErrors.Count -ne 0) { + throw 'Non-build packaging failure polluted build statistics.' +} +if ($projectBuildStatus -notmatch 'dotnet build/publish: 1' -or $projectBuildStatus -match 'winapp project') { + throw "No-build run polluted workflow labels: $projectBuildStatus" +} +if ($sandboxCommands.Count -ne 1 -or $sandboxFailures -ne 0) { throw 'Sandbox usage count incorrect.' } + +$skillPath = Join-Path (Split-Path $analyzer) 'SKILL.md' +foreach ($block in [regex]::Matches((Get-Content -LiteralPath $skillPath -Raw), '(?ms)^```powershell\r?\n(.*?)^```')) { + $null = [System.Management.Automation.Language.Parser]::ParseInput( + $block.Groups[1].Value, [ref]$tokens, [ref]$parseErrors) + if ($parseErrors.Count) { throw ($parseErrors.Message -join "`n") } +} +Write-Host "PASS: $($cases.Count) classification/category cases, synthetic report statistics, and PowerShell parsing." diff --git a/scripts/tests/Test-SetupVersionDetection.ps1 b/scripts/tests/Test-SetupVersionDetection.ps1 new file mode 100644 index 00000000..f9b36836 --- /dev/null +++ b/scripts/tests/Test-SetupVersionDetection.ps1 @@ -0,0 +1,35 @@ +#Requires -Version 7.0 +[CmdletBinding()] +param( + [string]$SkillPath = (Join-Path $PSScriptRoot '..\..\plugins\winui\agent-plugin\skills\winui-setup\SKILL.md') +) + +$ErrorActionPreference = 'Stop' +$text = Get-Content -LiteralPath $SkillPath -Raw +$match = [regex]::Match($text, '(?s)```powershell\r?\n(\$minimumDotNet.*?)```') +if (-not $match.Success) { throw 'Setup prerequisite detection block not found.' } +$check = [scriptblock]::Create($match.Groups[1].Value) + +function dotnet { '10.0.100 [C:\test-sdk]' } +function winapp { $script:versionOutput } +function Get-ItemProperty { @{ AllowDevelopmentWithoutDevLicense = 1 } } + +$cases = @( + @{ Text = '0.6.3-prerelease.39'; Expected = $false } + @{ Text = '0.6.9'; Expected = $false } + @{ Text = '0.7.0'; Expected = $true } + @{ Text = 'v0.7.0+build.123'; Expected = $true } + @{ Text = '0.7.0-preview.1'; Expected = $false } + @{ Text = '0.8.0'; Expected = $true } + @{ Text = 'Update available: 0.7.0'; Expected = $false } + @{ Text = "Update available: 0.8.0`n0.6.3"; Expected = $false } +) + +foreach ($case in $cases) { + $script:versionOutput = $case.Text -split "`n" + . $check + if ($winappOk -ne $case.Expected) { + throw "Unexpected setup result for '$($case.Text)': $winappOk, expected $($case.Expected)." + } +} +Write-Host "Passed $($cases.Count) setup version-detection cases." diff --git a/scripts/tests/Test-WinuiUiTestingSandbox.ps1 b/scripts/tests/Test-WinuiUiTestingSandbox.ps1 new file mode 100644 index 00000000..06ba16f4 --- /dev/null +++ b/scripts/tests/Test-WinuiUiTestingSandbox.ps1 @@ -0,0 +1,259 @@ +$ErrorActionPreference = 'Stop' +$skillPath = Join-Path $PSScriptRoot '..\..\plugins\winui\agent-plugin\skills\winui-ui-testing\SKILL.md' +$content = Get-Content -LiteralPath $skillPath -Raw +$blocks = @([regex]::Matches($content, '(?ms)^```powershell\r?\n(.*?)^```')) +if (-not $blocks.Count) { throw 'No PowerShell examples found.' } +foreach ($block in $blocks) { + $tokens = $null; $parseErrors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseInput( + $block.Groups[1].Value, [ref]$tokens, [ref]$parseErrors) + if ($parseErrors.Count) { throw ($parseErrors.Message -join "`n") } + foreach ($command in $ast.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.CommandAst] -and $node.GetCommandName() -eq 'winapp' + }, $true)) { + $parent = $command.Parent + while ($parent -and $parent -isnot [System.Management.Automation.Language.FunctionDefinitionAst]) { + $parent = $parent.Parent + } + if (-not $parent -or $parent.Name -ne 'Invoke-WinAppChecked') { + throw 'An example bypasses the checked/scoped command helper.' + } + } +} +$batch = @($blocks | Where-Object { $_.Groups[1].Value -match '# ui-tests\.ps1' }) +if ($batch.Count -ne 1) { throw 'Expected exactly one ui-tests.ps1 template.' } +$source = $batch[0].Groups[1].Value +if ($source -notmatch "\[Parameter\(Mandatory\)\]\[ValidateSet\('sandbox', 'local'\)\]\[string\]\`$Target" -or + $source -match "\`$Target = 'sandbox'") { + throw 'Batch testing must use the target selected before execution, not impose a Sandbox default.' +} +if ($content -match '2>\$null|BuildAndRun\.ps1|\$inspection\.elements') { + throw 'Unsafe error suppression, legacy rebuild instruction, or obsolete inspection schema.' +} +if ($source -notmatch 'UiTargetArgs' -or $source -notmatch 'ProcessScope' -or + $source -notmatch '\$inspection\.windows' -or $source -notmatch 'WINAPP_UI_WORKFLOW_ID') { + throw 'Missing scoped target, workflow, or windows/elements contract.' +} + +# Verify actual native exit scoping too: a local $LASTEXITCODE sentinel would +# mask the global value set by a native process, something a mock alone misses. +$tokens = $null; $parseErrors = $null +$batchAst = [System.Management.Automation.Language.Parser]::ParseInput($source, [ref]$tokens, [ref]$parseErrors) +$checkedHelper = $batchAst.Find({ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Invoke-WinAppChecked' +}, $true) +. ([scriptblock]::Create($checkedHelper.Extent.Text)) +Set-Alias -Name winapp -Value (Join-Path $PSHOME 'pwsh.exe') -Scope Local +try { + $global:LASTEXITCODE = 99 + Invoke-WinAppChecked @('-NoProfile', '-Command', 'exit 0') + $threw = $false + try { Invoke-WinAppChecked @('-NoProfile', '-Command', 'exit 7') } catch { $threw = $true } + if (-not $threw) { throw 'Native failure was not enforced by the template helper.' } +} finally { + Remove-Item Alias:\winapp +} +$additions = ($blocks | Where-Object { $_.Groups[1].Value.TrimStart().StartsWith('Test-UI ') } | + ForEach-Object { $_.Groups[1].Value }) -join "`n" +if (-not $additions) { throw 'Expected scoped advanced/picker/persistence examples.' } + +# A child PowerShell process supplies a mock winapp function. No real CLI, app, +# Sandbox, user session, input, or capture is touched by these tests. +$scratch = Join-Path $PSScriptRoot ('.ui-sandbox-regression-' + [guid]::NewGuid().ToString('N')) +$driver = @' +param([string]$Case, [string]$Template) +$ErrorActionPreference = 'Stop' +$global:Calls = [Collections.Generic.List[object]]::new() +$global:PickerOpened = $false +function global:winapp { + $arguments = @($args) + $global:Calls.Add(@{ arguments = $arguments; workflowId = $env:WINAPP_UI_WORKFLOW_ID }) + if ($Case -ne 'stale-exit') { $global:LASTEXITCODE = 0 } + if ($arguments[0] -eq 'run') { + if ($Case -in 'launch-error', 'local-launch-error') { $global:LASTEXITCODE = 7; return 'launch failed' } + if ($Case -eq 'missing-pid') { return '{"Sandbox":true,"ProcessScope":"sandbox","ExecutionTarget":{"selector":"sandbox"}}' } + if ($Case -eq 'wrong-scope') { return '{"ProcessId":321,"ProcessScope":"local","UiTargetArgs":"-a 321"}' } + if ($Case -eq 'local') { return '{"ProcessId":321}' } + return '{"ProcessId":321,"ProcessScope":"sandbox","UiTargetArgs":"--on sandbox -a 321"}' + } + if ($arguments[0] -eq 'target') { + if ($arguments[1] -eq 'snapshot') { return '{"workRoot":"C:\\WinApp\\work"}' } + if ($arguments[1] -eq 'exec' -and '-Command' -in $arguments) { return '{"UserName":"TestUser"}' } + return + } + if ($arguments[0] -ne 'ui') { throw 'Unexpected mock command.' } + $verb = $arguments[1] + if (($Case -eq 'early-native-failure' -and $verb -eq 'set-value') -or + ($Case -eq 'inspect-error' -and $verb -eq 'inspect') -or + ($Case -eq 'screenshot-error' -and $verb -eq 'screenshot') -or + ($Case -eq 'yield-error' -and $verb -eq 'yield')) { + $global:LASTEXITCODE = 7 + return 'mock native failure' + } + switch ($verb) { + 'list-windows' { + if ($Case -eq 'empty-window') { return '[]' } + if ($Case -eq 'malformed-window') { return 'not JSON' } + if ($global:PickerOpened -and '-a' -notin $arguments) { + return '[{"hwnd":"123","title":"App","processId":321},{"hwnd":"789","title":"Open","processId":999,"ownerHwnd":999},{"hwnd":"456","title":"Open","processId":654,"ownerHwnd":123}]' + } + return '[{"hwnd":"123","title":"App"},{"hwnd":"124","title":"Secondary"}]' + } + 'inspect' { + if ($Case -eq 'empty-inspection') { return '{"windows":[]}' } + if ($Case -eq 'obsolete-inspection') { return '{"elements":[{"type":"Button","automationId":"BtnSave"}]}' } + if ($Case -eq 'no-app-controls') { return '{"windows":[{"elements":[{"type":"Window"}]}]}' } + if ($Case -eq 'missing-id') { return '{"windows":[{"elements":[{"type":"Button","name":"Save"}]}]}' } + if ($Case -eq 'nested-missing-id') { return '{"windows":[{"elements":[{"type":"TabItem","automationId":"NavHome","children":[{"type":"Button","name":"Save"}]}]}]}' } + if ($Case -eq 'nested-valid-id') { return '{"windows":[{"elements":[{"type":"Window","children":[{"type":"Button","name":"Save","automationId":"BtnSave"}]}]}]}' } + return '{"windows":[{"elements":[{"type":"Button","name":"Save","automationId":"BtnSave","className":"Button"}]}]}' + } + 'invoke' { if ($arguments[2] -eq 'BtnOpenFile') { $global:PickerOpened = $true } } + 'get-value' { return '{"text":"TestUser"}' } + { $_ -in 'screenshot', 'record' } { + if ($Case -eq 'undelivered-capture') { return } + $path = $arguments[[array]::IndexOf($arguments, '--output') + 1] + if ($verb -eq 'screenshot' -and '-w' -notin $arguments) { + $path = Join-Path (Split-Path $path) ([IO.Path]::GetFileNameWithoutExtension($path) + '-123.png') + } + [IO.File]::WriteAllBytes($path, [byte[]]@(1, 2, 3, 4)) + } + } +} +$env:WINAPP_UI_WORKFLOW_ID = 'outer-workflow' +$parameters = @{ WorkflowId = 'regression-flow'; Target = 'sandbox' } +if ($Case -in 'local', 'local-reuse', 'local-launch-error') { $parameters.Target = 'local' } +if ($Case -eq 'local-reuse') { $parameters.AppPid = 321; $parameters.AppScope = 'local' } +if ($Case -eq 'reuse') { $parameters.AppPid = 321; $parameters.AppScope = 'sandbox' } +if ($Case -eq 'reuse-mismatch') { $parameters.AppPid = 321; $parameters.AppScope = 'local' } +& $Template @parameters +$code = $LASTEXITCODE +@{ calls = @($global:Calls.ToArray()); restoredWorkflowId = $env:WINAPP_UI_WORKFLOW_ID } | + ConvertTo-Json -Depth 8 | Set-Content -LiteralPath '.\calls.json' +exit $code +'@ +$cases = @( + @{ Name = 'pass'; Exit = 0 } + @{ Name = 'local'; Exit = 0 } + @{ Name = 'local-reuse'; Exit = 0 } + @{ Name = 'local-launch-error'; Exit = 1 } + @{ Name = 'reuse'; Exit = 0 } + @{ Name = 'extensions'; Exit = 0 } + @{ Name = 'early-native-failure'; Exit = 1 } + @{ Name = 'empty-window'; Exit = 1 } + @{ Name = 'malformed-window'; Exit = 1 } + @{ Name = 'empty-inspection'; Exit = 1 } + @{ Name = 'obsolete-inspection'; Exit = 1 } + @{ Name = 'no-app-controls'; Exit = 1 } + @{ Name = 'missing-id'; Exit = 1 } + @{ Name = 'nested-missing-id'; Exit = 1 } + @{ Name = 'nested-valid-id'; Exit = 0 } + @{ Name = 'inspect-error'; Exit = 1 } + @{ Name = 'screenshot-error'; Exit = 1 } + @{ Name = 'undelivered-capture'; Exit = 1 } + @{ Name = 'yield-error'; Exit = 1 } + @{ Name = 'wrong-scope'; Exit = 1 } + @{ Name = 'missing-pid'; Exit = 1 } + @{ Name = 'launch-error'; Exit = 1 } + @{ Name = 'reuse-mismatch'; Exit = 1 } + @{ Name = 'stale-exit'; Exit = 1 } +) +try { + New-Item -ItemType Directory -Path $scratch | Out-Null + $driverPath = Join-Path $scratch 'mock-driver.ps1' + $driver | Set-Content -LiteralPath $driverPath + $templatePath = Join-Path $scratch 'ui-tests.ps1' + $source | Set-Content -LiteralPath $templatePath + $extendedPath = Join-Path $scratch 'extended-ui-tests.ps1' + $source.Replace(" Test-UI 'Final screenshot delivered'", "$additions`n Test-UI 'Final screenshot delivered'") | + Set-Content -LiteralPath $extendedPath + foreach ($case in $cases) { + $directory = Join-Path $scratch $case.Name + New-Item -ItemType Directory -Path $directory | Out-Null + Push-Location $directory + try { + $template = if ($case.Name -eq 'extensions') { $extendedPath } else { $templatePath } + $output = & (Join-Path $PSHOME 'pwsh.exe') -NoProfile -File $driverPath -Case $case.Name -Template $template 2>&1 + if ($LASTEXITCODE -ne $case.Exit) { throw "$($case.Name): unexpected exit $LASTEXITCODE. $output" } + $report = Get-Content -LiteralPath '.\test-results.json' -Raw | ConvertFrom-Json + $log = Get-Content -LiteralPath '.\calls.json' -Raw | ConvertFrom-Json + if (($report.failed -gt 0) -ne ($case.Exit -ne 0)) { throw "$($case.Name): false PASS/FAIL report." } + if ($case.Name -eq 'missing-pid' -and + -not @($report.results | Where-Object { $_.detail -match 'discover the app in that target' }).Count) { + throw 'Scope-only unpackaged guest launch must explain how to supply the app PID.' + } + if ($log.restoredWorkflowId -ne 'outer-workflow') { throw "$($case.Name): workflow ID leaked." } + foreach ($call in $log.calls) { + $arguments = @($call.arguments) + if ($call.workflowId -ne 'regression-flow') { throw 'Cooperating command lost workflow identity.' } + if ($arguments[0] -notin 'ui', 'run') { continue } + if ($case.Name -in 'local', 'local-reuse', 'local-launch-error') { + if ('--on' -in $arguments) { throw 'Selected local mode unexpectedly routed remotely.' } + } else { + $onIndex = [array]::IndexOf($arguments, '--on') + if ($onIndex -lt 0 -or $arguments[$onIndex + 1] -ne 'sandbox') { + throw "Unscoped guest command: $($arguments -join ' ')" + } + } + if ($arguments[0] -eq 'ui' -and $arguments[1] -ne 'yield') { + if ($arguments[1] -eq 'screenshot' -and + ('-w' -notin $arguments -or $arguments[[array]::IndexOf($arguments, '-w') + 1] -ne '123')) { + throw 'Screenshot must select the known main window for an exact output path.' + } + if ($case.Name -eq 'extensions' -and $arguments[1] -eq 'list-windows' -and '-a' -notin $arguments) { + if ('-w' -in $arguments) { throw 'Picker discovery must enumerate the target, not one window.' } + } elseif ('-w' -in $arguments) { + if ('-a' -in $arguments) { throw 'Window calls must not carry an app selector too.' } + } elseif ('-a' -notin $arguments -or $arguments[[array]::IndexOf($arguments, '-a') + 1] -ne '321') { + throw 'UI command lost app PID.' + } + } + } + if ($case.Name -eq 'early-native-failure' -and + @($log.calls | Where-Object { $_.arguments[1] -eq 'invoke' -and $_.arguments[2] -eq 'BtnSave' }).Count) { + throw 'A multi-command test continued after its first native failure.' + } + if ($case.Name -in 'reuse', 'local-reuse' -and @($log.calls | Where-Object { $_.arguments[0] -eq 'run' }).Count) { + throw 'A valid scoped PID was unnecessarily relaunched.' + } + if ($case.Name -eq 'reuse-mismatch' -and @($log.calls).Count) { throw 'Mismatched PID reached the CLI.' } + if ($case.Exit -eq 0) { + if (@($report.screenshots).Count -lt 2) { throw 'Evidence paths were not reported.' } + foreach ($image in $report.screenshots) { + if (-not (Test-Path -LiteralPath $image -PathType Leaf)) { throw 'Evidence not on host.' } + } + } + if ($case.Name -eq 'extensions') { + foreach ($verb in 'send-keys', 'hover', 'drag', 'touch', 'pen', 'record') { + if (-not @($log.calls | Where-Object { $_.arguments[1] -eq $verb }).Count) { + throw "Advanced example did not exercise $verb." + } + } + if (-not @($log.calls | Where-Object { '-w' -in $_.arguments -and '456' -in $_.arguments }).Count) { + throw 'Picker HWND was not exercised.' + } + if (-not @($log.calls | Where-Object { $_.arguments[1] -eq 'list-windows' -and '-a' -notin $_.arguments }).Count) { + throw 'Picker discovery must enumerate windows beyond the app PID.' + } + if (@($log.calls | Where-Object { '-w' -in $_.arguments -and '789' -in $_.arguments }).Count) { + throw 'A picker not owned by the app was selected.' + } + $exec = @($log.calls | Where-Object { $_.arguments[0] -eq 'target' -and $_.arguments[1] -eq 'exec' }) + if ($exec.Count -ne 2) { throw 'Guest persistence/setup did not execute in target.' } + foreach ($transfer in @($log.calls | Where-Object { $_.arguments[1] -in 'push', 'pull' })) { + $guestPath = if ($transfer.arguments[1] -eq 'push') { $transfer.arguments[4] } else { $transfer.arguments[3] } + if ([IO.Path]::IsPathRooted($guestPath)) { throw 'Guest transfer path must be workRoot-relative.' } + } + } + } finally { + Pop-Location + } + } +} finally { + if (Test-Path -LiteralPath $scratch) { Remove-Item -LiteralPath $scratch -Recurse -Force } +} +Write-Host "PASS: $($blocks.Count) PowerShell examples parse; $($cases.Count) mocked UI scenarios, routing, evidence, and failure checks." +# Expected failing child cases leave LASTEXITCODE nonzero; Actions propagates it. +exit 0 diff --git a/src/tools/winui-analyzer/.editorconfig b/src/tools/winui-analyzer/.editorconfig deleted file mode 100644 index adf3f74a..00000000 --- a/src/tools/winui-analyzer/.editorconfig +++ /dev/null @@ -1,21 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 4 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.{json,yml,yaml,md}] -indent_size = 2 - -[*.{csproj,props,targets}] -indent_size = 2 - -[*.cs] -# C# style β€” keep close to dotnet/runtime defaults. -csharp_new_line_before_open_brace = all -csharp_style_namespace_declarations = file_scoped:warning -dotnet_sort_system_directives_first = true diff --git a/src/tools/winui-analyzer/CHANGELOG.md b/src/tools/winui-analyzer/CHANGELOG.md deleted file mode 100644 index 42d6cd0d..00000000 --- a/src/tools/winui-analyzer/CHANGELOG.md +++ /dev/null @@ -1,58 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Added -- **`WUI1001` / `WUI1002` β€” Data-driven UWPβ†’WinAppSDK API mapping rules** - sourced from the [Microsoft Learn API mapping table](https://learn.microsoft.com/windows/apps/windows-app-sdk/migrate-to-windows-app-sdk/api-mapping-table). - ~30 mappings shipped; adding more is a data PR (one row in `ApiMappings.g.cs` + one test). -- **`WUI1010` β€” Migration feature-area hints (Info)** sourced from the - [feature mapping table](https://learn.microsoft.com/windows/apps/windows-app-sdk/migrate-to-windows-app-sdk/feature-mapping-table). -- **`ProjectContext` detector** β€” gates `WUI1xxx` to projects classified as - `MigratingFromUwp` (heuristics: `Package.appxmanifest` AdditionalFile, `Windows.UI.*` - using directives). Greenfield WinUI 3 projects see no migration noise. -- **`Allowlists.cs`** β€” declarative per-rule carve-outs replacing inline string literals. - Now covers `GetForCurrentView`, `Window.Current`, UWP-XAML namespace false friends, - and the WebView2 containing-type guard. -- **`SuppressionTests.cs`** β€” pragma-suppression regression test for every shipping rule - (11 tests). A rule that doesn't honor `#pragma warning disable` will turn this red. -- **Corpus regression suite** β€” [`tools/run-corpus.ps1`](tools/run-corpus.ps1) clones a - curated set of open-source WinUI 3 apps, injects the analyzer, and reports every - diagnostic. Wired to a weekly CI job in `.github/workflows/corpus.yml`. -- **Release pipeline** β€” `.github/workflows/release.yml` builds, packs, optionally signs - (placeholder), publishes to NuGet on a `v*` tag, and creates a GitHub Release. Manual - dry-run available via workflow_dispatch. - -### Changed -- `UwpApiAnalyzer.GetForCurrentView` heuristic now consults `Allowlists` - instead of inline `Contains("ConnectedAnimationService")` β€” same behavior, easier to - extend, regression-tested. - -## [0.1.0-alpha] β€” 2026-04-20 - -### Added -- Initial release as a standalone NuGet package, extracted from the - `microsoft/win-dev-skills` repository. -- Categorized diagnostic ID methodology (`WUI0xxx` compat / `WUI1xxx` migration / - `WUI2xxx` runtime / `WUI3xxx` MVVM / `WUI4xxx` interop). See `RULES.md`. -- 17 diagnostics across the 5 categories. -- xUnit + `Microsoft.CodeAnalysis.CSharp.Analyzer.Testing` test harness with - positive / negative / false-positive-guard tests per rule. -- GitHub Actions CI: build + test + pack on every PR. - -### Changed -- **All `Error`-severity rules downgraded to `Warning`** (or `Info` for - `WUI2020`) to honor the new severity ceiling. Builds will not fail by default. - Users opt into build-breaking enforcement per-rule via `.editorconfig`. -- Diagnostic categories standardized to the `WinUI.` form - (`WinUI.Compatibility`, `WinUI.Runtime`, `WinUI.Mvvm`, `WinUI.Interop`). -- `helpLinkUri` populated for every rule, pointing to the corresponding section - in `RULES.md`. - -### Migration from in-tree `Microsoft.WindowsAppSDK.Analyzers` (legacy IDs `WUI001..WUI021`) -See the migration table in `RULES.md`. Legacy IDs are retired and not reused. diff --git a/src/tools/winui-analyzer/Directory.Build.props b/src/tools/winui-analyzer/Directory.Build.props deleted file mode 100644 index 28996b2e..00000000 --- a/src/tools/winui-analyzer/Directory.Build.props +++ /dev/null @@ -1,30 +0,0 @@ - - - - latest - enable - true - true - latest-recommended - en-US - https://github.com/microsoft/win-dev-skills - git - Microsoft - Microsoft - Microsoft.WindowsAppSDK.Analyzers - Β© Microsoft Corporation. All rights reserved. - MIT - https://github.com/microsoft/win-dev-skills - true - true - true - snupkg - true - true - - diff --git a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/AnalyzerTest.cs b/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/AnalyzerTest.cs deleted file mode 100644 index ceb6b74e..00000000 --- a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/AnalyzerTest.cs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Text; -using Xunit; - -namespace Microsoft.WindowsAppSDK.Analyzers.Tests; - -/// -/// Lightweight analyzer test harness that runs an analyzer against an in-memory -/// compilation and asserts on the produced diagnostic IDs (and optionally severities/counts). -/// -/// We deliberately avoid Microsoft.CodeAnalysis.CSharp.Analyzer.Testing's strict -/// span-matching: our rule set frequently reports at heuristic locations (XAML -/// AdditionalFiles, fuzzy code-behind correlation) where pinning exact spans in tests -/// is more brittle than the rule itself. ID-and-count assertions remain a strong test -/// because every analyzer is exercised end-to-end through the real Roslyn pipeline. -/// -public sealed class AnalyzerTest where TAnalyzer : DiagnosticAnalyzer, new() -{ - private readonly List<(string path, string content)> _sources = new(); - private readonly List<(string path, string content)> _additionalFiles = new(); - private readonly List<(string id, DiagnosticSeverity? severity)> _expected = new(); - private bool _expectClean; - - public AnalyzerTest WithSource(string source, string path = "Test0.cs") - { - _sources.Add((path, source)); - return this; - } - - public AnalyzerTest WithXaml(string path, string content) - { - _additionalFiles.Add((path, content)); - return this; - } - - public AnalyzerTest ExpectDiagnostic(string id, DiagnosticSeverity? severity = null) - { - _expected.Add((id, severity)); - return this; - } - - /// Marker that this test should produce zero analyzer diagnostics. - public AnalyzerTest ExpectClean() - { - _expectClean = true; - return this; - } - - public async Task RunAsync() - { - if (_sources.Count == 0) - { - // Always compile at least an empty unit so the analyzer can register. - _sources.Add(("Empty.cs", "namespace _ { class _Empty {} }")); - } - - var trees = _sources.Select(s => CSharpSyntaxTree.ParseText( - SourceText.From(s.content, Encoding.UTF8), - path: s.path)).ToImmutableArray(); - - var references = GetMetadataReferences(); - - var compilation = CSharpCompilation.Create( - assemblyName: "Microsoft.WindowsAppSDK.Analyzers.Tests.Sample", - syntaxTrees: trees, - references: references, - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - - var additionalTexts = _additionalFiles - .Select(f => (AdditionalText)new InMemoryAdditionalText(f.path, f.content)) - .ToImmutableArray(); - - var analyzer = new TAnalyzer(); - var withAnalyzers = compilation.WithAnalyzers( - ImmutableArray.Create(analyzer), - new AnalyzerOptions(additionalTexts)); - - var diagnostics = await withAnalyzers.GetAnalyzerDiagnosticsAsync(CancellationToken.None); - - // Filter to ours (analyzer-produced only). - var actual = diagnostics - .Where(d => analyzer.SupportedDiagnostics.Any(s => s.Id == d.Id)) - .ToList(); - - if (_expectClean || _expected.Count == 0) - { - if (actual.Count != 0) - { - throw new Xunit.Sdk.XunitException( - $"Expected no analyzer diagnostics but got {actual.Count}:{Environment.NewLine}" + - string.Join(Environment.NewLine, actual.Select(d => " " + d.ToString()))); - } - return; - } - - // Match by ID with multiplicity. Verify expected severity if specified. - var actualIds = actual.Select(d => d.Id).OrderBy(x => x).ToList(); - var expectedIds = _expected.Select(e => e.id).OrderBy(x => x).ToList(); - - Assert.Equal(expectedIds, actualIds); - - foreach (var exp in _expected.Where(e => e.severity.HasValue)) - { - var match = actual.FirstOrDefault(d => d.Id == exp.id); - Assert.NotNull(match); - Assert.Equal(exp.severity!.Value, match.Severity); - } - } - - private static ImmutableArray GetMetadataReferences() - { - // Use the same trusted assemblies the test runtime uses. - var trustedAssemblies = (string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") ?? string.Empty; - return trustedAssemblies - .Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries) - .Where(p => p.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) - .Select(p => (MetadataReference)MetadataReference.CreateFromFile(p)) - .ToImmutableArray(); - } - - private sealed class InMemoryAdditionalText : AdditionalText - { - private readonly SourceText _text; - public InMemoryAdditionalText(string path, string content) - { - Path = path; - _text = SourceText.From(content, Encoding.UTF8); - } - public override string Path { get; } - public override SourceText? GetText(CancellationToken cancellationToken = default) => _text; - } -} diff --git a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Microsoft.WindowsAppSDK.Analyzers.Tests.csproj b/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Microsoft.WindowsAppSDK.Analyzers.Tests.csproj deleted file mode 100644 index 667067a5..00000000 --- a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Microsoft.WindowsAppSDK.Analyzers.Tests.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - net10.0 - Microsoft.WindowsAppSDK.Analyzers.Tests - false - true - - false - - - - - - - - - - - - - - - diff --git a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/ApiMappingAnalyzerTests.cs b/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/ApiMappingAnalyzerTests.cs deleted file mode 100644 index 335c5d0d..00000000 --- a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/ApiMappingAnalyzerTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading.Tasks; -using Microsoft.WindowsAppSDK.Analyzers.Rules; -using Xunit; - -namespace Microsoft.WindowsAppSDK.Analyzers.Tests.Rules; - -/// -/// Tests for the data-driven UWPβ†’WinAppSDK mapping analyzer (WUI1001/WUI1002/WUI1010). -/// The analyzer is gated by ProjectContext: it only fires when the compilation -/// looks like a UWP-migration project. We trigger that by either (a) including -/// using Windows.UI.Xaml; in the source, or (b) adding a Package.appxmanifest -/// AdditionalFile with a UWP-style xmlns:uap. -/// -public sealed class ApiMappingAnalyzerTests -{ - private const string UwpManifest = @" - - -"; - - [Fact] - public async Task Wui1001FlagsCompositionNamespaceUsingInMigratingProject() - { - // Windows.UI.Composition IS a mapping entry β†’ WUI1001 fires; namespace prefix - // also matches a feature mapping β†’ WUI1010 fires alongside. - await new AnalyzerTest() - .WithSource("using Windows.UI.Composition; class C {}") - .WithXaml("Package.appxmanifest", UwpManifest) - .ExpectDiagnostic(DiagnosticIds.ApiMappingMatch) - .ExpectDiagnostic(DiagnosticIds.FeatureMappingHint) - .RunAsync(); - } - - [Fact] - public async Task Wui1002FlagsPrintManagerNoEquivalent() - { - // PrintManager has no WinAppSDK equivalent β€” should produce WUI1002. - // Trigger UWP-context detection via Package.appxmanifest. - await new AnalyzerTest() - .WithSource(@" -namespace Windows.Graphics.Printing { public class PrintManager { public static PrintManager GetForCurrentView() => new(); } } -namespace Sample { class C { void M() { var p = global::Windows.Graphics.Printing.PrintManager.GetForCurrentView(); } } }") - .WithXaml("Package.appxmanifest", UwpManifest) - .ExpectDiagnostic(DiagnosticIds.ApiMappingNoEquiv) - .RunAsync(); - } - - [Fact] - public async Task Wui1xxxDoesNotFireInGreenfieldProject() - { - // No Windows.UI.* using and no UWP manifest β†’ context = greenfield β†’ no diagnostics. - await new AnalyzerTest() - .WithSource(@" -using Microsoft.UI.Xaml; -namespace Microsoft.UI.Xaml { public class Window {} } -namespace Sample { class C {} }") - .ExpectClean() - .RunAsync(); - } - - [Fact] - public async Task Wui1010FeatureHintFiresOnFeatureNamespace() - { - await new AnalyzerTest() - .WithSource("using Windows.ApplicationModel.Background; class C {}") - .WithXaml("Package.appxmanifest", UwpManifest) - .ExpectDiagnostic(DiagnosticIds.FeatureMappingHint) - .RunAsync(); - } -} diff --git a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/AttachedPropertyAnalyzerTests.cs b/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/AttachedPropertyAnalyzerTests.cs deleted file mode 100644 index 78a9f1ef..00000000 --- a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/AttachedPropertyAnalyzerTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading.Tasks; -using Microsoft.WindowsAppSDK.Analyzers.Rules; -using Xunit; - -namespace Microsoft.WindowsAppSDK.Analyzers.Tests.Rules; - -public sealed class AttachedPropertyAnalyzerTests -{ - [Fact] - public async Task Wui2030FlagsNestedAutomationPropertiesInitializer() - { - await new AnalyzerTest() - .WithSource(@" -class Button { public object? AutomationProperties { get; set; } } -class C { void M() { - var b = new Button { AutomationProperties = { AutomationId = ""ok"" } }; -} }") - .ExpectDiagnostic(DiagnosticIds.AttachedPropertyInitializer) - .RunAsync(); - } - - [Fact] - public async Task Wui2030DoesNotFlagSimpleInitializer() - { - await new AnalyzerTest() - .WithSource(@" -class Button { public string? Content { get; set; } } -class C { void M() { var b = new Button { Content = ""hi"" }; } }") - .RunAsync(); - } - - [Fact] - public async Task Wui2030DoesNotFlagUnrelatedNestedInitializer() - { - // FP guard: nested initializer on a non-attached-property type. - await new AnalyzerTest() - .WithSource(@" -class Inner { public int Value { get; set; } } -class Outer { public Inner Child { get; } = new(); } -class C { void M() { var o = new Outer { Child = { Value = 1 } }; } }") - .RunAsync(); - } -} diff --git a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/GenAiApiAnalyzerTests.cs b/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/GenAiApiAnalyzerTests.cs deleted file mode 100644 index ad85aa91..00000000 --- a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/GenAiApiAnalyzerTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading.Tasks; -using Microsoft.WindowsAppSDK.Analyzers.Rules; -using Xunit; - -namespace Microsoft.WindowsAppSDK.Analyzers.Tests.Rules; - -public sealed class GenAiApiAnalyzerTests -{ - [Fact] - public async Task Wui4101FlagsSetInputSequences() - { - await new AnalyzerTest() - .WithSource(@" -class GeneratorParams { public void SetInputSequences(object s) {} } -class C { void M() { var p = new GeneratorParams(); p.SetInputSequences(null!); } }") - .ExpectDiagnostic(DiagnosticIds.GenAiSetInputSequences) - .RunAsync(); - } - - [Fact] - public async Task Wui4102FlagsComputeLogits() - { - await new AnalyzerTest() - .WithSource(@" -class Generator { public void ComputeLogits() {} } -class C { void M() { var g = new Generator(); g.ComputeLogits(); } }") - .ExpectDiagnostic(DiagnosticIds.GenAiComputeLogits) - .RunAsync(); - } - - [Fact] - public async Task Wui4103FlagsTokenizerStreamCtor() - { - await new AnalyzerTest() - .WithSource(@" -class Tokenizer {} -class TokenizerStream { public TokenizerStream(Tokenizer t) {} } -class C { void M() { var t = new Tokenizer(); var s = new TokenizerStream(t); } }") - .ExpectDiagnostic(DiagnosticIds.GenAiTokenizerStreamCtor) - .RunAsync(); - } - - [Fact] - public async Task GenAiDoesNotFlagUnrelatedClean() - { - await new AnalyzerTest() - .WithSource(@" -class C { void M() { var s = ""hello""; } }") - .RunAsync(); - } -} diff --git a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/MvvmPatternAnalyzerTests.cs b/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/MvvmPatternAnalyzerTests.cs deleted file mode 100644 index e1b708bc..00000000 --- a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/MvvmPatternAnalyzerTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading.Tasks; -using Microsoft.WindowsAppSDK.Analyzers.Rules; -using Xunit; - -namespace Microsoft.WindowsAppSDK.Analyzers.Tests.Rules; - -public sealed class MvvmPatternAnalyzerTests -{ - [Fact] - public async Task Wui3001FlagsFieldBackedObservableProperty() - { - await new AnalyzerTest() - .WithSource(@" -using System; -class ObservablePropertyAttribute : Attribute {} -partial class VM { [ObservableProperty] private string _name = """"; }") - .ExpectDiagnostic(DiagnosticIds.OldMvvmSyntax) - .RunAsync(); - } - - [Fact] - public async Task Wui3001DoesNotFlagPlainPrivateField() - { - await new AnalyzerTest() - .WithSource(@" -class VM { private string _name = """"; }") - .RunAsync(); - } - - [Fact] - public async Task Wui3001DoesNotFlagFieldWithUnrelatedAttribute() - { - await new AnalyzerTest() - .WithSource(@" -using System; -class JsonIgnoreAttribute : Attribute {} -class VM { [JsonIgnore] private string _name = """"; }") - .RunAsync(); - } -} diff --git a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/SuppressionTests.cs b/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/SuppressionTests.cs deleted file mode 100644 index 58ae7ed6..00000000 --- a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/SuppressionTests.cs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading.Tasks; -using Microsoft.WindowsAppSDK.Analyzers.Rules; -using Xunit; - -namespace Microsoft.WindowsAppSDK.Analyzers.Tests.Rules; - -/// -/// Suppression regression tests. Every shipping rule must honor -/// #pragma warning disable WUIxxxx. A rule that doesn't honor pragma suppression -/// is unsuppressible and therefore unshippable β€” this file is the gate. -/// -/// Pattern: take the smallest source/XAML that triggers each rule, wrap it in a pragma -/// disable, and assert ExpectClean(). If a future refactor breaks suppression -/// (e.g. by registering a SymbolAnalyzer at compilation-end without honoring filters) -/// these tests will turn red. -/// -public sealed class SuppressionTests -{ - // ─── WUI0001 β€” UWP XAML namespace ──────────────────────────────────────── - [Fact] - public async Task SuppressWui0001() - { - await new AnalyzerTest() - .WithSource(@" -#pragma warning disable WUI0001 -using Windows.UI.Xaml; -#pragma warning restore WUI0001 -namespace Sample { class C {} }") - .RunAsync(); - } - - // ─── WUI0002 β€” Window.Current ──────────────────────────────────────────── - [Fact] - public async Task SuppressWui0002() - { - await new AnalyzerTest() - .WithSource(@" -class Window { public static object? Current; } -class App { void M() { -#pragma warning disable WUI0002 - var w = Window.Current; -#pragma warning restore WUI0002 -} }") - .RunAsync(); - } - - // ─── WUI0004 β€” GetForCurrentView ───────────────────────────────────────── - [Fact] - public async Task SuppressWui0004() - { - await new AnalyzerTest() - .WithSource(@" -class StatusBar { public static StatusBar GetForCurrentView() => new(); } -class App { void M() { -#pragma warning disable WUI0004 - var s = StatusBar.GetForCurrentView(); -#pragma warning restore WUI0004 -} }") - .RunAsync(); - } - - // ─── WUI2001 β€” TabView raw content ─────────────────────────────────────── - [Fact] - public async Task SuppressWui2001() - { - await new AnalyzerTest() - .WithSource(@" -class TextBox {} -class TabViewItem { public object? Content { get; set; } } -class C { void M() { - var tabItem = new TabViewItem(); -#pragma warning disable WUI2001 - tabItem.Content = new TextBox(); -#pragma warning restore WUI2001 -} }") - .RunAsync(); - } - - // ─── WUI3001 β€” Old MVVM syntax ─────────────────────────────────────────── - [Fact] - public async Task SuppressWui3001() - { - await new AnalyzerTest() - .WithSource(@" -using System; -class ObservablePropertyAttribute : Attribute {} -partial class VM { -#pragma warning disable WUI3001 - [ObservableProperty] private string _name = """"; -#pragma warning restore WUI3001 -}") - .RunAsync(); - } - - // ─── WUI4001 β€” WebView2 NavigateToString without init ──────────────────── - [Fact] - public async Task SuppressWui4001() - { - await new AnalyzerTest() - .WithSource(@" -class WebView2 { public void NavigateToString(string s) {} } -class Page { WebView2 webView = new(); void Load() { -#pragma warning disable WUI4001 - webView.NavigateToString(""""); -#pragma warning restore WUI4001 -} }") - .RunAsync(); - } - - // ─── WUI4101 β€” GenAI SetInputSequences ─────────────────────────────────── - [Fact] - public async Task SuppressWui4101() - { - await new AnalyzerTest() - .WithSource(@" -class GeneratorParams { public void SetInputSequences(object s) {} } -class C { void M() { var p = new GeneratorParams(); -#pragma warning disable WUI4101 - p.SetInputSequences(null!); -#pragma warning restore WUI4101 -} }") - .RunAsync(); - } - - // ─── WUI4102 β€” GenAI ComputeLogits ─────────────────────────────────────── - [Fact] - public async Task SuppressWui4102() - { - await new AnalyzerTest() - .WithSource(@" -class Generator { public void ComputeLogits() {} } -class C { void M() { var g = new Generator(); -#pragma warning disable WUI4102 - g.ComputeLogits(); -#pragma warning restore WUI4102 -} }") - .RunAsync(); - } - - // ─── WUI4103 β€” GenAI TokenizerStream ctor ──────────────────────────────── - [Fact] - public async Task SuppressWui4103() - { - await new AnalyzerTest() - .WithSource(@" -class Tokenizer {} -class TokenizerStream { public TokenizerStream(Tokenizer t) {} } -class C { void M() { var t = new Tokenizer(); -#pragma warning disable WUI4103 - var s = new TokenizerStream(t); -#pragma warning restore WUI4103 -} }") - .RunAsync(); - } - - // ─── WUI2030 β€” Attached property nested initializer ────────────────────── - [Fact] - public async Task SuppressWui2030() - { - await new AnalyzerTest() - .WithSource(@" -class Button { public object? AutomationProperties { get; set; } } -class C { void M() { -#pragma warning disable WUI2030 - var b = new Button { AutomationProperties = { AutomationId = ""ok"" } }; -#pragma warning restore WUI2030 -} }") - .RunAsync(); - } - - // ─── WUI1001 β€” API mapping (data-driven) ───────────────────────────────── - [Fact] - public async Task SuppressWui1001() - { - await new AnalyzerTest() - .WithSource(@" -#pragma warning disable WUI1001, WUI1010 -using Windows.UI.Core; -#pragma warning restore WUI1001, WUI1010 -class C {}") - .WithXaml("Package.appxmanifest", @" -") - .RunAsync(); - } -} diff --git a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/TabViewContentAnalyzerTests.cs b/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/TabViewContentAnalyzerTests.cs deleted file mode 100644 index da93edd6..00000000 --- a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/TabViewContentAnalyzerTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading.Tasks; -using Microsoft.WindowsAppSDK.Analyzers.Rules; -using Xunit; - -namespace Microsoft.WindowsAppSDK.Analyzers.Tests.Rules; - -public sealed class TabViewContentAnalyzerTests -{ - [Fact] - public async Task Wui2001FlagsRawTextBoxAsTabContent() - { - // Heuristic fallback path: variable named "tab*" + raw control assignment. - await new AnalyzerTest() - .WithSource(@" -class TextBox {} -class TabViewItem { public object? Content { get; set; } } -class C { void M() { var tabItem = new TabViewItem(); tabItem.Content = new TextBox(); } }") - .ExpectDiagnostic(DiagnosticIds.TabViewRawContent) - .RunAsync(); - } - - [Fact] - public async Task Wui2001DoesNotFlagFrameAsContent() - { - await new AnalyzerTest() - .WithSource(@" -class Frame {} -class TabViewItem { public object? Content { get; set; } } -class C { void M() { var tabItem = new TabViewItem(); tabItem.Content = new Frame(); } }") - .RunAsync(); - } - - [Fact] - public async Task Wui2001DoesNotFlagContentAssignmentOnNonTabType() - { - // False-positive guard: ContentControl.Content assignment on non-tab variable. - await new AnalyzerTest() - .WithSource(@" -class TextBox {} -class ContentControl { public object? Content { get; set; } } -class C { void M() { var panel = new ContentControl(); panel.Content = new TextBox(); } }") - .RunAsync(); - } -} diff --git a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/UwpApiAnalyzerTests.cs b/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/UwpApiAnalyzerTests.cs deleted file mode 100644 index ed543856..00000000 --- a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/UwpApiAnalyzerTests.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.WindowsAppSDK.Analyzers.Rules; -using Xunit; - -namespace Microsoft.WindowsAppSDK.Analyzers.Tests.Rules; - -public sealed class UwpApiAnalyzerTests -{ - // ─── WUI0001 β€” UWP XAML namespace ──────────────────────────────────────── - [Fact] - public async Task Wui0001FlagsUsingWindowsUiXaml() - { - await new AnalyzerTest() - .WithSource(@" -using Windows.UI.Xaml; -namespace Sample { class C {} }") - .ExpectDiagnostic(DiagnosticIds.UwpXamlNamespace) - .RunAsync(); - } - - [Fact] - public async Task Wui0001DoesNotFlagMicrosoftUiXaml() - { - await new AnalyzerTest() - .WithSource(@" -namespace Microsoft.UI.Xaml { class Window {} } -namespace Sample { using Microsoft.UI.Xaml; class C {} }") - .RunAsync(); - } - - [Fact] - public async Task Wui0001DoesNotFlagSimilarlyNamedUserNamespace() - { - // False-positive guard: a user namespace called "Windows.UI.XamlSomething" should not match - // (we use StartsWith("Windows.UI.Xaml") which would actually match this β€” guard test - // intentionally uses a clearly different prefix to confirm the simple cases are clean). - await new AnalyzerTest() - .WithSource(@" -namespace Contoso.Windows.UI.Xaml { class C {} } -namespace Sample { using Contoso.Windows.UI.Xaml; class D {} }") - .RunAsync(); - } - - // ─── WUI0002 β€” Window.Current ──────────────────────────────────────────── - [Fact] - public async Task Wui0002FlagsWindowCurrent() - { - await new AnalyzerTest() - .WithSource(@" -class Window { public static object? Current; } -class App { void M() { var w = Window.Current; } }") - .ExpectDiagnostic(DiagnosticIds.WindowCurrent) - .RunAsync(); - } - - // ─── WUI0004 β€” GetForCurrentView ───────────────────────────────────────── - [Fact] - public async Task Wui0004FlagsGetForCurrentView() - { - await new AnalyzerTest() - .WithSource(@" -class StatusBar { public static StatusBar GetForCurrentView() => new(); } -class App { void M() { var s = StatusBar.GetForCurrentView(); } }") - .ExpectDiagnostic(DiagnosticIds.GetForCurrentView) - .RunAsync(); - } - - [Fact] - public async Task Wui0004DoesNotFlagConnectedAnimationServiceAllowlist() - { - // False-positive guard: ConnectedAnimationService.GetForCurrentView() still works in WinUI 3 - await new AnalyzerTest() - .WithSource(@" -class ConnectedAnimationService { public static ConnectedAnimationService GetForCurrentView() => new(); } -class App { void M() { var s = ConnectedAnimationService.GetForCurrentView(); } }") - .RunAsync(); - } - - // ─── Suppression ───────────────────────────────────────────────────────── - [Fact] - public async Task SuppressionPragmaSuppressesWui0001() - { - await new AnalyzerTest() - .WithSource(@" -#pragma warning disable WUI0001 -using Windows.UI.Xaml; -#pragma warning restore WUI0001 -namespace Sample { class C {} }") - .RunAsync(); - } -} diff --git a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/WebView2InitAnalyzerTests.cs b/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/WebView2InitAnalyzerTests.cs deleted file mode 100644 index 48ae8900..00000000 --- a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/WebView2InitAnalyzerTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading.Tasks; -using Microsoft.WindowsAppSDK.Analyzers.Rules; -using Xunit; - -namespace Microsoft.WindowsAppSDK.Analyzers.Tests.Rules; - -public sealed class WebView2InitAnalyzerTests -{ - [Fact] - public async Task Wui4001FlagsNavigateToStringWithoutInit() - { - await new AnalyzerTest() - .WithSource(@" -class WebView2 { public void NavigateToString(string s) {} } -class Page { WebView2 webView = new(); void Load() { webView.NavigateToString(""""); } }") - .ExpectDiagnostic(DiagnosticIds.WebView2NoInit) - .RunAsync(); - } - - [Fact] - public async Task Wui4001DoesNotFlagWhenEnsureCoreWebView2AsyncPresent() - { - await new AnalyzerTest() - .WithSource(@" -using System.Threading.Tasks; -class WebView2 { public Task EnsureCoreWebView2Async() => Task.CompletedTask; public void NavigateToString(string s) {} } -class Page { WebView2 webView = new(); - async Task Load() { await webView.EnsureCoreWebView2Async(); webView.NavigateToString(""""); } }") - .RunAsync(); - } - - [Fact] - public async Task Wui4001DoesNotFlagUnrelatedNavigateOnNonWebViewType() - { - // FP guard: an unrelated class with a Navigate() method should not flag. - await new AnalyzerTest() - .WithSource(@" -class Router { public void Navigate(string url) {} } -class Page { Router router = new(); void Go() { router.Navigate(""/""); } }") - .RunAsync(); - } -} diff --git a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/XamlAnalyzerTests.cs b/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/XamlAnalyzerTests.cs deleted file mode 100644 index 68a37c85..00000000 --- a/src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Rules/XamlAnalyzerTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.WindowsAppSDK.Analyzers.Rules; -using Xunit; - -namespace Microsoft.WindowsAppSDK.Analyzers.Tests.Rules; - -public sealed class XamlAnalyzerTests -{ - private const string MinimalCs = "namespace Sample { class C {} }"; - - [Fact] - public async Task Wui2010FlagsNestedXBindWithoutFallback() - { - var xaml = @" - -"; - await new AnalyzerTest() - .WithSource(MinimalCs) - .WithXaml("MainPage.xaml", xaml) - .ExpectDiagnostic(DiagnosticIds.XBindNestedNoFallback) - .ExpectDiagnostic(DiagnosticIds.XBindMissingMode) - .RunAsync(); - } - - [Fact] - public async Task Wui2011FlagsXBindWithoutMode() - { - var xaml = @" - -"; - await new AnalyzerTest() - .WithSource(MinimalCs) - .WithXaml("MainPage.xaml", xaml) - .ExpectDiagnostic(DiagnosticIds.XBindMissingMode) - .RunAsync(); - } - - [Fact] - public async Task Wui2011DoesNotFlagXBindWithMode() - { - var xaml = @" - -"; - await new AnalyzerTest() - .WithSource(MinimalCs) - .WithXaml("MainPage.xaml", xaml) - .RunAsync(); - } - - [Fact] - public async Task Wui2011DoesNotFlagCommandBinding() - { - // FP guard: command bindings are correctly OneTime. - var xaml = @" -