From c8053912d8d31033f97c8a0ee0b990b98c67e565 Mon Sep 17 00:00:00 2001 From: dilucesr Date: Mon, 29 Jun 2026 16:44:24 -0700 Subject: [PATCH 01/11] Add validation scripts for all sample apps --- .gitignore | 3 + AI/mcp-server/validate-sample.ps1 | 67 ++++ AI/ocr/validate-sample.ps1 | 79 ++++ .../validate-sample.ps1 | 48 +++ .../validate-sample.ps1 | 90 +++++ .../validate-sample.ps1 | 96 +++++ Custom Apps/legal-docs/validate-sample.ps1 | 63 +++ .../project-management/validate-sample.ps1 | 63 +++ Custom Apps/webhook/validate-sample.ps1 | 69 ++++ README.md | 12 + Tools/powershell/SampleValidation.ps1 | 369 ++++++++++++++++++ Tools/sample-validation/browser-smoke.mjs | 97 +++++ Tools/sample-validation/package-lock.json | 57 +++ Tools/sample-validation/package.json | 8 + 14 files changed, 1121 insertions(+) create mode 100644 AI/mcp-server/validate-sample.ps1 create mode 100644 AI/ocr/validate-sample.ps1 create mode 100644 Custom Apps/boilerplate-aspnet-webservice/validate-sample.ps1 create mode 100644 Custom Apps/boilerplate-react-azurefunction/validate-sample.ps1 create mode 100644 Custom Apps/boilerplate-typescript-react/validate-sample.ps1 create mode 100644 Custom Apps/legal-docs/validate-sample.ps1 create mode 100644 Custom Apps/project-management/validate-sample.ps1 create mode 100644 Custom Apps/webhook/validate-sample.ps1 create mode 100644 Tools/powershell/SampleValidation.ps1 create mode 100644 Tools/sample-validation/browser-smoke.mjs create mode 100644 Tools/sample-validation/package-lock.json create mode 100644 Tools/sample-validation/package.json diff --git a/.gitignore b/.gitignore index 2a152ee..0d300d3 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ bld/ # Visual Studio 2017 auto generated files Generated\ Files/ +# Local validation logs +**/.validation/ + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* diff --git a/AI/mcp-server/validate-sample.ps1 b/AI/mcp-server/validate-sample.ps1 new file mode 100644 index 0000000..7ab483d --- /dev/null +++ b/AI/mcp-server/validate-sample.ps1 @@ -0,0 +1,67 @@ +param( + [switch]$SkipInstall, + [switch]$SkipTests, + [switch]$SkipBrowser, + [switch]$KeepProcesses, + [switch]$Headed, + [int]$TimeoutSec = 90 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') + +$appRoot = $PSScriptRoot +$envFile = Join-Path $appRoot '.env' +$runtimeHandle = $null + +try { + Write-Step 'Preflight checks' + Assert-CommandExists 'node' + Assert-CommandExists 'npm' + + if (-not $SkipInstall) { + Write-Step 'Installing dependencies' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot + } + + Write-Step 'Building MCP server' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $appRoot + + if ($SkipTests) { + Write-Host 'Skipping tests because -SkipTests was specified.' -ForegroundColor Yellow + } + else { + Write-Host 'No automated test script is defined for this sample.' -ForegroundColor Yellow + } + + if (-not (Test-Path $envFile)) { + Write-Host 'Skipping runtime smoke check because .env is missing.' -ForegroundColor Yellow + Write-Host 'Build validation completed.' -ForegroundColor Green + return + } + + $environment = Get-DotEnvMap -Path $envFile + if (-not $environment.ContainsKey('PORT')) { + $environment['PORT'] = '3100' + } + + Write-Step 'Starting MCP server' + $logPath = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'mcp-server' + $runtimeHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $appRoot -LogPath $logPath -Environment $environment + + $healthUrl = "http://localhost:$($environment['PORT'])/health" + $healthResponse = Wait-ForHttpEndpoint -Url $healthUrl -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) + $body = [string]$healthResponse.Content + if ($body -notmatch '"status"\s*:\s*"ok"') { + throw "Health endpoint returned an unexpected response: $body" + } + + Write-Host "Runtime smoke check passed at $healthUrl" -ForegroundColor Green +} +finally { + if ($null -ne $runtimeHandle -and -not $KeepProcesses) { + Stop-LoggedProcess -Handle $runtimeHandle + } +} \ No newline at end of file diff --git a/AI/ocr/validate-sample.ps1 b/AI/ocr/validate-sample.ps1 new file mode 100644 index 0000000..60261ca --- /dev/null +++ b/AI/ocr/validate-sample.ps1 @@ -0,0 +1,79 @@ +param( + [switch]$SkipInstall, + [switch]$SkipTests, + [switch]$SkipBrowser, + [switch]$KeepProcesses, + [switch]$Headed, + [int]$TimeoutSec = 120 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +$toolRoot = Join-Path $repoRoot 'Tools\sample-validation' +$appRoot = $PSScriptRoot +$envFile = Join-Path $appRoot '.env' +$handles = @() + +try { + Write-Step 'Preflight checks' + Assert-CommandExists 'node' + Assert-CommandExists 'npm' + + if (-not $SkipInstall) { + Write-Step 'Installing dependencies' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot + } + + Write-Step 'Building backend' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build:backend') -WorkingDirectory $appRoot + + Write-Step 'Building frontend' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build-cre') -WorkingDirectory $appRoot + + if ($SkipTests) { + Write-Host 'Skipping frontend tests because -SkipTests was specified.' -ForegroundColor Yellow + } + else { + Write-Step 'Running frontend tests' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test-cre', '--', '--watchAll=false') -WorkingDirectory $appRoot -Environment @{ CI = 'true' } + } + + if (-not (Test-Path $envFile)) { + Write-Host 'Skipping runtime smoke checks because .env is missing.' -ForegroundColor Yellow + Write-Host 'Build and test validation completed.' -ForegroundColor Green + return + } + + Write-Step 'Starting backend' + $backendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'ocr-backend' + $backendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start:backend') -WorkingDirectory $appRoot -LogPath $backendLog -Environment @{ PORT = '3001' } + $handles += $backendHandle + [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:3001/api/echo' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $backendHandle) + + Write-Step 'Starting frontend' + $frontendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'ocr-frontend' + $frontendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start-cre') -WorkingDirectory $appRoot -LogPath $frontendLog -Environment @{ BROWSER = 'none'; PORT = '3102' } + $handles += $frontendHandle + [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:3102' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $frontendHandle) + + if ($SkipBrowser) { + Write-Host 'Skipping browser smoke because -SkipBrowser was specified.' -ForegroundColor Yellow + } + else { + Write-Step 'Running browser smoke' + Invoke-BrowserSmoke -ToolRoot $toolRoot -Url 'http://127.0.0.1:3102' -SkipInstall:$SkipInstall -Headed:$Headed -TimeoutSec $TimeoutSec -ExpectSelector '#root' + } + + Write-Host 'OCR sample validation completed.' -ForegroundColor Green +} +finally { + if (-not $KeepProcesses) { + foreach ($handle in ($handles | Sort-Object -Descending -Property LogPath)) { + Stop-LoggedProcess -Handle $handle + } + } +} \ No newline at end of file diff --git a/Custom Apps/boilerplate-aspnet-webservice/validate-sample.ps1 b/Custom Apps/boilerplate-aspnet-webservice/validate-sample.ps1 new file mode 100644 index 0000000..d957a10 --- /dev/null +++ b/Custom Apps/boilerplate-aspnet-webservice/validate-sample.ps1 @@ -0,0 +1,48 @@ +param( + [switch]$SkipInstall, + [switch]$SkipTests, + [switch]$SkipBrowser, + [switch]$KeepProcesses, + [switch]$Headed, + [int]$TimeoutSec = 120 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') + +$appRoot = $PSScriptRoot +$appSettingsPath = Join-Path $appRoot 'appsettings.json' +$runtimeHandle = $null + +try { + Write-Step 'Preflight checks' + Assert-CommandExists 'dotnet' + + Write-Step 'Restoring packages' + Invoke-ExternalCommand -FilePath 'dotnet' -Arguments @('restore') -WorkingDirectory $appRoot + + Write-Step 'Building app' + Invoke-ExternalCommand -FilePath 'dotnet' -Arguments @('build') -WorkingDirectory $appRoot + + Write-Host 'No automated test project is defined for this sample.' -ForegroundColor Yellow + + if (-not (Test-Path $appSettingsPath)) { + Write-Host 'Skipping runtime smoke check because appsettings.json is missing.' -ForegroundColor Yellow + Write-Host 'Build validation completed.' -ForegroundColor Green + return + } + + Write-Step 'Starting web app' + $logPath = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'aspnet-webservice' + $runtimeHandle = Start-LoggedProcess -FilePath 'dotnet' -Arguments @('run', '--urls', 'http://127.0.0.1:5080') -WorkingDirectory $appRoot -LogPath $logPath + [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:5080' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200, 302) -ProcessHandle $runtimeHandle) + + Write-Host 'ASP.NET sample validation completed.' -ForegroundColor Green +} +finally { + if ($null -ne $runtimeHandle -and -not $KeepProcesses) { + Stop-LoggedProcess -Handle $runtimeHandle + } +} \ No newline at end of file diff --git a/Custom Apps/boilerplate-react-azurefunction/validate-sample.ps1 b/Custom Apps/boilerplate-react-azurefunction/validate-sample.ps1 new file mode 100644 index 0000000..b66572b --- /dev/null +++ b/Custom Apps/boilerplate-react-azurefunction/validate-sample.ps1 @@ -0,0 +1,90 @@ +param( + [switch]$SkipInstall, + [switch]$SkipTests, + [switch]$SkipBrowser, + [switch]$KeepProcesses, + [switch]$Headed, + [int]$TimeoutSec = 120 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +$toolRoot = Join-Path $repoRoot 'Tools\sample-validation' +$appRoot = $PSScriptRoot +$clientRoot = Join-Path $appRoot 'packages\client-app' +$functionsRoot = Join-Path $appRoot 'packages\azure-functions' +$clientEnvPath = Join-Path $clientRoot '.env' +$localSettingsPath = Join-Path $functionsRoot 'local.settings.json' +$handles = @() + +try { + Write-Step 'Preflight checks' + Assert-CommandExists 'node' + Assert-CommandExists 'npm' + + if (-not $SkipInstall) { + Write-Step 'Installing root dependencies' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot + + Write-Step 'Installing client-app dependencies' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $clientRoot + + Write-Step 'Installing azure-functions dependencies' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $functionsRoot + } + + Write-Step 'Building client-app' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $clientRoot + + if ($SkipTests) { + Write-Host 'Skipping client-app tests because -SkipTests was specified.' -ForegroundColor Yellow + } + else { + Write-Step 'Running client-app tests' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test', '--', '--watchAll=false') -WorkingDirectory $clientRoot -Environment @{ CI = 'true' } + } + + if (Test-Path $localSettingsPath) { + Assert-CommandExists 'func' + Write-Step 'Starting Azure Functions host' + $backendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'react-azure-functions-api' + $backendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $functionsRoot -LogPath $backendLog + $handles += $backendHandle + [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:7071/api/ListContainers' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200, 401) -ProcessHandle $backendHandle) + } + else { + Write-Host 'Skipping Azure Functions runtime smoke because local.settings.json is missing.' -ForegroundColor Yellow + } + + if (Test-Path $clientEnvPath) { + Write-Step 'Starting client-app' + $frontendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'react-azure-functions-client' + $frontendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $clientRoot -LogPath $frontendLog -Environment @{ BROWSER = 'none'; PORT = '3000' } + $handles += $frontendHandle + [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:3000' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $frontendHandle) + + if ($SkipBrowser) { + Write-Host 'Skipping browser smoke because -SkipBrowser was specified.' -ForegroundColor Yellow + } + else { + Write-Step 'Running browser smoke' + Invoke-BrowserSmoke -ToolRoot $toolRoot -Url 'http://127.0.0.1:3000' -SkipInstall:$SkipInstall -Headed:$Headed -TimeoutSec $TimeoutSec -ExpectSelector '#root' + } + } + else { + Write-Host 'Skipping client-app runtime smoke because .env is missing.' -ForegroundColor Yellow + } + + Write-Host 'React Azure Functions sample validation completed.' -ForegroundColor Green +} +finally { + if (-not $KeepProcesses) { + foreach ($handle in ($handles | Sort-Object -Descending -Property LogPath)) { + Stop-LoggedProcess -Handle $handle + } + } +} \ No newline at end of file diff --git a/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 b/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 new file mode 100644 index 0000000..063f0dd --- /dev/null +++ b/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 @@ -0,0 +1,96 @@ +param( + [switch]$SkipInstall, + [switch]$SkipTests, + [switch]$SkipBrowser, + [switch]$KeepProcesses, + [switch]$Headed, + [int]$TimeoutSec = 120 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +$toolRoot = Join-Path $repoRoot 'Tools\sample-validation' +$appRoot = $PSScriptRoot +$functionApiRoot = Join-Path $appRoot 'function-api' +$clientRoot = Join-Path $appRoot 'react-client' +$localSettingsPath = Join-Path $functionApiRoot 'local.settings.json' +$clientEnvPath = Join-Path $clientRoot '.env' +$handles = @() + +try { + Write-Step 'Preflight checks' + Assert-CommandExists 'node' + Assert-CommandExists 'npm' + + if (-not $SkipInstall) { + Write-Step 'Installing root dependencies' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot + + Write-Step 'Installing function-api dependencies' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $functionApiRoot + + Write-Step 'Installing react-client dependencies' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $clientRoot + } + + Write-Step 'Building function-api' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $functionApiRoot + + Write-Step 'Building react-client' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $clientRoot + + if ($SkipTests) { + Write-Host 'Skipping react-client tests because -SkipTests was specified.' -ForegroundColor Yellow + } + else { + Write-Step 'Running react-client tests' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test', '--', '--watchAll=false') -WorkingDirectory $clientRoot -Environment @{ CI = 'true' } + } + + if (Test-Path $localSettingsPath) { + Assert-CommandExists 'func' + Write-Step 'Starting function-api host' + $backendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'typescript-react-api' + $backendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $functionApiRoot -LogPath $backendLog + $handles += $backendHandle + [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:7072/api/containers' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200, 401) -ProcessHandle $backendHandle) + } + else { + Write-Host 'Skipping function-api runtime smoke because local.settings.json is missing.' -ForegroundColor Yellow + } + + if (Test-Path $clientEnvPath) { + $clientEnv = Get-DotEnvMap -Path $clientEnvPath + $clientPort = if ($clientEnv.ContainsKey('PORT')) { $clientEnv['PORT'] } else { '8080' } + + Write-Step 'Starting react-client' + $frontendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'typescript-react-client' + $frontendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $clientRoot -LogPath $frontendLog -Environment @{ BROWSER = 'none' } + $handles += $frontendHandle + [void](Wait-ForHttpEndpoint -Url "http://127.0.0.1:$clientPort" -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $frontendHandle) + + if ($SkipBrowser) { + Write-Host 'Skipping browser smoke because -SkipBrowser was specified.' -ForegroundColor Yellow + } + else { + Write-Step 'Running browser smoke' + Invoke-BrowserSmoke -ToolRoot $toolRoot -Url "http://127.0.0.1:$clientPort" -SkipInstall:$SkipInstall -Headed:$Headed -TimeoutSec $TimeoutSec -ExpectSelector '#root' + } + } + else { + Write-Host 'Skipping react-client runtime smoke because .env is missing.' -ForegroundColor Yellow + } + + Write-Host 'TypeScript React sample validation completed.' -ForegroundColor Green +} +finally { + if (-not $KeepProcesses) { + foreach ($handle in ($handles | Sort-Object -Descending -Property LogPath)) { + Stop-LoggedProcess -Handle $handle + } + } +} \ No newline at end of file diff --git a/Custom Apps/legal-docs/validate-sample.ps1 b/Custom Apps/legal-docs/validate-sample.ps1 new file mode 100644 index 0000000..a418804 --- /dev/null +++ b/Custom Apps/legal-docs/validate-sample.ps1 @@ -0,0 +1,63 @@ +param( + [switch]$SkipInstall, + [switch]$SkipTests, + [switch]$SkipBrowser, + [switch]$KeepProcesses, + [switch]$Headed, + [int]$TimeoutSec = 90 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +$toolRoot = Join-Path $repoRoot 'Tools\sample-validation' +$appRoot = $PSScriptRoot +$runtimeHandle = $null + +try { + Write-Step 'Preflight checks' + Assert-CommandExists 'node' + Assert-CommandExists 'npm' + Assert-CommandExists 'npx' + + if (-not $SkipInstall) { + Write-Step 'Installing dependencies' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot + } + + Write-Step 'Building app' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $appRoot + + Write-Step 'Linting app' + try { + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'lint') -WorkingDirectory $appRoot + } + catch { + Write-Host "Lint reported existing issues and will not block runtime validation: $($_.Exception.Message)" -ForegroundColor Yellow + } + + Write-Host 'No automated test script is defined for this sample.' -ForegroundColor Yellow + + Write-Step 'Starting preview server' + $logPath = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'legal-docs' + $runtimeHandle = Start-LoggedProcess -FilePath 'npx' -Arguments @('vite', 'preview', '--host', '127.0.0.1', '--port', '4173') -WorkingDirectory $appRoot -LogPath $logPath + [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:4173' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $runtimeHandle) + + if ($SkipBrowser) { + Write-Host 'Skipping browser smoke because -SkipBrowser was specified.' -ForegroundColor Yellow + } + else { + Write-Step 'Running browser smoke' + Invoke-BrowserSmoke -ToolRoot $toolRoot -Url 'http://127.0.0.1:4173' -SkipInstall:$SkipInstall -Headed:$Headed -TimeoutSec $TimeoutSec -ExpectSelector '#root' + } + + Write-Host 'Legal docs sample validation completed.' -ForegroundColor Green +} +finally { + if ($null -ne $runtimeHandle -and -not $KeepProcesses) { + Stop-LoggedProcess -Handle $runtimeHandle + } +} \ No newline at end of file diff --git a/Custom Apps/project-management/validate-sample.ps1 b/Custom Apps/project-management/validate-sample.ps1 new file mode 100644 index 0000000..6ad34b0 --- /dev/null +++ b/Custom Apps/project-management/validate-sample.ps1 @@ -0,0 +1,63 @@ +param( + [switch]$SkipInstall, + [switch]$SkipTests, + [switch]$SkipBrowser, + [switch]$KeepProcesses, + [switch]$Headed, + [int]$TimeoutSec = 90 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +$toolRoot = Join-Path $repoRoot 'Tools\sample-validation' +$appRoot = $PSScriptRoot +$runtimeHandle = $null + +try { + Write-Step 'Preflight checks' + Assert-CommandExists 'node' + Assert-CommandExists 'npm' + Assert-CommandExists 'npx' + + if (-not $SkipInstall) { + Write-Step 'Installing dependencies' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot + } + + Write-Step 'Building app' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $appRoot + + Write-Step 'Linting app' + try { + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'lint') -WorkingDirectory $appRoot + } + catch { + Write-Host "Lint reported existing issues and will not block runtime validation: $($_.Exception.Message)" -ForegroundColor Yellow + } + + Write-Host 'No automated test script is defined for this sample.' -ForegroundColor Yellow + + Write-Step 'Starting preview server' + $logPath = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'project-management' + $runtimeHandle = Start-LoggedProcess -FilePath 'npx' -Arguments @('vite', 'preview', '--host', '127.0.0.1', '--port', '4173') -WorkingDirectory $appRoot -LogPath $logPath + [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:4173' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $runtimeHandle) + + if ($SkipBrowser) { + Write-Host 'Skipping browser smoke because -SkipBrowser was specified.' -ForegroundColor Yellow + } + else { + Write-Step 'Running browser smoke' + Invoke-BrowserSmoke -ToolRoot $toolRoot -Url 'http://127.0.0.1:4173' -SkipInstall:$SkipInstall -Headed:$Headed -TimeoutSec $TimeoutSec -ExpectSelector '#root' + } + + Write-Host 'Project management sample validation completed.' -ForegroundColor Green +} +finally { + if ($null -ne $runtimeHandle -and -not $KeepProcesses) { + Stop-LoggedProcess -Handle $runtimeHandle + } +} \ No newline at end of file diff --git a/Custom Apps/webhook/validate-sample.ps1 b/Custom Apps/webhook/validate-sample.ps1 new file mode 100644 index 0000000..9ecc315 --- /dev/null +++ b/Custom Apps/webhook/validate-sample.ps1 @@ -0,0 +1,69 @@ +param( + [switch]$SkipInstall, + [switch]$SkipTests, + [switch]$SkipBrowser, + [switch]$KeepProcesses, + [switch]$Headed, + [int]$TimeoutSec = 60 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') + +$appRoot = $PSScriptRoot +$packageRoot = Join-Path $appRoot 'src' +$runtimeHandle = $null + +try { + Write-Step 'Preflight checks' + Assert-CommandExists 'node' + Assert-CommandExists 'npm' + + if (-not $SkipInstall) { + Write-Step 'Installing dependencies' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $packageRoot + } + + Write-Host 'No automated test script is defined for this sample.' -ForegroundColor Yellow + + Write-Step 'Starting webhook listener' + $logPath = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'webhook' + $runtimeHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $packageRoot -LogPath $logPath -Environment @{ PORT = '3000' } + + $validationToken = 'sample-validation-token' + $validationUrl = "http://127.0.0.1:3000/webhook?validationToken=$validationToken" + $deadline = (Get-Date).AddSeconds($TimeoutSec) + $validated = $false + + while ((Get-Date) -lt $deadline) { + if ($runtimeHandle.Process.HasExited) { + $tail = Get-LogTail -Path $runtimeHandle.LogPath + throw "Webhook listener exited before responding. Recent log output:`n$tail" + } + + try { + $response = Invoke-WebRequest -Method Post -Uri $validationUrl -UseBasicParsing -TimeoutSec 5 -ContentType 'application/json' -Body '{}' + if ([string]$response.Content -eq $validationToken) { + $validated = $true + break + } + } + catch { + } + + Start-Sleep -Milliseconds 500 + } + + if (-not $validated) { + throw 'Webhook validation token echo check did not succeed.' + } + + Write-Host 'Webhook sample validation completed.' -ForegroundColor Green +} +finally { + if ($null -ne $runtimeHandle -and -not $KeepProcesses) { + Stop-LoggedProcess -Handle $runtimeHandle + } +} \ No newline at end of file diff --git a/README.md b/README.md index b8bead2..20e7425 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,18 @@ Runnable applications demonstrating SharePoint Embedded integration patterns. See [docker.md](./Custom%20Apps/docker.md) for instructions on running the boilerplate apps in VS Code dev containers. +## Sample Validation Scripts + +Runnable sample applications now expose a root-level `validate-sample.ps1` script that performs the sample's local build and an app-appropriate smoke check. + +Run the script from the sample root in PowerShell, for example: + +```powershell +./validate-sample.ps1 +``` + +Samples that require local tenant configuration will automatically downgrade to build-only or startup-only validation when their expected `.env`, `local.settings.json`, or `appsettings.json` files are missing. + ## AI Samples and assets for integrating SharePoint Embedded with AI tools and services. diff --git a/Tools/powershell/SampleValidation.ps1 b/Tools/powershell/SampleValidation.ps1 new file mode 100644 index 0000000..7c4f92d --- /dev/null +++ b/Tools/powershell/SampleValidation.ps1 @@ -0,0 +1,369 @@ +Set-StrictMode -Version Latest + +function Write-Step { + param( + [Parameter(Mandatory = $true)] + [string]$Message + ) + + Write-Host "`n==> $Message" -ForegroundColor Cyan +} + +function Assert-CommandExists { + param( + [Parameter(Mandatory = $true)] + [string]$Name + ) + + if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { + throw "Required command '$Name' was not found in PATH." + } +} + +function Resolve-CommandPath { + param( + [Parameter(Mandatory = $true)] + [string]$Name + ) + + if ($Name.Contains([System.IO.Path]::DirectorySeparatorChar) -or $Name.Contains([System.IO.Path]::AltDirectorySeparatorChar)) { + return $Name + } + + $command = Get-Command $Name -ErrorAction Stop + if ($null -ne $command.Source -and $command.Source.Length -gt 0) { + return $command.Source + } + + return $command.Name +} + +function Invoke-ExternalCommand { + param( + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [string[]]$Arguments = @(), + + [Parameter(Mandatory = $true)] + [string]$WorkingDirectory, + + [hashtable]$Environment = @{} + ) + + $resolvedFilePath = Resolve-CommandPath -Name $FilePath + + Push-Location $WorkingDirectory + $previous = @{} + + try { + foreach ($key in $Environment.Keys) { + $previous[$key] = [Environment]::GetEnvironmentVariable($key) + [Environment]::SetEnvironmentVariable($key, [string]$Environment[$key]) + } + + & $resolvedFilePath @Arguments + if ($LASTEXITCODE -ne 0) { + throw "Command '$FilePath $($Arguments -join ' ')' failed with exit code $LASTEXITCODE." + } + } + finally { + foreach ($key in $Environment.Keys) { + [Environment]::SetEnvironmentVariable($key, $previous[$key]) + } + Pop-Location + } +} + +function Get-DotEnvMap { + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + $values = @{} + foreach ($line in [System.IO.File]::ReadAllLines($Path)) { + $trimmed = $line.Trim() + if ([string]::IsNullOrWhiteSpace($trimmed) -or $trimmed.StartsWith('#')) { + continue + } + + $separatorIndex = $trimmed.IndexOf('=') + if ($separatorIndex -lt 1) { + continue + } + + $name = $trimmed.Substring(0, $separatorIndex).Trim() + $value = $trimmed.Substring($separatorIndex + 1).Trim() + if (($value.StartsWith('"') -and $value.EndsWith('"')) -or ($value.StartsWith("'") -and $value.EndsWith("'"))) { + $value = $value.Substring(1, $value.Length - 2) + } + + $values[$name] = $value + } + + return $values +} + +function Start-LoggedProcess { + param( + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [string[]]$Arguments = @(), + + [Parameter(Mandatory = $true)] + [string]$WorkingDirectory, + + [Parameter(Mandatory = $true)] + [string]$LogPath, + + [hashtable]$Environment = @{} + ) + + $resolvedFilePath = Resolve-CommandPath -Name $FilePath + $logDirectory = Split-Path -Parent $LogPath + if (-not (Test-Path $logDirectory)) { + New-Item -ItemType Directory -Path $logDirectory | Out-Null + } + + $stdoutPath = "$LogPath.stdout" + $stderrPath = "$LogPath.stderr" + if (Test-Path $stdoutPath) { + Remove-Item $stdoutPath -Force + } + if (Test-Path $stderrPath) { + Remove-Item $stderrPath -Force + } + + $startSplat = @{ + FilePath = $resolvedFilePath + ArgumentList = $Arguments + WorkingDirectory = $WorkingDirectory + RedirectStandardOutput = $stdoutPath + RedirectStandardError = $stderrPath + PassThru = $true + NoNewWindow = $true + } + + if ($Environment.Count -gt 0) { + $startSplat['Environment'] = $Environment + } + + $process = Start-Process @startSplat + if ($null -eq $process) { + throw "Failed to start process '$FilePath'." + } + + return [pscustomobject]@{ + Process = $process + LogPath = $LogPath + StdoutPath = $stdoutPath + StderrPath = $stderrPath + } +} + +function Stop-LoggedProcess { + param( + [Parameter(Mandatory = $true)] + [pscustomobject]$Handle + ) + + if ($null -ne $Handle.Process -and -not $Handle.Process.HasExited) { + $Handle.Process.Kill($true) + $Handle.Process.WaitForExit() + } + +} + +function Get-LogTail { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [int]$LineCount = 40 + ) + + $paths = @() + if ($Path) { + $paths += $Path + } + + $stdoutPath = "$Path.stdout" + $stderrPath = "$Path.stderr" + if (Test-Path $stdoutPath) { + $paths += $stdoutPath + } + if (Test-Path $stderrPath) { + $paths += $stderrPath + } + + if ($paths.Count -eq 0) { + return '' + } + + $lines = foreach ($candidate in $paths) { + if (Test-Path $candidate) { + "[$([System.IO.Path]::GetFileName($candidate))]" + Get-Content -Path $candidate -Tail $LineCount + } + } + + return ($lines -join [Environment]::NewLine) +} + +function Wait-ForHttpEndpoint { + param( + [Parameter(Mandatory = $true)] + [string]$Url, + + [int]$TimeoutSec = 60, + + [int[]]$AllowedStatusCodes = @(200), + + [pscustomobject]$ProcessHandle + ) + + $deadline = (Get-Date).AddSeconds($TimeoutSec) + while ((Get-Date) -lt $deadline) { + if ($null -ne $ProcessHandle -and $ProcessHandle.Process.HasExited) { + $tail = Get-LogTail -Path $ProcessHandle.LogPath + throw "Process exited while waiting for '$Url'. Recent log output:`n$tail" + } + + try { + $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 5 -MaximumRedirection 0 + if ($AllowedStatusCodes -contains [int]$response.StatusCode) { + return $response + } + } + catch { + $responseProperty = $_.Exception.PSObject.Properties['Response'] + $response = if ($null -ne $responseProperty) { $responseProperty.Value } else { $null } + if ($null -ne $response) { + $statusCode = $response.StatusCode.value__ + if ($AllowedStatusCodes -contains [int]$statusCode) { + return $response + } + } + } + + Start-Sleep -Milliseconds 500 + } + + $details = '' + if ($null -ne $ProcessHandle) { + $details = Get-LogTail -Path $ProcessHandle.LogPath + } + + throw "Timed out waiting for HTTP endpoint '$Url'. Recent log output:`n$details" +} + +function Test-FileContains { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [Parameter(Mandatory = $true)] + [string]$Pattern + ) + + if (-not (Test-Path $Path)) { + return $false + } + + return Select-String -Path $Path -Pattern $Pattern -SimpleMatch -Quiet +} + +function New-ValidationLogPath { + param( + [Parameter(Mandatory = $true)] + [string]$WorkingDirectory, + + [Parameter(Mandatory = $true)] + [string]$Name + ) + + $logRoot = Join-Path $WorkingDirectory ".validation" + if (-not (Test-Path $logRoot)) { + New-Item -ItemType Directory -Path $logRoot | Out-Null + } + + $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss-fff' + return Join-Path $logRoot "$Name-$timestamp.log" +} + +function Ensure-BrowserTooling { + param( + [Parameter(Mandatory = $true)] + [string]$ToolRoot, + + [switch]$SkipInstall + ) + + $playwrightPath = Join-Path $ToolRoot 'node_modules\playwright' + if (-not (Test-Path $playwrightPath)) { + if ($SkipInstall) { + Write-Host 'Shared browser validation tooling is missing; installing it even though -SkipInstall was specified.' -ForegroundColor Yellow + } + + Write-Step 'Installing shared browser validation tooling' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $ToolRoot + } + + Write-Step 'Installing Chromium for Playwright' + Invoke-ExternalCommand -FilePath 'npx' -Arguments @('playwright', 'install', 'chromium') -WorkingDirectory $ToolRoot +} + +function Invoke-BrowserSmoke { + param( + [Parameter(Mandatory = $true)] + [string]$ToolRoot, + + [Parameter(Mandatory = $true)] + [string]$Url, + + [switch]$SkipInstall, + + [switch]$Headed, + + [int]$TimeoutSec = 60, + + [int]$WaitMs = 1500, + + [string]$ExpectSelector, + + [string]$ExpectedText, + + [string]$ClickSelector, + + [switch]$FailOnConsoleError + ) + + Ensure-BrowserTooling -ToolRoot $ToolRoot -SkipInstall:$SkipInstall + + $arguments = @( + 'browser-smoke.mjs', + '--url', $Url, + '--timeout-ms', [string]($TimeoutSec * 1000), + '--wait-ms', [string]$WaitMs + ) + + if ($Headed) { + $arguments += '--headed' + } + if ($ExpectSelector) { + $arguments += @('--expect-selector', $ExpectSelector) + } + if ($ExpectedText) { + $arguments += @('--expect-text', $ExpectedText) + } + if ($ClickSelector) { + $arguments += @('--click-selector', $ClickSelector) + } + if ($FailOnConsoleError) { + $arguments += '--fail-on-console-error' + } + + Invoke-ExternalCommand -FilePath 'node' -Arguments $arguments -WorkingDirectory $ToolRoot +} \ No newline at end of file diff --git a/Tools/sample-validation/browser-smoke.mjs b/Tools/sample-validation/browser-smoke.mjs new file mode 100644 index 0000000..eb85953 --- /dev/null +++ b/Tools/sample-validation/browser-smoke.mjs @@ -0,0 +1,97 @@ +import { chromium } from 'playwright'; + +function parseArgs(argv) { + const args = {}; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token.startsWith('--')) { + continue; + } + + const key = token.slice(2); + const next = argv[index + 1]; + if (!next || next.startsWith('--')) { + args[key] = true; + continue; + } + + args[key] = next; + index += 1; + } + + return args; +} + +const args = parseArgs(process.argv.slice(2)); +if (!args.url) { + throw new Error('--url is required'); +} + +const timeoutMs = Number(args['timeout-ms'] ?? '60000'); +const waitMs = Number(args['wait-ms'] ?? '1500'); +const expectSelector = args['expect-selector'] ?? 'body'; +const expectText = args['expect-text']; +const clickSelector = args['click-selector']; +const headed = Boolean(args.headed); +const failOnConsoleError = Boolean(args['fail-on-console-error']); + +const browser = await chromium.launch({ headless: !headed }); +const page = await browser.newPage(); +const pageErrors = []; +const consoleErrors = []; + +page.on('pageerror', error => { + pageErrors.push(error.stack ?? String(error)); +}); + +if (failOnConsoleError) { + page.on('console', message => { + if (message.type() === 'error' || message.type() === 'assert') { + consoleErrors.push(message.text()); + } + }); +} + +try { + await page.goto(args.url, { waitUntil: 'domcontentloaded', timeout: timeoutMs }); + await page.waitForSelector(expectSelector, { state: 'visible', timeout: timeoutMs }); + await page.waitForLoadState('networkidle', { timeout: Math.min(timeoutMs, 5000) }).catch(() => {}); + + if (waitMs > 0) { + await page.waitForTimeout(waitMs); + } + + if (clickSelector) { + const locator = page.locator(clickSelector).first(); + if (await locator.count() === 0) { + throw new Error(`Could not find clickable selector: ${clickSelector}`); + } + + await locator.click(); + await page.waitForTimeout(750); + } + + if (expectText) { + await page.locator(`text=${expectText}`).first().waitFor({ state: 'visible', timeout: timeoutMs }); + } + + const bodyText = (await page.locator('body').innerText()).trim(); + if (bodyText.length === 0) { + throw new Error('The page rendered an empty body.'); + } + + if (pageErrors.length > 0) { + throw new Error(`Browser page errors detected:\n${pageErrors.join('\n\n')}`); + } + + if (consoleErrors.length > 0) { + throw new Error(`Browser console errors detected:\n${consoleErrors.join('\n\n')}`); + } + + console.log(`Browser smoke passed for ${args.url}`); +} +finally { + await page.close(); + await browser.close(); +} \ No newline at end of file diff --git a/Tools/sample-validation/package-lock.json b/Tools/sample-validation/package-lock.json new file mode 100644 index 0000000..3f2d93e --- /dev/null +++ b/Tools/sample-validation/package-lock.json @@ -0,0 +1,57 @@ +{ + "name": "spe-sample-validation-tools", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "spe-sample-validation-tools", + "dependencies": { + "playwright": "^1.57.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/Tools/sample-validation/package.json b/Tools/sample-validation/package.json new file mode 100644 index 0000000..93b631d --- /dev/null +++ b/Tools/sample-validation/package.json @@ -0,0 +1,8 @@ +{ + "name": "spe-sample-validation-tools", + "private": true, + "type": "module", + "dependencies": { + "playwright": "^1.57.0" + } +} \ No newline at end of file From 95cd0aa6022f7df90b81df17b2886ffe42bca017 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 01:35:08 +0000 Subject: [PATCH 02/11] Fix PowerShell validation scripts for cross-platform support --- AI/mcp-server/validate-sample.ps1 | 2 +- AI/ocr/validate-sample.ps1 | 6 +++--- .../validate-sample.ps1 | 2 +- .../validate-sample.ps1 | 10 +++++----- .../validate-sample.ps1 | 6 +++--- Custom Apps/legal-docs/validate-sample.ps1 | 6 +++--- Custom Apps/project-management/validate-sample.ps1 | 6 +++--- Custom Apps/webhook/validate-sample.ps1 | 14 ++++++++++++-- Tools/powershell/SampleValidation.ps1 | 12 ++++++++++-- 9 files changed, 41 insertions(+), 23 deletions(-) diff --git a/AI/mcp-server/validate-sample.ps1 b/AI/mcp-server/validate-sample.ps1 index 7ab483d..e5489ac 100644 --- a/AI/mcp-server/validate-sample.ps1 +++ b/AI/mcp-server/validate-sample.ps1 @@ -10,7 +10,7 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') +. (Join-Path $PSScriptRoot '../../Tools/powershell/SampleValidation.ps1') $appRoot = $PSScriptRoot $envFile = Join-Path $appRoot '.env' diff --git a/AI/ocr/validate-sample.ps1 b/AI/ocr/validate-sample.ps1 index 60261ca..d39d79b 100644 --- a/AI/ocr/validate-sample.ps1 +++ b/AI/ocr/validate-sample.ps1 @@ -10,10 +10,10 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') +. (Join-Path $PSScriptRoot '../../Tools/powershell/SampleValidation.ps1') -$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path -$toolRoot = Join-Path $repoRoot 'Tools\sample-validation' +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +$toolRoot = Join-Path $repoRoot 'Tools/sample-validation' $appRoot = $PSScriptRoot $envFile = Join-Path $appRoot '.env' $handles = @() diff --git a/Custom Apps/boilerplate-aspnet-webservice/validate-sample.ps1 b/Custom Apps/boilerplate-aspnet-webservice/validate-sample.ps1 index d957a10..d96bdb7 100644 --- a/Custom Apps/boilerplate-aspnet-webservice/validate-sample.ps1 +++ b/Custom Apps/boilerplate-aspnet-webservice/validate-sample.ps1 @@ -10,7 +10,7 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') +. (Join-Path $PSScriptRoot '../../Tools/powershell/SampleValidation.ps1') $appRoot = $PSScriptRoot $appSettingsPath = Join-Path $appRoot 'appsettings.json' diff --git a/Custom Apps/boilerplate-react-azurefunction/validate-sample.ps1 b/Custom Apps/boilerplate-react-azurefunction/validate-sample.ps1 index b66572b..7e3c052 100644 --- a/Custom Apps/boilerplate-react-azurefunction/validate-sample.ps1 +++ b/Custom Apps/boilerplate-react-azurefunction/validate-sample.ps1 @@ -10,13 +10,13 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') +. (Join-Path $PSScriptRoot '../../Tools/powershell/SampleValidation.ps1') -$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path -$toolRoot = Join-Path $repoRoot 'Tools\sample-validation' +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +$toolRoot = Join-Path $repoRoot 'Tools/sample-validation' $appRoot = $PSScriptRoot -$clientRoot = Join-Path $appRoot 'packages\client-app' -$functionsRoot = Join-Path $appRoot 'packages\azure-functions' +$clientRoot = Join-Path $appRoot 'packages/client-app' +$functionsRoot = Join-Path $appRoot 'packages/azure-functions' $clientEnvPath = Join-Path $clientRoot '.env' $localSettingsPath = Join-Path $functionsRoot 'local.settings.json' $handles = @() diff --git a/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 b/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 index 063f0dd..ecf20c1 100644 --- a/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 +++ b/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 @@ -10,10 +10,10 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') +. (Join-Path $PSScriptRoot '../../Tools/powershell/SampleValidation.ps1') -$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path -$toolRoot = Join-Path $repoRoot 'Tools\sample-validation' +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +$toolRoot = Join-Path $repoRoot 'Tools/sample-validation' $appRoot = $PSScriptRoot $functionApiRoot = Join-Path $appRoot 'function-api' $clientRoot = Join-Path $appRoot 'react-client' diff --git a/Custom Apps/legal-docs/validate-sample.ps1 b/Custom Apps/legal-docs/validate-sample.ps1 index a418804..2424298 100644 --- a/Custom Apps/legal-docs/validate-sample.ps1 +++ b/Custom Apps/legal-docs/validate-sample.ps1 @@ -10,10 +10,10 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') +. (Join-Path $PSScriptRoot '../../Tools/powershell/SampleValidation.ps1') -$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path -$toolRoot = Join-Path $repoRoot 'Tools\sample-validation' +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +$toolRoot = Join-Path $repoRoot 'Tools/sample-validation' $appRoot = $PSScriptRoot $runtimeHandle = $null diff --git a/Custom Apps/project-management/validate-sample.ps1 b/Custom Apps/project-management/validate-sample.ps1 index 6ad34b0..86bb2ab 100644 --- a/Custom Apps/project-management/validate-sample.ps1 +++ b/Custom Apps/project-management/validate-sample.ps1 @@ -10,10 +10,10 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') +. (Join-Path $PSScriptRoot '../../Tools/powershell/SampleValidation.ps1') -$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path -$toolRoot = Join-Path $repoRoot 'Tools\sample-validation' +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +$toolRoot = Join-Path $repoRoot 'Tools/sample-validation' $appRoot = $PSScriptRoot $runtimeHandle = $null diff --git a/Custom Apps/webhook/validate-sample.ps1 b/Custom Apps/webhook/validate-sample.ps1 index 9ecc315..dd6d03e 100644 --- a/Custom Apps/webhook/validate-sample.ps1 +++ b/Custom Apps/webhook/validate-sample.ps1 @@ -10,7 +10,7 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -. (Join-Path $PSScriptRoot '..\..\Tools\powershell\SampleValidation.ps1') +. (Join-Path $PSScriptRoot '../../Tools/powershell/SampleValidation.ps1') $appRoot = $PSScriptRoot $packageRoot = Join-Path $appRoot 'src' @@ -44,7 +44,17 @@ try { } try { - $response = Invoke-WebRequest -Method Post -Uri $validationUrl -UseBasicParsing -TimeoutSec 5 -ContentType 'application/json' -Body '{}' + $invokeWebRequestArguments = @{ + Method = 'Post' + Uri = $validationUrl + TimeoutSec = 5 + ContentType = 'application/json' + Body = '{}' + } + if ((Get-Command Invoke-WebRequest).Parameters.ContainsKey('UseBasicParsing')) { + $invokeWebRequestArguments['UseBasicParsing'] = $true + } + $response = Invoke-WebRequest @invokeWebRequestArguments if ([string]$response.Content -eq $validationToken) { $validated = $true break diff --git a/Tools/powershell/SampleValidation.ps1 b/Tools/powershell/SampleValidation.ps1 index 7c4f92d..f215afa 100644 --- a/Tools/powershell/SampleValidation.ps1 +++ b/Tools/powershell/SampleValidation.ps1 @@ -232,7 +232,15 @@ function Wait-ForHttpEndpoint { } try { - $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 5 -MaximumRedirection 0 + $invokeWebRequestArguments = @{ + Uri = $Url + TimeoutSec = 5 + MaximumRedirection = 0 + } + if ((Get-Command Invoke-WebRequest).Parameters.ContainsKey('UseBasicParsing')) { + $invokeWebRequestArguments['UseBasicParsing'] = $true + } + $response = Invoke-WebRequest @invokeWebRequestArguments if ($AllowedStatusCodes -contains [int]$response.StatusCode) { return $response } @@ -301,7 +309,7 @@ function Ensure-BrowserTooling { [switch]$SkipInstall ) - $playwrightPath = Join-Path $ToolRoot 'node_modules\playwright' + $playwrightPath = Join-Path $ToolRoot 'node_modules/playwright' if (-not (Test-Path $playwrightPath)) { if ($SkipInstall) { Write-Host 'Shared browser validation tooling is missing; installing it even though -SkipInstall was specified.' -ForegroundColor Yellow From cf55ade92a5c0388663742880280191956ee16a8 Mon Sep 17 00:00:00 2001 From: Greg Joseph Date: Tue, 30 Jun 2026 11:45:03 -0700 Subject: [PATCH 03/11] fix(validation): launch sample processes via executable shim on Windows Resolve-CommandPath returned the highest-precedence Get-Command match, which on Windows is the PowerShell script shim (e.g. npx.ps1). Start- LoggedProcess passes that path to Start-Process with output redirection, forcing the CreateProcess code path -- which cannot execute .ps1 (or the extensionless npm/npx) shims. Every Windows runtime/browser smoke step therefore failed immediately with "%1 is not a valid Win32 application". Resolve a directly executable form (.cmd/.exe/...) when one exists, while falling back to the Application command form for Linux/macOS, where the shebang executable can be exec'd directly. The call operator path (Invoke-ExternalCommand) is unaffected since it runs .cmd shims fine too. Verified on Windows (pwsh 7.4): Resolve-CommandPath now returns npx.cmd / npm.cmd / node.exe; Start-LoggedProcess launches with redirection without error; and project-management/validate-sample.ps1 reaches and passes the preview-server startup it previously crashed on (exit code 0). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Tools/powershell/SampleValidation.ps1 | 33 +++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/Tools/powershell/SampleValidation.ps1 b/Tools/powershell/SampleValidation.ps1 index f215afa..127abe8 100644 --- a/Tools/powershell/SampleValidation.ps1 +++ b/Tools/powershell/SampleValidation.ps1 @@ -30,12 +30,37 @@ function Resolve-CommandPath { return $Name } - $command = Get-Command $Name -ErrorAction Stop - if ($null -ne $command.Source -and $command.Source.Length -gt 0) { - return $command.Source + $commands = @(Get-Command $Name -All -ErrorAction Stop) + + # Start-LoggedProcess launches commands through Start-Process, which (when output is + # redirected) uses the OS CreateProcess API. CreateProcess cannot execute PowerShell + # script shims (.ps1) and, on Windows, cannot execute the extensionless shell-script + # shims that npm/npx ship. Get-Command returns the .ps1 shim first by precedence, so + # prefer a directly executable form (.cmd/.exe/...) that both the call operator and + # Start-Process can launch. + $executableExtensions = @('.exe', '.cmd', '.bat', '.com') + + $preferred = $commands | Where-Object { + $_.CommandType -eq 'Application' -and + $_.Source -and + $executableExtensions -contains [System.IO.Path]::GetExtension($_.Source).ToLowerInvariant() + } | Select-Object -First 1 + + if ($null -eq $preferred) { + # On non-Windows platforms the Application form is typically an extensionless + # executable or a shebang script that CreateProcess can exec directly. + $preferred = $commands | Where-Object { $_.CommandType -eq 'Application' } | Select-Object -First 1 } - return $command.Name + if ($null -eq $preferred) { + $preferred = $commands | Select-Object -First 1 + } + + if ($null -ne $preferred.Source -and $preferred.Source.Length -gt 0) { + return $preferred.Source + } + + return $preferred.Name } function Invoke-ExternalCommand { From be52813ac84b4decc64ea5083be9d0a7e8ec2f09 Mon Sep 17 00:00:00 2001 From: dilucesr Date: Thu, 2 Jul 2026 17:42:29 -0700 Subject: [PATCH 04/11] Fix tests and validator for ocr app --- AI/ocr/.gitignore | 2 + AI/ocr/server/.dist/.gitkeep | 0 AI/ocr/server/ReceiptProcessor.ts | 28 +++++++++--- AI/ocr/server/tsconfig.json | 3 +- AI/ocr/src/App.test.js | 28 ++++++++++-- AI/ocr/tsconfig.json | 2 +- AI/ocr/validate-sample.ps1 | 45 ++++++++++++++----- .../function-api/tsconfig.json | 4 +- 8 files changed, 89 insertions(+), 23 deletions(-) create mode 100644 AI/ocr/server/.dist/.gitkeep diff --git a/AI/ocr/.gitignore b/AI/ocr/.gitignore index 4d29575..c5431c8 100644 --- a/AI/ocr/.gitignore +++ b/AI/ocr/.gitignore @@ -10,6 +10,8 @@ # production /build +**/.dist/*.js +**/.dist/**/*.js # misc .DS_Store diff --git a/AI/ocr/server/.dist/.gitkeep b/AI/ocr/server/.dist/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/AI/ocr/server/ReceiptProcessor.ts b/AI/ocr/server/ReceiptProcessor.ts index b017ab8..487f8ab 100644 --- a/AI/ocr/server/ReceiptProcessor.ts +++ b/AI/ocr/server/ReceiptProcessor.ts @@ -7,6 +7,11 @@ import axios, { AxiosRequestConfig } from 'axios'; export abstract class ReceiptProcessor { public static async processDrive(driveId: string): Promise { + if (!this.hasDocumentAnalysisConfig()) { + console.log('Skipping receipt processing because DAC_RESOURCE_ENDPOINT or DAC_RESOURCE_KEY is not configured.'); + return; + } + const changedItems = await GraphProvider.getDriveChanges(driveId); for (const changedItem of changedItems) { if (changedItem.deleted && changedItem.deleted.state === "deleted") { @@ -33,13 +38,23 @@ export abstract class ReceiptProcessor { return name.split('.')[0]; } - private static dac = new DocumentAnalysisClient( - `${process.env["DAC_RESOURCE_ENDPOINT"]}`, - new AzureKeyCredential(`${process.env["DAC_RESOURCE_KEY"]}`) - ); - private static readonly SUPPORTED_FILE_EXTENSIONS = ['jpeg', 'jpg', 'png', 'bmp', 'tiff', 'pdf']; + private static hasDocumentAnalysisConfig(): boolean { + return Boolean(process.env["DAC_RESOURCE_ENDPOINT"] && process.env["DAC_RESOURCE_KEY"]); + } + + private static getDocumentAnalysisClient(): DocumentAnalysisClient { + const endpoint = process.env["DAC_RESOURCE_ENDPOINT"]; + const key = process.env["DAC_RESOURCE_KEY"]; + + if (!endpoint || !key) { + throw new Error('DAC_RESOURCE_ENDPOINT and DAC_RESOURCE_KEY must be configured to process receipts.'); + } + + return new DocumentAnalysisClient(endpoint, new AzureKeyCredential(key)); + } + private static async getDriveItemStream(url: string): Promise { const token = GraphProvider.graphAccessToken; const config: AxiosRequestConfig = { @@ -59,8 +74,9 @@ export abstract class ReceiptProcessor { } private static async analyzeReceiptStream(stream: Readable): Promise { + const client = this.getDocumentAnalysisClient(); - const poller = await this.dac.beginAnalyzeDocument("prebuilt-invoice", stream, { + const poller = await client.beginAnalyzeDocument("prebuilt-invoice", stream, { onProgress: ({ status }) => { console.log(`status: ${status}`); }, diff --git a/AI/ocr/server/tsconfig.json b/AI/ocr/server/tsconfig.json index 47e10e9..9127456 100644 --- a/AI/ocr/server/tsconfig.json +++ b/AI/ocr/server/tsconfig.json @@ -1,6 +1,7 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { + "outDir": ".dist", "target": "ES2015", "module": "commonjs", "lib": [ diff --git a/AI/ocr/src/App.test.js b/AI/ocr/src/App.test.js index 1f03afe..9580223 100644 --- a/AI/ocr/src/App.test.js +++ b/AI/ocr/src/App.test.js @@ -1,8 +1,30 @@ import { render, screen } from '@testing-library/react'; import App from './App'; -test('renders learn react link', () => { +jest.mock('@microsoft/mgt-element', () => ({ + Providers: { + globalProvider: undefined, + onProviderUpdated: jest.fn(), + removeProviderUpdatedListener: jest.fn() + }, + ProviderState: { + SignedIn: 'SignedIn' + } +})); + +jest.mock('@microsoft/mgt-react', () => ({ + Login: () => +})); + +jest.mock('@azure/msal-browser', () => ({ + InteractionRequiredAuthError: class InteractionRequiredAuthError extends Error {}, + PublicClientApplication: jest.fn().mockImplementation(() => ({ + acquireTokenSilent: jest.fn(), + acquireTokenPopup: jest.fn() + })) +})); + +test('renders the OCR sample title', () => { render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); + expect(screen.getByText(/sample spa sharepoint embedded app/i)).toBeInTheDocument(); }); diff --git a/AI/ocr/tsconfig.json b/AI/ocr/tsconfig.json index 2543390..175a0cf 100644 --- a/AI/ocr/tsconfig.json +++ b/AI/ocr/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ES2015", "module": "esnext", diff --git a/AI/ocr/validate-sample.ps1 b/AI/ocr/validate-sample.ps1 index d39d79b..ecb6e68 100644 --- a/AI/ocr/validate-sample.ps1 +++ b/AI/ocr/validate-sample.ps1 @@ -16,8 +16,28 @@ $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path $toolRoot = Join-Path $repoRoot 'Tools/sample-validation' $appRoot = $PSScriptRoot $envFile = Join-Path $appRoot '.env' +$constantsFile = Join-Path $appRoot 'src/common/constants.ts' $handles = @() +function Test-OcrFrontendConfigured { + param( + [Parameter(Mandatory = $true)] + [string]$ConstantsPath + ) + + if (-not (Test-Path $ConstantsPath)) { + return $false + } + + $content = Get-Content -Path $ConstantsPath -Raw + $clientIdMatch = [regex]::Match($content, "CLIENT_ENTRA_APP_CLIENT_ID\s*=\s*'([^']*)'") + if (-not $clientIdMatch.Success) { + return $false + } + + return -not [string]::IsNullOrWhiteSpace($clientIdMatch.Groups[1].Value) +} + try { Write-Step 'Preflight checks' Assert-CommandExists 'node' @@ -54,18 +74,23 @@ try { $handles += $backendHandle [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:3001/api/echo' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $backendHandle) - Write-Step 'Starting frontend' - $frontendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'ocr-frontend' - $frontendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start-cre') -WorkingDirectory $appRoot -LogPath $frontendLog -Environment @{ BROWSER = 'none'; PORT = '3102' } - $handles += $frontendHandle - [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:3102' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $frontendHandle) - - if ($SkipBrowser) { - Write-Host 'Skipping browser smoke because -SkipBrowser was specified.' -ForegroundColor Yellow + if (-not (Test-OcrFrontendConfigured -ConstantsPath $constantsFile)) { + Write-Host 'Skipping frontend runtime smoke because src/common/constants.ts does not contain a configured CLIENT_ENTRA_APP_CLIENT_ID.' -ForegroundColor Yellow } else { - Write-Step 'Running browser smoke' - Invoke-BrowserSmoke -ToolRoot $toolRoot -Url 'http://127.0.0.1:3102' -SkipInstall:$SkipInstall -Headed:$Headed -TimeoutSec $TimeoutSec -ExpectSelector '#root' + Write-Step 'Starting frontend' + $frontendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'ocr-frontend' + $frontendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start-cre') -WorkingDirectory $appRoot -LogPath $frontendLog -Environment @{ BROWSER = 'none'; PORT = '3102' } + $handles += $frontendHandle + [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:3102' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $frontendHandle) + + if ($SkipBrowser) { + Write-Host 'Skipping browser smoke because -SkipBrowser was specified.' -ForegroundColor Yellow + } + else { + Write-Step 'Running browser smoke' + Invoke-BrowserSmoke -ToolRoot $toolRoot -Url 'http://127.0.0.1:3102' -SkipInstall:$SkipInstall -Headed:$Headed -TimeoutSec $TimeoutSec -ExpectSelector '#root' + } } Write-Host 'OCR sample validation completed.' -ForegroundColor Green diff --git a/Custom Apps/boilerplate-typescript-react/function-api/tsconfig.json b/Custom Apps/boilerplate-typescript-react/function-api/tsconfig.json index 8ba78c0..f5ae6a0 100644 --- a/Custom Apps/boilerplate-typescript-react/function-api/tsconfig.json +++ b/Custom Apps/boilerplate-typescript-react/function-api/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ES2015", "module": "commonjs", @@ -10,7 +10,7 @@ "es2015.collection" ], "esModuleInterop": true, - "moduleResolution": "node", + "moduleResolution": "node16", "skipLibCheck": true, "strict": true, "sourceMap": true, From 376996e0bcccf8ea4a26638d23bb0b881457250b Mon Sep 17 00:00:00 2001 From: dilucesr Date: Fri, 3 Jul 2026 09:30:58 -0700 Subject: [PATCH 05/11] Fix Custom Apps validation scripts --- .../packages/client-app/src/App.test.js | 28 +++++++++++++++++-- .../function-api/package-lock.json | 3 ++ .../validate-sample.ps1 | 2 +- Custom Apps/legal-docs/validate-sample.ps1 | 11 ++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/Custom Apps/boilerplate-react-azurefunction/packages/client-app/src/App.test.js b/Custom Apps/boilerplate-react-azurefunction/packages/client-app/src/App.test.js index 1f03afe..ab392d7 100644 --- a/Custom Apps/boilerplate-react-azurefunction/packages/client-app/src/App.test.js +++ b/Custom Apps/boilerplate-react-azurefunction/packages/client-app/src/App.test.js @@ -1,8 +1,30 @@ import { render, screen } from '@testing-library/react'; import App from './App'; -test('renders learn react link', () => { +jest.mock('@microsoft/mgt-element', () => ({ + Providers: { + globalProvider: undefined, + onProviderUpdated: jest.fn(), + removeProviderUpdatedListener: jest.fn() + }, + ProviderState: { + SignedIn: 'SignedIn' + } +})); + +jest.mock('@microsoft/mgt-react', () => ({ + Login: () => +})); + +jest.mock('@azure/msal-browser', () => ({ + InteractionRequiredAuthError: class InteractionRequiredAuthError extends Error {}, + PublicClientApplication: jest.fn().mockImplementation(() => ({ + acquireTokenSilent: jest.fn(), + acquireTokenPopup: jest.fn() + })) +})); + +test('renders the sample app title', () => { render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); + expect(screen.getByText(/sample sharepoint embedded app/i)).toBeInTheDocument(); }); diff --git a/Custom Apps/boilerplate-typescript-react/function-api/package-lock.json b/Custom Apps/boilerplate-typescript-react/function-api/package-lock.json index 0d232ef..c99458a 100644 --- a/Custom Apps/boilerplate-typescript-react/function-api/package-lock.json +++ b/Custom Apps/boilerplate-typescript-react/function-api/package-lock.json @@ -22,6 +22,9 @@ "@types/node": "^18.x", "rimraf": "^5.0.0", "typescript": "^4.0.0" + }, + "engines": { + "node": ">=20" } }, "node_modules/@azure/abort-controller": { diff --git a/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 b/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 index ecf20c1..6b8d432 100644 --- a/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 +++ b/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 @@ -48,7 +48,7 @@ try { } else { Write-Step 'Running react-client tests' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test', '--', '--watchAll=false') -WorkingDirectory $clientRoot -Environment @{ CI = 'true' } + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test', '--', '--watchAll=false', '--passWithNoTests') -WorkingDirectory $clientRoot -Environment @{ CI = 'true' } } if (Test-Path $localSettingsPath) { diff --git a/Custom Apps/legal-docs/validate-sample.ps1 b/Custom Apps/legal-docs/validate-sample.ps1 index 2424298..e5522c8 100644 --- a/Custom Apps/legal-docs/validate-sample.ps1 +++ b/Custom Apps/legal-docs/validate-sample.ps1 @@ -17,12 +17,23 @@ $toolRoot = Join-Path $repoRoot 'Tools/sample-validation' $appRoot = $PSScriptRoot $runtimeHandle = $null +function Test-LegalDocsNodeSupported { + $nodeVersionText = (& (Resolve-CommandPath -Name 'node') '--version').Trim() + $nodeVersion = [Version]($nodeVersionText.TrimStart('v')) + return (($nodeVersion.Major -eq 20 -and $nodeVersion -ge [Version]'20.19.0') -or $nodeVersion -ge [Version]'22.12.0') +} + try { Write-Step 'Preflight checks' Assert-CommandExists 'node' Assert-CommandExists 'npm' Assert-CommandExists 'npx' + if (-not (Test-LegalDocsNodeSupported)) { + Write-Host 'Skipping legal-docs validation because the current Node.js runtime is below Vite 8 requirements (20.19+ or 22.12+).' -ForegroundColor Yellow + return + } + if (-not $SkipInstall) { Write-Step 'Installing dependencies' Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot From 8703280d717a2a2e9b97c829283fc5f191b50902 Mon Sep 17 00:00:00 2001 From: dilucesr Date: Fri, 3 Jul 2026 09:51:38 -0700 Subject: [PATCH 06/11] Enhance validation scripts to show script result at the end --- AI/mcp-server/validate-sample.ps1 | 16 ++-- AI/ocr/validate-sample.ps1 | 22 ++++-- .../validate-sample.ps1 | 7 +- .../validate-sample.ps1 | 29 +++++-- .../validate-sample.ps1 | 31 ++++++-- Custom Apps/legal-docs/validate-sample.ps1 | 18 +++-- .../project-management/validate-sample.ps1 | 14 +++- Custom Apps/webhook/validate-sample.ps1 | 10 ++- README.md | 4 + Tools/powershell/SampleValidation.ps1 | 75 ++++++++++++++++++- 10 files changed, 183 insertions(+), 43 deletions(-) diff --git a/AI/mcp-server/validate-sample.ps1 b/AI/mcp-server/validate-sample.ps1 index e5489ac..47be756 100644 --- a/AI/mcp-server/validate-sample.ps1 +++ b/AI/mcp-server/validate-sample.ps1 @@ -14,6 +14,7 @@ $ErrorActionPreference = 'Stop' $appRoot = $PSScriptRoot $envFile = Join-Path $appRoot '.env' +$nodeEnvironment = Get-ValidationNodeEnvironment $runtimeHandle = $null try { @@ -23,11 +24,11 @@ try { if (-not $SkipInstall) { Write-Step 'Installing dependencies' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot -Environment $nodeEnvironment } Write-Step 'Building MCP server' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $appRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $appRoot -Environment $nodeEnvironment if ($SkipTests) { Write-Host 'Skipping tests because -SkipTests was specified.' -ForegroundColor Yellow @@ -38,7 +39,7 @@ try { if (-not (Test-Path $envFile)) { Write-Host 'Skipping runtime smoke check because .env is missing.' -ForegroundColor Yellow - Write-Host 'Build validation completed.' -ForegroundColor Green + Write-ValidationSummary -Status 'SKIP_CONFIG' -Message 'Build validation passed; runtime smoke skipped because .env is missing.' return } @@ -49,16 +50,21 @@ try { Write-Step 'Starting MCP server' $logPath = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'mcp-server' - $runtimeHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $appRoot -LogPath $logPath -Environment $environment + $runtimeHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $appRoot -LogPath $logPath -Environment (Merge-EnvironmentTables @($nodeEnvironment, $environment)) $healthUrl = "http://localhost:$($environment['PORT'])/health" - $healthResponse = Wait-ForHttpEndpoint -Url $healthUrl -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) + $healthResponse = Wait-ForHttpEndpoint -Url $healthUrl -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $runtimeHandle $body = [string]$healthResponse.Content if ($body -notmatch '"status"\s*:\s*"ok"') { throw "Health endpoint returned an unexpected response: $body" } Write-Host "Runtime smoke check passed at $healthUrl" -ForegroundColor Green + Write-ValidationSummary -Status 'PASS' -Message "Build and runtime smoke checks passed at $healthUrl." +} +catch { + Write-ValidationSummary -Status 'FAIL' -Message $_.Exception.Message + throw } finally { if ($null -ne $runtimeHandle -and -not $KeepProcesses) { diff --git a/AI/ocr/validate-sample.ps1 b/AI/ocr/validate-sample.ps1 index ecb6e68..cb39678 100644 --- a/AI/ocr/validate-sample.ps1 +++ b/AI/ocr/validate-sample.ps1 @@ -17,6 +17,7 @@ $toolRoot = Join-Path $repoRoot 'Tools/sample-validation' $appRoot = $PSScriptRoot $envFile = Join-Path $appRoot '.env' $constantsFile = Join-Path $appRoot 'src/common/constants.ts' +$nodeEnvironment = Get-ValidationNodeEnvironment $handles = @() function Test-OcrFrontendConfigured { @@ -45,42 +46,43 @@ try { if (-not $SkipInstall) { Write-Step 'Installing dependencies' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot -Environment $nodeEnvironment } Write-Step 'Building backend' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build:backend') -WorkingDirectory $appRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build:backend') -WorkingDirectory $appRoot -Environment $nodeEnvironment Write-Step 'Building frontend' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build-cre') -WorkingDirectory $appRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build-cre') -WorkingDirectory $appRoot -Environment $nodeEnvironment if ($SkipTests) { Write-Host 'Skipping frontend tests because -SkipTests was specified.' -ForegroundColor Yellow } else { Write-Step 'Running frontend tests' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test-cre', '--', '--watchAll=false') -WorkingDirectory $appRoot -Environment @{ CI = 'true' } + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test-cre', '--', '--watchAll=false') -WorkingDirectory $appRoot -Environment (Merge-EnvironmentTables @($nodeEnvironment, @{ CI = 'true' })) } if (-not (Test-Path $envFile)) { Write-Host 'Skipping runtime smoke checks because .env is missing.' -ForegroundColor Yellow - Write-Host 'Build and test validation completed.' -ForegroundColor Green + Write-ValidationSummary -Status 'SKIP_CONFIG' -Message 'Build and tests passed; runtime smoke skipped because .env is missing.' return } Write-Step 'Starting backend' $backendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'ocr-backend' - $backendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start:backend') -WorkingDirectory $appRoot -LogPath $backendLog -Environment @{ PORT = '3001' } + $backendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start:backend') -WorkingDirectory $appRoot -LogPath $backendLog -Environment (Merge-EnvironmentTables @($nodeEnvironment, @{ PORT = '3001' })) $handles += $backendHandle [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:3001/api/echo' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $backendHandle) if (-not (Test-OcrFrontendConfigured -ConstantsPath $constantsFile)) { Write-Host 'Skipping frontend runtime smoke because src/common/constants.ts does not contain a configured CLIENT_ENTRA_APP_CLIENT_ID.' -ForegroundColor Yellow + Write-ValidationSummary -Status 'SKIP_CONFIG' -Message 'Build, tests, and backend smoke passed; frontend runtime smoke skipped because CLIENT_ENTRA_APP_CLIENT_ID is not configured.' } else { Write-Step 'Starting frontend' $frontendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'ocr-frontend' - $frontendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start-cre') -WorkingDirectory $appRoot -LogPath $frontendLog -Environment @{ BROWSER = 'none'; PORT = '3102' } + $frontendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start-cre') -WorkingDirectory $appRoot -LogPath $frontendLog -Environment (Merge-EnvironmentTables @($nodeEnvironment, @{ BROWSER = 'none'; PORT = '3102' })) $handles += $frontendHandle [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:3102' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $frontendHandle) @@ -91,10 +93,16 @@ try { Write-Step 'Running browser smoke' Invoke-BrowserSmoke -ToolRoot $toolRoot -Url 'http://127.0.0.1:3102' -SkipInstall:$SkipInstall -Headed:$Headed -TimeoutSec $TimeoutSec -ExpectSelector '#root' } + + Write-ValidationSummary -Status 'PASS' -Message 'Build, tests, backend smoke, and frontend runtime smoke checks passed.' } Write-Host 'OCR sample validation completed.' -ForegroundColor Green } +catch { + Write-ValidationSummary -Status 'FAIL' -Message $_.Exception.Message + throw +} finally { if (-not $KeepProcesses) { foreach ($handle in ($handles | Sort-Object -Descending -Property LogPath)) { diff --git a/Custom Apps/boilerplate-aspnet-webservice/validate-sample.ps1 b/Custom Apps/boilerplate-aspnet-webservice/validate-sample.ps1 index d96bdb7..6b681eb 100644 --- a/Custom Apps/boilerplate-aspnet-webservice/validate-sample.ps1 +++ b/Custom Apps/boilerplate-aspnet-webservice/validate-sample.ps1 @@ -30,7 +30,7 @@ try { if (-not (Test-Path $appSettingsPath)) { Write-Host 'Skipping runtime smoke check because appsettings.json is missing.' -ForegroundColor Yellow - Write-Host 'Build validation completed.' -ForegroundColor Green + Write-ValidationSummary -Status 'SKIP_CONFIG' -Message 'Restore and build passed; runtime smoke skipped because appsettings.json is missing.' return } @@ -40,6 +40,11 @@ try { [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:5080' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200, 302) -ProcessHandle $runtimeHandle) Write-Host 'ASP.NET sample validation completed.' -ForegroundColor Green + Write-ValidationSummary -Status 'PASS' -Message 'Restore, build, and runtime smoke checks passed.' +} +catch { + Write-ValidationSummary -Status 'FAIL' -Message $_.Exception.Message + throw } finally { if ($null -ne $runtimeHandle -and -not $KeepProcesses) { diff --git a/Custom Apps/boilerplate-react-azurefunction/validate-sample.ps1 b/Custom Apps/boilerplate-react-azurefunction/validate-sample.ps1 index 7e3c052..a67e0af 100644 --- a/Custom Apps/boilerplate-react-azurefunction/validate-sample.ps1 +++ b/Custom Apps/boilerplate-react-azurefunction/validate-sample.ps1 @@ -19,7 +19,9 @@ $clientRoot = Join-Path $appRoot 'packages/client-app' $functionsRoot = Join-Path $appRoot 'packages/azure-functions' $clientEnvPath = Join-Path $clientRoot '.env' $localSettingsPath = Join-Path $functionsRoot 'local.settings.json' +$nodeEnvironment = Get-ValidationNodeEnvironment $handles = @() +$runtimeSkipReasons = @() try { Write-Step 'Preflight checks' @@ -28,42 +30,43 @@ try { if (-not $SkipInstall) { Write-Step 'Installing root dependencies' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot -Environment $nodeEnvironment Write-Step 'Installing client-app dependencies' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $clientRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $clientRoot -Environment $nodeEnvironment Write-Step 'Installing azure-functions dependencies' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $functionsRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $functionsRoot -Environment $nodeEnvironment } Write-Step 'Building client-app' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $clientRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $clientRoot -Environment $nodeEnvironment if ($SkipTests) { Write-Host 'Skipping client-app tests because -SkipTests was specified.' -ForegroundColor Yellow } else { Write-Step 'Running client-app tests' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test', '--', '--watchAll=false') -WorkingDirectory $clientRoot -Environment @{ CI = 'true' } + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test', '--', '--watchAll=false') -WorkingDirectory $clientRoot -Environment (Merge-EnvironmentTables @($nodeEnvironment, @{ CI = 'true' })) } if (Test-Path $localSettingsPath) { Assert-CommandExists 'func' Write-Step 'Starting Azure Functions host' $backendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'react-azure-functions-api' - $backendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $functionsRoot -LogPath $backendLog + $backendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $functionsRoot -LogPath $backendLog -Environment $nodeEnvironment $handles += $backendHandle [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:7071/api/ListContainers' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200, 401) -ProcessHandle $backendHandle) } else { Write-Host 'Skipping Azure Functions runtime smoke because local.settings.json is missing.' -ForegroundColor Yellow + $runtimeSkipReasons += 'packages/azure-functions/local.settings.json is missing' } if (Test-Path $clientEnvPath) { Write-Step 'Starting client-app' $frontendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'react-azure-functions-client' - $frontendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $clientRoot -LogPath $frontendLog -Environment @{ BROWSER = 'none'; PORT = '3000' } + $frontendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $clientRoot -LogPath $frontendLog -Environment (Merge-EnvironmentTables @($nodeEnvironment, @{ BROWSER = 'none'; PORT = '3000' })) $handles += $frontendHandle [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:3000' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $frontendHandle) @@ -77,9 +80,21 @@ try { } else { Write-Host 'Skipping client-app runtime smoke because .env is missing.' -ForegroundColor Yellow + $runtimeSkipReasons += 'packages/client-app/.env is missing' } Write-Host 'React Azure Functions sample validation completed.' -ForegroundColor Green + + if ($runtimeSkipReasons.Count -gt 0) { + Write-ValidationSummary -Status 'SKIP_CONFIG' -Message "Build and tests passed; runtime smoke skipped because $($runtimeSkipReasons -join '; ')." + } + else { + Write-ValidationSummary -Status 'PASS' -Message 'Build, tests, Functions host startup, and frontend runtime smoke checks passed.' + } +} +catch { + Write-ValidationSummary -Status 'FAIL' -Message $_.Exception.Message + throw } finally { if (-not $KeepProcesses) { diff --git a/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 b/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 index 6b8d432..1ca4f16 100644 --- a/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 +++ b/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 @@ -28,43 +28,46 @@ try { if (-not $SkipInstall) { Write-Step 'Installing root dependencies' + $nodeEnvironment = Get-ValidationNodeEnvironment Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot + $runtimeSkipReasons = @() Write-Step 'Installing function-api dependencies' Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $functionApiRoot - + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot -Environment $nodeEnvironment Write-Step 'Installing react-client dependencies' Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $clientRoot - } + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $functionApiRoot -Environment $nodeEnvironment Write-Step 'Building function-api' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $functionApiRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $clientRoot -Environment $nodeEnvironment Write-Step 'Building react-client' Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $clientRoot - + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $functionApiRoot -Environment $nodeEnvironment if ($SkipTests) { Write-Host 'Skipping react-client tests because -SkipTests was specified.' -ForegroundColor Yellow - } + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $clientRoot -Environment $nodeEnvironment else { Write-Step 'Running react-client tests' Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test', '--', '--watchAll=false', '--passWithNoTests') -WorkingDirectory $clientRoot -Environment @{ CI = 'true' } } if (Test-Path $localSettingsPath) { - Assert-CommandExists 'func' + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test', '--', '--watchAll=false', '--passWithNoTests') -WorkingDirectory $clientRoot -Environment (Merge-EnvironmentTables @($nodeEnvironment, @{ CI = 'true' })) Write-Step 'Starting function-api host' $backendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'typescript-react-api' $backendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $functionApiRoot -LogPath $backendLog $handles += $backendHandle [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:7072/api/containers' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200, 401) -ProcessHandle $backendHandle) } - else { + $backendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $functionApiRoot -LogPath $backendLog -Environment $nodeEnvironment Write-Host 'Skipping function-api runtime smoke because local.settings.json is missing.' -ForegroundColor Yellow } if (Test-Path $clientEnvPath) { $clientEnv = Get-DotEnvMap -Path $clientEnvPath + $runtimeSkipReasons += 'function-api/local.settings.json is missing' $clientPort = if ($clientEnv.ContainsKey('PORT')) { $clientEnv['PORT'] } else { '8080' } Write-Step 'Starting react-client' @@ -73,7 +76,7 @@ try { $handles += $frontendHandle [void](Wait-ForHttpEndpoint -Url "http://127.0.0.1:$clientPort" -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $frontendHandle) - if ($SkipBrowser) { + $frontendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $clientRoot -LogPath $frontendLog -Environment (Merge-EnvironmentTables @($nodeEnvironment, @{ BROWSER = 'none' })) Write-Host 'Skipping browser smoke because -SkipBrowser was specified.' -ForegroundColor Yellow } else { @@ -87,10 +90,22 @@ try { Write-Host 'TypeScript React sample validation completed.' -ForegroundColor Green } + $runtimeSkipReasons += 'react-client/.env is missing' finally { if (-not $KeepProcesses) { foreach ($handle in ($handles | Sort-Object -Descending -Property LogPath)) { + + if ($runtimeSkipReasons.Count -gt 0) { + Write-ValidationSummary -Status 'SKIP_CONFIG' -Message "Build and tests passed; runtime smoke skipped because $($runtimeSkipReasons -join '; ')." + } + else { + Write-ValidationSummary -Status 'PASS' -Message 'Build, tests, backend startup, and frontend runtime smoke checks passed.' + } Stop-LoggedProcess -Handle $handle + catch { + Write-ValidationSummary -Status 'FAIL' -Message $_.Exception.Message + throw + } } } } \ No newline at end of file diff --git a/Custom Apps/legal-docs/validate-sample.ps1 b/Custom Apps/legal-docs/validate-sample.ps1 index e5522c8..83b6ae1 100644 --- a/Custom Apps/legal-docs/validate-sample.ps1 +++ b/Custom Apps/legal-docs/validate-sample.ps1 @@ -15,11 +15,11 @@ $ErrorActionPreference = 'Stop' $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path $toolRoot = Join-Path $repoRoot 'Tools/sample-validation' $appRoot = $PSScriptRoot +$nodeEnvironment = Get-ValidationNodeEnvironment $runtimeHandle = $null function Test-LegalDocsNodeSupported { - $nodeVersionText = (& (Resolve-CommandPath -Name 'node') '--version').Trim() - $nodeVersion = [Version]($nodeVersionText.TrimStart('v')) + $nodeVersion = Get-ValidationNodeVersion return (($nodeVersion.Major -eq 20 -and $nodeVersion -ge [Version]'20.19.0') -or $nodeVersion -ge [Version]'22.12.0') } @@ -31,20 +31,21 @@ try { if (-not (Test-LegalDocsNodeSupported)) { Write-Host 'Skipping legal-docs validation because the current Node.js runtime is below Vite 8 requirements (20.19+ or 22.12+).' -ForegroundColor Yellow + Write-ValidationSummary -Status 'SKIP_ENV' -Message 'Validation skipped because legal-docs requires Node 20.19+ or 22.12+. Set VALIDATION_NODE_COMMAND to a compatible Node executable to run it.' return } if (-not $SkipInstall) { Write-Step 'Installing dependencies' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot -Environment $nodeEnvironment } Write-Step 'Building app' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $appRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $appRoot -Environment $nodeEnvironment Write-Step 'Linting app' try { - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'lint') -WorkingDirectory $appRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'lint') -WorkingDirectory $appRoot -Environment $nodeEnvironment } catch { Write-Host "Lint reported existing issues and will not block runtime validation: $($_.Exception.Message)" -ForegroundColor Yellow @@ -54,7 +55,7 @@ try { Write-Step 'Starting preview server' $logPath = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'legal-docs' - $runtimeHandle = Start-LoggedProcess -FilePath 'npx' -Arguments @('vite', 'preview', '--host', '127.0.0.1', '--port', '4173') -WorkingDirectory $appRoot -LogPath $logPath + $runtimeHandle = Start-LoggedProcess -FilePath 'npx' -Arguments @('vite', 'preview', '--host', '127.0.0.1', '--port', '4173') -WorkingDirectory $appRoot -LogPath $logPath -Environment $nodeEnvironment [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:4173' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $runtimeHandle) if ($SkipBrowser) { @@ -66,6 +67,11 @@ try { } Write-Host 'Legal docs sample validation completed.' -ForegroundColor Green + Write-ValidationSummary -Status 'PASS' -Message 'Build, preview startup, and browser smoke checks passed.' +} +catch { + Write-ValidationSummary -Status 'FAIL' -Message $_.Exception.Message + throw } finally { if ($null -ne $runtimeHandle -and -not $KeepProcesses) { diff --git a/Custom Apps/project-management/validate-sample.ps1 b/Custom Apps/project-management/validate-sample.ps1 index 86bb2ab..f7c75da 100644 --- a/Custom Apps/project-management/validate-sample.ps1 +++ b/Custom Apps/project-management/validate-sample.ps1 @@ -15,6 +15,7 @@ $ErrorActionPreference = 'Stop' $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path $toolRoot = Join-Path $repoRoot 'Tools/sample-validation' $appRoot = $PSScriptRoot +$nodeEnvironment = Get-ValidationNodeEnvironment $runtimeHandle = $null try { @@ -25,15 +26,15 @@ try { if (-not $SkipInstall) { Write-Step 'Installing dependencies' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot -Environment $nodeEnvironment } Write-Step 'Building app' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $appRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $appRoot -Environment $nodeEnvironment Write-Step 'Linting app' try { - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'lint') -WorkingDirectory $appRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'lint') -WorkingDirectory $appRoot -Environment $nodeEnvironment } catch { Write-Host "Lint reported existing issues and will not block runtime validation: $($_.Exception.Message)" -ForegroundColor Yellow @@ -43,7 +44,7 @@ try { Write-Step 'Starting preview server' $logPath = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'project-management' - $runtimeHandle = Start-LoggedProcess -FilePath 'npx' -Arguments @('vite', 'preview', '--host', '127.0.0.1', '--port', '4173') -WorkingDirectory $appRoot -LogPath $logPath + $runtimeHandle = Start-LoggedProcess -FilePath 'npx' -Arguments @('vite', 'preview', '--host', '127.0.0.1', '--port', '4173') -WorkingDirectory $appRoot -LogPath $logPath -Environment $nodeEnvironment [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:4173' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $runtimeHandle) if ($SkipBrowser) { @@ -55,6 +56,11 @@ try { } Write-Host 'Project management sample validation completed.' -ForegroundColor Green + Write-ValidationSummary -Status 'PASS' -Message 'Build, preview startup, and browser smoke checks passed.' +} +catch { + Write-ValidationSummary -Status 'FAIL' -Message $_.Exception.Message + throw } finally { if ($null -ne $runtimeHandle -and -not $KeepProcesses) { diff --git a/Custom Apps/webhook/validate-sample.ps1 b/Custom Apps/webhook/validate-sample.ps1 index dd6d03e..d324c8f 100644 --- a/Custom Apps/webhook/validate-sample.ps1 +++ b/Custom Apps/webhook/validate-sample.ps1 @@ -14,6 +14,7 @@ $ErrorActionPreference = 'Stop' $appRoot = $PSScriptRoot $packageRoot = Join-Path $appRoot 'src' +$nodeEnvironment = Get-ValidationNodeEnvironment $runtimeHandle = $null try { @@ -23,14 +24,14 @@ try { if (-not $SkipInstall) { Write-Step 'Installing dependencies' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $packageRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $packageRoot -Environment $nodeEnvironment } Write-Host 'No automated test script is defined for this sample.' -ForegroundColor Yellow Write-Step 'Starting webhook listener' $logPath = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'webhook' - $runtimeHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $packageRoot -LogPath $logPath -Environment @{ PORT = '3000' } + $runtimeHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $packageRoot -LogPath $logPath -Environment (Merge-EnvironmentTables @($nodeEnvironment, @{ PORT = '3000' })) $validationToken = 'sample-validation-token' $validationUrl = "http://127.0.0.1:3000/webhook?validationToken=$validationToken" @@ -71,6 +72,11 @@ try { } Write-Host 'Webhook sample validation completed.' -ForegroundColor Green + Write-ValidationSummary -Status 'PASS' -Message 'Webhook listener startup and validation-token echo checks passed.' +} +catch { + Write-ValidationSummary -Status 'FAIL' -Message $_.Exception.Message + throw } finally { if ($null -ne $runtimeHandle -and -not $KeepProcesses) { diff --git a/README.md b/README.md index 20e7425..9d8dacf 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,10 @@ Run the script from the sample root in PowerShell, for example: Samples that require local tenant configuration will automatically downgrade to build-only or startup-only validation when their expected `.env`, `local.settings.json`, or `appsettings.json` files are missing. +Each script ends with a standardized summary line such as `VALIDATION_RESULT: PASS`, `VALIDATION_RESULT: SKIP_CONFIG`, or `VALIDATION_RESULT: SKIP_ENV` so the outcome is easy to parse. + +If a sample needs a newer Node.js runtime than the machine default, set `VALIDATION_NODE_COMMAND` to a compatible `node.exe` path before running the validator. This is especially useful for Vite-based samples that require a newer Node release than older CRA-based samples. + ## AI Samples and assets for integrating SharePoint Embedded with AI tools and services. diff --git a/Tools/powershell/SampleValidation.ps1 b/Tools/powershell/SampleValidation.ps1 index f215afa..2b4deea 100644 --- a/Tools/powershell/SampleValidation.ps1 +++ b/Tools/powershell/SampleValidation.ps1 @@ -9,6 +9,25 @@ function Write-Step { Write-Host "`n==> $Message" -ForegroundColor Cyan } +function Write-ValidationSummary { + param( + [Parameter(Mandatory = $true)] + [ValidateSet('PASS', 'FAIL', 'SKIP_ENV', 'SKIP_CONFIG')] + [string]$Status, + + [Parameter(Mandatory = $true)] + [string]$Message + ) + + $color = switch ($Status) { + 'PASS' { 'Green' } + 'FAIL' { 'Red' } + default { 'Yellow' } + } + + Write-Host "VALIDATION_RESULT: $Status - $Message" -ForegroundColor $color +} + function Assert-CommandExists { param( [Parameter(Mandatory = $true)] @@ -38,6 +57,54 @@ function Resolve-CommandPath { return $command.Name } +function Merge-EnvironmentTables { + param( + [hashtable[]]$Tables + ) + + $merged = @{} + foreach ($table in $Tables) { + if ($null -eq $table) { + continue + } + + foreach ($key in $table.Keys) { + $merged[$key] = $table[$key] + } + } + + return $merged +} + +function Get-ValidationNodeCommand { + $configured = [Environment]::GetEnvironmentVariable('VALIDATION_NODE_COMMAND') + if (-not [string]::IsNullOrWhiteSpace($configured)) { + return $configured.Trim() + } + + return 'node' +} + +function Get-ValidationNodeEnvironment { + $resolvedNodePath = Resolve-CommandPath -Name (Get-ValidationNodeCommand) + if (-not (Test-Path $resolvedNodePath)) { + return @{} + } + + $nodeDirectory = Split-Path -Parent $resolvedNodePath + if ([string]::IsNullOrWhiteSpace($nodeDirectory)) { + return @{} + } + + return @{ PATH = "$nodeDirectory;$([Environment]::GetEnvironmentVariable('PATH'))" } +} + +function Get-ValidationNodeVersion { + $nodeCommand = Resolve-CommandPath -Name (Get-ValidationNodeCommand) + $nodeVersionText = (& $nodeCommand '--version').Trim() + return [Version]($nodeVersionText.TrimStart('v')) +} + function Invoke-ExternalCommand { param( [Parameter(Mandatory = $true)] @@ -309,6 +376,7 @@ function Ensure-BrowserTooling { [switch]$SkipInstall ) + $nodeEnvironment = Get-ValidationNodeEnvironment $playwrightPath = Join-Path $ToolRoot 'node_modules/playwright' if (-not (Test-Path $playwrightPath)) { if ($SkipInstall) { @@ -316,11 +384,11 @@ function Ensure-BrowserTooling { } Write-Step 'Installing shared browser validation tooling' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $ToolRoot + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $ToolRoot -Environment $nodeEnvironment } Write-Step 'Installing Chromium for Playwright' - Invoke-ExternalCommand -FilePath 'npx' -Arguments @('playwright', 'install', 'chromium') -WorkingDirectory $ToolRoot + Invoke-ExternalCommand -FilePath 'npx' -Arguments @('playwright', 'install', 'chromium') -WorkingDirectory $ToolRoot -Environment $nodeEnvironment } function Invoke-BrowserSmoke { @@ -348,6 +416,7 @@ function Invoke-BrowserSmoke { [switch]$FailOnConsoleError ) + $nodeEnvironment = Get-ValidationNodeEnvironment Ensure-BrowserTooling -ToolRoot $ToolRoot -SkipInstall:$SkipInstall $arguments = @( @@ -373,5 +442,5 @@ function Invoke-BrowserSmoke { $arguments += '--fail-on-console-error' } - Invoke-ExternalCommand -FilePath 'node' -Arguments $arguments -WorkingDirectory $ToolRoot + Invoke-ExternalCommand -FilePath (Get-ValidationNodeCommand) -Arguments $arguments -WorkingDirectory $ToolRoot -Environment $nodeEnvironment } \ No newline at end of file From d6067306a9eaaf5520314895795b6eacfde21534 Mon Sep 17 00:00:00 2001 From: dilucesr Date: Fri, 3 Jul 2026 10:05:44 -0700 Subject: [PATCH 07/11] Fix linting errors --- .../copilot/CopilotChatContainer.tsx | 8 +++- .../legal-docs/src/components/ui/command.tsx | 2 +- .../legal-docs/src/components/ui/textarea.tsx | 2 +- .../legal-docs/src/services/copilotChat.ts | 29 ++++++++++-- Custom Apps/legal-docs/tailwind.config.ts | 3 +- Custom Apps/legal-docs/validate-sample.ps1 | 7 +-- .../src/components/CreateContainerForm.tsx | 5 +- .../src/components/DevModePanel.tsx | 4 +- .../copilot/CopilotChatContainer.tsx | 30 ++++++------ .../components/copilot/CopilotDesktopView.tsx | 19 ++++---- .../components/dashboard/RollupDashboard.tsx | 46 ++++++++++++++++--- .../src/components/ui/command.tsx | 2 +- .../src/components/ui/drawer.tsx | 19 ++++---- .../src/components/ui/textarea.tsx | 2 +- .../src/context/ApiCallsContext.tsx | 4 +- .../src/hooks/useContainerDetails.ts | 5 +- .../src/hooks/useFilePreview.ts | 5 +- .../project-management/src/hooks/useFiles.ts | 32 +++++++------ .../project-management/src/lib/utils.ts | 30 ++++++++++++ .../project-management/src/pages/Index.tsx | 23 +++++++--- .../project-management/src/pages/Projects.tsx | 12 +++-- .../src/pages/SearchResults.tsx | 16 ++++--- .../src/services/sharePointService.ts | 2 +- .../project-management/tailwind.config.ts | 3 +- .../project-management/validate-sample.ps1 | 7 +-- 25 files changed, 212 insertions(+), 105 deletions(-) diff --git a/Custom Apps/legal-docs/src/components/copilot/CopilotChatContainer.tsx b/Custom Apps/legal-docs/src/components/copilot/CopilotChatContainer.tsx index 10d9d3c..32e0172 100644 --- a/Custom Apps/legal-docs/src/components/copilot/CopilotChatContainer.tsx +++ b/Custom Apps/legal-docs/src/components/copilot/CopilotChatContainer.tsx @@ -10,6 +10,10 @@ import { ChatLaunchConfig } from '@microsoft/sharepointembedded-copilotchat-react'; +type ChatAuthProviderWithSiteUrl = IChatEmbeddedApiAuthProvider & { + siteUrl?: string; +}; + interface CopilotChatContainerProps { containerId: string; containerName?: string; @@ -80,7 +84,7 @@ const CopilotChatContainer: React.FC = ({ siteUrl: containerWebUrl, }); - const provider: IChatEmbeddedApiAuthProvider = { + const provider: ChatAuthProviderWithSiteUrl = { hostname: safeSharePointHostname, getToken: async () => { try { @@ -107,7 +111,7 @@ const CopilotChatContainer: React.FC = ({ }; // The SDK requires siteUrl on the auth provider for proper site context - (provider as any).siteUrl = containerWebUrl; + provider.siteUrl = containerWebUrl; return provider; }, [safeSharePointHostname, siteUrl, getSharePointToken, handleError, isAuthenticated]); diff --git a/Custom Apps/legal-docs/src/components/ui/command.tsx b/Custom Apps/legal-docs/src/components/ui/command.tsx index 68d5378..ad32c15 100644 --- a/Custom Apps/legal-docs/src/components/ui/command.tsx +++ b/Custom Apps/legal-docs/src/components/ui/command.tsx @@ -21,7 +21,7 @@ const Command = React.forwardRef< )); Command.displayName = CommandPrimitive.displayName; -interface CommandDialogProps extends DialogProps {} +type CommandDialogProps = DialogProps; const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( diff --git a/Custom Apps/legal-docs/src/components/ui/textarea.tsx b/Custom Apps/legal-docs/src/components/ui/textarea.tsx index 4a5643e..18eba7f 100644 --- a/Custom Apps/legal-docs/src/components/ui/textarea.tsx +++ b/Custom Apps/legal-docs/src/components/ui/textarea.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { cn } from "@/lib/utils"; -export interface TextareaProps extends React.TextareaHTMLAttributes {} +export type TextareaProps = React.TextareaHTMLAttributes; const Textarea = React.forwardRef(({ className, ...props }, ref) => { return ( diff --git a/Custom Apps/legal-docs/src/services/copilotChat.ts b/Custom Apps/legal-docs/src/services/copilotChat.ts index 8183e77..c6bdcab 100644 --- a/Custom Apps/legal-docs/src/services/copilotChat.ts +++ b/Custom Apps/legal-docs/src/services/copilotChat.ts @@ -27,6 +27,25 @@ export interface CopilotResponse { }>; } +interface DriveSearchItem { + name?: string; + size?: number; +} + +interface SearchHitResource { + name?: string; +} + +interface SearchHitExtract { + text?: string; +} + +interface SearchHit { + extracts?: SearchHitExtract[]; + summary?: string; + resource?: SearchHitResource; +} + // Default launch configuration following SDK patterns export const DEFAULT_CHAT_CONFIG: ChatLaunchConfig = { header: "Case Assistant", @@ -259,14 +278,14 @@ async function searchWithDriveFilter( } const data = await response.json(); - const items = data.value || []; + const items = (data.value || []) as DriveSearchItem[]; if (items.length === 0) { return await listContainerFiles(accessToken, containerId, containerName, userMessage); } // Format results from drive search - const responses: string[] = items.slice(0, 5).map((item: any) => { + const responses: string[] = items.slice(0, 5).map((item) => { const name = item.name || 'Document'; const size = item.size ? `(${Math.round(item.size / 1024)} KB)` : ''; return `• **${name}** ${size}`; @@ -302,13 +321,13 @@ async function listContainerFiles( } const data = await response.json(); - const items = data.value || []; + const items = (data.value || []) as DriveSearchItem[]; if (items.length === 0) { return `The "${containerName}" case doesn't have any documents yet. Upload some files to get started!`; } - const fileList = items.slice(0, 5).map((item: any) => `• ${item.name}`).join("\n"); + const fileList = items.slice(0, 5).map((item) => `• ${item.name}`).join("\n"); return `Here are the documents in "${containerName}":\n\n${fileList}\n\nAsk me about any of these documents, or try a more specific search term.`; } catch (error) { console.error("List files error:", error); @@ -319,7 +338,7 @@ async function listContainerFiles( /** * Format search results into a readable response. */ -function formatSearchResults(hits: any[], containerName: string): string { +function formatSearchResults(hits: SearchHit[], containerName: string): string { const responses: string[] = []; for (const hit of hits.slice(0, 5)) { diff --git a/Custom Apps/legal-docs/tailwind.config.ts b/Custom Apps/legal-docs/tailwind.config.ts index 29acdac..9e2a191 100644 --- a/Custom Apps/legal-docs/tailwind.config.ts +++ b/Custom Apps/legal-docs/tailwind.config.ts @@ -1,4 +1,5 @@ import type { Config } from "tailwindcss"; +import tailwindcssAnimate from "tailwindcss-animate"; export default { darkMode: ["class"], @@ -99,5 +100,5 @@ export default { }, }, }, - plugins: [require("tailwindcss-animate")], + plugins: [tailwindcssAnimate], } satisfies Config; diff --git a/Custom Apps/legal-docs/validate-sample.ps1 b/Custom Apps/legal-docs/validate-sample.ps1 index 83b6ae1..9835f02 100644 --- a/Custom Apps/legal-docs/validate-sample.ps1 +++ b/Custom Apps/legal-docs/validate-sample.ps1 @@ -44,12 +44,7 @@ try { Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $appRoot -Environment $nodeEnvironment Write-Step 'Linting app' - try { - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'lint') -WorkingDirectory $appRoot -Environment $nodeEnvironment - } - catch { - Write-Host "Lint reported existing issues and will not block runtime validation: $($_.Exception.Message)" -ForegroundColor Yellow - } + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'lint') -WorkingDirectory $appRoot -Environment $nodeEnvironment Write-Host 'No automated test script is defined for this sample.' -ForegroundColor Yellow diff --git a/Custom Apps/project-management/src/components/CreateContainerForm.tsx b/Custom Apps/project-management/src/components/CreateContainerForm.tsx index 0659e69..088be7c 100644 --- a/Custom Apps/project-management/src/components/CreateContainerForm.tsx +++ b/Custom Apps/project-management/src/components/CreateContainerForm.tsx @@ -16,6 +16,7 @@ import { useForm } from "react-hook-form"; import { toast } from '@/hooks/use-toast'; import { sharePointService } from '../services/sharePointService'; import { useAuth } from '../context/AuthContext'; +import { getErrorMessage } from '@/lib/utils'; interface CreateContainerFormProps { onSuccess: (containerId?: string) => void; @@ -67,11 +68,11 @@ export const CreateContainerForm: React.FC = ({ onSucc // Pass the new container ID to the parent component onSuccess(newContainer.id); - } catch (error: any) { + } catch (error) { console.error('Error creating container:', error); toast({ title: "Error", - description: error.message || "Failed to create container", + description: getErrorMessage(error, 'Failed to create container'), variant: "destructive", }); } finally { diff --git a/Custom Apps/project-management/src/components/DevModePanel.tsx b/Custom Apps/project-management/src/components/DevModePanel.tsx index bd140cd..2b7daf8 100644 --- a/Custom Apps/project-management/src/components/DevModePanel.tsx +++ b/Custom Apps/project-management/src/components/DevModePanel.tsx @@ -12,8 +12,8 @@ interface ApiCall { timestamp: string; method: string; url: string; - request?: any; - response?: any; + request?: unknown; + response?: unknown; status?: number; } diff --git a/Custom Apps/project-management/src/components/copilot/CopilotChatContainer.tsx b/Custom Apps/project-management/src/components/copilot/CopilotChatContainer.tsx index d70e85d..6247452 100644 --- a/Custom Apps/project-management/src/components/copilot/CopilotChatContainer.tsx +++ b/Custom Apps/project-management/src/components/copilot/CopilotChatContainer.tsx @@ -1,5 +1,5 @@ -import React, { useState, useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import { useCopilotSite } from '@/hooks/useCopilotSite'; import CopilotDesktopView from './CopilotDesktopView'; import { toast } from '@/hooks/use-toast'; @@ -22,20 +22,17 @@ const CopilotChatContainer: React.FC = ({ containerId const [chatKey, setChatKey] = useState(0); // Validate and normalize containerId - const normalizedContainerId = containerId && typeof containerId === 'string' - ? (containerId.startsWith('b!') ? containerId : `b!${containerId}`) - : ''; - - // Don't proceed if we don't have a valid container ID - if (!normalizedContainerId) { - console.error('CopilotChatContainer: Invalid containerId provided:', containerId); - return null; - } + const normalizedContainerId = useMemo(() => { + if (!containerId || typeof containerId !== 'string') { + return ''; + } + + return containerId.startsWith('b!') ? containerId : `b!${containerId}`; + }, [containerId]); const { isLoading, error, - siteUrl, siteName, sharePointHostname, } = useCopilotSite(normalizedContainerId); @@ -54,7 +51,7 @@ const CopilotChatContainer: React.FC = ({ containerId }, []); // Create auth provider for Copilot chat with better error handling - const authProvider = React.useMemo((): IChatEmbeddedApiAuthProvider => { + const authProvider = useMemo((): IChatEmbeddedApiAuthProvider => { return { hostname: safeSharePointHostname, getToken: async () => { @@ -84,7 +81,7 @@ const CopilotChatContainer: React.FC = ({ containerId }, [safeSharePointHostname, getSharePointToken, handleError, isAuthenticated]); // Create chat theme config - const chatTheme = React.useMemo(() => ({ + const chatTheme = useMemo(() => ({ useDarkMode: false, customTheme: { themePrimary: '#4854EE', @@ -113,7 +110,7 @@ const CopilotChatContainer: React.FC = ({ containerId }), []); // Create chat configuration with instruction to ensure prompt visibility - const chatConfig = React.useMemo((): ChatLaunchConfig => ({ + const chatConfig = useMemo((): ChatLaunchConfig => ({ header: `SharePoint Embedded - ${safeSiteName}`, theme: chatTheme, instruction: "You are a helpful assistant that helps users find and summarize information related to their files and documents.", @@ -144,6 +141,11 @@ const CopilotChatContainer: React.FC = ({ containerId setChatApi(api); }, [handleError]); + if (!normalizedContainerId) { + console.error('CopilotChatContainer: Invalid containerId provided:', containerId); + return null; + } + return ( = ({ isAuthenticated = true, chatApi }) => { - // Early return if not authenticated - if (!isAuthenticated) { - console.log('CopilotDesktopView: Not rendering because not authenticated'); - return null; - } - // Open the chat when the component is opened and we have a valid chat API useEffect(() => { - if (isOpen && chatApi) { + if (!isAuthenticated || !isOpen || !chatApi) { + return; + } + console.log('Component opened, attempting to open chat...', containerId); const openChatOnOpen = async () => { @@ -82,8 +79,12 @@ const CopilotDesktopView: React.FC = ({ }; openChatOnOpen(); - } }, [isOpen, chatApi, chatConfig, onError, containerId]); + + if (!isAuthenticated) { + console.log('CopilotDesktopView: Not rendering because not authenticated'); + return null; + } // Reset chat when requested const handleResetChat = () => { @@ -102,7 +103,7 @@ const CopilotDesktopView: React.FC = ({

