Files
ThothII/scripts/test-windows-clone-contract.ps1

342 lines
16 KiB
PowerShell

param(
[string]$RepositoryRoot = "",
[switch]$DockerStartup,
[int]$CommandTimeoutSeconds = 600
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
$script:SensitiveValues = [System.Collections.Generic.List[string]]::new()
function Protect-Output([string]$Value) {
$protected = $Value
foreach ($secret in $script:SensitiveValues) {
if (-not [string]::IsNullOrEmpty($secret)) {
$protected = $protected.Replace($secret, "[REDACTED]")
}
}
return $protected -replace '(?i)((?:password|token|api[_-]?key|secret|key)\s*[:=]\s*)[^\s,;]+', '$1[REDACTED]'
}
function Invoke-BoundedNative {
param(
[Parameter(Mandatory = $true)][string]$FilePath,
[Parameter(Mandatory = $true)][string[]]$Arguments,
[Parameter(Mandatory = $true)][string]$Label,
[string]$WorkingDirectory = "",
[int]$TimeoutSeconds = $CommandTimeoutSeconds,
[switch]$AllowFailure
)
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
$startInfo.FileName = $FilePath
$startInfo.UseShellExecute = $false
$startInfo.RedirectStandardOutput = $true
$startInfo.RedirectStandardError = $true
$startInfo.CreateNoWindow = $true
if (-not [string]::IsNullOrWhiteSpace($WorkingDirectory)) {
$startInfo.WorkingDirectory = $WorkingDirectory
}
foreach ($argument in $Arguments) {
[void]$startInfo.ArgumentList.Add($argument)
}
$process = [System.Diagnostics.Process]::new()
$process.StartInfo = $startInfo
if (-not $process.Start()) {
throw "$Label could not start"
}
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
$stderrTask = $process.StandardError.ReadToEndAsync()
if (-not $process.WaitForExit($TimeoutSeconds * 1000)) {
$process.Kill($true)
[void]$process.WaitForExit(30000)
throw "$Label timed out after $TimeoutSeconds seconds"
}
$stdout = $stdoutTask.GetAwaiter().GetResult()
$stderr = $stderrTask.GetAwaiter().GetResult()
$result = [pscustomobject]@{
ExitCode = $process.ExitCode
StdOut = $stdout
StdErr = $stderr
}
if (-not $AllowFailure -and $result.ExitCode -ne 0) {
$diagnostic = Protect-Output (($result.StdOut + "`n" + $result.StdErr).Trim())
throw "$Label failed with exit $($result.ExitCode): $diagnostic"
}
return $result
}
function Write-Utf8File([string]$Path, [string]$Contents) {
[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($Path)) | Out-Null
[System.IO.File]::WriteAllText($Path, $Contents, [System.Text.UTF8Encoding]::new($false))
}
function ConvertTo-YamlPath([string]$Path) {
return $Path.Replace('\', '/').Replace('"', '\"')
}
if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
$resolved = Invoke-BoundedNative -FilePath "git" -Arguments @("rev-parse", "--show-toplevel") -Label "resolve repository root"
$RepositoryRoot = $resolved.StdOut.Trim()
}
$RepositoryRoot = [System.IO.Path]::GetFullPath($RepositoryRoot)
$trackedResult = Invoke-BoundedNative -FilePath "git" -Arguments @("-C", $RepositoryRoot, "ls-files") -Label "list tracked files"
$tracked = @($trackedResult.StdOut -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
$scriptRelativePath = "scripts/test-windows-clone-contract.ps1"
$attribute = Invoke-BoundedNative -FilePath "git" -Arguments @("-C", $RepositoryRoot, "check-attr", "eol", "--", $scriptRelativePath) -Label "read PowerShell eol attribute"
if (-not $attribute.StdOut.Trim().EndsWith("eol: crlf", [System.StringComparison]::OrdinalIgnoreCase)) {
throw "the Windows contract script must have the repository eol=crlf attribute"
}
$scriptBytes = [System.IO.File]::ReadAllBytes((Join-Path $RepositoryRoot $scriptRelativePath))
if (-not ($scriptBytes -contains [byte]0x0D)) {
throw "the Windows contract script was not checked out with CRLF bytes"
}
$offenders = [System.Collections.Generic.List[string]]::new()
foreach ($relativePath in $tracked) {
$name = [System.IO.Path]::GetFileName($relativePath)
$mustBeLf = $relativePath.EndsWith(".sh", [System.StringComparison]::OrdinalIgnoreCase) -or
$relativePath.EndsWith(".yml", [System.StringComparison]::OrdinalIgnoreCase) -or
$relativePath.EndsWith(".yaml", [System.StringComparison]::OrdinalIgnoreCase) -or
$name.Equals("Dockerfile", [System.StringComparison]::OrdinalIgnoreCase) -or
$name.StartsWith("Dockerfile.", [System.StringComparison]::OrdinalIgnoreCase) -or
$name.EndsWith(".Dockerfile", [System.StringComparison]::OrdinalIgnoreCase)
if ($mustBeLf) {
$bytes = [System.IO.File]::ReadAllBytes((Join-Path $RepositoryRoot $relativePath))
if ($bytes -contains [byte]0x0D) {
$offenders.Add($relativePath)
}
}
}
if ($offenders.Count -ne 0) {
throw "CR byte 0x0D found in tracked LF contract files: $($offenders -join ', ')"
}
$runId = [guid]::NewGuid().ToString("N")
$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("ThothII Task 13 path with spaces " + $runId)
$spacedRepository = Join-Path $temporaryRoot "Task 13 path with spaces"
$fixtureRoot = Join-Path $temporaryRoot "Disposable Fixture Data"
$project = "thothii-win-" + $runId.Substring(0, 12)
$runLabel = "windows-" + $runId
$coreImage = "task13-windows-core-$($runId.Substring(0, 16)):local"
$frontendImage = "task13-windows-frontend-$($runId.Substring(0, 16)):local"
$composeArguments = @()
$startupAttempted = $false
$cleanupSucceeded = $true
$savedEnvironment = @{}
try {
[System.IO.Directory]::CreateDirectory($spacedRepository) | Out-Null
foreach ($relativePath in $tracked) {
$source = Join-Path $RepositoryRoot $relativePath
$destination = Join-Path $spacedRepository $relativePath
[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($destination)) | Out-Null
Copy-Item -LiteralPath $source -Destination $destination
}
$tht = Join-Path $spacedRepository "dist/tht/tht-windows-amd64.exe"
[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($tht)) | Out-Null
Invoke-BoundedNative -FilePath "go" -Arguments @("build", "-trimpath", "-o", $tht, "./cmd/tht") `
-WorkingDirectory (Join-Path $spacedRepository "tools/tht") -Label "build native Windows tht in spaced path" | Out-Null
Invoke-BoundedNative -FilePath $tht -Arguments @("--help") -Label "invoke native Windows tht from spaced path" | Out-Null
$piAuth = Join-Path $fixtureRoot "Pi Auth/pi-auth.json"
$secrets = Join-Path $fixtureRoot "Secrets/thothii.secrets"
$authConfigRoot = Join-Path $fixtureRoot "Auth Config"
$secretValue = "windows-contract-$runId"
$script:SensitiveValues.Add($secretValue)
Write-Utf8File $piAuth "{}`n"
Write-Utf8File $secrets "THT_MODEL_API_KEY=$secretValue`n"
[System.IO.Directory]::CreateDirectory($authConfigRoot) | Out-Null
$remote = Join-Path $fixtureRoot "Workspace Remote/remote.git"
$seed = Join-Path $fixtureRoot "Workspace Seed"
[System.IO.Directory]::CreateDirectory($seed) | Out-Null
Invoke-BoundedNative -FilePath "git" -Arguments @("init", "--bare", "--initial-branch=main", $remote) -Label "initialize Windows bare registry" | Out-Null
Invoke-BoundedNative -FilePath "git" -Arguments @("-C", $seed, "init", "--initial-branch=main") -Label "initialize Windows registry seed" | Out-Null
$workspaceFixture = Join-Path $spacedRepository "scripts/fixtures/workspace-registry-windows.yaml"
$workspaceDirectory = Join-Path $seed "task13-windows"
$workspaceDestination = Join-Path $workspaceDirectory "workspace.yaml"
$workspaceEvidence = Join-Path $workspaceDirectory "evidence"
$catalogDestination = Join-Path $seed "thoth-workspaces.yaml"
[System.IO.Directory]::CreateDirectory($workspaceDirectory) | Out-Null
Copy-Item -LiteralPath $workspaceFixture -Destination $workspaceDestination
Write-Utf8File (Join-Path $workspaceEvidence "guide.md") "guide v1`n"
Write-Utf8File $catalogDestination @"
schema_version: 1
workspaces:
- id: task13-windows
name: Task 13 Windows
"@
if (-not (Test-Path -LiteralPath $catalogDestination)) {
throw "the Windows root workspace catalog was not created"
}
if (-not (Test-Path -LiteralPath $workspaceDestination)) {
throw "the Windows nested workspace descriptor was not created"
}
if (-not (Test-Path -LiteralPath (Join-Path $workspaceEvidence "guide.md"))) {
throw "the Windows nested workspace evidence was not created"
}
if (Test-Path -LiteralPath (Join-Path $seed "workspaces/task13-windows.yaml")) {
throw "the Windows legacy flat descriptor path must not be used"
}
Invoke-BoundedNative -FilePath "git" -Arguments @("-C", $seed, "add", "thoth-workspaces.yaml", "task13-windows/workspace.yaml", "task13-windows/evidence/guide.md") -Label "stage Windows registry seed" | Out-Null
Invoke-BoundedNative -FilePath "git" -Arguments @("-C", $seed, "-c", "user.name=Task 13 Windows", "-c", "user.email=task13-windows@example.invalid", "commit", "-m", "Seed Windows smoke") -Label "commit Windows registry seed" | Out-Null
Invoke-BoundedNative -FilePath "git" -Arguments @("-C", $seed, "push", $remote, "HEAD:main") -Label "push Windows registry seed" | Out-Null
$envFile = Join-Path $fixtureRoot "Config/local.env"
$override = Join-Path $fixtureRoot "Config/compose.windows.yaml"
$installation = Join-Path $fixtureRoot "Config/thothii installation.yaml"
Write-Utf8File $envFile @"
THOTH_HTTP_PORT=0
THOTH_CORE_HTTP_PORT=0
PI_AUTH_FILE=$piAuth
THT_SECRETS_FILE=$secrets
THT_AUTH_CONFIG_ROOT=$authConfigRoot
THT_WORKSPACE_GIT_REMOTE=/fixtures/remote.git
THT_WORKSPACE_GIT_BRANCH=main
THT_WORKSPACE_INSTALLATION_ID=task13-windows
"@
$remoteYaml = ConvertTo-YamlPath $remote
Write-Utf8File $override @"
services:
core:
image: $coreImage
build:
labels:
io.thothii.task13.run: "$runLabel"
labels:
io.thothii.task13.run: "$runLabel"
volumes:
- "${remoteYaml}:/fixtures/remote.git:ro"
frontend:
image: $frontendImage
build:
labels:
io.thothii.task13.run: "$runLabel"
labels:
io.thothii.task13.run: "$runLabel"
qdrant:
labels:
io.thothii.task13.run: "$runLabel"
embedding:
labels:
io.thothii.task13.run: "$runLabel"
embedding-model-init:
labels:
io.thothii.task13.run: "$runLabel"
networks:
thothii:
labels:
io.thothii.task13.run: "$runLabel"
volumes:
settings:
labels:
io.thothii.task13.run: "$runLabel"
pi-state:
labels:
io.thothii.task13.run: "$runLabel"
workspace-registry:
labels:
io.thothii.task13.run: "$runLabel"
workspace-secrets:
labels:
io.thothii.task13.run: "$runLabel"
sessions:
labels:
io.thothii.task13.run: "$runLabel"
qdrant-data:
labels:
io.thothii.task13.run: "$runLabel"
embedding-models:
labels:
io.thothii.task13.run: "$runLabel"
auth-state:
labels:
io.thothii.task13.run: "$runLabel"
"@
$repoYaml = ConvertTo-YamlPath $spacedRepository
$envYaml = ConvertTo-YamlPath $envFile
$overrideYaml = ConvertTo-YamlPath $override
Write-Utf8File $installation @"
profile: local
projectDirectory: "$repoYaml"
envFile: "$envYaml"
overrides:
- "$overrideYaml"
"@
$composeArguments = @(
"compose", "--project-name", $project, "--project-directory", $spacedRepository,
"--env-file", $envFile,
"-f", (Join-Path $spacedRepository "compose.yaml"),
"-f", (Join-Path $spacedRepository "deploy/compose.local.yaml"),
"-f", $override
)
$render = Invoke-BoundedNative -FilePath "docker" -Arguments ($composeArguments + @("config", "--services")) -Label "render Windows Compose from spaced path"
$services = @($render.StdOut -split "`r?`n" | Where-Object { $_ } | Sort-Object)
if (($services -join ",") -ne "catalog-db,core,embedding,embedding-model-init,frontend,qdrant") {
throw "rendered Windows stack must contain the canonical six services"
}
Invoke-BoundedNative -FilePath "docker" -Arguments ($composeArguments + @("config", "--quiet")) -Label "validate Windows Compose from spaced path" | Out-Null
if ($DockerStartup) {
Invoke-BoundedNative -FilePath "docker" -Arguments @("info") -Label "verify Windows Docker Desktop readiness" -TimeoutSeconds 60 | Out-Null
$startupAttempted = $true
Invoke-BoundedNative -FilePath "docker" -Arguments ($composeArguments + @("build", "--pull")) -Label "build canonical Windows stack" | Out-Null
Invoke-BoundedNative -FilePath "docker" -Arguments ($composeArguments + @("up", "--detach", "--wait", "--wait-timeout", "180")) -Label "start canonical Windows stack" -TimeoutSeconds 300 | Out-Null
$running = Invoke-BoundedNative -FilePath "docker" -Arguments ($composeArguments + @("ps", "--status", "running", "--services")) -Label "inspect running Windows services"
$runningServices = @($running.StdOut -split "`r?`n" | Where-Object { $_ } | Sort-Object)
if (($runningServices -join ",") -ne "catalog-db,core,embedding,frontend,qdrant") {
throw "bounded Windows startup did not leave the five long-running services ready"
}
Invoke-BoundedNative -FilePath $tht -Arguments @("--installation", $installation, "status") -Label "invoke installation-aware Windows tht in spaced path" | Out-Null
}
}
finally {
if ($startupAttempted -and $composeArguments.Count -ne 0) {
try {
foreach ($kind in @("container", "volume", "network")) {
$listArgs = if ($kind -eq "container") { @($kind, "ls", "-aq") } else { @($kind, "ls", "-q") }
$listed = Invoke-BoundedNative -FilePath "docker" -Arguments ($listArgs + @("--filter", "label=com.docker.compose.project=$project")) -Label "enumerate Windows project $kind resources" -TimeoutSeconds 30
foreach ($id in @($listed.StdOut -split "`r?`n" | Where-Object { $_ })) {
$format = if ($kind -eq "container") { '{{ index .Config.Labels "io.thothii.task13.run" }}' } else { '{{ index .Labels "io.thothii.task13.run" }}' }
$inspected = Invoke-BoundedNative -FilePath "docker" -Arguments @($kind, "inspect", "--format", $format, $id) -Label "inspect Windows project $kind ownership" -TimeoutSeconds 30
if ($inspected.StdOut.Trim() -ne $runLabel) {
throw "refusing to remove foreign Windows project $kind resource"
}
}
}
Invoke-BoundedNative -FilePath "docker" -Arguments ($composeArguments + @("down", "--volumes", "--remove-orphans", "--timeout", "10")) -Label "remove exact Windows Compose project" -TimeoutSeconds 60 | Out-Null
foreach ($image in @($frontendImage, $coreImage)) {
$inspection = Invoke-BoundedNative -FilePath "docker" -Arguments @("image", "inspect", "--format", '{{ index .Config.Labels "io.thothii.task13.run" }}', $image) -Label "inspect Windows image ownership" -TimeoutSeconds 30 -AllowFailure
if ($inspection.ExitCode -eq 0) {
if ($inspection.StdOut.Trim() -ne $runLabel) {
throw "refusing to remove foreign Windows image $image"
}
Invoke-BoundedNative -FilePath "docker" -Arguments @("image", "rm", $image) -Label "remove exact Windows image" -TimeoutSeconds 60 | Out-Null
}
}
}
catch {
$cleanupSucceeded = $false
Write-Error (Protect-Output $_.Exception.Message)
}
}
foreach ($name in $savedEnvironment.Keys) {
[System.Environment]::SetEnvironmentVariable($name, $savedEnvironment[$name], "Process")
}
if ($cleanupSucceeded -and [System.IO.Directory]::Exists($temporaryRoot)) {
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force
}
}
if (-not $cleanupSucceeded) {
throw "Windows cleanup proof failed; fixture path retained for recovery"
}
if ($DockerStartup) {
Write-Output "Windows spaced-path build, native tht, bounded canonical startup, and exact cleanup passed."
} else {
Write-Output "Windows spaced-path clone, LF-byte, Compose render, and native tht build/invocation contracts passed; Docker startup mode was not requested."
}