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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .build/Pester.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,15 @@ begin {
}
}

# Only `.github/examples` is excluded. Those files are documentation samples
# demonstrating test style, not tests of shipping code, and they were the
# sole reason this exclusion was added. Real tests elsewhere under `.github`
# -- notably the agent-skill helper tests at
# `.github/skills/<skill>/Tests/*.Tests.ps1` -- MUST run, because they are
# the drift guards that pin the skills to the product code they parse.
$root = Get-Item "$PSScriptRoot\.."
$scripts = @(Get-ChildItem -Recurse $root |
Where-Object { $_.Name -like "*.Tests.ps1" -and $_.FullName -notmatch "\.github" })
Where-Object { $_.Name -like "*.Tests.ps1" -and $_.FullName -notmatch "\.github[\\/]examples[\\/]" })
Comment thread
Copilot marked this conversation as resolved.
Resolved

# Categorize test files by priority using comment tag: # PesterPriority: High
$highPriorityQueue = New-Object 'System.Collections.Generic.Queue[object]'
Expand Down
103 changes: 103 additions & 0 deletions .build/Tests/Pester.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'Pester runner branch selection' {
BeforeAll {
$Script:runnerPath = Join-Path -Path $PSScriptRoot -ChildPath '..\Pester.ps1'

if (-not ('PesterRunnerTestJob' -as [type])) {
Add-Type -TypeDefinition @'
using System.Management.Automation;
public sealed class PesterRunnerTestJob : Job {
public PesterRunnerTestJob(string name) : base("fixture", name) {
SetJobState(JobState.Running);
SetJobState(JobState.Completed);
}
public override string Location { get { return ""; } }
public override string StatusMessage { get { return ""; } }
public override bool HasMoreData { get { return false; } }
public override void StopJob() { }
}
'@
}

function git {
throw 'Unexpected git invocation without a mock.'
}

function Invoke-TestRunner {
& $Script:runnerPath -NoProgress -Branch main *> $null
}
}

BeforeEach {
$Script:runnerState = @{
ChangedFile = '.github/skills/analyze-debug-files/SKILL.md'
SelectedPaths = [System.Collections.Generic.List[string]]::new()
}
$state = $Script:runnerState
Mock -CommandName git -MockWith ({
switch ($args[0]) {
'log' { return '1111111111111111111111111111111111111111 Wed, 28 Oct 2026 07:46:41 -0500' }
'diff-tree' { return $state.ChangedFile }
'status' { return }
}
throw "Unexpected git arguments: $($args -join ' ')"
}.GetNewClosure())
Mock -CommandName Start-Job -MockWith ({
param($ArgumentList, $Name)
$state.SelectedPaths.Add([string]@($ArgumentList)[0])
return [PesterRunnerTestJob]::new($Name)
}.GetNewClosure())
Mock -CommandName Receive-Job -MockWith {
return [PSCustomObject]@{
Result = 'Passed'
Duration = [TimeSpan]::FromMilliseconds(1)
}
}
Mock -CommandName Remove-Job -MockWith {}
}

It 'skips documentation-only changes for <ChangedFile>, matching main' -ForEach @(
@{ ChangedFile = '.github/skills/analyze-debug-files/SKILL.md' }
@{ ChangedFile = '.github/skill-lib/README.md' }
@{ ChangedFile = 'README.md' }
) {
Comment thread
dpaulson45 marked this conversation as resolved.
Resolved
$Script:runnerState.ChangedFile = $ChangedFile
Invoke-TestRunner

$Script:runnerState.SelectedPaths.Count | Should -Be 0
}

It 'retains all test areas when <ChangedFile> changes' -ForEach @(
@{ ChangedFile = '.build/Pester.ps1' }
@{ ChangedFile = '.github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1' }
) {
$Script:runnerState.ChangedFile = $ChangedFile
Invoke-TestRunner

@($Script:runnerState.SelectedPaths | Where-Object {
$_ -notmatch '[\\/]\.github[\\/]'
}).Count | Should -BeGreaterThan 0
@($Script:runnerState.SelectedPaths | Where-Object {
(Split-Path -Path $_ -Leaf) -eq 'SkillDocContract.Tests.ps1'
}).Count | Should -Be 1
@($Script:runnerState.SelectedPaths | Where-Object {
$_ -match '[\\/]\.github[\\/]examples[\\/]'
}).Count | Should -Be 0
}

It 'retains all test areas without a branch filter even for documentation-only changes' {
& $Script:runnerPath -NoProgress *> $null

@($Script:runnerState.SelectedPaths | Where-Object {
$_ -notmatch '[\\/]\.github[\\/]'
}).Count | Should -BeGreaterThan 0
@($Script:runnerState.SelectedPaths | Where-Object {
(Split-Path -Path $_ -Leaf) -eq 'SkillDocContract.Tests.ps1'
}).Count | Should -Be 1
@($Script:runnerState.SelectedPaths | Where-Object {
$_ -match '[\\/]\.github[\\/]examples[\\/]'
}).Count | Should -Be 0
}
}
5 changes: 5 additions & 0 deletions .build/cspell-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,12 @@ mepfdns
mepfs
meso
mesos
metacharacters
mfcmapi
Mgmt
microsoftonline
misattributed
misrouting
mitigations
msdcs
MSDTC
Expand Down Expand Up @@ -163,6 +166,7 @@ subfolders
syncall
tcpip
TDSDSA
triaging
Truncater
UCMA
unconfigured
Expand All @@ -181,6 +185,7 @@ Webex
Weve
wevtutil
windir
worktree
xtap
Xlsb
Xtap
21 changes: 18 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ Specialized agents are available in `.github/agents/` for specific tasks:
Skills are available in `.github/skills/` and are automatically loaded when relevant:

