diff --git a/README.md b/README.md index 19cf169..ef745c0 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,13 @@ google-jules-skill/ python3 google-jules-control/scripts/jules_api.py doctor --compact ``` +키가 실제 API 인증까지 통과하는지 확인하려면 아래처럼 검증 probe를 추가합니다. +Add the validation probe when you need to confirm that the key authenticates with the API. + +```bash +python3 google-jules-control/scripts/jules_api.py doctor --compact --validate-api +``` + 4. 저장소를 Jules source로 해석합니다. Resolve a repository to a Jules source. diff --git a/docs/setup-and-test.md b/docs/setup-and-test.md index ed77685..6dba4fd 100644 --- a/docs/setup-and-test.md +++ b/docs/setup-and-test.md @@ -28,19 +28,23 @@ The script checks the current working directory first, then the skill root. ```bash python3 google-jules-control/scripts/jules_api.py doctor --compact +python3 google-jules-control/scripts/jules_api.py doctor --compact --validate-api ``` -정상 예시 / Healthy example: +검증 포함 정상 예시 / Healthy example with API validation: ```text -dotenv=yes api_key=yes api_ready=yes gh=yes gh_auth=yes merge_ready=yes jules_cli=no jules_cli_auth=not_installed cli_ready=no ready=yes +dotenv=yes api_key=yes api_validated=yes api_status=ok api_ready=yes gh=yes gh_auth=yes merge_ready=yes jules_cli=no jules_cli_auth=not_installed cli_ready=no ready=yes ``` REST API만 쓴다면 `jules_cli=no`는 문제 아닙니다. `jules_cli=no` is acceptable if you only use the REST API path. -`api_ready=yes`는 API helper가 바로 실행 가능한 상태를 뜻합니다. -`api_ready=yes` means the REST API helper can run. +기본 `doctor --compact`는 네트워크 API 호출을 하지 않습니다. 이 경우 `api_key=yes`는 키가 있다는 뜻이고, `api_ready=yes`는 아닙니다. +Plain `doctor --compact` does not make a network API call. In that mode, `api_key=yes` means the key is present, not that `api_ready=yes`. + +`api_ready=yes`는 `--validate-api` probe가 성공해서 현재 키로 Jules API 인증이 확인됐다는 뜻입니다. +`api_ready=yes` means the `--validate-api` probe succeeded and the current key authenticated with the Jules API. `cli_ready=yes`는 Jules CLI 경로도 바로 쓸 수 있다는 뜻입니다. `cli_ready=yes` means the Jules CLI path is also ready to use. @@ -127,8 +131,8 @@ python3 google-jules-control/scripts/jules_api.py stale-session-report --repo-fi - `ready=no`: `doctor`를 `--compact` 없이 실행해서 어떤 항목이 비었는지 확인합니다. Run `doctor` without `--compact` to see the missing dependency. -- `api_ready=no`: `.env`와 `JULES_API_KEY`를 먼저 점검합니다. - Check `.env` and `JULES_API_KEY` first. +- `api_ready=no`: `api_key`, `api_validated`, `api_status`를 함께 봅니다. 검증이 필요하면 `doctor --compact --validate-api`를 실행합니다. + Check `api_key`, `api_validated`, and `api_status` together. Run `doctor --compact --validate-api` when you need credential validation. - `cli_ready=no`: CLI 경로를 쓰려면 `jules login` 또는 CLI 인증 상태를 확인합니다. If you want the CLI path, check `jules login` or the CLI authentication state. - `merge_ready=no`: merge 기반 리포트 전에 `gh auth status`를 확인합니다. diff --git a/google-jules-control/SKILL.md b/google-jules-control/SKILL.md index 9e2286a..a6986d5 100644 --- a/google-jules-control/SKILL.md +++ b/google-jules-control/SKILL.md @@ -13,6 +13,7 @@ Use this skill to delegate coding work to Google Jules from an agentic workflow. - API path: put `JULES_API_KEY` in a `.env` file from `https://jules.google.com/settings`. - CLI path: install `@google/jules`, then run `jules login`. - API/CLI health check: run `python3 scripts/jules_api.py doctor --compact`. + - API credential validation: run `python3 scripts/jules_api.py doctor --compact --validate-api`. - Merge-aware reporting health check: run `python3 scripts/jules_api.py gh-auth-check --compact`. 2. Discover the target repository/source. - API path: run `python3 scripts/jules_api.py repo-to-source --repo owner/repo --compact` or `python3 scripts/jules_api.py list-sources`. @@ -115,7 +116,8 @@ python3 scripts/jules_api.py summary --session sessions/1234567890 Notes: -- `doctor --compact` now separates `api_ready`, `cli_ready`, and `merge_ready`. +- `doctor --compact` separates `api_key`, `api_validated`, `api_ready`, `cli_ready`, and `merge_ready`. +- `api_key=yes` only means the key is present. Use `doctor --compact --validate-api` when you need to prove that the key can authenticate. - Aggregated list/report commands collect all pages by default. `--page-token` is a starting point for aggregation, not a single-page mode. ### CLI workflow @@ -474,7 +476,7 @@ python3 scripts/jules_api.py close-ready-report --repo-filter owner/repo --requi - Use `close-ready-report` when the user wants close candidates plus ready-made close confirmation text in one step. - Prefer `--markdown` for human review and `--compact` for scripting or automation chaining. - Prefer a local `.env` file over shell profile edits for `JULES_API_KEY`. The script auto-loads `.env` from the current working directory or the skill root. -- Use `doctor` before the first live run to verify `.env`, `JULES_API_KEY`, `gh`, and `jules` CLI readiness in one place. +- Use `doctor` before the first live run to inspect `.env`, `JULES_API_KEY`, `gh`, and `jules` CLI readiness in one place. Add `--validate-api` when API credential validity matters. - Use `repo-to-source` when the user gives only `owner/repo` and you need the exact Jules source resource name. - Use `check-pr-readiness` before cleanup when you need to know whether a PR is actually merge-ready, not just merged or open. - Use `request-pr-rework` when GitHub reports blockers such as merge conflicts, failed checks, or requested changes and you want a follow-up message for Jules. diff --git a/google-jules-control/references/jules-reference.md b/google-jules-control/references/jules-reference.md index a47b4c4..399bd2f 100644 --- a/google-jules-control/references/jules-reference.md +++ b/google-jules-control/references/jules-reference.md @@ -95,6 +95,7 @@ The Jules REST API does not expose GitHub merge state directly. To close a sessi The bundled `jules_api.py` script automates this with: - `doctor --compact` +- `doctor --compact --validate-api` - `gh-auth-check --compact` - `repo-to-source --repo owner/repo --compact` - `cleanup-report --repo-filter owner/repo --require-all-merged` @@ -115,7 +116,7 @@ Implementation detail: - Merge readiness is checked through `gh pr view --json mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,...`. - If `gh` is missing or unauthenticated, merge status falls back to `unknown` and close should not proceed automatically. - `gh-auth-check` is the fast preflight check before any merge-aware report. -- `doctor` separates `api_ready`, `cli_ready`, and `merge_ready`. `ready=yes` means at least one control path is available. +- `doctor` separates `api_key`, `api_validated`, `api_ready`, `cli_ready`, and `merge_ready`. Without `--validate-api`, `api_key=yes` only means the key is present. With `--validate-api`, `api_ready=yes` means the API probe authenticated successfully. `ready=yes` means at least one validated control path is available. - `close-ready-report` distinguishes between `candidates` and `cautionCandidates`. Treat caution entries as manual-review items, not automatic close targets. - `close-merged-session` refuses `caution` sessions by default. Only use `--allow-caution-close` after explicit user approval. diff --git a/google-jules-control/scripts/jules_api.py b/google-jules-control/scripts/jules_api.py index 6e1ebcc..f990f9c 100644 --- a/google-jules-control/scripts/jules_api.py +++ b/google-jules-control/scripts/jules_api.py @@ -4,7 +4,9 @@ from __future__ import annotations import argparse +import contextlib import datetime as dt +import io import json import os from pathlib import Path @@ -422,6 +424,43 @@ def collect_jules_cli_status() -> dict[str, Any]: return payload +def collect_api_validation_status(*, validate: bool) -> dict[str, Any]: + api_key_present = bool(os.environ.get("JULES_API_KEY", "").strip()) + if not validate: + return { + "validated": False, + "ready": False, + "status": "not_checked", + "reason": "Run doctor --validate-api to verify Jules API credentials.", + } + if not api_key_present: + return { + "validated": True, + "ready": False, + "status": "missing_key", + "reason": "JULES_API_KEY is not set.", + } + + stderr = io.StringIO() + try: + with contextlib.redirect_stderr(stderr): + api_request("GET", "/sources", query={"pageSize": 1}) + except SystemExit as exc: + reason = stderr.getvalue().strip() or f"API validation failed with exit code {exc.code}." + return { + "validated": True, + "ready": False, + "status": "failed", + "reason": reason, + } + + return { + "validated": True, + "ready": True, + "status": "ok", + } + + def format_session_line(session: dict[str, Any]) -> str: repo = session.get("repo") or "-" title = session.get("title") or "(untitled)" @@ -1627,7 +1666,8 @@ def doctor(args: argparse.Namespace) -> None: api_key_present = bool(os.environ.get("JULES_API_KEY", "").strip()) gh_status = collect_gh_auth_status() jules_status = collect_jules_cli_status() - api_ready = api_key_present + api_validation = collect_api_validation_status(validate=args.validate_api) + api_ready = bool(api_validation["ready"]) cli_ready = bool(jules_status.get("ready")) merge_ready = bool(gh_status.get("installed") and gh_status.get("authenticated")) @@ -1639,6 +1679,7 @@ def doctor(args: argparse.Namespace) -> None: "julesApiKey": { "present": api_key_present, }, + "julesApiValidation": api_validation, "gh": gh_status, "julesCli": jules_status, "apiReady": api_ready, @@ -1654,6 +1695,8 @@ def doctor(args: argparse.Namespace) -> None: [ f"dotenv={'yes' if payload['dotenv']['found'] else 'no'}", f"api_key={'yes' if api_key_present else 'no'}", + f"api_validated={'yes' if api_validation['validated'] else 'no'}", + f"api_status={api_validation['status']}", f"api_ready={'yes' if api_ready else 'no'}", f"gh={'yes' if gh_status.get('installed') else 'no'}", f"gh_auth={'yes' if gh_status.get('authenticated') else 'no'}", @@ -1763,6 +1806,11 @@ def build_parser() -> argparse.ArgumentParser: help="Check .env, API key, gh auth, and Jules CLI readiness in one command.", ) doctor_parser.add_argument("--compact", action="store_true", help="Print a short status line instead of JSON.") + doctor_parser.add_argument( + "--validate-api", + action="store_true", + help="Probe the Jules API to verify that the current API key can authenticate.", + ) doctor_parser.set_defaults(func=doctor) gh_auth_check_parser = subparsers.add_parser( diff --git a/tests/test_jules_api.py b/tests/test_jules_api.py index af9ac34..02055b8 100644 --- a/tests/test_jules_api.py +++ b/tests/test_jules_api.py @@ -239,8 +239,8 @@ def test_close_merged_session_refuses_caution_without_override(self) -> None: self.assertEqual(1, api_request.call_count) - def test_doctor_reports_api_ready_without_merge_ready(self) -> None: - args = argparse.Namespace(compact=True) + def test_doctor_reports_api_key_present_without_api_ready_when_not_validated(self) -> None: + args = argparse.Namespace(compact=True, validate_api=False) output = io.StringIO() with mock.patch.object(jules_api, "find_dotenv_path", return_value=Path("/tmp/.env")): @@ -255,11 +255,65 @@ def test_doctor_reports_api_ready_without_merge_ready(self) -> None: jules_api.doctor(args) rendered = output.getvalue().strip() - self.assertIn("api_ready=yes", rendered) + self.assertIn("api_key=yes", rendered) + self.assertIn("api_validated=no", rendered) + self.assertIn("api_ready=no", rendered) self.assertIn("cli_ready=no", rendered) self.assertIn("merge_ready=no", rendered) + self.assertIn("ready=no", rendered) + + def test_doctor_validate_api_marks_api_ready_after_successful_probe(self) -> None: + args = argparse.Namespace(compact=True, validate_api=True) + output = io.StringIO() + + with mock.patch.object(jules_api, "find_dotenv_path", return_value=Path("/tmp/.env")): + with mock.patch.dict(jules_api.os.environ, {"JULES_API_KEY": "test-key"}, clear=False): + with mock.patch.object(jules_api, "collect_gh_auth_status", return_value={"installed": True, "authenticated": False}): + with mock.patch.object( + jules_api, + "collect_jules_cli_status", + return_value={"installed": False, "path": None, "authStatus": "not_installed", "ready": False}, + ): + with mock.patch.object(jules_api, "api_request", return_value={"sources": []}) as api_request: + with redirect_stdout(output): + jules_api.doctor(args) + + api_request.assert_called_once_with("GET", "/sources", query={"pageSize": 1}) + rendered = output.getvalue().strip() + self.assertIn("api_validated=yes", rendered) + self.assertIn("api_status=ok", rendered) + self.assertIn("api_ready=yes", rendered) self.assertIn("ready=yes", rendered) + def test_doctor_validate_api_reports_failed_probe_without_ready(self) -> None: + args = argparse.Namespace(compact=True, validate_api=True) + output = io.StringIO() + + with mock.patch.object(jules_api, "find_dotenv_path", return_value=Path("/tmp/.env")): + with mock.patch.dict(jules_api.os.environ, {"JULES_API_KEY": "test-key"}, clear=False): + with mock.patch.object(jules_api, "collect_gh_auth_status", return_value={"installed": True, "authenticated": False}): + with mock.patch.object( + jules_api, + "collect_jules_cli_status", + return_value={"installed": False, "path": None, "authStatus": "not_installed", "ready": False}, + ): + with mock.patch.object(jules_api, "api_request", side_effect=SystemExit(1)): + with redirect_stdout(output): + jules_api.doctor(args) + + rendered = output.getvalue().strip() + self.assertIn("api_validated=yes", rendered) + self.assertIn("api_status=failed", rendered) + self.assertIn("api_ready=no", rendered) + self.assertIn("ready=no", rendered) + + def test_doctor_parser_supports_api_validation_probe(self) -> None: + parser = jules_api.build_parser() + + args = parser.parse_args(["doctor", "--validate-api"]) + + self.assertTrue(args.validate_api) + def test_collect_jules_cli_status_marks_authenticated_when_probe_succeeds(self) -> None: version_result = mock.Mock(stdout="1.2.3\n", stderr="") probe_result = mock.Mock(stdout="repo-a\nrepo-b\n", stderr="")