Files
openclaw/test/github-cli-preflight.e2e.test.ts
mushuiyu886andAyaan Zaidi ad3268eccc fix(github): preflight CLI before device authorization (#135301)
Closes #135028

## What Problem This Solves

Agents → Tools → GitHub Identity could start a GitHub device authorization when the Gateway host had no `gh` executable. The operator received a valid code even though later GitHub operations could not run.

## Root Cause

The system, agent, and personal authorization owners requested a device code before checking their required local executable.

## Fix

- Share one GitHub CLI availability check.
- Run it at both lifecycle boundaries before device-code or pending-state creation.
- Return installation guidance only for the typed missing-CLI failure.
- Keep unrelated authorization failures bounded and generic.
- Re-probe on each explicit retry so an install or removal under the same `PATH` is recognized immediately.

## Evidence

- Baseline main `ff6fb73f01205480c15c2863cd04ea0539380b2f`: the real CLI and isolated Gateway returned a device URL and user code without `gh`. Public run: https://github.com/openclaw/openclaw/actions/runs/33642962313
- Exact candidate `70d15484b198efbd6c804f0c73fc3af9ae362cf1`, tree `64942dc7265acba1490bcbd9531cfdcdb7ed1f86`: sanitized direct AWS proof passed 97 focused Gateway tests.
- The real agent-scope CLI → isolated Gateway E2E passed and returned installation guidance with no device URL or user code.
- The exact-tree changed-scope gate passed, including TypeScript, lint, dead-export, database, and import-cycle checks.
- Oxfmt 0.60.0 and `git diff --check` passed.

## Surface

- Production: +31/-2 lines, net +29.
- Tests: +140/-0 lines.
- The production growth adds the typed prerequisite boundary and safe operator-visible recovery message.

## ClawSweeper Rank-up Move

- Skip further production-line reduction. One shared helper owns the check, but both lifecycle calls must remain: the personal owner creates pending state before the shared device transport, while the system/agent owner has a separate record boundary. Moving the check later reintroduces the bug.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-09-02 21:32:16 +05:30

50 lines
1.6 KiB
TypeScript

// E2E: a Gateway host without gh rejects authorization before device-code issuance.
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { resolveExecutablePath } from "../src/infra/executable-path.js";
import {
createOpenClawTestInstance,
type OpenClawTestInstance,
} from "./helpers/openclaw-test-instance.js";
const TEST_TIMEOUT_MS = 180_000;
const instances: OpenClawTestInstance[] = [];
afterEach(async () => {
await Promise.allSettled(instances.splice(0).map((instance) => instance.cleanup()));
});
describe("GitHub CLI authorization preflight", () => {
it(
"returns installation guidance through the real CLI and Gateway without requesting a code",
{ timeout: TEST_TIMEOUT_MS },
async () => {
const gatewayPath = path.dirname(process.execPath);
expect(resolveExecutablePath("gh", { env: { PATH: gatewayPath } })).toBeUndefined();
const instance = await createOpenClawTestInstance({
name: "github-cli-preflight",
env: { PATH: gatewayPath, OPENCLAW_PATH_BOOTSTRAPPED: "1" },
});
instances.push(instance);
await instance.startGateway();
const result = await instance.cli([
"gateway",
"call",
"tools.github.authorize.start",
"--params",
'{"scope":"agent","agentId":"main"}',
"--json",
]);
const output = `${result.stdout}\n${result.stderr}`;
expect(output).toContain(
"GitHub CLI (`gh`) is required on the Gateway host. Install it and retry.",
);
expect(output).not.toContain("github.com/login/device");
expect(output).not.toContain("userCode");
},
);
});