Skip to content

Commit 6f363b9

Browse files
OgeonX-AiAitomatesclaude
authored
fix(audit): resolve linting and syntax issues, update actions (#10)
* fix(audit): resolve linting and syntax issues, update actions * test: add Pester unit suite for operator safety and gh payload logic Cover the highest-blast-radius logic in the org-mutation engine: - Assert-SafeChangeSet sensitive-path and diff-budget guards - Get-ChangedFile porcelain parsing (renames, quoting, dedupe) - Search-Issue GraphQL request construction - Autopilot.Common helpers (Get-RepoName, Invoke-GhJson, Get-LogTail) Operator functions are lifted via AST so no live gh/git runs on load. Add tests/run-tests.ps1 single-command runner and wire Pester into CI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Kim Harjamäki <kim.harjamaki@prosimo.fi> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 60fe373 commit 6f363b9

10 files changed

Lines changed: 364 additions & 50 deletions

File tree

.github/workflows/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,14 @@ jobs:
2525
- name: Validate control-plane contracts
2626
shell: pwsh
2727
run: ./tests/contract-tests.ps1
28+
29+
- name: Run Pester unit tests
30+
shell: pwsh
31+
run: |
32+
Set-PSRepository PSGallery -InstallationPolicy Trusted
33+
Install-Module Pester -MinimumVersion 5.5.0 -Scope CurrentUser -Force -SkipPublisherCheck
34+
$config = New-PesterConfiguration
35+
$config.Run.Path = './tests'
36+
$config.Run.Exit = $true
37+
$config.Output.Verbosity = 'Detailed'
38+
Invoke-Pester -Configuration $config

.github/workflows/codeql.yml

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
name: "CodeQL"
22
on:
33
push:
4-
branches: [ "main", "master" ]
4+
branches: ["main", "master"]
55
pull_request:
6-
branches: [ "main", "master" ]
6+
branches: ["main", "master"]
77
jobs:
88
analyze:
99
name: Analyze
@@ -15,15 +15,15 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
language: [ 'python' ]
18+
language: ['python']
1919
steps:
20-
- name: Checkout repository
21-
uses: actions/checkout@v4
22-
- name: Initialize CodeQL
23-
uses: github/codeql-action/init@v3
24-
with:
25-
languages: ${{ matrix.language }}
26-
- name: Autobuild
27-
uses: github/codeql-action/autobuild@v3
28-
- name: Perform CodeQL Analysis
29-
uses: github/codeql-action/analyze@v3
20+
- name: Checkout repository
21+
uses: actions/checkout@v6
22+
- name: Initialize CodeQL
23+
uses: github/codeql-action/init@v3
24+
with:
25+
languages: ${{ matrix.language }}
26+
- name: Autobuild
27+
uses: github/codeql-action/autobuild@v3
28+
- name: Perform CodeQL Analysis
29+
uses: github/codeql-action/analyze@v3

.github/workflows/pages.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
build:
1616
runs-on: ubuntu-latest
1717
steps:
18-
- uses: actions/checkout@v4
18+
- uses: actions/checkout@v6
1919
- uses: actions/setup-python@v5
2020
with:
2121
python-version: 3.x

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,25 @@ flowchart LR
5656
- Minimal diffs only - no secrets, no destructive operations.
5757
- Required supported verification before PR creation, with explicit approved exceptions only.
5858

59+
## Testing
60+
61+
Run the full suite (workflow YAML validation, control-plane contract tests, and
62+
Pester unit tests) with a single command:
63+
64+
```powershell
65+
pwsh ./tests/run-tests.ps1
66+
```
67+
68+
Unit tests (Pester 5) cover the safety- and payload-critical logic:
69+
`Assert-SafeChangeSet` (sensitive-path and diff-budget guards), `Get-ChangedFile`
70+
(porcelain parsing), `Search-Issue` (GraphQL request construction), and the
71+
`Autopilot.Common` helpers (`Get-RepoName`, `Invoke-GhJson`, `Get-LogTail`).
72+
5973
## Workflows
6074

6175
| Workflow | Trigger | Purpose |
6276
|----------|---------|---------|
63-
| `ci.yml` | push/PR to main | Portfolio CI - YAML validation (ubuntu-latest) |
77+
| `ci.yml` | push/PR to main | Portfolio CI - YAML validation + Pester unit tests (ubuntu-latest) |
6478
| `autopilot-operator.yml` | schedule + dispatch | Core operator - scan issues, run Codex, open PRs |
6579
| `autopilot-org-installer.yml` | hourly + dispatch | Install intake workflow into opted-in repos |
6680
| `autopilot-create-issue.yml` | workflow_run failure | Create intake issue when monitored workflow fails |

scripts/autopilot-operator.ps1

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ Initialize-Log
88
Assert-Env -Name "ORG"
99
$org = $env:ORG
1010

11-
$maxIssues = [int]($env:MAX_ISSUES ?? 5)
12-
$dryRun = ($env:DRY_RUN ?? "false") -eq "true"
13-
$allowUnverified = ($env:ALLOW_UNVERIFIED ?? "false") -eq "true"
11+
$maxIssues = if ($env:MAX_ISSUES) { [int]$env:MAX_ISSUES } else { 5 }
12+
$dryRun = if ($env:DRY_RUN) { $env:DRY_RUN -eq "true" } else { $false }
13+
$allowUnverified = if ($env:ALLOW_UNVERIFIED) { $env:ALLOW_UNVERIFIED -eq "true" } else { $false }
1414
$allowlist = @()
1515
if ($env:REPO_ALLOWLIST) {
1616
$allowlist = $env:REPO_ALLOWLIST.Split(",") | ForEach-Object { $_.Trim() } | Where-Object { $_ }
@@ -23,7 +23,7 @@ Test-Tool -Name "codex"
2323
Write-Log "Autopilot operator starting for org: $org"
2424
Write-Log "Max issues: $maxIssues Dry run: $dryRun"
2525

26-
function Get-ChangedFiles {
26+
function Get-ChangedFile {
2727
$paths = @()
2828
foreach ($line in @(git status --porcelain)) {
2929
if (-not $line -or $line.Length -lt 4) { continue }
@@ -56,7 +56,7 @@ function Assert-SafeChangeSet {
5656
if ($changedLines -gt $MaxLines) { throw "Change set has $changedLines changed lines; limit is $MaxLines." }
5757
}
5858

59-
function Search-Issues {
59+
function Search-Issue {
6060
param([string]$SearchQuery, [int]$First)
6161
$gql = @'
6262
query($q:String!, $first:Int!) {
@@ -83,7 +83,7 @@ query($q:String!, $first:Int!) {
8383

8484
$issues = @()
8585
$query = "org:$org is:issue label:autofix label:queued -label:blocked -label:risky -label:needs-design -label:try-3"
86-
$issues += Search-Issues -SearchQuery $query -First $maxIssues
86+
$issues += Search-Issue -SearchQuery $query -First $maxIssues
8787

8888
if (-not $issues -or $issues.Count -eq 0) {
8989
Write-Log "No issues found."
@@ -117,7 +117,8 @@ foreach ($issue in $issues) {
117117
continue
118118
}
119119

120-
$attempt = 1 if ($existingLabels -contains "try-2") { $attempt = 3 }
120+
$attempt = 1
121+
if ($existingLabels -contains "try-2") { $attempt = 3 }
121122
elseif ($existingLabels -contains "try-1") { $attempt = 2 }
122123
$attemptLabel = $attemptLabels[$attempt - 1]
123124

@@ -234,9 +235,9 @@ foreach ($issue in $issues) {
234235
continue
235236
}
236237

237-
$filesChanged = @(Get-ChangedFiles)
238-
$maxChangedFiles = [int]($env:MAX_CHANGED_FILES ?? 20)
239-
$maxChangedLines = [int]($env:MAX_CHANGED_LINES ?? 1000)
238+
$filesChanged = @(Get-ChangedFile)
239+
$maxChangedFiles = if ($env:MAX_CHANGED_FILES) { [int]$env:MAX_CHANGED_FILES } else { 20 }
240+
$maxChangedLines = if ($env:MAX_CHANGED_LINES) { [int]$env:MAX_CHANGED_LINES } else { 1000 }
240241
Assert-SafeChangeSet -Paths $filesChanged -MaxFiles $maxChangedFiles -MaxLines $maxChangedLines
241242

242243
$verification = "skipped"

scripts/lib/Autopilot.Common.psm1

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ function Write-Log {
22
param([string]$Message, [string]$Level = "INFO")
33
$timestamp = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ")
44
$line = "[$timestamp] [$Level] $Message"
5-
Write-Host $line
5+
Write-Output $line
66
if ($script:LogFile) {
77
Add-Content -Path $script:LogFile -Value $line
88
}
@@ -24,8 +24,8 @@ function Assert-Env {
2424
}
2525

2626
function Invoke-Gh {
27-
param([string[]]$Args)
28-
$cmd = @("gh") + $Args
27+
param([string[]]$Arguments)
28+
$cmd = @("gh") + $Arguments
2929
Write-Log "Running: $($cmd -join ' ')"
3030
& $cmd
3131
if ($LASTEXITCODE -ne 0) {
@@ -34,8 +34,8 @@ function Invoke-Gh {
3434
}
3535

3636
function Invoke-GhJson {
37-
param([string[]]$Args)
38-
$json = gh @Args 2>$null
37+
param([string[]]$Arguments)
38+
$json = gh @Arguments 2>$null
3939
if (-not $json) { return $null }
4040
$trimmed = $json.Trim()
4141
$objIndex = $trimmed.IndexOf('{')
@@ -62,21 +62,21 @@ function Test-Tool {
6262
}
6363

6464
function Invoke-Checked {
65-
param([string]$Command, [string[]]$Args = @())
66-
Write-Log "Running: $Command $($Args -join ' ')"
67-
& $Command @Args
65+
param([string]$Command, [string[]]$Arguments = @())
66+
Write-Log "Running: $Command $($Arguments -join ' ')"
67+
& $Command @Arguments
6868
if ($LASTEXITCODE -ne 0) {
6969
throw "Command failed: $Command (exit $LASTEXITCODE)"
7070
}
7171
}
7272

7373
function Invoke-CheckedLogged {
74-
param([string]$Command, [string[]]$Args = @(), [string]$LogPath)
75-
Write-Log "Running: $Command $($Args -join ' ')"
74+
param([string]$Command, [string[]]$Arguments = @(), [string]$LogPath)
75+
Write-Log "Running: $Command $($Arguments -join ' ')"
7676
if ($LogPath) {
77-
& $Command @Args 2>&1 | Tee-Object -FilePath $LogPath -Append
77+
& $Command @Arguments 2>&1 | Tee-Object -FilePath $LogPath -Append
7878
} else {
79-
& $Command @Args
79+
& $Command @Arguments
8080
}
8181
if ($LASTEXITCODE -ne 0) {
8282
throw "Command failed: $Command (exit $LASTEXITCODE)"

tests/Autopilot.Common.Tests.ps1

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#requires -Modules Pester
2+
3+
# Unit tests for the pure/deterministic helper functions in
4+
# scripts/lib/Autopilot.Common.psm1. These functions build gh payloads,
5+
# normalise repo names, and parse tool output — the highest-value logic to
6+
# pin down because the operator mutates OTHER repos based on their results.
7+
8+
BeforeAll {
9+
$script:RepoRoot = Split-Path -Parent $PSScriptRoot
10+
$script:ModulePath = Join-Path $RepoRoot "scripts/lib/Autopilot.Common.psm1"
11+
Import-Module $ModulePath -Force
12+
}
13+
14+
AfterAll {
15+
Remove-Module Autopilot.Common -Force -ErrorAction SilentlyContinue
16+
}
17+
18+
Describe "Get-RepoName" {
19+
It "extracts owner/repo from an https clone URL" {
20+
Get-RepoName -RepoUrl "https://github.com/acme/widgets" | Should -Be "acme/widgets"
21+
}
22+
23+
It "tolerates a trailing slash" {
24+
Get-RepoName -RepoUrl "https://github.com/acme/widgets/" | Should -Be "acme/widgets"
25+
}
26+
27+
It "handles an ssh-style URL by taking the final two path segments" {
28+
Get-RepoName -RepoUrl "https://github.com/acme/deep/nested/widgets" | Should -Be "nested/widgets"
29+
}
30+
}
31+
32+
Describe "Invoke-GhJson" {
33+
Context "when gh returns clean JSON" {
34+
BeforeEach {
35+
Mock -CommandName gh -ModuleName Autopilot.Common -MockWith { '{"login":"octocat","type":"User"}' }
36+
}
37+
38+
It "parses a JSON object into a PSCustomObject" {
39+
$result = Invoke-GhJson -Arguments @("api", "user")
40+
$result.login | Should -Be "octocat"
41+
$result.type | Should -Be "User"
42+
}
43+
}
44+
45+
Context "when gh emits a leading warning line before the JSON" {
46+
BeforeEach {
47+
# gh sometimes prints noise on stdout before the payload; the
48+
# function must locate the first { or [ and parse from there.
49+
Mock -CommandName gh -ModuleName Autopilot.Common -MockWith { "Warning: something`n[{""number"":7}]" }
50+
}
51+
52+
It "strips the prefix and parses the array" {
53+
$result = Invoke-GhJson -Arguments @("api", "issues")
54+
$result[0].number | Should -Be 7
55+
}
56+
}
57+
58+
Context "when gh returns nothing" {
59+
BeforeEach {
60+
Mock -CommandName gh -ModuleName Autopilot.Common -MockWith { $null }
61+
}
62+
63+
It "returns null without throwing" {
64+
Invoke-GhJson -Arguments @("api", "nothing") | Should -BeNullOrEmpty
65+
}
66+
}
67+
}
68+
69+
Describe "Get-LogTail" {
70+
It "returns null when the log file does not exist" {
71+
Get-LogTail -LogPath (Join-Path $TestDrive "missing.log") -Lines 5 | Should -BeNullOrEmpty
72+
}
73+
74+
It "returns only the last N lines of an existing log" {
75+
$log = Join-Path $TestDrive "sample.log"
76+
1..10 | ForEach-Object { "line $_" } | Set-Content -Path $log
77+
$tail = Get-LogTail -LogPath $log -Lines 3
78+
$tail | Should -HaveCount 3
79+
$tail[-1] | Should -Be "line 10"
80+
$tail[0] | Should -Be "line 8"
81+
}
82+
}

0 commit comments

Comments
 (0)