Files
Bo 17849bbc24 Improve CLI checkpoint recovery, evidence status, and skill search (#1120)
A failed mining-checkpoint write could truncate the saved watermark and
cause retry to replay older events. The CLI also wrote evidence to
external roots that status could not inspect. This batch fixes those
behaviors and removes duplicate normalization from skill search.

- Mining checkpoints use the existing atomic storage writer. A real
partial-write regression test proves old bytes survive and retry retains
stable event IDs. Existing mode bits are preserved; new checkpoints use
0600. Symlink and special-file destinations are rejected before reading.
Before replacement, an empty same-directory probe checks ownership and
permission metadata, including ACLs and inherited permissions.
Unverifiable or different metadata returns an error and leaves the prior
checkpoint intact. This is a conservative refusal, not ACL migration. A
directory-sync error after rename can leave the new state visible.
- `ao status --evidence-root PATH` inspects an explicit existing non-Git
store, with matching text/JSON/YAML reports, no fallback on invalid
roots, and no reads through evidence symlinks. Omitted-flag behavior
remains unchanged.
- Skill-query normalization has one implementation, preserving
repetition versus first-occurrence semantics. Nine fixed shipped-catalog
queries remain byte-identical against a source-pinned baseline.

Validation: Go build, vet, tests, race/shuffle with atomic coverage,
repository Bats and aggregate suites, regeneration, applicable gates and
lint passed. Final Linux and Windows correctness CI and all required
checks passed. A fresh author-distinct review verified every acceptance
criterion across all 23 changed paths with no unchecked scope. Nine
production-query outputs match the source-pinned baseline.

The first CI attempt exposed a test-child coverage flush under its
temporary file-size limit; the test now restores that limit before exit.
Fresh review then exposed ACL loss despite green CI. The permission
guard and native regression tests repair that defect while preserving
the original access requirement. The failure cases and repair costs are
retained in the evaluation.
2026-09-09 18:48:58 -04:00

239 lines
8.9 KiB
PowerShell

# test-windows-smoke.ps1 - Native Windows smoke tests for AgentOps installers and ao.
#
# Stabilization (soc-92ka, 2026-05-03):
# - Set-StrictMode -Version Latest enforced (catches uninitialized vars,
# out-of-bounds indexing, unset properties at runtime instead of producing
# silent empty strings that flake downstream).
# - All path construction uses Join-Path or
# [System.IO.Path]::DirectorySeparatorChar; no hard-coded forward slashes
# and no embedded backslash separators in path segments.
# - Nested PowerShell invocations re-use the current $PSHOME pwsh (or
# fall back to powershell.exe) to avoid execution-policy / host drift
# between Windows PowerShell 5.1 and PowerShell 7+.
#
# POSIX-symlink-dependent checks: SKIPPED on Windows.
# This script intentionally does NOT exercise checks that require POSIX
# symlink semantics. Windows GitHub runners do not create symlinks during
# `actions/checkout@v6` by default, and the repo policy bans symlinks
# anyway (see CLAUDE.md "No symlinks. Ever."). Any future check that
# relies on a POSIX symlink (e.g. `Test-Path -PathType SymbolicLink`,
# `(Get-Item).LinkType -eq 'SymbolicLink'`, `readlink`-style traversal,
# or symlink-aware copy/sync) MUST be guarded by `if (-not $IsWindows)`
# or omitted from this script. The canonical symlink audit lives in
# the Linux-only `plugin-load-test` job, not here.
[CmdletBinding()]
param(
[string]$RepoRoot
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
# ---------------------------------------------------------------------------
# Path helpers
# ---------------------------------------------------------------------------
# Resolve a chain of segments into an absolute path using Join-Path so the
# host's [System.IO.Path]::DirectorySeparatorChar is always honored. Accepts
# any number of trailing segments.
function Join-PathSegments {
param(
[Parameter(Mandatory = $true)][string]$Base,
[Parameter(ValueFromRemainingArguments = $true)][string[]]$Segments
)
$current = $Base
foreach ($segment in $Segments) {
if ([string]::IsNullOrEmpty($segment)) { continue }
$current = Join-Path -Path $current -ChildPath $segment
}
return $current
}
if ([string]::IsNullOrWhiteSpace($RepoRoot)) {
$scriptRoot = if ($PSScriptRoot) {
$PSScriptRoot
} else {
Split-Path -Parent $MyInvocation.MyCommand.Path
}
$repoRootCandidate = Join-PathSegments -Base $scriptRoot -Segments '..', '..'
$RepoRoot = (Resolve-Path -LiteralPath $repoRootCandidate).Path
}
# Pin the PowerShell host used for nested installer runs. Prefer the same
# host this script is running under (`pwsh` on CI via `shell: pwsh`);
# fall back to Windows PowerShell only if pwsh is unavailable. Mixing
# hosts has been a recurring flake source on Windows runners.
$nestedPSHost = $null
$pshExe = Get-Command -Name 'pwsh' -ErrorAction SilentlyContinue
if ($pshExe) {
$nestedPSHost = $pshExe.Source
} else {
$winPS = Get-Command -Name 'powershell' -ErrorAction SilentlyContinue
if ($winPS) {
$nestedPSHost = $winPS.Source
} else {
throw "Neither pwsh nor powershell is available on PATH"
}
}
function Write-Step {
param([string]$Message)
Write-Host "==> $Message"
}
function Test-PowerShellSyntax {
param([string]$Path)
$errors = $null
$null = [System.Management.Automation.PSParser]::Tokenize((Get-Content -Raw -LiteralPath $Path), [ref]$errors)
if ($errors) {
$errors | ForEach-Object { Write-Error $_ }
throw "PowerShell syntax check failed: $Path"
}
}
function Invoke-GoTest {
param([string[]]$TestArgs)
$cliDir = Join-PathSegments -Base $RepoRoot -Segments 'cli'
Push-Location -LiteralPath $cliDir
try {
& go test @TestArgs
}
finally {
Pop-Location
}
if ($LASTEXITCODE -ne 0) {
throw "go test failed: $($TestArgs -join ' ')"
}
}
function Invoke-NestedScript {
param(
[Parameter(Mandatory = $true)][string]$ScriptPath,
[string[]]$ScriptArgs
)
$argList = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $ScriptPath)
if ($ScriptArgs) {
$argList += $ScriptArgs
}
& $nestedPSHost @argList
}
function Remove-IfExists {
param([string]$Path)
if ($Path -and (Test-Path -LiteralPath $Path)) {
Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue
}
}
# ---------------------------------------------------------------------------
# 1. PowerShell installer syntax
# ---------------------------------------------------------------------------
Write-Step "Checking PowerShell installer syntax"
Test-PowerShellSyntax (Join-PathSegments -Base $RepoRoot -Segments 'scripts', 'install-ao.ps1')
$codexTombstone = Join-PathSegments -Base $RepoRoot -Segments 'scripts', 'install-codex.ps1'
Test-PowerShellSyntax $codexTombstone
$tombstoneText = Get-Content -Raw -LiteralPath $codexTombstone
if ($tombstoneText -notmatch 'removed in 3\.3' -or $tombstoneText -notmatch 'ao skills link') {
throw "install-codex.ps1 must remain an AgentOps 3.3 tombstone pointing at ao skills link"
}
# ---------------------------------------------------------------------------
# 2. Install ao release binary into a temp directory
# ---------------------------------------------------------------------------
Write-Step "Installing ao release binary into a temp directory"
$aoInstallDir = Join-PathSegments -Base ([System.IO.Path]::GetTempPath()) -Segments ("agentops-ao-smoke-" + [Guid]::NewGuid().ToString("N"))
try {
$installAoScript = Join-PathSegments -Base $RepoRoot -Segments 'scripts', 'install-ao.ps1'
Invoke-NestedScript -ScriptPath $installAoScript -ScriptArgs @('-InstallDir', $aoInstallDir, '-NoPathUpdate')
if ($LASTEXITCODE -ne 0) {
throw "install-ao.ps1 failed"
}
$releaseAO = Join-PathSegments -Base $aoInstallDir -Segments 'ao.exe'
& $releaseAO version
if ($LASTEXITCODE -ne 0) {
throw "installed ao.exe version smoke failed"
}
}
finally {
Remove-IfExists -Path $aoInstallDir
}
# ---------------------------------------------------------------------------
# 3. Build local ao, link skills, and check the current doctor contract
# ---------------------------------------------------------------------------
Write-Step "Building local ao, linking skills, and checking doctor guidance"
$builtAO = Join-PathSegments -Base ([System.IO.Path]::GetTempPath()) -Segments ("ao-windows-smoke-" + [Guid]::NewGuid().ToString("N") + ".exe")
$linkHome = Join-PathSegments -Base ([System.IO.Path]::GetTempPath()) -Segments ("agentops-link-home-" + [Guid]::NewGuid().ToString("N"))
try {
$cliDir = Join-PathSegments -Base $RepoRoot -Segments 'cli'
$aoCmdPkg = Join-PathSegments -Base '.' -Segments 'cmd', 'ao'
Push-Location -LiteralPath $cliDir
try {
& go build -o $builtAO $aoCmdPkg
}
finally {
Pop-Location
}
if ($LASTEXITCODE -ne 0) {
throw "go build failed"
}
New-Item -ItemType Directory -Force -Path $linkHome | Out-Null
$prevHome = $env:HOME
$prevUserProfile = $env:USERPROFILE
try {
$env:HOME = $linkHome
$env:USERPROFILE = $linkHome
Push-Location -LiteralPath $RepoRoot
try {
& $builtAO skills link
if ($LASTEXITCODE -ne 0) {
throw "ao skills link failed"
}
}
finally {
Pop-Location
}
$agentsSkills = Join-PathSegments -Base $linkHome -Segments '.agents', 'skills'
if (-not (Test-Path -LiteralPath $agentsSkills)) {
throw "ao skills link did not create $agentsSkills"
}
}
finally {
if ($null -ne $prevHome) { $env:HOME = $prevHome } else { Remove-Item Env:HOME -ErrorAction SilentlyContinue }
if ($null -ne $prevUserProfile) { $env:USERPROFILE = $prevUserProfile }
}
$doctorText = (& $builtAO doctor 2>&1) -join "`n"
if ($LASTEXITCODE -gt 1) {
throw "ao doctor failed (exit $LASTEXITCODE)"
}
if ($doctorText -notmatch 'ao skills link') {
throw "doctor output did not include the ao skills link install hint"
}
}
finally {
Remove-IfExists -Path $builtAO
Remove-IfExists -Path $linkHome
}
# ---------------------------------------------------------------------------
# 4. Focused Windows-sensitive Go tests
# ---------------------------------------------------------------------------
Write-Step "Running focused Windows-sensitive Go tests"
Invoke-GoTest -TestArgs @("-timeout", "3m", "./internal/quality")
Invoke-GoTest -TestArgs @("-timeout", "3m", "./cmd/ao", "-run", "^(TestBatchForge_appendForgedRecord|TestAppendForgedRecord|TestBatchForgeSkipsAlreadyForged|TestLoadAndFilterTranscripts_RespectsForgedIndex|TestCanonicalArtifactPath|TestCobraDemoConceptsCommand|TestCobraDemoQuickCommand|TestCobraShowConcepts)$")
Invoke-GoTest -TestArgs @("-timeout", "3m", "./internal/storage", "-run", "^TestWithLockedFile_")
Invoke-GoTest -TestArgs @("-timeout", "3m", "./internal/provenanceapp", "-run", "^Test(MineSession|WriteMineState)", "-count", "1")
Invoke-GoTest -TestArgs @("-timeout", "3m", "./internal/config", "-run", "^TestSave_ConcurrentPatchesPreserveBothUpdates$")
Write-Host "Windows smoke tests passed"