test: add plugin install docker e2e lanes

This commit is contained in:
Peter Steinberger
2026-05-09 10:31:43 +01:00
parent 1f4f51e8e2
commit c443469452
11 changed files with 705 additions and 2 deletions

View File

@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
- Dependencies: refresh workspace dependency pins and lockfile, including `@openai/codex` `0.130.0`, `acpx` `0.7.0`, AWS SDK `3.1044.0`, OpenTelemetry `0.217.0`, `typebox` `1.1.38`, `vite` `8.0.11`, `oxfmt` `0.48.0`, and `oxlint` `1.63.0`, and update the Codex harness model snapshot for the new bundled app-server catalog.
- Plugins/install: add guarded plugin install overrides so onboarding and repair tests can route specific plugins to registry specs or local `npm pack` artifacts via environment variables.
- Tests/Docker: add Codex on-demand install and live plugin-tool dependency E2E lanes for packaged onboarding and npm-pack plugin proof.
- Plugins/ACPX: accept an optional `args` array in `agents.<name>` config so paths and flag values containing spaces stay intact when spawning ACP agent processes. Thanks @TheArchitectit and @BunsDev.
- Agents: inject the current provider/model identity into system prompts, including configured prompt overrides and CLI hook prompt replacements, so agents can answer model-identity questions from the actual runtime selection.
- Plugins/CLI: add the optional bundled `oc-path` plugin, providing `openclaw path` for surgical `oc://` access to markdown, JSONC, and JSONL workspace files.

View File

@@ -81,6 +81,14 @@ When debugging real providers/models (requires real creds):
`OPENCLAW_LIVE_CODEX_HARNESS_IMAGE_PROBE=0 OPENCLAW_LIVE_CODEX_HARNESS_MCP_PROBE=0 OPENCLAW_LIVE_CODEX_HARNESS_GUARDIAN_PROBE=0 OPENCLAW_LIVE_CODEX_HARNESS_SUBAGENT_PROBE=1 pnpm test:docker:live-codex-harness`.
This exits after the sub-agent probe unless
`OPENCLAW_LIVE_CODEX_HARNESS_SUBAGENT_ONLY=0` is set.
- Codex on-demand install smoke: `pnpm test:docker:codex-on-demand`
- Installs the packaged OpenClaw tarball in Docker, runs OpenAI API-key
onboarding, and verifies the Codex plugin plus `@openai/codex` dependency
were downloaded into the managed npm root on demand.
- Live plugin tool dependency smoke: `pnpm test:docker:live-plugin-tool`
- Packs a fixture plugin with a real `slugify` dependency, installs it through
`npm-pack:`, verifies the dependency under the managed npm root, then asks a
live OpenAI model to call the plugin tool and return the hidden slug.
- Crestodian rescue command smoke: `pnpm test:live:crestodian-rescue-channel`
- Opt-in belt-and-suspenders check for the message-channel rescue command
surface. It exercises `/crestodian status`, queues a persistent model

View File

@@ -1557,6 +1557,7 @@
"test:docker:live-codex-bind": "OPENCLAW_LIVE_CODEX_BIND=1 OPENCLAW_LIVE_CODEX_TEST_FILES=src/gateway/gateway-codex-bind.live.test.ts bash scripts/test-live-codex-harness-docker.sh",
"test:docker:live-codex-harness": "bash scripts/test-live-codex-harness-docker.sh",
"test:docker:live-codex-npm-plugin": "bash scripts/e2e/codex-npm-plugin-live-docker.sh",
"test:docker:live-plugin-tool": "bash scripts/e2e/live-plugin-tool-docker.sh",
"test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh",
"test:docker:live-gateway:claude": "OPENCLAW_LIVE_GATEWAY_PROVIDERS=claude-cli OPENCLAW_LIVE_GATEWAY_MODELS=claude-cli/claude-sonnet-4-6 bash scripts/test-live-gateway-models-docker.sh",
"test:docker:live-gateway:codex": "OPENCLAW_LIVE_GATEWAY_PROVIDERS=codex-cli OPENCLAW_LIVE_GATEWAY_MODELS=codex-cli/gpt-5.5 bash scripts/test-live-gateway-models-docker.sh",
@@ -1568,6 +1569,7 @@
"test:docker:live:all": "OPENCLAW_DOCKER_ALL_LIVE_MODE=only node scripts/test-docker-all.mjs",
"test:docker:local:all": "OPENCLAW_DOCKER_ALL_LIVE_MODE=skip node scripts/test-docker-all.mjs",
"test:docker:mcp-channels": "bash scripts/e2e/mcp-channels-docker.sh",
"test:docker:codex-on-demand": "bash scripts/e2e/codex-on-demand-docker.sh",
"test:docker:npm-onboard-channel-agent": "bash scripts/e2e/npm-onboard-channel-agent-docker.sh",
"test:docker:npm-onboard-discord-channel-agent": "OPENCLAW_NPM_ONBOARD_CHANNEL=discord bash scripts/e2e/npm-onboard-channel-agent-docker.sh",
"test:docker:npm-onboard-slack-channel-agent": "OPENCLAW_NPM_ONBOARD_CHANNEL=slack bash scripts/e2e/npm-onboard-channel-agent-docker.sh",

