diff --git a/CLAUDE.md b/CLAUDE.md index d217a8a..d651428 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,6 +27,23 @@ git push -u origin feat/my-feature gh pr create ``` +### Agent pushes go through the GitHub MCP (operator decision 2026-09-01) + +Agent-authored branches are pushed with the `github` MCP tools (`create_branch` + +`push_files`, committing as the `goodolclint-claude` App), not local `git push` over the +operator's SSH key. Local git stays for everything else — branches, commits, diffs; only +the push goes through the API. `push_files` creates its own commit from full file +contents, so the commit message is passed to the tool and no `Co-Authored-By` trailer is +needed. + +Every API push is byte-verified before the PR: commit the identical change locally, +`git fetch`, and `git diff origin/ --` must be empty. `push_files` +cannot express a removal — use `delete_file` for deletions, and a rename is `push_files` +of the new path plus `delete_file` of the old. + +**Exception: `.github/workflows/*`.** The App token has no `workflows` permission, so +GitHub refuses those pushes; they stay on operator-attended local `git push`. + ### Dev container (recommended) A Docker-based dev environment replicates the full CI setup locally. Works on ARM Macs diff --git a/tests/PSProxmoxVE.Tests/Integration/16_Cluster.Tests.ps1 b/tests/PSProxmoxVE.Tests/Integration/16_Cluster.Tests.ps1 index d982d70..26c729e 100644 --- a/tests/PSProxmoxVE.Tests/Integration/16_Cluster.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Integration/16_Cluster.Tests.ps1 @@ -204,9 +204,13 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' { $script:NodeBName | Should -Not -BeNullOrEmpty } - It 'Get-PveClusterStatus shows 2 nodes online' { + It 'Get-PveClusterStatus shows node B online by name' { if (Skip-IfNoNodeB) { return } if (Skip-IfNoCluster) { return } + if (-not $script:NodeBName) { + Set-ItResult -Skipped -Because 'Node B name was not discovered' + return + } # Corosync membership reaches the status endpoint seconds after # the join task completes — poll rather than assert instantly. @@ -214,8 +218,8 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' { do { $status = Get-PveClusterStatus -ErrorAction Stop $nodeEntries = @($status) | Where-Object { $_.Type -eq 'node' } - $onlineNodes = @($nodeEntries) | Where-Object { $_.Online -eq 1 } - if (@($onlineNodes).Count -ge 2) { break } + $nodeB = @($nodeEntries) | Where-Object { $_.Name -eq $script:NodeBName } | Select-Object -First 1 + if ($nodeB -and $nodeB.Online -eq 1) { break } Start-Sleep -Seconds 3 } while ([DateTime]::UtcNow -lt $deadline) @@ -225,8 +229,45 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' { Write-Host " node=$($n.Name) nodeid=$($n.NodeId) ring0=$($n.Ip) online=$($n.Online) local=$($n.Local)" } - @($nodeEntries).Count | Should -BeGreaterOrEqual 2 - @($onlineNodes).Count | Should -BeGreaterOrEqual 2 + $nodeB | Should -Not -BeNullOrEmpty -Because "node A's cluster status should list $($script:NodeBName)" + $nodeB.Online | Should -Be 1 -Because "node A should see $($script:NodeBName) online" + } + + It 'Node B reports itself as part of the cluster' { + if (Skip-IfNoNodeB) { return } + if (Skip-IfNoCluster) { return } + if (-not $script:NodeBName) { + Set-ItResult -Skipped -Because 'Node B name was not discovered' + return + } + + # Node A seeing node B is not the same claim as node B believing it + # joined: in the #93 failure node B's pmxcfs stayed in local mode and + # reported online=0 while corosync membership looked healthy. + $secPw = ConvertTo-SecureString $script:PasswordB -AsPlainText -Force + $credB = New-Object System.Management.Automation.PSCredential('root@pam', $secPw) + Connect-PveServer ` + -Server $script:HostB ` + -Port $script:Port ` + -Credential $credB ` + -SkipCertificateCheck + + try { + $statusB = Get-PveClusterStatus -ErrorAction Stop + $clusterB = @($statusB) | Where-Object { $_.Type -eq 'cluster' } | Select-Object -First 1 + $selfB = @($statusB) | + Where-Object { $_.Type -eq 'node' -and $_.Name -eq $script:NodeBName } | + Select-Object -First 1 + + Write-Host "node B's own view: cluster=$($clusterB.Name) quorate=$($clusterB.Quorate) self-online=$($selfB.Online) self-local=$($selfB.Local)" + + $clusterB | Should -Not -BeNullOrEmpty -Because "$($script:NodeBName) should report a cluster entry, not local mode" + $selfB | Should -Not -BeNullOrEmpty -Because "$($script:NodeBName) should list itself as a cluster node" + $selfB.Online | Should -Be 1 -Because "$($script:NodeBName) should consider itself online in the cluster" + } + finally { + Connect-TestPve + } } }