- **dependency-analysis** — Maps cascading impact of changes using build system XML. Auto-loads when working with shared functions or assessing change impact. Can also be invoked explicitly: `"Use the /dependency-analysis skill to analyze impact of changing FunctionName"`
- **analyze-debug-files** — Analyzes a directory of CSS-Exchange debug logs, pins the source to a release-tag baseline, and produces a root-cause analysis of unhandled exceptions. Use: `"Use the /analyze-debug-files skill on C:\logs"`
- **find-release-tag-for-script-version** — Given a script name and version (e.g. `HealthChecker 26.10.02.1907`), finds the earliest GitHub release whose `ScriptVersions.csv` shipped that build.
- **find-related-github-issues** — Given an exception signature, searches Issues and PRs and classifies each hit as a possible duplicate or merely similar.
- **trace-code-introduction** — Given a file path, line range, and pinned SHA, identifies the commit and PR that introduced that code, with a conservative regression verdict.

Shared filesystem-safety helpers used by these skills live in `.github/skill-lib/`. See its README — skills that depend on it are repository-scoped and are not independently distributable.

**Skill authoring rules:**

- Keep `SKILL.md` under 500 lines and roughly 5,000 tokens, per the [Agent Skills specification](https://github.com/agentskills/agentskills). Move detail into a `reference/` subfolder that the skill loads on demand.
- Put executable logic in a real `.ps1` beside `SKILL.md`, never in a fenced code block. Code in markdown gets **zero** PSScriptAnalyzer, formatter, and Pester coverage.
- Supported front matter keys are `name`, `description`, `license`, and `allowed-tools`. Any other key is silently ignored.
- Do **not** declare `allowed-tools: shell` on a skill that consumes untrusted input (debug logs, GitHub issue/PR content, downloaded release assets). Keeping it omitted preserves the human confirmation step against prompt injection.
- `.build/Pester.ps1` discovers skill tests at `.github/skills/<skill>/Tests/*.Tests.ps1` — put them beside the skill they test, not in a product folder. Contract tests for `.github/skill-lib/` live at `.github/skill-lib/Tests/` because those helpers are shared across skills.

## Personal Overrides

Expand Down Expand Up @@ -117,15 +131,15 @@ For formatting all changed files on a branch:
- Uses `cspell` (Node.js-based tool)
- Dictionary is in `.build/cspell-words.txt`
- Most spelling issues are caught by using proper camelCase or PascalCase variable names
- Note: `.github/` is excluded from spell checking via `.build/cspell.json`
- Note: `.build/cspell.json` lists `.github` in `ignorePaths`, but that pattern does **not** match descendants — `.github/**` files ARE spell-checked in practice. Add new words to `.build/cspell-words.txt` as normal.

### Testing (Pester)

```powershell
.build/Pester.ps1
```

- Runs all Pester tests (`*.Tests.ps1`) found recursively across the repository (excludes `.github/`)
- Runs all Pester tests (`*.Tests.ps1`) found recursively across the repository, including skill tests (excludes only `.github/examples/`)
- Runs tests in parallel (auto-scales to CPU cores)
- To test only changed files: `.build/Pester.ps1 -Branch main`
- Tests use `BeforeAll` to dot-source scripts and set up the environment
Expand Down Expand Up @@ -222,6 +236,8 @@ Tests follow a **colocated pattern**: `*.Tests.ps1` files are placed in a `Tests
- `Setup/Tests/*.Tests.ps1` - Tests for Setup scripts
- `Diagnostics/HealthChecker/Tests/*.Tests.ps1` - Tests for HealthChecker
- `Admin/MonitorExchangeAuthCertificate/DataCollection/Tests/*.Tests.ps1` - Tests colocated near the script
- `.github/skills/<skill-name>/Tests/*.Tests.ps1` - Tests for agent skill helper scripts
- `.github/skill-lib/Tests/*.Tests.ps1` - Contract tests for the shared skill helpers (they are consumed by more than one skill, so they are not owned by any single skill's folder)

Basic test structure:

Expand Down Expand Up @@ -250,4 +266,3 @@ Key conventions:
- Store mock data in XML files in a `Tests/Data/` subfolder: `Import-Clixml $Script:parentPath\Tests\Data\MockData.xml`
- For complex multi-script testing (e.g., HealthChecker), create helper functions in `NotPublished.ps1` files (e.g., `HealthCheckerTests.ImportCode.NotPublished.ps1`)
- Load functions dynamically with `Get-PesterScriptContent` and `Invoke-Expression` when testing individual functions from large scripts

89 changes: 89 additions & 0 deletions .github/skill-lib/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# skill-lib

Shared filesystem-safety helpers dot-sourced by skills under `.github/skills/`.

This directory sits **beside** `.github/skills/`, not inside it, so the skill
loader does not attempt to discover a `SKILL.md` here. Each helper is a
single-purpose `.ps1` file with a documented contract at the top; skills
dot-source only the helpers they need.

## When to add a helper here

Extract a helper into this directory only when it is:

- **Byte-for-byte reusable** across two or more skills. If two callers need
different behavior, keep the helpers inline in each skill — divergent
copies with the same name are worse than duplication because they silently
break the "same call, same behavior" contract downstream.
- **Purely defensive filesystem plumbing** (path validation, reparse-point
detection, DOS-device probes, handle equality checks). Domain logic,
reporting helpers, and one-of-a-kind orchestration stay in the calling
skill.

## Consuming a helper

Skills dot-source with a `$PSScriptRoot`-relative path that walks up out of
`.github/skills/<skill-name>/` and back down into `.github/skill-lib/`:

```powershell
. $PSScriptRoot\..\..\skill-lib\Resolve-SkillLocalPath.ps1
```

The `analyze-debug-files` helpers — `Get-DebugFileMetadata.ps1`,
`Assert-BaselineTrusted.ps1`, `Get-BaselineDependencyGraph.ps1`, and
`Write-DebugAnalysisReport.ps1` — all use this same `$PSScriptRoot`-relative
form, because each is a real `.ps1` file where the variable is well-defined.

`Resolve-SkillLocalPath` owns the shared path policy and loads the two defensive
primitives itself. It returns a validated absolute path, or `$null` on refusal.
Callers must use that returned path rather than normalize the input again.
Relative paths resolve against PowerShell's current location. `-PathType Leaf`
requires an existing file; the default requires a directory. `-AllowMissing`
performs the same locality checks without requiring existence or an item type,
for paths that the caller will create.

Each helper's `.DESCRIPTION` carries a `Consumed by:` list. That list is not
decorative — `Tests/SkillLibContract.Tests.ps1` derives the real consumer set
from both the skills and this library, and fails when either the list or the "Skills that depend
on this directory" section below falls out of sync.

## Repository-only: these skills are not independently distributable

Because the helpers live **outside** each skill's own directory, the
`.github/skills/<skill-name>/` folders are **not** self-contained. The Agent
Skills loader discovers and bundles only the files inside a skill's directory,
so copying one of these skill folders on its own — or installing it with
`gh skill install` — produces a skill that fails at dot-source time.

Skills that depend on this directory:

- `.github/skills/analyze-debug-files/`
- `.github/skills/trace-code-introduction/`

These are **repository-scoped skills**, intended to run from a
`microsoft/CSS-Exchange` clone. That is a deliberate trade: a single canonical
copy of each safety helper is worth more than independent distributability,
because divergent copies of a path-validation routine fail silently and
dangerously. If a skill here ever needs to be distributed standalone, vendor the
helpers into its own directory at that point rather than loosening this rule.

`find-release-tag-for-script-version` intentionally carries equivalent inline
filesystem checks to remain self-contained. It is not a consumer of this
directory. When changing the canonical policy or its primitives, review that
standalone copy for the same edge case; do not silently make it depend on this
library.

## Add-Type namespace convention

Helpers that use `Add-Type` for P/Invoke declare types under the
`SkillLib.*` namespace and guard the declaration with an `-as [type]`
check so re-sourcing is a no-op:

```powershell
if (-not ('SkillLib.DosDeviceHelper' -as [type])) {
Add-Type -Namespace 'SkillLib' -Name 'DosDeviceHelper' -MemberDefinition ...
}
```

A single shared namespace means the P/Invoke types load once per PowerShell
session even when multiple skills consume the same helper.
Loading
Loading