View File

@@ -14,7 +14,7 @@ const packageJson = JSON.parse(readText("package.json"));
const packageScripts = new Set(Object.keys(packageJson.scripts ?? {}));
// These lanes prove package-installed surfaces against live auth, so they
// intentionally need both live credentials and a package-backed image.
const livePackageBackedLanes = new Set(["live-codex-npm-plugin", "openwebui"]);
const livePackageBackedLanes = new Set(["live-codex-npm-plugin", "live-plugin-tool", "openwebui"]);
function readText(relativePath) {
return fs.readFileSync(path.join(ROOT_DIR, relativePath), "utf8");

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Installs a prepared OpenClaw npm tarball in Docker, runs OpenAI onboarding,
# and verifies the Codex plugin plus @openai/codex dependency are downloaded on demand.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-codex-on-demand-e2e" OPENCLAW_CODEX_ON_DEMAND_E2E_IMAGE)"
DOCKER_TARGET="${OPENCLAW_CODEX_ON_DEMAND_DOCKER_TARGET:-bare}"
HOST_BUILD="${OPENCLAW_CODEX_ON_DEMAND_HOST_BUILD:-1}"
PACKAGE_TGZ="${OPENCLAW_CURRENT_PACKAGE_TGZ:-}"
docker_e2e_build_or_reuse "$IMAGE_NAME" codex-on-demand "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "$DOCKER_TARGET"
prepare_package_tgz() {
if [ -n "$PACKAGE_TGZ" ]; then
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz codex-on-demand "$PACKAGE_TGZ")"
return 0
fi
if [ "$HOST_BUILD" = "0" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then
echo "OPENCLAW_CODEX_ON_DEMAND_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ" >&2
exit 1
fi
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz codex-on-demand)"
}
prepare_package_tgz
docker_e2e_package_mount_args "$PACKAGE_TGZ"
run_log="$(docker_e2e_run_log codex-on-demand)"
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 codex-on-demand empty)"
echo "Running Codex on-demand Docker E2E..."
if ! docker_e2e_run_with_harness \
-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 -s >"$run_log" 2>&1 <<'EOF'; then
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}"
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
export npm_config_prefix="$NPM_CONFIG_PREFIX"
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
export NPM_CONFIG_CACHE="${NPM_CONFIG_CACHE:-$XDG_CACHE_HOME/npm}"
export npm_config_cache="$NPM_CONFIG_CACHE"
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
export OPENAI_API_KEY="sk-openclaw-codex-on-demand-e2e"
dump_debug_logs() {
local status="$1"
echo "Codex on-demand scenario failed with exit code $status" >&2
openclaw_e2e_dump_logs \
/tmp/openclaw-install.log \
/tmp/openclaw-onboard.json \
/tmp/openclaw-plugins-list.json \
/tmp/openclaw-codex-inspect.json
}
trap 'status=$?; dump_debug_logs "$status"; exit "$status"' ERR
mkdir -p "$NPM_CONFIG_PREFIX" "$XDG_CACHE_HOME" "$NPM_CONFIG_CACHE"
chmod 700 "$XDG_CACHE_HOME" "$NPM_CONFIG_CACHE" || true
openclaw_e2e_install_package /tmp/openclaw-install.log
command -v openclaw >/dev/null
openclaw_e2e_assert_dep_absent "@openclaw/codex" "$HOME/.openclaw" "$NPM_CONFIG_PREFIX"
openclaw_e2e_assert_dep_absent "@openai/codex" "$HOME/.openclaw" "$NPM_CONFIG_PREFIX"
echo "Running non-interactive OpenAI onboarding; Codex should install on demand..."
openclaw onboard --non-interactive --accept-risk \
--mode local \
--auth-choice openai-api-key \
--secret-input-mode ref \
--skip-daemon \
--skip-ui \
--skip-channels \
--skip-skills \
--skip-health \
--json >/tmp/openclaw-onboard.json
openclaw plugins list --json >/tmp/openclaw-plugins-list.json
openclaw plugins inspect codex --runtime --json >/tmp/openclaw-codex-inspect.json
node scripts/e2e/lib/codex-on-demand/assertions.mjs
echo "Codex on-demand Docker E2E passed"
EOF
docker_e2e_print_log "$run_log"
rm -f "$run_log"
exit 1
fi
rm -f "$run_log"
echo "Codex on-demand Docker E2E passed"

View File

@@ -0,0 +1,117 @@
import fs from "node:fs";
import path from "node:path";
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
function stateDir() {
return process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME, ".openclaw");
}
function configPath() {
return process.env.OPENCLAW_CONFIG_PATH || path.join(stateDir(), "openclaw.json");
}
function realPathMaybe(filePath) {
try {
return fs.realpathSync(filePath);
} catch {
return path.resolve(filePath);
}
}
function assertPathInside(parentPath, childPath, label) {
const parent = realPathMaybe(parentPath);
const child = realPathMaybe(childPath);
const relative = path.relative(parent, child);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error(`${label} resolved outside ${parentPath}: ${child}`);
}
}
function installRecords() {
const indexPath = path.join(stateDir(), "plugins", "installs.json");
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
return index.installRecords || index.records || cfg.plugins?.installs || {};
}
function findPackageJson(packageName, roots) {
const packagePath = packageName.startsWith("@")
? path.join(...packageName.split("/"), "package.json")
: path.join(packageName, "package.json");
const candidates = roots.map((root) => path.join(root, "node_modules", packagePath));
return candidates.find((candidate) => fs.existsSync(candidate));
}
const cfg = readJson(configPath());
const inspect = readJson("/tmp/openclaw-codex-inspect.json");
const records = installRecords();
const codexRecord = records.codex || inspect.install;
if (!codexRecord) {
throw new Error(`missing codex install record: ${JSON.stringify(records)}`);
}
if (codexRecord.source !== "npm") {
throw new Error(`expected npm codex install record, got ${codexRecord.source}`);
}
if (!String(codexRecord.spec || "").includes("@openclaw/codex")) {
throw new Error(`expected @openclaw/codex install spec, got ${codexRecord.spec}`);
}
const npmRoot = path.join(stateDir(), "npm");
const installPath = String(codexRecord.installPath || "").replace(/^~(?=$|\/)/u, process.env.HOME);
if (!installPath) {
throw new Error(`missing codex installPath: ${JSON.stringify(codexRecord)}`);
}
assertPathInside(npmRoot, installPath, "codex install path");
const codexPackageJson = path.join(installPath, "package.json");
if (!fs.existsSync(codexPackageJson)) {
throw new Error(`missing npm-installed @openclaw/codex package: ${codexPackageJson}`);
}
const codexPackage = readJson(codexPackageJson);
if (codexPackage.name !== "@openclaw/codex") {
throw new Error(`unexpected codex package name: ${codexPackage.name}`);
}
const openAiCodexPackageJson = findPackageJson("@openai/codex", [installPath, npmRoot]);
if (!openAiCodexPackageJson) {
throw new Error("missing @openai/codex dependency under managed npm root");
}
assertPathInside(npmRoot, openAiCodexPackageJson, "@openai/codex dependency");
const list = readJson("/tmp/openclaw-plugins-list.json");
const plugin = (list.plugins || []).find((entry) => entry.id === "codex");
if (!plugin || plugin.enabled !== true || plugin.status !== "loaded") {
throw new Error(`codex plugin was not enabled+loaded: ${JSON.stringify(plugin)}`);
}
if (inspect.plugin?.id !== "codex" || inspect.plugin?.status !== "loaded") {
throw new Error(`unexpected codex inspect state: ${JSON.stringify(inspect.plugin)}`);
}
const hasHarness =
(Array.isArray(inspect.plugin?.agentHarnessIds) &&
inspect.plugin.agentHarnessIds.includes("codex")) ||
(Array.isArray(inspect.capabilities) &&
inspect.capabilities.some(
(entry) => entry?.kind === "agent-harness" && entry.ids?.includes("codex"),
));
if (!hasHarness) {
throw new Error(`codex harness was not registered: ${JSON.stringify(inspect.plugin)}`);
}
const primaryModel = cfg.agents?.defaults?.model?.primary;
if (primaryModel !== "openai/gpt-5.5") {
throw new Error(`expected OpenAI onboarding model openai/gpt-5.5, got ${primaryModel}`);
}
const providerRuntime = cfg.models?.providers?.openai?.agentRuntime?.id;
if (providerRuntime && providerRuntime !== "codex") {
throw new Error(`unexpected OpenAI provider runtime: ${providerRuntime}`);
}
const authPath = path.join(stateDir(), "agents", "main", "agent", "auth-profiles.json");
const authRaw = fs.readFileSync(authPath, "utf8");
if (!authRaw.includes("OPENAI_API_KEY")) {
throw new Error("auth profile did not persist OPENAI_API_KEY env ref");
}
if (authRaw.includes("sk-openclaw-codex-on-demand-e2e")) {
throw new Error("auth profile persisted the raw OpenAI test key");
}

View File

@@ -0,0 +1,269 @@
import fs from "node:fs";
import path from "node:path";
const command = process.argv[2];
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`missing ${name}`);
}
return value;
}
function stateDir() {
return process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME, ".openclaw");
}
function configPath() {
return process.env.OPENCLAW_CONFIG_PATH || path.join(stateDir(), "openclaw.json");
}
function realPathMaybe(filePath) {
try {
return fs.realpathSync(filePath);
} catch {
return path.resolve(filePath);
}
}
function assertPathInside(parentPath, childPath, label) {
const parent = realPathMaybe(parentPath);
const child = realPathMaybe(childPath);
const relative = path.relative(parent, child);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error(`${label} resolved outside ${parentPath}: ${child}`);
}
}
function writeJson(file, value) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
}
function installRecords() {
const indexPath = path.join(stateDir(), "plugins", "installs.json");
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
const cfg = fs.existsSync(configPath()) ? readJson(configPath()) : {};
return index.installRecords || index.records || cfg.plugins?.installs || {};
}
function pluginInstallPath() {
const pluginId = requireEnv("PLUGIN_ID");
const inspect = fs.existsSync("/tmp/openclaw-plugin-inspect.json")
? readJson("/tmp/openclaw-plugin-inspect.json")
: {};
const record = installRecords()[pluginId] || inspect.install;
if (!record) {
throw new Error(`missing ${pluginId} install record`);
}
if (record.source !== "npm" || record.artifactKind !== "npm-pack") {
throw new Error(`expected npm-pack install record: ${JSON.stringify(record)}`);
}
return String(record.installPath || "").replace(/^~(?=$|\/)/u, process.env.HOME);
}
function writeFixture() {
const dir = process.argv[3];
if (!dir) {
throw new Error("write-fixture requires output dir");
}
const pluginId = requireEnv("PLUGIN_ID");
const pluginName = requireEnv("PLUGIN_NAME");
const version = requireEnv("PLUGIN_VERSION");
const toolName = requireEnv("TOOL_NAME");
const seed = requireEnv("SEED");
writeJson(path.join(dir, "package.json"), {
name: pluginName,
version,
dependencies: { slugify: "^1.6.6" },
openclaw: { extensions: ["./index.js"] },
});
writeJson(path.join(dir, "openclaw.plugin.json"), {
id: pluginId,
name: "E2E Slug Tool",
description: "Docker E2E plugin tool fixture",
activation: { onStartup: true },
contracts: { tools: [toolName] },
configSchema: { type: "object", additionalProperties: false },
});
fs.writeFileSync(
path.join(dir, "index.js"),
`const slugify = require("slugify");\n` +
`const value = slugify(${JSON.stringify(seed)}, { lower: true, strict: true });\n` +
`module.exports = {\n` +
` id: ${JSON.stringify(pluginId)},\n` +
` name: "E2E Slug Tool",\n` +
` register(api) {\n` +
` api.registerTool({\n` +
` name: ${JSON.stringify(toolName)},\n` +
` description: "Return the hidden Docker E2E slug generated by the plugin dependency.",\n` +
` parameters: { type: "object", properties: {}, additionalProperties: false },\n` +
` async execute() {\n` +
` return { content: [{ type: "text", text: value }] };\n` +
` },\n` +
` });\n` +
` },\n` +
`};\n`,
);
}
function configure() {
const modelRef = requireEnv("MODEL_REF");
const pluginId = requireEnv("PLUGIN_ID");
const toolName = requireEnv("TOOL_NAME");
const cfgPath = configPath();
const cfg = fs.existsSync(cfgPath) ? readJson(cfgPath) : {};
const [providerId, modelId] = modelRef.split("/");
if (providerId !== "openai" || !modelId) {
throw new Error(`live plugin tool E2E expects an openai/* model, got ${modelRef}`);
}
cfg.plugins = {
...cfg.plugins,
enabled: true,
allow: Array.from(new Set([...(cfg.plugins?.allow || []), "openai", pluginId])).toSorted(
(left, right) => left.localeCompare(right),
),
entries: {
...cfg.plugins?.entries,
openai: { ...cfg.plugins?.entries?.openai, enabled: true },
[pluginId]: { ...cfg.plugins?.entries?.[pluginId], enabled: true },
},
};
cfg.tools = {
...cfg.tools,
allow: [toolName],
};
cfg.models = {
...cfg.models,
mode: "merge",
providers: {
...cfg.models?.providers,
openai: {
...cfg.models?.providers?.openai,
api: "openai-responses",
baseUrl: (process.env.OPENAI_BASE_URL || "https://api.openai.com/v1").trim(),
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
agentRuntime: { id: "pi" },
timeoutSeconds: 300,
models: [
{
id: modelId,
name: modelId,
api: "openai-responses",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
contextTokens: 96000,
maxTokens: 512,
},
],
},
},
};
cfg.agents = {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
model: { primary: modelRef, fallbacks: [] },
models: {
...cfg.agents?.defaults?.models,
[modelRef]: {
...cfg.agents?.defaults?.models?.[modelRef],
agentRuntime: { id: "pi" },
params: { transport: "sse", openaiWsWarmup: false },
},
},
workspace: path.join(stateDir(), "workspace"),
skipBootstrap: true,
timeoutSeconds: 420,
},
};
writeJson(cfgPath, cfg);
}
function findDependencyPackageJson(packageName) {
const installPath = pluginInstallPath();
const npmRoot = path.join(stateDir(), "npm");
return [
path.join(installPath, "node_modules", packageName, "package.json"),
path.join(npmRoot, "node_modules", packageName, "package.json"),
].find((candidate) => fs.existsSync(candidate));
}
function assertInstalled() {
const pluginId = requireEnv("PLUGIN_ID");
const pluginName = requireEnv("PLUGIN_NAME");
const toolName = requireEnv("TOOL_NAME");
const npmRoot = path.join(stateDir(), "npm");
const installPath = pluginInstallPath();
assertPathInside(npmRoot, installPath, "fixture plugin install path");
const packageJson = path.join(installPath, "package.json");
if (!fs.existsSync(packageJson)) {
throw new Error(`missing fixture plugin package.json: ${packageJson}`);
}
const pkg = readJson(packageJson);
if (pkg.name !== pluginName) {
throw new Error(`unexpected fixture package name: ${pkg.name}`);
}
const slugifyPackageJson = findDependencyPackageJson("slugify");
if (!slugifyPackageJson) {
throw new Error("missing slugify dependency installed by npm-pack plugin install");
}
assertPathInside(npmRoot, slugifyPackageJson, "slugify dependency");
const list = readJson("/tmp/openclaw-plugins-list.json");
const plugin = (list.plugins || []).find((entry) => entry.id === pluginId);
if (!plugin || plugin.enabled !== true || plugin.status !== "loaded") {
throw new Error(`fixture plugin was not enabled+loaded: ${JSON.stringify(plugin)}`);
}
const inspect = readJson("/tmp/openclaw-plugin-inspect.json");
const toolNames = Array.isArray(inspect.tools)
? inspect.tools.flatMap((entry) => (Array.isArray(entry?.names) ? entry.names : []))
: [];
if (!toolNames.includes(toolName)) {
throw new Error(`fixture tool was not registered: ${JSON.stringify(inspect.tools)}`);
}
}
function assertAgentTurn() {
const expected = requireEnv("EXPECTED_SLUG");
const toolName = requireEnv("TOOL_NAME");
const stdout = fs.readFileSync("/tmp/openclaw-agent.json", "utf8");
const stderr = fs.existsSync("/tmp/openclaw-agent.err")
? fs.readFileSync("/tmp/openclaw-agent.err", "utf8")
: "";
const response = JSON.parse(stdout);
const text = (response.payloads || []).map((payload) => payload?.text || "").join("\n");
if (!text.includes(expected)) {
throw new Error(
`live agent reply did not contain tool slug ${expected}:\nstdout=${stdout}\nstderr=${stderr}`,
);
}
const sessionsDir = path.join(stateDir(), "agents", "main", "sessions");
const sessionFiles = fs
.readdirSync(sessionsDir, { recursive: true })
.map((entry) => path.join(sessionsDir, String(entry)))
.filter((entry) => entry.endsWith(".jsonl") && fs.existsSync(entry));
const transcript = sessionFiles.map((file) => fs.readFileSync(file, "utf8")).join("\n");
if (!transcript.includes(toolName) || !transcript.includes(expected)) {
throw new Error(
`session transcript did not show ${toolName} returning ${expected}; checked ${sessionFiles.join(", ")}`,
);
}
}
const commands = {
"write-fixture": writeFixture,
configure,
"assert-installed": assertInstalled,
"assert-agent-turn": assertAgentTurn,
};
const fn = commands[command];
if (!fn) {
throw new Error(`unknown live plugin tool assertion command: ${command}`);
}
fn();

View File

@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# Installs a packed plugin with a real npm dependency, exposes its tool to a
# live OpenAI agent turn, and verifies the model received the dependency-made string.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-live-plugin-tool-e2e" OPENCLAW_LIVE_PLUGIN_TOOL_E2E_IMAGE)"
DOCKER_TARGET="${OPENCLAW_LIVE_PLUGIN_TOOL_DOCKER_TARGET:-bare}"
HOST_BUILD="${OPENCLAW_LIVE_PLUGIN_TOOL_HOST_BUILD:-1}"
PACKAGE_TGZ="${OPENCLAW_CURRENT_PACKAGE_TGZ:-}"
PROFILE_FILE="${OPENCLAW_LIVE_PLUGIN_TOOL_PROFILE_FILE:-${OPENCLAW_TESTBOX_PROFILE_FILE:-$HOME/.openclaw-testbox-live.profile}}"
if [ ! -f "$PROFILE_FILE" ] && [ -f "$HOME/.profile" ]; then
PROFILE_FILE="$HOME/.profile"
fi
docker_e2e_build_or_reuse "$IMAGE_NAME" live-plugin-tool "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "$DOCKER_TARGET"
prepare_package_tgz() {
if [ -n "$PACKAGE_TGZ" ]; then
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz live-plugin-tool "$PACKAGE_TGZ")"
return 0
fi
if [ "$HOST_BUILD" = "0" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then
echo "OPENCLAW_LIVE_PLUGIN_TOOL_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ" >&2
exit 1
fi
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz live-plugin-tool)"
}
prepare_package_tgz
PROFILE_MOUNT=()
PROFILE_STATUS="none"
if [ -f "$PROFILE_FILE" ] && [ -r "$PROFILE_FILE" ]; then
set -a
# shellcheck disable=SC1090
source "$PROFILE_FILE"
set +a
PROFILE_MOUNT=(-v "$PROFILE_FILE":/home/appuser/.profile:ro)
PROFILE_STATUS="$PROFILE_FILE"
fi
docker_e2e_package_mount_args "$PACKAGE_TGZ"
run_log="$(docker_e2e_run_log live-plugin-tool)"
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 live-plugin-tool empty)"
echo "Running live plugin tool Docker E2E..."
echo "Profile file: $PROFILE_STATUS"
if ! docker_e2e_run_with_harness \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e OPENAI_API_KEY \
-e OPENAI_BASE_URL \
-e OPENCLAW_LIVE_PLUGIN_TOOL_MODEL="${OPENCLAW_LIVE_PLUGIN_TOOL_MODEL:-openai/gpt-5.4-mini}" \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
"${PROFILE_MOUNT[@]}" \
-i "$IMAGE_NAME" bash -s >"$run_log" 2>&1 <<'EOF'; then
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}"
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
export npm_config_prefix="$NPM_CONFIG_PREFIX"
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
export NPM_CONFIG_CACHE="${NPM_CONFIG_CACHE:-$XDG_CACHE_HOME/npm}"
export npm_config_cache="$NPM_CONFIG_CACHE"
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
export OPENCLAW_AGENT_HARNESS_FALLBACK=none
for profile_path in "$HOME/.profile" /home/appuser/.profile; do
if [ -f "$profile_path" ] && [ -r "$profile_path" ]; then
set +e +u
source "$profile_path"
set -euo pipefail
break
fi
done
if [ -z "${OPENAI_API_KEY:-}" ]; then
echo "ERROR: OPENAI_API_KEY was not available after sourcing ~/.profile." >&2
exit 1
fi
export OPENAI_API_KEY
if [ -n "${OPENAI_BASE_URL:-}" ]; then
export OPENAI_BASE_URL
fi
MODEL_REF="${OPENCLAW_LIVE_PLUGIN_TOOL_MODEL:?missing OPENCLAW_LIVE_PLUGIN_TOOL_MODEL}"
PLUGIN_ID="e2e-slug-tool"
PLUGIN_NAME="@openclaw/e2e-slug-tool"
PLUGIN_VERSION="0.0.0-e2e.1"
TOOL_NAME="e2e_slug_probe"
SEED="OpenClaw E2E Plugin Tool $(date +%s)-$RANDOM"
EXPECTED_SLUG="$(printf '%s' "$SEED" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')"
export MODEL_REF PLUGIN_ID PLUGIN_NAME PLUGIN_VERSION TOOL_NAME SEED EXPECTED_SLUG
dump_debug_logs() {
local status="$1"
echo "Live plugin tool scenario failed with exit code $status" >&2
openclaw_e2e_dump_logs \
/tmp/openclaw-install.log \
/tmp/openclaw-plugin-install.log \
/tmp/openclaw-plugin-enable.log \
/tmp/openclaw-plugins-list.json \
/tmp/openclaw-plugin-inspect.json \
/tmp/openclaw-agent.json \
/tmp/openclaw-agent.err
}
trap 'status=$?; dump_debug_logs "$status"; exit "$status"' ERR
mkdir -p "$NPM_CONFIG_PREFIX" "$XDG_CACHE_HOME" "$NPM_CONFIG_CACHE"
chmod 700 "$XDG_CACHE_HOME" "$NPM_CONFIG_CACHE" || true
openclaw_e2e_install_package /tmp/openclaw-install.log
command -v openclaw >/dev/null
fixture_dir="$(mktemp -d /tmp/openclaw-live-plugin-tool.XXXXXX)"
plugin_dir="$fixture_dir/package"
mkdir -p "$plugin_dir"
node scripts/e2e/lib/live-plugin-tool/assertions.mjs write-fixture "$plugin_dir"
plugin_pack="$(cd "$plugin_dir" && npm pack --pack-destination "$fixture_dir" --silent)"
plugin_tgz="$fixture_dir/$plugin_pack"
echo "Installing fixture plugin from npm-pack: $plugin_tgz"
openclaw plugins install "npm-pack:$plugin_tgz" --force >/tmp/openclaw-plugin-install.log 2>&1
node scripts/e2e/lib/live-plugin-tool/assertions.mjs configure
openclaw plugins enable "$PLUGIN_ID" >/tmp/openclaw-plugin-enable.log 2>&1
openclaw plugins list --json >/tmp/openclaw-plugins-list.json
openclaw plugins inspect "$PLUGIN_ID" --runtime --json >/tmp/openclaw-plugin-inspect.json
node scripts/e2e/lib/live-plugin-tool/assertions.mjs assert-installed
echo "Running live OpenAI agent turn that must call $TOOL_NAME..."
openclaw agent --local \
--agent main \
--session-id live-plugin-tool \
--model "$MODEL_REF" \
--message "Call the tool named ${TOOL_NAME}. Reply with only the exact text returned by that tool. Do not compute, transform, or explain it." \
--thinking off \
--timeout 420 \
--json >/tmp/openclaw-agent.json 2>/tmp/openclaw-agent.err
node scripts/e2e/lib/live-plugin-tool/assertions.mjs assert-agent-turn
echo "Live plugin tool Docker E2E passed"
EOF
docker_e2e_print_log "$run_log"
rm -f "$run_log"
exit 1
fi
rm -f "$run_log"
echo "Live plugin tool Docker E2E passed"

View File

@@ -331,7 +331,8 @@ function laneCredentialRequirements(poolLane) {
if (
poolLane.name === "openwebui" ||
poolLane.name === "openai-web-search-minimal" ||
poolLane.name === "live-codex-npm-plugin"
poolLane.name === "live-codex-npm-plugin" ||
poolLane.name === "live-plugin-tool"
) {
credentials.push("openai");
}

View File

@@ -182,6 +182,11 @@ export const mainLanes = [
stateScenario: "empty",
weight: 2,
}),
npmLane("codex-on-demand", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:codex-on-demand", {
resources: ["service"],
stateScenario: "empty",
weight: 3,
}),
npmLane(
"npm-onboard-channel-agent",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:npm-onboard-channel-agent",
@@ -360,6 +365,15 @@ export const tailLanes = [
weight: 3,
},
),
liveLane("live-plugin-tool", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-plugin-tool", {
cacheKey: "plugin-tool",
e2eImageKind: "bare",
provider: "openai",
resources: ["npm"],
stateScenario: "empty",
timeoutMs: 20 * 60 * 1000,
weight: 3,
}),
liveLane(
"live-cli-backend-codex",
liveDockerScriptCommand(

View File

@@ -593,6 +593,46 @@ describe("scripts/lib/docker-e2e-plan", () => {
});
});
it("plans the Codex on-demand onboarding lane as package-backed npm proof", () => {
const plan = planFor({ selectedLaneNames: ["codex-on-demand"] });
expect(plan.lanes).toEqual([
expect.objectContaining({
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:codex-on-demand",
imageKind: "bare",
live: false,
name: "codex-on-demand",
resources: ["docker", "npm", "service"],
stateScenario: "empty",
}),
]);
expect(plan.needs).toMatchObject({
bareImage: true,
package: true,
});
});
it("plans the live plugin tool lane as package-backed OpenAI proof", () => {
const plan = planFor({ selectedLaneNames: ["live-plugin-tool"] });
expect(plan.credentials).toEqual(["openai"]);
expect(plan.lanes).toEqual([
expect.objectContaining({
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-plugin-tool",
imageKind: "bare",
live: true,
name: "live-plugin-tool",
resources: ["docker", "live", "live:openai", "npm"],
stateScenario: "empty",
}),
]);
expect(plan.needs).toMatchObject({
bareImage: true,
liveImage: true,
package: true,
});
});
it("plans Open WebUI as a live-auth functional image lane", () => {
const plan = planFor({
includeOpenWebUI: true,