diff --git a/README.ja.md b/README.ja.md index 32b028c..c83f63d 100644 --- a/README.ja.md +++ b/README.ja.md @@ -171,7 +171,7 @@ Kyrozen は: | `/api_key` | API キーを変更 | | `/learn` | プロジェクトファイルを即座にメモリにスキャン | | `/forget` | 最近の学習を表示;`/forget キーワード` で誤った学習を削除 | -| `/update` | 固定された GitHub `v2.0.3` リリース wheel を再インストール(プロジェクトへ git pull しない) | +| `/update` | 固定された GitHub `v2.0.3` wheel でインストール済みツールを更新;成功後に Kyrozen を再起動(プロジェクトへ git pull しない) | | `/self-learning` | 個別の自己学習機能をオン/オフ | ### Web UI モード diff --git a/README.ko.md b/README.ko.md index 7aa68f8..f79cb46 100644 --- a/README.ko.md +++ b/README.ko.md @@ -171,7 +171,7 @@ Kyrozen의 동작: | `/api_key` | API 키 변경 | | `/learn` | 프로젝트 파일을 즉시 메모리에 스캔 | | `/forget` | 최근 학습 확인; `/forget 키워드`로 잘못된 학습 삭제 | -| `/update` | 고정된 GitHub `v2.0.3` 릴리스 wheel을 다시 설치 (프로젝트에 git pull을 실행하지 않음) | +| `/update` | 고정된 GitHub `v2.0.3` wheel로 설치된 도구를 제자리에서 업데이트; 성공 후 Kyrozen 재시작 (프로젝트에 git pull을 실행하지 않음) | | `/self-learning` | 개별 자기 학습 기능 켜기/끄기 | ### Web UI 모드 diff --git a/README.md b/README.md index 8e883da..91cbbb9 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ Kyrozen will: | `/api_key` | Change your API key | | `/learn` | Immediately scan project files into memory | | `/forget` | Show recent learnings; `/forget keyword` to delete bad learnings | -| `/update` | Reinstall the pinned `v2.0.3` GitHub release wheel (never pulls into a project) | +| `/update` | Update the installed tool in place from the pinned `v2.0.3` GitHub wheel; restart Kyrozen after success (never pulls into a project) | | `/agent auto\|coder\|researcher` | Choose automatic routing or an isolated learning profile | | `/learning status [profile]` | Show candidate, canary, active, retired, and rolled-back artifacts | | `/learning metrics [profile]` | Show verified completion, corrections, errors, cost, and latency metrics | diff --git a/README.zh-CN.md b/README.zh-CN.md index d5de6e2..a787a43 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -167,7 +167,7 @@ Kyrozen 会: | `/api_key` | 更改 API 密钥 | | `/learn` | 立即扫描项目文件存入记忆 | | `/forget` | 查看最近的学习记录;`/forget 关键词` 删除错误学习 | -| `/update` | 重新安装固定的 GitHub `v2.0.3` 发布 wheel(不会向项目执行 git pull) | +| `/update` | 使用固定的 GitHub `v2.0.3` wheel 就地更新已安装工具;成功后重启 Kyrozen(不会向项目执行 git pull) | | `/self-learning` | 开关各项自学习功能 | ### Web UI 模式 diff --git a/docs/self-evolution.md b/docs/self-evolution.md index b53ffd9..abfcc34 100644 --- a/docs/self-evolution.md +++ b/docs/self-evolution.md @@ -9,7 +9,7 @@ Historical verification snapshot: `51be33361422e55e1f2f00c33a0e0f8c56132a91` (the post-#54 `main` revision, captured before this #55 documentation-only update). Snapshot date: 2026-09-04. -Current repository test count at this snapshot: **201 unittest cases**. +Current repository test count at this snapshot: **202 unittest cases**. ## Verified surface diff --git a/main.py b/main.py index 68abf0d..a68ddf7 100644 --- a/main.py +++ b/main.py @@ -1297,7 +1297,8 @@ def _load_project_files_into_memory(*, force: bool = False) -> None: def _self_update() -> str: """Upgrade the installed package without touching the active project.""" - if shutil.which("uv") is None: + uv_path = shutil.which("uv") + if uv_path is None: return ( "OpenKyrozen is package-managed. Install or update it with the official " f"{RELEASE_TAG} installer (uv is required)." @@ -1307,7 +1308,7 @@ def _self_update() -> str: if sys.version_info[:2] in {(3, 12), (3, 13)} else "3.12" ) command = [ - "uv", "tool", "install", "--python", requested_python, "--force", + uv_path, "tool", "install", "--python", requested_python, "--force", "--with", "fastapi", "--with", "uvicorn", RELEASE_WHEEL_URL, ] try: @@ -1319,18 +1320,25 @@ def _self_update() -> str: ) if result.returncode != 0: retry = subprocess.run( - ["uv", "--no-cache", *command[1:]], + [uv_path, "--no-cache", *command[1:]], capture_output=True, text=True, timeout=60, ) - if retry.returncode == 0: - result = retry + result = retry out = result.stdout.strip() or "" err = result.stderr.strip() or "" + diagnostics = "\n".join(part for part in (out, err) if part) if result.returncode != 0: - return f"Update failed:\n{err}" - return f"Updated OpenKyrozen from GitHub release {RELEASE_TAG}:\n{out}" + return ( + f"Update failed (uv exit {result.returncode}):\n" + f"{diagnostics or 'uv returned no diagnostics.'}" + ) + return ( + f"Updated OpenKyrozen from GitHub release {RELEASE_TAG}:\n" + f"{diagnostics or 'uv completed successfully.'}\n" + "Restart kyrozen to use the updated process." + ) except subprocess.TimeoutExpired: return "Update timed out." except FileNotFoundError: diff --git a/tests/test_distribution.py b/tests/test_distribution.py index 4a09e8d..fe30dbe 100644 --- a/tests/test_distribution.py +++ b/tests/test_distribution.py @@ -243,7 +243,7 @@ def test_update_uses_the_package_manager_instead_of_git_pull(self): f"{sys.version_info.major}.{sys.version_info.minor}" if sys.version_info[:2] in {(3, 12), (3, 13)} else "3.12" ) - self.assertEqual(command[:6], ["uv", "tool", "install", "--python", expected_python, "--force"]) + self.assertEqual(command[:6], ["/usr/local/bin/uv", "tool", "install", "--python", expected_python, "--force"]) self.assertIn("--with", command) self.assertIn("fastapi", command) self.assertIn("uvicorn", command) @@ -265,9 +265,26 @@ def test_update_retries_without_uv_cache_after_install_failure(self): self.assertEqual(run.call_count, 2) self.assertEqual( run.call_args_list[1].args[0][:3], - ["uv", "--no-cache", "tool"], + ["/usr/local/bin/uv", "--no-cache", "tool"], ) + def test_update_preserves_stdout_only_failure_diagnostics(self): + import main + + failed = subprocess.CompletedProcess( + ["/usr/local/bin/uv", "tool", "install"], 1, + stdout="release asset unavailable", stderr="", + ) + retry_failed = subprocess.CompletedProcess( + ["/usr/local/bin/uv", "--no-cache", "tool", "install"], 1, + stdout="retry also failed", stderr="", + ) + with patch("main.shutil.which", return_value="/usr/local/bin/uv"), \ + patch("main.subprocess.run", side_effect=[failed, retry_failed]): + result = main._self_update() + self.assertIn("uv exit 1", result) + self.assertIn("retry also failed", result) + def test_docker_starts_server_in_explicit_project_mode(self): dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8") self.assertIn('"server.py", "--project", "/app"', dockerfile)