Skip to content

Commit 09dc407

Browse files
committed
test(claude): verify docker-backed auth login
1 parent 19bd8c3 commit 09dc407

7 files changed

Lines changed: 154 additions & 3 deletions

File tree

.github/workflows/check.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,26 @@ jobs:
249249
- name: Login context notice
250250
run: bash scripts/e2e/login-context.sh
251251

252+
e2e-auth-claude-login:
253+
name: E2E (Claude auth login)
254+
runs-on: ubuntu-latest
255+
timeout-minutes: 40
256+
env:
257+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: "0"
258+
DOCKER_GIT_E2E_REUSE_WORKSPACE_INSTALL: "1"
259+
steps:
260+
- uses: actions/checkout@v6
261+
with:
262+
submodules: true
263+
- name: Install dependencies
264+
uses: ./.github/actions/setup
265+
- name: Free Docker disk
266+
uses: ./.github/actions/free-docker-disk
267+
- name: Docker info
268+
run: docker version && docker compose version
269+
- name: Claude auth login warning path
270+
run: bash scripts/e2e/auth-claude-login.sh
271+
252272
e2e-runtime-volumes-ssh:
253273
name: E2E (Runtime volumes + SSH)
254274
runs-on: ubuntu-latest

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ services:
2727
DOCKER_GIT_EXCHANGE_AGENT_COMMAND: ${DOCKER_GIT_EXCHANGE_AGENT_COMMAND:-}
2828
DOCKER_GIT_EXCHANGE_AGENT_TIMEOUT_MS: ${DOCKER_GIT_EXCHANGE_AGENT_TIMEOUT_MS:-3600000}
2929
DOCKER_GIT_OUTBOX_POLLING_INTERVAL_MS: ${DOCKER_GIT_OUTBOX_POLLING_INTERVAL_MS:-5000}
30+
DOCKER_GIT_CLAUDE_OAUTH_TOKEN: ${DOCKER_GIT_CLAUDE_OAUTH_TOKEN:-}
3031
ports:
3132
- "${DOCKER_GIT_API_BIND_HOST:-127.0.0.1}:${DOCKER_GIT_API_PORT:-3334}:${DOCKER_GIT_API_PORT:-3334}"
3233
dns:

