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 http from "node:http";
import http, { type OutgoingHttpHeaders } from "node:http";
import path from "node:path";
interface SmokeBundleFixture {
......@@ -12,7 +12,7 @@ interface SmokeBundleFixture {
downloadUrl: string;
}
interface SmokeBundleResponseHeaders {
type SmokeBundleResponseHeaders = OutgoingHttpHeaders & {
"Content-Type": string;
"Content-Length": string;
"Cache-Control": string;
......
......@@ -75,6 +75,62 @@ function 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 {
param([string]$Path)
......@@ -181,6 +237,7 @@ if (Test-Path $LogsPath) {
Remove-Item $LogsPath -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path $UserDataPath, $LogsPath | Out-Null
Prepare-SmokeWorkspaceFixture -WorkspaceRoot $UserDataPath
$env:QJCLAW_SMOKE_OUTPUT = $SmokeOutput
$env:QJCLAW_SMOKE_CLOUD_API_BASE_URL = "http://127.0.0.1:$SmokePort"
......@@ -258,6 +315,12 @@ const persistedAssistantContent = String(
''
);
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'
|| (streamSmoke.phase === 'error' && persistedAssistantContent.length > 0);
if (!sendResult.system || !sendResult.system.isPackaged) {
......@@ -270,6 +333,22 @@ const diagnosticsPath = String(sendResult.diagnostics && sendResult.diagnostics.
if (!diagnosticsPath || !fs.existsSync(diagnosticsPath)) {
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 resourcesPath = String(diagnostics.paths && diagnostics.paths.resourcesPath || '');
if (!resourcesPath || !fs.existsSync(resourcesPath)) {
......@@ -323,13 +402,13 @@ if (expectBundled === 'true') {
if (!runtimeStatus.pythonReady) {
throw new Error('Installed smoke bundled runtime did not report a ready Python payload.');
}
if (!sendResult.status || sendResult.status.state !== 'connected') {
throw new Error('Installed smoke reported unexpected Gateway state after bundled runtime start: ' + (sendResult.status && sendResult.status.state));
if (!gatewayOperationalDuringSmoke) {
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:')) {
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));
}
const summary = {
......@@ -345,6 +424,7 @@ const summary = {
userDataPath: String(sendResult.system.userDataPath || ''),
logsPath: String(sendResult.system.logsPath || ''),
gatewayState: String(sendResult.status && sendResult.status.state || ''),
gatewayHealthOk: Boolean(sendResult.health && sendResult.health.ok),
runtimeActiveMode: String(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.activeMode || ''),
runtimeProcessState: String(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.processState || ''),
runtimeGatewayUrl: String(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.gatewayUrl || ''),
......@@ -353,6 +433,7 @@ const summary = {
runtimePythonReady: Boolean(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.pythonReady),
runtimePythonVersion: String(sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.pythonVersion || ''),
runtimePythonPackages: sendResult.runtimeStatusAfterProbe && sendResult.runtimeStatusAfterProbe.installedPythonPackages || [],
runtimeTelemetryState: String(runtimeTelemetryAfterWait.state || ''),
authState: String(sendResult.session && sendResult.session.state || ''),
skillCount: Array.isArray(sendResult.skills) ? sendResult.skills.length : 0,
streamPhase: String(streamSmoke.phase || ''),
......@@ -360,6 +441,7 @@ const summary = {
streamDeltaEventCount: Number(streamSmoke.deltaEventCount || 0),
streamCompletedEventCount: Number(streamSmoke.completedEventCount || 0),
diagnosticsPath,
tracePath,
runtimeResourceDir,
bundledPythonExecutable: packagedPythonExe,
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