Skip to content
Open
Show file tree
Hide file tree
Changes from 62 commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
438a0f0
Improve maui-copilot reviewer: gate & deep-stage reliability fixes
Copilot Jul 29, 2026
defbe1a
Reviewer: de-noise gate-log excerpts + mark previous AI Summary outdated
Copilot Jul 29, 2026
59400f8
Gate: credit compile-coupled new-API PRs as PASSED (not INCONCLUSIVE)
Copilot Jul 30, 2026
49f81c7
Gate: don't double-message a new-snapshot-no-baseline as an infra error
Copilot Jul 30, 2026
b3cdec3
Gate: classify MSBuild-server/BuildTasks flake as infra (ENV), not a …
Copilot Jul 30, 2026
03db4dd
review-trigger: don't fail /review silently on a bad --branch; tell t…
Copilot Jul 30, 2026
bf4d197
Gate: classify NETSDK1147 missing-workload as infra (ENV), not a code…
Copilot Jul 31, 2026
fc6110d
Post: never crash-and-silence when phase content is missing but a ver…
Copilot Jul 31, 2026
29e0165
review-trigger: harden the scripts checkout so a hang never silently …
Copilot Jul 31, 2026
d173931
ci-copilot: telemetry token-usage stage must never red-fail a green r…
Copilot Jul 31, 2026
5f6895e
Fix deep UI-failure analysis silently dropped at high failure counts …
Copilot Aug 1, 2026
9cd0ebe
Gate: free disk space BEFORE the build so android agents don't hit 'N…
Copilot Aug 1, 2026
c9d23ba
Gate: reinstall .NET workloads after a .dotnet wipe so android gates …
Copilot Aug 1, 2026
67ba776
Gate: classify a fixture-wide OneTimeSetUp app-launch/crash-recovery …
Copilot Aug 2, 2026
deed1c3
Gate: give a persisted APP_CRASH an honest message, not the transient…
Copilot Aug 2, 2026
1dbfa0b
Deep: exclude new-baseline failures from Android flaky-retry
Copilot Aug 2, 2026
7a8886d
Fix: inline review comments silently dropped (orphaned sentinel)
Aug 3, 2026
f32fa00
Gate: snapshot SIZE mismatch is INCONCLUSIVE, not a false FAILED
Aug 3, 2026
b21e1f4
Gate: with-fix native-lib load failure is INCONCLUSIVE regardless of …
Aug 3, 2026
edd7093
Fix: truncate AI review body under GitHub's 65536-char limit
Aug 4, 2026
b98a5e0
Reviewer: upgrade to gpt-5.6-sol (long context, max effort); use opus…
Aug 4, 2026
9c2fff2
Reviewer: adopt the review-process model refresh (GPT-5.6 Sol majority)
Copilot Aug 4, 2026
a636f9e
Reviewer: trim try-fix panel from 4 models to 2 (Opus 5 + Sol) for speed
Copilot Aug 4, 2026
c1bb1a4
Reviewer: time-box Phase 2 try-fix so it never times out Task 3
Copilot Aug 4, 2026
db3b7d5
Reviewer: post AI Summary AFTER expert inline findings
Copilot Aug 4, 2026
55a0d21
Reviewer: make the timeout-fallback summary use an expandable section
Copilot Aug 4, 2026
ea8faa5
Reviewer: retry Copilot on transient auth-validation 401 so expert se…
Copilot Aug 5, 2026
6072e1c
Reviewer: explain missing expert sections instead of silently droppin…
Copilot Aug 5, 2026
030dfb8
Reviewer: notice mentions expired COPILOT_TOKEN as a cause of missing…
Copilot Aug 5, 2026
06d2e55
Reviewer: run Deep UI Tests even when the review agent times out (Can…
Copilot Aug 5, 2026
0d303b5
Reviewer: preserve all AI summary sections
Copilot Aug 5, 2026
b50418c
Reviewer: avoid full checkout in review trigger
Copilot Aug 5, 2026
d285e95
Reviewer: trust timed-out Gate verdict in summary
Copilot Aug 5, 2026
5d368ba
Reviewer: clear stale signal labels
Copilot Aug 5, 2026
7d03d24
Reviewer: trust Gate verdict when labeling
Copilot Aug 5, 2026
44d7e75
Fix reviewer timeout and emulator recovery
Copilot Aug 6, 2026
7d941a4
Size reviewer budgets for delegated agents
Copilot Aug 6, 2026
9f4fe60
Merge origin/main into improved-reviewer, resolving conflicts
Copilot Aug 6, 2026
2c48c4d
Restore review trigger authorization headers
Copilot Aug 7, 2026
16f0c07
Fix completed reviewer outcomes
Copilot Aug 7, 2026
3515534
Merge remote reviewer trigger fix
Copilot Aug 7, 2026
4c6409d
Recognize legacy expert verdict headings
Copilot Aug 7, 2026
fb3c7b3
Fix reviewer provisioning warnings and timeouts
Copilot Aug 7, 2026
e60f2a0
Improve pipeline result handling
Copilot Aug 7, 2026
f2572e1
Fix reviewer discovery and cleanup hangs
Copilot Aug 7, 2026
e256794
Avoid reviewer telemetry checkout timeouts
Copilot Aug 7, 2026
77815b5
Sanitize UI failure category logs
Copilot Aug 7, 2026
08bb78b
Mark test category receiver unused
Copilot Aug 7, 2026
683fc95
Harden reviewer post-processing and Catalyst setup
Copilot Aug 7, 2026
04451d4
Report skipped deep UI tests
Copilot Aug 7, 2026
b2b2714
Include expert review in AI summary
Copilot Aug 7, 2026
81e836e
Fix reviewer test filtering fallbacks
Copilot Aug 7, 2026
ff35c61
Treat host-incompatible Gate workloads as inconclusive
Copilot Aug 7, 2026
e156597
Merge concurrent reviewer filtering fixes
Copilot Aug 7, 2026
120948f
Preserve Gate device crash diagnostics
Copilot Aug 7, 2026
3f2e14a
Use release packaging for Android Gate tests
Copilot Aug 7, 2026
122424b
Prevent vacuous deep UI test runs
Copilot Aug 7, 2026
c95a5a9
Treat zero-test deep runs as inconclusive
Copilot Aug 7, 2026
af759eb
Harden reviewer console and findings parsing
Copilot Aug 7, 2026
d7915e4
Fix device test class filtering in shared runners
Copilot Aug 7, 2026
95d2674
Veto contradictory approvals, disambiguate startup failures, guard ba…
Copilot Aug 7, 2026
f7da72f
Clarify restore contract and fix test formatting
Copilot Aug 7, 2026
b688d6f
Fix Gate class filtering through trusted runner injection
Copilot Aug 7, 2026
9739702
Fix Gate device-test rebuild isolation
Copilot Aug 7, 2026
f0b11e3
Skip reviewer deep stages after cancellation
Copilot Aug 7, 2026
a0830fb
Name reviewer summary phases explicitly
Copilot Aug 7, 2026
dc6b983
Recognize repeated Windows Gate crash repros
Copilot Aug 7, 2026
6f43a2d
Harden Notification Center lifecycle
Copilot Aug 8, 2026
6072b6a
Fix Windows device test class filtering
Copilot Aug 8, 2026
6a17651
Document Windows class-isolated device tests
Copilot Aug 8, 2026
1d1f57b
Isolate XHarness Gate retry results
Copilot Aug 8, 2026
8a3eacf
Handle SIP-protected Notification Center
Copilot Aug 8, 2026
a481469
Preserve review locks across active builds
Copilot Aug 8, 2026
40648ff
Fix snapshot asset publishing
Copilot Aug 8, 2026
6546bcd
Harden Windows Gate target timeouts
Copilot Aug 8, 2026
3466f44
Address validated reviewer follow-ups: sanitize AI categories, fix ca…
Copilot Aug 8, 2026
055fc8f
Harden scoped Windows Gate failure precedence
Copilot Aug 8, 2026
46b3ecd
Fix merge-conflict review fallback
Copilot Aug 8, 2026
4e65d04
Hide stale incomplete merge notices
Copilot Aug 8, 2026
f3d7bf1
Fix Retina Catalyst screenshot crops
Copilot Aug 8, 2026
e2acb32
Apply Catalyst crop override after PR merge
Copilot Aug 8, 2026
8bfb730
Fix reviewer PR metadata updates
Copilot Aug 8, 2026
126e4cf
Fix device test method detection
Copilot Aug 8, 2026
39d780e
Fix Catalyst app recovery dialog cascade
Copilot Aug 8, 2026
0837529
Recover missed review trigger comments
Copilot Aug 8, 2026
2560818
Fix reviewer candidate validation worktree
Copilot Aug 8, 2026
dbe1395
Fix regression test runner roots
Copilot Aug 8, 2026
e605ca9
Preserve trusted reviewer candidate baseline
Copilot Aug 8, 2026
80630e6
Exclude legacy candidate sandboxes
Copilot Aug 8, 2026
80c2c99
Make Android retry TRX authoritative
Copilot Aug 8, 2026
ec39b5c
Fix compile-coupled device Gate detection
Copilot Aug 8, 2026
8144354
Require exact test path for compile coupling
Copilot Aug 8, 2026
854d773
Make successful review dispatch visible
Copilot Aug 8, 2026
cf1a354
Harden reviewer observability checks
Copilot Aug 8, 2026
af8d7e9
Fix metadata updates for reviewer candidates
Copilot Aug 8, 2026
f0702a9
Bound deep UI diagnostic artifacts
Copilot Aug 8, 2026
8c38558
Fix PR finalize fenced body parsing
Copilot Aug 8, 2026
e23c33f
Bound deep UI diagnostic artifacts
Copilot Aug 8, 2026
b6d5167
Fix PR metadata title prefixes
Copilot Aug 9, 2026
6c2e68f
Fix reused Android emulator recovery
Copilot Aug 9, 2026
a7533e3
Pin Gate test detection to review snapshot
Copilot Aug 9, 2026
831bc6b
Fix reviewer snapshot drift across stages
Aug 9, 2026
0e41471
Fix reviewer recovery and failure classification
Aug 12, 2026
69e43c0
Harden reviewer recovery edge cases
Aug 12, 2026
5563130
Preserve Mac screenshot evidence on crop failures
Aug 13, 2026
bf360b5
Clarify simulator runtime cleanup progress
Aug 13, 2026
49924ff
Fix reviewer workload recovery after .NET wipes
Aug 13, 2026
af843cf
Preserve screenshots in Catalyst source override
Aug 13, 2026
0cc8946
Rebuild reviewer build tasks after .NET recovery
Aug 13, 2026
b63bdd2
Round Mac screenshot crop dimensions
Aug 13, 2026
3fae392
Fix reviewer device-test class detection
Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions .github/instructions/ci-copilot-pipeline-security.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,35 @@ Once the PR is merged into the worktree, the author controls every `.csproj`, `D

## Rules

1. **Per-task `env:` scoping.** Only put tokens a task needs. The Copilot-agent task gets `COPILOT_GITHUB_TOKEN` only — never `GH_TOKEN`. Pass `--secret-env-vars=GH_TOKEN,GITHUB_TOKEN,COPILOT_GITHUB_TOKEN` to the Copilot CLI.
1. **Per-task `env:` scoping.** Only put tokens in tasks that need them. The Copilot-agent task gets `COPILOT_GITHUB_TOKEN` only — never `GH_TOKEN`. The Post task runs in its own Microsoft-hosted job and receives `GH_COMMENT_TOKEN` only in its posting step. Pass `--secret-env-vars=GH_TOKEN,GITHUB_TOKEN,COPILOT_GITHUB_TOKEN` to the Copilot CLI.

2. **`persistCredentials: false` on every `checkout: self`** unless the task pushes. Default checkout writes the service-connection PAT into `.git/config` as `extraheader`, readable by any subprocess.

3. **Trusted-copy scripts before merging the PR.** Setup task (still on `main`) copies `.github/scripts`, `.github/skills`, `eng/scripts` to `$(Build.ArtifactStagingDirectory)/trusted-github/`, then `chmod -R a-w`. Later tasks invoke scripts from `$TRUSTED/...`, never from the merged worktree. In PowerShell use `$ScriptsDir` / `$SkillsDir` / `$EngScriptsDir` (canonical impl in `Review-PR.ps1`). New post-merge scripts must be added to the Setup copy block.
3. **Trusted-copy scripts before merging the PR.** Setup copies `.github/scripts`, `.github/skills`, and `eng/scripts` to `$(Build.ArtifactStagingDirectory)/trusted-github/` before switching branches or merging the PR. Gate and CopilotReview invoke scripts through `$ScriptsDir`, `$SkillsDir`, and `$EngScriptsDir`, never from the merged worktree. New scripts used by those phases must be added to the Setup copy block.

4. **Strip tokens before invoking PR-controlled code.** Wrap every `dotnet build|test|run|pack`, `msbuild`, `dotnet cake`, `BuildAndRun*.ps1`, `Run-DeviceTests.ps1`, `Invoke-UITestWithRetry.ps1` in `Invoke-WithoutGhTokens { ... }` (defined in `Review-PR.ps1` and `verify-tests-fail.ps1` — saves/clears/restores `GH_TOKEN`, `GITHUB_TOKEN`, `COPILOT_GITHUB_TOKEN`). **Wrap as close to the subprocess as possible, not at the outer trusted-script boundary** — a trusted script may itself need `gh` for metadata (e.g., `verify-tests-fail.ps1` calls `Detect-TestsInDiff.ps1` which uses `gh api`), so wrapping the whole script breaks its detection path. Wrap only the line that launches the PR-controlled process. Exception: scripts that ONLY call `gh` for PR metadata (`Detect-TestsInDiff.ps1`, `Find-RegressionRisks.ps1`, `detect-ui-test-categories.ps1`) don't need wrapping at all — they keep the token.
4. **Run Post from a clean pipeline checkout.** Post runs in a separate Microsoft-hosted job, checks out `$(Build.SourceVersion)` with `clean: true` and `persistCredentials: false`, and executes `.github/scripts`, `.github/skills`, and `eng/scripts` from that checkout. It downloads review results separately and copies only `CustomAgentLogsTmp` into the expected data path. Do not copy artifact content over script directories.

5. **Cross-phase signal files in `$(Agent.TempDirectory)`** (or `$TRUSTED`), never `$RepoRoot/...`. PR code can overwrite anything in the worktree, including a gate verdict. Readers must not silently fall back to a worktree path if the trusted one is missing.
5. **Strip tokens before invoking PR-controlled code.** Wrap every `dotnet build|test|run|pack`, `msbuild`, `dotnet cake`, `BuildAndRun*.ps1`, `Run-DeviceTests.ps1`, `Invoke-UITestWithRetry.ps1` in `Invoke-WithoutGhTokens { ... }` (defined in `Review-PR.ps1` and `verify-tests-fail.ps1` — saves/clears/restores `GH_TOKEN`, `GITHUB_TOKEN`, `COPILOT_GITHUB_TOKEN`). **Wrap as close to the subprocess as possible, not at the outer trusted-script boundary** — a trusted script may itself need `gh` for metadata (e.g., `verify-tests-fail.ps1` calls `Detect-TestsInDiff.ps1` which uses `gh api`), so wrapping the whole script breaks its detection path. Wrap only the line that launches the PR-controlled process. Exception: scripts that ONLY call `gh` for PR metadata (`Detect-TestsInDiff.ps1`, `Find-RegressionRisks.ps1`, `detect-ui-test-categories.ps1`) don't need wrapping at all — they keep the token.

6. **Strip `##vso[...]` from PR-controlled stdout.** Pipe through `tr -d '\r' | sed -E 's/##vso\[[^]]*\]//g'` — bare `sed` misses CRLF lines and the agent will execute the directive.
6. **Cross-phase and cross-job results.** Same-job phase files belong in `$(Agent.TempDirectory)` or the trusted staging directory, never the merged worktree. Cross-job values use named output variables with a fixed set of expected values or pipeline artifacts. Download artifacts outside the checkout, copy only the required data directory, and never transfer scripts for Post to run.

7. **`gh-aw` workflows.** Pin compiler version (≥ v0.68.4 strips `pull-requests: write` per `gh-aw#28767`). Regenerate `.lock.yml` with `gh aw compile` in the **same commit** as any `.md` frontmatter edit (stale lock ⇒ all dispatches fail). `workflow_dispatch` triggers must restore trusted `.github/` from main (see `Checkout-GhAwPr.ps1`).
7. **Strip `##vso[...]` from PR-controlled stdout.** Pipe through `tr -d '\r' | sed -E 's/##vso\[[^]]*\]//g'` — bare `sed` misses CRLF lines and the agent will execute the directive.

8. **No token republish.** Don't `setvariable` a token (visible to every later task, even with `issecret=true`). Don't write tokens to worktree files. Don't echo token names.
8. **`gh-aw` workflows.** Pin compiler version (≥ v0.68.4 strips `pull-requests: write` per `gh-aw#28767`). Regenerate `.lock.yml` with `gh aw compile` in the **same commit** as any `.md` frontmatter edit (stale lock ⇒ all dispatches fail). `workflow_dispatch` triggers must restore trusted `.github/` from main (see `Checkout-GhAwPr.ps1`).

9. **No token republish.** Don't `setvariable` a token (visible to every later task, even with `issecret=true`). Don't write tokens to worktree files. Don't echo token names.

## Review checklist

- [ ] New `checkout: self` has `persistCredentials: false`.
- [ ] New `env:` block lists only the tokens that task needs; Copilot task has no `GH_TOKEN`.
- [ ] New post-merge script invoked via `$ScriptsDir` / `$SkillsDir` / `$EngScriptsDir`, not `$RepoRoot/...`, AND added to Setup copy block.
- [ ] New Gate or CopilotReview script is invoked through `$ScriptsDir` / `$SkillsDir` / `$EngScriptsDir` and is included in the Setup copy block.
- [ ] Post runs in its own Microsoft-hosted job from a clean checkout of the pipeline revision.
- [ ] Post executes scripts from the checkout and copies only expected result data from pipeline artifacts.
- [ ] Artifact content cannot overwrite `.github/scripts`, `.github/skills`, or `eng/scripts`.
- [ ] Gate labels use the fixed `RunGate.gateResult` output.
- [ ] New invocation of PR-controlled code (`dotnet test|build|run`, `BuildAndRun*`, `Run-DeviceTests`, `Invoke-UITestWithRetry`) is wrapped in `Invoke-WithoutGhTokens` AT THE CALL SITE (not at an outer boundary).
- [ ] New cross-phase state file lives under `$(Agent.TempDirectory)` / `$TRUSTED`.
- [ ] New same-job phase state uses `$(Agent.TempDirectory)` / trusted staging; new cross-job state uses an output variable or pipeline artifact.
- [ ] New PR-stdout pipe uses `tr -d '\r' | sed -E 's/##vso\[[^]]*\]//g'`.
- [ ] Edited `.github/workflows/*.md` has matching `.lock.yml` regenerated in same commit.

Expand Down
18 changes: 10 additions & 8 deletions .github/pr-review/pr-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@

- Phases 1-2 (Pre-Flight, Try-Fix) must be complete before starting
- Gate result is available from the prompt (ran separately before this skill)
- **Read `pre-flight/content.md`** to get the code-review summary (verdict, confidence, error/warning counts)
- Optionally read `pre-flight/code-review.md` for full findings if needed for the recommendation
- **Read `pre-flight/content.md`** for issue/PR context
- **Read `expert-pr-eval/content.md`** for the code-review verdict, confidence, and findings
- Read `try-fix/content.md` and the individual candidate outputs for the comparison

---

Expand All @@ -25,11 +26,11 @@
|----------|-----------|----------------|
| 1 | Code review verdict is `NEEDS_CHANGES` (any ❌ errors) | `⚠️ REQUEST CHANGES` — code review found errors |
| 2 | Gate failed (tests fail with fix) | `⚠️ REQUEST CHANGES` — fix doesn't work |
| 3 | Alternative fix found via Try-Fix that is simpler/better | `⚠️ REQUEST CHANGES` — suggest alternative |
| 3 | `pr-plus-reviewer` or a `try-fix-*` candidate wins | `⚠️ REQUEST CHANGES` — submitted PR needs the winning changes |
| 4 | Code review verdict is `NEEDS_DISCUSSION` | `⚠️ REQUEST CHANGES` — include code review concerns |
| 5 | PR's fix selected AND Gate passed AND code review LGTM or SKIPPED | `✅ APPROVE` |
| 5 | Raw `pr` candidate wins AND Gate permits approval AND code review is LGTM or SKIPPED | `✅ APPROVE` |

**🚨 Hard gate:** If the code review (from Pre-Flight) has verdict `NEEDS_CHANGES`, the final recommendation MUST be `REQUEST CHANGES` regardless of Gate or Try-Fix results. Code-review ❌ Errors cannot be overridden by passing tests alone.
**🚨 Hard gate:** If the expert code review has verdict `NEEDS_CHANGES`, the final recommendation MUST be `REQUEST CHANGES` regardless of Gate or Try-Fix results. Code-review ❌ Errors cannot be overridden by passing tests alone.

**Code review SKIPPED:** If the code-review sub-agent failed or timed out (verdict = `SKIPPED`), the hard gate does NOT apply. Proceed as if code review was not available — base the recommendation on Gate and Try-Fix results only. Note in the report that code review was unavailable.

Expand All @@ -47,7 +48,7 @@
mkdir -p CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/report
```

Write `content.md`:
Write `content.md`. Its first non-empty line must be exactly the canonical heading shown below:
```markdown
## {✅/⚠️} Final Recommendation: {APPROVE/REQUEST CHANGES}

Expand All @@ -60,8 +61,8 @@ Write `content.md`:
| Try-Fix | ✅ COMPLETE | {N} attempts, {M} passing |
| Report | ✅ COMPLETE | |

### Code Review Impact on Try-Fix
{Brief description of how code-review findings influenced try-fix exploration. Did any model specifically address a code review ❌ Error? Did failure-mode probes reveal issues that guided fix approaches?}
### Code Review and Candidate Comparison
{Briefly identify which candidates address the expert review findings and whether any candidate leaves a ❌ Error unresolved. Do not imply the expert pass influenced earlier try-fix attempts; it runs after those attempts.}

### Summary
{Brief summary of the review}
Expand Down Expand Up @@ -96,6 +97,7 @@ Standard markers in content.md: `✅ PASSED`, `❌ FAILED`, `Selected Fix: PR`,

## Common Mistakes

- ❌ Replacing the required first line with `## Result`, `**Winner:**`, or equivalent prose
- ❌ Rushing the report — take time for clear justification
- ❌ Running git commands — user handles commit/push
- ❌ Posting comments — this phase only produces output files, never posts to GitHub
33 changes: 33 additions & 0 deletions .github/scripts/Apply-PRFinalize.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

BeforeAll {
$scriptPath = Join-Path $PSScriptRoot 'apply-pr-finalize.ps1'
$script:ScriptText = Get-Content -Raw -LiteralPath $scriptPath
$tokens = $null
$parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile($scriptPath, [ref]$tokens, [ref]$parseErrors)
Expand Down Expand Up @@ -38,6 +39,7 @@ BeforeAll {
'Get-FinalizeRecommendation',
'Merge-PreservedTitlePrefix',
'Merge-PreservedBodyPreamble',
'New-PullRequestUpdatePayload',
'New-ExclusiveTempFile'
)) {
$function = $ast.Find({
Expand Down Expand Up @@ -226,6 +228,37 @@ Old description.
}
}

Describe 'New-PullRequestUpdatePayload' {
It 'includes only the title when only the title changed' {
$payload = New-PullRequestUpdatePayload `
-TitleChanged $true `
-Title '[Android] RadioButton: Clear reset borders' `
-BodyChanged $false `
-Body 'unchanged'

@($payload.Keys) | Should -Be @('title')
$payload.title | Should -Be '[Android] RadioButton: Clear reset borders'
}

It 'includes only the body when only the body changed' {
$payload = New-PullRequestUpdatePayload `
-TitleChanged $false `
-Title 'unchanged' `
-BodyChanged $true `
-Body "### Change`n`nUpdated details."

@($payload.Keys) | Should -Be @('body')
$payload.body | Should -Be "### Change`n`nUpdated details."
}

It 'uses the REST pull-request endpoint for reads and writes' {
$script:ScriptText | Should -Match ([regex]::Escape('$prOutput = @(& gh api "repos/$Repo/pulls/$PRNumber"'))
$script:ScriptText | Should -Match ([regex]::Escape('$ghArgs = @(''api'', "repos/$Repo/pulls/$PRNumber", ''--method'', ''PATCH'', ''--input'', $payloadFile, ''--silent'')'))
$script:ScriptText | Should -Not -Match '\bgh\s+pr\s+(?:view|edit)\b'
$script:ScriptText | Should -Not -Match ([regex]::Escape("@('pr', 'edit'"))
}
}

Describe 'ConvertTo-AzdoSafeConsole' {
# Behaviour is pinned to the canonical implementation in Review-PR.ps1; these mirror the
# assertions in Review-PR.Tests.ps1 so the duplicated copy can't silently drift.
Expand Down
83 changes: 83 additions & 0 deletions .github/scripts/BuildAndRunHostApp.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#Requires -Modules Pester

# Focused tests for the Android per-test flaky-retry classification in
# BuildAndRunHostApp.ps1: "Baseline snapshot not yet created" failures are
# brand-new VerifyScreenshot tests (no committed baseline). They are
# deterministic new-baseline results, NOT emulator flake, and must be excluded
# from the flaky-retry set (retrying them wastes a full re-run and can exhaust
# the deep category time budget on snapshot-heavy PRs).
#
# The retry logic is embedded in a large script rather than a callable function,
# so these tests exercise the exact classification predicate used there.

Describe 'Android flaky-retry new-baseline exclusion' {
BeforeAll {
$script:BaselineRegex = '(?i)Baseline snapshot not yet created'

# Mirrors the predicate in BuildAndRunHostApp.ps1: given TRX-style
# results ({ status; name; error }), return the names to retry
# (Failed and NOT a new-baseline failure).
function Get-RetryNames {
param([object[]]$Results)
$failed = @($Results | Where-Object { $_.status -eq 'Failed' })
@($failed |
Where-Object { ($_.error -as [string]) -notmatch $script:BaselineRegex } |
ForEach-Object { $_.name })
}

Describe 'MacCatalyst Apple Account dialog dismissal' {
It 'prefers the trusted staged script location before the repository fallback' {
$scriptContent = Get-Content (Join-Path $PSScriptRoot 'BuildAndRunHostApp.ps1') -Raw
$trustedPath = '../eng-scripts/dismiss-apple-account-dialog.sh'
$fallbackPath = '../../eng/scripts/dismiss-apple-account-dialog.sh'

$scriptContent.IndexOf($trustedPath) | Should -BeLessThan $scriptContent.IndexOf($fallbackPath)
}
}

function Get-BaselineCount {
param([object[]]$Results)
@($Results | Where-Object {
$_.status -eq 'Failed' -and (($_.error -as [string]) -match $script:BaselineRegex)
}).Count
}
}

It 'excludes baseline-not-created failures from the retry set' {
$results = @(
[pscustomobject]@{ status='Failed'; name='SearchBar_Material3_A'; error='Baseline snapshot not yet created: /snapshots/android/SearchBar_A.png' }
[pscustomobject]@{ status='Failed'; name='SearchBar_Material3_B'; error='Baseline snapshot not yet created: /snapshots/android/SearchBar_B.png' }
[pscustomobject]@{ status='Passed'; name='Switch_C'; error='' }
)
(Get-RetryNames -Results $results).Count | Should -Be 0
Get-BaselineCount -Results $results | Should -Be 2
}

It 'keeps genuine (non-baseline) flaky failures in the retry set' {
$results = @(
[pscustomobject]@{ status='Failed'; name='Flaky_Timeout'; error='System.TimeoutException: element not found' }
[pscustomobject]@{ status='Failed'; name='New_Snapshot'; error='Baseline snapshot not yet created: /snapshots/android/New.png' }
[pscustomobject]@{ status='Passed'; name='Ok_Test'; error='' }
)
$retry = Get-RetryNames -Results $results
$retry | Should -Contain 'Flaky_Timeout'
$retry | Should -Not -Contain 'New_Snapshot'
$retry.Count | Should -Be 1
Get-BaselineCount -Results $results | Should -Be 1
}

It 'is case-insensitive on the baseline signature' {
$results = @(
[pscustomobject]@{ status='Failed'; name='X'; error='BASELINE SNAPSHOT NOT YET CREATED: /p.png' }
)
(Get-RetryNames -Results $results).Count | Should -Be 0
Get-BaselineCount -Results $results | Should -Be 1
}

It 'treats null/empty error as a retryable (non-baseline) failure' {
$results = @(
[pscustomobject]@{ status='Failed'; name='NoErrText'; error=$null }
)
(Get-RetryNames -Results $results) | Should -Contain 'NoErrText'
}
}
Loading
Loading