Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
671
scripts/e2e/lib/upgrade-survivor/assertions.mjs
Normal file
671
scripts/e2e/lib/upgrade-survivor/assertions.mjs
Normal file
@@ -0,0 +1,671 @@
|
||||
// Assertions for upgrade-survivor E2E scenarios.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { readPluginInstallIndex } from "../plugin-index-sqlite.mjs";
|
||||
|
||||
const command = process.argv[2];
|
||||
const SCENARIOS = new Set([
|
||||
"base",
|
||||
"acpx-openclaw-tools-bridge",
|
||||
"feishu-channel",
|
||||
"bootstrap-persona",
|
||||
"channel-post-core-restore",
|
||||
"plugin-deps-cleanup",
|
||||
"configured-plugin-installs",
|
||||
"stale-source-plugin-shadow",
|
||||
"tilde-log-path",
|
||||
"versioned-runtime-deps",
|
||||
]);
|
||||
|
||||
const PERSONA_FILES = new Map([
|
||||
["BOOTSTRAP.md", "# Existing Bootstrap\n\nDo not overwrite me during update.\n"],
|
||||
["SOUL.md", "# Existing Soul\n\nKeep this voice intact.\n"],
|
||||
["USER.md", "# Existing User\n\nPrefers survivor tests.\n"],
|
||||
["MEMORY.md", "# Existing Memory\n\nUpgrade reports came from real users.\n"],
|
||||
]);
|
||||
|
||||
const LEGACY_SESSION_MAIN_ID = "upgrade-main-session";
|
||||
const LEGACY_SESSION_DIRECT_ID = "upgrade-direct-session";
|
||||
const LEGACY_SESSION_GROUP_ID = "upgrade-group-session";
|
||||
|
||||
function requireEnv(name) {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`${name} is required`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function readJson(file) {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
}
|
||||
|
||||
function resolveHomePath(value) {
|
||||
if (typeof value !== "string" || value.length === 0) {
|
||||
return "";
|
||||
}
|
||||
if (value === "~") {
|
||||
return process.env.HOME || value;
|
||||
}
|
||||
if (value.startsWith("~/")) {
|
||||
return path.join(process.env.HOME || "", value.slice(2));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isPathInside(parent, child) {
|
||||
const relative = path.relative(parent, child);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function isPathInsideManagedNpmProjectPackageRoot(params) {
|
||||
const relative = path.relative(path.join(params.stateDir, "npm", "projects"), params.installPath);
|
||||
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
return false;
|
||||
}
|
||||
const segments = relative.split(path.sep);
|
||||
const packageSegments = params.packageName.split("/");
|
||||
return (
|
||||
segments.length === 2 + packageSegments.length &&
|
||||
Boolean(segments[0]) &&
|
||||
segments[1] === "node_modules" &&
|
||||
packageSegments.every((segment, index) => segments[index + 2] === segment)
|
||||
);
|
||||
}
|
||||
|
||||
function write(file, contents) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, contents);
|
||||
}
|
||||
|
||||
function writeJson(file, value) {
|
||||
write(file, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function seedLegacySessionMetadata(stateDir) {
|
||||
const legacySessionsDir = path.join(stateDir, "sessions");
|
||||
writeJson(path.join(legacySessionsDir, "sessions.json"), {
|
||||
main: {
|
||||
sessionId: LEGACY_SESSION_MAIN_ID,
|
||||
sessionFile: path.join(legacySessionsDir, `${LEGACY_SESSION_MAIN_ID}.jsonl`),
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
updatedAt: 1710000000000,
|
||||
skillsSnapshot: {
|
||||
prompt: "legacy prompt survives as metadata",
|
||||
resolvedSkills: [
|
||||
{
|
||||
name: "legacy-heavy-skill-cache",
|
||||
filePath: "/tmp/openclaw-old-package/skills/legacy-heavy-skill-cache/SKILL.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"+15551234567": {
|
||||
sessionId: LEGACY_SESSION_DIRECT_ID,
|
||||
sessionFile: path.join(legacySessionsDir, `${LEGACY_SESSION_DIRECT_ID}.jsonl`),
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
updatedAt: 1710000000100,
|
||||
},
|
||||
"slack:channel:CUPGRADE": {
|
||||
sessionId: LEGACY_SESSION_GROUP_ID,
|
||||
sessionFile: path.join(legacySessionsDir, `${LEGACY_SESSION_GROUP_ID}.jsonl`),
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
updatedAt: 1710000000200,
|
||||
lastChannel: "slack",
|
||||
lastTo: "CUPGRADE",
|
||||
},
|
||||
});
|
||||
for (const sessionId of [
|
||||
LEGACY_SESSION_MAIN_ID,
|
||||
LEGACY_SESSION_DIRECT_ID,
|
||||
LEGACY_SESSION_GROUP_ID,
|
||||
]) {
|
||||
write(
|
||||
path.join(legacySessionsDir, `${sessionId}.jsonl`),
|
||||
`${JSON.stringify({ type: "session", id: sessionId })}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getScenario() {
|
||||
const scenario = process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIO || "base";
|
||||
assert(SCENARIOS.has(scenario), `unknown upgrade survivor scenario: ${scenario}`);
|
||||
return scenario;
|
||||
}
|
||||
|
||||
function getConfig() {
|
||||
return readJson(requireEnv("OPENCLAW_CONFIG_PATH"));
|
||||
}
|
||||
|
||||
function getCoverage() {
|
||||
const file = process.env.OPENCLAW_UPGRADE_SURVIVOR_CONFIG_COVERAGE_JSON;
|
||||
if (!file || !fs.existsSync(file)) {
|
||||
return null;
|
||||
}
|
||||
return readJson(file);
|
||||
}
|
||||
|
||||
function acceptsIntent(coverage, id) {
|
||||
if (!coverage) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
Array.isArray(coverage.acceptedIntents) &&
|
||||
coverage.acceptedIntents.includes(id) &&
|
||||
!coverage.skippedIntents?.includes(id)
|
||||
);
|
||||
}
|
||||
|
||||
function hasCoverage(coverage) {
|
||||
return Boolean(coverage);
|
||||
}
|
||||
|
||||
function seedState() {
|
||||
const stateDir = requireEnv("OPENCLAW_STATE_DIR");
|
||||
const workspace = requireEnv("OPENCLAW_TEST_WORKSPACE_DIR");
|
||||
const scenario = getScenario();
|
||||
|
||||
write(
|
||||
path.join(workspace, "IDENTITY.md"),
|
||||
"# Upgrade Survivor\n\nThis workspace must survive package update and doctor repair.\n",
|
||||
);
|
||||
if (scenario === "bootstrap-persona") {
|
||||
for (const [fileName, contents] of PERSONA_FILES) {
|
||||
write(path.join(workspace, fileName), contents);
|
||||
}
|
||||
}
|
||||
writeJson(path.join(workspace, ".openclaw", "workspace-state.json"), {
|
||||
version: 1,
|
||||
setupCompletedAt: "2026-04-01T00:00:00.000Z",
|
||||
});
|
||||
writeJson(path.join(stateDir, "agents", "main", "sessions", "legacy-session.json"), {
|
||||
id: "legacy-session",
|
||||
agentId: "main",
|
||||
title: "Existing user session",
|
||||
});
|
||||
seedLegacySessionMetadata(stateDir);
|
||||
|
||||
const runtimeRoot = path.join(stateDir, "plugin-runtime-deps");
|
||||
for (const plugin of ["discord", "telegram", "whatsapp"]) {
|
||||
writeJson(path.join(runtimeRoot, plugin, ".openclaw-runtime-deps-stamp.json"), {
|
||||
version: 0,
|
||||
plugin,
|
||||
stale: true,
|
||||
});
|
||||
write(
|
||||
path.join(
|
||||
runtimeRoot,
|
||||
plugin,
|
||||
".openclaw-runtime-deps-copy-stale",
|
||||
"node_modules",
|
||||
"stale-sentinel",
|
||||
"package.json",
|
||||
),
|
||||
`${JSON.stringify({ name: "stale-sentinel", version: "0.0.0" }, null, 2)}\n`,
|
||||
);
|
||||
}
|
||||
if (scenario === "versioned-runtime-deps") {
|
||||
const version = process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_VERSION || "2026.4.24";
|
||||
for (const plugin of ["discord", "feishu", "telegram", "whatsapp"]) {
|
||||
writeJson(
|
||||
path.join(
|
||||
runtimeRoot,
|
||||
`openclaw-${version}-${plugin}`,
|
||||
".openclaw-runtime-deps-stamp.json",
|
||||
),
|
||||
{
|
||||
packageVersion: version,
|
||||
plugin,
|
||||
stale: true,
|
||||
},
|
||||
);
|
||||
write(
|
||||
path.join(
|
||||
runtimeRoot,
|
||||
`openclaw-${version}-${plugin}`,
|
||||
"node_modules",
|
||||
"stale-sentinel",
|
||||
"package.json",
|
||||
),
|
||||
`${JSON.stringify({ name: "stale-sentinel", version: "0.0.0" }, null, 2)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
writeJson(path.join(stateDir, "survivor-baseline.json"), {
|
||||
agents: ["main", "ops"],
|
||||
discordGuild: "222222222222222222",
|
||||
discordChannel: "333333333333333333",
|
||||
telegramGroup: "-1001234567890",
|
||||
whatsappGroup: "120363000000000000@g.us",
|
||||
workspaceIdentity: path.join(workspace, "IDENTITY.md"),
|
||||
scenario,
|
||||
});
|
||||
}
|
||||
|
||||
function assertConfigSurvived() {
|
||||
const config = getConfig();
|
||||
const coverage = getCoverage();
|
||||
|
||||
if (acceptsIntent(coverage, "update")) {
|
||||
assert(config.update?.channel === "stable", "update.channel was not preserved");
|
||||
}
|
||||
if (acceptsIntent(coverage, "gateway")) {
|
||||
assert(config.gateway?.auth?.mode === "token", "gateway auth mode was not preserved");
|
||||
}
|
||||
|
||||
if (acceptsIntent(coverage, "models")) {
|
||||
assert(config.models?.providers?.openai, "OpenAI model provider missing");
|
||||
}
|
||||
|
||||
if (acceptsIntent(coverage, "agents")) {
|
||||
const agents = config.agents?.list ?? [];
|
||||
assert(Array.isArray(agents), "agents.list missing after update/doctor");
|
||||
assert(
|
||||
agents.some((agent) => agent?.id === "main"),
|
||||
"main agent missing",
|
||||
);
|
||||
assert(
|
||||
agents.some((agent) => agent?.id === "ops"),
|
||||
"ops agent missing",
|
||||
);
|
||||
if (hasCoverage(coverage)) {
|
||||
assert(config.agents?.defaults?.contextTokens === 64000, "default contextTokens changed");
|
||||
} else {
|
||||
assert(
|
||||
agents.find((agent) => agent?.id === "main")?.contextTokens === 64000,
|
||||
"main agent contextTokens changed",
|
||||
);
|
||||
}
|
||||
if (!hasCoverage(coverage) || !coverage.skippedIntents?.includes("agent-modern-preferences")) {
|
||||
assert(
|
||||
agents.find((agent) => agent?.id === "ops")?.fastModeDefault === true,
|
||||
"ops fastModeDefault changed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (acceptsIntent(coverage, "skills")) {
|
||||
assert(config.skills?.allowBundled?.includes("memory"), "memory skill allowlist changed");
|
||||
}
|
||||
|
||||
if (acceptsIntent(coverage, "plugins")) {
|
||||
const pluginAllow = config.plugins?.allow ?? [];
|
||||
assert(pluginAllow.includes("discord"), "discord plugin allow entry missing");
|
||||
assert(pluginAllow.includes("telegram"), "telegram plugin allow entry missing");
|
||||
if (getScenario() === "configured-plugin-installs") {
|
||||
assert(pluginAllow.includes("matrix"), "matrix plugin allow entry missing");
|
||||
} else {
|
||||
assert(pluginAllow.includes("whatsapp"), "whatsapp plugin allow entry missing");
|
||||
}
|
||||
if (hasCoverage(coverage) && acceptsIntent(coverage, "feishu-channel")) {
|
||||
assert(pluginAllow.includes("feishu"), "feishu plugin allow entry missing");
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCoverage(coverage) && acceptsIntent(coverage, "acpx-openclaw-tools-bridge")) {
|
||||
const pluginAllow = config.plugins?.allow ?? [];
|
||||
assert(pluginAllow.includes("acpx"), "ACPX plugin allow entry missing");
|
||||
assert(config.plugins?.entries?.acpx?.enabled === true, "ACPX plugin entry changed");
|
||||
assert(
|
||||
config.plugins?.entries?.acpx?.config?.openClawToolsMcpBridge === true,
|
||||
"ACPX OpenClaw tools bridge config changed",
|
||||
);
|
||||
}
|
||||
|
||||
if (hasCoverage(coverage) && acceptsIntent(coverage, "configured-plugin-installs")) {
|
||||
const pluginAllow = config.plugins?.allow ?? [];
|
||||
assert(pluginAllow.includes("discord"), "configured install discord allow entry missing");
|
||||
assert(pluginAllow.includes("telegram"), "configured install telegram allow entry missing");
|
||||
assert(pluginAllow.includes("matrix"), "configured install matrix allow entry missing");
|
||||
assert(
|
||||
config.plugins?.entries?.matrix?.enabled === true,
|
||||
"configured install matrix entry changed",
|
||||
);
|
||||
}
|
||||
|
||||
if (acceptsIntent(coverage, "discord-channel")) {
|
||||
const discord = config.channels?.discord;
|
||||
assert(discord?.enabled === true, "discord enabled flag changed");
|
||||
const discordAllowFrom = discord.allowFrom ?? discord.dm?.allowFrom;
|
||||
const discordDmPolicy = discord.dmPolicy ?? discord.dm?.policy;
|
||||
assert(discordDmPolicy === "allowlist", "discord DM policy changed");
|
||||
assert(
|
||||
Array.isArray(discordAllowFrom) && discordAllowFrom.includes("111111111111111111"),
|
||||
"discord allowFrom changed",
|
||||
);
|
||||
assert(
|
||||
discord.guilds?.["222222222222222222"]?.channels?.["333333333333333333"]?.requireMention ===
|
||||
true,
|
||||
"discord guild channel mention policy changed",
|
||||
);
|
||||
assert(discord.threadBindings?.idleHours === 72, "discord thread binding ttl changed");
|
||||
}
|
||||
|
||||
if (acceptsIntent(coverage, "telegram-channel")) {
|
||||
const telegram = config.channels?.telegram;
|
||||
assert(telegram?.enabled === true, "telegram enabled flag changed");
|
||||
assert(
|
||||
telegram.groups?.["-1001234567890"]?.requireMention === true,
|
||||
"telegram group policy changed",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
acceptsIntent(coverage, "whatsapp-channel") &&
|
||||
getScenario() !== "configured-plugin-installs"
|
||||
) {
|
||||
const whatsapp = config.channels?.whatsapp;
|
||||
assert(whatsapp?.enabled === true, "whatsapp enabled flag changed");
|
||||
const whatsappGroup = whatsapp.groups?.["120363000000000000@g.us"];
|
||||
if (hasCoverage(coverage)) {
|
||||
assert(whatsappGroup?.requireMention === true, "whatsapp group policy changed");
|
||||
} else {
|
||||
assert(
|
||||
whatsappGroup?.systemPrompt === "Use the existing WhatsApp group prompt.",
|
||||
"whatsapp group policy changed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (getScenario() === "channel-post-core-restore") {
|
||||
const whatsapp = config.channels?.whatsapp;
|
||||
assert(whatsapp?.enabled === true, "post-core channel restore dropped WhatsApp");
|
||||
assert(
|
||||
whatsapp.groups?.["120363000000000000@g.us"]?.requireMention === true,
|
||||
"post-core channel restore changed WhatsApp group config",
|
||||
);
|
||||
}
|
||||
|
||||
if (hasCoverage(coverage) && acceptsIntent(coverage, "configured-plugin-installs")) {
|
||||
const matrix = config.channels?.matrix;
|
||||
assert(matrix?.enabled === true, "matrix enabled flag changed");
|
||||
assert(matrix?.homeserver === "https://matrix.example.invalid", "matrix homeserver changed");
|
||||
assert(matrix?.userId === "@upgrade-survivor:matrix.example.invalid", "matrix userId changed");
|
||||
assert(
|
||||
!config.channels?.whatsapp,
|
||||
"whatsapp channel config should be absent in matrix scenario",
|
||||
);
|
||||
}
|
||||
|
||||
if (hasCoverage(coverage) && acceptsIntent(coverage, "feishu-channel")) {
|
||||
const feishu = config.channels?.feishu;
|
||||
assert(feishu?.enabled === true, "feishu enabled flag changed");
|
||||
assert(feishu?.connectionMode === "webhook", "feishu connection mode changed");
|
||||
assert(feishu?.defaultAccount === "default", "feishu default account changed");
|
||||
assert(feishu?.accounts?.default?.appId === "cli_upgrade_survivor", "feishu account changed");
|
||||
assert(
|
||||
feishu.groups?.oc_upgrade_survivor?.requireMention === true,
|
||||
"feishu group mention policy changed",
|
||||
);
|
||||
}
|
||||
|
||||
if (hasCoverage(coverage) && acceptsIntent(coverage, "logging")) {
|
||||
assert(
|
||||
config.logging?.file === "~/openclaw-upgrade-survivor/gateway.jsonl",
|
||||
"logging.file tilde path changed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertStateSurvived() {
|
||||
const stateDir = requireEnv("OPENCLAW_STATE_DIR");
|
||||
const workspace = requireEnv("OPENCLAW_TEST_WORKSPACE_DIR");
|
||||
const scenario = getScenario();
|
||||
const stage = process.env.OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE || "survival";
|
||||
assert(fs.existsSync(path.join(workspace, "IDENTITY.md")), "workspace identity file missing");
|
||||
assert(
|
||||
fs.existsSync(path.join(stateDir, "agents", "main", "sessions", "legacy-session.json")),
|
||||
"legacy session file missing",
|
||||
);
|
||||
if (stage !== "baseline") {
|
||||
assertSessionMetadataMigrated(stateDir);
|
||||
}
|
||||
const legacyRuntimeRoot = path.join(stateDir, "plugin-runtime-deps");
|
||||
if (stage === "baseline") {
|
||||
if (fs.existsSync(legacyRuntimeRoot)) {
|
||||
assert(
|
||||
fs.existsSync(path.join(legacyRuntimeRoot, "discord")),
|
||||
"legacy plugin runtime deps root exists but discord debris is missing before doctor cleanup",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
assert(
|
||||
!fs.existsSync(legacyRuntimeRoot),
|
||||
`legacy plugin runtime deps root survived update/doctor: ${legacyRuntimeRoot}`,
|
||||
);
|
||||
}
|
||||
if (scenario === "bootstrap-persona") {
|
||||
for (const [fileName, contents] of PERSONA_FILES) {
|
||||
const actual = fs.readFileSync(path.join(workspace, fileName), "utf8");
|
||||
assert(actual === contents, `${fileName} was changed during update/doctor`);
|
||||
}
|
||||
}
|
||||
if (scenario === "stale-source-plugin-shadow") {
|
||||
const staleRoot = path.join(stateDir, "extensions", "opik-openclaw");
|
||||
assert(
|
||||
fs.existsSync(path.join(staleRoot, "src", "index.ts")),
|
||||
"source-only plugin shadow fixture missing",
|
||||
);
|
||||
}
|
||||
if (scenario === "versioned-runtime-deps") {
|
||||
if (stage === "baseline") {
|
||||
return;
|
||||
}
|
||||
const version = process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_VERSION || "2026.4.24";
|
||||
const runtimeRoot = path.join(stateDir, "plugin-runtime-deps");
|
||||
const staleVersionedRoots = fs.existsSync(runtimeRoot)
|
||||
? fs.readdirSync(runtimeRoot).filter((entry) => entry.startsWith(`openclaw-${version}-`))
|
||||
: [];
|
||||
assert(
|
||||
staleVersionedRoots.length === 0,
|
||||
`stale versioned runtime deps survived update/doctor: ${staleVersionedRoots.join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertSessionMetadataMigrated(stateDir) {
|
||||
const legacyStorePath = path.join(stateDir, "sessions", "sessions.json");
|
||||
const agentSessionsDir = path.join(stateDir, "agents", "main", "sessions");
|
||||
const targetStorePath = path.join(agentSessionsDir, "sessions.json");
|
||||
assert(
|
||||
!fs.existsSync(legacyStorePath),
|
||||
`legacy sessions.json survived migration: ${legacyStorePath}`,
|
||||
);
|
||||
for (const sessionId of [
|
||||
LEGACY_SESSION_MAIN_ID,
|
||||
LEGACY_SESSION_DIRECT_ID,
|
||||
LEGACY_SESSION_GROUP_ID,
|
||||
]) {
|
||||
assert(
|
||||
fs.existsSync(path.join(agentSessionsDir, `${sessionId}.jsonl`)),
|
||||
`legacy session transcript was not moved for ${sessionId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const store = readMigratedSessionStore(stateDir, targetStorePath);
|
||||
const main = store["agent:main:main"];
|
||||
const direct = store["agent:main:+15551234567"];
|
||||
const group = store["agent:main:slack:channel:cupgrade"];
|
||||
assert(main?.sessionId === LEGACY_SESSION_MAIN_ID, "main legacy session row missing");
|
||||
assert(direct?.sessionId === LEGACY_SESSION_DIRECT_ID, "direct legacy session row missing");
|
||||
assert(group?.sessionId === LEGACY_SESSION_GROUP_ID, "channel legacy session row missing");
|
||||
assert(
|
||||
main?.sessionFile === path.join(agentSessionsDir, `${LEGACY_SESSION_MAIN_ID}.jsonl`),
|
||||
"main legacy session row still points at the old sessions directory",
|
||||
);
|
||||
assert(
|
||||
direct?.sessionFile === path.join(agentSessionsDir, `${LEGACY_SESSION_DIRECT_ID}.jsonl`),
|
||||
"direct legacy session row still points at the old sessions directory",
|
||||
);
|
||||
assert(
|
||||
group?.sessionFile === path.join(agentSessionsDir, `${LEGACY_SESSION_GROUP_ID}.jsonl`),
|
||||
"channel legacy session row still points at the old sessions directory",
|
||||
);
|
||||
assert(
|
||||
main.skillsSnapshot?.prompt === "legacy prompt survives as metadata",
|
||||
"legacy session metadata prompt was not preserved",
|
||||
);
|
||||
assert(
|
||||
main.skillsSnapshot?.resolvedSkills === undefined,
|
||||
"heavy resolvedSkills cache was persisted into migrated session metadata",
|
||||
);
|
||||
}
|
||||
|
||||
function readMigratedSessionStore(stateDir, targetStorePath) {
|
||||
if (fs.existsSync(targetStorePath)) {
|
||||
return readJson(targetStorePath);
|
||||
}
|
||||
|
||||
const dbPath = path.join(stateDir, "agents", "main", "agent", "openclaw-agent.sqlite");
|
||||
assert(fs.existsSync(dbPath), `agent session store missing: ${targetStorePath} or ${dbPath}`);
|
||||
|
||||
let db;
|
||||
try {
|
||||
db = new DatabaseSync(dbPath, { readOnly: true });
|
||||
const rows = db
|
||||
.prepare("SELECT key, value_json FROM cache_entries WHERE scope = ?")
|
||||
.all("session_entries");
|
||||
const store = {};
|
||||
for (const row of rows) {
|
||||
if (typeof row?.key !== "string" || typeof row?.value_json !== "string") {
|
||||
continue;
|
||||
}
|
||||
store[row.key] = JSON.parse(row.value_json);
|
||||
}
|
||||
return store;
|
||||
} finally {
|
||||
db?.close();
|
||||
}
|
||||
}
|
||||
|
||||
function readInstalledPluginIndex() {
|
||||
const stateDir = requireEnv("OPENCLAW_STATE_DIR");
|
||||
const index = readPluginInstallIndex({ stateDir });
|
||||
assert(index.installRecords, "installed plugin index missing");
|
||||
return index;
|
||||
}
|
||||
|
||||
function assertExternalPluginInstall(records, pluginId, packageName) {
|
||||
const record = records[pluginId];
|
||||
assert(record, `configured external ${pluginId} plugin install record missing`);
|
||||
const installedFromNpm = record.source === "npm";
|
||||
const installedFromOfficialClawHubNpmPack =
|
||||
record.source === "clawhub" &&
|
||||
record.clawhubChannel === "official" &&
|
||||
record.artifactKind === "npm-pack";
|
||||
assert(
|
||||
installedFromNpm || installedFromOfficialClawHubNpmPack,
|
||||
`configured external ${pluginId} plugin must be installed from npm or official ClawHub npm-pack, got: ${record.source}`,
|
||||
);
|
||||
const installPath = resolveHomePath(record.installPath);
|
||||
assert(
|
||||
installPath,
|
||||
`configured external ${pluginId} plugin installPath missing: ${JSON.stringify(record)}`,
|
||||
);
|
||||
assert(
|
||||
fs.existsSync(installPath),
|
||||
`configured external ${pluginId} plugin installPath missing on disk: ${installPath}`,
|
||||
);
|
||||
assert(
|
||||
fs.existsSync(path.join(installPath, "package.json")),
|
||||
`configured external ${pluginId} plugin package.json missing: ${installPath}`,
|
||||
);
|
||||
const packageJson = readJson(path.join(installPath, "package.json"));
|
||||
assert(
|
||||
packageJson.name === packageName,
|
||||
`configured external ${pluginId} package name changed: ${packageJson.name}`,
|
||||
);
|
||||
if (installedFromNpm) {
|
||||
const stateDir = requireEnv("OPENCLAW_STATE_DIR");
|
||||
assert(
|
||||
isPathInsideManagedNpmProjectPackageRoot({ stateDir, installPath, packageName }),
|
||||
`configured external ${pluginId} npm install path outside managed npm project root: ${installPath}`,
|
||||
);
|
||||
assert(
|
||||
String(record.spec ?? record.resolvedSpec ?? "").startsWith(packageName),
|
||||
`configured external ${pluginId} plugin npm spec changed`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
assert(
|
||||
record.clawhubPackage === packageName,
|
||||
`configured external ${pluginId} ClawHub package changed: ${record.clawhubPackage}`,
|
||||
);
|
||||
const extensionsRoot = path.join(requireEnv("OPENCLAW_STATE_DIR"), "extensions");
|
||||
assert(
|
||||
isPathInside(extensionsRoot, installPath),
|
||||
`configured external ${pluginId} ClawHub install path outside managed extensions root: ${installPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
function assertConfiguredPluginInstalls() {
|
||||
const coverage = getCoverage();
|
||||
const stage = process.env.OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE || "survival";
|
||||
if (!hasCoverage(coverage) || !acceptsIntent(coverage, "configured-plugin-installs")) {
|
||||
return;
|
||||
}
|
||||
if (stage === "baseline") {
|
||||
return;
|
||||
}
|
||||
const index = readInstalledPluginIndex();
|
||||
const records = index.installRecords ?? {};
|
||||
assertOptionalConfiguredPluginIndex(records, index.plugins ?? [], {
|
||||
bundled: true,
|
||||
packageName: "@openclaw/matrix",
|
||||
pluginId: "matrix",
|
||||
});
|
||||
assertOptionalConfiguredPluginIndex(records, index.plugins ?? [], {
|
||||
packageName: "@openclaw/brave-plugin",
|
||||
pluginId: "brave",
|
||||
});
|
||||
assert(!records.telegram, "internal telegram plugin should not be installed externally");
|
||||
}
|
||||
|
||||
function assertOptionalConfiguredPluginIndex(
|
||||
records,
|
||||
plugins,
|
||||
{ bundled = false, packageName, pluginId },
|
||||
) {
|
||||
const record = records[pluginId];
|
||||
const plugin = plugins.find((entry) => entry?.pluginId === pluginId);
|
||||
if (record) {
|
||||
assertExternalPluginInstall(records, pluginId, packageName);
|
||||
}
|
||||
if (plugin) {
|
||||
assert(
|
||||
plugin.enabled !== false,
|
||||
`configured ${bundled ? "bundled" : "external"} ${pluginId} plugin is disabled`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertStatusJson([file]) {
|
||||
const status = readJson(file);
|
||||
assert(status && typeof status === "object", "gateway status JSON was not an object");
|
||||
const text = JSON.stringify(status);
|
||||
assert(/running|connected|ok|ready/u.test(text), "gateway status did not report a healthy state");
|
||||
}
|
||||
|
||||
if (command === "seed") {
|
||||
seedState();
|
||||
} else if (command === "assert-config") {
|
||||
assertConfigSurvived();
|
||||
} else if (command === "assert-state") {
|
||||
assertStateSurvived();
|
||||
assertConfiguredPluginInstalls();
|
||||
} else if (command === "assert-status-json") {
|
||||
assertStatusJson(process.argv.slice(3));
|
||||
} else {
|
||||
throw new Error(`unknown upgrade-survivor assertion command: ${command ?? "<missing>"}`);
|
||||
}
|
||||
Reference in New Issue
Block a user