packages/lib/src/usecases/auth-claude.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ export const authClaudeLogin = (
279279
yield* _(
280280
Effect.logWarning(
281281
`Claude OAuth token saved (${accountLabel}), but the API probe failed (exit=${probeExitCode}). ` +
282+
`Login is complete because the token was captured and persisted; live Claude API access is not yet verified. ` +
282283
`The token may need a moment to activate, or there was a transient network issue. ` +
283284
`Verify later with 'docker-git auth claude status'.`
284285
)

packages/lib/tests/usecases/auth-claude-login.test.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,22 @@ const setupTokenOutput = (token: string): string =>
2929
" Store this token securely. You won't be able to see it again."
3030
].join("\n")
3131

32+
const setupTokenOutputWithoutToken = (): string =>
33+
[
34+
"Welcome to Claude Code",
35+
"",
36+
" OAuth flow finished without printing a long-lived token.",
37+
""
38+
].join("\n")
39+
3240
const isSetupToken = (args: ReadonlyArray<string>): boolean => args.includes("setup-token")
3341
const isPingProbe = (args: ReadonlyArray<string>): boolean => args.includes("-p") && args.includes("ping")
3442

3543
// CHANGE: fake docker executor that captures a setup-token and lets the ping probe fail
3644
// WHY: reproduce issue-439 where a successful OAuth login was discarded by a failing probe
3745
// REF: issue-439
3846
const makeFakeExecutor = (
39-
token: string,
47+
token: string | null,
4048
pingExitCode: number
4149
): CommandExecutor.CommandExecutor => {
4250
const start = (command: Command.Command): Effect.Effect<CommandExecutor.Process, never> =>
@@ -45,7 +53,11 @@ const makeFakeExecutor = (
4553
const invocation = flattened[flattened.length - 1]!
4654
const args = invocation.args
4755

48-
const stdoutText = isSetupToken(args) ? setupTokenOutput(token) : ""
56+
const stdoutText = isSetupToken(args)
57+
? token === null
58+
? setupTokenOutputWithoutToken()
59+
: setupTokenOutput(token)
60+
: ""
4961
const exitCode = isPingProbe(args) ? pingExitCode : 0
5062
const stdout = stdoutText.length === 0 ? Stream.empty : Stream.succeed(encode(stdoutText))
5163

@@ -136,6 +148,34 @@ const runLoginAndReadToken = (
136148
return yield* _(fs.readFileString(path.join(claudeAuthPath, "default", ".oauth-token")))
137149
})
138150

151+
const runLoginWithoutCapturedToken = (
152+
root: string
153+
): Effect.Effect<void, unknown, FileSystem.FileSystem | Path.Path> =>
154+
Effect.gen(function*(_) {
155+
const fs = yield* _(FileSystem.FileSystem)
156+
const path = yield* _(Path.Path)
157+
const claudeAuthPath = path.join(root, ".docker-git/.orch/auth/claude")
158+
const tokenPath = path.join(claudeAuthPath, "default", ".oauth-token")
159+
160+
const error = yield* _(
161+
authClaudeLogin({
162+
_tag: "AuthClaudeLogin",
163+
label: null,
164+
claudeAuthPath
165+
}).pipe(
166+
Effect.provideService(CommandExecutor.CommandExecutor, makeFakeExecutor(null, 0)),
167+
Effect.flip
168+
)
169+
)
170+
171+
expect(error._tag).toBe("AuthError")
172+
if (error._tag === "AuthError") {
173+
expect(error.message).toContain("without a captured token")
174+
}
175+
const hasTokenFile = yield* _(fs.exists(tokenPath))
176+
expect(hasTokenFile).toBe(false)
177+
})
178+
139179
describe("authClaudeLogin", () => {
140180
// Regression for issue-439: a non-zero probe exit must not discard a created token.
141181
it.effect("persists the OAuth token even when the post-login API probe fails", () =>
@@ -159,4 +199,14 @@ describe("authClaudeLogin", () => {
159199
})
160200
)
161201
).pipe(Effect.provide(NodeContext.layer)))
202+
203+
it.effect("fails when setup-token completes without a captured OAuth token", () =>
204+
withTempDir((root) =>
205+
withPatchedEnv(
206+
{ HOME: root, DOCKER_GIT_STATE_AUTO_SYNC: "0", DOCKER_GIT_PROJECTS_ROOT: undefined },
207+
Effect.gen(function*(_) {
208+
yield* _(runLoginWithoutCapturedToken(root))
209+
})
210+
)
211+
).pipe(Effect.provide(NodeContext.layer)))
162212
})

scripts/e2e/_lib.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ exec sudo -n env \
4242
"DOCKER_GIT_PROJECTS_ROOT_VOLUME=${DOCKER_GIT_PROJECTS_ROOT_VOLUME:-}" \
4343
"DOCKER_GIT_PROJECT_DOCKER_HOST=${DOCKER_GIT_PROJECT_DOCKER_HOST:-}" \
4444
"DOCKER_GIT_PROJECT_SSH_BIND_HOST=${DOCKER_GIT_PROJECT_SSH_BIND_HOST:-}" \
45+
"DOCKER_GIT_CLAUDE_OAUTH_TOKEN=${DOCKER_GIT_CLAUDE_OAUTH_TOKEN:-}" \
4546
"UBUNTU_APT_MIRROR=${UBUNTU_APT_MIRROR:-}" \
4647
docker "$@"
4748
EOF

scripts/e2e/auth-claude-login.sh

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
RUN_ID="$(date +%s)-$RANDOM"
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
7+
source "$REPO_ROOT/scripts/e2e/_lib.sh"
8+
9+
ROOT_BASE="${DOCKER_GIT_E2E_ROOT_BASE:-/tmp/docker-git-e2e-root}"
10+
mkdir -p "$ROOT_BASE"
11+
ROOT="$(mktemp -d "$ROOT_BASE/auth-claude-login.XXXXXX")"
12+
chmod 0777 "$ROOT"
13+
KEEP="${KEEP:-0}"
14+
15+
export DOCKER_GIT_PROJECTS_ROOT="$ROOT"
16+
export DOCKER_GIT_STATE_AUTO_SYNC=0
17+
export DOCKER_GIT_API_CONTAINER_NAME="docker-git-e2e-auth-claude-$RUN_ID-api"
18+
export DOCKER_GIT_PROJECTS_ROOT_VOLUME="docker-git-e2e-auth-claude-$RUN_ID-projects"
19+
export COMPOSE_PROJECT_NAME="docker-git-e2e-auth-claude-$RUN_ID"
20+
export DOCKER_GIT_CLAUDE_OAUTH_TOKEN="${DOCKER_GIT_CLAUDE_OAUTH_TOKEN:-sk-ant-oat01-DOCKER-GIT-E2E-FAKE-TOKEN-000000000000}"
21+
22+
LOG_FILE="/tmp/docker-git-auth-claude-login-$RUN_ID.log"
23+
24+
fail() {
25+
echo "e2e/auth-claude-login: $*" >&2
26+
exit 1
27+
}
28+
29+
on_error() {
30+
local line="$1"
31+
echo "e2e/auth-claude-login: failed at line $line" >&2
32+
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | head -n 80 || true
33+
docker logs "$DOCKER_GIT_API_CONTAINER_NAME" --tail 200 || true
34+
(cd "$REPO_ROOT" && docker compose ps) || true
35+
(cd "$REPO_ROOT" && docker compose logs --no-color --tail 200) || true
36+
}
37+
38+
cleanup() {
39+
(cd "$REPO_ROOT" && docker compose down -v --remove-orphans) >/dev/null 2>&1 || true
40+
if [[ "$KEEP" == "1" ]]; then
41+
echo "e2e/auth-claude-login: KEEP=1 set; preserving temp dir: $ROOT" >&2
42+
echo "e2e/auth-claude-login: log file: $LOG_FILE" >&2
43+
return
44+
fi
45+
rm -rf "$ROOT" >/dev/null 2>&1 || true
46+
rm -f "$LOG_FILE" >/dev/null 2>&1 || true
47+
}
48+
49+
trap 'on_error $LINENO' ERR
50+
trap cleanup EXIT
51+
52+
command -v timeout >/dev/null 2>&1 || fail "missing 'timeout' command"
53+
54+
dg_ensure_docker "$ROOT/.e2e-bin"
55+
dg_prepare_docker_git_cli "$REPO_ROOT" "$ROOT/.e2e-bin"
56+
57+
set +e
58+
timeout 180s bash -lc 'cd "$1" && bun packages/app/dist/src/docker-git/main.js auth claude login' bash "$REPO_ROOT" \
59+
>"$LOG_FILE" 2>&1
60+
login_exit=$?
61+
set -e
62+
63+
if [[ "$login_exit" -ne 0 ]]; then
64+
cat "$LOG_FILE" >&2 || true
65+
fail "docker-git auth claude login failed (exit: $login_exit)"
66+
fi
67+
68+
grep -Fq -- "Claude OAuth token saved" "$LOG_FILE" \
69+
|| fail "expected saved-token warning in auth claude login output"
70+
71+
grep -Fq -- "live Claude API access is not yet verified" "$LOG_FILE" \
72+
|| fail "expected diagnostic API probe warning in auth claude login output"
73+
74+
docker exec "$DOCKER_GIT_API_CONTAINER_NAME" \
75+
test -s "$ROOT/.orch/auth/claude/default/.oauth-token" \
76+
|| fail "expected persisted Claude OAuth token in controller state"
77+
78+
echo "e2e/auth-claude-login: docker-backed Claude login warning path verified" >&2

scripts/e2e/run-all.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
55

66
cases=("$@")
77
if [[ "${#cases[@]}" -eq 0 ]]; then
8-
cases=("local-package-cli" "browser-command" "clone-cache" "login-context" "runtime-volumes-ssh" "clone-auto-open-ssh" "opencode-autoconnect")
8+
cases=("local-package-cli" "browser-command" "clone-cache" "login-context" "auth-claude-login" "runtime-volumes-ssh" "clone-auto-open-ssh" "opencode-autoconnect")
99
fi
1010

1111
for case_name in "${cases[@]}"; do

0 commit comments

Comments
 (0)