mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-05 08:23:53 +08:00
Completes the handoff task for the Windows harness: the native test job ran with the MSYS-shared /tmp and the runner's inherited LocalAppData\Temp ACLs, which the daemon's strict private-filesystem checks reject by design. The test-windows job now creates a per-user root under the profile with an owner-stamped, protected current-SID DACL and routes TEMP/TMP (native form) and TMPDIR (POSIX form) through it before scripts/test.sh runs. The guards script already hardened its root DACL but ownership is never inherited on Windows: bundle copies created by the admin-group runner token can come out Administrators-owned, and the launcher's exe policy requires the exact current user as owner. Stamp the current SID on the staged bundle dir and both executables explicitly. Job topology, gates, and triggers are unchanged; the new step is a sub-second pwsh block in the existing test-windows job. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
274 lines
12 KiB
PowerShell
274 lines
12 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Run the native-Windows product-surface test suite for codebase-memory-mcp.
|
|
|
|
.DESCRIPTION
|
|
Builds the payload and permanent launcher if they are not already present,
|
|
stages them under their release names, then runs the deterministic Windows
|
|
integration tests under tests/windows/ through the launcher (real stdio /
|
|
CLI / HTTP UI, real SQLite DB).
|
|
|
|
Two categories of test:
|
|
|
|
GUARDS - regression guards for Windows bugs already fixed on main.
|
|
They must stay GREEN (exit 0); a RED (exit 1) means the fix
|
|
regressed and fails this runner.
|
|
* test_non_ascii_path.py guards #636/#357 (fixed by #700)
|
|
* test_non_ascii_cache_dump.py guards #996 (writer cbm_fopen)
|
|
* test_hook_augment.py guards #618 (fixed by #619)
|
|
* test_ui_drive_listing.py guards #548 (roots field)
|
|
* test_cli_non_ascii_arg.py guards #423/#20 (wide-argv main())
|
|
* test_windows_launcher.py guards the permanent launcher,
|
|
managed layout, portable refusal, and crash containment
|
|
|
|
KNOWN REDS - genuine, still-open Windows bugs reproduced at the product
|
|
surface. They are EXPECTED to be RED (exit 1) and are opt-in
|
|
(never gate CI). If one turns GREEN the underlying bug was
|
|
fixed and it should be promoted to a guard.
|
|
* (none currently - test_cli_non_ascii_arg.py was promoted to a
|
|
guard when the wide-argv fix for #423/#20 landed)
|
|
|
|
Determinism: the runner sets CBM_INDEX_SUPERVISOR=0 so the path / hook / drive
|
|
guards index in-process (the pass-level readers under test, e.g. #700's cbm_fopen
|
|
routing, run in-process either way). The non-ASCII CLI guard is the exception - it
|
|
drops that override to cross the real supervisor -> worker spawn, where the second
|
|
half of #423/#20 lives (CreateProcessW delivering the wide command line).
|
|
|
|
On native Windows the MinGW/LLVM toolchain ships no libasan/libubsan, so the
|
|
build disables sanitizers (SANITIZE=). Where the toolchain provides
|
|
AddressSanitizer/UBSan (Linux containers, WSL), prefer scripts/test.sh.
|
|
|
|
.PARAMETER Binary
|
|
Path to an existing portable payload executable. If omitted, the script
|
|
builds it (target selected by -Target) into build/c/.
|
|
|
|
.PARAMETER Launcher
|
|
Path to the permanent launcher executable. If omitted, the script uses
|
|
build/c/codebase-memory-mcp-launcher.exe, building target cbm-launcher when
|
|
needed.
|
|
|
|
.PARAMETER Target
|
|
Makefile.cbm target used when building: 'cbm-with-ui' (default; needed for the
|
|
drive-picker guard's embedded HTTP UI) or 'cbm' (no UI - the drive guard then
|
|
reports a precondition and is skipped).
|
|
|
|
.PARAMETER GuardsOnly
|
|
Run only the green guards (the CI gate). Skips the opt-in known-red repros.
|
|
|
|
.PARAMETER Make
|
|
Path to GNU make (default: 'make' on PATH; MSYS2 ships it at
|
|
C:\msys64\usr\bin\make.exe).
|
|
|
|
.EXAMPLE
|
|
pwsh -File scripts/test-windows.ps1
|
|
.EXAMPLE
|
|
pwsh -File scripts/test-windows.ps1 -GuardsOnly -Binary build\c\codebase-memory-mcp.exe
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$Binary,
|
|
[string]$Launcher,
|
|
[ValidateSet("cbm-with-ui", "cbm")]
|
|
[string]$Target = "cbm-with-ui",
|
|
[switch]$GuardsOnly,
|
|
[string]$Make = "make"
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
|
Set-Location $repoRoot
|
|
|
|
$python = (Get-Command python -ErrorAction SilentlyContinue)
|
|
if (-not $python) { $python = (Get-Command py -ErrorAction SilentlyContinue) }
|
|
if (-not $python) { throw "Python 3 is required to run the Windows tests." }
|
|
$py = $python.Source
|
|
|
|
# A writable Windows temp dir that GNU make forwards to the native gcc. MSYS2
|
|
# strips TMP/TEMP from the environment it hands native children, so pass them as
|
|
# make command-line variables (make exports those to recipe processes).
|
|
$tmp = $env:TEMP
|
|
if (-not $tmp) { $tmp = "$env:USERPROFILE\AppData\Local\Temp" }
|
|
|
|
function Resolve-Binary {
|
|
param([string]$Explicit)
|
|
if ($Explicit) { return (Resolve-Path $Explicit).Path }
|
|
$built = Join-Path $repoRoot "build\c\codebase-memory-mcp.exe"
|
|
if (Test-Path $built) { return $built }
|
|
Write-Host "Building $Target via Makefile.cbm ..." -ForegroundColor Cyan
|
|
& $Make "-j" "-f" "Makefile.cbm" $Target "SANITIZE=" "TMP=$tmp" "TEMP=$tmp" "TMPDIR=$tmp" | Out-Host
|
|
$buildExit = $LASTEXITCODE
|
|
if ($buildExit -ne 0) { throw "build failed (exit $buildExit)" }
|
|
if (-not (Test-Path $built)) { throw "binary not produced at $built" }
|
|
return $built
|
|
}
|
|
|
|
function Resolve-Launcher {
|
|
param([string]$Explicit)
|
|
if ($Explicit) { return (Resolve-Path $Explicit).Path }
|
|
$built = Join-Path $repoRoot "build\c\codebase-memory-mcp-launcher.exe"
|
|
if (Test-Path $built) { return $built }
|
|
Write-Host "Building permanent launcher via Makefile.cbm ..." -ForegroundColor Cyan
|
|
& $Make "-j" "-f" "Makefile.cbm" "cbm-launcher" "SANITIZE=" "TMP=$tmp" "TEMP=$tmp" "TMPDIR=$tmp" | Out-Host
|
|
$buildExit = $LASTEXITCODE
|
|
if ($buildExit -ne 0) { throw "launcher build failed (exit $buildExit)" }
|
|
if (-not (Test-Path $built)) { throw "launcher not produced at $built" }
|
|
return $built
|
|
}
|
|
|
|
function Resolve-AbiMismatchLauncher {
|
|
$built = Join-Path $repoRoot "build\c\codebase-memory-mcp-launcher-abi2.exe"
|
|
if (Test-Path $built) { return $built }
|
|
Write-Host "Building launcher ABI mismatch fixture via Makefile.cbm ..." -ForegroundColor Cyan
|
|
& $Make "-j" "-f" "Makefile.cbm" "build/c/codebase-memory-mcp-launcher-abi2.exe" "SANITIZE=" "TMP=$tmp" "TEMP=$tmp" "TMPDIR=$tmp" | Out-Host
|
|
$buildExit = $LASTEXITCODE
|
|
if ($buildExit -ne 0) { throw "launcher ABI fixture build failed (exit $buildExit)" }
|
|
if (-not (Test-Path $built)) { throw "launcher ABI fixture not produced at $built" }
|
|
return $built
|
|
}
|
|
|
|
$bin = Resolve-Binary -Explicit $Binary
|
|
$launcherBin = Resolve-Launcher -Explicit $Launcher
|
|
$abiMismatchLauncher = Resolve-AbiMismatchLauncher
|
|
Write-Host "Payload: $bin" -ForegroundColor Green
|
|
Write-Host "Launcher: $launcherBin" -ForegroundColor Green
|
|
Write-Host "ABI mismatch fixture: $abiMismatchLauncher" -ForegroundColor Green
|
|
|
|
$previousTemp = $env:TEMP
|
|
$previousTmp = $env:TMP
|
|
$previousTmpDir = $env:TMPDIR
|
|
$guardRoot = $null
|
|
try {
|
|
$userProfile = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile)
|
|
if (-not $userProfile) { throw "could not resolve the current user's profile directory" }
|
|
$guardRoot = Join-Path $userProfile ("cbm-windows-guards-root-" + [guid]::NewGuid().ToString("N"))
|
|
New-Item -ItemType Directory -Path $guardRoot | Out-Null
|
|
|
|
# GitHub-hosted runner profile children can inherit mutation-capable ACEs
|
|
# even though the profile ancestry itself passes the launcher's bounded
|
|
# trust policy. Replace that inheritance before creating any executable or
|
|
# Python temporary descendant. Use SIDs rather than localized account names.
|
|
$currentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
if (-not $currentSid) { throw "could not resolve the current user's SID" }
|
|
$guardAcl = [System.Security.AccessControl.DirectorySecurity]::new()
|
|
$guardAcl.SetOwner($currentSid)
|
|
$guardAcl.SetAccessRuleProtection($true, $false)
|
|
$guardRule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
$currentSid,
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
|
|
[System.Security.AccessControl.InheritanceFlags]::ObjectInherit),
|
|
[System.Security.AccessControl.PropagationFlags]::None,
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
)
|
|
$guardAcl.AddAccessRule($guardRule) | Out-Null
|
|
Set-Acl -LiteralPath $guardRoot -AclObject $guardAcl
|
|
|
|
$guardBundle = Join-Path $guardRoot ("cbm-windows-guards-" + [guid]::NewGuid().ToString("N"))
|
|
New-Item -ItemType Directory -Path $guardBundle | Out-Null
|
|
$guardBin = Join-Path $guardBundle "codebase-memory-mcp.exe"
|
|
$guardPayload = Join-Path $guardBundle "codebase-memory-mcp.payload.exe"
|
|
Copy-Item -LiteralPath $launcherBin -Destination $guardBin
|
|
Copy-Item -LiteralPath $bin -Destination $guardPayload
|
|
|
|
# Ownership is never inherited on Windows: descendants created under the
|
|
# hardened root by an admin-group token can default to the Administrators
|
|
# SID, and the launcher's exe policy demands the exact current user as
|
|
# owner. Stamp the current SID explicitly on everything staged here.
|
|
foreach ($staged in @($guardBundle, $guardBin, $guardPayload)) {
|
|
$stagedAcl = Get-Acl -LiteralPath $staged
|
|
$stagedAcl.SetOwner($currentSid)
|
|
Set-Acl -LiteralPath $staged -AclObject $stagedAcl
|
|
}
|
|
Write-Host "Guard bundle: $guardBin" -ForegroundColor Green
|
|
|
|
# The launcher deliberately rejects GitHub's shared D:\a ancestry and the
|
|
# hosted runner's inherited LocalAppData\Temp ACL. Keep launcher fixtures
|
|
# and Python-created descendants below the accepted profile ancestry.
|
|
$env:TEMP = $guardRoot
|
|
$env:TMP = $guardRoot
|
|
$env:TMPDIR = $guardRoot
|
|
$env:PYTHONUTF8 = "1" # encode argv/stdio as UTF-8
|
|
$env:CBM_INDEX_SUPERVISOR = "0" # in-process indexing (see .DESCRIPTION)
|
|
|
|
# Green regression guards - must stay GREEN (exit 0). RED (exit 1) = the fix for
|
|
# the referenced issue regressed. The drive-picker guard needs the embedded HTTP
|
|
# UI (build target cbm-with-ui); against a non-UI binary it reports a precondition
|
|
# (exit 2) and is skipped rather than failed.
|
|
$guards = @(
|
|
"tests\windows\test_non_ascii_path.py",
|
|
"tests\windows\test_non_ascii_cache_dump.py",
|
|
"tests\windows\test_hook_augment.py",
|
|
"tests\windows\test_ui_drive_listing.py",
|
|
"tests\windows\test_cli_non_ascii_arg.py",
|
|
"tests\windows\test_windows_launcher.py"
|
|
)
|
|
|
|
# Opt-in known-red repros - EXPECTED red (exit 1); never gate CI. Currently empty:
|
|
# test_cli_non_ascii_arg.py was promoted to a guard when #423/#20's wide-argv fix landed.
|
|
$knownReds = @()
|
|
|
|
$guardFailures = @()
|
|
$guardSkips = @()
|
|
$fixedKeepers = @()
|
|
|
|
Write-Host "`n--- Green guards ---" -ForegroundColor Cyan
|
|
foreach ($t in $guards) {
|
|
Write-Host "`n=== $t ===" -ForegroundColor Cyan
|
|
if ($t -eq "tests\windows\test_windows_launcher.py") {
|
|
& $py $t $guardBin $guardPayload $abiMismatchLauncher
|
|
} else {
|
|
& $py $t $guardBin
|
|
}
|
|
$code = $LASTEXITCODE
|
|
if ($code -eq 0) {
|
|
Write-Host "GREEN ($t)" -ForegroundColor Green
|
|
} elseif ($code -eq 1 -or $t -eq "tests\windows\test_windows_launcher.py") {
|
|
Write-Host "RED ($t) - REGRESSION: a fixed Windows bug is broken again" -ForegroundColor Red
|
|
$guardFailures += $t
|
|
} else {
|
|
Write-Host "PRECONDITION ($t) exit=$code - skipped (see message above)" -ForegroundColor Yellow
|
|
$guardSkips += $t
|
|
}
|
|
}
|
|
|
|
if (-not $GuardsOnly) {
|
|
Write-Host "`n--- Known reds (opt-in, expected red) ---" -ForegroundColor Cyan
|
|
foreach ($t in $knownReds) {
|
|
Write-Host "`n=== $t ===" -ForegroundColor Cyan
|
|
& $py $t $guardBin
|
|
$code = $LASTEXITCODE
|
|
if ($code -eq 1) {
|
|
Write-Host "RED ($t) - expected; the underlying Windows bug is still open" -ForegroundColor DarkYellow
|
|
} elseif ($code -eq 0) {
|
|
Write-Host "GREEN ($t) - the bug appears FIXED; promote this to a guard" -ForegroundColor Green
|
|
$fixedKeepers += $t
|
|
} else {
|
|
Write-Host "PRECONDITION ($t) exit=$code - skipped (see message above)" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
$env:TEMP = $previousTemp
|
|
$env:TMP = $previousTmp
|
|
$env:TMPDIR = $previousTmpDir
|
|
if ($guardRoot) {
|
|
Remove-Item -LiteralPath $guardRoot -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
if ($guardSkips.Count -gt 0) {
|
|
Write-Host ("Guards skipped (precondition): {0} - e.g. the drive-picker guard " -f $guardSkips.Count) -ForegroundColor Yellow
|
|
Write-Host "needs a UI build (-Target cbm-with-ui, the default)." -ForegroundColor Yellow
|
|
}
|
|
if ($fixedKeepers.Count -gt 0) {
|
|
Write-Host ("Known-red repros that are now GREEN (promote to guards): {0}" -f ($fixedKeepers -join ", ")) -ForegroundColor Green
|
|
}
|
|
if ($guardFailures.Count -gt 0) {
|
|
Write-Host ("REGRESSION: {0} green guard(s) went red: {1}" -f $guardFailures.Count, ($guardFailures -join ", ")) -ForegroundColor Red
|
|
Write-Host "A previously-fixed Windows bug is broken again (see the guard's docstring and its referenced issue)." -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
Write-Host "All Windows green guards passed." -ForegroundColor Green
|
|
exit 0
|