mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
test(acp): harden embedded bind live coverage
This commit is contained in:
@@ -308,10 +308,10 @@ Notes:
|
||||
- Overrides:
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENT=claude`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENT=codex`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_ACPX_COMMAND=/full/path/to/acpx`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND='npx -y @agentclientprotocol/claude-agent-acp@<version>'`
|
||||
- Notes:
|
||||
- This lane uses the gateway `chat.send` surface with admin-only synthetic originating-route fields so tests can attach message-channel context without pretending to deliver externally.
|
||||
- When `OPENCLAW_LIVE_ACP_BIND_ACPX_COMMAND` is unset, the test uses the configured/bundled acpx command. If your harness auth depends on env vars from `~/.profile`, prefer a custom `acpx` command that preserves provider env.
|
||||
- When `OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND` is unset, the test uses the embedded `acpx` plugin's built-in agent registry for the selected ACP harness agent.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -330,8 +330,8 @@ pnpm test:docker:live-acp-bind
|
||||
Docker notes:
|
||||
|
||||
- The Docker runner lives at `scripts/test-live-acp-bind-docker.sh`.
|
||||
- It sources `~/.profile`, copies the matching CLI auth home (`~/.claude` or `~/.codex`) into the container, installs `acpx` into a writable npm prefix, then installs the requested live CLI (`@anthropic-ai/claude-code` or `@openai/codex`) if missing.
|
||||
- Inside Docker, the runner sets `OPENCLAW_LIVE_ACP_BIND_ACPX_COMMAND=$HOME/.npm-global/bin/acpx` so acpx keeps provider env vars from the sourced profile available to the child harness CLI.
|
||||
- It sources `~/.profile`, copies the matching CLI auth home (`~/.claude` or `~/.codex`) into the container, then installs the requested live CLI (`@anthropic-ai/claude-code` or `@openai/codex`) if missing.
|
||||
- Inside Docker, the runner relies on the embedded `acpx` plugin's built-in agent registry and the sourced profile env; use `OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND` only when you need a custom harness command.
|
||||
|
||||
### Recommended live recipes
|
||||
|
||||
|
||||
@@ -10,12 +10,6 @@ WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
|
||||
PROFILE_FILE="${OPENCLAW_PROFILE_FILE:-$HOME/.profile}"
|
||||
CLI_TOOLS_DIR="${OPENCLAW_DOCKER_CLI_TOOLS_DIR:-$HOME/.cache/openclaw/docker-cli-tools}"
|
||||
ACP_AGENT="${OPENCLAW_LIVE_ACP_BIND_AGENT:-claude}"
|
||||
ACPX_VERSION="${OPENCLAW_DOCKER_ACPX_VERSION:-$(node -p "const pkg=require(process.argv[1]); process.stdout.write(String(pkg.dependencies?.acpx ?? ''))" "$ROOT_DIR/extensions/acpx/package.json")}"
|
||||
|
||||
if [[ -z "$ACPX_VERSION" ]]; then
|
||||
echo "Unable to resolve bundled ACPX version from extensions/acpx/package.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$ACP_AGENT" in
|
||||
claude)
|
||||
@@ -76,7 +70,7 @@ if ((${#AUTH_DIRS[@]} > 0)); then
|
||||
for auth_dir in "${AUTH_DIRS[@]}"; do
|
||||
host_path="$HOME/$auth_dir"
|
||||
if [[ -d "$host_path" ]]; then
|
||||
EXTERNAL_AUTH_MOUNTS+=(-v "$host_path":/home/node/"$auth_dir":ro)
|
||||
EXTERNAL_AUTH_MOUNTS+=(-v "$host_path":/host-auth/"$auth_dir":ro)
|
||||
fi
|
||||
done
|
||||
fi
|
||||
@@ -93,7 +87,18 @@ read -r -d '' LIVE_TEST_CMD <<'EOF' || true
|
||||
set -euo pipefail
|
||||
[ -f "$HOME/.profile" ] && source "$HOME/.profile" || true
|
||||
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||
IFS=',' read -r -a auth_dirs <<<"${OPENCLAW_DOCKER_AUTH_DIRS_RESOLVED:-}"
|
||||
IFS=',' read -r -a auth_files <<<"${OPENCLAW_DOCKER_AUTH_FILES_RESOLVED:-}"
|
||||
if ((${#auth_dirs[@]} > 0)); then
|
||||
for auth_dir in "${auth_dirs[@]}"; do
|
||||
[ -n "$auth_dir" ] || continue
|
||||
if [ -d "/host-auth/$auth_dir" ]; then
|
||||
mkdir -p "$HOME/$auth_dir"
|
||||
cp -R "/host-auth/$auth_dir/." "$HOME/$auth_dir"
|
||||
chmod -R u+rwX "$HOME/$auth_dir" || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if ((${#auth_files[@]} > 0)); then
|
||||
for auth_file in "${auth_files[@]}"; do
|
||||
[ -n "$auth_file" ] || continue
|
||||
@@ -103,9 +108,6 @@ if ((${#auth_files[@]} > 0)); then
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ ! -x "$HOME/.npm-global/bin/acpx" ]; then
|
||||
npm_config_prefix="$HOME/.npm-global" npm install -g "acpx@${OPENCLAW_DOCKER_ACPX_VERSION}"
|
||||
fi
|
||||
agent="${OPENCLAW_LIVE_ACP_BIND_AGENT:-claude}"
|
||||
case "$agent" in
|
||||
claude)
|
||||
@@ -162,7 +164,7 @@ elif [ -d /app/dist/extensions ]; then
|
||||
export OPENCLAW_BUNDLED_PLUGINS_DIR=/app/dist/extensions
|
||||
fi
|
||||
cd "$tmp_dir"
|
||||
export OPENCLAW_LIVE_ACP_BIND_ACPX_COMMAND="$HOME/.npm-global/bin/acpx"
|
||||
export OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND="${OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND:-}"
|
||||
pnpm test:live src/gateway/gateway-acp-bind.live.test.ts
|
||||
EOF
|
||||
|
||||
@@ -174,6 +176,7 @@ echo "==> Agent: $ACP_AGENT"
|
||||
echo "==> Auth dirs: ${AUTH_DIRS_CSV:-none}"
|
||||
echo "==> Auth files: ${AUTH_FILES_CSV:-none}"
|
||||
docker run --rm -t \
|
||||
-u node \
|
||||
--entrypoint bash \
|
||||
-e ANTHROPIC_API_KEY \
|
||||
-e ANTHROPIC_API_KEY_OLD \
|
||||
@@ -185,12 +188,12 @@ docker run --rm -t \
|
||||
-e NODE_OPTIONS=--disable-warning=ExperimentalWarning \
|
||||
-e OPENCLAW_SKIP_CHANNELS=1 \
|
||||
-e OPENCLAW_VITEST_FS_MODULE_CACHE=0 \
|
||||
-e OPENCLAW_DOCKER_ACPX_VERSION="$ACPX_VERSION" \
|
||||
-e OPENCLAW_DOCKER_AUTH_DIRS_RESOLVED="$AUTH_DIRS_CSV" \
|
||||
-e OPENCLAW_DOCKER_AUTH_FILES_RESOLVED="$AUTH_FILES_CSV" \
|
||||
-e OPENCLAW_LIVE_TEST=1 \
|
||||
-e OPENCLAW_LIVE_ACP_BIND=1 \
|
||||
-e OPENCLAW_LIVE_ACP_BIND_AGENT="$ACP_AGENT" \
|
||||
-e OPENCLAW_LIVE_ACP_BIND_ACPX_COMMAND="${OPENCLAW_LIVE_ACP_BIND_ACPX_COMMAND:-}" \
|
||||
-e OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND="${OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND:-}" \
|
||||
-v "$ROOT_DIR":/src:ro \
|
||||
-v "$CONFIG_DIR":/home/node/.openclaw \
|
||||
-v "$WORKSPACE_DIR":/home/node/.openclaw/workspace \
|
||||
|
||||
@@ -8,7 +8,12 @@ import { getAcpRuntimeBackend } from "../acp/runtime/registry.js";
|
||||
import { isLiveTestEnabled } from "../agents/live-test-helpers.js";
|
||||
import { clearRuntimeConfigSnapshot, loadConfig } from "../config/config.js";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import {
|
||||
pinActivePluginChannelRegistry,
|
||||
releasePinnedPluginChannelRegistry,
|
||||
} from "../plugins/runtime.js";
|
||||
import { extractFirstTextBlock } from "../shared/chat-message-content.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { sleep } from "../utils.js";
|
||||
import { GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||
import { GatewayClient } from "./client.js";
|
||||
@@ -21,6 +26,22 @@ const describeLive = LIVE && ACP_BIND_LIVE ? describe : describe.skip;
|
||||
const CONNECT_TIMEOUT_MS = 90_000;
|
||||
const LIVE_TIMEOUT_MS = 240_000;
|
||||
|
||||
function createSlackCurrentConversationBindingRegistry() {
|
||||
return createTestRegistry([
|
||||
{
|
||||
pluginId: "slack",
|
||||
source: "test",
|
||||
plugin: {
|
||||
id: "slack",
|
||||
meta: { aliases: [] },
|
||||
conversationBindings: {
|
||||
supportsCurrentConversationBinding: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function normalizeAcpAgent(raw: string | undefined): "claude" | "codex" {
|
||||
const normalized = raw?.trim().toLowerCase();
|
||||
if (normalized === "codex") {
|
||||
@@ -44,6 +65,11 @@ function extractAssistantTexts(messages: unknown[]): string[] {
|
||||
.filter((value): value is string => typeof value === "string" && value.trim().length > 0);
|
||||
}
|
||||
|
||||
function extractLastAssistantText(messages: unknown[]): string | null {
|
||||
const texts = extractAssistantTexts(messages);
|
||||
return texts.at(-1) ?? null;
|
||||
}
|
||||
|
||||
function extractSpawnedAcpSessionKey(texts: string[]): string | null {
|
||||
for (const text of texts) {
|
||||
const match = text.match(/Spawned ACP session (\S+) \(/);
|
||||
@@ -320,7 +346,8 @@ describeLive("gateway live (ACP bind)", () => {
|
||||
skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
|
||||
};
|
||||
const liveAgent = normalizeAcpAgent(process.env.OPENCLAW_LIVE_ACP_BIND_AGENT);
|
||||
const acpxCommand = process.env.OPENCLAW_LIVE_ACP_BIND_ACPX_COMMAND?.trim() || undefined;
|
||||
const agentCommandOverride =
|
||||
process.env.OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND?.trim() || undefined;
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-live-acp-bind-"));
|
||||
const tempStateDir = path.join(tempRoot, "state");
|
||||
const tempConfigPath = path.join(tempRoot, "openclaw.json");
|
||||
@@ -331,6 +358,7 @@ describeLive("gateway live (ACP bind)", () => {
|
||||
const conversationId = `user:${slackUserId}`;
|
||||
const accountId = "default";
|
||||
const followupNonce = randomBytes(4).toString("hex").toUpperCase();
|
||||
const memoryNonce = randomBytes(4).toString("hex").toUpperCase();
|
||||
|
||||
clearRuntimeConfigSnapshot();
|
||||
process.env.OPENCLAW_STATE_DIR = tempStateDir;
|
||||
@@ -343,6 +371,13 @@ describeLive("gateway live (ACP bind)", () => {
|
||||
|
||||
const cfg = loadConfig();
|
||||
const acpxEntry = cfg.plugins?.entries?.acpx;
|
||||
const existingAgentOverrides =
|
||||
typeof acpxEntry?.config === "object" &&
|
||||
acpxEntry.config &&
|
||||
typeof acpxEntry.config.agents === "object" &&
|
||||
acpxEntry.config.agents
|
||||
? acpxEntry.config.agents
|
||||
: undefined;
|
||||
const nextCfg = {
|
||||
...cfg,
|
||||
gateway: {
|
||||
@@ -373,10 +408,14 @@ describeLive("gateway live (ACP bind)", () => {
|
||||
...acpxEntry?.config,
|
||||
permissionMode: "approve-all",
|
||||
nonInteractivePermissions: "deny",
|
||||
...(acpxCommand
|
||||
...(agentCommandOverride
|
||||
? {
|
||||
command: acpxCommand,
|
||||
expectedVersion: "any",
|
||||
agents: {
|
||||
...existingAgentOverrides,
|
||||
[liveAgent]: {
|
||||
command: agentCommandOverride,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
@@ -402,6 +441,8 @@ describeLive("gateway live (ACP bind)", () => {
|
||||
timeoutMs: CONNECT_TIMEOUT_MS,
|
||||
});
|
||||
logLiveStep("gateway websocket connected");
|
||||
const channelRegistry = createSlackCurrentConversationBindingRegistry();
|
||||
pinActivePluginChannelRegistry(channelRegistry);
|
||||
|
||||
try {
|
||||
const { mainAssistantTexts, spawnedSessionKey } = await bindConversationAndWait({
|
||||
@@ -421,21 +462,39 @@ describeLive("gateway live (ACP bind)", () => {
|
||||
client,
|
||||
sessionKey: originalSessionKey,
|
||||
idempotencyKey: `idem-followup-${randomUUID()}`,
|
||||
message: `Please include the token ACP-BIND-${followupNonce} in your reply.`,
|
||||
message: `Reply with exactly this token and nothing else: ACP-BIND-${followupNonce}`,
|
||||
originatingChannel: "slack",
|
||||
originatingTo: conversationId,
|
||||
originatingAccountId: accountId,
|
||||
});
|
||||
logLiveStep("follow-up turn completed");
|
||||
|
||||
await sendChatAndWait({
|
||||
client,
|
||||
sessionKey: originalSessionKey,
|
||||
idempotencyKey: `idem-memory-${randomUUID()}`,
|
||||
message:
|
||||
"Reply with exactly two uppercase tokens separated by a single space: " +
|
||||
"first, the token from your immediately previous assistant reply; " +
|
||||
`second, ACP-BIND-MEMORY-${memoryNonce}. No extra text.`,
|
||||
originatingChannel: "slack",
|
||||
originatingTo: conversationId,
|
||||
originatingAccountId: accountId,
|
||||
});
|
||||
logLiveStep("memory follow-up turn completed");
|
||||
|
||||
const boundHistory = await client.request<{ messages?: unknown[] }>("chat.history", {
|
||||
sessionKey: spawnedSessionKey,
|
||||
limit: 12,
|
||||
limit: 16,
|
||||
});
|
||||
const assistantTexts = extractAssistantTexts(boundHistory.messages ?? []);
|
||||
const lastAssistantText = extractLastAssistantText(boundHistory.messages ?? []);
|
||||
expect(assistantTexts.join("\n\n")).toContain(`ACP-BIND-${followupNonce}`);
|
||||
expect(lastAssistantText).toContain(`ACP-BIND-${followupNonce}`);
|
||||
expect(lastAssistantText).toContain(`ACP-BIND-MEMORY-${memoryNonce}`);
|
||||
logLiveStep("bound session transcript contains follow-up token");
|
||||
} finally {
|
||||
releasePinnedPluginChannelRegistry(channelRegistry);
|
||||
clearRuntimeConfigSnapshot();
|
||||
await client.stopAndWait({ timeoutMs: 2_000 }).catch(() => {});
|
||||
await server.close();
|
||||
|
||||
@@ -105,6 +105,12 @@ describe("bundled plugin metadata", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps config schemas on all bundled plugin manifests", () => {
|
||||
for (const entry of listBundledPluginMetadata()) {
|
||||
expect(entry.manifest.configSchema).toEqual(expect.any(Object));
|
||||
}
|
||||
});
|
||||
|
||||
it("prefers built generated paths when present and falls back to source paths", () => {
|
||||
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-metadata-");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user