mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-26 10:13:27 +08:00
refactor(e2e): consolidate runner setup and observations
Share per-variant setup, process handling, bounded HTTP reads and upgrade observations while preserving lane arguments, output and lifecycle order. Close the diagnostic dependency copies used by isolated and frozen runners.
This commit is contained in:
59 files changed
+887
-1375
No files matched your search
@@ -195,28 +195,15 @@ const commandParentSignals: NodeJS.Signals[] =
|
||||
let commandShutdownPromise: Promise<void[]> | undefined;
|
||||
let commandSignalHandlersInstalled = false;
|
||||
|
||||
function installCommandSignalHandlers() {
|
||||
if (commandSignalHandlersInstalled) {
|
||||
function setCommandSignalHandlersInstalled(installed: boolean) {
|
||||
if (commandSignalHandlersInstalled === installed) {
|
||||
return;
|
||||
}
|
||||
commandSignalHandlersInstalled = true;
|
||||
commandSignalHandlersInstalled = installed;
|
||||
for (const signal of commandParentSignals) {
|
||||
const handler = commandSignalHandlers.get(signal);
|
||||
if (handler) {
|
||||
process.on(signal, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeCommandSignalHandlers() {
|
||||
if (!commandSignalHandlersInstalled) {
|
||||
return;
|
||||
}
|
||||
commandSignalHandlersInstalled = false;
|
||||
for (const signal of commandParentSignals) {
|
||||
const handler = commandSignalHandlers.get(signal);
|
||||
if (handler) {
|
||||
process.off(signal, handler);
|
||||
process[installed ? "on" : "off"](signal, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,53 +296,34 @@ export function readPositiveTimerMs(raw: string | undefined, fallback: number, l
|
||||
}
|
||||
|
||||
export function resolveKitchenSinkRpcConfig(env: ProcessEnv = process.env) {
|
||||
const commandTimeoutMs = readPositiveTimerMs(
|
||||
env.OPENCLAW_KITCHEN_SINK_RPC_COMMAND_MS,
|
||||
DEFAULT_COMMAND_TIMEOUT_MS,
|
||||
const integer = (key: string, fallback: number) => readPositiveInt(env[key], fallback, key);
|
||||
const timer = (key: string, fallback: number) => readPositiveTimerMs(env[key], fallback, key);
|
||||
const commandTimeoutMs = timer(
|
||||
"OPENCLAW_KITCHEN_SINK_RPC_COMMAND_MS",
|
||||
DEFAULT_COMMAND_TIMEOUT_MS,
|
||||
);
|
||||
return {
|
||||
commandMaxRssMiB: readPositiveInt(
|
||||
env.OPENCLAW_KITCHEN_SINK_COMMAND_MAX_RSS_MIB,
|
||||
DEFAULT_MAX_COMMAND_RSS_MIB,
|
||||
commandMaxRssMiB: integer(
|
||||
"OPENCLAW_KITCHEN_SINK_COMMAND_MAX_RSS_MIB",
|
||||
DEFAULT_MAX_COMMAND_RSS_MIB,
|
||||
),
|
||||
commandTimeoutMs,
|
||||
fetchBodyMaxBytes: readPositiveInt(
|
||||
env.OPENCLAW_KITCHEN_SINK_RPC_FETCH_BODY_BYTES,
|
||||
DEFAULT_FETCH_BODY_MAX_BYTES,
|
||||
fetchBodyMaxBytes: integer(
|
||||
"OPENCLAW_KITCHEN_SINK_RPC_FETCH_BODY_BYTES",
|
||||
DEFAULT_FETCH_BODY_MAX_BYTES,
|
||||
),
|
||||
fetchTimeoutMs: readPositiveTimerMs(
|
||||
env.OPENCLAW_KITCHEN_SINK_RPC_FETCH_MS,
|
||||
DEFAULT_FETCH_TIMEOUT_MS,
|
||||
"OPENCLAW_KITCHEN_SINK_RPC_FETCH_MS",
|
||||
),
|
||||
installTimeoutMs: readPositiveTimerMs(
|
||||
env.OPENCLAW_KITCHEN_SINK_RPC_INSTALL_MS,
|
||||
Math.max(commandTimeoutMs, DEFAULT_INSTALL_TIMEOUT_MS),
|
||||
fetchTimeoutMs: timer("OPENCLAW_KITCHEN_SINK_RPC_FETCH_MS", DEFAULT_FETCH_TIMEOUT_MS),
|
||||
installTimeoutMs: timer(
|
||||
"OPENCLAW_KITCHEN_SINK_RPC_INSTALL_MS",
|
||||
Math.max(commandTimeoutMs, DEFAULT_INSTALL_TIMEOUT_MS),
|
||||
),
|
||||
maxRssMiB: readPositiveInt(
|
||||
env.OPENCLAW_KITCHEN_SINK_MAX_RSS_MIB,
|
||||
DEFAULT_MAX_RSS_MIB,
|
||||
"OPENCLAW_KITCHEN_SINK_MAX_RSS_MIB",
|
||||
),
|
||||
outputCaptureChars: readPositiveInt(
|
||||
env.OPENCLAW_KITCHEN_SINK_OUTPUT_CAPTURE_CHARS,
|
||||
DEFAULT_OUTPUT_CAPTURE_CHARS,
|
||||
maxRssMiB: integer("OPENCLAW_KITCHEN_SINK_MAX_RSS_MIB", DEFAULT_MAX_RSS_MIB),
|
||||
outputCaptureChars: integer(
|
||||
"OPENCLAW_KITCHEN_SINK_OUTPUT_CAPTURE_CHARS",
|
||||
DEFAULT_OUTPUT_CAPTURE_CHARS,
|
||||
),
|
||||
readyTimeoutMs: readPositiveTimerMs(
|
||||
env.OPENCLAW_KITCHEN_SINK_RPC_READY_MS,
|
||||
DEFAULT_READY_TIMEOUT_MS,
|
||||
"OPENCLAW_KITCHEN_SINK_RPC_READY_MS",
|
||||
),
|
||||
rpcTimeoutMs: readPositiveTimerMs(
|
||||
env.OPENCLAW_KITCHEN_SINK_RPC_CALL_MS,
|
||||
DEFAULT_RPC_TIMEOUT_MS,
|
||||
"OPENCLAW_KITCHEN_SINK_RPC_CALL_MS",
|
||||
),
|
||||
readyTimeoutMs: timer("OPENCLAW_KITCHEN_SINK_RPC_READY_MS", DEFAULT_READY_TIMEOUT_MS),
|
||||
rpcTimeoutMs: timer("OPENCLAW_KITCHEN_SINK_RPC_CALL_MS", DEFAULT_RPC_TIMEOUT_MS),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -529,7 +497,7 @@ export function runCommand(
|
||||
detached: spawnOptions.detached ?? process.platform !== "win32",
|
||||
});
|
||||
activeCommandChildren.add(child);
|
||||
installCommandSignalHandlers();
|
||||
setCommandSignalHandlersInstalled(true);
|
||||
const startedAt = Date.now();
|
||||
let stdout = { text: "", truncatedChars: 0 };
|
||||
let stderr = { text: "", truncatedChars: 0 };
|
||||
@@ -692,7 +660,7 @@ async function finishTimedOutCommandProcessTree(
|
||||
function releaseCommandChild(child: CommandChild) {
|
||||
activeCommandChildren.delete(child);
|
||||
if (activeCommandChildren.size === 0 && !commandShutdownPromise) {
|
||||
removeCommandSignalHandlers();
|
||||
setCommandSignalHandlersInstalled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,7 +684,7 @@ async function shutdownActiveCommands(signal: NodeJS.Signals) {
|
||||
}),
|
||||
),
|
||||
).finally(() => {
|
||||
removeCommandSignalHandlers();
|
||||
setCommandSignalHandlersInstalled(false);
|
||||
process.kill(process.pid, signal);
|
||||
});
|
||||
return commandShutdownPromise;
|
||||
@@ -793,16 +761,11 @@ export function signalProcessGroup(
|
||||
useProcessGroup?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const {
|
||||
platform = process.platform,
|
||||
runTaskkill = childProcess.spawnSync,
|
||||
useProcessGroup = platform !== "win32",
|
||||
} = options;
|
||||
signalChildProcessTree(child, signal, {
|
||||
killProcess: (pid, childSignal) => process.kill(pid, childSignal),
|
||||
platform,
|
||||
runTaskkill,
|
||||
useProcessGroup,
|
||||
platform: options.platform,
|
||||
runTaskkill: options.runTaskkill,
|
||||
useProcessGroup: options.useProcessGroup,
|
||||
killProcess: defaultKillProcess,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1531,17 +1494,12 @@ export function signalGateway(
|
||||
useProcessGroup?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const {
|
||||
platform = process.platform,
|
||||
runTaskkill = childProcess.spawnSync,
|
||||
useProcessGroup = platform !== "win32",
|
||||
} = options;
|
||||
return signalChildProcessTree(child, signal, {
|
||||
platform: options.platform,
|
||||
runTaskkill: options.runTaskkill,
|
||||
useProcessGroup: options.useProcessGroup,
|
||||
groupEsrchMeansExited: true,
|
||||
killProcess,
|
||||
platform,
|
||||
runTaskkill,
|
||||
useProcessGroup,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1551,17 +1509,17 @@ function signalChildProcessTree(
|
||||
options: {
|
||||
groupEsrchMeansExited?: boolean;
|
||||
killProcess: KillProcess;
|
||||
platform: NodeJS.Platform;
|
||||
runTaskkill: TaskkillRunner;
|
||||
useProcessGroup: boolean;
|
||||
platform?: NodeJS.Platform;
|
||||
runTaskkill?: TaskkillRunner;
|
||||
useProcessGroup?: boolean;
|
||||
},
|
||||
) {
|
||||
const {
|
||||
groupEsrchMeansExited = false,
|
||||
killProcess,
|
||||
platform,
|
||||
runTaskkill,
|
||||
useProcessGroup,
|
||||
platform = process.platform,
|
||||
runTaskkill = childProcess.spawnSync,
|
||||
useProcessGroup = platform !== "win32",
|
||||
} = options;
|
||||
if (useProcessGroup && typeof child.pid === "number") {
|
||||
try {
|
||||
@@ -1833,12 +1791,16 @@ export function assertTtsProviderCoverage(payload: unknown, surface: "providers"
|
||||
}
|
||||
}
|
||||
|
||||
export function assertKitchenSinkSearchInvokeResult(payload: unknown) {
|
||||
function kitchenSinkToolOutput(payload: unknown, kind: "search" | "text" | "image job") {
|
||||
const candidate = asRecord(payload);
|
||||
if (candidate.ok !== true || candidate.source !== "plugin") {
|
||||
throw new Error(`Kitchen Sink search tool invoke failed: ${boundedJsonPreview(payload)}`);
|
||||
throw new Error(`Kitchen Sink ${kind} tool invoke failed: ${boundedJsonPreview(payload)}`);
|
||||
}
|
||||
const output = assertObjectPayload(candidate.output, "Kitchen Sink search tool output");
|
||||
return assertObjectPayload(candidate.output, `Kitchen Sink ${kind} tool output`);
|
||||
}
|
||||
|
||||
export function assertKitchenSinkSearchInvokeResult(payload: unknown) {
|
||||
const output = kitchenSinkToolOutput(payload, "search");
|
||||
const results = Array.isArray(output.results) ? output.results : [];
|
||||
const hasFixture = results.some(
|
||||
(entry) => asRecord(entry).title === "Kitchen Sink image fixture",
|
||||
@@ -1851,11 +1813,7 @@ export function assertKitchenSinkSearchInvokeResult(payload: unknown) {
|
||||
}
|
||||
|
||||
export function assertKitchenSinkTextInvokeResult(payload: unknown) {
|
||||
const candidate = asRecord(payload);
|
||||
if (candidate.ok !== true || candidate.source !== "plugin") {
|
||||
throw new Error(`Kitchen Sink text tool invoke failed: ${boundedJsonPreview(payload)}`);
|
||||
}
|
||||
const output = assertObjectPayload(candidate.output, "Kitchen Sink text tool output");
|
||||
const output = kitchenSinkToolOutput(payload, "text");
|
||||
if (
|
||||
output.route !== "tool:kitchen_sink_text" ||
|
||||
typeof output.text !== "string" ||
|
||||
@@ -1868,11 +1826,7 @@ export function assertKitchenSinkTextInvokeResult(payload: unknown) {
|
||||
}
|
||||
|
||||
export function assertKitchenSinkImageJobInvokeResult(payload: unknown) {
|
||||
const candidate = asRecord(payload);
|
||||
if (candidate.ok !== true || candidate.source !== "plugin") {
|
||||
throw new Error(`Kitchen Sink image job tool invoke failed: ${boundedJsonPreview(payload)}`);
|
||||
}
|
||||
const output = assertObjectPayload(candidate.output, "Kitchen Sink image job tool output");
|
||||
const output = kitchenSinkToolOutput(payload, "image job");
|
||||
const image = assertObjectPayload(output.image, "Kitchen Sink image job image");
|
||||
const imageMetadata = assertObjectPayload(
|
||||
image.metadata,
|
||||
|
||||
@@ -996,7 +996,14 @@ async function smokePlugin(pluginId, pluginDir, requiresConfig, pluginIndex, plu
|
||||
);
|
||||
}
|
||||
|
||||
async function runGatewaySmoke(options, probe, successMessage) {
|
||||
async function runGatewaySmoke(initialOptions, probe, successMessage, isolatedState) {
|
||||
let options = initialOptions;
|
||||
if (isolatedState) {
|
||||
const { label, config } = isolatedState;
|
||||
const env = createIsolatedStateEnv(label);
|
||||
writeConfig(ensureGatewayConfig(config, options.port), env);
|
||||
options = { ...options, env };
|
||||
}
|
||||
const child = startGateway(options);
|
||||
try {
|
||||
await waitForReady({ ...options, child });
|
||||
@@ -1288,28 +1295,12 @@ async function smokeTtsGlobalDisable(pluginId, pluginDir, provider, pluginIndex,
|
||||
return;
|
||||
}
|
||||
const port = resolveRuntimeSmokePort(pluginIndex, 1);
|
||||
const env = createIsolatedStateEnv(`tts-disabled-${pluginId}`);
|
||||
writeConfig(
|
||||
ensureGatewayConfig(
|
||||
{
|
||||
plugins: {
|
||||
enabled: false,
|
||||
},
|
||||
tts: {
|
||||
provider: selectedProvider,
|
||||
},
|
||||
},
|
||||
port,
|
||||
),
|
||||
env,
|
||||
);
|
||||
const logPath = `/tmp/openclaw-plugin-runtime-${pluginIndex}-${pluginId}-tts-disabled.log`;
|
||||
await runGatewaySmoke(
|
||||
{
|
||||
logPath,
|
||||
port,
|
||||
entrypoint,
|
||||
env,
|
||||
skipChannels: true,
|
||||
pluginId: `${pluginId}:tts-disabled`,
|
||||
},
|
||||
@@ -1319,6 +1310,17 @@ async function smokeTtsGlobalDisable(pluginId, pluginDir, provider, pluginIndex,
|
||||
assertSpeechProviderVisible(providers, selectedProvider, "tts.providers global-disable");
|
||||
},
|
||||
`Global-disable TTS smoke passed for ${pluginId}/${selectedProvider}`,
|
||||
{
|
||||
label: `tts-disabled-${pluginId}`,
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: false,
|
||||
},
|
||||
tts: {
|
||||
provider: selectedProvider,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1332,10 +1334,20 @@ async function smokeOpenAiTts(pluginIndex) {
|
||||
return;
|
||||
}
|
||||
const port = resolveRuntimeSmokePort(pluginIndex, 2);
|
||||
const env = createIsolatedStateEnv("tts-openai-live");
|
||||
writeConfig(
|
||||
ensureGatewayConfig(
|
||||
{
|
||||
const logPath = `/tmp/openclaw-plugin-runtime-${pluginIndex}-openai-tts-live.log`;
|
||||
await runGatewaySmoke(
|
||||
{ entrypoint, port, logPath, skipChannels: true, pluginId: "openai:tts-live" },
|
||||
async (options) => {
|
||||
await assertBaseGatewayProbes(options);
|
||||
const result = await retryRpcCall("tts.convert", { text: "ok", provider: "openai" }, options);
|
||||
if (!isNonEmptyString(result.audioPath) || !fs.existsSync(result.audioPath)) {
|
||||
throw new Error(`tts.convert did not produce an audio file: ${JSON.stringify(result)}`);
|
||||
}
|
||||
},
|
||||
"OpenAI key-backed TTS smoke passed",
|
||||
{
|
||||
label: "tts-openai-live",
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
allow: ["openai"],
|
||||
@@ -1352,21 +1364,7 @@ async function smokeOpenAiTts(pluginIndex) {
|
||||
},
|
||||
},
|
||||
},
|
||||
port,
|
||||
),
|
||||
env,
|
||||
);
|
||||
const logPath = `/tmp/openclaw-plugin-runtime-${pluginIndex}-openai-tts-live.log`;
|
||||
await runGatewaySmoke(
|
||||
{ entrypoint, port, logPath, env, skipChannels: true, pluginId: "openai:tts-live" },
|
||||
async (options) => {
|
||||
await assertBaseGatewayProbes(options);
|
||||
const result = await retryRpcCall("tts.convert", { text: "ok", provider: "openai" }, options);
|
||||
if (!isNonEmptyString(result.audioPath) || !fs.existsSync(result.audioPath)) {
|
||||
throw new Error(`tts.convert did not produce an audio file: ${JSON.stringify(result)}`);
|
||||
}
|
||||
},
|
||||
"OpenAI key-backed TTS smoke passed",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,15 @@ const workspaceDir = requireEnv("OPENCLAW_TEST_WORKSPACE_DIR");
|
||||
const token = requireEnv("OPENCLAW_GATEWAY_TOKEN");
|
||||
const timeoutSeconds = readPositiveIntEnv("OPENCLAW_CODEX_MEDIA_PATH_TIMEOUT_SECONDS", 180);
|
||||
const gatewayPort = readTcpPortEnv("PORT", 18790);
|
||||
const agentConfig = {
|
||||
model: { primary: "openai/gpt-5.6-luna", fallbacks: [] },
|
||||
models: {
|
||||
"openai/gpt-5.6-luna": {
|
||||
agentRuntime: { id: "codex" },
|
||||
},
|
||||
},
|
||||
workspace: workspaceDir,
|
||||
};
|
||||
|
||||
const config = {
|
||||
gateway: {
|
||||
@@ -43,27 +52,13 @@ const config = {
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-5.6-luna", fallbacks: [] },
|
||||
models: {
|
||||
"openai/gpt-5.6-luna": {
|
||||
agentRuntime: { id: "codex" },
|
||||
},
|
||||
},
|
||||
workspace: workspaceDir,
|
||||
...agentConfig,
|
||||
skipBootstrap: true,
|
||||
timeoutSeconds,
|
||||
sandbox: { mode: "off" },
|
||||
},
|
||||
entries: {
|
||||
main: {
|
||||
model: { primary: "openai/gpt-5.6-luna", fallbacks: [] },
|
||||
models: {
|
||||
"openai/gpt-5.6-luna": {
|
||||
agentRuntime: { id: "codex" },
|
||||
},
|
||||
},
|
||||
workspace: workspaceDir,
|
||||
},
|
||||
main: agentConfig,
|
||||
},
|
||||
},
|
||||
skills: { allowBundled: [] },
|
||||
|
||||
@@ -637,24 +637,14 @@ function extractOpenClawToolTimeline(transcriptEvents) {
|
||||
return timeline;
|
||||
}
|
||||
|
||||
function findSuccessfulToolResult(timeline, call, beforeIndex = Number.POSITIVE_INFINITY) {
|
||||
function findToolResult(timeline, call, beforeIndex = Number.POSITIVE_INFINITY, successful = true) {
|
||||
return timeline.find(
|
||||
(entry) =>
|
||||
entry.type === "result" &&
|
||||
entry.id === call.id &&
|
||||
entry.index > call.index &&
|
||||
entry.index < beforeIndex &&
|
||||
entry.isError !== true,
|
||||
);
|
||||
}
|
||||
|
||||
function findToolResult(timeline, call, beforeIndex = Number.POSITIVE_INFINITY) {
|
||||
return timeline.find(
|
||||
(entry) =>
|
||||
entry.type === "result" &&
|
||||
entry.id === call.id &&
|
||||
entry.index > call.index &&
|
||||
entry.index < beforeIndex,
|
||||
(!successful || entry.isError !== true),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -692,11 +682,11 @@ function assertFollowthroughTranscript({ transcriptEvents, progressMarker, compl
|
||||
);
|
||||
}
|
||||
const [progressCall, completeCall] = markerCalls;
|
||||
const progressResult = findSuccessfulToolResult(timeline, progressCall, completeCall.index);
|
||||
const progressResult = findToolResult(timeline, progressCall, completeCall.index);
|
||||
if (!progressResult) {
|
||||
throw new Error("missing successful progress message result before completion");
|
||||
}
|
||||
const completeResult = findSuccessfulToolResult(timeline, completeCall);
|
||||
const completeResult = findToolResult(timeline, completeCall);
|
||||
if (!completeResult) {
|
||||
throw new Error("missing successful completion message result");
|
||||
}
|
||||
@@ -728,13 +718,13 @@ function assertFollowthroughTranscript({ transcriptEvents, progressMarker, compl
|
||||
entry.index < completeCall.index,
|
||||
);
|
||||
const successfulWorkCalls = workCalls.filter((entry) =>
|
||||
findSuccessfulToolResult(timeline, entry, completeCall.index),
|
||||
findToolResult(timeline, entry, completeCall.index),
|
||||
);
|
||||
if (successfulWorkCalls.length === 0) {
|
||||
throw new Error("expected successful workspace work between progress and completion");
|
||||
}
|
||||
const unsettledWorkCalls = workCalls.filter(
|
||||
(entry) => !findToolResult(timeline, entry, completeCall.index),
|
||||
(entry) => !findToolResult(timeline, entry, completeCall.index, false),
|
||||
);
|
||||
if (unsettledWorkCalls.length > 0) {
|
||||
throw new Error(
|
||||
|
||||
@@ -253,31 +253,23 @@ run_cross_state_approval_flow() {
|
||||
exec_source_hash="$(sha256sum "$exec_source" | awk '{print $1}')"
|
||||
plugin_source_hash="$(sha256sum "$plugin_source" | awk '{print $1}')"
|
||||
|
||||
run_logged_command "$automated_log" "$command_timeout" env \
|
||||
OPENCLAW_STATE_DIR="$custom_state_dir" \
|
||||
OPENCLAW_CONFIG_PATH="$custom_state_dir/openclaw.json" \
|
||||
OPENCLAW_UPDATE_IN_PROGRESS=1 \
|
||||
"$npm_bin" doctor --repair --yes --non-interactive
|
||||
local log
|
||||
local -a update_env=(OPENCLAW_UPDATE_IN_PROGRESS=1)
|
||||
for log in "$automated_log" "$direct_log"; do
|
||||
run_logged_command "$log" "$command_timeout" env \
|
||||
"${update_env[@]}" \
|
||||
OPENCLAW_STATE_DIR="$custom_state_dir" \
|
||||
OPENCLAW_CONFIG_PATH="$custom_state_dir/openclaw.json" \
|
||||
"$npm_bin" doctor --repair --yes --non-interactive
|
||||
|
||||
test "$(sha256sum "$exec_source" | awk '{print $1}')" = "$exec_source_hash"
|
||||
test "$(sha256sum "$plugin_source" | awk '{print $1}')" = "$plugin_source_hash"
|
||||
test ! -e "$exec_source.migrated"
|
||||
test ! -e "$plugin_source.migrated"
|
||||
test ! -e "$custom_state_dir/exec-approvals.json"
|
||||
test "$(plugin_binding_approval_count "$state_database")" = "0"
|
||||
|
||||
run_logged_command "$direct_log" "$command_timeout" env \
|
||||
-u OPENCLAW_UPDATE_IN_PROGRESS \
|
||||
OPENCLAW_STATE_DIR="$custom_state_dir" \
|
||||
OPENCLAW_CONFIG_PATH="$custom_state_dir/openclaw.json" \
|
||||
"$npm_bin" doctor --repair --yes --non-interactive
|
||||
|
||||
test "$(sha256sum "$exec_source" | awk '{print $1}')" = "$exec_source_hash"
|
||||
test "$(sha256sum "$plugin_source" | awk '{print $1}')" = "$plugin_source_hash"
|
||||
test ! -e "$exec_source.migrated"
|
||||
test ! -e "$plugin_source.migrated"
|
||||
test ! -e "$custom_state_dir/exec-approvals.json"
|
||||
test "$(plugin_binding_approval_count "$state_database")" = "0"
|
||||
test "$(sha256sum "$exec_source" | awk '{print $1}')" = "$exec_source_hash"
|
||||
test "$(sha256sum "$plugin_source" | awk '{print $1}')" = "$plugin_source_hash"
|
||||
test ! -e "$exec_source.migrated"
|
||||
test ! -e "$plugin_source.migrated"
|
||||
test ! -e "$custom_state_dir/exec-approvals.json"
|
||||
test "$(plugin_binding_approval_count "$state_database")" = "0"
|
||||
update_env=(-u OPENCLAW_UPDATE_IN_PROGRESS)
|
||||
done
|
||||
}
|
||||
|
||||
run_cross_state_approval_flow
|
||||
|
||||
@@ -87,6 +87,19 @@ function extractTranscriptToolCalls(message) {
|
||||
if (message.role !== "assistant") {
|
||||
return calls;
|
||||
}
|
||||
const appendCall = (call, functionRecord) => {
|
||||
const tool = readNonEmptyString(call.name) ?? readNonEmptyString(functionRecord?.name);
|
||||
if (tool) {
|
||||
calls.push({
|
||||
id:
|
||||
readNonEmptyString(call.id) ??
|
||||
readNonEmptyString(call.toolCallId) ??
|
||||
readNonEmptyString(call.toolUseId),
|
||||
tool,
|
||||
input: call.arguments ?? call.input ?? functionRecord?.arguments,
|
||||
});
|
||||
}
|
||||
};
|
||||
const content = message.content;
|
||||
if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
@@ -97,18 +110,7 @@ function extractTranscriptToolCalls(message) {
|
||||
if (type !== "tool_use" && type !== "toolcall" && type !== "tool_call") {
|
||||
continue;
|
||||
}
|
||||
const tool = readNonEmptyString(block.name);
|
||||
if (!tool) {
|
||||
continue;
|
||||
}
|
||||
calls.push({
|
||||
id:
|
||||
readNonEmptyString(block.id) ??
|
||||
readNonEmptyString(block.toolCallId) ??
|
||||
readNonEmptyString(block.toolUseId),
|
||||
tool,
|
||||
input: block.arguments ?? block.input,
|
||||
});
|
||||
appendCall(block);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,19 +121,7 @@ function extractTranscriptToolCalls(message) {
|
||||
if (!isRecord(call)) {
|
||||
continue;
|
||||
}
|
||||
const functionRecord = isRecord(call.function) ? call.function : undefined;
|
||||
const tool = readNonEmptyString(call.name) ?? readNonEmptyString(functionRecord?.name);
|
||||
if (!tool) {
|
||||
continue;
|
||||
}
|
||||
calls.push({
|
||||
id:
|
||||
readNonEmptyString(call.id) ??
|
||||
readNonEmptyString(call.toolCallId) ??
|
||||
readNonEmptyString(call.toolUseId),
|
||||
tool,
|
||||
input: call.arguments ?? call.input ?? functionRecord?.arguments,
|
||||
});
|
||||
appendCall(call, isRecord(call.function) ? call.function : undefined);
|
||||
}
|
||||
return calls;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
// Raw launchers meet the repo's Node 24.16.0 minimum, where native TS stripping is enabled.
|
||||
import { truncateUtf16Safe } from "../../../packages/normalization-core/src/utf16-slice.ts";
|
||||
import { readPositiveIntEnv } from "./env-limits.mjs";
|
||||
import { readBoundedRequestBody } from "./request-body.mjs";
|
||||
|
||||
const DEFAULT_REQUEST_MAX_BYTES = 4 * 1024 * 1024;
|
||||
const DEFAULT_REQUEST_LOG_BODY_MAX_BYTES = 256 * 1024;
|
||||
@@ -38,39 +39,7 @@ export function isRequestBodyTooLargeError(error) {
|
||||
* @param {{ requestLogBodyMaxBytes?: number; requestMaxBytes: number }} [limits]
|
||||
*/
|
||||
export function readBody(req, limits = readMockOpenAiHttpLimits()) {
|
||||
const { requestMaxBytes } = limits;
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = "";
|
||||
let bytes = 0;
|
||||
let settled = false;
|
||||
req.setEncoding("utf8");
|
||||
req.on("data", (chunk) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
bytes += Buffer.byteLength(chunk, "utf8");
|
||||
if (bytes > requestMaxBytes) {
|
||||
settled = true;
|
||||
body = "";
|
||||
req.resume();
|
||||
reject(requestBodyTooLargeError(requestMaxBytes));
|
||||
return;
|
||||
}
|
||||
body += chunk;
|
||||
});
|
||||
req.on("end", () => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
req.on("error", (error) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
});
|
||||
});
|
||||
return readBoundedRequestBody(req, limits.requestMaxBytes, requestBodyTooLargeError);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -188,8 +188,11 @@ async function nodes() {
|
||||
return (await cli(`nodes-${Date.now()}`, ["nodes", "status"])).nodes;
|
||||
}
|
||||
|
||||
async function nodeRow(name) {
|
||||
return (await nodes()).find((row) => row.displayName === name && row.connected && row.paired);
|
||||
async function nodeRow(name, version) {
|
||||
const row = (await nodes()).find(
|
||||
(entry) => entry.displayName === name && entry.connected && entry.paired,
|
||||
);
|
||||
return version === undefined || row?.version === version ? row : null;
|
||||
}
|
||||
|
||||
async function invoke(nodeId, action = "ping") {
|
||||
@@ -508,10 +511,7 @@ try {
|
||||
assert(JSON.stringify(completed).includes("NODE_UPDATE_HOLD_COMPLETED"));
|
||||
const updated = await waitFor(
|
||||
"automatic version activation",
|
||||
async () => {
|
||||
const row = await nodeRow(positive.name);
|
||||
return row?.version === versions[0] ? row : null;
|
||||
},
|
||||
() => nodeRow(positive.name, versions[0]),
|
||||
180_000,
|
||||
() => readNodeUpdateFailure(positive.logPath),
|
||||
);
|
||||
@@ -677,10 +677,7 @@ try {
|
||||
});
|
||||
const defaultUpdated = await waitFor(
|
||||
"default-plugin node activates while idle",
|
||||
async () => {
|
||||
const row = await nodeRow(defaultPlugins.name);
|
||||
return row?.version === versions[0] ? row : null;
|
||||
},
|
||||
() => nodeRow(defaultPlugins.name, versions[0]),
|
||||
900_000,
|
||||
() => readNodeUpdateFailure(defaultPlugins.logPath),
|
||||
);
|
||||
@@ -763,10 +760,7 @@ try {
|
||||
metadataReleased = true;
|
||||
const sharedUpdated = await waitFor(
|
||||
"shared-state node activates independently",
|
||||
async () => {
|
||||
const row = await nodeRow(sharedNode.name);
|
||||
return row?.version === versions[0] ? row : null;
|
||||
},
|
||||
() => nodeRow(sharedNode.name, versions[0]),
|
||||
900_000,
|
||||
() => readNodeUpdateFailure(sharedNode.logPath),
|
||||
);
|
||||
|
||||
@@ -215,6 +215,20 @@ try {
|
||||
await rpc("skills.upload.commit", { uploadId });
|
||||
return { source: "upload", agentId: "main", uploadId, slug, force };
|
||||
}
|
||||
async function waitForUploadPolicy(label, enabled) {
|
||||
await wait(label, async () => {
|
||||
try {
|
||||
await rpc("skills.upload.begin", {
|
||||
kind: "skill-archive",
|
||||
slug: enabled ? "admission" : slug,
|
||||
sizeBytes: 1,
|
||||
});
|
||||
return enabled;
|
||||
} catch (error) {
|
||||
return enabled ? false : error.message.includes("uploads are disabled");
|
||||
}
|
||||
});
|
||||
}
|
||||
await wait("paired workspace activation", async () => {
|
||||
try {
|
||||
await rpc("skills.status", { agentId: "main" });
|
||||
@@ -247,14 +261,7 @@ try {
|
||||
const pendingUpload = await stage("replacement", true);
|
||||
config.gateway.uploads.enabled = false;
|
||||
await setConfig();
|
||||
await wait("uploads disabled", async () => {
|
||||
try {
|
||||
await rpc("skills.upload.begin", { kind: "skill-archive", slug, sizeBytes: 1 });
|
||||
return false;
|
||||
} catch (e) {
|
||||
return e.message.includes("uploads are disabled");
|
||||
}
|
||||
});
|
||||
await waitForUploadPolicy("uploads disabled", false);
|
||||
await assert.rejects(rpc("skills.install", pendingUpload), /uploads are disabled/);
|
||||
assert.deepEqual(snapshot(), before);
|
||||
assert.deepEqual(metadata(), metadataBefore);
|
||||
@@ -265,14 +272,7 @@ try {
|
||||
});
|
||||
config.gateway.uploads.enabled = true;
|
||||
await setConfig();
|
||||
await wait("uploads reenabled", async () => {
|
||||
try {
|
||||
await rpc("skills.upload.begin", { kind: "skill-archive", slug: "admission", sizeBytes: 1 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
await waitForUploadPolicy("uploads reenabled", true);
|
||||
fs.writeFileSync(barrier + ".release", "");
|
||||
fs.writeFileSync(barrier + ".armed", "");
|
||||
const pending = rpc("skills.install", pendingUpload).then(
|
||||
@@ -291,14 +291,7 @@ try {
|
||||
record("paused-after-backup", { targetAbsent: true, backupCount: backups.length });
|
||||
config.gateway.uploads.enabled = false;
|
||||
await setConfig();
|
||||
await wait("revocation committed", async () => {
|
||||
try {
|
||||
await rpc("skills.upload.begin", { kind: "skill-archive", slug, sizeBytes: 1 });
|
||||
return false;
|
||||
} catch (e) {
|
||||
return e.message.includes("uploads are disabled");
|
||||
}
|
||||
});
|
||||
await waitForUploadPolicy("revocation committed", false);
|
||||
fs.writeFileSync(barrier + ".release", "release");
|
||||
const refused = await pending;
|
||||
assert(JSON.stringify(refused).includes("uploads are disabled"), JSON.stringify(refused));
|
||||
|
||||
@@ -64,26 +64,34 @@ function tableExists(db, tableName) {
|
||||
);
|
||||
}
|
||||
|
||||
function readCurrentSqlitePluginIndex(db) {
|
||||
if (!tableExists(db, "config_machine_state")) {
|
||||
return {};
|
||||
function readBoundedIndexRow(db, table, keyColumn, key, jsonColumns, columns) {
|
||||
if (!tableExists(db, table)) {
|
||||
return undefined;
|
||||
}
|
||||
const source = `FROM ${table} WHERE ${keyColumn} = ?`;
|
||||
const lengths = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT octet_length(value_json) AS value_json_bytes
|
||||
FROM config_machine_state
|
||||
WHERE state_key = ?
|
||||
`,
|
||||
`SELECT ${jsonColumns.map((column) => `octet_length(${column}) AS ${column}_bytes`).join(", ")} ${source}`,
|
||||
)
|
||||
.get(STATE_KEY);
|
||||
.get(key);
|
||||
if (!lengths) {
|
||||
return {};
|
||||
return undefined;
|
||||
}
|
||||
assertIndexJsonByteLength(lengths.value_json_bytes, "plugin index value_json");
|
||||
const row = db
|
||||
.prepare("SELECT value_json FROM config_machine_state WHERE state_key = ?")
|
||||
.get(STATE_KEY);
|
||||
for (const column of jsonColumns) {
|
||||
assertIndexJsonByteLength(lengths[`${column}_bytes`], `plugin index ${column}`);
|
||||
}
|
||||
return db.prepare(`SELECT ${columns} ${source}`).get(key);
|
||||
}
|
||||
|
||||
function readCurrentSqlitePluginIndex(db) {
|
||||
const row = readBoundedIndexRow(
|
||||
db,
|
||||
"config_machine_state",
|
||||
"state_key",
|
||||
STATE_KEY,
|
||||
["value_json"],
|
||||
"value_json",
|
||||
);
|
||||
if (!row) {
|
||||
return {};
|
||||
}
|
||||
@@ -92,40 +100,16 @@ function readCurrentSqlitePluginIndex(db) {
|
||||
}
|
||||
|
||||
function readPreV13SqlitePluginIndex(db) {
|
||||
if (!tableExists(db, "installed_plugin_index")) {
|
||||
return {};
|
||||
}
|
||||
const lengths = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT octet_length(install_records_json) AS install_records_json_bytes,
|
||||
octet_length(plugins_json) AS plugins_json_bytes,
|
||||
octet_length(diagnostics_json) AS diagnostics_json_bytes
|
||||
FROM installed_plugin_index
|
||||
WHERE index_key = ?
|
||||
`,
|
||||
)
|
||||
.get(PRE_V13_INDEX_KEY);
|
||||
if (!lengths) {
|
||||
return {};
|
||||
}
|
||||
assertIndexJsonByteLength(
|
||||
lengths.install_records_json_bytes,
|
||||
"plugin index install_records_json",
|
||||
const row = readBoundedIndexRow(
|
||||
db,
|
||||
"installed_plugin_index",
|
||||
"index_key",
|
||||
PRE_V13_INDEX_KEY,
|
||||
["install_records_json", "plugins_json", "diagnostics_json"],
|
||||
`version, warning, host_contract_version, compat_registry_version,
|
||||
migration_version, policy_hash, generated_at_ms, refresh_reason,
|
||||
install_records_json, plugins_json, diagnostics_json`,
|
||||
);
|
||||
assertIndexJsonByteLength(lengths.plugins_json_bytes, "plugin index plugins_json");
|
||||
assertIndexJsonByteLength(lengths.diagnostics_json_bytes, "plugin index diagnostics_json");
|
||||
const row = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT version, warning, host_contract_version, compat_registry_version,
|
||||
migration_version, policy_hash, generated_at_ms, refresh_reason,
|
||||
install_records_json, plugins_json, diagnostics_json
|
||||
FROM installed_plugin_index
|
||||
WHERE index_key = ?
|
||||
`,
|
||||
)
|
||||
.get(PRE_V13_INDEX_KEY);
|
||||
if (!row) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -587,7 +587,7 @@ function assertNpmPluginUpdateUnchanged() {
|
||||
assertNpmPlugin();
|
||||
}
|
||||
|
||||
function assertNpmPluginRemoved() {
|
||||
function assertNpmPluginUninstalled(keepFiles) {
|
||||
const installPath = fs.readFileSync(scratchFile("plugins-npm-install-path.txt"), "utf8").trim();
|
||||
const packageParent = path.dirname(installPath);
|
||||
const nodeModulesPath = path.basename(packageParent).startsWith("@")
|
||||
@@ -599,42 +599,25 @@ function assertNpmPluginRemoved() {
|
||||
.trim();
|
||||
assertPluginRemoved({
|
||||
pluginId: "demo-plugin-npm",
|
||||
listFile: scratchFile("plugins-npm-uninstalled.json"),
|
||||
listFile: scratchFile(`plugins-npm-${keepFiles ? "retained" : "uninstalled"}.json`),
|
||||
// Historical --keep-files retains a discoverable directory; current releases
|
||||
// instead persist an exact disabled marker.
|
||||
allowLegacyRetainedListing: keepFiles && pluginUninstallMode === "legacy",
|
||||
});
|
||||
if (fs.existsSync(installPath)) {
|
||||
throw new Error(`npm managed package still exists after uninstall: ${installPath}`);
|
||||
for (const [label, file] of [
|
||||
["package", installPath],
|
||||
["dependency", dependencyPackagePath],
|
||||
]) {
|
||||
if (fs.existsSync(file) !== keepFiles) {
|
||||
const outcome = keepFiles ? "was deleted by --keep-files" : "still exists after uninstall";
|
||||
throw new Error(`npm managed ${label} ${outcome}: ${file}`);
|
||||
}
|
||||
}
|
||||
if (fs.existsSync(dependencyPackagePath)) {
|
||||
throw new Error(
|
||||
`npm managed dependency still exists after uninstall: ${dependencyPackagePath}`,
|
||||
);
|
||||
}
|
||||
if (pluginUninstallMode !== "legacy" && fs.existsSync(projectRoot)) {
|
||||
if (!keepFiles && pluginUninstallMode !== "legacy" && fs.existsSync(projectRoot)) {
|
||||
throw new Error(`npm managed project still exists after uninstall: ${projectRoot}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNpmPluginRetained() {
|
||||
const installPath = fs.readFileSync(scratchFile("plugins-npm-install-path.txt"), "utf8").trim();
|
||||
const dependencyPackagePath = fs
|
||||
.readFileSync(scratchFile("plugins-npm-dependency-path.txt"), "utf8")
|
||||
.trim();
|
||||
assertPluginRemoved({
|
||||
pluginId: "demo-plugin-npm",
|
||||
listFile: scratchFile("plugins-npm-retained.json"),
|
||||
// Historical --keep-files removed config ownership but retained a
|
||||
// discoverable plugin directory. Its list entry is expected until the
|
||||
// subsequent reinstall; current releases persist an exact disabled marker.
|
||||
allowLegacyRetainedListing: pluginUninstallMode === "legacy",
|
||||
});
|
||||
if (!fs.existsSync(installPath)) {
|
||||
throw new Error(`npm managed package was deleted by --keep-files: ${installPath}`);
|
||||
}
|
||||
if (!fs.existsSync(dependencyPackagePath)) {
|
||||
throw new Error(`npm managed dependency was deleted by --keep-files: ${dependencyPackagePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNpmPluginReinstalled() {
|
||||
if (pluginUninstallMode === "legacy") {
|
||||
return;
|
||||
@@ -878,9 +861,9 @@ const commands = {
|
||||
"plugin-file-removed": () => assertLocalPluginRemoved(localPluginScenarios.file),
|
||||
"plugin-npm": assertNpmPlugin,
|
||||
"plugin-npm-update": assertNpmPluginUpdateUnchanged,
|
||||
"plugin-npm-retained": assertNpmPluginRetained,
|
||||
"plugin-npm-retained": () => assertNpmPluginUninstalled(true),
|
||||
"plugin-npm-reinstalled": assertNpmPluginReinstalled,
|
||||
"plugin-npm-removed": assertNpmPluginRemoved,
|
||||
"plugin-npm-removed": () => assertNpmPluginUninstalled(false),
|
||||
"invalid-openclaw-extensions": assertInvalidOpenClawExtensionsRejected,
|
||||
"bundle-disabled": assertClaudeBundleDisabled,
|
||||
"bundle-inspect": assertClaudeBundleInspect,
|
||||
|
||||
@@ -17,21 +17,11 @@ run_plugins_clawhub_scenario() {
|
||||
echo "$server_pid" >"$server_pid_file"
|
||||
openclaw_plugins_register_fixture_pid_file "$server_pid_file"
|
||||
|
||||
for _ in $(seq 1 100); do
|
||||
if [[ -s "$server_port_file" ]]; then
|
||||
export OPENCLAW_CLAWHUB_URL="http://127.0.0.1:$(cat "$server_port_file")"
|
||||
return 0
|
||||
fi
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
openclaw_plugins_print_fixture_log "$server_log"
|
||||
return 1
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
openclaw_plugins_print_fixture_log "$server_log"
|
||||
echo "Timed out waiting for ClawHub fixture server." >&2
|
||||
return 1
|
||||
openclaw_plugins_wait_fixture_port "$server_pid" "$server_port_file" "$server_log" "ClawHub fixture server"
|
||||
local readiness_status=$?
|
||||
[ "$readiness_status" -eq 0 ] || return "$readiness_status"
|
||||
export OPENCLAW_CLAWHUB_URL="http://127.0.0.1:$(cat "$server_port_file")"
|
||||
return 0
|
||||
}
|
||||
|
||||
local clawhub_default_plugin_spec="clawhub:@openclaw/plugin-e2e-fixture"
|
||||
|
||||
@@ -141,41 +141,20 @@ openclaw_plugins_fixture_exit_trap() {
|
||||
exit "$status"
|
||||
}
|
||||
|
||||
pack_fixture_plugin_with_cli_registry_dependency() {
|
||||
local pack_dir="$1"
|
||||
local output_tgz="$2"
|
||||
local id="$3"
|
||||
local version="$4"
|
||||
local method="$5"
|
||||
local name="$6"
|
||||
local cli_root="$7"
|
||||
local cli_output="$8"
|
||||
pack_fixture_archive() {
|
||||
local fixture="$1"
|
||||
local pack_dir="$2"
|
||||
local output_tgz="$3"
|
||||
shift 3
|
||||
|
||||
mkdir -p "$pack_dir/package"
|
||||
node scripts/e2e/lib/fixture.mjs plugin-cli-registry-dep "$pack_dir/package" "$id" "$version" "$method" "$name" "$cli_root" "$cli_output"
|
||||
tar -czf "$output_tgz" -C "$pack_dir" package
|
||||
}
|
||||
|
||||
pack_fake_is_number_package() {
|
||||
local pack_dir="$1"
|
||||
local output_tgz="$2"
|
||||
|
||||
mkdir -p "$pack_dir/package"
|
||||
node scripts/e2e/lib/fixture.mjs fake-is-number-package "$pack_dir/package"
|
||||
node scripts/e2e/lib/fixture.mjs "$fixture" "$pack_dir/package" "$@"
|
||||
tar -czf "$output_tgz" -C "$pack_dir" package
|
||||
}
|
||||
|
||||
# Frozen corrupt-update scenarios are mounted onto the current harness.
|
||||
pack_fixture_plugin() {
|
||||
local pack_dir="$1"
|
||||
local output_tgz="$2"
|
||||
local id="$3"
|
||||
local version="$4"
|
||||
local method="$5"
|
||||
local name="$6"
|
||||
|
||||
mkdir -p "$pack_dir/package"
|
||||
node scripts/e2e/lib/fixture.mjs plugin "$pack_dir/package" "$id" "$version" "$method" "$name"
|
||||
tar -czf "$output_tgz" -C "$pack_dir" package
|
||||
pack_fixture_archive plugin "$1" "$2" "$3" "$4" "$5" "$6"
|
||||
}
|
||||
|
||||
pack_fixture_plugin_with_invalid_extension_entry() {
|
||||
@@ -199,6 +178,24 @@ NODE
|
||||
tar -czf "$output_tgz" -C "$pack_dir" package
|
||||
}
|
||||
|
||||
openclaw_plugins_wait_fixture_port() {
|
||||
local server_pid="$1" server_port_file="$2" server_log="$3" label="$4"
|
||||
for _ in $(seq 1 100); do
|
||||
if [[ -s "$server_port_file" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
openclaw_plugins_print_fixture_log "$server_log"
|
||||
return 1
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
openclaw_plugins_print_fixture_log "$server_log"
|
||||
echo "Timed out waiting for $label." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
start_npm_fixture_registry() {
|
||||
local package_name="$1"
|
||||
local version="$2"
|
||||
@@ -217,21 +214,11 @@ start_npm_fixture_registry() {
|
||||
echo "$server_pid" >"$server_pid_file"
|
||||
openclaw_plugins_register_fixture_pid_file "$server_pid_file"
|
||||
|
||||
for _ in $(seq 1 100); do
|
||||
if [[ -s "$server_port_file" ]]; then
|
||||
export NPM_CONFIG_REGISTRY="http://127.0.0.1:$(cat "$server_port_file")"
|
||||
# Override both spellings so an inherited prerelease registry cannot win in npm.
|
||||
export npm_config_registry="$NPM_CONFIG_REGISTRY"
|
||||
return 0
|
||||
fi
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
openclaw_plugins_print_fixture_log "$server_log"
|
||||
return 1
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
openclaw_plugins_print_fixture_log "$server_log"
|
||||
echo "Timed out waiting for npm fixture registry." >&2
|
||||
return 1
|
||||
openclaw_plugins_wait_fixture_port "$server_pid" "$server_port_file" "$server_log" "npm fixture registry"
|
||||
local readiness_status=$?
|
||||
[ "$readiness_status" -eq 0 ] || return "$readiness_status"
|
||||
export NPM_CONFIG_REGISTRY="http://127.0.0.1:$(cat "$server_port_file")"
|
||||
# Override both spellings so an inherited prerelease registry cannot win in npm.
|
||||
export npm_config_registry="$NPM_CONFIG_REGISTRY"
|
||||
return 0
|
||||
}
|
||||
@@ -134,7 +134,7 @@ node scripts/e2e/lib/plugins/assertions.mjs demo-plugin
|
||||
|
||||
echo "Testing tgz install flow..."
|
||||
pack_dir="$(mktemp -d "$OPENCLAW_PLUGINS_TMP_DIR/openclaw-plugin-pack.XXXXXX")"
|
||||
pack_fixture_plugin "$pack_dir" "$OPENCLAW_PLUGINS_TMP_DIR/demo-plugin-tgz.tgz" demo-plugin-tgz 0.0.1 demo.tgz "Demo Plugin TGZ"
|
||||
pack_fixture_archive plugin "$pack_dir" "$OPENCLAW_PLUGINS_TMP_DIR/demo-plugin-tgz.tgz" demo-plugin-tgz 0.0.1 demo.tgz "Demo Plugin TGZ"
|
||||
|
||||
run_plugins_fixture_logged install-tgz plugins install "$OPENCLAW_PLUGINS_TMP_DIR/demo-plugin-tgz.tgz" --force
|
||||
run_plugins_openclaw_capture "$OPENCLAW_PLUGINS_TMP_DIR/plugins2.json" plugins list --json
|
||||
@@ -196,8 +196,8 @@ npm_pack_dir="$(mktemp -d "$OPENCLAW_PLUGINS_TMP_DIR/openclaw-plugin-npm-pack.XX
|
||||
npm_dep_pack_dir="$(mktemp -d "$OPENCLAW_PLUGINS_TMP_DIR/openclaw-plugin-npm-dep-pack.XXXXXX")"
|
||||
invalid_npm_pack_dir="$(mktemp -d "$OPENCLAW_PLUGINS_TMP_DIR/openclaw-plugin-invalid-metadata-pack.XXXXXX")"
|
||||
npm_registry_dir="$(mktemp -d "$OPENCLAW_PLUGINS_TMP_DIR/openclaw-plugin-npm-registry.XXXXXX")"
|
||||
pack_fixture_plugin_with_cli_registry_dependency "$npm_pack_dir" "$OPENCLAW_PLUGINS_TMP_DIR/demo-plugin-npm.tgz" demo-plugin-npm 0.0.1 demo.npm "Demo Plugin NPM" demo-npm "demo-plugin-npm:pong"
|
||||
pack_fake_is_number_package "$npm_dep_pack_dir" "$OPENCLAW_PLUGINS_TMP_DIR/is-number-7.0.0.tgz"
|
||||
pack_fixture_archive plugin-cli-registry-dep "$npm_pack_dir" "$OPENCLAW_PLUGINS_TMP_DIR/demo-plugin-npm.tgz" demo-plugin-npm 0.0.1 demo.npm "Demo Plugin NPM" demo-npm "demo-plugin-npm:pong"
|
||||
pack_fixture_archive fake-is-number-package "$npm_dep_pack_dir" "$OPENCLAW_PLUGINS_TMP_DIR/is-number-7.0.0.tgz"
|
||||
pack_fixture_plugin_with_invalid_extension_entry "$invalid_npm_pack_dir" "$OPENCLAW_PLUGINS_TMP_DIR/demo-plugin-invalid-metadata.tgz" demo-plugin-invalid-metadata 0.0.1 demo.invalid.metadata "Demo Plugin Invalid Metadata"
|
||||
start_npm_fixture_registry "@openclaw/demo-plugin-npm" "0.0.1" "$OPENCLAW_PLUGINS_TMP_DIR/demo-plugin-npm.tgz" "$npm_registry_dir" "is-number" "7.0.0" "$OPENCLAW_PLUGINS_TMP_DIR/is-number-7.0.0.tgz" "@openclaw/demo-plugin-invalid-metadata" "0.0.1" "$OPENCLAW_PLUGINS_TMP_DIR/demo-plugin-invalid-metadata.tgz"
|
||||
|
||||
|
||||
@@ -148,14 +148,13 @@ openclaw_prepublish_plugin_registry_run_mounted() (
|
||||
local registry_root registry_pid="" command_pid=""
|
||||
registry_root="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-prepublish-registry.XXXXXX")"
|
||||
cleanup_registry_command() {
|
||||
if [ -n "$command_pid" ]; then
|
||||
kill "$command_pid" >/dev/null 2>&1 || true
|
||||
wait "$command_pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$registry_pid" ]; then
|
||||
kill "$registry_pid" >/dev/null 2>&1 || true
|
||||
wait "$registry_pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
local pid
|
||||
for pid in "$command_pid" "$registry_pid"; do
|
||||
if [ -n "$pid" ]; then
|
||||
kill "$pid" >/dev/null 2>&1 || true
|
||||
wait "$pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
rm -rf "$registry_root"
|
||||
}
|
||||
trap cleanup_registry_command EXIT
|
||||
|
||||
@@ -56,17 +56,6 @@ dump_debug_logs() {
|
||||
}
|
||||
openclaw_e2e_enable_failure_diagnostics
|
||||
|
||||
start_gateway() {
|
||||
local log_path="$1"
|
||||
gateway_pid="$(openclaw_e2e_start_gateway "$entry" "$PORT" "$log_path")"
|
||||
openclaw_e2e_wait_gateway_ready "$gateway_pid" "$log_path" 300 "$PORT"
|
||||
}
|
||||
|
||||
stop_gateway() {
|
||||
openclaw_e2e_terminate_gateways "${gateway_pid:-}"
|
||||
gateway_pid=""
|
||||
}
|
||||
|
||||
openclaw_e2e_install_package "$INSTALL_LOG"
|
||||
command -v openclaw >/dev/null
|
||||
package_root="$(openclaw_e2e_package_root)"
|
||||
@@ -89,19 +78,7 @@ openclaw_e2e_enable_openclaw_cli_timeout
|
||||
mock_pid="$(openclaw_e2e_start_mock_openai "$MOCK_PORT" "$MOCK_OPENAI_LOG")"
|
||||
openclaw_e2e_wait_mock_openai "$MOCK_PORT"
|
||||
|
||||
openclaw onboard \
|
||||
--non-interactive \
|
||||
--accept-risk \
|
||||
--flow quickstart \
|
||||
--mode local \
|
||||
--auth-choice skip \
|
||||
--gateway-port "$PORT" \
|
||||
--gateway-bind loopback \
|
||||
--skip-daemon \
|
||||
--skip-ui \
|
||||
--skip-channels \
|
||||
--skip-skills \
|
||||
--skip-health >"$ONBOARD_LOG" 2>&1
|
||||
openclaw_release_onboard "$PORT" openclaw >"$ONBOARD_LOG" 2>&1
|
||||
cp "$OPENCLAW_CONFIG_PATH" "$CONFIG_JSON"
|
||||
openclaw plugins list --json >"$PLUGINS_JSON" 2>"$PLUGINS_STDERR_LOG"
|
||||
node scripts/e2e/lib/release-scenarios/assertions.mjs assert-file-contains "$PLUGINS_JSON" memory-core
|
||||
|
||||
@@ -30,17 +30,7 @@ openclaw_e2e_install_package /tmp/openclaw-release-plugin-marketplace-install.lo
|
||||
command -v openclaw >/dev/null
|
||||
openclaw_e2e_enable_openclaw_cli_timeout
|
||||
|
||||
openclaw onboard \
|
||||
--non-interactive \
|
||||
--accept-risk \
|
||||
--flow quickstart \
|
||||
--mode local \
|
||||
--auth-choice skip \
|
||||
--skip-daemon \
|
||||
--skip-ui \
|
||||
--skip-channels \
|
||||
--skip-skills \
|
||||
--skip-health >/tmp/openclaw-release-plugin-marketplace-onboard.log 2>&1
|
||||
openclaw_release_onboard "" openclaw >/tmp/openclaw-release-plugin-marketplace-onboard.log 2>&1
|
||||
|
||||
marketplace_root="$HOME/.claude/plugins/marketplaces/release-fixture-marketplace"
|
||||
marketplace_assertions="scripts/e2e/lib/release-plugin-marketplace/lifecycle-assertions.mjs"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
openclaw_release_prepare_docker() {
|
||||
local lane="$1"
|
||||
run_log=""
|
||||
trap 'docker_e2e_cleanup_package_run "${PACKAGE_TGZ:-}" "${run_log:-}"' EXIT
|
||||
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz "$lane" "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")"
|
||||
docker_e2e_package_mount_args "$PACKAGE_TGZ"
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" "$lane" "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD"
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 "$lane" empty)"
|
||||
run_log="$(docker_e2e_run_log "$lane")"
|
||||
}
|
||||
|
||||
openclaw_release_run_docker() {
|
||||
if ! docker_e2e_run_with_harness "$@" >"$run_log" 2>&1; then
|
||||
docker_e2e_print_log "$run_log"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -18,3 +18,50 @@ openclaw_release_scenario_logs() {
|
||||
shift 2
|
||||
done
|
||||
}
|
||||
|
||||
openclaw_release_onboard() {
|
||||
local port="$1"
|
||||
shift
|
||||
set -- "$@" onboard \
|
||||
--non-interactive \
|
||||
--accept-risk \
|
||||
--flow quickstart \
|
||||
--mode local \
|
||||
--auth-choice skip
|
||||
if [ -n "$port" ]; then
|
||||
set -- "$@" --gateway-port "$port" --gateway-bind loopback
|
||||
fi
|
||||
"$@" \
|
||||
--skip-daemon \
|
||||
--skip-ui \
|
||||
--skip-channels \
|
||||
--skip-skills \
|
||||
--skip-health
|
||||
}
|
||||
|
||||
start_gateway() {
|
||||
local log_path="$1"
|
||||
gateway_pid="$(openclaw_e2e_start_gateway "$entry" "$PORT" "$log_path")"
|
||||
openclaw_e2e_wait_gateway_ready "$gateway_pid" "$log_path" 300 "$PORT"
|
||||
}
|
||||
|
||||
stop_gateway() {
|
||||
openclaw_e2e_terminate_gateways "${gateway_pid:-}"
|
||||
gateway_pid=""
|
||||
}
|
||||
|
||||
start_clickclack_fixture() {
|
||||
local token="$1"
|
||||
CLICKCLACK_FIXTURE_PORT="$CLICKCLACK_PORT" \
|
||||
CLICKCLACK_FIXTURE_TOKEN="$token" \
|
||||
CLICKCLACK_FIXTURE_STATE="$CLICKCLACK_STATE" \
|
||||
node scripts/e2e/lib/release-user-journey/clickclack-fixture.mjs >"$CLICKCLACK_SERVER_LOG" 2>&1 &
|
||||
clickclack_pid="$!"
|
||||
for _ in $(seq 1 100); do
|
||||
if openclaw_e2e_probe_http_status "http://127.0.0.1:$CLICKCLACK_PORT/health" 200 >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
openclaw_e2e_probe_http_status "http://127.0.0.1:$CLICKCLACK_PORT/health" 200
|
||||
}
|
||||
@@ -100,12 +100,6 @@ fs.writeFileSync(process.argv[2], JSON.stringify({
|
||||
NODE
|
||||
}
|
||||
|
||||
start_gateway() {
|
||||
local log_path="$1"
|
||||
gateway_pid="$(openclaw_e2e_start_gateway "$entry" "$PORT" "$log_path")"
|
||||
openclaw_e2e_wait_gateway_ready "$gateway_pid" "$log_path" 300 "$PORT"
|
||||
}
|
||||
|
||||
echo "Installing published baseline $BASELINE_SPEC..."
|
||||
if ! openclaw_e2e_maybe_timeout "${OPENCLAW_E2E_NPM_INSTALL_TIMEOUT:-600s}" npm install -g "$BASELINE_SPEC" --no-fund --no-audit >"$BASELINE_INSTALL_LOG" 2>&1; then
|
||||
cat "$BASELINE_INSTALL_LOG" >&2 || true
|
||||
@@ -120,32 +114,9 @@ openclaw_e2e_enable_openclaw_cli_timeout
|
||||
mock_pid="$(openclaw_e2e_start_mock_openai "$MOCK_PORT" "$OPENAI_LOG")"
|
||||
openclaw_e2e_wait_mock_openai "$MOCK_PORT"
|
||||
|
||||
CLICKCLACK_FIXTURE_PORT="$CLICKCLACK_PORT" \
|
||||
CLICKCLACK_FIXTURE_TOKEN="$CLICKCLACK_TEST_TOKEN" \
|
||||
CLICKCLACK_FIXTURE_STATE="$CLICKCLACK_STATE" \
|
||||
node scripts/e2e/lib/release-user-journey/clickclack-fixture.mjs >"$CLICKCLACK_SERVER_LOG" 2>&1 &
|
||||
clickclack_pid="$!"
|
||||
for _ in $(seq 1 100); do
|
||||
if openclaw_e2e_probe_http_status "http://127.0.0.1:$CLICKCLACK_PORT/health" 200 >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
openclaw_e2e_probe_http_status "http://127.0.0.1:$CLICKCLACK_PORT/health" 200
|
||||
start_clickclack_fixture "$CLICKCLACK_TEST_TOKEN"
|
||||
|
||||
openclaw_e2e_run_command node "$baseline_entry" onboard \
|
||||
--non-interactive \
|
||||
--accept-risk \
|
||||
--flow quickstart \
|
||||
--mode local \
|
||||
--auth-choice skip \
|
||||
--gateway-port "$PORT" \
|
||||
--gateway-bind loopback \
|
||||
--skip-daemon \
|
||||
--skip-ui \
|
||||
--skip-channels \
|
||||
--skip-skills \
|
||||
--skip-health >"$ONBOARD_LOG" 2>&1
|
||||
openclaw_release_onboard "$PORT" openclaw_e2e_run_command node "$baseline_entry" >"$ONBOARD_LOG" 2>&1
|
||||
record_baseline_setup onboard
|
||||
|
||||
plugin_dir="$(mktemp -d "$scenario_tmp/plugin.XXXXXX")"
|
||||
|
||||
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import http from "node:http";
|
||||
import { readPositiveIntEnv, readTcpPortEnv } from "../env-limits.mjs";
|
||||
import { readBoundedRequestBody } from "../request-body.mjs";
|
||||
|
||||
const port = readTcpPortEnv("CLICKCLACK_FIXTURE_PORT", 44181);
|
||||
const requestMaxBytes = readPositiveIntEnv("CLICKCLACK_FIXTURE_REQUEST_MAX_BYTES", 4 * 1024 * 1024);
|
||||
@@ -82,49 +83,17 @@ function checkAuth(req, res) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = "";
|
||||
let bytes = 0;
|
||||
let settled = false;
|
||||
req.setEncoding("utf8");
|
||||
req.on("data", (chunk) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
bytes += Buffer.byteLength(chunk, "utf8");
|
||||
if (bytes > requestMaxBytes) {
|
||||
settled = true;
|
||||
body = "";
|
||||
req.resume();
|
||||
reject(
|
||||
Object.assign(
|
||||
new Error(`ClickClack fixture request body exceeded ${requestMaxBytes} bytes`),
|
||||
{ code: "ETOOBIG" },
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
body += chunk;
|
||||
});
|
||||
req.on("end", () => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
try {
|
||||
resolve(body ? JSON.parse(body) : {});
|
||||
} catch {
|
||||
resolve({});
|
||||
}
|
||||
});
|
||||
req.on("error", (error) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
});
|
||||
});
|
||||
async function readBody(req) {
|
||||
const body = await readBoundedRequestBody(req, requestMaxBytes, (limit) =>
|
||||
Object.assign(new Error(`ClickClack fixture request body exceeded ${limit} bytes`), {
|
||||
code: "ETOOBIG",
|
||||
}),
|
||||
);
|
||||
try {
|
||||
return body ? JSON.parse(body) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function handleRequestError(res, error) {
|
||||
|
||||
@@ -65,17 +65,6 @@ dump_debug_logs() {
|
||||
}
|
||||
openclaw_e2e_enable_failure_diagnostics
|
||||
|
||||
start_gateway() {
|
||||
local log_path="$1"
|
||||
gateway_pid="$(openclaw_e2e_start_gateway "$entry" "$PORT" "$log_path")"
|
||||
openclaw_e2e_wait_gateway_ready "$gateway_pid" "$log_path" 300 "$PORT"
|
||||
}
|
||||
|
||||
stop_gateway() {
|
||||
openclaw_e2e_terminate_gateways "${gateway_pid:-}"
|
||||
gateway_pid=""
|
||||
}
|
||||
|
||||
write_journey_plugin() {
|
||||
node --input-type=module - "$@" <<'NODE'
|
||||
import { writeCliPlugin } from "./scripts/e2e/lib/fixtures/plugins.mjs";
|
||||
@@ -92,33 +81,10 @@ openclaw_e2e_enable_openclaw_cli_timeout
|
||||
mock_pid="$(openclaw_e2e_start_mock_openai "$MOCK_PORT" "$OPENAI_LOG")"
|
||||
openclaw_e2e_wait_mock_openai "$MOCK_PORT"
|
||||
|
||||
CLICKCLACK_FIXTURE_PORT="$CLICKCLACK_PORT" \
|
||||
CLICKCLACK_FIXTURE_TOKEN="$CLICKCLACK_BOT_TOKEN" \
|
||||
CLICKCLACK_FIXTURE_STATE="$CLICKCLACK_STATE" \
|
||||
node scripts/e2e/lib/release-user-journey/clickclack-fixture.mjs >"$CLICKCLACK_SERVER_LOG" 2>&1 &
|
||||
clickclack_pid="$!"
|
||||
for _ in $(seq 1 100); do
|
||||
if openclaw_e2e_probe_http_status "http://127.0.0.1:$CLICKCLACK_PORT/health" 200 >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
openclaw_e2e_probe_http_status "http://127.0.0.1:$CLICKCLACK_PORT/health" 200
|
||||
start_clickclack_fixture "$CLICKCLACK_BOT_TOKEN"
|
||||
|
||||
echo "Running non-interactive onboarding..."
|
||||
openclaw onboard \
|
||||
--non-interactive \
|
||||
--accept-risk \
|
||||
--flow quickstart \
|
||||
--mode local \
|
||||
--auth-choice skip \
|
||||
--gateway-port "$PORT" \
|
||||
--gateway-bind loopback \
|
||||
--skip-daemon \
|
||||
--skip-ui \
|
||||
--skip-channels \
|
||||
--skip-skills \
|
||||
--skip-health >"$ONBOARD_LOG" 2>&1
|
||||
openclaw_release_onboard "$PORT" openclaw >"$ONBOARD_LOG" 2>&1
|
||||
node scripts/e2e/lib/release-user-journey/assertions.mjs assert-onboard "$HOME"
|
||||
node scripts/e2e/lib/release-user-journey/assertions.mjs configure-mock-model "$MOCK_PORT"
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @param {NodeJS.ReadableStream} request
|
||||
* @param {number} maxBytes
|
||||
* @param {(limit: number) => Error} tooLargeError
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export function readBoundedRequestBody(request, maxBytes, tooLargeError) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = "";
|
||||
let bytes = 0;
|
||||
let settled = false;
|
||||
request.setEncoding("utf8");
|
||||
request.on("data", (chunk) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
bytes += Buffer.byteLength(chunk, "utf8");
|
||||
if (bytes > maxBytes) {
|
||||
settled = true;
|
||||
body = "";
|
||||
request.resume();
|
||||
reject(tooLargeError(maxBytes));
|
||||
return;
|
||||
}
|
||||
body += chunk;
|
||||
});
|
||||
request.on("end", () => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
request.on("error", (error) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -130,6 +130,14 @@ export async function countSessionLogMentions(params: {
|
||||
}): Promise<Record<string, number>> {
|
||||
const limits = params.limits ?? readSessionLogMentionLimits();
|
||||
const counts = Object.fromEntries(Object.keys(params.needles).map((key) => [key, 0]));
|
||||
const countRecord = (text: string) => {
|
||||
const scanText = sessionLogScanText(text);
|
||||
if (scanText !== null) {
|
||||
for (const [key, needle] of Object.entries(params.needles)) {
|
||||
counts[key] = (counts[key] ?? 0) + countOccurrences(scanText, needle);
|
||||
}
|
||||
}
|
||||
};
|
||||
let files: string[];
|
||||
try {
|
||||
files = await fs.readdir(params.sessionsDir);
|
||||
@@ -166,13 +174,7 @@ export async function countSessionLogMentions(params: {
|
||||
limit: limits.fileMaxBytes,
|
||||
});
|
||||
for (const line of raw.split(/\r?\n/u)) {
|
||||
const scanText = sessionLogScanText(line);
|
||||
if (scanText === null) {
|
||||
continue;
|
||||
}
|
||||
for (const [key, needle] of Object.entries(params.needles)) {
|
||||
counts[key] = (counts[key] ?? 0) + countOccurrences(scanText, needle);
|
||||
}
|
||||
countRecord(line);
|
||||
}
|
||||
}
|
||||
if (path.basename(params.sessionsDir) !== "sessions") {
|
||||
@@ -210,13 +212,7 @@ export async function countSessionLogMentions(params: {
|
||||
label: "total",
|
||||
limit: limits.totalMaxBytes,
|
||||
});
|
||||
const scanText = sessionLogScanText(eventJson);
|
||||
if (scanText === null) {
|
||||
continue;
|
||||
}
|
||||
for (const [key, needle] of Object.entries(params.needles)) {
|
||||
counts[key] = (counts[key] ?? 0) + countOccurrences(scanText, needle);
|
||||
}
|
||||
countRecord(eventJson);
|
||||
}
|
||||
return counts;
|
||||
} catch (error) {
|
||||
|
||||
@@ -443,22 +443,14 @@ function main() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(mode === "first-hop-tarball" ||
|
||||
mode === "negative-tarball" ||
|
||||
mode === "future-tarball" ||
|
||||
mode === "unsupported-admission-tarball" ||
|
||||
mode === "future-runtime-tarball") &&
|
||||
packageRoot &&
|
||||
outputTarball
|
||||
) {
|
||||
const pack = {
|
||||
"first-hop-tarball": packFirstHopUpdateFixture,
|
||||
"negative-tarball": packNegativeUpdateFixture,
|
||||
"future-tarball": packFutureUpdateFixture,
|
||||
"unsupported-admission-tarball": packUnsupportedAdmissionFixture,
|
||||
"future-runtime-tarball": packFutureRuntimeFixture,
|
||||
}[mode];
|
||||
const packers = {
|
||||
"first-hop-tarball": packFirstHopUpdateFixture,
|
||||
"negative-tarball": packNegativeUpdateFixture,
|
||||
"future-tarball": packFutureUpdateFixture,
|
||||
"unsupported-admission-tarball": packUnsupportedAdmissionFixture,
|
||||
"future-runtime-tarball": packFutureRuntimeFixture,
|
||||
};
|
||||
if (Object.hasOwn(packers, mode) && packageRoot && outputTarball) {
|
||||
const fixtureArg =
|
||||
mode === "negative-tarball"
|
||||
? (sequence ?? "")
|
||||
@@ -466,7 +458,7 @@ function main() {
|
||||
? 0
|
||||
: Number(sequence);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(pack(packageRoot, outputTarball, fixtureArg), null, 2)}\n`,
|
||||
`${JSON.stringify(packers[mode](packageRoot, outputTarball, fixtureArg), null, 2)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import {
|
||||
readSqliteTranscriptPayload,
|
||||
sqliteTranscriptPayloadColumns,
|
||||
@@ -135,13 +134,16 @@ function isPathInsideManagedNpmProjectPackageRoot(params) {
|
||||
function seedLegacySessionMetadata(stateDir) {
|
||||
const legacySessionsDir = path.join(stateDir, "agents", "main", "sessions");
|
||||
const baseUpdatedAt = Date.now() - 24 * 60 * 60 * 1000;
|
||||
writeJson(path.join(legacySessionsDir, "sessions.json"), {
|
||||
const entry = (sessionId, offset) => ({
|
||||
sessionId,
|
||||
sessionFile: path.join(legacySessionsDir, `${sessionId}.jsonl`),
|
||||
modelProvider: "openai",
|
||||
model: "gpt-5.5",
|
||||
updatedAt: baseUpdatedAt + offset,
|
||||
});
|
||||
const sessions = {
|
||||
"agent:main:main": {
|
||||
sessionId: LEGACY_SESSION_MAIN_ID,
|
||||
sessionFile: path.join(legacySessionsDir, `${LEGACY_SESSION_MAIN_ID}.jsonl`),
|
||||
modelProvider: "openai",
|
||||
model: "gpt-5.5",
|
||||
updatedAt: baseUpdatedAt,
|
||||
...entry(LEGACY_SESSION_MAIN_ID, 0),
|
||||
skillsSnapshot: {
|
||||
prompt: "legacy prompt survives as metadata",
|
||||
resolvedSkills: [
|
||||
@@ -152,33 +154,17 @@ function seedLegacySessionMetadata(stateDir) {
|
||||
],
|
||||
},
|
||||
},
|
||||
"agent:main:+15551234567": {
|
||||
sessionId: LEGACY_SESSION_DIRECT_ID,
|
||||
sessionFile: path.join(legacySessionsDir, `${LEGACY_SESSION_DIRECT_ID}.jsonl`),
|
||||
modelProvider: "openai",
|
||||
model: "gpt-5.5",
|
||||
updatedAt: baseUpdatedAt + 100,
|
||||
},
|
||||
"agent:main:+15551234567": entry(LEGACY_SESSION_DIRECT_ID, 100),
|
||||
"agent:main:slack:channel:cupgrade": {
|
||||
sessionId: LEGACY_SESSION_GROUP_ID,
|
||||
sessionFile: path.join(legacySessionsDir, `${LEGACY_SESSION_GROUP_ID}.jsonl`),
|
||||
modelProvider: "openai",
|
||||
model: "gpt-5.5",
|
||||
updatedAt: baseUpdatedAt + 200,
|
||||
...entry(LEGACY_SESSION_GROUP_ID, 200),
|
||||
lastChannel: "slack",
|
||||
lastTo: "CUPGRADE",
|
||||
...(getScenario() === "acpx-openclaw-tools-bridge" ? { acp: LEGACY_ACP_META } : {}),
|
||||
},
|
||||
});
|
||||
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`,
|
||||
);
|
||||
};
|
||||
writeJson(path.join(legacySessionsDir, "sessions.json"), sessions);
|
||||
for (const { sessionId, sessionFile } of Object.values(sessions)) {
|
||||
write(sessionFile, `${JSON.stringify({ type: "session", id: sessionId })}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,12 +686,9 @@ function assertLegacyOperatorPendingDelivery([updateJson, updateErr]) {
|
||||
"published update stopped on legacy pending-delivery state",
|
||||
);
|
||||
}
|
||||
const db = new DatabaseSync(path.join(stateDir, "agents/ops/agent/openclaw-agent.sqlite"), {
|
||||
readOnly: true,
|
||||
});
|
||||
let liveRow;
|
||||
let recoveredInputs = [];
|
||||
try {
|
||||
readDatabase(path.join(stateDir, "agents/ops/agent/openclaw-agent.sqlite"), (db) => {
|
||||
liveRow = readLegacyOperatorPendingDelivery(db);
|
||||
if (
|
||||
liveRow &&
|
||||
@@ -718,9 +701,7 @@ function assertLegacyOperatorPendingDelivery([updateJson, updateErr]) {
|
||||
.all("legacy-pending-delivery")
|
||||
.map((event) => JSON.parse(readSqliteTranscriptPayload(event)).message);
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
let row = liveRow;
|
||||
if (process.env.OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE === "auto-auth") {
|
||||
const witness = readJson(
|
||||
@@ -1232,9 +1213,7 @@ function assertSessionMetadataMigrated(stateDir, stage) {
|
||||
function readMigratedSessionStore(stateDir, targetStorePath) {
|
||||
const dbPath = path.join(stateDir, "agents", "main", "agent", "openclaw-agent.sqlite");
|
||||
if (fs.existsSync(dbPath)) {
|
||||
let db;
|
||||
try {
|
||||
db = new DatabaseSync(dbPath, { readOnly: true });
|
||||
const snapshot = readDatabase(dbPath, (db) => {
|
||||
const tables = new Set(
|
||||
db
|
||||
.prepare(
|
||||
@@ -1248,32 +1227,21 @@ function readMigratedSessionStore(stateDir, targetStorePath) {
|
||||
);
|
||||
// SQLite is authoritative once it owns a supported session table. A stale
|
||||
// sessions.json must not hide missing, malformed, or unreadable database state.
|
||||
const source = tables.has("session_nodes")
|
||||
? "session_nodes"
|
||||
: tables.has("session_entries")
|
||||
? "session_entries"
|
||||
: tables.has("cache_entries")
|
||||
? "cache_entries"
|
||||
: null;
|
||||
const source = ["session_nodes", "session_entries", "cache_entries"].find((table) =>
|
||||
tables.has(table),
|
||||
);
|
||||
if (source) {
|
||||
const rows =
|
||||
const query =
|
||||
source === "session_nodes"
|
||||
? db
|
||||
.prepare(
|
||||
`SELECT session_key AS key, current_session_id AS session_id, entry_json AS value_json
|
||||
FROM session_nodes`,
|
||||
)
|
||||
.all()
|
||||
? `SELECT session_key AS key, current_session_id AS session_id, entry_json AS value_json
|
||||
FROM session_nodes`
|
||||
: source === "session_entries"
|
||||
? db
|
||||
.prepare(
|
||||
`SELECT session_key AS key, session_id, entry_json AS value_json
|
||||
FROM session_entries`,
|
||||
)
|
||||
.all()
|
||||
: db
|
||||
.prepare("SELECT key, value_json FROM cache_entries WHERE scope = ?")
|
||||
.all("session_entries");
|
||||
? `SELECT session_key AS key, session_id, entry_json AS value_json
|
||||
FROM session_entries`
|
||||
: "SELECT key, value_json FROM cache_entries WHERE scope = ?";
|
||||
const rows = db
|
||||
.prepare(query)
|
||||
.all(...(source === "cache_entries" ? ["session_entries"] : []));
|
||||
const store = {};
|
||||
for (const row of rows) {
|
||||
if (typeof row?.key !== "string" || typeof row?.value_json !== "string") {
|
||||
@@ -1304,8 +1272,10 @@ function readMigratedSessionStore(stateDir, targetStorePath) {
|
||||
}
|
||||
return { source, store };
|
||||
}
|
||||
} finally {
|
||||
db?.close();
|
||||
return undefined;
|
||||
});
|
||||
if (snapshot) {
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
sqliteTranscriptPayloadColumns,
|
||||
transcriptIdentity,
|
||||
} from "../../../lib/sqlite-transcript-payload.mjs";
|
||||
import { hashFile as hashObservedFile } from "./fixture-files.mjs";
|
||||
import { readDatabase } from "./observations.mjs";
|
||||
|
||||
const RESTORED_TRANSCRIPT = "agents/main/sessions/upgrade-restored-index-history.jsonl";
|
||||
@@ -20,20 +21,7 @@ const quoteIdentifier = (value) => `"${value.replaceAll('"', '""')}"`;
|
||||
const compareSessionKeys = (left, right) =>
|
||||
left.key < right.key ? -1 : left.key > right.key ? 1 : 0;
|
||||
|
||||
function hashFile(file) {
|
||||
const hash = createHash("sha256");
|
||||
const descriptor = fs.openSync(file, "r");
|
||||
try {
|
||||
const buffer = Buffer.alloc(1024 * 1024);
|
||||
let size;
|
||||
while ((size = fs.readSync(descriptor, buffer, 0, buffer.length, null)) > 0) {
|
||||
hash.update(buffer.subarray(0, size));
|
||||
}
|
||||
return hash.digest("hex");
|
||||
} finally {
|
||||
fs.closeSync(descriptor);
|
||||
}
|
||||
}
|
||||
const hashFile = (file) => hashObservedFile(file, 1024 * 1024);
|
||||
|
||||
function containedPath(root, relative) {
|
||||
assert(typeof relative === "string" && relative.length > 0, "missing inventory path");
|
||||
|
||||
@@ -389,21 +389,15 @@ export function resolveUpgradeSurvivorOpenClawCommand(
|
||||
params: UpgradeSurvivorCommandParams = {},
|
||||
) {
|
||||
const platform = params.platform ?? process.platform;
|
||||
if (platform === "win32") {
|
||||
const comSpec = params.comSpec ?? resolveWindowsCmdExePath(params.env ?? process.env);
|
||||
return {
|
||||
command: comSpec,
|
||||
args: ["/d", "/s", "/c", buildCmdExeCommandLine("openclaw.cmd", argv)],
|
||||
commandLabel: ["openclaw", ...argv].join(" "),
|
||||
shell: false,
|
||||
windowsVerbatimArguments: true,
|
||||
};
|
||||
}
|
||||
const windows = platform === "win32";
|
||||
return {
|
||||
command: "openclaw",
|
||||
args: argv,
|
||||
command: windows
|
||||
? (params.comSpec ?? resolveWindowsCmdExePath(params.env ?? process.env))
|
||||
: "openclaw",
|
||||
args: windows ? ["/d", "/s", "/c", buildCmdExeCommandLine("openclaw.cmd", argv)] : argv,
|
||||
commandLabel: ["openclaw", ...argv].join(" "),
|
||||
shell: false,
|
||||
...(windows ? { windowsVerbatimArguments: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,12 @@ import { setTimeout as delay } from "node:timers/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { isMainThread } from "node:worker_threads";
|
||||
import { readPositiveIntEnv } from "../env-limits.mjs";
|
||||
import { inspectCronBackups, readDatabase } from "./observations.mjs";
|
||||
import {
|
||||
findInstalledPackageRoot,
|
||||
inspectCronBackups,
|
||||
readDatabase,
|
||||
recordProcessExitSnapshot,
|
||||
} from "./observations.mjs";
|
||||
|
||||
const fixtureName = "cron-owner-fixture.json";
|
||||
const prefix = "owner-proof-";
|
||||
@@ -94,22 +99,21 @@ function snapshot(fixture) {
|
||||
|
||||
function installedProcessIdentity(entrypoint) {
|
||||
const file = fs.realpathSync(entrypoint);
|
||||
for (let root = path.dirname(file), depth = 0; depth < 4; root = path.dirname(root), depth++) {
|
||||
const root = findInstalledPackageRoot(path.dirname(file), 4);
|
||||
if (root) {
|
||||
const manifestPath = path.join(root, "package.json");
|
||||
if (fs.existsSync(manifestPath) && readJson(manifestPath).name === "openclaw") {
|
||||
const manifest = fs.readFileSync(manifestPath);
|
||||
const build = fs.readFileSync(path.join(root, "dist/build-info.json"));
|
||||
const info = JSON.parse(build);
|
||||
assert.equal(info.version, JSON.parse(manifest).version);
|
||||
return {
|
||||
version: info.version,
|
||||
commit: info.commit,
|
||||
manifestSha256: hash(manifest),
|
||||
buildInfoSha256: hash(build),
|
||||
entrypoint: path.relative(root, file),
|
||||
entrypointSha256: hash(fs.readFileSync(file)),
|
||||
};
|
||||
}
|
||||
const manifest = fs.readFileSync(manifestPath);
|
||||
const build = fs.readFileSync(path.join(root, "dist/build-info.json"));
|
||||
const info = JSON.parse(build);
|
||||
assert.equal(info.version, JSON.parse(manifest).version);
|
||||
return {
|
||||
version: info.version,
|
||||
commit: info.commit,
|
||||
manifestSha256: hash(manifest),
|
||||
buildInfoSha256: hash(build),
|
||||
entrypoint: path.relative(root, file),
|
||||
entrypointSha256: hash(fs.readFileSync(file)),
|
||||
};
|
||||
}
|
||||
throw new Error("Observed CLI is outside an OpenClaw package");
|
||||
}
|
||||
@@ -145,15 +149,7 @@ function observeProcess() {
|
||||
receipt.observationError = String(error);
|
||||
}
|
||||
const file = path.join(artifacts, `cron-owner-${role}-${process.pid}.json`);
|
||||
writeJson(file, receipt);
|
||||
process.once("exit", (exitCode) => {
|
||||
try {
|
||||
receipt.after = snapshot(fixture);
|
||||
} catch (error) {
|
||||
receipt.observationError = String(error);
|
||||
}
|
||||
writeJson(file, { ...receipt, exitCode });
|
||||
});
|
||||
recordProcessExitSnapshot(file, receipt, () => snapshot(fixture));
|
||||
}
|
||||
|
||||
function gateway(artifacts, name, method, params) {
|
||||
@@ -667,19 +663,14 @@ function assertRuntime(p) {
|
||||
assert.deepEqual(
|
||||
receipts.map((row) => Object.assign({}, row)),
|
||||
[
|
||||
{
|
||||
job_id: `${prefix}explicit`,
|
||||
agent_id: "research",
|
||||
status: "ok",
|
||||
request_run_id: runs.find((run) => run.jobId === `${prefix}explicit`).runId,
|
||||
},
|
||||
{
|
||||
job_id: `${prefix}historical`,
|
||||
agent_id: "ops",
|
||||
status: "ok",
|
||||
request_run_id: runs.find((run) => run.jobId === `${prefix}historical`).runId,
|
||||
},
|
||||
],
|
||||
["explicit", "research"],
|
||||
["historical", "ops"],
|
||||
].map(([name, agentId]) => ({
|
||||
job_id: `${prefix}${name}`,
|
||||
agent_id: agentId,
|
||||
status: "ok",
|
||||
request_run_id: runs.find((run) => run.jobId === `${prefix}${name}`).runId,
|
||||
})),
|
||||
);
|
||||
const proofPath = path.join(p.artifacts, "cron-owner-proof.json");
|
||||
const proof = {
|
||||
|
||||
@@ -646,6 +646,22 @@ function migrationProjection(section, value, sanitize = (text) => text) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
function projectMigrationSections(read, sanitize) {
|
||||
return Object.fromEntries(
|
||||
["doctor", "sessions", "archives", "sibling"].map((section) => {
|
||||
try {
|
||||
return [
|
||||
section,
|
||||
{ availability: "captured", ...migrationProjection(section, read(section), sanitize) },
|
||||
];
|
||||
} catch {
|
||||
omissions[`migration-${section}`] ??= reasons[3];
|
||||
return [section, { availability: "unavailable" }];
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// A native read-only open can create missing WAL sidecars or require journal recovery.
|
||||
// This bootstrap observer never copies operator databases or imports a migrating runtime reader.
|
||||
function assertNativeSqliteObservationSafe(handles, label) {
|
||||
@@ -847,16 +863,7 @@ function captureMigrationEvidence(stateRoot, artifactRoot, observationRoot) {
|
||||
};
|
||||
},
|
||||
};
|
||||
return Object.fromEntries(
|
||||
Object.entries(sources).map(([section, read]) => {
|
||||
try {
|
||||
return [section, { availability: "captured", ...migrationProjection(section, read()) }];
|
||||
} catch {
|
||||
omissions[`migration-${section}`] ??= reasons[3];
|
||||
return [section, { availability: "unavailable" }];
|
||||
}
|
||||
}),
|
||||
);
|
||||
return projectMigrationSections((section) => sources[section]());
|
||||
}
|
||||
|
||||
function sessionMigrationProjection(raw, kind, runId, sanitize = (text) => text) {
|
||||
@@ -1136,13 +1143,17 @@ function armPostCoreCapture() {
|
||||
}
|
||||
}
|
||||
|
||||
async function pluginIdentities(stateRoot, artifactRoot) {
|
||||
const unavailable = {
|
||||
function unknownPluginIdentity() {
|
||||
return {
|
||||
availability: "unknown",
|
||||
evidence: "persisted index + current bytes; not observed loaded modules",
|
||||
reader: "SQLite or historical fallback; missing/error is not absence",
|
||||
plugins: [],
|
||||
};
|
||||
}
|
||||
|
||||
async function pluginIdentities(stateRoot, artifactRoot) {
|
||||
const unavailable = unknownPluginIdentity();
|
||||
const handles = [];
|
||||
try {
|
||||
// The existing reader opens SQLite read-only. Fence every file it may read;
|
||||
@@ -2074,12 +2085,7 @@ export function publishDiagnostics(
|
||||
} catch {
|
||||
// Do not promote a partial or unbound receipt into a reported Doctor outcome.
|
||||
}
|
||||
report.pluginIdentity = {
|
||||
availability: "unknown",
|
||||
evidence: "persisted index + current bytes; not observed loaded modules",
|
||||
reader: "SQLite or historical fallback; missing/error is not absence",
|
||||
plugins: [],
|
||||
};
|
||||
report.pluginIdentity = unknownPluginIdentity();
|
||||
if (snapshot.pluginIdentity?.availability === "observed") {
|
||||
try {
|
||||
const plugins = boundedList(snapshot.pluginIdentity.plugins).map((entry) => {
|
||||
@@ -2120,23 +2126,13 @@ export function publishDiagnostics(
|
||||
omissions["plugin identity"] = reasons[3];
|
||||
}
|
||||
}
|
||||
report.migration = Object.fromEntries(
|
||||
["doctor", "sessions", "archives", "sibling"].map((section) => {
|
||||
try {
|
||||
const value = snapshot.migration?.[section];
|
||||
if (value?.availability !== "captured") {
|
||||
throw new Error();
|
||||
}
|
||||
return [
|
||||
section,
|
||||
{ availability: "captured", ...migrationProjection(section, value, sanitize) },
|
||||
];
|
||||
} catch {
|
||||
omissions[`migration-${section}`] ??= reasons[3];
|
||||
return [section, { availability: "unavailable" }];
|
||||
}
|
||||
}),
|
||||
);
|
||||
report.migration = projectMigrationSections((section) => {
|
||||
const value = snapshot.migration?.[section];
|
||||
if (value?.availability !== "captured") {
|
||||
throw new Error();
|
||||
}
|
||||
return value;
|
||||
}, sanitize);
|
||||
writeReport(artifactRoot, destination, "failure.json", report, publicLimit);
|
||||
if (Object.keys(omissions).length) {
|
||||
process.stderr.write(
|
||||
|
||||
@@ -8,7 +8,12 @@ import { setTimeout as delay } from "node:timers/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { isMainThread } from "node:worker_threads";
|
||||
import { readPositiveIntEnv } from "../env-limits.mjs";
|
||||
import { inspectCronBackups, readDatabase } from "./observations.mjs";
|
||||
import {
|
||||
findInstalledPackageRoot,
|
||||
inspectCronBackups,
|
||||
readDatabase,
|
||||
recordProcessExitSnapshot,
|
||||
} from "./observations.mjs";
|
||||
import {
|
||||
assertWorkerCellPackageIdentity,
|
||||
readWorkerCellPackageIdentity,
|
||||
@@ -386,31 +391,17 @@ function observeProcess() {
|
||||
fixture.databasePath,
|
||||
path.join(process.env.OPENCLAW_STATE_DIR, "state/openclaw.sqlite"),
|
||||
);
|
||||
let root = path.dirname(fs.realpathSync(process.argv[1]));
|
||||
for (let depth = 0; depth < 3; depth++, root = path.dirname(root)) {
|
||||
if (
|
||||
fs.existsSync(path.join(root, "package.json")) &&
|
||||
readJson(path.join(root, "package.json")).name === "openclaw"
|
||||
) {
|
||||
receipt.identity = installedIdentity(root);
|
||||
receipt.entrypoint = path.relative(root, fs.realpathSync(process.argv[1]));
|
||||
break;
|
||||
}
|
||||
const root = findInstalledPackageRoot(path.dirname(fs.realpathSync(process.argv[1])), 3);
|
||||
if (root) {
|
||||
receipt.identity = installedIdentity(root);
|
||||
receipt.entrypoint = path.relative(root, fs.realpathSync(process.argv[1]));
|
||||
}
|
||||
receipt.before = snapshot(fixture);
|
||||
} catch (error) {
|
||||
receipt.observationError = String(error);
|
||||
}
|
||||
const file = path.join(observations, `dreaming-cron-${role}-${process.pid}.json`);
|
||||
writeJson(file, receipt);
|
||||
process.once("exit", (exitCode) => {
|
||||
try {
|
||||
receipt.after = snapshot(fixture);
|
||||
} catch (error) {
|
||||
receipt.observationError = String(error);
|
||||
}
|
||||
writeJson(file, { ...receipt, exitCode });
|
||||
});
|
||||
recordProcessExitSnapshot(file, receipt, () => snapshot(fixture));
|
||||
}
|
||||
|
||||
function assertUpdated(artifacts, observations, packageRoot, candidateTarball) {
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export function hashFile(filename, chunkBytes) {
|
||||
const hash = createHash("sha256");
|
||||
const descriptor = fs.openSync(filename, "r");
|
||||
try {
|
||||
const buffer = Buffer.alloc(chunkBytes);
|
||||
let size;
|
||||
while ((size = fs.readSync(descriptor, buffer, 0, buffer.length, null)) > 0) {
|
||||
hash.update(buffer.subarray(0, size));
|
||||
}
|
||||
return hash.digest("hex");
|
||||
} finally {
|
||||
fs.closeSync(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
export function childOf(root, file) {
|
||||
const relative = path.relative(root, file);
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,11 @@ import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { isMainThread } from "node:worker_threads";
|
||||
import { readDatabase } from "./observations.mjs";
|
||||
import {
|
||||
findInstalledPackageRoot,
|
||||
readDatabase,
|
||||
recordProcessExitSnapshot,
|
||||
} from "./observations.mjs";
|
||||
|
||||
const MIGRATION = "state:cron-run-logs-to-task-runs:v1";
|
||||
const FIXTURE_NAME = "legacy-operator-cron-history.json";
|
||||
@@ -193,15 +197,9 @@ function observeUpdateProcess() {
|
||||
fixture.databasePath,
|
||||
path.join(process.env.OPENCLAW_STATE_DIR, "state/openclaw.sqlite"),
|
||||
);
|
||||
let root = path.dirname(fs.realpathSync(process.argv[1]));
|
||||
for (let depth = 0; depth < 3; depth++, root = path.dirname(root)) {
|
||||
if (
|
||||
fs.existsSync(path.join(root, "package.json")) &&
|
||||
readJson(path.join(root, "package.json")).name === "openclaw"
|
||||
) {
|
||||
receipt.identity = installedIdentity(root);
|
||||
break;
|
||||
}
|
||||
const root = findInstalledPackageRoot(path.dirname(fs.realpathSync(process.argv[1])), 3);
|
||||
if (root) {
|
||||
receipt.identity = installedIdentity(root);
|
||||
}
|
||||
receipt.updateInProgress = process.env.OPENCLAW_UPDATE_IN_PROGRESS === "1";
|
||||
receipt.before = snapshot(fixture);
|
||||
@@ -209,15 +207,7 @@ function observeUpdateProcess() {
|
||||
receipt.observationError = String(error);
|
||||
}
|
||||
const file = path.join(observations, `cron-history-${command}-${process.pid}.json`);
|
||||
writeJson(file, receipt);
|
||||
process.once("exit", (exitCode) => {
|
||||
try {
|
||||
receipt.after = snapshot(fixture);
|
||||
} catch (error) {
|
||||
receipt.observationError = String(error);
|
||||
}
|
||||
writeJson(file, { ...receipt, exitCode });
|
||||
});
|
||||
recordProcessExitSnapshot(file, receipt, () => snapshot(fixture));
|
||||
}
|
||||
|
||||
function assertProcessReceipt(observations, witness, role) {
|
||||
|
||||
@@ -885,16 +885,18 @@ async function verifyReconnect(params: {
|
||||
}): Promise<void> {
|
||||
const WebSocket = loadWebSocket(params.packageRoot);
|
||||
await assertMissingPassword({ WebSocket, credentials: params.credentials });
|
||||
const node = await connect({
|
||||
WebSocket,
|
||||
url: params.credentials.url,
|
||||
client: params.credentials.client,
|
||||
mode: "node",
|
||||
role: "node",
|
||||
scopes: params.credentials.node.scopes,
|
||||
auth: { token: params.credentials.node.token },
|
||||
identity: params.credentials.identity,
|
||||
});
|
||||
const connectRole = (role: ConnectRole) =>
|
||||
connect({
|
||||
WebSocket,
|
||||
url: params.credentials.url,
|
||||
client: params.credentials.client,
|
||||
mode: role === "node" ? "node" : "ui",
|
||||
role,
|
||||
scopes: params.credentials[role].scopes,
|
||||
auth: { token: params.credentials[role].token },
|
||||
identity: params.credentials.identity,
|
||||
});
|
||||
const node = await connectRole("node");
|
||||
let operator: ConnectResult | undefined;
|
||||
try {
|
||||
const nodeTransition = persistHelloCredential({
|
||||
@@ -903,16 +905,7 @@ async function verifyReconnect(params: {
|
||||
hello: node.hello,
|
||||
});
|
||||
writePrivateJson(params.credentialsFile, params.credentials);
|
||||
operator = await connect({
|
||||
WebSocket,
|
||||
url: params.credentials.url,
|
||||
client: params.credentials.client,
|
||||
mode: "ui",
|
||||
role: "operator",
|
||||
scopes: params.credentials.operator.scopes,
|
||||
auth: { token: params.credentials.operator.token },
|
||||
identity: params.credentials.identity,
|
||||
});
|
||||
operator = await connectRole("operator");
|
||||
const operatorTransition = persistHelloCredential({
|
||||
credentials: params.credentials,
|
||||
role: "operator",
|
||||
|
||||
@@ -146,7 +146,7 @@ function startThread(params, requestedId) {
|
||||
injectedItemsByThread.set(id, []);
|
||||
threadConfigurations.set(id, { params: structuredClone(params), response });
|
||||
loaded.add(id);
|
||||
return { ...threadConfigurations.get(id).response, thread: thread(id) };
|
||||
return response;
|
||||
}
|
||||
|
||||
function resumeChangesConfiguration(params, configured) {
|
||||
@@ -259,17 +259,16 @@ function completeTurn(socket, phase, threadId, turn, text, status = "completed")
|
||||
notify(socket, phase, "thread/status/changed", { threadId, status: { type: "idle" } });
|
||||
}
|
||||
|
||||
function collabItem(socket, phase, threadId, turn, item, method) {
|
||||
if (method === "item/completed") {
|
||||
turn.items.push(item);
|
||||
}
|
||||
notify(socket, phase, method, { threadId, turnId: turn.id, item });
|
||||
}
|
||||
|
||||
function runPhase(socket, phase, threadId, turn, input) {
|
||||
if (turn.status !== "inProgress") {
|
||||
return;
|
||||
}
|
||||
const collabItem = (item, method = "item/completed") => {
|
||||
if (method === "item/completed") {
|
||||
turn.items.push(item);
|
||||
}
|
||||
notify(socket, phase, method, { threadId, turnId: turn.id, item });
|
||||
};
|
||||
if (
|
||||
phase === "seed-assignments" &&
|
||||
threadId === parentId &&
|
||||
@@ -287,23 +286,16 @@ function runPhase(socket, phase, threadId, turn, input) {
|
||||
]) {
|
||||
const child = startThread({ cwd: thread(parentId).cwd }, id).thread;
|
||||
notify(socket, phase, "thread/started", { thread: child });
|
||||
collabItem(
|
||||
socket,
|
||||
phase,
|
||||
threadId,
|
||||
turn,
|
||||
{
|
||||
id: `spawn-${id}`,
|
||||
type: "collabAgentToolCall",
|
||||
tool: "spawnAgent",
|
||||
status: "completed",
|
||||
senderThreadId: parentId,
|
||||
receiverThreadIds: [id],
|
||||
prompt: "Synthetic native upgrade assignment",
|
||||
agentsStates: { [id]: { status: "running", message: null } },
|
||||
},
|
||||
"item/completed",
|
||||
);
|
||||
collabItem({
|
||||
id: `spawn-${id}`,
|
||||
type: "collabAgentToolCall",
|
||||
tool: "spawnAgent",
|
||||
status: "completed",
|
||||
senderThreadId: parentId,
|
||||
receiverThreadIds: [id],
|
||||
prompt: "Synthetic native upgrade assignment",
|
||||
agentsStates: { [id]: { status: "running", message: null } },
|
||||
});
|
||||
const childTurn = startTurn(socket, phase, id, turnId);
|
||||
if (id === completeId) {
|
||||
completeTurn(socket, phase, id, childTurn, "NATIVE_UPGRADE_PENDING_RESULT");
|
||||
@@ -332,19 +324,12 @@ function runPhase(socket, phase, threadId, turn, input) {
|
||||
agentsStates: {},
|
||||
};
|
||||
closeTurns.set(socket, { phase, threadId, turn });
|
||||
collabItem(socket, phase, threadId, turn, item, "item/started");
|
||||
collabItem(
|
||||
socket,
|
||||
phase,
|
||||
threadId,
|
||||
turn,
|
||||
{
|
||||
...item,
|
||||
status: "completed",
|
||||
agentsStates: { [runningId]: { status: "running", message: null } },
|
||||
},
|
||||
"item/completed",
|
||||
);
|
||||
collabItem(item, "item/started");
|
||||
collabItem({
|
||||
...item,
|
||||
status: "completed",
|
||||
agentsStates: { [runningId]: { status: "running", message: null } },
|
||||
});
|
||||
return;
|
||||
}
|
||||
completeTurn(
|
||||
|
||||
@@ -26,3 +26,31 @@ export function inspectCronBackups(databasePath) {
|
||||
.digest("hex"),
|
||||
}));
|
||||
}
|
||||
|
||||
export function findInstalledPackageRoot(startDirectory, maxDepth) {
|
||||
let directory = startDirectory;
|
||||
for (let depth = 0; depth < maxDepth; depth++, directory = path.dirname(directory)) {
|
||||
const manifest = path.join(directory, "package.json");
|
||||
if (
|
||||
fs.existsSync(manifest) &&
|
||||
JSON.parse(fs.readFileSync(manifest, "utf8")).name === "openclaw"
|
||||
) {
|
||||
return directory;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function recordProcessExitSnapshot(file, receipt, after) {
|
||||
const write = (value) =>
|
||||
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
||||
write(receipt);
|
||||
process.once("exit", (exitCode) => {
|
||||
try {
|
||||
receipt.after = after();
|
||||
} catch (error) {
|
||||
receipt.observationError = String(error);
|
||||
}
|
||||
write({ ...receipt, exitCode });
|
||||
});
|
||||
}
|
||||
@@ -14,14 +14,13 @@ function installPackageActivationFault({ journal, evidence, cut, terminate }) {
|
||||
// oxlint-disable-next-line typescript/unbound-method -- Capture for interception/restoration; every call supplies the intercepted database through .call.
|
||||
const exec = DatabaseSync.prototype.exec;
|
||||
const armed = new WeakSet();
|
||||
const matched = (row) => row?.phase === "publication-complete";
|
||||
const observeCommit = (db) => {
|
||||
if (!armed.has(db) || db.location() !== journal) {
|
||||
return;
|
||||
}
|
||||
armed.delete(db);
|
||||
const row = prepare.call(db, "SELECT * FROM package_activation WHERE slot = 1").get();
|
||||
if (!matched(row) || fs.existsSync(evidence)) {
|
||||
if (row?.phase !== "publication-complete" || fs.existsSync(evidence)) {
|
||||
return;
|
||||
}
|
||||
fs.writeFileSync(evidence, JSON.stringify({ cut, pid: process.pid, journal, row }));
|
||||
@@ -55,10 +54,6 @@ function installPackageActivationFault({ journal, evidence, cut, terminate }) {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return () => {
|
||||
DatabaseSync.prototype.prepare = prepare;
|
||||
DatabaseSync.prototype.exec = exec;
|
||||
};
|
||||
}
|
||||
|
||||
const specPath = process.env.OPENCLAW_SURVIVOR_PACKAGE_FAULT;
|
||||
|
||||
@@ -3,8 +3,8 @@ import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { readDatabase } from "./observations.mjs";
|
||||
|
||||
const digest = (file) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
||||
const identity = (file) => {
|
||||
@@ -191,13 +191,9 @@ function main([command, artifacts, ...args]) {
|
||||
if (fault.cut === "verification") {
|
||||
assert.equal(read(`${fault.evidence}.doctor`).exitCode, 1);
|
||||
}
|
||||
const db = new DatabaseSync(fault.journal, { readOnly: true });
|
||||
let row;
|
||||
try {
|
||||
row = db.prepare("SELECT * FROM package_activation WHERE slot = 1").get();
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
const row = readDatabase(fault.journal, (db) =>
|
||||
db.prepare("SELECT * FROM package_activation WHERE slot = 1").get(),
|
||||
);
|
||||
assert.equal(
|
||||
row.phase,
|
||||
"publication-complete",
|
||||
@@ -252,17 +248,14 @@ function main([command, artifacts, ...args]) {
|
||||
);
|
||||
assertCandidate();
|
||||
const row = read(fault.evidence).row;
|
||||
const db = new DatabaseSync(fault.journal, { readOnly: true });
|
||||
try {
|
||||
readDatabase(fault.journal, (db) => {
|
||||
const actual = db.prepare("SELECT * FROM package_activation WHERE slot = 1").get();
|
||||
assert.deepEqual(
|
||||
{ ...actual },
|
||||
row,
|
||||
"the rejected old updater must not alter its recovery obligation",
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
value.status = "passed";
|
||||
value.targetSelector = "unmodified-released-9.8";
|
||||
value.firstHop = {
|
||||
|
||||
@@ -264,6 +264,21 @@ process.exitCode = await runCancelableCommand(async (signal) => {
|
||||
|
||||
const port = await freePort();
|
||||
const token = "published-driver-synthetic-token";
|
||||
const probe = async (name, version) => {
|
||||
await run(name, "openclaw", [
|
||||
"gateway",
|
||||
"probe",
|
||||
"--url",
|
||||
`ws://127.0.0.1:${port}`,
|
||||
"--token",
|
||||
token,
|
||||
"--json",
|
||||
]);
|
||||
const target = output(name).targets.find((entry) => entry.url === `ws://127.0.0.1:${port}`);
|
||||
assert.equal(target?.connect.ok, true);
|
||||
assert.equal(target.server.version, version);
|
||||
return target;
|
||||
};
|
||||
const config = {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
@@ -321,20 +336,7 @@ process.exitCode = await runCancelableCommand(async (signal) => {
|
||||
await run("install-service", "openclaw", ["gateway", "install", "--force", "--json"]);
|
||||
await ready("before-ready", port);
|
||||
if (legacySqlite) {
|
||||
await run("running-before", "openclaw", [
|
||||
"gateway",
|
||||
"probe",
|
||||
"--url",
|
||||
`ws://127.0.0.1:${port}`,
|
||||
"--token",
|
||||
token,
|
||||
"--json",
|
||||
]);
|
||||
const serving = output("running-before").targets.find(
|
||||
(entry) => entry.url === `ws://127.0.0.1:${port}`,
|
||||
);
|
||||
assert.equal(serving?.connect.ok, true);
|
||||
assert.equal(serving.server.version, driverVersion);
|
||||
const serving = await probe("running-before", driverVersion);
|
||||
const buildComparable =
|
||||
typeof serving.server.buildId === "string" && typeof driverBuild.buildId === "string";
|
||||
if (buildComparable) {
|
||||
@@ -470,20 +472,7 @@ process.exitCode = await runCancelableCommand(async (signal) => {
|
||||
"Update did not replace the managed service",
|
||||
);
|
||||
await ready("after-ready", port);
|
||||
await run("running-version", "openclaw", [
|
||||
"gateway",
|
||||
"probe",
|
||||
"--url",
|
||||
`ws://127.0.0.1:${port}`,
|
||||
"--token",
|
||||
token,
|
||||
"--json",
|
||||
]);
|
||||
const target = output("running-version").targets.find(
|
||||
(entry) => entry.url === `ws://127.0.0.1:${port}`,
|
||||
);
|
||||
assert.equal(target?.connect.ok, true);
|
||||
assert.equal(target.server.version, build.version);
|
||||
const target = await probe("running-version", build.version);
|
||||
for (const session of sessions) {
|
||||
const history = await gateway(`${session.kind}-after`, "chat.history", {
|
||||
...session.params,
|
||||
|
||||
@@ -1,35 +1,21 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
readJson as readRecoveryJson,
|
||||
writeJson as writeRecoveryJson,
|
||||
} from "../fixtures/common.mjs";
|
||||
import { hashFile } from "./fixture-files.mjs";
|
||||
|
||||
export function recoveryFileIdentity(file) {
|
||||
const stat = fs.lstatSync(file, { bigint: true });
|
||||
assert(stat.isFile(), `expected regular recovery file: ${file}`);
|
||||
const digest = createHash("sha256");
|
||||
const buffer = Buffer.alloc(64 * 1024);
|
||||
const fd = fs.openSync(file, "r");
|
||||
try {
|
||||
for (;;) {
|
||||
const bytes = fs.readSync(fd, buffer, 0, buffer.length, null);
|
||||
if (!bytes) {
|
||||
break;
|
||||
}
|
||||
digest.update(buffer.subarray(0, bytes));
|
||||
}
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
return {
|
||||
dev: String(stat.dev),
|
||||
ino: String(stat.ino),
|
||||
size: Number(stat.size),
|
||||
mtimeNs: String(stat.mtimeNs),
|
||||
sha256: digest.digest("hex"),
|
||||
sha256: hashFile(file, 64 * 1024),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1422,14 +1422,7 @@ candidate_update_spec() {
|
||||
printf '%s\n' "$CANDIDATE_SPEC"
|
||||
return 0
|
||||
fi
|
||||
case "$CANDIDATE_SPEC" in
|
||||
file:*)
|
||||
printf '%s\n' "$CANDIDATE_SPEC"
|
||||
;;
|
||||
*)
|
||||
printf 'file:%s\n' "$CANDIDATE_SPEC"
|
||||
;;
|
||||
esac
|
||||
printf 'file:%s\n' "${CANDIDATE_SPEC#file:}"
|
||||
}
|
||||
|
||||
is_extended_stable_release_version() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
readSqliteTranscriptPayload,
|
||||
@@ -38,29 +37,18 @@ const PREEXISTING_SESSION_FIXTURES = [
|
||||
export function measureVolumeDoctorBudget(stateDir) {
|
||||
const counts = { sessions: 0, events: 0, cronJobs: 0, pluginRoots: 0 };
|
||||
for (const agentId of VOLUME_AGENT_IDS) {
|
||||
const db = new DatabaseSync(
|
||||
path.join(stateDir, "agents", agentId, "agent", "openclaw-agent.sqlite"),
|
||||
{ readOnly: true },
|
||||
);
|
||||
try {
|
||||
readDatabase(path.join(stateDir, "agents", agentId, "agent", "openclaw-agent.sqlite"), (db) => {
|
||||
counts.sessions += Number(
|
||||
db.prepare("SELECT count(*) AS count FROM session_nodes").get().count,
|
||||
);
|
||||
counts.events += Number(
|
||||
db.prepare("SELECT count(*) AS count FROM transcript_events").get().count,
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
const db = new DatabaseSync(path.join(stateDir, "state", "openclaw.sqlite"), {
|
||||
readOnly: true,
|
||||
});
|
||||
try {
|
||||
readDatabase(path.join(stateDir, "state", "openclaw.sqlite"), (db) => {
|
||||
counts.cronJobs = Number(db.prepare("SELECT count(*) AS count FROM cron_jobs").get().count);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
const index = readPluginInstallIndex({ stateDir, configPath: null });
|
||||
counts.pluginRoots = new Set(
|
||||
(index.plugins ?? []).filter((plugin) => plugin.enabled).map((plugin) => plugin.rootDir),
|
||||
|
||||
@@ -7,7 +7,7 @@ import { DatabaseSync } from "node:sqlite";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { stripVTControlCharacters } from "node:util";
|
||||
import { isMainThread } from "node:worker_threads";
|
||||
import { readDatabase } from "./observations.mjs";
|
||||
import { findInstalledPackageRoot, readDatabase } from "./observations.mjs";
|
||||
import {
|
||||
assertWorkshopProposalsRetired,
|
||||
captureWorkshopLegacyState,
|
||||
@@ -95,25 +95,24 @@ export function captureWorkshopCandidate(tarball, artifactRoot, candidateVersion
|
||||
}
|
||||
|
||||
function hasMalformedWorkshopIndex(filename) {
|
||||
const database = new DatabaseSync(filename, { readOnly: true });
|
||||
try {
|
||||
database.prepare("SELECT review_id FROM skill_workshop_collection_reviews LIMIT 1").get();
|
||||
return false;
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error)) {
|
||||
return readDatabase(filename, (database) => {
|
||||
try {
|
||||
database.prepare("SELECT review_id FROM skill_workshop_collection_reviews LIMIT 1").get();
|
||||
return false;
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error)) {
|
||||
throw error;
|
||||
}
|
||||
if (error.message.includes("malformed database schema") && error.message.includes(INDEX)) {
|
||||
return true;
|
||||
}
|
||||
// Candidate Doctor retires the table that carries the malformed index.
|
||||
if (error.message.includes("no such table: skill_workshop_collection_reviews")) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (error.message.includes("malformed database schema") && error.message.includes(INDEX)) {
|
||||
return true;
|
||||
}
|
||||
// Candidate Doctor retires the table that carries the malformed index.
|
||||
if (error.message.includes("no such table: skill_workshop_collection_reviews")) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function inspectMalformedState(filename) {
|
||||
@@ -455,14 +454,9 @@ function observeProcess() {
|
||||
}
|
||||
let identity = null;
|
||||
try {
|
||||
let directory = path.dirname(fs.realpathSync(process.argv[1]));
|
||||
// Installed CLI entrypoints are at the package root or inside dist.
|
||||
for (let depth = 0; depth < 3; depth++, directory = path.dirname(directory)) {
|
||||
const manifest = path.join(directory, "package.json");
|
||||
if (fs.existsSync(manifest) && readJson(manifest).name === "openclaw") {
|
||||
identity = installedIdentity(directory);
|
||||
break;
|
||||
}
|
||||
const root = findInstalledPackageRoot(path.dirname(fs.realpathSync(process.argv[1])), 3);
|
||||
if (root) {
|
||||
identity = installedIdentity(root);
|
||||
}
|
||||
} catch {
|
||||
// Missing identity rejects the evidence without changing the observed CLI.
|
||||
|
||||
@@ -377,14 +377,19 @@ function completedResponseEvent(id, output, inputTokens, outputTokens) {
|
||||
};
|
||||
}
|
||||
|
||||
function responseEvents(text, deltas = [text]) {
|
||||
const item = {
|
||||
function assistantMessage(text, phase, id = "msg_e2e_1") {
|
||||
return {
|
||||
type: "message",
|
||||
id: "msg_e2e_1",
|
||||
id,
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
...(phase ? { phase } : {}),
|
||||
content: [{ type: "output_text", text, annotations: [] }],
|
||||
};
|
||||
}
|
||||
|
||||
function responseEvents(text, deltas = [text]) {
|
||||
const item = assistantMessage(text);
|
||||
return [...messageEvents(item, text, deltas), completedResponseEvent("resp_e2e", [item], 11, 7)];
|
||||
}
|
||||
|
||||
@@ -457,14 +462,7 @@ function functionCallEvents(call) {
|
||||
// transport reads it straight off the item, so an untagged item produces no
|
||||
// preamble at all and the scenario silently proves nothing.
|
||||
function preambleThenToolCallEvents(preamble, name, args) {
|
||||
const item = {
|
||||
type: "message",
|
||||
id: "msg_e2e_preamble",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
phase: "commentary",
|
||||
content: [{ type: "output_text", text: preamble, annotations: [] }],
|
||||
};
|
||||
const item = assistantMessage(preamble, "commentary", "msg_e2e_preamble");
|
||||
const call = buildMockFunctionCall(name, args);
|
||||
return [
|
||||
...messageEvents(item, preamble, splitResponseText(preamble)),
|
||||
@@ -900,15 +898,7 @@ const server = http.createServer((req, res) => {
|
||||
id: "resp_e2e",
|
||||
object: "response",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
type: "message",
|
||||
id: "msg_e2e_1",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: responseText, annotations: [] }],
|
||||
},
|
||||
],
|
||||
output: [assistantMessage(responseText)],
|
||||
usage: { input_tokens: 11, output_tokens: 7, total_tokens: 18 },
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -703,34 +703,48 @@ Remove-Item -Path $runDir -Recurse -Force -ErrorAction SilentlyContinue`,
|
||||
);
|
||||
}
|
||||
|
||||
export class LinuxGuest {
|
||||
private vmName: string;
|
||||
private phases: PhaseRunner;
|
||||
private getEnv: () => Record<string, string>;
|
||||
abstract class ParallelsGuest<TOptions extends PosixGuestOptions> {
|
||||
protected phases: PhaseRunner;
|
||||
protected runCommand = run;
|
||||
protected checkGuestSession = false;
|
||||
protected abstract label: string;
|
||||
protected abstract transportArgs(args: string[], env?: Record<string, string>): string[];
|
||||
|
||||
constructor(vmName: string, phases: PhaseRunner, getEnv = () => ({})) {
|
||||
this.vmName = vmName;
|
||||
constructor(phases: PhaseRunner) {
|
||||
this.phases = phases;
|
||||
this.getEnv = getEnv;
|
||||
}
|
||||
|
||||
exec(args: string[], options: PosixGuestOptions = {}): string {
|
||||
exec(args: string[], options?: TOptions): string {
|
||||
return this.run(args, options).stdout.trim();
|
||||
}
|
||||
|
||||
run(args: string[], options: PosixGuestOptions = {}): CommandResult {
|
||||
const result = run("prlctl", this.transportArgs(args, options.env), {
|
||||
run(args: string[], options?: TOptions): CommandResult {
|
||||
const result = this.runCommand("prlctl", this.transportArgs(args, options?.env), {
|
||||
check: false,
|
||||
input: options.input,
|
||||
timeoutMs: this.phases.remainingTimeoutMs(options.timeoutMs),
|
||||
input: options?.input,
|
||||
timeoutMs: this.phases.remainingTimeoutMs(options?.timeoutMs),
|
||||
});
|
||||
this.phases.append(result.stdout);
|
||||
this.phases.append(result.stderr);
|
||||
throwIfFailed("Linux guest command", result, options.check);
|
||||
appendCommandResult(this.phases, result);
|
||||
if (this.checkGuestSession) {
|
||||
throwIfGuestSessionUnavailable(this.label, result, options?.check);
|
||||
}
|
||||
throwIfFailed(this.label, result, options?.check);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private transportArgs(args: string[], env: Record<string, string> = {}): string[] {
|
||||
export class LinuxGuest extends ParallelsGuest<PosixGuestOptions> {
|
||||
protected label = "Linux guest command";
|
||||
private vmName: string;
|
||||
private getEnv: () => Record<string, string>;
|
||||
|
||||
constructor(vmName: string, phases: PhaseRunner, getEnv = () => ({})) {
|
||||
super(phases);
|
||||
this.vmName = vmName;
|
||||
this.getEnv = getEnv;
|
||||
}
|
||||
|
||||
protected transportArgs(args: string[], env: Record<string, string> = {}): string[] {
|
||||
const envArgs = Object.entries({
|
||||
HOME: "/root",
|
||||
OPENCLAW_ALLOW_ROOT: "1",
|
||||
@@ -766,20 +780,18 @@ type MacosGuestInput = {
|
||||
getEnv?: () => Record<string, string>;
|
||||
};
|
||||
|
||||
export class MacosGuest {
|
||||
export class MacosGuest extends ParallelsGuest<PosixGuestOptions> {
|
||||
protected label = "macOS guest command";
|
||||
protected override runCommand = runMacosHostCommand;
|
||||
protected override checkGuestSession = true;
|
||||
private input: MacosGuestInput;
|
||||
private phases: PhaseRunner;
|
||||
|
||||
constructor(input: MacosGuestInput, phases: PhaseRunner) {
|
||||
super(phases);
|
||||
this.input = input;
|
||||
this.phases = phases;
|
||||
}
|
||||
|
||||
exec(args: string[], options: PosixGuestOptions = {}): string {
|
||||
return this.run(args, options).stdout.trim();
|
||||
}
|
||||
|
||||
private transportArgs(args: string[], env: Record<string, string> = {}): string[] {
|
||||
protected transportArgs(args: string[], env: Record<string, string> = {}): string[] {
|
||||
const envArgs = Object.entries({ PATH: this.input.path, ...this.input.getEnv?.(), ...env }).map(
|
||||
([key, value]) => `${key}=${value}`,
|
||||
);
|
||||
@@ -802,19 +814,6 @@ export class MacosGuest {
|
||||
: ["exec", this.input.vmName, "--current-user", "/usr/bin/env", ...envArgs, ...args];
|
||||
}
|
||||
|
||||
run(args: string[], options: PosixGuestOptions = {}): CommandResult {
|
||||
const result = runMacosHostCommand("prlctl", this.transportArgs(args, options.env), {
|
||||
check: false,
|
||||
input: options.input,
|
||||
timeoutMs: this.phases.remainingTimeoutMs(options.timeoutMs),
|
||||
});
|
||||
this.phases.append(result.stdout);
|
||||
this.phases.append(result.stderr);
|
||||
throwIfGuestSessionUnavailable("macOS guest command", result, options.check);
|
||||
throwIfFailed("macOS guest command", result, options.check);
|
||||
return result;
|
||||
}
|
||||
|
||||
sh(script: string, env: Record<string, string> = {}): string {
|
||||
const scriptPath = `/tmp/${guestScriptName("sh")}`;
|
||||
try {
|
||||
@@ -850,31 +849,19 @@ export class MacosGuest {
|
||||
}
|
||||
}
|
||||
|
||||
export class WindowsGuest {
|
||||
export class WindowsGuest extends ParallelsGuest<GuestExecOptions> {
|
||||
protected label = "Windows guest command";
|
||||
private vmName: string;
|
||||
private phases: PhaseRunner;
|
||||
private getEnv: () => Record<string, string>;
|
||||
|
||||
constructor(vmName: string, phases: PhaseRunner, getEnv = () => ({})) {
|
||||
super(phases);
|
||||
this.vmName = vmName;
|
||||
this.phases = phases;
|
||||
this.getEnv = getEnv;
|
||||
}
|
||||
|
||||
exec(args: string[], options: GuestExecOptions = {}): string {
|
||||
return this.run(args, options).stdout.trim();
|
||||
}
|
||||
|
||||
run(args: string[], options: GuestExecOptions = {}): CommandResult {
|
||||
const result = run("prlctl", ["exec", this.vmName, "--current-user", ...args], {
|
||||
check: false,
|
||||
input: options.input,
|
||||
timeoutMs: this.phases.remainingTimeoutMs(options.timeoutMs),
|
||||
});
|
||||
this.phases.append(result.stdout);
|
||||
this.phases.append(result.stderr);
|
||||
throwIfFailed("Windows guest command", result, options.check);
|
||||
return result;
|
||||
protected transportArgs(args: string[]): string[] {
|
||||
return ["exec", this.vmName, "--current-user", ...args];
|
||||
}
|
||||
|
||||
powershell(script: string, options: GuestExecOptions = {}): string {
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
shouldSkipSnapshotRestore,
|
||||
shellQuote,
|
||||
validateSnapshotRestoreMode,
|
||||
warn,
|
||||
withProgressOnStderr,
|
||||
writeJson,
|
||||
writeSummaryMarkdown,
|
||||
@@ -45,10 +44,12 @@ import {
|
||||
printSmokeTargetSummary,
|
||||
posixAgentTurnScript,
|
||||
posixStopGatewayScript,
|
||||
posixRefOnboardArgs,
|
||||
parseSmokeCliArgs,
|
||||
SmokeRunController,
|
||||
smokeDefaultOptions,
|
||||
smokeDefaultStatus,
|
||||
verifyPosixGateway,
|
||||
type SmokeCliOptions,
|
||||
} from "./smoke-common.ts";
|
||||
|
||||
@@ -391,28 +392,7 @@ fi`);
|
||||
}
|
||||
|
||||
private runRefOnboard(): void {
|
||||
this.guest.exec([
|
||||
"/usr/bin/env",
|
||||
`${this.auth.apiKeyEnv}=${this.auth.apiKeyValue}`,
|
||||
"openclaw",
|
||||
"onboard",
|
||||
"--non-interactive",
|
||||
"--mode",
|
||||
"local",
|
||||
"--auth-choice",
|
||||
this.auth.authChoice,
|
||||
...(this.auth.tokenProvider ? ["--token-provider", this.auth.tokenProvider] : []),
|
||||
"--secret-input-mode",
|
||||
"ref",
|
||||
"--gateway-port",
|
||||
"18789",
|
||||
"--gateway-bind",
|
||||
"loopback",
|
||||
"--skip-skills",
|
||||
"--skip-health",
|
||||
"--accept-risk",
|
||||
"--json",
|
||||
]);
|
||||
this.guest.exec(posixRefOnboardArgs(this.auth));
|
||||
}
|
||||
|
||||
private injectBadPluginFixture(): void {
|
||||
@@ -490,20 +470,7 @@ setsid sh -lc ` +
|
||||
}
|
||||
|
||||
private verifyGatewayStatus(): void {
|
||||
for (let attempt = 1; attempt <= 8; attempt++) {
|
||||
const result = this.guest.run(
|
||||
["openclaw", "gateway", "status", "--deep", "--require-rpc", "--timeout", "15000"],
|
||||
{ check: false },
|
||||
);
|
||||
if (result.status === 0) {
|
||||
return;
|
||||
}
|
||||
if (attempt < 8) {
|
||||
warn(`gateway-status retry ${attempt}`);
|
||||
run("sleep", ["5"]);
|
||||
}
|
||||
}
|
||||
throw new Error("gateway status did not become RPC-ready");
|
||||
verifyPosixGateway(this.guest);
|
||||
}
|
||||
|
||||
private async verifyBadPluginDiagnostic(lane: "fresh" | "upgrade"): Promise<void> {
|
||||
|
||||
@@ -48,9 +48,11 @@ import {
|
||||
printSmokeTargetSummary,
|
||||
posixAgentTurnScript,
|
||||
posixStopGatewayScript,
|
||||
posixRefOnboardArgs,
|
||||
SmokeRunController,
|
||||
smokeDefaultOptions,
|
||||
smokeDefaultStatus,
|
||||
verifyPosixGateway,
|
||||
type SmokeCliOptions,
|
||||
} from "./smoke-common.ts";
|
||||
|
||||
@@ -317,16 +319,7 @@ class MacosSmoke extends SmokeRunController<MacosOptions> {
|
||||
}
|
||||
|
||||
protected async runFreshLane(): Promise<void> {
|
||||
await this.phases.phase("fresh.restore-snapshot", 780, () => this.restoreSnapshot());
|
||||
await this.phases.phase("fresh.reset-state", 180, () => this.resetState());
|
||||
await this.phases.phase("fresh.install-main", 420, () =>
|
||||
this.installMain("openclaw-main-fresh.tgz"),
|
||||
);
|
||||
this.status.freshVersion = await this.extractLastVersion("fresh.install-main");
|
||||
await this.phases.phase("fresh.verify-main-version", 60, () => this.verifyTargetVersion());
|
||||
await this.phases.phase("fresh.verify-bundle-permissions", 180, () =>
|
||||
this.verifyBundlePermissions(),
|
||||
);
|
||||
await this.runInstallLane("fresh");
|
||||
await this.phases.phase("fresh.install-companions", 600, () =>
|
||||
installSmokeRuntimeCompanions({
|
||||
provider: this.options.provider,
|
||||
@@ -341,31 +334,39 @@ class MacosSmoke extends SmokeRunController<MacosOptions> {
|
||||
}
|
||||
|
||||
protected async runUpgradeLane(): Promise<void> {
|
||||
await this.phases.phase("upgrade.restore-snapshot", 780, () => this.restoreSnapshot());
|
||||
await this.phases.phase("upgrade.reset-state", 180, () => this.resetState());
|
||||
await this.phases.phase("upgrade.install-latest", 420, () => this.installLatestRelease());
|
||||
this.status.latestInstalledVersion = await this.extractLastVersion("upgrade.install-latest");
|
||||
await this.phases.phase("upgrade.verify-latest-version", 60, () =>
|
||||
this.verifyVersionContains(this.installVersion),
|
||||
);
|
||||
if (this.options.skipLatestRefCheck) {
|
||||
this.status.upgradePrecheck = "skipped";
|
||||
} else if (
|
||||
await this.phases.phaseReturns("upgrade.latest-ref-precheck", 180, () =>
|
||||
this.captureLatestRefFailure(),
|
||||
)
|
||||
) {
|
||||
this.status.upgradePrecheck = "latest-ref-pass";
|
||||
} else {
|
||||
this.status.upgradePrecheck = "latest-ref-fail";
|
||||
}
|
||||
if (this.options.targetPackageSpec) {
|
||||
await this.phases.phase("upgrade.install-main", 420, () =>
|
||||
this.installMain("openclaw-main-upgrade.tgz"),
|
||||
await this.runInstallLane("upgrade");
|
||||
await this.phases.phase("upgrade.onboard-ref", 420, () => this.runRefOnboard());
|
||||
await this.runGatewaySmoke("upgrade");
|
||||
}
|
||||
|
||||
private async runInstallLane(lane: "fresh" | "upgrade"): Promise<void> {
|
||||
await this.phases.phase(`${lane}.restore-snapshot`, 780, () => this.restoreSnapshot());
|
||||
await this.phases.phase(`${lane}.reset-state`, 180, () => this.resetState());
|
||||
if (lane === "upgrade") {
|
||||
await this.phases.phase("upgrade.install-latest", 420, () => this.installLatestRelease());
|
||||
this.status.latestInstalledVersion = await this.extractLastVersion("upgrade.install-latest");
|
||||
await this.phases.phase("upgrade.verify-latest-version", 60, () =>
|
||||
this.verifyVersionContains(this.installVersion),
|
||||
);
|
||||
this.status.upgradeVersion = await this.extractLastVersion("upgrade.install-main");
|
||||
await this.phases.phase("upgrade.verify-main-version", 60, () => this.verifyTargetVersion());
|
||||
await this.phases.phase("upgrade.verify-bundle-permissions", 180, () =>
|
||||
if (this.options.skipLatestRefCheck) {
|
||||
this.status.upgradePrecheck = "skipped";
|
||||
} else if (
|
||||
await this.phases.phaseReturns("upgrade.latest-ref-precheck", 180, () =>
|
||||
this.captureLatestRefFailure(),
|
||||
)
|
||||
) {
|
||||
this.status.upgradePrecheck = "latest-ref-pass";
|
||||
} else {
|
||||
this.status.upgradePrecheck = "latest-ref-fail";
|
||||
}
|
||||
}
|
||||
if (lane === "fresh" || this.options.targetPackageSpec) {
|
||||
await this.phases.phase(`${lane}.install-main`, 420, () =>
|
||||
this.installMain(`openclaw-main-${lane}.tgz`),
|
||||
);
|
||||
this.status[`${lane}Version`] = await this.extractLastVersion(`${lane}.install-main`);
|
||||
await this.phases.phase(`${lane}.verify-main-version`, 60, () => this.verifyTargetVersion());
|
||||
await this.phases.phase(`${lane}.verify-bundle-permissions`, 180, () =>
|
||||
this.verifyBundlePermissions(),
|
||||
);
|
||||
} else {
|
||||
@@ -377,8 +378,6 @@ class MacosSmoke extends SmokeRunController<MacosOptions> {
|
||||
this.verifyDevChannelUpdate(),
|
||||
);
|
||||
}
|
||||
await this.phases.phase("upgrade.onboard-ref", 420, () => this.runRefOnboard());
|
||||
await this.runGatewaySmoke("upgrade");
|
||||
}
|
||||
|
||||
private async runGatewaySmoke(lane: "fresh" | "upgrade"): Promise<void> {
|
||||
@@ -632,28 +631,7 @@ fi`);
|
||||
|
||||
private runRefOnboard(): void {
|
||||
const daemonFlag = this.guestTransport === "sudo" ? "--skip-health" : "--install-daemon";
|
||||
this.guest.exec([
|
||||
"/usr/bin/env",
|
||||
`${this.auth.apiKeyEnv}=${this.auth.apiKeyValue}`,
|
||||
guestOpenClaw,
|
||||
"onboard",
|
||||
"--non-interactive",
|
||||
"--mode",
|
||||
"local",
|
||||
"--auth-choice",
|
||||
this.auth.authChoice,
|
||||
...(this.auth.tokenProvider ? ["--token-provider", this.auth.tokenProvider] : []),
|
||||
"--secret-input-mode",
|
||||
"ref",
|
||||
"--gateway-port",
|
||||
"18789",
|
||||
"--gateway-bind",
|
||||
"loopback",
|
||||
daemonFlag,
|
||||
"--skip-skills",
|
||||
"--accept-risk",
|
||||
"--json",
|
||||
]);
|
||||
this.guest.exec(posixRefOnboardArgs(this.auth, daemonFlag));
|
||||
}
|
||||
|
||||
private captureLatestRefFailure(): void {
|
||||
@@ -739,24 +717,7 @@ sleep 1`,
|
||||
}
|
||||
|
||||
private verifyGateway(): void {
|
||||
for (let attempt = 1; attempt <= 8; attempt++) {
|
||||
const result = this.guestOpenClaw([
|
||||
"gateway",
|
||||
"status",
|
||||
"--deep",
|
||||
"--require-rpc",
|
||||
"--timeout",
|
||||
"15000",
|
||||
]);
|
||||
if (result) {
|
||||
return;
|
||||
}
|
||||
if (attempt < 8) {
|
||||
warn(`gateway-status retry ${attempt}`);
|
||||
run("sleep", ["5"]);
|
||||
}
|
||||
}
|
||||
throw new Error("gateway status did not become RPC-ready");
|
||||
verifyPosixGateway(this.guest);
|
||||
}
|
||||
|
||||
private showGatewayStatusCompat(): void {
|
||||
|
||||
@@ -564,7 +564,6 @@ export class NpmUpdateSmoke {
|
||||
private updateTargetTarball = "";
|
||||
private targetTarballPath = "";
|
||||
private targetTarballBuildCommit = "";
|
||||
private targetDependencyPackages: NpmRegistryPackage[] = [];
|
||||
private targetRegistryPackages: NpmRegistryPackage[] = [];
|
||||
private targetTarballVersion = "";
|
||||
private targetRegistryHostUrl = "";
|
||||
@@ -820,7 +819,7 @@ export class NpmUpdateSmoke {
|
||||
buildCommitShort: this.targetTarballBuildCommit.slice(0, 7),
|
||||
path: hostedTarballPath,
|
||||
version: this.targetTarballVersion,
|
||||
registryPackages: [...this.targetDependencyPackages, ...this.targetRegistryPackages],
|
||||
registryPackages: this.targetRegistryPackages,
|
||||
};
|
||||
} else if (!this.options.updateTarget || this.options.updateTarget === "local-main") {
|
||||
const providerConfig = resolveProviderConfig(this.options.provider);
|
||||
@@ -1362,59 +1361,44 @@ export class NpmUpdateSmoke {
|
||||
]);
|
||||
this.targetTarballVersion = targetPackageJson.version ?? "";
|
||||
this.targetTarballBuildCommit = targetBuildCommit;
|
||||
this.targetDependencyPackages = await Promise.all(
|
||||
this.options.dependencyTarballs.map(async (dependencyTarball) => {
|
||||
const tarballPath = path.resolve(dependencyTarball);
|
||||
if (!existsSync(tarballPath)) {
|
||||
throw new Error(`dependency tarball does not exist: ${tarballPath}`);
|
||||
}
|
||||
const dependencyPackage = await extractPackageJsonFromTgz<{
|
||||
name?: string;
|
||||
version?: string;
|
||||
}>(tarballPath, "package/package.json");
|
||||
const name = dependencyPackage.name ?? "";
|
||||
const version = dependencyPackage.version ?? "";
|
||||
if (!name || !version || name === "openclaw") {
|
||||
throw new Error(`dependency tarball has invalid package metadata: ${tarballPath}`);
|
||||
}
|
||||
if (targetPackageJson.dependencies?.[name] !== version) {
|
||||
throw new Error(
|
||||
`target tarball requires ${name}@${targetPackageJson.dependencies?.[name] ?? "<missing>"}, but companion tarball provides ${version}`,
|
||||
);
|
||||
}
|
||||
return { name, version, tarballPath };
|
||||
}),
|
||||
);
|
||||
this.targetRegistryPackages = await Promise.all(
|
||||
this.options.registryPackageTarballs.map(async (registryPackageTarball) => {
|
||||
const tarballPath = path.resolve(registryPackageTarball);
|
||||
if (!existsSync(tarballPath)) {
|
||||
throw new Error(`registry package tarball does not exist: ${tarballPath}`);
|
||||
}
|
||||
const registryPackage = await extractPackageJsonFromTgz<{
|
||||
name?: string;
|
||||
version?: string;
|
||||
}>(tarballPath, "package/package.json");
|
||||
const name = registryPackage.name ?? "";
|
||||
const version = registryPackage.version ?? "";
|
||||
if (!name || !version || name === "openclaw") {
|
||||
throw new Error(`registry package tarball has invalid metadata: ${tarballPath}`);
|
||||
}
|
||||
if (version !== this.targetTarballVersion) {
|
||||
throw new Error(
|
||||
`registry package ${name}@${version} does not match candidate ${this.targetTarballVersion}`,
|
||||
);
|
||||
}
|
||||
return { name, version, tarballPath };
|
||||
}),
|
||||
);
|
||||
const registryPackageNames = new Set(
|
||||
[...this.targetDependencyPackages, ...this.targetRegistryPackages].map((pkg) => pkg.name),
|
||||
);
|
||||
if (
|
||||
registryPackageNames.size !==
|
||||
this.targetDependencyPackages.length + this.targetRegistryPackages.length
|
||||
) {
|
||||
const readTarballs = (tarballs: string[], kind: "dependency" | "registry package") =>
|
||||
Promise.all(
|
||||
tarballs.map(async (tarball) => {
|
||||
const tarballPath = path.resolve(tarball);
|
||||
if (!existsSync(tarballPath)) {
|
||||
throw new Error(`${kind} tarball does not exist: ${tarballPath}`);
|
||||
}
|
||||
const pkg = await extractPackageJsonFromTgz<{
|
||||
name?: string;
|
||||
version?: string;
|
||||
}>(tarballPath, "package/package.json");
|
||||
const name = pkg.name ?? "";
|
||||
const version = pkg.version ?? "";
|
||||
if (!name || !version || name === "openclaw") {
|
||||
throw new Error(
|
||||
`${kind} tarball has invalid ${kind === "dependency" ? "package " : ""}metadata: ${tarballPath}`,
|
||||
);
|
||||
}
|
||||
if (kind === "dependency") {
|
||||
if (targetPackageJson.dependencies?.[name] !== version) {
|
||||
throw new Error(
|
||||
`target tarball requires ${name}@${targetPackageJson.dependencies?.[name] ?? "<missing>"}, but companion tarball provides ${version}`,
|
||||
);
|
||||
}
|
||||
} else if (version !== this.targetTarballVersion) {
|
||||
throw new Error(
|
||||
`registry package ${name}@${version} does not match candidate ${this.targetTarballVersion}`,
|
||||
);
|
||||
}
|
||||
return { name, version, tarballPath };
|
||||
}),
|
||||
);
|
||||
this.targetRegistryPackages = [
|
||||
...(await readTarballs(this.options.dependencyTarballs, "dependency")),
|
||||
...(await readTarballs(this.options.registryPackageTarballs, "registry package")),
|
||||
];
|
||||
const registryPackageNames = new Set(this.targetRegistryPackages.map((pkg) => pkg.name));
|
||||
if (registryPackageNames.size !== this.targetRegistryPackages.length) {
|
||||
throw new Error("candidate registry tarballs must have unique package names");
|
||||
}
|
||||
if (!this.targetTarballVersion || !this.targetTarballBuildCommit) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { stripLeadingPackageManagerSeparator } from "../../lib/arg-utils.mts";
|
||||
import { resolveProviderConfig } from "../../lib/cross-os-release-checks/config.ts";
|
||||
import { parseTcpPort } from "./env-limits.ts";
|
||||
import { extractLastOpenClawVersionFromLog } from "./filesystem.ts";
|
||||
import { run, say, die, shellQuote } from "./host-command.ts";
|
||||
import { run, say, die, shellQuote, warn } from "./host-command.ts";
|
||||
import {
|
||||
resolveHostIp,
|
||||
resolveHostPort,
|
||||
@@ -20,7 +20,15 @@ import {
|
||||
packOpenClaw,
|
||||
} from "./package-artifact.ts";
|
||||
import { ensureValue, parseMode, parseProvider } from "./provider-auth.ts";
|
||||
import type { HostServer, Mode, PackageArtifact, Provider, SnapshotInfo } from "./types.ts";
|
||||
import type {
|
||||
CommandResult,
|
||||
HostServer,
|
||||
Mode,
|
||||
PackageArtifact,
|
||||
Provider,
|
||||
ProviderAuth,
|
||||
SnapshotInfo,
|
||||
} from "./types.ts";
|
||||
|
||||
interface SmokeHostOptions {
|
||||
hostIp?: string;
|
||||
@@ -143,17 +151,7 @@ export function parseSmokeCliArgs<TOptions extends SmokeCliOptions>(
|
||||
return options;
|
||||
}
|
||||
|
||||
interface SmokeLaneStatuses {
|
||||
freshAgent: string;
|
||||
freshGateway: string;
|
||||
freshMain: string;
|
||||
freshVersion: string;
|
||||
latestInstalledVersion: string;
|
||||
upgrade: string;
|
||||
upgradeAgent: string;
|
||||
upgradeGateway: string;
|
||||
upgradeVersion: string;
|
||||
}
|
||||
type SmokeLaneStatuses = typeof smokeDefaultStatus;
|
||||
|
||||
export abstract class SmokeRunController<TOptions extends SmokeRunOptions & SmokeHostOptions> {
|
||||
protected hostIp = "";
|
||||
@@ -234,6 +232,54 @@ export function npmRegistryEnv(registry?: string): Record<string, string> {
|
||||
return registry ? { NPM_CONFIG_REGISTRY: registry, npm_config_registry: registry } : {};
|
||||
}
|
||||
|
||||
export function posixRefOnboardArgs(
|
||||
auth: ProviderAuth,
|
||||
daemonFlag?: "--install-daemon" | "--skip-health",
|
||||
): string[] {
|
||||
return [
|
||||
"/usr/bin/env",
|
||||
`${auth.apiKeyEnv}=${auth.apiKeyValue}`,
|
||||
"openclaw",
|
||||
"onboard",
|
||||
"--non-interactive",
|
||||
"--mode",
|
||||
"local",
|
||||
"--auth-choice",
|
||||
auth.authChoice,
|
||||
...(auth.tokenProvider ? ["--token-provider", auth.tokenProvider] : []),
|
||||
"--secret-input-mode",
|
||||
"ref",
|
||||
"--gateway-port",
|
||||
"18789",
|
||||
"--gateway-bind",
|
||||
"loopback",
|
||||
...(daemonFlag ? [daemonFlag] : []),
|
||||
"--skip-skills",
|
||||
...(daemonFlag ? [] : ["--skip-health"]),
|
||||
"--accept-risk",
|
||||
"--json",
|
||||
];
|
||||
}
|
||||
|
||||
export function verifyPosixGateway(guest: {
|
||||
run(args: string[], options: { check: false }): CommandResult;
|
||||
}): void {
|
||||
for (let attempt = 1; attempt <= 8; attempt++) {
|
||||
const result = guest.run(
|
||||
["openclaw", "gateway", "status", "--deep", "--require-rpc", "--timeout", "15000"],
|
||||
{ check: false },
|
||||
);
|
||||
if (result.status === 0) {
|
||||
return;
|
||||
}
|
||||
if (attempt < 8) {
|
||||
warn(`gateway-status retry ${attempt}`);
|
||||
run("sleep", ["5"]);
|
||||
}
|
||||
}
|
||||
throw new Error("gateway status did not become RPC-ready");
|
||||
}
|
||||
|
||||
export function posixAgentTurnScript(input: {
|
||||
command: string;
|
||||
sessionIdExpression: string;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Snapshots script supports OpenClaw repository automation.
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { die, run } from "./host-command.ts";
|
||||
import type { Mode, SnapshotInfo } from "./types.ts";
|
||||
|
||||
@@ -99,33 +98,19 @@ function stringSimilarity(a: string, b: string): number {
|
||||
if (a === b) {
|
||||
return 1;
|
||||
}
|
||||
const rows = a.length + 1;
|
||||
const cols = b.length + 1;
|
||||
const matrix = Array.from({ length: rows }, () => Array<number>(cols).fill(0));
|
||||
for (let i = 0; i < rows; i++) {
|
||||
expectDefined(matrix[i], `snapshot similarity matrix row ${i}`)[0] = i;
|
||||
}
|
||||
const firstRow = expectDefined(matrix[0], "snapshot similarity first matrix row");
|
||||
for (let j = 0; j < cols; j++) {
|
||||
firstRow[j] = j;
|
||||
}
|
||||
for (let i = 1; i < rows; i++) {
|
||||
const row = expectDefined(matrix[i], `snapshot similarity matrix row ${i}`);
|
||||
const previousRow = expectDefined(matrix[i - 1], `snapshot similarity matrix row ${i - 1}`);
|
||||
for (let j = 1; j < cols; j++) {
|
||||
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
let diagonal = row[0]!;
|
||||
row[0] = i;
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
const above = row[j]!;
|
||||
row[j] = Math.min(
|
||||
expectDefined(previousRow[j], `snapshot similarity deletion cell ${i},${j}`) + 1,
|
||||
expectDefined(row[j - 1], `snapshot similarity insertion cell ${i},${j - 1}`) + 1,
|
||||
expectDefined(
|
||||
previousRow[j - 1],
|
||||
`snapshot similarity substitution cell ${i - 1},${j - 1}`,
|
||||
) + (a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1),
|
||||
above + 1,
|
||||
row[j - 1]! + 1,
|
||||
diagonal + (a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1),
|
||||
);
|
||||
diagonal = above;
|
||||
}
|
||||
}
|
||||
const distance = expectDefined(
|
||||
expectDefined(matrix[a.length], "snapshot similarity final matrix row")[b.length],
|
||||
"snapshot similarity final distance",
|
||||
);
|
||||
return 1 - distance / Math.max(a.length, b.length, 1);
|
||||
return 1 - row[b.length]! / Math.max(a.length, b.length, 1);
|
||||
}
|
||||
@@ -8,27 +8,16 @@ set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
|
||||
source "$ROOT_DIR/scripts/e2e/lib/release-scenarios/docker.sh"
|
||||
|
||||
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-release-media-memory-e2e" OPENCLAW_RELEASE_MEDIA_MEMORY_E2E_IMAGE)"
|
||||
SKIP_BUILD="${OPENCLAW_RELEASE_MEDIA_MEMORY_E2E_SKIP_BUILD:-0}"
|
||||
run_log=""
|
||||
trap 'docker_e2e_cleanup_package_run "${PACKAGE_TGZ:-}" "${run_log:-}"' EXIT
|
||||
|
||||
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz release-media-memory "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")"
|
||||
docker_e2e_package_mount_args "$PACKAGE_TGZ"
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" release-media-memory "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD"
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 release-media-memory empty)"
|
||||
|
||||
run_log="$(docker_e2e_run_log release-media-memory)"
|
||||
openclaw_release_prepare_docker release-media-memory
|
||||
echo "Running release media memory Docker E2E..."
|
||||
if ! docker_e2e_run_with_harness \
|
||||
openclaw_release_run_docker \
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
|
||||
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
|
||||
-i "$IMAGE_NAME" bash scripts/e2e/lib/release-media-memory/scenario.sh >"$run_log" 2>&1; then
|
||||
docker_e2e_print_log "$run_log"
|
||||
exit 1
|
||||
fi
|
||||
-i "$IMAGE_NAME" bash scripts/e2e/lib/release-media-memory/scenario.sh
|
||||
|
||||
echo "Release media memory Docker E2E passed."
|
||||
@@ -7,27 +7,16 @@ set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
|
||||
source "$ROOT_DIR/scripts/e2e/lib/release-scenarios/docker.sh"
|
||||
|
||||
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-release-plugin-marketplace-e2e" OPENCLAW_RELEASE_PLUGIN_MARKETPLACE_E2E_IMAGE)"
|
||||
SKIP_BUILD="${OPENCLAW_RELEASE_PLUGIN_MARKETPLACE_E2E_SKIP_BUILD:-0}"
|
||||
run_log=""
|
||||
trap 'docker_e2e_cleanup_package_run "${PACKAGE_TGZ:-}" "${run_log:-}"' EXIT
|
||||
|
||||
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz release-plugin-marketplace "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")"
|
||||
docker_e2e_package_mount_args "$PACKAGE_TGZ"
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" release-plugin-marketplace "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD"
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 release-plugin-marketplace empty)"
|
||||
|
||||
run_log="$(docker_e2e_run_log release-plugin-marketplace)"
|
||||
openclaw_release_prepare_docker release-plugin-marketplace
|
||||
echo "Running release plugin marketplace Docker E2E..."
|
||||
if ! docker_e2e_run_with_harness \
|
||||
openclaw_release_run_docker \
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
|
||||
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
|
||||
-i "$IMAGE_NAME" bash scripts/e2e/lib/release-plugin-marketplace/scenario.sh >"$run_log" 2>&1; then
|
||||
docker_e2e_print_log "$run_log"
|
||||
exit 1
|
||||
fi
|
||||
-i "$IMAGE_NAME" bash scripts/e2e/lib/release-plugin-marketplace/scenario.sh
|
||||
|
||||
echo "Release plugin marketplace Docker E2E passed."
|
||||
@@ -8,19 +8,11 @@ set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
|
||||
source "$ROOT_DIR/scripts/e2e/lib/release-scenarios/docker.sh"
|
||||
|
||||
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-release-upgrade-user-journey-e2e" OPENCLAW_RELEASE_UPGRADE_USER_JOURNEY_E2E_IMAGE)"
|
||||
SKIP_BUILD="${OPENCLAW_RELEASE_UPGRADE_USER_JOURNEY_E2E_SKIP_BUILD:-0}"
|
||||
run_log=""
|
||||
trap 'docker_e2e_cleanup_package_run "${PACKAGE_TGZ:-}" "${run_log:-}"' EXIT
|
||||
|
||||
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz release-upgrade-user-journey "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")"
|
||||
docker_e2e_package_mount_args "$PACKAGE_TGZ"
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" release-upgrade-user-journey "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD"
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 release-upgrade-user-journey empty)"
|
||||
|
||||
run_log="$(docker_e2e_run_log release-upgrade-user-journey)"
|
||||
openclaw_release_prepare_docker release-upgrade-user-journey
|
||||
DOCKER_ENV_ARGS=(
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64"
|
||||
@@ -38,12 +30,9 @@ if [ -n "${OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC:-}" ]; then
|
||||
fi
|
||||
|
||||
echo "Running release upgrade user journey Docker E2E..."
|
||||
if ! docker_e2e_run_with_harness \
|
||||
openclaw_release_run_docker \
|
||||
"${DOCKER_ENV_ARGS[@]}" \
|
||||
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
|
||||
-i "$IMAGE_NAME" bash scripts/e2e/lib/release-upgrade-user-journey/scenario.sh >"$run_log" 2>&1; then
|
||||
docker_e2e_print_log "$run_log"
|
||||
exit 1
|
||||
fi
|
||||
-i "$IMAGE_NAME" bash scripts/e2e/lib/release-upgrade-user-journey/scenario.sh
|
||||
|
||||
echo "Release upgrade user journey Docker E2E passed. Evidence: $ARTIFACT_DIR"
|
||||
@@ -7,6 +7,7 @@ set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
|
||||
source "$ROOT_DIR/scripts/e2e/lib/release-scenarios/docker.sh"
|
||||
|
||||
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-release-user-journey-e2e" OPENCLAW_RELEASE_USER_JOURNEY_E2E_IMAGE)"
|
||||
SKIP_BUILD="${OPENCLAW_RELEASE_USER_JOURNEY_E2E_SKIP_BUILD:-0}"
|
||||
@@ -16,26 +17,14 @@ HTTP_TIMEOUT_MS="$(
|
||||
HTTP_BODY_MAX_BYTES="$(
|
||||
docker_e2e_read_positive_int_env OPENCLAW_RELEASE_USER_JOURNEY_HTTP_BODY_MAX_BYTES 1048576
|
||||
)"
|
||||
run_log=""
|
||||
trap 'docker_e2e_cleanup_package_run "${PACKAGE_TGZ:-}" "${run_log:-}"' EXIT
|
||||
|
||||
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz release-user-journey "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")"
|
||||
docker_e2e_package_mount_args "$PACKAGE_TGZ"
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" release-user-journey "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD"
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 release-user-journey empty)"
|
||||
|
||||
run_log="$(docker_e2e_run_log release-user-journey)"
|
||||
openclaw_release_prepare_docker release-user-journey
|
||||
echo "Running release user journey Docker E2E..."
|
||||
if ! docker_e2e_run_with_harness \
|
||||
openclaw_release_run_docker \
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
-e "OPENCLAW_RELEASE_USER_JOURNEY_HTTP_TIMEOUT_MS=$HTTP_TIMEOUT_MS" \
|
||||
-e "OPENCLAW_RELEASE_USER_JOURNEY_HTTP_BODY_MAX_BYTES=$HTTP_BODY_MAX_BYTES" \
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
|
||||
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
|
||||
-i "$IMAGE_NAME" bash scripts/e2e/lib/release-user-journey/scenario.sh >"$run_log" 2>&1; then
|
||||
docker_e2e_print_log "$run_log"
|
||||
exit 1
|
||||
fi
|
||||
-i "$IMAGE_NAME" bash scripts/e2e/lib/release-user-journey/scenario.sh
|
||||
|
||||
echo "Release user journey Docker E2E passed."
|
||||
@@ -170,69 +170,40 @@ async function main() {
|
||||
|
||||
const gatewayRestarts: string[] = [];
|
||||
const gatewayCommand = makeParams("/openclaw restart gateway", cfg).command;
|
||||
const gatewayPlan = await runSystemAgentRescueMessage({
|
||||
cfg,
|
||||
command: gatewayCommand,
|
||||
commandBody: "/openclaw restart gateway",
|
||||
agentId: "default",
|
||||
isGroup: false,
|
||||
deps: {
|
||||
runGatewayRestart: async () => {
|
||||
gatewayRestarts.push("restart");
|
||||
},
|
||||
const restartDeps = {
|
||||
runGatewayRestart: async () => {
|
||||
gatewayRestarts.push("restart");
|
||||
},
|
||||
});
|
||||
};
|
||||
const runGatewayCommand = (
|
||||
commandBody: string,
|
||||
deps?: Parameters<typeof runSystemAgentRescueMessage>[0]["deps"],
|
||||
) =>
|
||||
runSystemAgentRescueMessage({
|
||||
cfg,
|
||||
command: gatewayCommand,
|
||||
commandBody,
|
||||
agentId: "default",
|
||||
isGroup: false,
|
||||
deps,
|
||||
});
|
||||
const gatewayPlan = await runGatewayCommand("/openclaw restart gateway", restartDeps);
|
||||
assert(
|
||||
gatewayPlan?.includes("Reply /openclaw yes to apply"),
|
||||
"gateway restart did not require approval",
|
||||
);
|
||||
const pluginList = await runSystemAgentRescueMessage({
|
||||
cfg,
|
||||
command: gatewayCommand,
|
||||
commandBody: "/openclaw plugins list",
|
||||
agentId: "default",
|
||||
isGroup: false,
|
||||
deps: {
|
||||
runPluginsList: async (runtime) => runtime.log("plugin rows"),
|
||||
},
|
||||
const pluginList = await runGatewayCommand("/openclaw plugins list", {
|
||||
runPluginsList: async (runtime) => runtime.log("plugin rows"),
|
||||
});
|
||||
assert(pluginList === "plugin rows", "read-only rescue command did not run");
|
||||
const revokedApproval = await runSystemAgentRescueMessage({
|
||||
cfg,
|
||||
command: gatewayCommand,
|
||||
commandBody: "/openclaw yes",
|
||||
agentId: "default",
|
||||
isGroup: false,
|
||||
deps: {
|
||||
runGatewayRestart: async () => {
|
||||
gatewayRestarts.push("restart");
|
||||
},
|
||||
},
|
||||
});
|
||||
const revokedApproval = await runGatewayCommand("/openclaw yes", restartDeps);
|
||||
assert(
|
||||
revokedApproval === "No pending OpenClaw rescue change is waiting for approval.",
|
||||
"fresh rescue command did not revoke the older pending change",
|
||||
);
|
||||
assert(gatewayRestarts.length === 0, "revoked gateway restart was invoked");
|
||||
await runSystemAgentRescueMessage({
|
||||
cfg,
|
||||
command: gatewayCommand,
|
||||
commandBody: "/openclaw restart gateway",
|
||||
agentId: "default",
|
||||
isGroup: false,
|
||||
});
|
||||
const gatewayApplied = await runSystemAgentRescueMessage({
|
||||
cfg,
|
||||
command: gatewayCommand,
|
||||
commandBody: "/openclaw yes",
|
||||
agentId: "default",
|
||||
isGroup: false,
|
||||
deps: {
|
||||
runGatewayRestart: async () => {
|
||||
gatewayRestarts.push("restart");
|
||||
},
|
||||
},
|
||||
});
|
||||
await runGatewayCommand("/openclaw restart gateway");
|
||||
const gatewayApplied = await runGatewayCommand("/openclaw yes", restartDeps);
|
||||
assert(
|
||||
gatewayApplied?.includes("[openclaw] done: gateway.restart"),
|
||||
"gateway restart did not apply",
|
||||
|
||||
@@ -161,6 +161,7 @@ if [ "$UPGRADE_TARGET_TRAIN" = extended-stable ]; then
|
||||
cp "$HARNESS_ROOT_DIR/scripts/e2e/lib/upgrade-survivor/backup-rollback-summary.mjs" "$UPGRADE_SCENARIO_STAGE/backup-rollback-summary.mjs"
|
||||
cp "$HARNESS_ROOT_DIR/scripts/e2e/lib/upgrade-survivor/native-assignment-summary.mjs" "$UPGRADE_SCENARIO_STAGE/native-assignment-summary.mjs"
|
||||
cp "$HARNESS_ROOT_DIR/scripts/e2e/lib/upgrade-survivor/plugin-policy-summary.mjs" "$UPGRADE_SCENARIO_STAGE/plugin-policy-summary.mjs"
|
||||
cp "$HARNESS_ROOT_DIR/scripts/e2e/lib/upgrade-survivor/observations.mjs" "$UPGRADE_SCENARIO_STAGE/observations.mjs"
|
||||
cp "$HARNESS_ROOT_DIR/scripts/e2e/lib/upgrade-survivor/package-activation-recovery.mjs" "$UPGRADE_SCENARIO_STAGE/package-activation-recovery.mjs"
|
||||
chmod 0755 "$UPGRADE_SCENARIO_STAGE"
|
||||
UPGRADE_SCENARIO_ARGS+=(
|
||||
|
||||
@@ -10,6 +10,7 @@ export function copySurvivorCaptureClosure(workDir: string) {
|
||||
"scripts/e2e/lib/env-limits.mjs",
|
||||
"scripts/e2e/lib/text-file-utils.mjs",
|
||||
UPGRADE_SURVIVOR_DIAGNOSTICS_PATH,
|
||||
"scripts/e2e/lib/upgrade-survivor/observations.mjs",
|
||||
"scripts/e2e/lib/upgrade-survivor/package-activation-recovery.mjs",
|
||||
"scripts/e2e/lib/upgrade-survivor/backup-rollback-summary.mjs",
|
||||
"scripts/e2e/lib/upgrade-survivor/native-assignment-summary.mjs",
|
||||
|
||||
@@ -2577,7 +2577,7 @@ docker_e2e_docker_run_cmd run demo
|
||||
expect(script, path).toContain("openclaw_e2e_enable_openclaw_cli_timeout");
|
||||
}
|
||||
expect(readFileSync(RELEASE_UPGRADE_USER_JOURNEY_SCENARIO_PATH, "utf8")).toContain(
|
||||
'openclaw_e2e_run_command node "$baseline_entry" onboard',
|
||||
'openclaw_release_onboard "$PORT" openclaw_e2e_run_command node "$baseline_entry"',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -102,9 +102,7 @@ openclaw_e2e_fixture_plugin_command openclaw_e2e_maybe_timeout 5s node "$OPENCLA
|
||||
const sweepFixtureLoader = `
|
||||
source() {
|
||||
if [[ "$1" == scripts/e2e/lib/plugins/fixtures.sh ]]; then
|
||||
pack_fixture_plugin() { :; }
|
||||
pack_fixture_plugin_with_cli_registry_dependency() { :; }
|
||||
pack_fake_is_number_package() { :; }
|
||||
pack_fixture_archive() { :; }
|
||||
pack_fixture_plugin_with_invalid_extension_entry() { :; }
|
||||
start_npm_fixture_registry() { :; }
|
||||
openclaw_plugins_cleanup_fixture_servers() { :; }
|
||||
|
||||
Reference in new issue
Block a user