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

fix(desktop): recover gateway chat startup after employee-key bind

parent fa1a14fe
import type {
AppConfig,
GatewayStatus,
RuntimeCloudFetchAction,
RuntimeStatus
} from "@qjclaw/shared-types";
import { isTransientLocalGatewayError } from "./workspace-startup.js";
export interface ChatGatewayRecoveryCoordinator {
getConfig: () => Promise<AppConfig>;
runtimeStatus: () => Promise<RuntimeStatus>;
gatewayStatus: () => Promise<GatewayStatus | null>;
startManagedRuntime: (
reason: string,
options?: {
action?: RuntimeCloudFetchAction;
restart?: boolean;
config?: AppConfig;
inputToken?: string;
}
) => Promise<RuntimeStatus>;
shouldRefreshGatewayClient: (config?: AppConfig, inputToken?: string) => Promise<boolean>;
reconfigureGatewayClient: (config?: AppConfig, inputToken?: string) => Promise<void>;
connectGatewayClientWithRetry: () => Promise<void>;
}
function isManagedRuntimeMode(config: AppConfig): boolean {
return config.runtimeMode !== "external-gateway";
}
function isGatewayConnected(status: GatewayStatus | null): boolean {
return status?.state === "connected";
}
function toRuntimeNotReadyError(status: RuntimeStatus): Error {
return new Error(status.lastError ?? status.message ?? "Local assistant is not ready.");
}
function toGatewayNotReadyError(status: GatewayStatus | null): Error {
return new Error(status?.lastError ?? status?.message ?? "Gateway is not connected.");
}
export function isRecoverableGatewayChatError(message?: string): boolean {
if (!message) {
return false;
}
const normalized = message.toLowerCase();
return normalized.includes("gateway websocket is not open")
|| isTransientLocalGatewayError(message);
}
export async function ensureGatewayReadyForChat(
coordinator: ChatGatewayRecoveryCoordinator,
reason: string,
inputToken?: string
): Promise<void> {
const config = await coordinator.getConfig();
let runtimeStatus = await coordinator.runtimeStatus();
const managedRuntimeMode = isManagedRuntimeMode(config);
if (managedRuntimeMode && runtimeStatus.processState !== "running") {
runtimeStatus = await coordinator.startManagedRuntime(reason, {
action: "init",
config,
inputToken
});
}
const shouldRefreshGatewayClient = await coordinator.shouldRefreshGatewayClient(config, inputToken);
if (shouldRefreshGatewayClient) {
await coordinator.reconfigureGatewayClient(config, inputToken);
}
let gatewayStatus = await coordinator.gatewayStatus();
if (!isGatewayConnected(gatewayStatus) || shouldRefreshGatewayClient) {
await coordinator.connectGatewayClientWithRetry();
gatewayStatus = await coordinator.gatewayStatus();
}
runtimeStatus = await coordinator.runtimeStatus();
if (managedRuntimeMode && runtimeStatus.processState !== "running") {
throw toRuntimeNotReadyError(runtimeStatus);
}
if (!isGatewayConnected(gatewayStatus)) {
throw toGatewayNotReadyError(gatewayStatus);
}
}
export async function recoverGatewayForChat(
coordinator: ChatGatewayRecoveryCoordinator,
_reason: string,
inputToken?: string
): Promise<void> {
const config = await coordinator.getConfig();
await coordinator.reconfigureGatewayClient(config, inputToken);
await coordinator.connectGatewayClientWithRetry();
const runtimeStatus = await coordinator.runtimeStatus();
if (isManagedRuntimeMode(config) && runtimeStatus.processState !== "running") {
throw toRuntimeNotReadyError(runtimeStatus);
}
const gatewayStatus = await coordinator.gatewayStatus();
if (!isGatewayConnected(gatewayStatus)) {
throw toGatewayNotReadyError(gatewayStatus);
}
}
export async function runGatewayChatRequestWithRecovery<T>(
coordinator: ChatGatewayRecoveryCoordinator,
input: {
reason: string;
inputToken?: string;
execute: () => Promise<T>;
}
): Promise<T> {
await ensureGatewayReadyForChat(coordinator, input.reason, input.inputToken);
try {
return await input.execute();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!isRecoverableGatewayChatError(message)) {
throw error;
}
await recoverGatewayForChat(coordinator, `${input.reason}-recover`, input.inputToken);
return input.execute();
}
}
......@@ -51,6 +51,9 @@ import {
refreshProjectContextAfterExecution,
shouldRefreshProjectContextAfterExecution
} from "./services/project-context-lifecycle.js";
import {
runGatewayChatRequestWithRecovery
} from "./chat-gateway-recovery.js";
import {
buildChatSummary,
shouldRetryBootstrapWarmup,
......@@ -399,6 +402,16 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
return status;
};
const chatGatewayRecoveryCoordinator = {
getConfig: () => getEffectiveConfig(),
runtimeStatus: () => runtimeManager.status(),
gatewayStatus: () => gatewayClient.status().catch(() => null),
startManagedRuntime,
shouldRefreshGatewayClient,
reconfigureGatewayClient,
connectGatewayClientWithRetry
};
let workspaceWarmupTail: Promise<void> = Promise.resolve();
let workspaceWarmupInFlight = false;
let bootstrapRecoveryAttempts = 0;
......@@ -780,7 +793,10 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
executionPolicy: preparedExecution.executionPolicy
};
}
const result = await gatewayClient.sendPrompt(executionSessionId, preparedExecution.gatewayPrompt ?? prompt);
const result = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
reason: "chat-send",
execute: () => gatewayClient.sendPrompt(executionSessionId, preparedExecution.gatewayPrompt ?? prompt)
});
await projectStore.appendSessionMessage(result.sessionId, result.reply);
await projectStore.updateSessionLastActive(result.sessionId).catch(() => undefined);
runtimeCloudSupervisor.noteMessageSent(result.sessionId, result.reply.content, preparedExecution.executionPolicy.modelId, executionSkillId);
......@@ -970,7 +986,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
executionPolicy: executionPolicy ?? undefined
};
}
const stream = await gatewayClient.streamPrompt(executionSessionId, preparedExecution.gatewayPrompt ?? prompt, {
const stream = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
reason: "chat-stream",
execute: () => gatewayClient.streamPrompt(executionSessionId, preparedExecution.gatewayPrompt ?? prompt, {
onStarted: ({ sessionId: nextSessionId, runId }) => {
queueOrSend({
type: "started",
......@@ -1033,6 +1051,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
});
queueProjectContextRefresh();
}
})
});
ready = true;
flushQueuedEvents({
......@@ -1098,6 +1117,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
await runtimeManager.setRequestedMode(config.runtimeMode);
if (config.runtimeMode !== "external-gateway" && (await secretManager.getApiKey())) {
await reconfigureGatewayClient(config, input.gatewayToken);
void queueWorkspaceWarmup("config-save", {
action: "init",
config,
......@@ -1224,6 +1244,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
await runtimeManager.setRequestedMode(config.runtimeMode);
if (config.runtimeMode !== "external-gateway" && (await secretManager.getApiKey())) {
await reconfigureGatewayClient(config, input.gatewayToken);
void queueWorkspaceWarmup("config-save", {
action: "init",
config,
......
......@@ -12,7 +12,8 @@ export function isTransientLocalGatewayError(message?: string): boolean {
}
const normalized = message.toLowerCase();
return normalized.includes("gateway closed during connect (1006)")
return normalized.includes("gateway websocket is not open")
|| normalized.includes("gateway closed during connect (1006)")
|| normalized.includes("gateway closed during connect (1005)")
|| normalized.includes("gateway closed during connect (1000)")
|| normalized.includes("econnrefused")
......
......@@ -3,7 +3,7 @@
- `apps/ui` emits its production bundle into `apps/desktop/dist/renderer`
- `apps/desktop` packages the final EXE
- `vendor/openclaw-runtime` is reserved for the pinned runtime payload
- `installer-smoke.ps1` performs a real silent NSIS install into `.tmp`, launches the installed app in smoke mode, and validates packaged paths plus diagnostics output
- `installer-smoke.ps1` now splits NSIS validation into installer materialization first and installed-app smoke second; it records preflight old-install evidence, classifies installer failures (`empty-exit-zero-files`, `missing-uninstaller-only`, `partial-materialization`, `app-smoke-failure`), retries the empty-exit case once by default, and writes a combined JSON summary instead of only the raw renderer smoke payload; `pnpm smoke:installer`
- `electron-smoke.ps1` launches the desktop app directly under Electron with isolated `userData` and `logs` paths, then validates execution-policy smoke output; it now also supports preparing a workspace-entry fixture, preserving `userData`, and remote bundle-specific assertions
- `materialize-runtime-payload.ps1` generates a local bundled runtime payload under `vendor/openclaw-runtime/` by copying the local `node.exe`, the installed OpenClaw package, a local OpenClaw config snapshot, and a self-contained Python runtime with the locked dependency set installed into it; when the existing payload manifest's `materializationKey` still matches the current inputs, it short-circuits and reuses the payload without rerunning `pip` upgrade or dependency installation
- `materialize-runtime-cache-smoke.ps1` materializes an isolated runtime directory twice and asserts the first run is a cache miss while the second run is a cache hit that skips `pip` upgrade and locked dependency installation; `pnpm smoke:materialize-cache`
......@@ -12,6 +12,8 @@
- `cloud-bundle-smoke.ps1` generates real same-project bundle variants, serves them through the smoke cloud API, and validates the full `cloud zip -> bundle sync -> active project -> workspace-entry` chain for payload `sync`, cached `init`, and same-`projectId` replacement with refreshed README/shared-entry materialization; `pnpm smoke:cloud-bundle`
- `default-chat-smoke.ps1` compiles the targeted `default-chat-context-smoke.ts` service-level smoke with the local desktop TypeScript toolchain and verifies `chat-fallback` routing, project context injection into the prepared prompt, post-execution snapshot refresh/rebind, and reuse of the refreshed snapshot on the next request; `pnpm smoke:default-chat`
- `installer-smoke.ps1` validates the packaged Python runtime by importing the preinstalled table/document/web dependencies from `resources/vendor/openclaw-runtime/python/python.exe`
- `installer-path-change-smoke.ps1` installs once to an initial path, reinstalls the same package to a second path, and asserts the relocated run still materializes `Uninstall QianjiangClaw.exe` while reporting prior-install evidence; `pnpm smoke:installer:path-change`
- `installer-target-residue-smoke.ps1` preseeds the target directory with a stale `Uninstall QianjiangClaw.exe`, runs the real NSIS installer silently, and verifies the packaged install can overwrite removable residue instead of failing at the uninstaller write step; `pnpm smoke:installer:target-residue`
- `project-context-refresh-smoke.ps1` compiles the targeted `project-context-refresh-smoke.ts` service-level smoke with the local desktop TypeScript toolchain, runs it under Node, and verifies ProjectContextService snapshot cache, dirty invalidation, refresh, and `session.contextSnapshotId` rebinding; `pnpm smoke:project-context-refresh`
- `project-empty-inventory-smoke.ps1` compiles the targeted `project-empty-inventory-smoke.ts` service-level smoke with the local desktop TypeScript toolchain, runs it under Node, and verifies that an empty project inventory stays empty, session listing returns `[]`, session creation is blocked with the pending-cloud message, and the first synced bundle-backed project becomes active cleanly; `pnpm smoke:empty-project-inventory`
- `project-bundle-reconcile-smoke.ps1` compiles the targeted `project-bundle-reconcile-smoke.ts` service-level smoke with the local desktop TypeScript toolchain, runs it under Node, and verifies stale bundle project removal, shared `skills/` cleanup, shared `cron/` cleanup, manifest pruning, and empty-inventory cleanup without recreating a local fallback project; `pnpm smoke:bundle-reconcile`
......@@ -19,4 +21,5 @@
- `project-bundle-replacement-smoke.ps1` compiles the targeted `project-bundle-replacement-smoke.ts` service-level smoke with the local desktop TypeScript toolchain, runs it under Node, and verifies same-project replacement, shared `skills/` and `cron/` ownership cleanup, rollback on an injected post-commit failure, and successful recovery on the next sync; `pnpm smoke:bundle-replacement`
- `project-bundle-churn-smoke.ps1` compiles the targeted `project-bundle-churn-smoke.ts` service-level smoke with the local desktop TypeScript toolchain, runs it under Node, and verifies multi-project churn with stable survivors, same-project replacement, project removal, project addition, active-project fallback, and session survival inside unaffected projects; `pnpm smoke:bundle-churn`
- `workspace-startup-smoke.ps1` compiles the targeted `workspace-startup-smoke.ts` service-level smoke with the local desktop TypeScript toolchain, runs it under Node, and verifies packaged startup error classification plus local OpenClaw isolation policy; `pnpm smoke:workspace-startup`
- `chat-gateway-recovery-smoke.ps1` compiles the targeted `chat-gateway-recovery-smoke.ts` service-level smoke with the local desktop TypeScript toolchain, runs it under Node, and verifies send-time gateway readiness enforcement plus single-shot reconnect/retry for `Gateway websocket is not open.`; `pnpm smoke:chat-gateway-recovery`
- `project-isolation-smoke.ps1` runs the main project-isolation regression gate end to end, including workspace-entry, default-chat, cloud-bundle Electron lifecycle coverage, project-context refresh, empty-project inventory, bundle reconcile, bundle freshness, bundle replacement, and multi-project churn; `pnpm smoke:project-isolation`
param(
[string]$ResultPath
)
$ErrorActionPreference = 'Stop'
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$desktopAppRoot = Join-Path $repoRoot 'apps\desktop'
$sourcePath = Join-Path $repoRoot 'build\scripts\chat-gateway-recovery-smoke.ts'
$tempRoot = Join-Path $repoRoot '.tmp\chat-gateway-recovery-smoke'
$compileRoot = Join-Path $tempRoot 'compiled'
$entryPath = Join-Path $compileRoot 'build\scripts\chat-gateway-recovery-smoke.js'
$compilePackagePath = Join-Path $compileRoot 'package.json'
$resolvedResultPath = if ($ResultPath) { $ResultPath } else { Join-Path $tempRoot 'result.json' }
function Write-Utf8File {
param([string]$FilePath, [string]$Content)
$encoding = New-Object System.Text.UTF8Encoding $false
[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($FilePath)) | Out-Null
[System.IO.File]::WriteAllText($FilePath, $Content, $encoding)
}
if (-not (Test-Path $sourcePath)) {
throw "Chat gateway recovery smoke source was not found: $sourcePath"
}
if (Test-Path $compileRoot) {
Remove-Item $compileRoot -Recurse -Force
}
New-Item -ItemType Directory -Path $compileRoot -Force | Out-Null
$compileArgs = @(
'pnpm',
'--dir', $desktopAppRoot,
'exec',
'tsc',
'--module', 'ES2022',
'--moduleResolution', 'node',
'--target', 'ES2022',
'--lib', 'ES2022',
'--types', 'node',
'--esModuleInterop',
'--allowSyntheticDefaultImports',
'--skipLibCheck',
'--outDir', $compileRoot,
$sourcePath
)
Write-Host 'Compiling chat-gateway-recovery smoke with local TypeScript'
corepack @compileArgs
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
if (-not (Test-Path $entryPath)) {
throw "Chat gateway recovery smoke entry was not emitted: $entryPath"
}
Write-Utf8File -FilePath $compilePackagePath -Content '{"type":"module"}'
Write-Host 'Running chat-gateway-recovery smoke'
node $entryPath $resolvedResultPath
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
if (-not (Test-Path $resolvedResultPath)) {
throw "Chat gateway recovery smoke did not produce a result file: $resolvedResultPath"
}
import { mkdir, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { AppConfig, GatewayStatus, RuntimeStatus } from "../../packages/shared-types/src/index.js";
import {
ensureGatewayReadyForChat,
isRecoverableGatewayChatError,
runGatewayChatRequestWithRecovery,
type ChatGatewayRecoveryCoordinator
} from "../../apps/desktop/src/main/chat-gateway-recovery.js";
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function createConfig(overrides: Partial<AppConfig> = {}): AppConfig {
return {
setupMode: "employee-key",
provider: "openai",
baseUrl: "https://api.example.com",
apiKeyConfigured: true,
gatewayTokenConfigured: true,
authTokenConfigured: true,
defaultModel: "gpt-test",
workspacePath: "D:/qjclaw/.tmp/workspace",
gatewayUrl: "ws://127.0.0.1:18789",
cloudApiBaseUrl: "https://cloud.example.com",
runtimeCloudApiBaseUrl: "https://cloud.example.com",
runtimeMode: "bundled-runtime",
...overrides
};
}
function createRuntimeStatus(overrides: Partial<RuntimeStatus> = {}): RuntimeStatus {
return {
requestedMode: "bundled-runtime",
selectedMode: "bundled-runtime",
activeMode: "bundled-runtime",
payloadState: "ready",
processState: "running",
runtimeDir: "D:/runtime",
nodeExecutable: "D:/runtime/node/node.exe",
openClawEntry: "D:/runtime/openclaw/index.js",
defaultConfigPath: "D:/runtime/config/openclaw.json",
pythonExecutable: "D:/runtime/python/python.exe",
pythonManifestPath: "D:/runtime/python/python-manifest.json",
pythonReady: true,
pythonVersion: "3.11.0",
installedPythonPackages: [],
pythonMissingModules: [],
runtimeDataDir: "D:/runtime-data",
runtimeStateDir: "D:/runtime-data/state",
runtimeLogsDir: "D:/runtime-data/logs",
logFilePath: "D:/runtime-data/logs/runtime-manager.log",
gatewayUrl: "ws://127.0.0.1:18889",
gatewayTokenConfigured: true,
pid: 1234,
startedAt: new Date().toISOString(),
stoppedAt: undefined,
lastExitCode: undefined,
lastError: undefined,
message: "Managed bundled runtime process is running and Gateway is ready.",
modeReason: "Bundled runtime is selected.",
detectedFiles: [],
missingFiles: [],
checkedAt: new Date().toISOString(),
...overrides
};
}
function createGatewayStatus(overrides: Partial<GatewayStatus> = {}): GatewayStatus {
return {
state: "connected",
url: "ws://127.0.0.1:18889",
host: "127.0.0.1",
port: 18889,
version: "test",
transport: "websocket",
lastConnectedAt: new Date().toISOString(),
lastError: undefined,
availableMethods: [],
message: "Gateway connected.",
...overrides
};
}
function createCoordinatorFixture(input: {
config?: AppConfig;
runtimeStatus?: RuntimeStatus;
gatewayStatus?: GatewayStatus | null;
shouldRefreshGatewayClient?: boolean[];
onStartManagedRuntime?: () => RuntimeStatus;
}): ChatGatewayRecoveryCoordinator & {
counters: {
startManagedRuntime: number;
reconfigureGatewayClient: number;
connectGatewayClientWithRetry: number;
shouldRefreshGatewayClient: number;
};
} {
const config = input.config ?? createConfig();
let runtimeStatus = input.runtimeStatus ?? createRuntimeStatus();
let gatewayStatus = input.gatewayStatus ?? createGatewayStatus();
const refreshDecisions = [...(input.shouldRefreshGatewayClient ?? [false])];
const counters = {
startManagedRuntime: 0,
reconfigureGatewayClient: 0,
connectGatewayClientWithRetry: 0,
shouldRefreshGatewayClient: 0
};
return {
counters,
getConfig: async () => config,
runtimeStatus: async () => runtimeStatus,
gatewayStatus: async () => gatewayStatus,
startManagedRuntime: async () => {
counters.startManagedRuntime += 1;
runtimeStatus = input.onStartManagedRuntime?.() ?? createRuntimeStatus();
return runtimeStatus;
},
shouldRefreshGatewayClient: async () => {
counters.shouldRefreshGatewayClient += 1;
return refreshDecisions.shift() ?? false;
},
reconfigureGatewayClient: async () => {
counters.reconfigureGatewayClient += 1;
gatewayStatus = createGatewayStatus({
state: "disconnected",
message: "Gateway configuration updated."
});
},
connectGatewayClientWithRetry: async () => {
counters.connectGatewayClientWithRetry += 1;
gatewayStatus = createGatewayStatus();
}
};
}
async function main(): Promise<void> {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, "..", "..");
const resultPath = path.resolve(process.argv[2] ?? path.join(repoRoot, ".tmp", "chat-gateway-recovery-smoke", "result.json"));
const tempRoot = path.dirname(resultPath);
await rm(tempRoot, { recursive: true, force: true });
await mkdir(tempRoot, { recursive: true });
assert(isRecoverableGatewayChatError("Gateway websocket is not open."), "Expected websocket-not-open to be recoverable.");
assert(isRecoverableGatewayChatError("Gateway closed during connect (1006)."), "Expected 1006 to be recoverable.");
assert(!isRecoverableGatewayChatError("Policy violation"), "Policy violation must not be treated as recoverable chat transport noise.");
const readinessCoordinator = createCoordinatorFixture({
runtimeStatus: createRuntimeStatus({
processState: "starting",
message: "Managed bundled runtime is starting."
}),
gatewayStatus: createGatewayStatus({
state: "disconnected",
message: "Gateway disconnected."
}),
shouldRefreshGatewayClient: [true]
});
await ensureGatewayReadyForChat(readinessCoordinator, "chat-send");
assert(readinessCoordinator.counters.startManagedRuntime === 1, "Bundled runtime chat preparation should start the managed runtime when it is not running.");
assert(readinessCoordinator.counters.reconfigureGatewayClient === 1, "Bundled runtime chat preparation should reconfigure the gateway client when refresh is required.");
assert(readinessCoordinator.counters.connectGatewayClientWithRetry === 1, "Bundled runtime chat preparation should reconnect the gateway client when disconnected.");
const retryCoordinator = createCoordinatorFixture({
shouldRefreshGatewayClient: [false]
});
let executeCount = 0;
const retryResult = await runGatewayChatRequestWithRecovery(retryCoordinator, {
reason: "chat-stream",
execute: async () => {
executeCount += 1;
if (executeCount === 1) {
throw new Error("Gateway websocket is not open.");
}
return {
requestId: "request-1",
sessionId: "session-1"
};
}
});
assert(executeCount === 2, "Recoverable chat failures should retry the gateway request exactly once.");
assert(retryCoordinator.counters.reconfigureGatewayClient === 1, "Recoverable chat failures should force a gateway reconfigure before retry.");
assert(retryCoordinator.counters.connectGatewayClientWithRetry === 1, "Recoverable chat failures should reconnect the gateway before retry.");
const failureCoordinator = createCoordinatorFixture({
shouldRefreshGatewayClient: [false]
});
let unrecoverableError = "";
try {
await runGatewayChatRequestWithRecovery(failureCoordinator, {
reason: "chat-send",
execute: async () => {
throw new Error("Policy violation");
}
});
} catch (error) {
unrecoverableError = error instanceof Error ? error.message : String(error);
}
assert(unrecoverableError === "Policy violation", "Unrecoverable gateway errors should surface unchanged.");
assert(failureCoordinator.counters.reconfigureGatewayClient === 0, "Unrecoverable gateway errors must not trigger forced reconnect.");
assert(failureCoordinator.counters.connectGatewayClientWithRetry === 0, "Unrecoverable gateway errors must not trigger forced reconnect.");
const summary = {
ok: true,
readinessCounters: readinessCoordinator.counters,
retryCounters: retryCoordinator.counters,
retryResult,
executeCount,
unrecoverableError
};
await writeFile(resultPath, JSON.stringify(summary, null, 2), "utf8");
console.log(JSON.stringify(summary, null, 2));
}
main().catch(async (error) => {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, "..", "..");
const resultPath = path.resolve(process.argv[2] ?? path.join(repoRoot, ".tmp", "chat-gateway-recovery-smoke", "result.json"));
const failure = {
ok: false,
error: error instanceof Error ? error.stack ?? error.message : String(error)
};
await mkdir(path.dirname(resultPath), { recursive: true });
await writeFile(resultPath, JSON.stringify(failure, null, 2), "utf8");
console.error(failure.error);
process.exitCode = 1;
});
......@@ -113,6 +113,7 @@ async function main(): Promise<void> {
await mkdir(tempRoot, { recursive: true });
const transientCodes = [
"Gateway websocket is not open.",
"Gateway closed during connect (1006).",
"Gateway closed during connect (1005).",
"Gateway closed during connect (1000).",
......
......@@ -28,7 +28,10 @@
"smoke:bundle-replacement": "powershell -ExecutionPolicy Bypass -File build/scripts/project-bundle-replacement-smoke.ps1",
"smoke:bundle-churn": "powershell -ExecutionPolicy Bypass -File build/scripts/project-bundle-churn-smoke.ps1",
"smoke:workspace-startup": "powershell -ExecutionPolicy Bypass -File build/scripts/workspace-startup-smoke.ps1",
"smoke:installer:bundled-runtime": "powershell -ExecutionPolicy Bypass -File build/scripts/installer-smoke.ps1 -RuntimeMode bundled-runtime -ExpectBundledRuntime"
"smoke:chat-gateway-recovery": "powershell -ExecutionPolicy Bypass -File build/scripts/chat-gateway-recovery-smoke.ps1",
"smoke:installer:bundled-runtime": "powershell -ExecutionPolicy Bypass -File build/scripts/installer-smoke.ps1 -RuntimeMode bundled-runtime -ExpectBundledRuntime",
"smoke:installer:path-change": "powershell -ExecutionPolicy Bypass -File build/scripts/installer-path-change-smoke.ps1",
"smoke:installer:target-residue": "powershell -ExecutionPolicy Bypass -File build/scripts/installer-target-residue-smoke.ps1"
},
"pnpm": {
"onlyBuiltDependencies": [
......
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