mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 06:36:55 +01:00
Merge main after Job Queue sample link fix
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76eb42c4-2acd-4f9c-a898-f4a44f9d46f7
This commit is contained in:
commit
0a018fd9fb
48 changed files with 1145 additions and 49 deletions
171
tools/Test-ReviewContract.ps1
Normal file
171
tools/Test-ReviewContract.ps1
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Validates the bounded leaf-range normalization contract.
|
||||
|
||||
.DESCRIPTION
|
||||
BCQuality has no executable findings-report consumer. These assertions keep
|
||||
the normative DO contract, AL coordinator, and standalone runner aligned
|
||||
while exercising the exact normalization predicate against representative
|
||||
safe and ambiguous inputs.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string] $Root = (Resolve-Path (Join-Path $PSScriptRoot '..'))
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Root = (Resolve-Path -LiteralPath $Root).Path
|
||||
|
||||
function Assert-True {
|
||||
param(
|
||||
[bool] $Condition,
|
||||
[string] $Message
|
||||
)
|
||||
|
||||
if (-not $Condition) {
|
||||
throw "Assertion failed: $Message"
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-Contains {
|
||||
param(
|
||||
[string] $Text,
|
||||
[string] $Expected,
|
||||
[string] $Message
|
||||
)
|
||||
|
||||
Assert-True $Text.Contains($Expected) $Message
|
||||
}
|
||||
|
||||
function Test-PositiveInteger {
|
||||
param([object] $Value)
|
||||
|
||||
if (($null -eq $Value) -or ($Value -is [bool]) -or ($Value -isnot [ValueType])) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$number = [double]$Value
|
||||
return [double]::IsFinite($number) -and ($number -gt 0) -and ([math]::Truncate($number) -eq $number)
|
||||
}
|
||||
|
||||
function Test-RangeNormalizationEligibility {
|
||||
param([pscustomobject] $Finding)
|
||||
|
||||
if ($Finding.PSObject.Properties.Name -contains 'suggested-code') {
|
||||
return $false
|
||||
}
|
||||
if (-not ($Finding.PSObject.Properties.Name -contains 'location')) {
|
||||
return $false
|
||||
}
|
||||
if (-not ($Finding.location.PSObject.Properties.Name -contains 'line')) {
|
||||
return $false
|
||||
}
|
||||
if (-not ($Finding.location.PSObject.Properties.Name -contains 'range')) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$range = $Finding.location.range
|
||||
if (-not ($range.PSObject.Properties.Name -contains 'start-line') -or
|
||||
-not ($range.PSObject.Properties.Name -contains 'end-line')) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$line = $Finding.location.line
|
||||
$startLine = $range.'start-line'
|
||||
$endLine = $range.'end-line'
|
||||
if (-not (Test-PositiveInteger $line) -or
|
||||
-not (Test-PositiveInteger $startLine) -or
|
||||
-not (Test-PositiveInteger $endLine)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
return ($startLine -le $line) -and ($line -le $endLine) -and ($startLine -ne $line)
|
||||
}
|
||||
|
||||
$transportSentence = 'Capture the exact Task return as the immutable raw audit payload and primary transport.'
|
||||
$doContract = Get-Content -LiteralPath (Join-Path $Root 'skills/do.md') -Raw
|
||||
$coordinatorContract = Get-Content -LiteralPath (Join-Path $Root 'microsoft/skills/review/al-code-review.md') -Raw
|
||||
$runnerContract = Get-Content -LiteralPath (Join-Path $Root 'docs/standalone-runner.md') -Raw
|
||||
|
||||
foreach ($surface in @(
|
||||
[pscustomobject]@{ Name = 'DO'; Text = ($doContract -replace '\s+', ' ') }
|
||||
[pscustomobject]@{ Name = 'AL coordinator'; Text = ($coordinatorContract -replace '\s+', ' ') }
|
||||
[pscustomobject]@{ Name = 'standalone runner'; Text = ($runnerContract -replace '\s+', ' ') }
|
||||
)) {
|
||||
Assert-Contains $surface.Text $transportSentence "$($surface.Name) preserves exact Task transport wording"
|
||||
}
|
||||
|
||||
$normalizedDoContract = $doContract -replace '\s+', ' '
|
||||
foreach ($expected in @(
|
||||
'positive integers',
|
||||
'start-line <= line <= end-line',
|
||||
'does not contain the `suggested-code` field',
|
||||
'remove only',
|
||||
'private run telemetry or artifacts',
|
||||
'Validate the entire normalized candidate',
|
||||
'If any other validation defect exists',
|
||||
'salvage arbitrary individual findings'
|
||||
)) {
|
||||
Assert-Contains $normalizedDoContract $expected "DO documents '$expected'"
|
||||
}
|
||||
|
||||
$cases = @(
|
||||
[pscustomobject]@{
|
||||
Name = 'contained mismatched range without suggested code'
|
||||
Expected = $true
|
||||
Finding = '{"message":"keep me","location":{"file":"src/codeunit.al","line":37,"range":{"start-line":36,"end-line":38}}}' | ConvertFrom-Json
|
||||
}
|
||||
[pscustomobject]@{
|
||||
Name = 'aligned range'
|
||||
Expected = $false
|
||||
Finding = '{"location":{"line":37,"range":{"start-line":37,"end-line":38}}}' | ConvertFrom-Json
|
||||
}
|
||||
[pscustomobject]@{
|
||||
Name = 'suggested code present'
|
||||
Expected = $false
|
||||
Finding = '{"location":{"line":37,"range":{"start-line":36,"end-line":38}},"suggested-code":""}' | ConvertFrom-Json
|
||||
}
|
||||
[pscustomobject]@{
|
||||
Name = 'line outside range'
|
||||
Expected = $false
|
||||
Finding = '{"location":{"line":39,"range":{"start-line":36,"end-line":38}}}' | ConvertFrom-Json
|
||||
}
|
||||
[pscustomobject]@{
|
||||
Name = 'reversed range'
|
||||
Expected = $false
|
||||
Finding = '{"location":{"line":37,"range":{"start-line":38,"end-line":36}}}' | ConvertFrom-Json
|
||||
}
|
||||
[pscustomobject]@{
|
||||
Name = 'zero bound'
|
||||
Expected = $false
|
||||
Finding = '{"location":{"line":1,"range":{"start-line":0,"end-line":2}}}' | ConvertFrom-Json
|
||||
}
|
||||
[pscustomobject]@{
|
||||
Name = 'fractional primary line'
|
||||
Expected = $false
|
||||
Finding = '{"location":{"line":37.5,"range":{"start-line":36,"end-line":38}}}' | ConvertFrom-Json
|
||||
}
|
||||
[pscustomobject]@{
|
||||
Name = 'missing end line'
|
||||
Expected = $false
|
||||
Finding = '{"location":{"line":37,"range":{"start-line":36}}}' | ConvertFrom-Json
|
||||
}
|
||||
)
|
||||
|
||||
foreach ($case in $cases) {
|
||||
$actual = Test-RangeNormalizationEligibility $case.Finding
|
||||
Assert-True ($actual -eq $case.Expected) "$($case.Name) eligibility is $($case.Expected)"
|
||||
}
|
||||
|
||||
$rawFinding = $cases[0].Finding
|
||||
$candidateFinding = $rawFinding | ConvertTo-Json -Depth 10 | ConvertFrom-Json
|
||||
$candidateFinding.location.PSObject.Properties.Remove('range')
|
||||
|
||||
Assert-True ($rawFinding.location.PSObject.Properties.Name -contains 'range') 'raw finding remains unchanged'
|
||||
Assert-True (-not ($candidateFinding.location.PSObject.Properties.Name -contains 'range')) 'candidate removes only the optional range'
|
||||
Assert-True ($candidateFinding.location.line -eq $rawFinding.location.line) 'candidate preserves the primary line'
|
||||
Assert-True ($candidateFinding.message -ceq $rawFinding.message) 'candidate preserves all other finding content'
|
||||
|
||||
Write-Output "Review contract validation passed ($($cases.Count) normalization cases)."
|
||||
|
|
@ -195,12 +195,42 @@ foreach ($domain in $leafDomains) {
|
|||
}
|
||||
|
||||
$override = if ($overrides.ContainsKey($domain)) { $overrides[$domain] } else { $null }
|
||||
$selectedArticle = $null
|
||||
if ($override -and ($override.PSObject.Properties.Name -contains 'article')) {
|
||||
$articleName = [string]$override.article
|
||||
$hasArticleOverride = $override -and ($override.PSObject.Properties.Name -contains 'article')
|
||||
$hasArticlesOverride = $override -and ($override.PSObject.Properties.Name -contains 'articles')
|
||||
if ($hasArticleOverride -and $hasArticlesOverride) {
|
||||
$problems.Add("${domain}: override must specify either 'article' or 'articles', not both.") | Out-Null
|
||||
continue
|
||||
}
|
||||
|
||||
$articleNames = @()
|
||||
if ($hasArticlesOverride) {
|
||||
$articleNames = @($override.articles)
|
||||
if (-not $articleNames.Count) {
|
||||
$problems.Add("${domain}: override 'articles' must contain at least one article.") | Out-Null
|
||||
continue
|
||||
}
|
||||
} elseif ($hasArticleOverride) {
|
||||
$articleNames = @($override.article)
|
||||
} else {
|
||||
$articleNames = @($articles | Select-Object -First 1 | ForEach-Object BaseName)
|
||||
}
|
||||
|
||||
$selectedArticles = [System.Collections.Generic.List[object]]::new()
|
||||
$seenArticleNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($articleNameValue in $articleNames) {
|
||||
if ($articleNameValue -isnot [string] -or [string]::IsNullOrWhiteSpace([string]$articleNameValue)) {
|
||||
$problems.Add("${domain}: override article names must be non-empty strings.") | Out-Null
|
||||
continue
|
||||
}
|
||||
$articleName = [string]$articleNameValue
|
||||
if ($articleName.EndsWith('.md')) {
|
||||
$articleName = [System.IO.Path]::GetFileNameWithoutExtension($articleName)
|
||||
}
|
||||
if (-not $seenArticleNames.Add($articleName)) {
|
||||
$problems.Add("${domain}: override contains duplicate article: $articleName.md") | Out-Null
|
||||
continue
|
||||
}
|
||||
|
||||
$selectedArticle = $articles | Where-Object BaseName -eq $articleName | Select-Object -First 1
|
||||
if (-not $selectedArticle) {
|
||||
$articleExists = @(
|
||||
|
|
@ -218,32 +248,41 @@ foreach ($domain in $leafDomains) {
|
|||
}
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
$selectedArticle = $articles | Select-Object -First 1
|
||||
$selectedArticles.Add($selectedArticle) | Out-Null
|
||||
}
|
||||
if (-not $selectedArticle) {
|
||||
$problems.Add("${domain}: no article has both .good.al and .bad.al companion samples.") | Out-Null
|
||||
if (-not $selectedArticles.Count) {
|
||||
if (-not $articleNames.Count) {
|
||||
$problems.Add("${domain}: no article has both .good.al and .bad.al companion samples.") | Out-Null
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
$articlePath = [string]$selectedArticle.ArticlePath
|
||||
$sampleDirectory = (Split-Path -Parent $articlePath).Replace('\', '/')
|
||||
$context = if ($override -and ($override.PSObject.Properties.Name -contains 'context')) {
|
||||
[string]$override.context
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
foreach ($kind in 'bad', 'good') {
|
||||
$case = [pscustomobject]@{
|
||||
id = "$domain-$kind"
|
||||
domain = $domain
|
||||
input = "$sampleDirectory/$($selectedArticle.BaseName).$kind.al"
|
||||
expected = if ($kind -eq 'bad') { @($articlePath) } else { @() }
|
||||
for ($articleIndex = 0; $articleIndex -lt $selectedArticles.Count; $articleIndex++) {
|
||||
$selectedArticle = $selectedArticles[$articleIndex]
|
||||
$articlePath = [string]$selectedArticle.ArticlePath
|
||||
$sampleDirectory = (Split-Path -Parent $articlePath).Replace('\', '/')
|
||||
foreach ($kind in 'bad', 'good') {
|
||||
$caseId = if ($articleIndex -eq 0) {
|
||||
"$domain-$kind"
|
||||
} else {
|
||||
"$domain-$($selectedArticle.BaseName)-$kind"
|
||||
}
|
||||
$case = [pscustomobject]@{
|
||||
id = $caseId
|
||||
domain = $domain
|
||||
input = "$sampleDirectory/$($selectedArticle.BaseName).$kind.al"
|
||||
expected = if ($kind -eq 'bad') { @($articlePath) } else { @() }
|
||||
}
|
||||
if ($context) {
|
||||
$case | Add-Member -NotePropertyName context -NotePropertyValue $context
|
||||
}
|
||||
$caseList.Add($case) | Out-Null
|
||||
}
|
||||
if ($context) {
|
||||
$case | Add-Member -NotePropertyName context -NotePropertyValue $context
|
||||
}
|
||||
$caseList.Add($case) | Out-Null
|
||||
}
|
||||
}
|
||||
$cases = @($caseList)
|
||||
|
|
@ -434,6 +473,7 @@ if ($PrepareDirectory) {
|
|||
}
|
||||
|
||||
if (-not $ResultsPath -and -not $ResultsDirectory) {
|
||||
& (Join-Path $PSScriptRoot 'Test-ReviewContract.ps1') -Root $Root
|
||||
Write-Host "Review fixture validation PASSED: $($cases.Count) cases cover $($leafDomains.Count) leaf domains." -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue