Files
codex-app-server/cloud/tests/test_pr_app_token.py
T
host 25d9b3d938 App-token:推分支建 PR 改走 GitHub App installation token
- github_app:纯标准库 RS256 签 App JWT(PKCS#1/PKCS#8),
  换 installation token(现换现用、不缓存),按仓库名查安装
  记录,任一步失败回落 None
- pr:app_token 路径 push 走 credential helper(token 只进
  子进程环境),建 PR 走 REST API(读 default_branch 当 base);
  不传 token 时 gh 路径不变
- server:/pr 端点按项目 repo_url 解析仓库名、现换 token,
  响应加 auth_via=app|pat,换失败回落 PAT 不报错
- 测试:JWT/换 token/解析 9 个、pr App 路径 6 个、端点级
  App 与 PAT 回落 2 个
2026-10-26 16:47:55 +08:00

177 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""pr.py 的 App-token 路径单测:push 走 credential helper、建 PR
走 GitHub API、gh 完全不被调用;remote 解析与失败分支。
不联网、不跑真实 git/gh:git 用假 runner,GitHub API 用假
客户端;token 值绝不出现在记录的命令里(只出现变量名)。
"""
import os
import sys
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
try:
from cloud.pr import (
PrStepError,
check_pr_ready,
create_task_pr,
parse_git_remote_owner_repo,
)
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.pr import (
PrStepError,
check_pr_ready,
create_task_pr,
parse_git_remote_owner_repo,
)
def _result(returncode=0, stdout="", stderr=""):
return SimpleNamespace(returncode=returncode, stdout=stdout,
stderr=stderr)
class _Git:
"""假 git runner:remote/branch/add/commit/push 全按脚本回。"""
def __init__(self, remote="https://github.com/octo/a.git"):
self.calls = []
self.remote = remote
def __call__(self, cmd, cwd):
self.calls.append(list(cmd))
if "remote" in cmd:
return _result(0, self.remote + "\n")
if "branch" in cmd:
return _result(0, "host/t1\n")
if "add" in cmd:
return _result(0, "")
if "commit" in cmd:
return _result(1, "",
"nothing to commit, working tree clean")
if "push" in cmd:
return _result(0, "")
return _result(0, "")
def _gh_forbidden(cmd, cwd):
raise AssertionError("App 路径不应调用 gh:%r" % (cmd,))
class _FakeGithubApi:
"""假 GitHub API:记录调用,GET 仓库回 default_branch,
POST pulls 回 PR 链接;失败模式可配。"""
def __init__(self, default_branch="main", pulls_status=201):
self.calls = []
self.default_branch = default_branch
self.pulls_status = pulls_status
def __call__(self, method, url, token, payload):
self.calls.append((method, url, token, payload))
if method == "GET" and url.endswith("/repos/octo/a"):
return 200, {"default_branch": self.default_branch}
if method == "POST" and url.endswith("/repos/octo/a/pulls"):
if self.pulls_status != 201:
return self.pulls_status, {"message": "Validation Failed"}
return 201, {"html_url": "https://github.com/octo/a/pull/42",
"number": 42}
raise AssertionError("unexpected API call: %s %s" % (method, url))
def _worktree(tmp):
d = os.path.join(tmp, "t1")
os.makedirs(d, exist_ok=True)
return d
class ParseRemoteTestCase(unittest.TestCase):
def test_variants(self):
self.assertEqual(
parse_git_remote_owner_repo(
"https://github.com/octo/a.git"), ("octo", "a"))
self.assertEqual(
parse_git_remote_owner_repo(
"https://github.com/octo/a"), ("octo", "a"))
self.assertEqual(
parse_git_remote_owner_repo(
"https://user@github.com/octo/a.git"), ("octo", "a"))
self.assertEqual(
parse_git_remote_owner_repo(
"git@github.com:octo/a.git"), ("octo", "a"))
self.assertEqual(
parse_git_remote_owner_repo(
"ssh://git@github.com/octo/a.git"), ("octo", "a"))
def test_non_github_is_none(self):
self.assertIsNone(parse_git_remote_owner_repo(
"https://example.com/octo/a.git"))
self.assertIsNone(parse_git_remote_owner_repo(""))
self.assertIsNone(parse_git_remote_owner_repo(None))
class AppPathTestCase(unittest.TestCase):
def test_push_uses_credential_helper_and_pr_via_api(self):
with tempfile.TemporaryDirectory() as tmp:
_worktree(tmp)
git = _Git()
api = _FakeGithubApi()
result = create_task_pr(
"t1", 1, worktrees_root=tmp, git_runner=git,
gh_runner=_gh_forbidden, gh_which="/nonexistent/gh",
app_token="fake-app-token", github_api=api)
self.assertEqual(
result["pr_url"], "https://github.com/octo/a/pull/42")
self.assertEqual(result["pr_number"], 42)
self.assertEqual(result["branch"], "host/t1")
pushes = [c for c in git.calls if "push" in c]
self.assertEqual(len(pushes), 1)
helper = [a for a in pushes[0]
if a.startswith("credential.helper=")][0]
self.assertIn("$GIT_TOKEN", helper)
self.assertNotIn("fake-app-token", " ".join(pushes[0]))
posts = [c for c in api.calls if c[0] == "POST"]
self.assertEqual(len(posts), 1)
self.assertEqual(posts[0][2], "fake-app-token")
self.assertEqual(posts[0][3]["head"], "host/t1")
self.assertEqual(posts[0][3]["base"], "main")
self.assertEqual(posts[0][3]["title"], "cloud: t1")
def test_app_path_skips_gh_checks(self):
# gh_which 指到不存在的路径、gh_runner 会炸:App 路径下
# check_pr_ready 仍应通过(只查 worktree + remote)。
with tempfile.TemporaryDirectory() as tmp:
_worktree(tmp)
self.assertTrue(check_pr_ready(
os.path.join(tmp, "t1"), git_runner=_Git(),
gh_which="/nonexistent/gh", gh_runner=_gh_forbidden,
app_token="x"))
def test_non_github_remote_fails_at_push(self):
with tempfile.TemporaryDirectory() as tmp:
_worktree(tmp)
with self.assertRaises(PrStepError) as ctx:
create_task_pr(
"t1", 1, worktrees_root=tmp,
git_runner=_Git("https://example.com/octo/a.git"),
gh_runner=_gh_forbidden, app_token="x",
github_api=_FakeGithubApi())
self.assertEqual(ctx.exception.step, "push")
def test_api_failure_is_pr_create_step(self):
with tempfile.TemporaryDirectory() as tmp:
_worktree(tmp)
with self.assertRaises(PrStepError) as ctx:
create_task_pr(
"t1", 1, worktrees_root=tmp, git_runner=_Git(),
gh_runner=_gh_forbidden, app_token="x",
github_api=_FakeGithubApi(pulls_status=422))
self.assertEqual(ctx.exception.step, "pr_create")
if __name__ == "__main__":
unittest.main()