diff --git a/electron/gateway/config-sync.ts b/electron/gateway/config-sync.ts index 6da184b8..163e4e1f 100644 --- a/electron/gateway/config-sync.ts +++ b/electron/gateway/config-sync.ts @@ -36,7 +36,10 @@ import { prependPathEntry } from '../utils/env-path'; import { copyPluginFromNodeModules, fixupPluginManifest, cpSyncSafe, buildCandidateSources, repairTrustedOfficialPluginInstallRecords, removeTrustedOfficialPluginInstallRecord, resolvePluginNpmPackagePath } from '../utils/plugin-install'; import { safeRmSync } from '../utils/safe-fs'; import { CLAWX_OPENAI_IMAGE_PROVIDER_KEY } from '../utils/openclaw-image-relay-constants'; -import { ensureOpenClaw2026_7_1UpgradeSnapshot } from '../utils/openclaw-upgrade-snapshot'; +import { + ensureOpenClaw2026_7_1UpgradeSnapshot, + quarantineLegacyUpdateCheckState, +} from '../utils/openclaw-upgrade-snapshot'; import { stripSystemdSupervisorEnv } from './config-sync-env'; import { cleanupAgentsSymlinkedSkills, cleanupStalePluginRuntimeDeps } from './skills-symlink-cleanup'; import { @@ -651,6 +654,19 @@ export async function prepareGatewayLaunchContext(port: number): Promise { + try { + const cleanup = await quarantineLegacyUpdateCheckState(); + if (cleanup.status === 'quarantined') { + logger.info( + `[upgrade] Quarantined conflicting legacy update-check state: ${cleanup.sourcePath} → ${cleanup.backupPath}`, + ); + } + } catch (error) { + logger.warn('[upgrade] Failed to quarantine legacy update-check state:', error); + } + }); + const appSettings = await measureAsync(timingsMs, 'settingsMs', getAllSettings); const prelaunchSummary = await measureAsync(timingsMs, 'prelaunchSyncMs', async () => ( await syncGatewayConfigBeforeLaunch(appSettings, openclawDir) diff --git a/electron/gateway/manager.ts b/electron/gateway/manager.ts index 7a5b8338..059dc13d 100644 --- a/electron/gateway/manager.ts +++ b/electron/gateway/manager.ts @@ -735,6 +735,10 @@ export class GatewayManager extends EventEmitter { logger.info('Gateway ready fallback RPC router probe succeeded'); this.resetGatewayReadyFallback(); this.setStatus({ gatewayReady: true }); + // A fast Gateway can emit gateway.ready before the WebSocket client is + // attached. A successful router probe is equivalent readiness, so it + // must also complete the one-time migration snapshot lifecycle. + void this.cleanupOpenClawUpgradeSnapshot(); } } catch (error) { this.capabilityMonitor.recordCoreProbe({ diff --git a/electron/utils/openclaw-upgrade-snapshot.ts b/electron/utils/openclaw-upgrade-snapshot.ts index cc7b288c..18caf14d 100644 --- a/electron/utils/openclaw-upgrade-snapshot.ts +++ b/electron/utils/openclaw-upgrade-snapshot.ts @@ -1,5 +1,6 @@ import { chmod, copyFile, lstat, mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'; import { basename, dirname, join, relative, resolve } from 'node:path'; +import { DatabaseSync } from 'node:sqlite'; import { resolveOpenClawConfigPath, resolveOpenClawStateDir } from './paths'; const UPGRADE_ID = 'openclaw-2026.7.1'; @@ -23,6 +24,12 @@ export type OpenClawUpgradeSnapshotCleanupResult = { snapshotDir: string; }; +export type LegacyUpdateCheckCleanupResult = { + status: 'quarantined' | 'missing' | 'deferred'; + sourcePath: string; + backupPath?: string; +}; + type SnapshotOptions = { stateDir?: string; configPath?: string; @@ -57,6 +64,44 @@ async function copyFileIfPresent(source: string, destination: string, copied: st copied.push(destination); } +async function pathExists(path: string): Promise { + try { + await lstat(path); + return true; + } catch { + return false; + } +} + +async function resolveAvailableBackupPath(basePath: string): Promise { + if (!await pathExists(basePath)) return basePath; + + const timestamp = Date.now(); + for (let suffix = 0; suffix < 100; suffix += 1) { + const candidate = `${basePath}.${timestamp}${suffix === 0 ? '' : `-${suffix}`}`; + if (!await pathExists(candidate)) return candidate; + } + throw new Error(`Could not allocate backup path for ${basePath}`); +} + +function hasCanonicalUpdateCheckState(sqlitePath: string): boolean { + let db: DatabaseSync | undefined; + try { + db = new DatabaseSync(sqlitePath, { readOnly: true }); + const row = db.prepare(` + SELECT 1 AS present + FROM update_check_state + WHERE state_key = ? + LIMIT 1 + `).get('default') as { present?: number } | undefined; + return row?.present === 1; + } catch { + return false; + } finally { + db?.close(); + } +} + async function copyTree( sourceRoot: string, destinationRoot: string, @@ -161,6 +206,46 @@ export async function ensureOpenClaw2026_7_1UpgradeSnapshot( } } +/** + * OpenClaw 2026.7.1 refuses Gateway readiness when the legacy update-check JSON + * differs from an existing canonical SQLite row. The JSON contains updater + * bookkeeping only, and upstream would archive it when both copies match. Once + * SQLite has the canonical row, move the legacy file out of the active state + * root so a harmless mismatch cannot trap startup or an ineffective doctor + * retry loop. If SQLite has no row yet, leave the JSON for upstream to import. + */ +export async function quarantineLegacyUpdateCheckState( + options: Pick = {}, +): Promise { + const stateDir = resolve(options.stateDir ?? resolveOpenClawStateDir()); + const sourcePath = join(stateDir, 'update-check.json'); + let sourceInfo; + try { + sourceInfo = await lstat(sourcePath); + } catch { + return { status: 'missing', sourcePath }; + } + if (!sourceInfo.isFile() && !sourceInfo.isSymbolicLink()) { + return { status: 'deferred', sourcePath }; + } + + const sqlitePath = join(stateDir, 'state', 'openclaw.sqlite'); + if (!hasCanonicalUpdateCheckState(sqlitePath)) { + return { status: 'deferred', sourcePath }; + } + + const backupDir = join(stateDir, 'backups'); + await mkdir(backupDir, { recursive: true, mode: SNAPSHOT_DIR_MODE }); + const backupPath = await resolveAvailableBackupPath( + join(backupDir, `clawx-${UPGRADE_ID}-legacy-update-check.json`), + ); + await rename(sourcePath, backupPath); + if (sourceInfo.isFile()) { + await chmod(backupPath, SNAPSHOT_FILE_MODE); + } + return { status: 'quarantined', sourcePath, backupPath }; +} + /** * Removes the one-time OpenClaw 2026.7.1 pre-migration snapshot after Gateway * startup succeeds so duplicated config/auth/SQLite secrets do not linger. diff --git a/harness/reference/openclaw-config-delivery.md b/harness/reference/openclaw-config-delivery.md index cd01b708..f1568ed6 100644 --- a/harness/reference/openclaw-config-delivery.md +++ b/harness/reference/openclaw-config-delivery.md @@ -1,6 +1,6 @@ # OpenClaw Config Delivery -ClawX bundles OpenClaw 2026.7.1. OpenClaw owns the field-level decision between a no-op snapshot update, hot application, subsystem restart, and in-process Gateway restart. +ClawX bundles OpenClaw 2026.7.1-2. OpenClaw owns the field-level decision between a no-op snapshot update, hot application, subsystem restart, and in-process Gateway restart. Provider, Agent, Channel, skill, proxy, image-generation, and plugin-install helpers express config changes as mutators. One Main-owned coordinator owns selection of the authoritative baseline and the commit: @@ -16,6 +16,8 @@ Gateway WebSocket tracing must redact the complete serialized `raw` payload for Coordinator-backed reads follow the same authority rule: prefer the runtime-shaped `config.get.config` object while Gateway is running and use JSON5 file parsing while it is not. Compound views derive all config-backed fields from one snapshot. -OpenClaw 2026.7.1 keeps auth-profile SQLite snapshots in memory. After a completed auth-store write batch, ClawX calls `secrets.reload` once when Gateway is running. `config.set` does not replace this refresh. Agent `models.json` needs no explicit RPC because OpenClaw re-reads it when its file fingerprint changes. +OpenClaw 2026.7.1-2 keeps auth-profile SQLite snapshots in memory. After a completed auth-store write batch, ClawX calls `secrets.reload` once when Gateway is running. `config.set` does not replace this refresh. Agent `models.json` needs no explicit RPC because OpenClaw re-reads it when its file fingerprint changes. + +Before launch, upgrade compatibility cleanup checks the canonical `state/openclaw.sqlite` update-check row. If it exists, the SQLite row is authoritative and any legacy root `update-check.json` is moved with restrictive permissions under `backups/`; otherwise the JSON remains in place for OpenClaw to import. This cleanup runs after the one-time upgrade snapshot and prevents harmless updater-bookkeeping differences from blocking Gateway readiness or triggering an ineffective doctor retry. The snapshot is removed after either the native ready event or a successful RPC-router readiness fallback, covering the race where a fast Gateway emits readiness before ClawX attaches its WebSocket client. Full ClawX process replacement remains necessary after a successful coordinator commit when values are injected only at process creation, including proxy environment changes, or for explicit manual lifecycle and health/crash recovery. OpenClaw config categories must not be duplicated as a ClawX restart whitelist. diff --git a/harness/specs/tasks/upgrade-openclaw-2026-7-1-2.md b/harness/specs/tasks/upgrade-openclaw-2026-7-1-2.md new file mode 100644 index 00000000..e1b74d19 --- /dev/null +++ b/harness/specs/tasks/upgrade-openclaw-2026-7-1-2.md @@ -0,0 +1,56 @@ +--- +id: upgrade-openclaw-2026-7-1-2 +title: Upgrade the bundled OpenClaw runtime to 2026.7.1-2 +scenario: gateway-backend-communication +taskType: runtime-bridge +intent: Apply the OpenClaw 2026.7.1 correction releases without regressing ClawX channels, providers, models, ACP chat, or packaged runtime behavior. +touchedAreas: + - package.json + - pnpm-lock.yaml + - electron/gateway/config-sync.ts + - electron/gateway/manager.ts + - electron/utils/openclaw-upgrade-snapshot.ts + - tests/unit/gateway-ready-fallback.test.ts + - tests/unit/openclaw-bundle-config.test.ts + - tests/unit/openclaw-upgrade-snapshot.test.ts + - harness/reference/openclaw-config-delivery.md + - harness/specs/tasks/upgrade-openclaw-2026-7-1.md + - harness/specs/tasks/upgrade-openclaw-2026-7-1-2.md +expectedUserBehavior: + - A direct ClawX 0.5.1 upgrade from OpenClaw 2026.6.10 preserves configuration, authentication, sessions, ClawX-managed selected models, channel credentials, and managed channel plugin identities through the inherited 2026.7.1 migration. + - Manual catalog-only model references require explicit upgrade preflight because the upstream unconfigured global model catalog is smaller in 2026.7.1 than in 2026.6.10; explicit ClawX-managed provider models are not dependent on that catalog entry remaining built in. + - Existing configuration, authentication, sessions, models, and channel credentials remain usable after upgrading from OpenClaw 2026.7.1. + - The supported ClawX channel catalog and effective plugin identities remain unchanged. + - Official managed plugin updates tolerate repaired npm lock metadata and singleton-array npm view responses. + - Codex turns continue to their authoritative terminal result after progress replies. + - Legacy migration residue, Memory Core derived-sidecar conflicts, and guarded WSL EROFS permission results do not cause avoidable Gateway startup failures. + - When canonical SQLite update-check state already exists, conflicting legacy update-check JSON is backed up outside the active state root before launch instead of causing a doctor retry loop. +requiredProfiles: + - fast + - comms +requiredTests: + - tests/unit/gateway-ready-fallback.test.ts + - tests/unit/openclaw-bundle-config.test.ts + - tests/unit/openclaw-upgrade-snapshot.test.ts + - tests/unit/plugin-install.test.ts + - tests/unit/plugin-install-index.test.ts + - tests/unit/channel-config.test.ts + - tests/unit/openclaw-auth.test.ts + - tests/unit/acp-chat-service.test.ts + - tests/unit/gateway-startup-orchestrator.test.ts +acceptance: + - The OpenClaw runtime is pinned to 2026.7.1-2 and resolves @openclaw/ai 2026.7.1-2. + - External channel plugin package versions and ClawX's supported channel catalog remain unchanged because the correction release does not change channel APIs or manifests. + - Provider and model configuration behavior remains unchanged because the correction release does not change provider or model catalog sources. + - The bundled-runtime patch and pruning pipeline succeeds against OpenClaw 2026.7.1-2. + - Prelaunch preserves legacy update-check JSON for upstream import when SQLite has no canonical row, and quarantines it with restrictive permissions when SQLite is already authoritative. + - The pre-migration snapshot is removed after either the native ready event or an equivalent successful RPC-router readiness fallback, so a missed early event does not leave duplicated auth/SQLite secrets behind. + - Type checks, targeted channel/provider/ACP tests, communication regression checks, and harness validation pass. +docs: + required: false +--- + +Use this task spec for the correction-release upgrade from OpenClaw 2026.7.1 +to 2026.7.1-2. It inherits the runtime and migration compatibility work captured +in `upgrade-openclaw-2026-7-1.md` and focuses on proving that the correction +release does not widen ClawX's channel or model surface. diff --git a/harness/specs/tasks/upgrade-openclaw-2026-7-1.md b/harness/specs/tasks/upgrade-openclaw-2026-7-1.md index 9b8164e2..b6f85dfa 100644 --- a/harness/specs/tasks/upgrade-openclaw-2026-7-1.md +++ b/harness/specs/tasks/upgrade-openclaw-2026-7-1.md @@ -29,6 +29,7 @@ touchedAreas: - tests/e2e/cron-run-live-status.spec.ts - tests/unit/gateway-startup-recovery.test.ts - tests/unit/gateway-startup-orchestrator.test.ts + - tests/unit/gateway-ready-fallback.test.ts - tests/unit/openclaw-cli.test.ts - tests/unit/openclaw-bundle-config.test.ts - tests/unit/openclaw-upgrade-snapshot.test.ts @@ -39,11 +40,13 @@ touchedAreas: - README.zh-CN.md - README.ja-JP.md - README.ru-RU.md + - harness/reference/openclaw-config-delivery.md - harness/specs/scenarios/gateway-backend-communication.md - harness/specs/rules/acp-chat-state-and-history.md - harness/specs/tasks/upgrade-openclaw-2026-7-1.md + - harness/specs/tasks/upgrade-openclaw-2026-7-1-2.md expectedUserBehavior: - - Existing OpenClaw 2026.6.10 configuration, authentication, sessions, and channel credentials remain usable after upgrade, with a one-time pre-migration snapshot of migration-critical config/auth/SQLite state that is removed after Gateway startup succeeds. + - Existing ClawX-managed OpenClaw 2026.6.10 configuration, authentication, sessions, selected provider models, and channel credentials remain usable after upgrade, with a one-time pre-migration snapshot of migration-critical config/auth/SQLite state that is removed after Gateway startup succeeds. - ClawX reconciles old managed channel-plugin install records with its current mirrored extensions, removes records for unconfigured mirrors, and links declared `openclaw` peers to the bundled runtime before OpenClaw's post-core payload smoke check. - ClawX starts and communicates with the bundled OpenClaw 2026.7.1 Gateway, including migration and control-plane safe-mode startup states. - ClawX registers the compatibility-patched WeCom mirror as a local-path install with static channel metadata so OpenClaw startup migration does not replace it with the raw mismatched npm package. @@ -79,6 +82,7 @@ acceptance: - Configured mirrored plugins that declare an `openclaw` peer have a runtime link to the current bundled OpenClaw package before migration validation; stale install records for unconfigured mirrors are removed so missing directories cannot block startup. - Gateway recovery performs at most one doctor repair per startup flow and does not retry fatal runtime, EX_CONFIG, invalid migration, or active migration-lease failures indefinitely. - Electron Main reads current cron history through Gateway `cron.runs`, retains legacy JSONL as a compatibility fallback, and supplements only empty cron ACP replay in memory without replacing non-empty replay. + - ClawX-managed selected models remain explicit in provider configuration, but upgrade preflight treats manual catalog-only model references separately: OpenClaw's unconfigured `models list --all` catalog changes from 140 entries in 2026.6.10 to 86 in 2026.7.1, removing the built-in Venice, Fireworks, Tencent TokenHub, and Z.AI catalog groups plus several older Moonshot entries. - ACP 1.1 type checks, targeted runtime tests, communication regression checks, and harness validation pass. docs: required: true diff --git a/package.json b/package.json index d59c5c88..4415c765 100644 --- a/package.json +++ b/package.json @@ -175,7 +175,7 @@ "monaco-editor": "^0.55.1", "mpg123-decoder": "^1.0.3", "ms": "^2.1.3", - "openclaw": "2026.7.1", + "openclaw": "2026.7.1-2", "opusscript": "^0.1.1", "pdfjs-dist": "^5.7.284", "playwright-core": "1.59.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98e2b78b..1ea77f90 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,7 +65,7 @@ importers: version: 1.3.7 '@larksuite/openclaw-lark': specifier: 2026.7.9 - version: 2026.7.9(openclaw@2026.7.1(encoding@0.1.13)) + version: 2026.7.9(openclaw@2026.7.1-2(encoding@0.1.13)) '@larksuiteoapi/node-sdk': specifier: ^1.61.1 version: 1.62.0 @@ -74,13 +74,13 @@ importers: version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@openclaw/discord': specifier: 2026.7.1 - version: 2026.7.1(openclaw@2026.7.1(encoding@0.1.13)) + version: 2026.7.1(openclaw@2026.7.1-2(encoding@0.1.13)) '@openclaw/qqbot': specifier: 2026.7.1 - version: 2026.7.1(openclaw@2026.7.1(encoding@0.1.13)) + version: 2026.7.1(openclaw@2026.7.1-2(encoding@0.1.13)) '@openclaw/whatsapp': specifier: 2026.7.1 - version: 2026.7.1(openclaw@2026.7.1(encoding@0.1.13)) + version: 2026.7.1(openclaw@2026.7.1-2(encoding@0.1.13)) '@playwright/test': specifier: ^1.56.1 version: 1.59.0 @@ -125,7 +125,7 @@ importers: version: 0.34.48 '@soimy/dingtalk': specifier: 3.6.6 - version: 3.6.6(openclaw@2026.7.1(encoding@0.1.13)) + version: 3.6.6(openclaw@2026.7.1-2(encoding@0.1.13)) '@streamdown/cjk': specifier: ^1.0.3 version: 1.0.3(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.4)(unified@11.0.5) @@ -140,7 +140,7 @@ importers: version: 1.1.0 '@tencent-weixin/openclaw-weixin': specifier: ^2.4.6 - version: 2.4.6(openclaw@2026.7.1(encoding@0.1.13)) + version: 2.4.6(openclaw@2026.7.1-2(encoding@0.1.13)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -173,7 +173,7 @@ importers: version: 5.2.0(vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.9.0)) '@wecom/wecom-openclaw-plugin': specifier: 2026.7.2 - version: 2026.7.2(openclaw@2026.7.1(encoding@0.1.13)) + version: 2026.7.2(openclaw@2026.7.1-2(encoding@0.1.13)) '@whiskeysockets/baileys': specifier: 7.0.0-rc.9 version: 7.0.0-rc.9(audio-decode@2.2.3)(jimp@1.6.1)(sharp@0.34.5) @@ -262,8 +262,8 @@ importers: specifier: ^2.1.3 version: 2.1.3 openclaw: - specifier: 2026.7.1 - version: 2026.7.1(encoding@0.1.13) + specifier: 2026.7.1-2 + version: 2026.7.1-2(encoding@0.1.13) opusscript: specifier: ^0.1.1 version: 0.1.1 @@ -1496,8 +1496,8 @@ packages: resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==} engines: {node: ^18.17.0 || >=20.5.0} - '@openclaw/ai@2026.7.1': - resolution: {integrity: sha512-FsKy5DXSHf4qyN8Huoz/10HZRgoEwLF4uk8UWaCafaIler+q5Fsl51HcrIqIrEe0S38OT7LOaxnR++MOshAlmw==} + '@openclaw/ai@2026.7.1-2': + resolution: {integrity: sha512-st+NH0cxlQqdbEur//yYqM7WlYBjeEBnop3cztJTSCONKjv6LNoGguI9cH65asZG94FdM/39z857isRGPnZvEw==} engines: {node: '>=22.19.0'} '@openclaw/discord@2026.7.1': @@ -5315,8 +5315,8 @@ packages: zod: optional: true - openclaw@2026.7.1: - resolution: {integrity: sha512-ge/Xss99CHAjPL/ikmH/UFoiOrjcxDB4sW3y9mhyCD+dYW3wzV7TKbAVdkrXFgAG2d2BjpJofP97zUZ+umxo8g==} + openclaw@2026.7.1-2: + resolution: {integrity: sha512-ycF3yPcbjN6bUPeaUx6Mh6vze1hQWoD3CT/wWcmD7a8xaHHHRUaAlaq+lFxMHf1ssEgODVAwjlzYqp2twkYZ7g==} engines: {node: '>=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0'} hasBin: true @@ -6366,10 +6366,6 @@ packages: resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} engines: {node: '>=18'} - tar@7.5.15: - resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==} - engines: {node: '>=18'} - tar@7.5.19: resolution: {integrity: sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==} engines: {node: '>=18'} @@ -7369,7 +7365,7 @@ snapshots: ora: 5.4.1 read-binary-file-arch: 1.0.6 semver: 7.7.4 - tar: 7.5.15 + tar: 7.5.19 yargs: 17.7.2 transitivePeerDependencies: - supports-color @@ -8021,7 +8017,7 @@ snapshots: '@kurkle/color@0.3.4': {} - '@larksuite/openclaw-lark@2026.7.9(openclaw@2026.7.1(encoding@0.1.13))': + '@larksuite/openclaw-lark@2026.7.9(openclaw@2026.7.1-2(encoding@0.1.13))': dependencies: '@larksuiteoapi/node-sdk': 1.66.1 '@sinclair/typebox': 0.34.49 @@ -8029,7 +8025,7 @@ snapshots: undici-types: 8.3.0 zod: 4.4.3 optionalDependencies: - openclaw: 2026.7.1(encoding@0.1.13) + openclaw: 2026.7.1-2(encoding@0.1.13) transitivePeerDependencies: - bufferutil - debug @@ -8281,7 +8277,7 @@ snapshots: dependencies: semver: 7.7.4 - '@openclaw/ai@2026.7.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': + '@openclaw/ai@2026.7.1-2(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.109.1(zod@4.4.3) '@google/genai': 2.10.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) @@ -8301,9 +8297,9 @@ snapshots: - ws - zod - '@openclaw/discord@2026.7.1(openclaw@2026.7.1(encoding@0.1.13))': + '@openclaw/discord@2026.7.1(openclaw@2026.7.1-2(encoding@0.1.13))': optionalDependencies: - openclaw: 2026.7.1(encoding@0.1.13) + openclaw: 2026.7.1-2(encoding@0.1.13) '@openclaw/fs-safe@0.4.1': optionalDependencies: @@ -8314,13 +8310,13 @@ snapshots: dependencies: undici: 8.5.0 - '@openclaw/qqbot@2026.7.1(openclaw@2026.7.1(encoding@0.1.13))': + '@openclaw/qqbot@2026.7.1(openclaw@2026.7.1-2(encoding@0.1.13))': optionalDependencies: - openclaw: 2026.7.1(encoding@0.1.13) + openclaw: 2026.7.1-2(encoding@0.1.13) - '@openclaw/whatsapp@2026.7.1(openclaw@2026.7.1(encoding@0.1.13))': + '@openclaw/whatsapp@2026.7.1(openclaw@2026.7.1-2(encoding@0.1.13))': optionalDependencies: - openclaw: 2026.7.1(encoding@0.1.13) + openclaw: 2026.7.1-2(encoding@0.1.13) '@opentelemetry/semantic-conventions@1.43.0': {} @@ -9004,7 +9000,7 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - '@soimy/dingtalk@3.6.6(openclaw@2026.7.1(encoding@0.1.13))': + '@soimy/dingtalk@3.6.6(openclaw@2026.7.1-2(encoding@0.1.13))': dependencies: axios: 1.13.6(debug@4.4.3) dingtalk-stream: 2.1.5 @@ -9013,7 +9009,7 @@ snapshots: pdf-parse: 2.4.5 zod: 4.4.3 optionalDependencies: - openclaw: 2026.7.1(encoding@0.1.13) + openclaw: 2026.7.1-2(encoding@0.1.13) transitivePeerDependencies: - bufferutil - debug @@ -9059,9 +9055,9 @@ snapshots: dependencies: qrcode-terminal: 0.12.0 - '@tencent-weixin/openclaw-weixin@2.4.6(openclaw@2026.7.1(encoding@0.1.13))': + '@tencent-weixin/openclaw-weixin@2.4.6(openclaw@2026.7.1-2(encoding@0.1.13))': dependencies: - openclaw: 2026.7.1(encoding@0.1.13) + openclaw: 2026.7.1-2(encoding@0.1.13) qrcode-terminal: 0.12.0 zod: 4.4.3 @@ -9550,7 +9546,7 @@ snapshots: - debug - utf-8-validate - '@wecom/wecom-openclaw-plugin@2026.7.2(openclaw@2026.7.1(encoding@0.1.13))': + '@wecom/wecom-openclaw-plugin@2026.7.2(openclaw@2026.7.1-2(encoding@0.1.13))': dependencies: '@wecom/aibot-node-sdk': 1.0.6 fast-xml-parser: 5.7.3 @@ -9558,7 +9554,7 @@ snapshots: undici: 7.24.6 zod: 4.4.3 optionalDependencies: - openclaw: 2026.7.1(encoding@0.1.13) + openclaw: 2026.7.1-2(encoding@0.1.13) transitivePeerDependencies: - bufferutil - debug @@ -9980,7 +9976,7 @@ snapshots: minipass-pipeline: 1.2.4 p-map: 7.0.4 ssri: 12.0.0 - tar: 7.5.15 + tar: 7.5.19 unique-filename: 4.0.0 cacheable-lookup@5.0.4: {} @@ -12625,7 +12621,7 @@ snapshots: nopt: 8.1.0 proc-log: 5.0.0 semver: 7.7.4 - tar: 7.5.15 + tar: 7.5.19 tinyglobby: 0.2.15 which: 5.0.0 transitivePeerDependencies: @@ -12699,7 +12695,7 @@ snapshots: ws: 8.21.0 zod: 4.4.3 - openclaw@2026.7.1(encoding@0.1.13): + openclaw@2026.7.1-2(encoding@0.1.13): dependencies: '@agentclientprotocol/sdk': 1.1.0(zod@4.4.3) '@anthropic-ai/sdk': 0.109.1(zod@4.4.3) @@ -12714,7 +12710,7 @@ snapshots: '@mistralai/mistralai': 2.4.0 '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) '@mozilla/readability': 0.6.0 - '@openclaw/ai': 2026.7.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + '@openclaw/ai': 2026.7.1-2(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) '@openclaw/fs-safe': 0.4.1 '@openclaw/proxyline': 0.3.3(undici@8.5.0) '@silvia-odwyer/photon-node': 0.3.4 @@ -13971,14 +13967,6 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - tar@7.5.15: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.3 - minizlib: 3.1.0 - yallist: 5.0.0 - tar@7.5.19: dependencies: '@isaacs/fs-minipass': 4.0.1 diff --git a/tests/unit/gateway-ready-fallback.test.ts b/tests/unit/gateway-ready-fallback.test.ts index fb515d69..cbad2209 100644 --- a/tests/unit/gateway-ready-fallback.test.ts +++ b/tests/unit/gateway-ready-fallback.test.ts @@ -2,6 +2,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +const { removeUpgradeSnapshotMock } = vi.hoisted(() => ({ + removeUpgradeSnapshotMock: vi.fn(async () => ({ status: 'removed' as const, snapshotDir: '/tmp/snapshot' })), +})); + vi.mock('electron', () => ({ app: { getPath: () => '/tmp', @@ -23,6 +27,10 @@ vi.mock('@electron/utils/config', () => ({ PORTS: { OPENCLAW_GATEWAY: 18789 }, })); +vi.mock('@electron/utils/openclaw-upgrade-snapshot', () => ({ + removeOpenClaw2026_7_1UpgradeSnapshot: removeUpgradeSnapshotMock, +})); + vi.mock('@electron/gateway/startup-orchestrator', () => ({ runGatewayStartupSequence: vi.fn(async () => { throw new Error('startup unavailable in unit test'); @@ -103,6 +111,7 @@ describe('GatewayManager gatewayReady fallback', () => { const readyUpdate = statusUpdates.find((u) => u.gatewayReady === true); expect(readyUpdate).toBeDefined(); expect(rpcSpy).toHaveBeenCalledWith('system-presence', {}, 5_000); + expect(removeUpgradeSnapshotMock).toHaveBeenCalledOnce(); }); it('keeps gatewayReady=false when fallback RPC router probe fails', async () => { diff --git a/tests/unit/openclaw-bundle-config.test.ts b/tests/unit/openclaw-bundle-config.test.ts index 757b1716..7ee56008 100644 --- a/tests/unit/openclaw-bundle-config.test.ts +++ b/tests/unit/openclaw-bundle-config.test.ts @@ -8,14 +8,14 @@ import { describe, expect, it } from 'vitest'; const require = createRequire(import.meta.url); describe('openclaw bundle config', () => { - it('pins the OpenClaw 2026.7.1 runtime compatibility matrix', () => { + it('pins the OpenClaw 2026.7.1-2 runtime compatibility matrix', () => { const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf8')) as { dependencies?: Record; devDependencies?: Record; }; expect(packageJson.dependencies?.['@agentclientprotocol/sdk']).toBe('1.1.0'); expect(packageJson.devDependencies).toMatchObject({ - openclaw: '2026.7.1', + openclaw: '2026.7.1-2', electron: '40.10.6', '@openclaw/discord': '2026.7.1', '@openclaw/qqbot': '2026.7.1', @@ -66,7 +66,8 @@ describe('openclaw bundle config', () => { expect(lockfile).not.toContain("'@soimy/dingtalk@3.6.4':"); expect(lockfile).not.toContain("'@wecom/wecom-openclaw-plugin@2026.6.23':"); expect(lockfile).not.toContain("'@larksuite/openclaw-lark@2026.6.10':"); - expect(lockfile).toContain("'@openclaw/ai@2026.7.1':"); + expect(lockfile).not.toContain("'@openclaw/ai@2026.7.1':"); + expect(lockfile).toContain("'@openclaw/ai@2026.7.1-2':"); }); it('includes Electron runtime-only packages needed in packaged builds', async () => { diff --git a/tests/unit/openclaw-upgrade-snapshot.test.ts b/tests/unit/openclaw-upgrade-snapshot.test.ts index 075fb768..46c29ca0 100644 --- a/tests/unit/openclaw-upgrade-snapshot.test.ts +++ b/tests/unit/openclaw-upgrade-snapshot.test.ts @@ -2,9 +2,11 @@ import { chmod, mkdtemp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { DatabaseSync } from 'node:sqlite'; import { afterEach, describe, expect, it } from 'vitest'; import { ensureOpenClaw2026_7_1UpgradeSnapshot, + quarantineLegacyUpdateCheckState, removeOpenClaw2026_7_1UpgradeSnapshot, } from '@electron/utils/openclaw-upgrade-snapshot'; @@ -64,6 +66,57 @@ describe('OpenClaw 2026.7.1 upgrade snapshot', () => { .resolves.toBe('{"version":"old"}\n'); }); + it('quarantines legacy update-check JSON when SQLite is already authoritative', async () => { + const stateDir = await createTempStateDir(); + const sourcePath = join(stateDir, 'update-check.json'); + const sqliteDir = join(stateDir, 'state'); + const sqlitePath = join(sqliteDir, 'openclaw.sqlite'); + await mkdir(sqliteDir, { recursive: true }); + await writeFile(sourcePath, '{"lastCheckedAt":"legacy"}\n'); + + const db = new DatabaseSync(sqlitePath); + db.exec(` + CREATE TABLE update_check_state ( + state_key TEXT PRIMARY KEY, + last_checked_at TEXT + ); + INSERT INTO update_check_state (state_key, last_checked_at) + VALUES ('default', 'canonical'); + `); + db.close(); + + const result = await quarantineLegacyUpdateCheckState({ stateDir }); + expect(result.status).toBe('quarantined'); + expect(result.backupPath).toContain('clawx-openclaw-2026.7.1-legacy-update-check.json'); + await expect(stat(sourcePath)).rejects.toThrow(); + await expect(readFile(result.backupPath!, 'utf8')).resolves.toBe('{"lastCheckedAt":"legacy"}\n'); + expect((await stat(result.backupPath!)).mode & 0o777).toBe(0o600); + }); + + it('leaves legacy update-check JSON for upstream import when SQLite has no canonical row', async () => { + const stateDir = await createTempStateDir(); + const sourcePath = join(stateDir, 'update-check.json'); + const sqliteDir = join(stateDir, 'state'); + const sqlitePath = join(sqliteDir, 'openclaw.sqlite'); + await mkdir(sqliteDir, { recursive: true }); + await writeFile(sourcePath, '{"lastCheckedAt":"legacy"}\n'); + + const db = new DatabaseSync(sqlitePath); + db.exec(` + CREATE TABLE update_check_state ( + state_key TEXT PRIMARY KEY, + last_checked_at TEXT + ); + `); + db.close(); + + await expect(quarantineLegacyUpdateCheckState({ stateDir })).resolves.toMatchObject({ + status: 'deferred', + sourcePath, + }); + await expect(readFile(sourcePath, 'utf8')).resolves.toBe('{"lastCheckedAt":"legacy"}\n'); + }); + it('removes the snapshot directory after successful cleanup', async () => { const stateDir = await createTempStateDir(); const configPath = join(stateDir, 'openclaw.json');