Enhance MauiBot AI summary review output + support net11-targeting PRs - #35677
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35677Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35677" |
Skill Validation Results
❌ Skill Validation Results —
|
|
/review -b feature/enhanced-reviewer -p android |
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 1 findings
See inline comments for details.
Runs the Pester suite on every PR that touches .github/scripts/**, .github/skills/**, eng/scripts/**, or eng/pipelines/ci-copilot.yml. Make this a REQUIRED check via repo settings → branches → branch protection. Defense-in-depth gates: * Explicit required-test-files list (refuses to run if any required .Tests.ps1 has been deleted/renamed — prevents silent green from "nothing ran"). Includes the four tests added by dotnet#35677 (Post-AISummaryComment, Review-PR, Aggregate-UITestArtifacts, Run-DeviceTests, Remove-StaleMauiBotComments) plus this PR's new Write-SafeSubprocessOutput and Review-PR.Phase-Audit suites. * Minimum-passed-count floor (catches BeforeAll discovery errors that flip green tests into Skipped state — Pester reports "Passed: 0, Failed: 0" as success otherwise). * Zero-skipped tolerance (catches leftover -Skip / Inconclusive / NotRun states). * Pinned Pester 5.3+ with -SkipPublisherCheck for the runner image's pre-installed copy. The workflow also runs Verify-AuditNegativeControls.ps1 — an end-to-end harness that injects ~245 bypass patterns into a real Gate-reachable script and asserts the audit catches each. This catches regressions where the extractor works but scope detection silently drops lines. Without this workflow, the .Tests.ps1 files already in the repo (plus the ones added by dotnet#35677) are unrun and provide no verification value.
… fail-closed Gate state, HMAC verdict, Phase-Audit (#35804) > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Stacked on #35677 This PR layers a security-hardening stack on top of @kubaflo's #35677 (`feature/enhanced-reviewer`). When merged here, the combined work flows to `main` via #35677. ## Scope Re-applies and updates the security audit work originally landed in #35324 against the latest #35677 base, plus three new defenses caught by adversarial review rounds. | # | Change | Why | |---|---|---| | F1 | Cross-phase state hardening — dedicated `phase-state/` subdir; CopilotReview re-derives `` instead of restoring a persisted path; fail-closed on missing/garbage Gate verdict | Original review said writable `Split-Path -Parent` of `\` allowed PR test code to forge `gate-result.txt` and to redirect `& pwsh -File` execution under `COPILOT_GITHUB_TOKEN` | | F1.A | HMAC-sign `gate-result.txt` via per-job `openssl rand -hex 32` key passed to CopilotReview/Post through an AzDO secret variable | Adversarial reviewer demonstrated empirically that `chmod -R a-w` is bypassable same-user (`mv state state.bak && mkdir state && echo PASSED > ...`). Key generated AFTER pwsh exits → PR-spawned daemons cannot read it | | F2 | `Out-SafePRSubprocessLine` helper + inline `-replace '(?i)##(vso\[\|\[)', '##~SANITIZED~\'` at every PR-controlled `Write-Host` site + `& pwsh -NoProfile -File` subprocess wraps at every shared runner site | Prevents PR test code from forging `##vso[task.setvariable / task.complete / task.uploadfile]` AzDO logging commands through stdout | | F3 | Marker-comment hiding now requires `Test-IsMauiBotCommentAuthor` | A maintainer who quotes `<!-- AI Summary -->` in their own comment was being silently hidden | | D1 / D1.A | Whitelist `^(NONE\|ALL\|[A-Za-z0-9_,]+)\$` before re-emitting `detectedCategories`; `^[A-Za-z0-9_\-]+\$` for `aiSummaryReviewNodeId` | Closes a future-proofing class where PR-controlled text could inject a second `##vso` directive via setvariable echo | | M1 / M2 | Setup invokes `\/scripts/Review-PR.ps1` (not the worktree); `exit \` on failure; Tasks 2-4 gated on `ne(variables['SetupFailed'], 'true')` so a half-set-up worktree can't keep running | Original review said the worktree-vs-trusted asymmetry was inconsistent and the lack of short-circuit let Gate run on stale state | | Phase-Audit | AST-based static analyzer of the `` call-graph closure: default-deny `gh`-write-verb classifier + YAML token-binding audit, run as a Pester suite | Locks the contract that Gate's GH_TOKEN is read-only; any future write-verb regression fails CI | | Pester CI | `security-scripts-pester.yml` workflow runs all 9 `.Tests.ps1` files on every PR touching `.github/scripts`, `.github/skills`, `eng/scripts`, or `eng/pipelines/ci-copilot.yml`. Required-files allowlist + minPassed floor + zero-skipped tolerance → silent test deletion / Pester skip cannot land green | Pre-existing PS tests had no CI runner; this is the first Pester gate in the repo | The original sanitizer / phase-state work passed an adversarial review with three independent reviewers (opus-4.8 + opus-4.7-xhigh + gpt-5.5); the rebase onto #35677's new Copilot-token-usage commits was re-validated by the same set. `gpt-5.5` caught one pre-existing F2 gap (UI/Device runner `\` re-emit at L1526/1539 was raw `Write-Host`) which is fixed in this branch. ## Validation - **Pester: 233/233 green** (50s runtime) - **Phase-Audit Axis A: 21/21** — no `gh` write-verb regressions on real Gate-reachable scripts - **Phase-Audit Axis B: 3/3** — every YAML task's `GH_TOKEN` binding is the documented PAT alias; `CopilotReview` and the new `AnalyzeCopilotTokenUsage` stage have no GH_TOKEN - **openssl ↔ .NET HMAC cross-check** verified locally (`-mac HMAC -macopt hexkey:` round-trips with `Convert.FromHexString`) - **YAML lint**: `yaml.safe_load` clean ## Note for #35677 owner The PR title and description should be reviewed against the actual diff before merge; this stack only adds defenses, no functional changes to the review pipeline beyond the trust-boundary tightening. cc @kubaflo --------- Co-authored-by: Copilot <Copilot@users.noreply.github.com>
<!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Summary - Moves the actual `/review rerun` command implementation out of #35677 and into this follow-up PR. - Adds deterministic `/review rerun` eligibility checks for new comments or commits and applies `s/agent-ready-for-rerun` when another review is justified. - Adds deterministic rerun context for pre-flight so the next review can focus on activity since the prior AI Summary / rerun checkpoint. - Adds an hourly gh-aw workflow (`rerun-review-scanner.md` + compiled `.lock.yml`) that scans queued PRs, asks AI for a `trigger` / `skip` decision, and uses a custom safe-output job for reactions, queue-label cleanup, and AzDO triggering. - Adds helper scripts for querying queued PRs and processing scanner decisions. ## Validation - `gh aw compile .github/workflows/rerun-review-scanner.md` - Parsed changed PowerShell scripts with `System.Management.Automation.Language.Parser`. - Parsed `.github/workflows/rerun-review-scanner.lock.yml` and `.github/workflows/review-trigger.yml` as YAML. - `Invoke-Pester .github/scripts/Resolve-RerunEligibility.Tests.ps1,.github/scripts/Post-AISummaryComment.Tests.ps1,.github/scripts/Remove-StaleMauiBotComments.Tests.ps1 -CI` - Read-only query helper run: `Query-RerunReadyPRs.ps1 -MaxPRs 1 -OutputPath /tmp/rerun-candidates.json` ## Depends on - #35677 for the AI Summary layout and visible `/review rerun` instruction text. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Tomas Grosup <tomasgrosup@microsoft.com> Co-authored-by: Shane Neuville <shane94@hotmail.com>
Per user feedback, the AI Summary 'Gate: Inconclusive' (8957e5) and 'Platform' (8250df) badges, plus the review-tests 'Failures' badge (8250df), used purple — which is too close to GitHub's merged-PR purple and reads as confusing. Recolor: - Gate Inconclusive -> teal (0e7490) - Platform -> blue (1f6feb) - Failures -> amber (bf8700) No other semantics change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cted categories' When the deep UI test stage produces no results (most often because the PR build failed or the stage was skipped), the UI Tests section was posted as just '**Detected UI test categories:** X' with nothing else — confusing readers into thinking tests silently didn't run (e.g. #33007, which has committed merge-conflict markers in PublicAPI.Unshipped.txt that block the build). Add Add-MissingUITestResultsNote: when the uitests content is only the detected- categories placeholder (no DEEP_UITESTS block / 'Deep UI tests' / 'N passed' results), append a [!WARNING] note explaining the run produced no results — likely a failed build (see Gate) or a skipped stage — and to fix it and '/review rerun'. No-op for already-populated results or the no-categories/full-matrix placeholders. Tests: 5 new cases in Post-AISummaryComment.Tests.ps1; full .github/scripts suite 336/336. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
|
|
… N real failures)
When the Android HostApp crashes mid-run (e.g. during a heavy category like
Shell), the test that was running fails in UITestBaseTearDown with 'The app was
expected to be running still, investigate as possible crash', and every
SUBSEQUENT fixture's OneTimeSetUp then times out waiting for 'Go To Test button'
(the app is dead / 'keeps stopping'). The result was a whole category rendered
as e.g. 'Shell 0/312 (312 ❌)' — looking like 312 real test failures when it was
one crash cascading.
The setup-failure classifier required 100% of a category's failures to match the
OneTimeSetUp/Go-To-Test signature; the 1-2 crash-teardown failures didn't match,
so the category fell back to 'N regular failures'. Now:
- The matcher also recognizes the app-crash teardown signature
('investigate as possible crash' / UITestBase.UITestBaseTearDown), so the
whole cascade is recognized as a single setup/infra event.
- A new SetupFailureIsAppCrash flag distinguishes a crash from a pure
navigation/fixture setup flake, and the crash entry is preferred as the
representative sample so the rendered message shows the crash.
- The renderer surfaces crashes CAUTIOUSLY and honestly: '⚠️ the HostApp
crashed mid-run … an app crash can be an infrastructure flake OR a regression
introduced by this PR — review the screenshots + logcat', instead of either
'N ❌' (misleading) or 'infrastructure, not a PR failure' (which could hide a
real regression). Genuine passes in other categories (e.g. Navigation 88/88)
are unaffected and still shown.
Validated against the real PR #36133 deep artifact: Navigation stays 88/88
(SetupFailure=False); Shell -> SetupFailure=True, IsAppCrash=True; ViewBaseTests
-> SetupFailure=True, IsAppCrash=False (secondary cascade). Pester: 10/10.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… category
When the Android HostApp crashes mid-run, the test that was running fails in
UITestBaseTearDown ('investigate as possible crash'), and then EVERY following
fixture's OneTimeSetUp times out on 'Go To Test button' — because a crash can
leave the Android 'App keeps stopping'/ANR system dialog up, which blocks the
relaunched activity. The existing Reset()/relaunch recovery doesn't dismiss that
system dialog or clear corrupt app state, so one crash cascades into the entire
category showing 0 passed (e.g. ScrollView 0/198, Shell 0/312).
This makes the gallery navigation self-heal so a crash costs ~one failure
instead of the whole category (matching normal UI-test behaviour):
- New Appium lifecycle command 'recoverFromCrash' (Android): dismiss the system
crash/ANR dialog (BACK+HOME), 'am force-stop' the app, optionally 'pm clear'
it on a hard recovery to break a corrupt-state startup crash loop, then
relaunch (ActivateApp, with an adb monkey fallback if the driver is wedged).
Non-Android falls back to force-close + relaunch.
- New shared helper App.WaitForGoToTestButtonWithRecovery(): waits for the
gallery as before; on timeout (Android only) it runs a tiered recovery —
soft, then hard (pm clear) — with a short 45s retry per tier, then surfaces
the failure if the app still can't come back (deterministic crash / broken
emulator).
- Both gallery entry points use it: _GalleryUITest -> NavigateToGallery and
_IssuesUITest.NavigateToIssue.
The healthy path is unchanged (first 2-minute wait succeeds and returns); the
recovery only runs after a crash has already broken the run, so it can't make
passing tests fail. Builds clean (Controls.TestCases.Mac.Tests, 0 warnings).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR enhances the MauiBot/Copilot PR review pipeline output and robustness, and adds support for PRs targeting net11.0 (branch-aware base checkout + TFM selection) while improving deep UI test resilience and diagnostics.
Changes:
- Add base-branch auto-detection/validation in
ci-copilot.ymlso workloads + merge base follow the PR’s actual target branch (e.g.,net11.0). - Improve deep UI test reliability and reporting (timeouts around
adb, crash/fixture-setup classification, richer artifacts, time-budget guardrails). - Add telemetry aggregation/publishing for Copilot token usage plus supporting tests and label/outcome parsing hardening.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/TestUtils/src/UITest.Appium/Actions/AppiumLifecycleActions.cs | Adds an Appium command to attempt crash recovery (Android-focused) via adb + relaunch. |
| src/Controls/tests/TestCases.Shared.Tests/UtilExtensions.cs | Uses crash-recovery-aware wait for “Go To Test” to reduce cascading OneTimeSetUp failures. |
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs | Switches issue navigation to the new recovery-aware “Go To Test” wait. |
| eng/scripts/detect-ui-test-categories.ps1 | Skips redundant fork-head checkout when already on prepared review worktree; adds dependency-bump smoke category fallback. |
| eng/pipelines/ci-copilot.yml | Adds base-branch validation/checkout, more robust adb handling, trusted gate verdict propagation, deep UI test budget/diagnostics, and token-usage stage. |
| .github/workflows/copilot-review-tests.md | Tweaks review-trigger text formatting for rerun instructions. |
| .github/workflows/copilot-review-tests.lock.yml | Regenerates gh-aw lock metadata for the workflow doc change. |
| .github/skills/verify-tests-fail-without-fix/scripts/Verify-TestsFail.Tests.ps1 | New Pester tests for build-error classification (ensures “build broke” => INCONCLUSIVE). |
| .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 | Improves env/build error detection, retry recovery, and supports PR-deleted fix files. |
| .github/skills/run-device-tests/scripts/Run-DeviceTests.ps1 | Makes device-test TFMs branch-aware (net10 vs net11). |
| .github/skills/review-test-failures/SKILL.md | Updates the skill template wording/formatting. |
| .github/scripts/shared/Update-AgentLabels.Tests.ps1 | New tests for label derivation from authoritative artifacts (winner.json, gate-result.txt). |
| .github/scripts/shared/Update-AgentLabels.ps1 | Uses gate-result.txt + winner.json as primary sources instead of prose parsing. |
| .github/scripts/shared/shared-utils.ps1 | Adds Get-MauiTfmVersion to derive MAUI TFM version from repo metadata. |
| .github/scripts/shared/Get-AggregatedTrxFromDirectory.ps1 | Adds app-crash cascade detection/flagging for deep UI TRX aggregation. |
| .github/scripts/shared/Aggregate-UITestArtifacts.Tests.ps1 | Adds coverage for new “app crash cascade” classification behavior. |
| .github/scripts/shared/Aggregate-UITestArtifacts.ps1 | Mirrors app-crash cascade detection/flagging in the shared aggregator. |
| .github/scripts/shared/Aggregate-CopilotTokenUsage.ps1 | New script to aggregate Copilot telemetry records into publishable artifacts. |
| .github/scripts/Review-Tests.ps1 | Adjusts badge coloring and consolidates rerun guidance into one line. |
| .github/scripts/Review-PR.Tests.ps1 | Adds tests for Copilot token usage helpers and AzDO log-command defanging. |
| .github/scripts/Post-AISummaryComment.Tests.ps1 | Expands tests for gate-status mapping, approve veto behavior, and UI-results annotations. |
| .github/scripts/post-ai-summary-comment.ps1 | Updates AI summary rendering, gate/status chip behavior, approve veto logic, and UI-results annotation. |
| .github/scripts/DetectUITestCategories.Tests.ps1 | New tests for prepared-review-worktree detection helper. |
| .github/scripts/BuildAndRunHostApp.ps1 | Makes HostApp TFMs branch-aware; adds Android warm-up and Windows app-path resolution. |
| .github/scripts/Aggregate-CopilotTokenUsage.Tests.ps1 | New Pester tests validating token-usage aggregation outputs. |
| if [ -d "$TRUSTED/scripts" ]; then | ||
| rm -rf .github/scripts .github/skills eng/scripts | ||
| cp -r "$TRUSTED/scripts" .github/scripts | ||
| cp -r "$TRUSTED/skills" .github/skills | ||
| cp -r "$TRUSTED/eng-scripts" eng/scripts |
There was a problem hiding this comment.
🔍 AI-generated reply on @kubaflo's behalf.
Valid refinement — flagging as a follow-up rather than changing it blind, since it touches the deep-UI security orchestration on a branch with concurrent work. Context: the copy at :1360-1362 is deliberately security-positive today (it replaces the PR's .github/scripts with the trusted main versions before the per-category loop, per the comment citing rule 3), so PR scripts never execute. Your point stands that the trusted copy then lives in a PR-writable worktree (chmod u+w) and could in principle be mutated by a later untrusted dotnet test within the job. The clean fix is to invoke the deep-UI scripts directly from $TRUSTED (read-only) and drop the cp-back entirely — but that requires rewiring every .github/scripts/… call in the stage (retry wrapper, aggregators, label/token scripts) to $TRUSTED/… while keeping CWD on the worktree src/. I'd rather land that as its own validated change. Leaving open.
| # Dot-source the shared retry wrapper so Stage 2 gets the same | ||
| # env-error detection, device recovery, and retry logic as Stage 1. | ||
| $retryScript = ".github/scripts/shared/Invoke-UITestWithRetry.ps1" | ||
| $hasRetryWrapper = Test-Path $retryScript | ||
|
|
There was a problem hiding this comment.
🔍 AI-generated reply on @kubaflo's behalf.
Same root cause as the trusted-copy thread above — the per-category retry wrapper resolves from .github/scripts/shared/… (worktree) rather than $TRUSTED. Mitigated today by the trusted-over-worktree restore at :1360, but the proper fix is to resolve it (and the other stage scripts) from $TRUSTED and fail closed if absent. Tracking with the companion thread; leaving open for a dedicated, pipeline-validated change rather than a partial edit on a contested branch.
PureWeen
left a comment
There was a problem hiding this comment.
Multi-model adversarial review ⚠️
Methodology: 3 independent reviewers (different frontier models) analyzed this round in parallel, with adversarial cross-validation — single-reviewer findings were sent to the other two models for adjudication before inclusion. This is review round 12 (HEAD 8eca146f).
The security architecture held up under scrutiny again. The findings below are all bounded by the same fact: MauiBot's posted review event (APPROVE/COMMENT/REQUEST_CHANGES) is advisory — human approval is still required and the real merge gate is the maui-pr build. So none of these are merge-blocking risks; they're edge cases where the advisory APPROVE could be slightly too generous.
✅ Verified safe (trust chain held)
Spot-checked and confirmed correct by multiple reviewers:
- Gate trust chain —
Get-AIReviewEventForRunfails closed on an emptyTrustedGateResult(throws), keys the APPROVE veto off the trusted pipeline verdict (not the agent-writablegate-result.txt), and is covered by a test asserting a forgedgate-result.txtcannot flip a trusted PASSED. Solid. - Trusted-script overlay / DeepUITests / fork-head handling — the fork head is never checked into the trusted worktree;
detect-ui-test-categories.ps1skips fork checkout (trusted-gated). Aggregate-CopilotTokenUsage.ps1— null-safe sums, and the path filter requires/copilot-token-usage/raw/while excluding the PR/agent-writableCustomAgentLogsTmp+agent-pr-sessiondirs.AppiumLifecycleActions.RunAdb/WaitForGoToTestButtonWithRecovery—RunAdbusesArgumentList(no shell injection) with a 15s timeout+Kill; the recovery helper rethrows on genuine failure (does not swallow).- The round-11 💡 (the
tools.dotnet-is-a-proxy note) was addressed — thanks.
⚠️ A non-compiling fix is reported as INCONCLUSIVE instead of FAILED — 2/3 reviewers
verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 (status map L1238, exit routing L1850 / L1893).
$gateInfraError (L1850) lumps both the without-fix (baseline) build error and the with-fix build error into one non-blocking bucket → exit 3 (INCONCLUSIVE). But the script already distinguishes them in prose just above: L1278 $wBuildError renders "🩺 Fix does not compile" vs L1282 $woBuildError renders "Base branch does not compile".
A baseline build failure is legitimately inconclusive (you can't establish the bug exists). But a with-fix-only build failure means the PR's own fix is broken — that's a definitive failure, not infra noise, yet it currently exits 3 (non-blocking) on every branch.
Scenario: PR adds a fix that doesn't compile; baseline builds fine. The verify gate exits 3 (INCONCLUSIVE) rather than 1 (FAILED), so the advisory signal under-reports a genuinely broken fix.
Suggested fix: at L1850, only treat baseline build errors as infra-INCONCLUSIVE. If $wBuildError.Count -gt 0 -and $woBuildError.Count -eq 0 (only the fix fails to build), keep FAILED/exit 1. The two sets are already computed at L1257-1258.
Mitigation: the maui-pr build blocks a non-compiling fix regardless, so this only weakens the advisory verify signal — it doesn't let broken code merge.
⚠️ An all-crash deep-UI run leaves APPROVE intact — 2/3 reviewers (design question)
post-ai-summary-comment.ps1 Test-RunValidationFailed (veto regex L477) vs the deep-UI render in ci-copilot.yml (L1887-1906).
When every deep-UI category crashes the HostApp (regularFailed==0, appCrashCategories>0), the render uses ⚠️ and "the HostApp crashed mid-run, so N tests could not complete" (L1890/L1906). The veto regex only matches ❌ **…tests** or a non-zero N failed count — so the all-crash header matches neither, the veto doesn't fire, and an APPROVE recommendation survives a run where zero deep-UI tests actually completed.
This is partly deliberate: an app crash is genuinely ambiguous (emulator/infra flake vs. PR regression), and you intentionally render it ⚠️ rather than ❌ so a good PR isn't shown as "all failed". The narrow question is whether a positive APPROVE is right when no deep-UI test produced a passing signal — arguably it should land on COMMENT (neutral) rather than APPROVE. Mixed runs (regularFailed>0) already render ❌ and do veto, and the body prominently warns the human either way.
Option: when appCrashCategories>0 -and regularFailed==0, downgrade APPROVE→COMMENT (not REQUEST_CHANGES — the crash may be a flake).
💡 A strictly-better alternative fix doesn't downgrade an explicit APPROVE — 2/3 flagged, but tested as intentional
post-ai-summary-comment.ps1 Get-AIReviewEventForRun L510: the non-PR-winner check (Test-HasNonPRWinner) only upgrades COMMENT→REQUEST_CHANGES, leaving an explicit APPROVE intact.
Two reviewers flagged this as a veto gap. On adjudication it turns out to be deliberate and explicitly unit-tested — Post-AISummaryComment.Tests.ps1 has a test named "does not override an exact approve recommendation" (isPRFix=$false + Final Recommendation: APPROVE → APPROVE). The design treats a non-PR winner as an advisory "we found a stronger alternative" preference, not a validation failure. Flagging only to confirm the intent is what you want; no change needed unless you'd prefer a strictly-better alternative to soften APPROVE→COMMENT.
Investigated and discarded (single-reviewer, refuted on follow-up)
- Labels from agent-writable artifacts (raised as Security): discarded. The
s/agent-*labels are triage/metrics signal only — the real merge-readiness check uses GitHub-nativereviewDecision, which derives from the gate-protected posted review event. Not a trust boundary; at most a cosmetic dashboard inconsistency. NaN/Infinityin token aggregation: discarded as unreachable.[double]::TryParse("NaN"/"Infinity", Float)does succeed and would throw on the later[long]round — but the aggregator only reads the pipeline-writtenraw/token logs (typed integer counts), which are not PR-influenceable and never emit non-finite values. Purely defensive; left as a 💡 for optional hardening (reject non-finite inGet-NumericOrNull).
Non-blocking observation
This branch is currently CONFLICTING / DIRTY against main — there's a real merge conflict to resolve before it can merge (a fresh conflict from the new commits, not a code-quality issue).
Test coverage: strong. The veto/gate logic is well covered by Post-AISummaryComment.Tests.ps1 (forged-gate, INCONCLUSIVE-doesn't-block, deep-UI real-render veto, TRX-wording false-trigger). Gaps map exactly to the findings above: no test for a with-fix-only build error (Finding A) or an all-crash deep-UI run (Finding B).
Prior reviews: round-11 verdict was clean; the round-11 suggestion was addressed this round. No unresolved blocking threads from prior rounds.
3 independent reviewers · adversarial consensus · code-only review (CI status intentionally out of scope).
…adb robustness PureWeen / Copilot round-12 findings: - [PureWeen A] verify-tests-fail.ps1: a with-fix-ONLY build error (baseline compiles, the PR's fix does not) is now FAILED (exit 1), not INCONCLUSIVE (exit 3). Only a baseline build error or an env error stays infra-INCONCLUSIVE. Adds a clear console note for the non-compiling-fix case. - [PureWeen B] post-ai-summary-comment.ps1: add Test-DeepUITestsHadNoSignal and soften APPROVE->COMMENT (not REQUEST_CHANGES) when every deep-UI category hit a setup/crash failure and nothing passed — an APPROVE over a run where zero deep-UI tests completed was too generous. Mixed/regular failures still veto to REQUEST_CHANGES. - [Copilot] AppiumLifecycleActions.RunAdb: drain stdout/stderr asynchronously so a chatty command (adb shell monkey) can't fill the pipe buffer and deadlock WaitForExit. - [Copilot] BuildAndRunHostApp.ps1: replace the non-portable external 'timeout 90 adb wait-for-device' with a .NET process timeout (the timeout binary differs/absent on Windows). Tests: +6 Post-AISummaryComment cases (Finding A/B), 36/36 + verify-tests-fail 5/5; UITest.Appium builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen — addressed the round-12 findings in
Tests: +6 On the merge conflict: still outstanding — I deliberately did not fold a |
PureWeen
left a comment
There was a problem hiding this comment.
Adversarial re-review — round 13 (commit 2317c95)
Thanks for turning the round-12 notes around so quickly, @kubaflo. This commit ("Address round-12 review: build-error classification, all-crash veto, adb robustness") is a direct response to the previous review, so I focused on verifying each of the three fixes is correct and complete — i.e. that it closes the exact scenario originally flagged, not just an adjacent one.
I read the full source (not just the diff), traced the trusted-gate trust chain end-to-end, and empirically tested both regex/branch changes against the real pipeline render strings. Three reviewers reached the same conclusion independently: the adb fix is fully correct; the other two fixes handle a sibling case but each misses the originally-flagged one.
✅ adb robustness — correct and complete (verified)
This one is done right:
RunAdb(AppiumLifecycleActions.cs:374-408) — bothRedirectStandardOutputandRedirectStandardErrorare set (L381-382) withUseShellExecute=false, soBeginOutputReadLine()/BeginErrorReadLine()(L395-396) won't throw, and the no-op handlers drain both pipes beforeWaitForExit(15000)— the deadlock is eliminated.Kill()is try/catch-guarded. ✅BuildAndRunHostApp.ps1wait-for-device (~L283-287) — portableStart-Process -PassThru -NoNewWindow+WaitForExit(90000)+ guardedKill()correctly replaces the non-portabletimeout 90 adb. ✅
No regression here.
⚠️ Finding B (all-crash deep-UI APPROVE veto) — still incomplete: it catches the setup-failure sibling but misses the originally-flagged app-crash render
(2 of 3 reviewers rated this must-fix; all 3 agree the originally-flagged escape is still open.)
The new Test-DeepUITestsHadNoSignal (post-ai-summary-comment.ps1:485-503) keys its positive match on (L501):
'(?im)could not run:\s*OneTimeSetUp/fixture setup failure'
But ci-copilot.yml emits two mutually-exclusive regularFailed==0 "no-signal" headers, and appCrashCategories>0 takes priority (L1900):
- App-crash render —
ci-copilot.yml:1906(the originally-flagged case):… the HostApp crashed mid-run, so N tests could not complete … - Pure setup-failure render —
ci-copilot.yml:1911(the sibling):… could not run: OneTimeSetUp/fixture setup failure …
The regex wording appears only in the L1911 sibling. The L1906 app-crash header (and the per-category app-crash body at L1800, app crashed mid-run; … could not complete) never contain could not run: OneTimeSetUp/fixture setup failure.
Concrete trigger: every deep-UI category crashes the HostApp (appCrashCategories>0, regularFailed==0, totalPassed==0) and the run recommends APPROVE → Test-DeepUITestsHadNoSignal returns $false → the APPROVE→COMMENT soften at L534 does not fire → a positive APPROVE survives a run where zero deep-UI tests completed — the exact escape Finding B set out to close. The doc-comment (L487) claims HostApp-crash coverage, and the new tests in Post-AISummaryComment.Tests.ps1 only exercise the could not run: OneTimeSetUp/fixture setup failure wording, so the gap isn't caught.
Suggested fix — broaden the positive match to the app-crash render, keeping the completed-test guards:
$noSignalHeader = ($uiContent -match '(?im)could not run:\s*OneTimeSetUp/fixture setup failure') -or
($uiContent -match '(?im)the HostApp crashed mid-run, so .*could not complete')
return ($noSignalHeader -and
$uiContent -notmatch '(?im)\b\d+\s+passed\b' -and
$uiContent -notmatch '(?im)\b[1-9]\d*\s+failed\b')The \b\d+\s+passed\b guard still correctly excludes the app-crash-with-passes header (L1908, which emits passNote). Please add a test using the literal L1906 wording so the app-crash path is covered.
(Scope note: this is bounded by APPROVE being advisory — a human still merges — so it's not a hard-gate bypass. But it leaves the originally-identified misleading-APPROVE escape open, so it's worth closing in this same PR.)
⚠️ Finding A (build-error classification) — verdict/veto fix is correct ✅, but the report headline + Gate chip still render INCONCLUSIVE for a non-compiling fix
The important half is done right and I verified it: the exit-code split (verify-tests-fail.ps1:1851-1854) means a with-fix-only build error (baseline compiles) now hits the else branch → exit 1 → Review-PR.ps1 maps to FAILED → ci-copilot.yml:827 makes the process exit code authoritative → GATE_VERDICT=FAILED → the APPROVE veto fires → REQUEST_CHANGES. Definitive build failures now block and veto APPROVE correctly. ✅
What's still inconsistent is the display — and it's the exact contradiction round-12 pointed at. Write-MarkdownReport (L1236-1238) still computes $hasBuildError from both without-fix and with-fix results:
$hasBuildError = ($WithoutFixResultsList | ? { $_.BuildError }) -or ($WithFixResultsList | ? { $_.BuildError })
$status = if ($VerificationPassed) { "PASSED" } elseif ($hasEnvError -or $hasBuildError) { "INCONCLUSIVE" } else { "FAILED" }So a non-compiling fix renders ⚠️ INCONCLUSIVE in the report headline while the gate exits FAILED and the body says "Fix does NOT compile … definitive failure". That report is copied verbatim into gate/content.md, and Get-GateStatus (post-ai-summary-comment.ps1:235) parses the INCONCLUSIVE header → the human-facing Gate status chip (L744) shows Inconclusive. Net: the posted comment frames a definitive, blocking build failure as a non-blocking infra flake — the inconsistency round-12 asked to remove.
Suggested fix — mirror the L1851-1854 split inside Write-MarkdownReport so only a baseline build/env error maps to INCONCLUSIVE:
$baselineBuildError = ($WithoutFixResultsList | Where-Object { $_.BuildError }).Count -gt 0
$status = if ($VerificationPassed) { "PASSED" }
elseif ($hasEnvError -or $baselineBuildError) { "INCONCLUSIVE" }
else { "FAILED" }Summary
| Round-12 fix | Correct? | Complete? |
|---|---|---|
| adb robustness | ✅ | ✅ Complete |
| A — build-error classification | ✅ verdict/veto/blocking now correct (exit-code authoritative) | verify-tests-fail.ps1:1238) + Gate chip still render INCONCLUSIVE for a with-fix-only build error |
| B — all-crash deep-UI veto | ❌ regex misses the app-crash render (ci-copilot.yml:1906) — the originally-flagged all-crash APPROVE escape is still open |
New regressions introduced by this commit: none — the trusted-gate trust chain (FAILED veto + exit-code authority) is intact. Both open items are incomplete fixes of the originally-flagged issues, not new breakage.
One non-blocking housekeeping note: the PR is currently CONFLICTING / DIRTY against main — there's a merge conflict to resolve before this can merge.
🔍 Methodology: three independent reviewers cross-checked each round-12 fix against full source + the real pipeline render strings; empirical regex/branch testing confirmed the app-crash gap. Posted as a non-blocking comment (not an approval/changes-requested) — sign-off is a human call.
PureWeen round-13 verified the round-12 fixes were correct but incomplete: - [A] verify-tests-fail.ps1: the exit-code/veto split was right, but Write-MarkdownReport still computed the report headline from BOTH baseline and with-fix build errors, so a non-compiling fix rendered '⚠️ INCONCLUSIVE' in the headline + Gate chip while the gate exited FAILED. Mirror the split: only a BASELINE build/env error → INCONCLUSIVE. - [B] post-ai-summary-comment.ps1: Test-DeepUITestsHadNoSignal matched only the OneTimeSetUp/fixture setup-failure render, missing the originally-flagged app-crash render (ci-copilot.yml ~1906 'the HostApp crashed mid-run, so N … could not complete'), which takes priority when appCrashCategories>0. Broaden the match to both no-signal headers, keeping the 'N passed'/'N failed' guards (so the with-passes crash header is still excluded). Added a test using the literal app-crash wording. Tests: 43/43 (Post-AISummaryComment + Verify-TestsFail). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen — thanks for the precise round-13 verification; you were right that A and B each closed a sibling case but not the originally-flagged one. Both completed in
Tests: 43/43 ( Merge conflict still pending — noted; will land as a focused merge once the in-flight branch commits settle. |
| else | ||
| REPO=$(git config --get remote.origin.url | sed -E 's#.*github\.com[:/]+([^/]+/[^/.]+)(\.git)?$#\1#') | ||
| [ -z "$REPO" ] && REPO="dotnet/maui" | ||
| BASE_REF=$(curl -fsSL -H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json" \ | ||
| "https://api.github.com/repos/${REPO}/pulls/${PARAM_PR_NUMBER}" \ | ||
| | python3 -c "import sys,json;print(json.load(sys.stdin)['base']['ref'])") | ||
| fi |
| # Prefer gh; fall back to REST (gh isn't pre-installed on macOS agents this early). | ||
| if command -v gh >/dev/null 2>&1; then | ||
| BASE_REF=$(gh pr view "${PARAM_PR_NUMBER}" --json baseRefName -q .baseRefName) | ||
| else | ||
| REPO=$(git config --get remote.origin.url | sed -E 's#.*github\.com[:/]+([^/]+/[^/.]+)(\.git)?$#\1#') | ||
| [ -z "$REPO" ] && REPO="dotnet/maui" | ||
| BASE_REF=$(curl -fsSL -H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json" \ | ||
| "https://api.github.com/repos/${REPO}/pulls/${PARAM_PR_NUMBER}" \ | ||
| | python3 -c "import sys,json;print(json.load(sys.stdin)['base']['ref'])") | ||
| fi |
| # Match coded build errors generally — `error <ABBR><NNNN>` — so the MAUI XAML | ||
| # compiler (MAUIX####, e.g. MAUIX2017 "set multiple times"), MSBuild (MSB####), | ||
| # C# (CS####), .NET SDK (NETSDK####), NuGet (NU####) and Android (XA####) diagnostics | ||
| # are all caught. This matters on branches where an unrelated test fixture fails to | ||
| # compile (e.g. the net11 Controls.Xaml.UnitTests MAUIX2017 baseline break): the whole | ||
| # test assembly won't build, so the PR's own test can't run — that is INCONCLUSIVE, not | ||
| # "the fix does not pass". The negative lookahead on `0 error(s)` avoids false positives | ||
| # on MSBuild summary lines like "Build succeeded. 0 Error(s)". |
PureWeen
left a comment
There was a problem hiding this comment.
Adversarial re-review — round 14 (commit 5369e1d)
Both round-13 items are now fully closed @kubaflo — thanks for the quick turnaround. This commit ("round-13: complete Findings A & B (display headline + app-crash veto)") directly completes the two incomplete-fix findings from the last review, so I focused on verifying each now closes the exact originally-flagged scenario (not just an adjacent one) and introduces no new regression. Three reviewers plus an independent empirical pass all reached the same conclusion: complete, correct, no regressions.
✅ Finding A (build-error headline / Gate chip) — RESOLVED
The headline now keys off a baseline-only build error, mirroring the exit-code split exactly:
$baselineBuildError = @($WithoutFixResultsList | Where-Object { $_.BuildError }).Count -gt 0
$status = if ($VerificationPassed) { "PASSED" } elseif ($hasEnvError -or $baselineBuildError) { "INCONCLUSIVE" } else { "FAILED" }Verified consistent across every layer for a with-fix-only build error (baseline compiles, the PR's fix doesn't):
- headline
$status→ ❌ FAILED (was⚠️ INCONCLUSIVE) —verify-tests-fail.ps1:1241-1243 - matches the exit-code predicate
$gateInfraError = $anyEnvError -or $baselineBuildError→ exit 1 (same.Count -gt 0shape, L1856/L1859) - matches the body classification
🩺 Fix does not compile(L1283) and the console box (L1916) - the human-facing Gate chip now renders
Failed:Get-GateStatus's structured markerGate Result:\s*(?:\S+\s*)?(FAILED|…)(L235) consumes the emoji and returnsFailedbefore the\binconclusive\bword-fallback (L244) is ever reached.
The old $hasBuildError variable is fully removed (zero remaining references). The contradiction round-13 flagged is gone.
✅ Finding B (all-crash deep-UI APPROVE veto) — RESOLVED
Test-DeepUITestsHadNoSignal now matches both no-signal renders:
$noSignalHeader = ($uiContent -match '(?im)could not run:\s*OneTimeSetUp/fixture setup failure') -or
($uiContent -match '(?im)the HostApp crashed mid-run, so .*could not complete')I empirically tested the function against the exact ci-copilot.yml render strings for every variant:
| Deep-UI render | Softened? | Correct? |
|---|---|---|
| L1906 app-crash, no passes (the originally-flagged escape) | yes → COMMENT | ✅ |
| L1906 app-crash with passes | no (kept APPROVE) | ✅ |
| L1908 app-crash + regular failures | no | ✅ |
| L1911 setup-failure, no passes (sibling, already handled) | yes | ✅ |
| L1913 setup-failure with passes/fails | no | ✅ |
| normal pass/fail run | no | ✅ |
L1800/L1803 per-category <details> body |
no false-positive | ✅ |
The originally-flagged all-crash escape is closed: because appCrashCategories>0 forces the crash header (L1900→L1906), matching that header explicitly is exactly right. No false-positive on the <details> body — it says "app crashed mid-run;" / "crashed during this category", neither of which contains the required literal "the HostApp crashed mid-run, so". The (?im) (no s flag) is intentionally safe — . can't cross newlines, so .* stays on the single header line. And the two new tests (Post-AISummaryComment.Tests.ps1:326-336) exercise the app-crash path directly — both the no-passes (TRUE) and with-passes (FALSE) cases.
✅ No new regressions
@(...).Count -gt 0is a strict hardening over the old truthy-object-or(null-safe; now matches the exit-code block's idiom). Logically equivalent for the true/false cases.- Broadening
$noSignalHeaderdoes not risk over-softening — it's only one of three conjuncts; anyN passedor non-zeroN failedanywhere in the report still forces$falseand preserves APPROVE. - No third no-signal render path remains; the all-empty "0 passed, 0 failed" no-tests case is a distinct scenario, correctly out of scope.
Summary
| Round-13 finding | Status |
|---|---|
| A — headline/Gate chip for a non-compiling fix | ✅ Complete & correct |
| B — app-crash APPROVE veto | ✅ Complete & correct |
| New regressions | None |
From a code standpoint this is clean — I have no remaining findings on the review-infrastructure changes. The one non-code item left before merge: the PR is still CONFLICTING / DIRTY against main, so there's a merge conflict to resolve.
🔍 Methodology: three independent reviewers cross-checked each round-13 fix against full source + the real pipeline render strings, with empirical regex/branch testing of all deep-UI render variants. Posted as a non-blocking comment (not an approval/changes-requested) — sign-off is a human call.
# Conflicts: # .github/scripts/Review-Tests.ps1 # .github/workflows/copilot-review-tests.lock.yml
dotnet#35677) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Summary - Posts MauiBot AI Summary output as a pull request review with parsed `APPROVE`, `REQUEST_CHANGES`, or safe `COMMENT` fallback. - Uses the new AI Review Summary layout with segmented status chips, collapsed review sessions, and merged Future Action content. - Keeps PR finalization out of the automated review process; AI Summary updates no longer preserve or merge `SECTION:PR-FINALIZE` blocks. - Adds visible AI Summary guidance telling users to comment `/review rerun` after new comments or commits when they want a fresh review. The command implementation is intentionally split into a follow-up PR. - Hides stale MauiBot AI Summary / try-fix artifacts with GitHub minimization instead of deleting them, while preserving same-run try-fix and AI Summary reviews. - Updates the Copilot pipeline to pass review IDs and patch review bodies after deep UI tests. - Hardens gate setup/retry handling by committing squashed PR changes before verification, resetting the review branch before gate retries, and detecting BlazorWebView unit-test project paths. ## Validation - Parsed changed PowerShell scripts with `System.Management.Automation.Language.Parser`. - Parsed `.github/workflows/review-trigger.yml` as YAML. - `Invoke-Pester .github/scripts/Post-AISummaryComment.Tests.ps1,.github/scripts/Remove-StaleMauiBotComments.Tests.ps1 -CI` - Dry-run AI Summary generation verified the rerun note, segmented chips, and collapsed review session layout. - Verified `Detect-TestsInDiff.ps1` maps `src/BlazorWebView/tests/MauiBlazorWebView.UnitTests/UriExtensions_Tests.cs` to `src/BlazorWebView/tests/MauiBlazorWebView.UnitTests/MauiBlazorWebView.UnitTests.csproj`. ## net11.0-targeting PR support This branch also makes the Copilot review pipeline handle **`net11.0`-targeting PRs** as well as `main`/net10, from a single pipeline branch (consolidates and supersedes dotnet#35994): - **Runtime base-branch auto-detection** (`eng/pipelines/ci-copilot.yml`): the `CopilotReview` and `DeepUITests` stages read the PR's `baseRefName` via `gh`, allowlist-validate it (`^(main|net[0-9]+\.0)$`), and check out that base **before** workload install and the squash-merge — so workloads (net11 `11.0.100` vs net10 `10.0.100`) and the merge base follow the PR. Trusted scripts are captured from the pipeline ref first (security rule 3); Task 1 Setup runs from `$TRUSTED`. - **Branch-aware test TargetFramework**: `BuildAndRunHostApp.ps1` and `run-device-tests/Run-DeviceTests.ps1` now derive the TFM from `Directory.Build.props` (`Get-MauiTfmVersion` in `shared-utils.ps1`) instead of hardcoding `net10.0`, so deep-UI/device tests build `net11.0-android` on net11. The `DeepUITests` stage restores the reviewed pipeline-branch scripts over the worktree before the per-category loop. `main`/net10 behavior is unchanged by design (base-detection checks out `main` = the original flow; `global.json` stays `10.0.108`). `review-trigger.yml` and `Review-PR.ps1` are intentionally untouched. **Validation:** 8 live net11 runs with 100% correct base detection (e.g. dotnet#35891 gate ran net11 Core tests 44/45 → 45/45, conflict-free squash-merge onto net11.0); reproduced and root-caused a separate net11.0-baseline `MAUIX2017` Xaml.UnitTests build break (unrelated to this change). Confirmation runs from this branch cover one net11 PR and one main PR. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Tomas Grosup <tomasgrosup@microsoft.com> Co-authored-by: Copilot <Copilot@users.noreply.github.com> Co-authored-by: Copilot CI <copilot-ci@microsoft.com> Co-authored-by: kubaflo <kubaflo@users.noreply.github.com>
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Summary
APPROVE,REQUEST_CHANGES, or safeCOMMENTfallback.SECTION:PR-FINALIZEblocks./review rerunafter new comments or commits when they want a fresh review. The command implementation is intentionally split into a follow-up PR.Validation
System.Management.Automation.Language.Parser..github/workflows/review-trigger.ymlas YAML.Invoke-Pester .github/scripts/Post-AISummaryComment.Tests.ps1,.github/scripts/Remove-StaleMauiBotComments.Tests.ps1 -CIDetect-TestsInDiff.ps1mapssrc/BlazorWebView/tests/MauiBlazorWebView.UnitTests/UriExtensions_Tests.cstosrc/BlazorWebView/tests/MauiBlazorWebView.UnitTests/MauiBlazorWebView.UnitTests.csproj.net11.0-targeting PR support
This branch also makes the Copilot review pipeline handle
net11.0-targeting PRs as well asmain/net10, from a single pipeline branch (consolidates and supersedes #35994):eng/pipelines/ci-copilot.yml): theCopilotReviewandDeepUITestsstages read the PR'sbaseRefNameviagh, allowlist-validate it (^(main|net[0-9]+\.0)$), and check out that base before workload install and the squash-merge — so workloads (net1111.0.100vs net1010.0.100) and the merge base follow the PR. Trusted scripts are captured from the pipeline ref first (security rule 3); Task 1 Setup runs from$TRUSTED.BuildAndRunHostApp.ps1andrun-device-tests/Run-DeviceTests.ps1now derive the TFM fromDirectory.Build.props(Get-MauiTfmVersioninshared-utils.ps1) instead of hardcodingnet10.0, so deep-UI/device tests buildnet11.0-androidon net11. TheDeepUITestsstage restores the reviewed pipeline-branch scripts over the worktree before the per-category loop.main/net10 behavior is unchanged by design (base-detection checks outmain= the original flow;global.jsonstays10.0.108).review-trigger.ymlandReview-PR.ps1are intentionally untouched.Validation: 8 live net11 runs with 100% correct base detection (e.g. #35891 gate ran net11 Core tests 44/45 → 45/45, conflict-free squash-merge onto net11.0); reproduced and root-caused a separate net11.0-baseline
MAUIX2017Xaml.UnitTests build break (unrelated to this change). Confirmation runs from this branch cover one net11 PR and one main PR.