Skip to content

Commit 82287da

Browse files
fix(stations/notify): skip unpublished source component
Co-Authored-By: Cursor <cursoragent@cursor.com>
1 parent 8970ff0 commit 82287da

2 files changed

Lines changed: 105 additions & 22 deletions

File tree

scripts/windows-native-acceptance.ps1

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -400,18 +400,26 @@ function Get-UnpublishedComponentIds {
400400
# pins real native assets. Those entries are the only components source
401401
# acceptance may skip: a component with declared assets must still install
402402
# and smoke, even if the report labels it "unsupported".
403+
#
404+
# Windows PowerShell 5.1 treats an empty JSON object as a truthy
405+
# PSCustomObject whose .PSObject.Properties.Count is unreliable unless the
406+
# Properties collection is forced through @(). Without that wrapper,
407+
# agent-notify's empty assets map is never classified as unpublished and the
408+
# post-setup health assertion fails with unsupported-component-platform.
403409
if (-not (Test-Path -LiteralPath $ManifestPath)) {
404410
throw "bundled compatibility manifest not found at $ManifestPath"
405411
}
406412
$manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
407-
$unpublished = @()
413+
$unpublished = [System.Collections.Generic.List[string]]::new()
408414
foreach ($property in $manifest.components.PSObject.Properties) {
409415
$assets = $property.Value.assets
410-
if (-not $assets -or $assets.PSObject.Properties.Count -eq 0) {
411-
$unpublished += $property.Name
416+
if ($null -eq $assets -or @($assets.PSObject.Properties).Count -eq 0) {
417+
$unpublished.Add([string]$property.Name)
412418
}
413419
}
414-
return $unpublished
420+
# Write-Output -NoEnumerate keeps a single empty-asset id as a one-element
421+
# string[] instead of unrolling to a scalar that foreach would iterate by char.
422+
Write-Output -NoEnumerate $unpublished.ToArray()
415423
}
416424

417425
function Assert-AllComponentsHealthy {
@@ -575,8 +583,20 @@ try {
575583
& brigade --version
576584
if ($LASTEXITCODE -ne 0) { throw "brigade --version failed" }
577585

586+
# Source-mode acceptance runs against the bundled compatibility manifest,
587+
# which carries not-yet-released components with empty assets. Compute that
588+
# empty-asset set before both setup invocations so online and offline setup
589+
# are only expected to install published components; post-setup health and
590+
# smoke assertions reuse the same set. Published/release acceptance never
591+
# skips anything and keeps bare setup (every published component).
592+
[string[]]$unpublishedIds = @()
578593
if ($InstallMode -eq "source") {
594+
$bundledManifestPath = Join-Path $RepoRoot "src\brigade\templates\components\manifest-v1.json"
595+
[string[]]$unpublishedIds = @(Get-UnpublishedComponentIds -ManifestPath $bundledManifestPath)
596+
579597
Write-Step "brigade setup (online)"
598+
# Standalone manifest + published_component_ids omits empty-asset entries
599+
# such as agent-notify; do not add flags that would request them.
580600
& brigade setup --manifest-source standalone
581601
if ($LASTEXITCODE -ne 0) { throw "brigade setup failed" }
582602

@@ -594,16 +614,6 @@ try {
594614
if ($LASTEXITCODE -ne 0) { throw "brigade setup --offline failed" }
595615
}
596616

597-
# Source-mode acceptance runs against the bundled compatibility manifest,
598-
# which carries not-yet-released components with empty assets. Only those
599-
# entries may be skipped; published components (declared assets) must still
600-
# install and smoke. Published/release acceptance never skips anything.
601-
$unpublishedIds = @()
602-
if ($InstallMode -eq "source") {
603-
$bundledManifestPath = Join-Path $RepoRoot "src\brigade\templates\components\manifest-v1.json"
604-
$unpublishedIds = Get-UnpublishedComponentIds -ManifestPath $bundledManifestPath
605-
}
606-
607617
$report = Get-ComponentReport -StderrRoot $acceptRoot
608618
Assert-AllComponentsHealthy -Report $report -Skippable $unpublishedIds
609619
$managedBin = Join-Path $env:LOCALAPPDATA "brigade\bin"

tests/test_ci_workflow.py

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,15 @@ def test_windows_native_acceptance_source_setup_uses_standalone_manifest_online_
317317

318318
source = re.search(r'if \(\$InstallMode -eq "source"\) \{(?P<body>.*?)\n \}', setup, re.DOTALL)
319319
assert source is not None
320-
assert "& brigade setup --manifest-source standalone" in source.group("body")
321-
assert "& brigade setup --offline --manifest-source standalone" in source.group("body")
320+
body = source.group("body")
321+
# Empty-asset ids are derived before either setup invocation so online and
322+
# offline share the published-only expectation set.
323+
unpublished_at = body.index("$unpublishedIds = @(Get-UnpublishedComponentIds -ManifestPath $bundledManifestPath)")
324+
online_at = body.index('Write-Step "brigade setup (online)"')
325+
offline_at = body.index('Write-Step "brigade setup --offline"')
326+
assert unpublished_at < online_at < offline_at
327+
assert "& brigade setup --manifest-source standalone" in body
328+
assert "& brigade setup --offline --manifest-source standalone" in body
322329

323330

324331
def test_windows_native_acceptance_pypi_setup_keeps_exact_manifest_default_and_digest_check():
@@ -541,7 +548,10 @@ def test_windows_native_acceptance_source_mode_skips_only_bundled_unpublished_co
541548
unpublished_fn = _extract_powershell_function(script, "Get-UnpublishedComponentIds")
542549
assert "function Get-UnpublishedComponentIds" in unpublished_fn
543550
assert "ConvertFrom-Json" in unpublished_fn
544-
assert "PSObject.Properties.Count -eq 0" in unpublished_fn
551+
# Force Properties through @(...) so Windows PowerShell 5.1 reports Count 0
552+
# for empty "assets": {} maps (bare .Count is unreliable and dropped agent-notify).
553+
assert "@($assets.PSObject.Properties).Count -eq 0" in unpublished_fn
554+
assert "Write-Output -NoEnumerate" in unpublished_fn
545555
# The skip set is read from the bundled manifest on disk, not from the
546556
# component report, so an unsupported component with declared assets is
547557
# never treated as skippable.
@@ -550,8 +560,20 @@ def test_windows_native_acceptance_source_mode_skips_only_bundled_unpublished_co
550560
main = script[script.index("$acceptRoot = $null") :]
551561
source_block = main[main.index('if ($InstallMode -eq "source") {') : main.index("$report = Get-ComponentReport")]
552562
assert 'Join-Path $RepoRoot "src\\brigade\\templates\\components\\manifest-v1.json"' in source_block
553-
assert "$unpublishedIds = Get-UnpublishedComponentIds -ManifestPath $bundledManifestPath" in source_block
554-
assert "$unpublishedIds = @" in main
563+
assert (
564+
"[string[]]$unpublishedIds = @(Get-UnpublishedComponentIds -ManifestPath $bundledManifestPath)" in source_block
565+
)
566+
# Empty-asset omission applies to both setup requests: ids are computed
567+
# before online and offline standalone setup, then reused for health/smoke.
568+
assert source_block.index(
569+
"[string[]]$unpublishedIds = @(Get-UnpublishedComponentIds -ManifestPath $bundledManifestPath)"
570+
) < source_block.index('Write-Step "brigade setup (online)"')
571+
assert source_block.index('Write-Step "brigade setup (online)"') < source_block.index(
572+
'Write-Step "brigade setup --offline"'
573+
)
574+
assert "& brigade setup --manifest-source standalone" in source_block
575+
assert "& brigade setup --offline --manifest-source standalone" in source_block
576+
assert "[string[]]$unpublishedIds = @()" in main
555577
assert "Assert-AllComponentsHealthy -Report $report -Skippable $unpublishedIds" in main
556578

557579
healthy_fn = _extract_powershell_function(script, "Assert-AllComponentsHealthy")
@@ -573,6 +595,52 @@ def test_windows_native_acceptance_source_mode_skips_only_bundled_unpublished_co
573595
assert "& $agentNotifyExe version --json" in agent_block
574596

575597

598+
def test_windows_native_acceptance_source_setup_command_contract_omits_empty_asset_components():
599+
"""Source-mode setup must request only published components: both online and
600+
offline use standalone manifest selection (which omits empty-asset entries
601+
via published_component_ids) after deriving that same empty-asset set for
602+
post-setup assertions. Nonempty published assets stay required."""
603+
script = (ROOT / "scripts/windows-native-acceptance.ps1").read_text()
604+
manifest = json.loads((ROOT / "src/brigade/templates/components/manifest-v1.json").read_text())
605+
unpublished = [component_id for component_id, record in manifest["components"].items() if not record.get("assets")]
606+
published = [component_id for component_id, record in manifest["components"].items() if record.get("assets")]
607+
assert unpublished == ["agent-notify"]
608+
assert "graphtrail" in published
609+
610+
main = script[script.index("$acceptRoot = $null") :]
611+
source_block = main[main.index('if ($InstallMode -eq "source") {') : main.index("$report = Get-ComponentReport")]
612+
online_cmd = "& brigade setup --manifest-source standalone"
613+
offline_cmd = "& brigade setup --offline --manifest-source standalone"
614+
assert source_block.count(online_cmd) == 1
615+
assert source_block.count(offline_cmd) == 1
616+
# No alternate manifest path or component-selection flag that could request
617+
# unpublished agent-notify or hide a published component failure.
618+
assert "--manifest " not in source_block
619+
assert "Get-UnpublishedComponentIds" in source_block
620+
assert "@($assets.PSObject.Properties).Count -eq 0" in _extract_powershell_function(
621+
script, "Get-UnpublishedComponentIds"
622+
)
623+
624+
625+
def test_windows_native_acceptance_pypi_setup_command_contract_is_strict():
626+
"""PyPI/released mode must issue bare setup online and offline (every
627+
published component) and never populate the empty-asset skip set."""
628+
script = (ROOT / "scripts/windows-native-acceptance.ps1").read_text()
629+
main = script[script.index("$acceptRoot = $null") :]
630+
setup = main[main.index("[string[]]$unpublishedIds = @()") : main.index("$report = Get-ComponentReport")]
631+
published = re.search(r"else \{(?P<body>.*?)\n \}", setup, re.DOTALL)
632+
assert published is not None
633+
body = published.group("body")
634+
assert re.search(r"(?m)^ & brigade setup$", body)
635+
assert re.search(r"(?m)^ & brigade setup --offline$", body)
636+
assert "--manifest-source" not in body
637+
assert "Get-UnpublishedComponentIds" not in body
638+
# Default empty skip set is only replaced inside the source branch.
639+
assert setup.index("[string[]]$unpublishedIds = @()") < setup.index('if ($InstallMode -eq "source") {')
640+
source_block = setup[setup.index('if ($InstallMode -eq "source") {') : setup.index("else {")]
641+
assert "Get-UnpublishedComponentIds" in source_block
642+
643+
576644
def test_windows_native_acceptance_pypi_mode_never_skips_agent_notify():
577645
"""Published/release acceptance keeps the strict five-component contract:
578646
no skip set, agent-notify must install, digest-check, and smoke by absolute
@@ -581,9 +649,12 @@ def test_windows_native_acceptance_pypi_mode_never_skips_agent_notify():
581649
main = script[script.index("$acceptRoot = $null") :]
582650

583651
# The skip set is only populated in source mode; pypi mode leaves it empty.
584-
source_block = main[main.index('if ($InstallMode -eq "source") {') : main.index("$report = Get-ComponentReport")]
585-
assert "$unpublishedIds = @()" in source_block
652+
setup = main[main.index("[string[]]$unpublishedIds = @()") : main.index("$report = Get-ComponentReport")]
653+
source_block = setup[setup.index('if ($InstallMode -eq "source") {') : setup.index("else {")]
654+
pypi_block = setup[setup.index("else {") :]
655+
assert "[string[]]$unpublishedIds = @()" in setup
586656
assert "Get-UnpublishedComponentIds" in source_block
657+
assert "Get-UnpublishedComponentIds" not in pypi_block
587658

588659
assert "Assert-ManagedComponentDigests -Manifest $releaseManifest -Report $report -ManagedBin $managedBin" in main
589660
digest_fn = _extract_powershell_function(script, "Assert-ManagedComponentDigests")
@@ -626,5 +697,7 @@ def test_windows_native_acceptance_pypi_mode_rejects_bare_agent_notify_metadata(
626697
# Source-mode skip behavior is preserved: the unpublished-ids derivation and
627698
# the required-ids gating are unchanged.
628699
source_block = main[main.index('if ($InstallMode -eq "source") {') : main.index("$report = Get-ComponentReport")]
629-
assert "$unpublishedIds = Get-UnpublishedComponentIds -ManifestPath $bundledManifestPath" in source_block
700+
assert (
701+
"[string[]]$unpublishedIds = @(Get-UnpublishedComponentIds -ManifestPath $bundledManifestPath)" in source_block
702+
)
630703
assert "Where-Object { $unpublishedIds -notcontains $_ }" in main

0 commit comments

Comments
 (0)