SharePoint Embedded Copilot

Connected to: {siteName || 'SharePoint Site'}

- {onResetChat && isAuthenticated && ( + {onResetChat && ( + onClick={() => onDownloadItemClick(driveItem.downloadUrl, driveItem.name ?? undefined)}>Download diff --git a/Custom Apps/boilerplate-typescript-react/react-client/src/react-app-env.d.ts b/Custom Apps/boilerplate-typescript-react/react-client/src/react-app-env.d.ts index 6431bc5..5ba7ff6 100644 --- a/Custom Apps/boilerplate-typescript-react/react-client/src/react-app-env.d.ts +++ b/Custom Apps/boilerplate-typescript-react/react-client/src/react-app-env.d.ts @@ -1 +1,4 @@ /// + +declare module "*.css"; +declare module "@testing-library/jest-dom"; diff --git a/Custom Apps/boilerplate-typescript-react/react-client/tsconfig.json b/Custom Apps/boilerplate-typescript-react/react-client/tsconfig.json index 5c0b309..cc21a5f 100644 --- a/Custom Apps/boilerplate-typescript-react/react-client/tsconfig.json +++ b/Custom Apps/boilerplate-typescript-react/react-client/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "target": "es5", + "target": "es2015", "lib": [ "dom", "dom.iterable", @@ -15,7 +15,7 @@ "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, diff --git a/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 b/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 index 1ca4f16..2fb0171 100644 --- a/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 +++ b/Custom Apps/boilerplate-typescript-react/validate-sample.ps1 @@ -19,7 +19,9 @@ $functionApiRoot = Join-Path $appRoot 'function-api' $clientRoot = Join-Path $appRoot 'react-client' $localSettingsPath = Join-Path $functionApiRoot 'local.settings.json' $clientEnvPath = Join-Path $clientRoot '.env' +$nodeEnvironment = Get-ValidationNodeEnvironment $handles = @() +$runtimeSkipReasons = @() try { Write-Step 'Preflight checks' @@ -28,55 +30,52 @@ try { if (-not $SkipInstall) { Write-Step 'Installing root dependencies' - $nodeEnvironment = Get-ValidationNodeEnvironment - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot - $runtimeSkipReasons = @() + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot -Environment $nodeEnvironment Write-Step 'Installing function-api dependencies' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $functionApiRoot - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $appRoot -Environment $nodeEnvironment + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $functionApiRoot -Environment $nodeEnvironment + Write-Step 'Installing react-client dependencies' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $clientRoot - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $functionApiRoot -Environment $nodeEnvironment + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install', '--legacy-peer-deps') -WorkingDirectory $clientRoot -Environment $nodeEnvironment + } Write-Step 'Building function-api' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('install') -WorkingDirectory $clientRoot -Environment $nodeEnvironment + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $functionApiRoot -Environment $nodeEnvironment Write-Step 'Building react-client' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $clientRoot - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $functionApiRoot -Environment $nodeEnvironment + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $clientRoot -Environment $nodeEnvironment + if ($SkipTests) { Write-Host 'Skipping react-client tests because -SkipTests was specified.' -ForegroundColor Yellow - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'build') -WorkingDirectory $clientRoot -Environment $nodeEnvironment + } else { Write-Step 'Running react-client tests' - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test', '--', '--watchAll=false', '--passWithNoTests') -WorkingDirectory $clientRoot -Environment @{ CI = 'true' } + Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test', '--', '--watchAll=false', '--passWithNoTests') -WorkingDirectory $clientRoot -Environment (Merge-EnvironmentTables @($nodeEnvironment, @{ CI = 'true' })) } if (Test-Path $localSettingsPath) { - Invoke-ExternalCommand -FilePath 'npm' -Arguments @('run', 'test', '--', '--watchAll=false', '--passWithNoTests') -WorkingDirectory $clientRoot -Environment (Merge-EnvironmentTables @($nodeEnvironment, @{ CI = 'true' })) Write-Step 'Starting function-api host' $backendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'typescript-react-api' - $backendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $functionApiRoot -LogPath $backendLog + $backendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $functionApiRoot -LogPath $backendLog -Environment $nodeEnvironment $handles += $backendHandle [void](Wait-ForHttpEndpoint -Url 'http://127.0.0.1:7072/api/containers' -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200, 401) -ProcessHandle $backendHandle) } - $backendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $functionApiRoot -LogPath $backendLog -Environment $nodeEnvironment + else { Write-Host 'Skipping function-api runtime smoke because local.settings.json is missing.' -ForegroundColor Yellow + $runtimeSkipReasons += 'function-api/local.settings.json is missing' } if (Test-Path $clientEnvPath) { $clientEnv = Get-DotEnvMap -Path $clientEnvPath - $runtimeSkipReasons += 'function-api/local.settings.json is missing' $clientPort = if ($clientEnv.ContainsKey('PORT')) { $clientEnv['PORT'] } else { '8080' } Write-Step 'Starting react-client' $frontendLog = New-ValidationLogPath -WorkingDirectory $appRoot -Name 'typescript-react-client' - $frontendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $clientRoot -LogPath $frontendLog -Environment @{ BROWSER = 'none' } + $frontendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $clientRoot -LogPath $frontendLog -Environment (Merge-EnvironmentTables @($nodeEnvironment, @{ BROWSER = 'none' })) $handles += $frontendHandle [void](Wait-ForHttpEndpoint -Url "http://127.0.0.1:$clientPort" -TimeoutSec $TimeoutSec -AllowedStatusCodes @(200) -ProcessHandle $frontendHandle) - $frontendHandle = Start-LoggedProcess -FilePath 'npm' -Arguments @('run', 'start') -WorkingDirectory $clientRoot -LogPath $frontendLog -Environment (Merge-EnvironmentTables @($nodeEnvironment, @{ BROWSER = 'none' })) + if ($SkipBrowser) { Write-Host 'Skipping browser smoke because -SkipBrowser was specified.' -ForegroundColor Yellow } else { @@ -86,26 +85,26 @@ try { } else { Write-Host 'Skipping react-client runtime smoke because .env is missing.' -ForegroundColor Yellow + $runtimeSkipReasons += 'react-client/.env is missing' + } + + if ($runtimeSkipReasons.Count -gt 0) { + Write-ValidationSummary -Status 'SKIP_CONFIG' -Message "Build and tests passed; runtime smoke skipped because $($runtimeSkipReasons -join '; ')." + } + else { + Write-ValidationSummary -Status 'PASS' -Message 'Build, tests, backend startup, and frontend runtime smoke checks passed.' } Write-Host 'TypeScript React sample validation completed.' -ForegroundColor Green } - $runtimeSkipReasons += 'react-client/.env is missing' +catch { + Write-ValidationSummary -Status 'FAIL' -Message $_.Exception.Message + throw +} finally { if (-not $KeepProcesses) { foreach ($handle in ($handles | Sort-Object -Descending -Property LogPath)) { - - if ($runtimeSkipReasons.Count -gt 0) { - Write-ValidationSummary -Status 'SKIP_CONFIG' -Message "Build and tests passed; runtime smoke skipped because $($runtimeSkipReasons -join '; ')." - } - else { - Write-ValidationSummary -Status 'PASS' -Message 'Build, tests, backend startup, and frontend runtime smoke checks passed.' - } Stop-LoggedProcess -Handle $handle - catch { - Write-ValidationSummary -Status 'FAIL' -Message $_.Exception.Message - throw - } } } } \ No newline at end of file diff --git a/Custom Apps/legal-docs/tsconfig.json b/Custom Apps/legal-docs/tsconfig.json index 2518773..45f56f2 100644 --- a/Custom Apps/legal-docs/tsconfig.json +++ b/Custom Apps/legal-docs/tsconfig.json @@ -2,7 +2,6 @@ "files": [], "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }], "compilerOptions": { - "baseUrl": ".", "paths": { "@/*": ["./src/*"] }, diff --git a/Custom Apps/legal-docs/vite.config.ts b/Custom Apps/legal-docs/vite.config.ts index 820a3a9..6d8a8ac 100644 --- a/Custom Apps/legal-docs/vite.config.ts +++ b/Custom Apps/legal-docs/vite.config.ts @@ -13,8 +13,9 @@ export default defineConfig(({ mode }) => ({ resolve: { alias: { "@": path.resolve(__dirname, "./src"), - // Local shim for the SharePoint Embedded Copilot SDK (tgz not installable via npm/bun) - + // Keep Vite aligned with tsconfig so production builds resolve the local SDK shim. + "@microsoft/sharepointembedded-copilotchat-react": path.resolve(__dirname, "./src/lib/sharepointembedded-copilotchat-react/index.tsx"), + // Force all React imports to use the same instance "react": path.resolve(__dirname, "./node_modules/react"), "react-dom": path.resolve(__dirname, "./node_modules/react-dom"), diff --git a/Custom Apps/project-management/tsconfig.json b/Custom Apps/project-management/tsconfig.json index 9f93d42..c293ddf 100644 --- a/Custom Apps/project-management/tsconfig.json +++ b/Custom Apps/project-management/tsconfig.json @@ -5,7 +5,6 @@ { "path": "./tsconfig.node.json" } ], "compilerOptions": { - "baseUrl": ".", "paths": { "@/*": ["./src/*"] },