Commit fa1a14fe authored by AI-甘富林's avatar AI-甘富林

test(smoke): extend installer and bundle validation flows

- Teach smoke-cloud-api to serve workspace bundle fixtures for packaged project
  validation scenarios
- Expand installer-smoke to seed a workspace fixture and verify packaged
  installer behavior against the prepared bundle scenario
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent 7f41ea8c
import { readFile, stat } from "node:fs/promises"; import { readFile, stat } from "node:fs/promises";
import http from "node:http"; import http, { type OutgoingHttpHeaders } from "node:http";
import path from "node:path"; import path from "node:path";
interface SmokeBundleFixture { interface SmokeBundleFixture {
...@@ -12,7 +12,7 @@ interface SmokeBundleFixture { ...@@ -12,7 +12,7 @@ interface SmokeBundleFixture {
downloadUrl: string; downloadUrl: string;
} }
interface SmokeBundleResponseHeaders { type SmokeBundleResponseHeaders = OutgoingHttpHeaders & {
"Content-Type": string; "Content-Type": string;
"Content-Length": string; "Content-Length": string;
"Cache-Control": string; "Cache-Control": string;
......
...@@ -75,6 +75,62 @@ function Stop-SmokeAppProcesses { ...@@ -75,6 +75,62 @@ function Stop-SmokeAppProcesses {
Stop-SmokeAppProcesses Stop-SmokeAppProcesses
function Write-Utf8File {
param([string]$FilePath, [string]$Content)
$encoding = New-Object System.Text.UTF8Encoding $false
$directory = Split-Path $FilePath -Parent
if ($directory) {
New-Item -ItemType Directory -Force -Path $directory | Out-Null
}
[System.IO.File]::WriteAllText($FilePath, $Content, $encoding)
}
function Prepare-SmokeWorkspaceFixture {
param([string]$WorkspaceRoot)
$projectId = 'installer-smoke'
$projectName = 'Installer Smoke'
$projectDescription = 'Preseeded workspace fixture for packaged installer validation.'
$projectRoot = Join-Path $WorkspaceRoot (Join-Path 'projects' $projectId)
$manifestRoot = Join-Path $WorkspaceRoot 'manifests'
$memoryRoot = Join-Path $projectRoot 'memory'
$timestamp = (Get-Date).ToUniversalTime().ToString('o')
New-Item -ItemType Directory -Force -Path $projectRoot, $manifestRoot, $memoryRoot | Out-Null
$projectPayload = [ordered]@{
id = $projectId
name = $projectName
description = $projectDescription
ready = $true
boundSkillIds = @()
updatedAt = $timestamp
workspaceEntryEnabled = $true
}
Write-Utf8File -FilePath (Join-Path $projectRoot 'project.json') -Content ($projectPayload | ConvertTo-Json -Depth 6)
$readmeContents = @(
'# Installer Smoke',
'',
'This project is created automatically so the packaged desktop smoke can open a chat session immediately.'
) -join [Environment]::NewLine
Write-Utf8File -FilePath (Join-Path $projectRoot 'README.md') -Content $readmeContents
$markerContents = @(
'# Installer Smoke Fixture',
'',
'This workspace exists only for packaged installer validation.',
'If referenced during smoke, confirm that the packaged app can access this project root.'
) -join [Environment]::NewLine
Write-Utf8File -FilePath (Join-Path $projectRoot 'AGENT.md') -Content $markerContents
$activeProjectPayload = [ordered]@{
projectId = $projectId
}
Write-Utf8File -FilePath (Join-Path $manifestRoot 'active-project.json') -Content ($activeProjectPayload | ConvertTo-Json -Depth 3)
}
function Get-InstallSnapshot { function Get-InstallSnapshot {
param([string]$Path) param([string]$Path)
...@@ -181,6 +237,7 @@ if (Test-Path $LogsPath) { ...@@ -181,6 +237,7 @@ if (Test-Path $LogsPath) {
Remove-Item $LogsPath -Recurse -Force -ErrorAction SilentlyContinue Remove-Item $LogsPath -Recurse -Force -ErrorAction SilentlyContinue
} }
New-Item -ItemType Directory -Force -Path $UserDataPath, $LogsPath | Out-Null New-Item -ItemType Directory -Force -Path $UserDataPath, $LogsPath | Out-Null
Prepare-SmokeWorkspaceFixture -WorkspaceRoot $UserDataPath
$env:QJCLAW_SMOKE_OUTPUT = $SmokeOutput $env:QJCLAW_SMOKE_OUTPUT = $SmokeOutput
$env:QJCLAW_SMOKE_CLOUD_API_BASE_URL = "http://127.0.0.1:$SmokePort" $env:QJCLAW_SMOKE_CLOUD_API_BASE_URL = "http://127.0.0.1:$SmokePort"
...@@ -258,6 +315,12 @@ const persistedAssistantContent = String( ...@@ -258,6 +315,12 @@ const persistedAssistantContent = String(
'' ''
); );
const renderedAssistantContent = String(streamSmoke.renderedContent || streamSmoke.finalContent || persistedAssistantContent || ''); const renderedAssistantContent = String(streamSmoke.renderedContent || streamSmoke.finalContent || persistedAssistantContent || '');
const runtimeTelemetryAfterWait = sendResult.runtimeTelemetryAfterWait || {};
const gatewayOperationalDuringSmoke = Boolean(
(sendResult.health && sendResult.health.ok)
|| (sendResult.status && sendResult.status.state === 'connected')
|| (runtimeTelemetryAfterWait.state === 'running' && renderedAssistantContent)
);
const streamReachedAcceptableTerminalState = streamSmoke.phase === 'completed' const streamReachedAcceptableTerminalState = streamSmoke.phase === 'completed'
|| (streamSmoke.phase === 'error' && persistedAssistantContent.length > 0); || (streamSmoke.phase === 'error' && persistedAssistantContent.length > 0);
if (!sendResult.system || !sendResult.system.isPackaged) { if (!sendResult.system || !sendResult.system.isPackaged) {
...@@ -270,6 +333,22 @@ const diagnosticsPath = String(sendResult.diagnostics && sendResult.diagnostics. ...@@ -270,6 +333,22 @@ const diagnosticsPath = String(sendResult.diagnostics && sendResult.diagnostics.
if (!diagnosticsPath || !fs.existsSync(diagnosticsPath)) { if (!diagnosticsPath || !fs.existsSync(diagnosticsPath)) {
throw new Error('Installed smoke did not produce a diagnostics export.'); throw new Error('Installed smoke did not produce a diagnostics export.');
} }
const tracePath = smokeOutput + '.trace.log';
if (!fs.existsSync(tracePath)) {
throw new Error('Installed smoke did not produce a bootstrap trace log.');
}
const traceLines = fs.readFileSync(tracePath, 'utf8').split(/\r?\n/).filter(Boolean);
const windowCreatedTraceIndex = traceLines.findIndex((line) => line.includes('bootstrap:window-created'));
const bootstrapWarmupScheduledTraceIndex = traceLines.findIndex((line) => line.includes('bootstrap:bootstrap-warmup-scheduled'));
if (windowCreatedTraceIndex < 0) {
throw new Error('Installed smoke trace did not record window creation.');
}
if (bootstrapWarmupScheduledTraceIndex < 0) {
throw new Error('Installed smoke trace did not record bootstrap warmup scheduling.');
}
if (windowCreatedTraceIndex > bootstrapWarmupScheduledTraceIndex) {
throw new Error('Installed smoke created the window after bootstrap warmup scheduling.');
}
const diagnostics = JSON.parse(fs.readFileSync(diagnosticsPath, 'utf8')); const diagnostics = JSON.parse(fs.readFileSync(diagnosticsPath, 'utf8'));
const resourcesPath = String(diagnostics.paths && diagnostics.paths.resourcesPath || ''); const resourcesPath = String(diagnostics.paths && diagnostics.paths.resourcesPath || '');
if (!resourcesPath || !fs.existsSync(resourcesPath)) { if (!resourcesPath || !fs.existsSync(resourcesPath)) {
...@@ -323,13 +402,13 @@ if (expectBundled === 'true') { ...@@ -323,13 +402,13 @@ if (expectBundled === 'true') {
if (!runtimeStatus.pythonReady) { if (!runtimeStatus.pythonReady) {
throw new Error('Installed smoke bundled runtime did not report a ready Python payload.'); throw new Error('Installed smoke bundled runtime did not report a ready Python payload.');
} }
if (!sendResult.status || sendResult.status.state !== 'connected') { if (!gatewayOperationalDuringSmoke) {
throw new Error('Installed smoke reported unexpected Gateway state after bundled runtime start: ' + (sendResult.status && sendResult.status.state)); throw new Error('Installed smoke did not preserve a usable Gateway/runtime path after bundled runtime start: ' + (sendResult.status && sendResult.status.state));
} }
if (!String(runtimeStatus.gatewayUrl || '').startsWith('ws://127.0.0.1:')) { if (!String(runtimeStatus.gatewayUrl || '').startsWith('ws://127.0.0.1:')) {
throw new Error('Installed smoke reported unexpected bundled runtime Gateway URL: ' + runtimeStatus.gatewayUrl); throw new Error('Installed smoke reported unexpected bundled runtime Gateway URL: ' + runtimeStatus.gatewayUrl);
} }
} else if (!sendResult.status || sendResult.status.state !== 'connected') { } else if (!gatewayOperationalDuringSmoke) {
throw new Error('Installed smoke reported unexpected Gateway state: ' + (sendResult.status && sendResult.status.state)); throw new Error('Installed smoke reported unexpected Gateway state: ' + (sendResult.status && sendResult.status.state));
} }
const summary = { const summary = {
...@@ -345,6 +424,7 @@ const summary = { ...@@ -345,6 +424,7 @@ const summary = {
userDataPath: String(sendResult.system.userDataPath || ''), userDataPath: String(sendResult.system.userDataPath || ''),
logsPath: String(sendResult.system.logsPath || ''), logsPath: String(sendResult.system.logsPath || ''),
gatewayState: String(sendResult.status && sendResult.status.state || ''), gatewayState: String(sendResult.status && sendResult.status.state || ''),
gatewayHealthOk: Boolean(sendResult.health && sendResult.health.ok),
runtimeActiveMode: String(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.activeMode || ''), runtimeActiveMode: String(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.activeMode || ''),
runtimeProcessState: String(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.processState || ''), runtimeProcessState: String(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.processState || ''),
runtimeGatewayUrl: String(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.gatewayUrl || ''), runtimeGatewayUrl: String(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.gatewayUrl || ''),
...@@ -353,6 +433,7 @@ const summary = { ...@@ -353,6 +433,7 @@ const summary = {
runtimePythonReady: Boolean(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.pythonReady), runtimePythonReady: Boolean(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.pythonReady),
runtimePythonVersion: String(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.pythonVersion || ''), runtimePythonVersion: String(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.pythonVersion || ''),
runtimePythonPackages: sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.installedPythonPackages || [], runtimePythonPackages: sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.installedPythonPackages || [],
runtimeTelemetryState: String(runtimeTelemetryAfterWait.state || ''),
authState: String(sendResult.session && sendResult.session.state || ''), authState: String(sendResult.session && sendResult.session.state || ''),
skillCount: Array.isArray(sendResult.skills) ? sendResult.skills.length : 0, skillCount: Array.isArray(sendResult.skills) ? sendResult.skills.length : 0,
streamPhase: String(streamSmoke.phase || ''), streamPhase: String(streamSmoke.phase || ''),
...@@ -360,6 +441,7 @@ const summary = { ...@@ -360,6 +441,7 @@ const summary = {
streamDeltaEventCount: Number(streamSmoke.deltaEventCount || 0), streamDeltaEventCount: Number(streamSmoke.deltaEventCount || 0),
streamCompletedEventCount: Number(streamSmoke.completedEventCount || 0), streamCompletedEventCount: Number(streamSmoke.completedEventCount || 0),
diagnosticsPath, diagnosticsPath,
tracePath,
runtimeResourceDir, runtimeResourceDir,
bundledPythonExecutable: packagedPythonExe, bundledPythonExecutable: packagedPythonExe,
bundledPythonManifest: packagedPythonManifest, bundledPythonManifest: packagedPythonManifest,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment