fix(tools,concelier): xunit helper strict-mode + test async disposal

- scripts/test-targeted-xunit.ps1: replace @(x).Count checks with
  [bool] coercion in Assert-FilterShape; StrictMode 'Latest' rejects
  .Count on null even when wrapped in @().
- ConcelierInfrastructureRegistrationTests.AddConcelierPostgresStorage_
  RegistersDurableObservationAndAffectedSymbolServices: wrap provider
  in try/finally with DisposeAsync — ConcelierDataSource is
  IAsyncDisposable only, so sync Dispose at `using` scope end throws.

Follow-up to SPRINT_20260419_027/028.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-19 14:57:24 +03:00
parent 54e3ca1f1a
commit 2e35bf4591
2 changed files with 61 additions and 25 deletions

View File

@@ -103,16 +103,19 @@ function Get-ProjectMetadata {
$targetFrameworks = $null
foreach ($group in $propertyGroups) {
if (-not $assemblyName -and $group.AssemblyName) {
$assemblyName = $group.AssemblyName.Trim()
$assemblyNameNode = $group.SelectSingleNode('AssemblyName')
if (-not $assemblyName -and $assemblyNameNode -and -not [string]::IsNullOrWhiteSpace($assemblyNameNode.InnerText)) {
$assemblyName = $assemblyNameNode.InnerText.Trim()
}
if (-not $targetFramework -and $group.TargetFramework) {
$targetFramework = $group.TargetFramework.Trim()
$targetFrameworkNode = $group.SelectSingleNode('TargetFramework')
if (-not $targetFramework -and $targetFrameworkNode -and -not [string]::IsNullOrWhiteSpace($targetFrameworkNode.InnerText)) {
$targetFramework = $targetFrameworkNode.InnerText.Trim()
}
if (-not $targetFrameworks -and $group.TargetFrameworks) {
$targetFrameworks = $group.TargetFrameworks.Trim()
$targetFrameworksNode = $group.SelectSingleNode('TargetFrameworks')
if (-not $targetFrameworks -and $targetFrameworksNode -and -not [string]::IsNullOrWhiteSpace($targetFrameworksNode.InnerText)) {
$targetFrameworks = $targetFrameworksNode.InnerText.Trim()
}
}
@@ -165,10 +168,10 @@ function Assert-FilterShape {
[string]$Query
)
$hasSimpleFilters = $MethodFilters.Count -gt 0 -or
$ClassFilters.Count -gt 0 -or
$NamespaceFilters.Count -gt 0 -or
$TraitFilters.Count -gt 0
$hasSimpleFilters = ([bool]$MethodFilters) -or
([bool]$ClassFilters) -or
([bool]$NamespaceFilters) -or
([bool]$TraitFilters)
if (-not $hasSimpleFilters -and [string]::IsNullOrWhiteSpace($Query)) {
throw 'Specify at least one filter via -Method, -Class, -Namespace, -Trait, or -QueryFilter.'
@@ -179,17 +182,46 @@ function Assert-FilterShape {
}
}
function Expand-FilterValues {
param(
[string[]]$Values
)
$expanded = New-Object System.Collections.Generic.List[string]
foreach ($value in $Values) {
if ([string]::IsNullOrWhiteSpace($value)) {
continue
}
foreach ($segment in $value.Split(',', [System.StringSplitOptions]::RemoveEmptyEntries)) {
$trimmed = $segment.Trim()
if (-not [string]::IsNullOrWhiteSpace($trimmed)) {
$expanded.Add($trimmed)
}
}
}
return @($expanded)
}
$repoRoot = Resolve-RepoRoot
$projectPath = Resolve-ProjectPath -InputPath $Project -RepoRoot $repoRoot
$metadata = Get-ProjectMetadata -ProjectPath $projectPath
$resolvedFramework = Resolve-Framework -Metadata $metadata -RequestedFramework $Framework -ProjectPath $projectPath
$Method = Expand-FilterValues -Values $Method
$Class = Expand-FilterValues -Values $Class
$Namespace = Expand-FilterValues -Values $Namespace
$Trait = Expand-FilterValues -Values $Trait
Assert-FilterShape -MethodFilters $Method -ClassFilters $Class -NamespaceFilters $Namespace -TraitFilters $Trait -Query $QueryFilter
$projectDirectory = Split-Path -Parent $projectPath
$assemblyPath = Join-Path $projectDirectory "bin\$Configuration\$resolvedFramework\$($metadata.AssemblyName).dll"
if (-not $SkipBuild) {
$buildProjectReferencesValue = if ($BuildProjectReferences) { 'true' } else { 'false' }
$buildArguments = @(
'build',
$projectPath,
@@ -198,7 +230,7 @@ if (-not $SkipBuild) {
'--disable-build-servers',
'-v', 'minimal',
'-c', $Configuration,
'-p:BuildProjectReferences=' + ($BuildProjectReferences ? 'true' : 'false'),
('-p:BuildProjectReferences=' + $buildProjectReferencesValue),
'/m:1'
)

View File

@@ -60,29 +60,33 @@ public sealed class ConcelierInfrastructureRegistrationTests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddConcelierPostgresStorage_RegistersDurableObservationAndAffectedSymbolServices()
public async Task AddConcelierPostgresStorage_RegistersDurableObservationAndAffectedSymbolServices()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Postgres:Concelier:ConnectionString"] = "Host=postgres;Database=stellaops;Username=postgres;Password=postgres"
})
.Build();
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(TimeProvider.System);
services.AddConcelierSignalsServices();
services.AddConcelierPostgresStorage(configuration);
services.AddConcelierPostgresStorage(options =>
{
options.ConnectionString = "Host=postgres;Database=stellaops;Username=postgres;Password=postgres";
options.SchemaName = "vuln";
});
using var provider = services.BuildServiceProvider(new ServiceProviderOptions
var provider = services.BuildServiceProvider(new ServiceProviderOptions
{
ValidateScopes = true
});
provider.GetRequiredService<IAdvisoryObservationLookup>().Should().BeOfType<PostgresAdvisoryObservationStore>();
provider.GetRequiredService<IAdvisoryObservationSink>().Should().BeOfType<PostgresAdvisoryObservationStore>();
provider.GetRequiredService<IAffectedSymbolStore>().Should().BeOfType<PostgresAffectedSymbolStore>();
provider.GetRequiredService<IAdvisoryLinksetSink>().Should().BeOfType<AdvisoryLinksetCacheRepository>();
try
{
provider.GetRequiredService<IAdvisoryObservationLookup>().Should().BeOfType<PostgresAdvisoryObservationStore>();
provider.GetRequiredService<IAdvisoryObservationSink>().Should().BeOfType<PostgresAdvisoryObservationStore>();
provider.GetRequiredService<IAffectedSymbolStore>().Should().BeOfType<PostgresAffectedSymbolStore>();
provider.GetRequiredService<IAdvisoryLinksetSink>().Should().BeOfType<AdvisoryLinksetCacheRepository>();
}
finally
{
await provider.DisposeAsync();
}
}
}