diff --git a/.github/actions/docker-e2e-plan/action.yml b/.github/actions/docker-e2e-plan/action.yml index 1936cf613648..27ecb73a239d 100644 --- a/.github/actions/docker-e2e-plan/action.yml +++ b/.github/actions/docker-e2e-plan/action.yml @@ -140,19 +140,33 @@ runs: run: | set -euo pipefail credentials=",$CREDENTIALS," - if [[ "$credentials" == *",openai,"* ]]; then - [[ -n "${OPENAI_API_KEY:-}" ]] || { - echo "OPENAI_API_KEY is required for selected Docker E2E lanes." >&2 - exit 1 - } - fi - if [[ "$credentials" == *",anthropic,"* && -z "${ANTHROPIC_API_TOKEN:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then - echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for selected Docker E2E lanes." >&2 + require_any() { + local label="$1" + shift + local key + for key in "$@"; do + if [[ -n "${!key:-}" ]]; then + return 0 + fi + done + echo "Missing credential for ${label}: expected one of $*" >&2 exit 1 + } + if [[ "$credentials" == *",openai,"* ]]; then + require_any OpenAI OPENAI_API_KEY + fi + if [[ "$credentials" == *",codex,"* ]]; then + require_any Codex OPENCLAW_CODEX_AUTH_JSON + fi + if [[ "$credentials" == *",anthropic,"* ]]; then + require_any Anthropic ANTHROPIC_API_TOKEN ANTHROPIC_API_KEY OPENCLAW_CLAUDE_CREDENTIALS_JSON OPENCLAW_CLAUDE_JSON fi if [[ "$credentials" == *",factory,"* ]]; then - [[ -n "${FACTORY_API_KEY:-}" ]] || { - echo "FACTORY_API_KEY is required for selected Docker E2E lanes." >&2 - exit 1 - } + require_any Factory FACTORY_API_KEY + fi + if [[ "$credentials" == *",gemini,"* ]]; then + require_any Gemini GEMINI_API_KEY GOOGLE_API_KEY OPENCLAW_GEMINI_SETTINGS_JSON + fi + if [[ "$credentials" == *",opencode,"* ]]; then + require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY fi diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index d7a7bf2c3012..d74d7f499364 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -844,21 +844,35 @@ jobs: run: | set -euo pipefail credentials=",$CREDENTIALS," - if [[ "$credentials" == *",openai,"* ]]; then - [[ -n "${OPENAI_API_KEY:-}" ]] || { - echo "OPENAI_API_KEY is required for selected Docker E2E lanes." >&2 - exit 1 - } - fi - if [[ "$credentials" == *",anthropic,"* && -z "${ANTHROPIC_API_TOKEN:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then - echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for selected Docker E2E lanes." >&2 + require_any() { + local label="$1" + shift + local key + for key in "$@"; do + if [[ -n "${!key:-}" ]]; then + return 0 + fi + done + echo "Missing credential for ${label}: expected one of $*" >&2 exit 1 + } + if [[ "$credentials" == *",openai,"* ]]; then + require_any OpenAI OPENAI_API_KEY + fi + if [[ "$credentials" == *",codex,"* ]]; then + require_any Codex OPENCLAW_CODEX_AUTH_JSON + fi + if [[ "$credentials" == *",anthropic,"* ]]; then + require_any Anthropic ANTHROPIC_API_TOKEN ANTHROPIC_API_KEY OPENCLAW_CLAUDE_CREDENTIALS_JSON OPENCLAW_CLAUDE_JSON fi if [[ "$credentials" == *",factory,"* ]]; then - [[ -n "${FACTORY_API_KEY:-}" ]] || { - echo "FACTORY_API_KEY is required for selected Docker E2E lanes." >&2 - exit 1 - } + require_any Factory FACTORY_API_KEY + fi + if [[ "$credentials" == *",gemini,"* ]]; then + require_any Gemini GEMINI_API_KEY GOOGLE_API_KEY OPENCLAW_GEMINI_SETTINGS_JSON + fi + if [[ "$credentials" == *",opencode,"* ]]; then + require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY fi - name: Run Docker E2E chunk @@ -1088,21 +1102,35 @@ jobs: run: | set -euo pipefail credentials=",$CREDENTIALS," - if [[ "$credentials" == *",openai,"* ]]; then - [[ -n "${OPENAI_API_KEY:-}" ]] || { - echo "OPENAI_API_KEY is required for selected Docker E2E lanes." >&2 - exit 1 - } - fi - if [[ "$credentials" == *",anthropic,"* && -z "${ANTHROPIC_API_TOKEN:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then - echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for selected Docker E2E lanes." >&2 + require_any() { + local label="$1" + shift + local key + for key in "$@"; do + if [[ -n "${!key:-}" ]]; then + return 0 + fi + done + echo "Missing credential for ${label}: expected one of $*" >&2 exit 1 + } + if [[ "$credentials" == *",openai,"* ]]; then + require_any OpenAI OPENAI_API_KEY + fi + if [[ "$credentials" == *",codex,"* ]]; then + require_any Codex OPENCLAW_CODEX_AUTH_JSON + fi + if [[ "$credentials" == *",anthropic,"* ]]; then + require_any Anthropic ANTHROPIC_API_TOKEN ANTHROPIC_API_KEY OPENCLAW_CLAUDE_CREDENTIALS_JSON OPENCLAW_CLAUDE_JSON fi if [[ "$credentials" == *",factory,"* ]]; then - [[ -n "${FACTORY_API_KEY:-}" ]] || { - echo "FACTORY_API_KEY is required for selected Docker E2E lanes." >&2 - exit 1 - } + require_any Factory FACTORY_API_KEY + fi + if [[ "$credentials" == *",gemini,"* ]]; then + require_any Gemini GEMINI_API_KEY GOOGLE_API_KEY OPENCLAW_GEMINI_SETTINGS_JSON + fi + if [[ "$credentials" == *",opencode,"* ]]; then + require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY fi - name: Run targeted Docker E2E lanes diff --git a/scripts/lib/docker-e2e-plan.mjs b/scripts/lib/docker-e2e-plan.mjs index 863a60b34fcf..16fc89399f12 100644 --- a/scripts/lib/docker-e2e-plan.mjs +++ b/scripts/lib/docker-e2e-plan.mjs @@ -325,6 +325,7 @@ export function findLaneByName(name) { } function laneCredentialRequirements(poolLane) { + const resources = laneResources(poolLane); const credentials = []; if (poolLane.name === "install-e2e-openai") { credentials.push("openai"); @@ -332,17 +333,23 @@ function laneCredentialRequirements(poolLane) { if (poolLane.name === "install-e2e-anthropic") { credentials.push("anthropic"); } - if (poolLane.name === "live-acp-bind-droid") { + if (resources.includes("live:openai")) { + credentials.push("openai"); + } + if (resources.includes("live:codex")) { + credentials.push("codex"); + } + if (resources.includes("live:claude")) { + credentials.push("anthropic"); + } + if (resources.includes("live:droid")) { credentials.push("factory"); } - if ( - poolLane.name === "openwebui" || - poolLane.name === "openai-chat-tools" || - poolLane.name === "openai-web-search-minimal" || - poolLane.name === "live-codex-npm-plugin" || - poolLane.name === "live-plugin-tool" - ) { - credentials.push("openai"); + if (resources.includes("live:gemini")) { + credentials.push("gemini"); + } + if (resources.includes("live:opencode")) { + credentials.push("opencode"); } return credentials; } diff --git a/scripts/lib/docker-e2e-scenarios.mjs b/scripts/lib/docker-e2e-scenarios.mjs index 8d41ac2fd714..606fc442965e 100644 --- a/scripts/lib/docker-e2e-scenarios.mjs +++ b/scripts/lib/docker-e2e-scenarios.mjs @@ -485,7 +485,7 @@ export const tailLanes = [ ), liveLane("live-codex-harness", liveDockerScriptCommand("test-live-codex-harness-docker.sh"), { cacheKey: "codex-harness", - provider: "openai", + provider: "codex-cli", resources: ["npm"], timeoutMs: LIVE_ACP_TIMEOUT_MS, weight: 3, @@ -523,7 +523,7 @@ export const tailLanes = [ ), { cacheKey: "codex-harness", - provider: "openai", + provider: "codex-cli", resources: ["npm"], timeoutMs: LIVE_ACP_TIMEOUT_MS, weight: 3, @@ -547,8 +547,8 @@ export const tailLanes = [ liveDockerScriptCommand("test-live-acp-bind-docker.sh", "OPENCLAW_LIVE_ACP_BIND_AGENT=codex"), { cacheKey: "acp-bind-codex", - provider: "openai", - resources: ["npm"], + provider: "codex-cli", + resources: ["live:openai", "npm"], timeoutMs: LIVE_ACP_TIMEOUT_MS, weight: 3, }, diff --git a/scripts/test-live-codex-harness-docker.sh b/scripts/test-live-codex-harness-docker.sh index cc1437a644f6..9a58dd05f346 100644 --- a/scripts/test-live-codex-harness-docker.sh +++ b/scripts/test-live-codex-harness-docker.sh @@ -267,8 +267,8 @@ if ! "$NPM_CONFIG_PREFIX/bin/codex" exec \ --skip-git-repo-check \ "Reply exactly: $codex_preflight_token" >"$codex_preflight_log" 2>&1; then if grep -q "Failed to extract accountId from token" "$codex_preflight_log"; then - echo "SKIP: Codex auth cannot extract accountId from the available token; skipping live Codex harness lane." - exit 0 + echo "ERROR: Codex auth cannot extract accountId from the available token; refresh OPENCLAW_CODEX_AUTH_JSON or use OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key." >&2 + exit 1 fi cat "$codex_preflight_log" >&2 exit 1 diff --git a/test/scripts/docker-e2e-plan.test.ts b/test/scripts/docker-e2e-plan.test.ts index e10a87c6ab27..5b4ec31584bb 100644 --- a/test/scripts/docker-e2e-plan.test.ts +++ b/test/scripts/docker-e2e-plan.test.ts @@ -660,6 +660,7 @@ describe("scripts/lib/docker-e2e-plan", () => { it("plans a live-only selected lane without package e2e images", () => { const plan = planFor({ selectedLaneNames: ["live-models"] }); + expect(plan.credentials).toEqual(["anthropic", "gemini"]); expect(plan.lanes.map((lane) => lane.name)).toEqual(["live-models"]); expect(plan.needs).toEqual({ bareImage: false, @@ -670,6 +671,28 @@ describe("scripts/lib/docker-e2e-plan", () => { }); }); + it("derives live Docker credentials from lane resources", () => { + const cases = [ + { credentials: ["anthropic", "gemini"], name: "live-models" }, + { credentials: ["anthropic", "gemini"], name: "live-gateway" }, + { credentials: ["anthropic"], name: "live-cli-backend-claude" }, + { credentials: ["gemini"], name: "live-cli-backend-gemini" }, + { credentials: ["codex"], name: "live-codex-harness" }, + { credentials: ["openai"], name: "live-codex-media-path" }, + { credentials: ["openai"], name: "live-subagent-announce" }, + { credentials: ["codex"], name: "live-codex-bind" }, + { credentials: ["anthropic"], name: "live-acp-bind-claude" }, + { credentials: ["codex", "openai"], name: "live-acp-bind-codex" }, + { credentials: ["factory"], name: "live-acp-bind-droid" }, + { credentials: ["gemini"], name: "live-acp-bind-gemini" }, + { credentials: ["opencode"], name: "live-acp-bind-opencode" }, + ] as const; + + for (const { credentials, name } of cases) { + expect(planFor({ selectedLaneNames: [name] }).credentials, name).toEqual(credentials); + } + }); + it("plans the Codex npm plugin live lane as package-backed OpenAI proof", () => { const plan = planFor({ selectedLaneNames: ["live-codex-npm-plugin"] }); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 761e6639035c..796b094c37b5 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -633,7 +633,7 @@ describe("package artifact reuse", () => { ); }); - it("plumbs Factory credentials through planned Docker E2E live lanes", () => { + it("plumbs live credentials through planned Docker E2E live lanes", () => { const reusableWorkflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8"); const releaseChecksWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8"); const scheduledWorkflow = readFileSync(SCHEDULED_LIVE_CHECKS_WORKFLOW, "utf8"); @@ -645,9 +645,20 @@ describe("package artifact reuse", () => { expect(hydrateScript).toContain(" FACTORY_API_KEY \\"); expect(dockerPlanAction).toContain('if [[ "$credentials" == *",factory,"* ]]; then'); - expect(dockerPlanAction).toContain( - "FACTORY_API_KEY is required for selected Docker E2E lanes.", - ); + expectTextToIncludeAll(dockerPlanAction, [ + 'if [[ "$credentials" == *",openai,"* ]]; then', + "require_any OpenAI OPENAI_API_KEY", + 'if [[ "$credentials" == *",codex,"* ]]; then', + "require_any Codex OPENCLAW_CODEX_AUTH_JSON", + 'if [[ "$credentials" == *",anthropic,"* ]]; then', + "require_any Anthropic ANTHROPIC_API_TOKEN ANTHROPIC_API_KEY OPENCLAW_CLAUDE_CREDENTIALS_JSON OPENCLAW_CLAUDE_JSON", + 'if [[ "$credentials" == *",factory,"* ]]; then', + "require_any Factory FACTORY_API_KEY", + 'if [[ "$credentials" == *",gemini,"* ]]; then', + "require_any Gemini GEMINI_API_KEY GOOGLE_API_KEY OPENCLAW_GEMINI_SETTINGS_JSON", + 'if [[ "$credentials" == *",opencode,"* ]]; then', + "require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY", + ]); for (const workflow of [ reusableWorkflow, releaseChecksWorkflow, @@ -660,7 +671,16 @@ describe("package artifact reuse", () => { } expect(reusableWorkflow).toContain("FACTORY_API_KEY:\n required: false"); expect(packageAcceptanceWorkflow).toContain("FACTORY_API_KEY:\n required: false"); - expect(reusableWorkflow).toContain('if [[ "$credentials" == *",factory,"* ]]; then'); + expectTextToIncludeAll(reusableWorkflow, [ + 'if [[ "$credentials" == *",openai,"* ]]; then', + "require_any OpenAI OPENAI_API_KEY", + 'if [[ "$credentials" == *",codex,"* ]]; then', + "require_any Codex OPENCLAW_CODEX_AUTH_JSON", + 'if [[ "$credentials" == *",gemini,"* ]]; then', + "require_any Gemini GEMINI_API_KEY GOOGLE_API_KEY OPENCLAW_GEMINI_SETTINGS_JSON", + 'if [[ "$credentials" == *",opencode,"* ]]; then', + "require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY", + ]); }); it("allows the Telegram lane to run from reusable package acceptance artifacts", () => { diff --git a/test/scripts/test-live-codex-harness-docker.test.ts b/test/scripts/test-live-codex-harness-docker.test.ts index 33f800cbee0d..2db7e6679ddd 100644 --- a/test/scripts/test-live-codex-harness-docker.test.ts +++ b/test/scripts/test-live-codex-harness-docker.test.ts @@ -41,23 +41,17 @@ describe("scripts/test-live-codex-harness-docker.sh", () => { it("forwards API-key auth through both OpenAI and Codex env names", () => { const script = fs.readFileSync(SCRIPT_PATH, "utf8"); - expect(script).toContain('printf \'OPENAI_API_KEY=%s\\n\' "${OPENAI_API_KEY}"'); - expect(script).toContain('printf \'CODEX_API_KEY=%s\\n\' "${CODEX_API_KEY:-$OPENAI_API_KEY}"'); - expect(script.indexOf("OPENAI_API_KEY=%s")).toBeLessThan( - script.indexOf("CODEX_API_KEY=%s"), - ); + expect(script).toContain("printf 'OPENAI_API_KEY=%s\\n' \"${OPENAI_API_KEY}\""); + expect(script).toContain("printf 'CODEX_API_KEY=%s\\n' \"${CODEX_API_KEY:-$OPENAI_API_KEY}\""); + expect(script.indexOf("OPENAI_API_KEY=%s")).toBeLessThan(script.indexOf("CODEX_API_KEY=%s")); }); it("keeps API-key runs on the ephemeral Docker home", () => { const script = fs.readFileSync(SCRIPT_PATH, "utf8"); expect(script).toContain('DOCKER_USER="$(id -u):$(id -g)"'); - expect(script).toContain( - 'if [[ "$CODEX_HARNESS_AUTH_MODE" == "api-key" ]]; then', - ); - expect(script).toContain( - 'if [[ -z "${DOCKER_HOME_DIR:-}" ]]; then', - ); + expect(script).toContain('if [[ "$CODEX_HARNESS_AUTH_MODE" == "api-key" ]]; then'); + expect(script).toContain('if [[ -z "${DOCKER_HOME_DIR:-}" ]]; then'); expect(script).not.toContain('DOCKER_USER="0:0"'); expect(script).toContain( 'DOCKER_HOME_DIR="$(mktemp -d "${RUNNER_TEMP:-/tmp}/openclaw-docker-home.XXXXXX")"', @@ -74,9 +68,7 @@ describe("scripts/test-live-codex-harness-docker.sh", () => { expect(script).toContain( 'chmod 0777 "$DOCKER_HOME_DIR" "$CONFIG_DIR" "$WORKSPACE_DIR" || true', ); - expect(script).toContain( - 'if [[ "$CODEX_HARNESS_AUTH_MODE" != "api-key" ]]; then', - ); + expect(script).toContain('if [[ "$CODEX_HARNESS_AUTH_MODE" != "api-key" ]]; then'); expect(script.indexOf('PROFILE_STATUS="api-key-env"')).toBeLessThan( script.indexOf("openclaw_live_append_array DOCKER_RUN_ARGS PROFILE_MOUNT"), ); @@ -97,4 +89,17 @@ describe("scripts/test-live-codex-harness-docker.sh", () => { '-e OPENCLAW_LIVE_CODEX_BIND_PROVIDER="${OPENCLAW_LIVE_CODEX_BIND_PROVIDER:-}"', ); }); + + it("fails instead of skipping when Codex auth cannot identify an account", () => { + const script = fs.readFileSync(SCRIPT_PATH, "utf8"); + + expect(script).toContain("Failed to extract accountId from token"); + expect(script).toContain( + "ERROR: Codex auth cannot extract accountId from the available token; refresh OPENCLAW_CODEX_AUTH_JSON or use OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key.", + ); + expect(script).not.toContain( + "SKIP: Codex auth cannot extract accountId from the available token; skipping live Codex harness lane.", + ); + expect(script).not.toMatch(/Failed to extract accountId from token[\s\S]{0,180}exit 0/u); + }); });