From 7b73cc8e2bcb92d2d94f7bf19121ea87b4ab20b7 Mon Sep 17 00:00:00 2001 From: Yi Lu Date: Wed, 15 Jul 2026 00:44:06 -0700 Subject: [PATCH] fix(update): use `claude plugin update` with reinstall fallback `npx claude-smart update` (Claude Code host) previously stopped services and ran a full reinstall. Use Claude Code's own plugin update path instead: `claude plugin marketplace add` then `claude plugin update`, falling back to the stop+reinstall flow only if the update subcommand fails. Mirrored across the Node (`runUpdate`) and Python (`cmd_update`) entrypoints, and factored the shared post-update bootstrap into `bootstrapClaudeCodeRuntime` / `_finish_claude_code_install`. Check for the `claude` CLI before stopping the backend/dashboard so a missing-CLI abort no longer leaves services stopped. --- bin/claude-smart.js | 72 +++++++++++++++++---- plugin/src/claude_smart/cli.py | 108 +++++++++++++++++++++++-------- tests/test_cli_install.py | 57 +++++++++++++++-- tests/test_install_scripts.py | 113 +++++++++++++++++++++------------ 4 files changed, 259 insertions(+), 91 deletions(-) diff --git a/bin/claude-smart.js b/bin/claude-smart.js index 1e98187..bab19fd 100755 --- a/bin/claude-smart.js +++ b/bin/claude-smart.js @@ -1710,7 +1710,7 @@ function printHelp() { " 4. Restart OpenCode.", "", "Update:", - " npx claude-smart update Reinstall Claude Code support from this package", + " npx claude-smart update Update Claude Code support from this package", " npx claude-smart update --host codex Reinstall Codex support from this package", " npx claude-smart update --host opencode Reinstall OpenCode support from this package", " npx claude-smart setup Configure managed/read-only/global setup", @@ -2155,12 +2155,52 @@ async function runUpdate(args) { return; } + if (!hasClaudeCli()) { + process.stderr.write( + "error: 'claude' CLI not found on PATH. " + + "Install Claude Code first: https://claude.com/claude-code\n", + ); + process.exit(1); + } + const pluginRoot = findClaudeCodePluginRoot(); if (pluginRoot) { stopClaudeSmartServices(pluginRoot); } - process.stdout.write("Updating claude-smart by reinstalling from this package...\n"); - await runInstall(args, { retryInstallAfterUninstall: true }); + + const setup = configureReflexioSetup(HOST_CLAUDE_CODE); + process.stdout.write("Updating claude-smart from this package...\n"); + let code = await runClaude(["plugin", "marketplace", "add", PACKAGE_ROOT], { + spinnerLabel: "Adding marketplace…", + }); + if (code !== 0) { + process.stderr.write( + `error: \`claude plugin marketplace add ${PACKAGE_ROOT}\` failed (exit ${code})\n`, + ); + process.exit(code); + } + + code = await runClaude(["plugin", "update", PLUGIN_SPEC], { + spinnerLabel: "Updating claude-smart…", + }); + if (code !== 0) { + process.stderr.write( + `warning: \`claude plugin update ${PLUGIN_SPEC}\` failed (exit ${code}); falling back to reinstall.\n`, + ); + await runInstall(args, { retryInstallAfterUninstall: true }); + return; + } + + await bootstrapClaudeCodeRuntime(setup.readOnly, "updated"); + process.stdout.write( + [ + "", + "claude-smart updated and dependencies are prepared. Restart Claude Code in your project.", + "The reflexio backend and dashboard auto-start on session start.", + "Opt out with CLAUDE_SMART_BACKEND_AUTOSTART=0 or CLAUDE_SMART_DASHBOARD_AUTOSTART=0.", + "", + ].join("\n"), + ); } async function runUpdateCodex(args) { @@ -2282,6 +2322,20 @@ async function runInstall(args, options = {}) { } } + await bootstrapClaudeCodeRuntime(readOnly, "installed"); + + process.stdout.write( + [ + "", + "claude-smart installed and dependencies are prepared. Restart Claude Code in your project.", + "The reflexio backend and dashboard auto-start on session start.", + "Opt out with CLAUDE_SMART_BACKEND_AUTOSTART=0 or CLAUDE_SMART_DASHBOARD_AUTOSTART=0.", + "", + ].join("\n"), + ); +} + +async function bootstrapClaudeCodeRuntime(readOnly, action) { try { await refreshClaudeCodeCacheIfVendorMissing(); const pluginRoot = await bootstrapClaudeCodeInstall(); @@ -2299,23 +2353,13 @@ async function runInstall(args, options = {}) { } } catch (err) { process.stderr.write( - `error: claude-smart installed, but dependency bootstrap failed: ${err && err.message ? err.message : err}\n`, + `error: claude-smart ${action}, but dependency bootstrap failed: ${err && err.message ? err.message : err}\n`, ); process.stderr.write( "Fix the issue above, then run /claude-smart:restart or restart Claude Code to retry.\n", ); process.exit(1); } - - process.stdout.write( - [ - "", - "claude-smart installed and dependencies are prepared. Restart Claude Code in your project.", - "The reflexio backend and dashboard auto-start on session start.", - "Opt out with CLAUDE_SMART_BACKEND_AUTOSTART=0 or CLAUDE_SMART_DASHBOARD_AUTOSTART=0.", - "", - ].join("\n"), - ); } async function runInstallCodex(args) { diff --git a/plugin/src/claude_smart/cli.py b/plugin/src/claude_smart/cli.py index 003351a..8339b14 100644 --- a/plugin/src/claude_smart/cli.py +++ b/plugin/src/claude_smart/cli.py @@ -37,7 +37,15 @@ from urllib.parse import urlparse from urllib.request import url2pathname -from claude_smart import context_format, cs_cite, env_config, ids, publish, runtime, state +from claude_smart import ( + context_format, + cs_cite, + env_config, + ids, + publish, + runtime, + state, +) from claude_smart.reflexio_adapter import Adapter _HOST_CLAUDE_CODE = runtime.HOST_CLAUDE_CODE @@ -987,7 +995,9 @@ def _configure_reflexio_setup(host: str = _HOST_CLAUDE_CODE) -> bool: os.environ.pop("CLAUDE_SMART_MANAGED_SETUP", None) added = env_config.ensure_local_env_defaults(_CLAUDE_SMART_ENV_PATH, host=host) if added: - sys.stdout.write(f"Seeded {_CLAUDE_SMART_ENV_PATH} with {', '.join(added)}.\n") + sys.stdout.write( + f"Seeded {_CLAUDE_SMART_ENV_PATH} with {', '.join(added)}.\n" + ) return read_only @@ -1071,7 +1081,9 @@ def _opencode_global_config_dir() -> Path: return base / "opencode" -def _opencode_config_path(*, global_config: bool = False, cwd: Path | None = None) -> Path: +def _opencode_config_path( + *, global_config: bool = False, cwd: Path | None = None +) -> Path: if global_config: config_dir = _opencode_global_config_dir() for name in _OPENCODE_CONFIG_NAMES: @@ -1108,9 +1120,9 @@ def _is_default_opencode_package_path(package_path: Path) -> bool: if _same_real_path(package_path, _OPENCODE_LOCAL_PACKAGE_DIR): return True try: - return package_path.resolve(strict=False) == _OPENCODE_LOCAL_PACKAGE_DIR.resolve( + return package_path.resolve( strict=False - ) + ) == _OPENCODE_LOCAL_PACKAGE_DIR.resolve(strict=False) except OSError: return False @@ -1192,7 +1204,9 @@ def _has_extraction_provider() -> bool: resolved = Path(cli_path).expanduser() if resolved.is_file() and os.access(resolved, os.X_OK): return True - return bool(shutil.which("claude") or shutil.which("codex") or _resolve_opencode_path()) + return bool( + shutil.which("claude") or shutil.which("codex") or _resolve_opencode_path() + ) def _extraction_provider_error() -> str: @@ -1221,7 +1235,9 @@ def _persist_opencode_path() -> list[str]: if not resolved: return [] os.environ[_OPENCODE_PATH_ENV] = resolved - return env_config.set_env_vars(_CLAUDE_SMART_ENV_PATH, {_OPENCODE_PATH_ENV: resolved}) + return env_config.set_env_vars( + _CLAUDE_SMART_ENV_PATH, {_OPENCODE_PATH_ENV: resolved} + ) def _opencode_prerequisite_error() -> str | None: @@ -1358,7 +1374,9 @@ def _bootstrap_opencode_install(read_only: bool) -> tuple[bool, str]: if not bash: return False, "bash is required to bootstrap claude-smart dependencies" scripts_dir = plugin_root / "scripts" - result = subprocess.run([bash, str(scripts_dir / "smart-install.sh")], cwd=plugin_root) + result = subprocess.run( + [bash, str(scripts_dir / "smart-install.sh")], cwd=plugin_root + ) if result.returncode != 0: return False, f"smart-install.sh failed in {plugin_root}" if _INSTALL_FAILURE_MARKER.is_file(): @@ -1572,10 +1590,22 @@ def cmd_install(args: argparse.Namespace) -> int: sys.stderr.write(f"error: {' '.join(cmd)} failed (exit {exc.returncode})\n") return exc.returncode or 1 + rc = _finish_claude_code_install(read_only=read_only, action="installed") + if rc != 0: + return rc + + sys.stdout.write( + "\nclaude-smart installed and dependencies are prepared. " + "Restart Claude Code in your project.\n" + ) + return 0 + + +def _finish_claude_code_install(*, read_only: bool, action: str) -> int: bootstrapped, message = _bootstrap_claude_code_install() if not bootstrapped: sys.stderr.write( - f"error: claude-smart installed, but dependency bootstrap failed: {message}\n" + f"error: claude-smart {action}, but dependency bootstrap failed: {message}\n" ) sys.stderr.write( "Fix the issue above, then run /claude-smart:restart or restart Claude Code to retry.\n" @@ -1588,20 +1618,11 @@ def cmd_install(args: argparse.Namespace) -> int: "Installed read-only hook manifest; publish interactions hooks are disabled.\n" ) sys.stdout.write(f"Prepared claude-smart runtime at {message}.\n") - - sys.stdout.write( - "\nclaude-smart installed and dependencies are prepared. " - "Restart Claude Code in your project.\n" - ) return 0 def cmd_update(args: argparse.Namespace) -> int: - """Update claude-smart by reinstalling from this package. - - The simplified install path registers the bundled package root as the - marketplace, so update is a stop + reinstall instead of - ``claude plugin update``. + """Update claude-smart through Claude Code's plugin update path. Args: args (argparse.Namespace): Parsed CLI args. @@ -1614,12 +1635,44 @@ def cmd_update(args: argparse.Namespace) -> int: if getattr(args, "host", _HOST_CLAUDE_CODE) == _HOST_OPENCODE: return cmd_update_opencode(args) + if not shutil.which("claude"): + sys.stderr.write( + "error: 'claude' CLI not found on PATH. " + "Install Claude Code first: https://claude.com/claude-code\n" + ) + return 1 + _run_service(_DASHBOARD_SCRIPT, "stop") _run_service(_BACKEND_SCRIPT, "stop") - sys.stdout.write("Updating claude-smart by reinstalling from this package...\n") - install_args = argparse.Namespace(**vars(args), refresh_existing=True) - install_args.host = _HOST_CLAUDE_CODE - return cmd_install(install_args) + read_only = _configure_reflexio_setup(host=_HOST_CLAUDE_CODE) + + sys.stdout.write("Updating claude-smart from this package...\n") + for cmd in ( + ["claude", "plugin", "marketplace", "add", str(_REPO_ROOT)], + ["claude", "plugin", "update", _PLUGIN_SPEC], + ): + try: + subprocess.run(cmd, check=True) + except subprocess.CalledProcessError as exc: + if cmd[1:3] == ["plugin", "update"]: + sys.stderr.write( + f"warning: {' '.join(cmd)} failed (exit {exc.returncode}); " + "falling back to reinstall.\n" + ) + install_args = argparse.Namespace(**vars(args), refresh_existing=True) + install_args.host = _HOST_CLAUDE_CODE + return cmd_install(install_args) + sys.stderr.write(f"error: {' '.join(cmd)} failed (exit {exc.returncode})\n") + return exc.returncode or 1 + + rc = _finish_claude_code_install(read_only=read_only, action="updated") + if rc != 0: + return rc + sys.stdout.write( + "\nclaude-smart updated and dependencies are prepared. " + "Restart Claude Code in your project.\n" + ) + return 0 def cmd_update_codex(_args: argparse.Namespace) -> int: @@ -1750,13 +1803,12 @@ def cmd_uninstall_opencode(args: argparse.Namespace) -> int: except OSError: pass if changed: - sys.stdout.write(f"Removed claude-smart OpenCode plugin entries from {config_path}.\n") + sys.stdout.write( + f"Removed claude-smart OpenCode plugin entries from {config_path}.\n" + ) else: sys.stdout.write("OpenCode config did not include claude-smart.\n") - sys.stdout.write( - "Restart OpenCode to apply. " - f"{_LOCAL_DATA_NOTICE}" - ) + sys.stdout.write(f"Restart OpenCode to apply. {_LOCAL_DATA_NOTICE}") return 0 diff --git a/tests/test_cli_install.py b/tests/test_cli_install.py index ee825d5..b1b9bb1 100644 --- a/tests/test_cli_install.py +++ b/tests/test_cli_install.py @@ -215,11 +215,7 @@ def test_install_setup_updates_existing_host_and_local_defaults( ) -> None: env_path = tmp_path / ".claude-smart" / ".env" env_path.parent.mkdir() - env_path.write_text( - "# keep\n" - "CLAUDE_SMART_HOST=codex\n" - 'CLAUDE_SMART_READ_ONLY="1"\n' - ) + env_path.write_text('# keep\nCLAUDE_SMART_HOST=codex\nCLAUDE_SMART_READ_ONLY="1"\n') monkeypatch.setattr(cli, "_CLAUDE_SMART_ENV_PATH", env_path) monkeypatch.setenv("CLAUDE_SMART_USE_LOCAL_EMBEDDING", "0") @@ -235,7 +231,9 @@ def test_install_setup_updates_existing_host_and_local_defaults( assert 'CLAUDE_SMART_READ_ONLY="1"' in text -def test_opencode_install_persists_resolved_cli_path(monkeypatch, tmp_path: Path) -> None: +def test_opencode_install_persists_resolved_cli_path( + monkeypatch, tmp_path: Path +) -> None: runtime_env_path = tmp_path / ".claude-smart" / ".env" opencode = tmp_path / "bin" / "opencode" opencode.parent.mkdir() @@ -453,20 +451,65 @@ def test_update_parser_accepts_host_codex() -> None: assert args.host == "codex" -def test_cmd_update_claude_code_stops_services_then_installs(monkeypatch) -> None: +def test_cmd_update_claude_code_stops_services_then_updates( + monkeypatch, + tmp_path: Path, +) -> None: + calls: list[tuple[str, str] | list[str]] = [] + + def fake_run_service(script, subcmd): + calls.append((script.name, subcmd)) + return 0 + + def fake_run(cmd, check=False): + calls.append([str(part) for part in cmd]) + return argparse.Namespace(returncode=0) + + monkeypatch.setattr(cli.shutil, "which", lambda name: f"/bin/{name}") + monkeypatch.setattr(cli, "_configure_reflexio_setup", lambda **_kwargs: False) + monkeypatch.setattr(cli, "_run_service", fake_run_service) + monkeypatch.setattr(cli.subprocess, "run", fake_run) + monkeypatch.setattr( + cli, "_bootstrap_claude_code_install", lambda: (True, str(tmp_path / "plugin")) + ) + monkeypatch.setattr(cli, "_restore_publish_hooks_from_source", lambda _root: None) + + rc = cli.cmd_update(argparse.Namespace(host="claude-code", marker="kept")) + + assert rc == 0 + assert calls == [ + ("dashboard-service.sh", "stop"), + ("backend-service.sh", "stop"), + ["claude", "plugin", "marketplace", "add", str(cli._REPO_ROOT)], + ["claude", "plugin", "update", cli._PLUGIN_SPEC], + ] + + +def test_cmd_update_claude_code_falls_back_to_install( + monkeypatch, +) -> None: calls: list[tuple[str, str]] = [] def fake_run_service(script, subcmd): calls.append((script.name, subcmd)) return 0 + def fake_run(cmd, check=False): + command = [str(part) for part in cmd] + if command[1:3] == ["plugin", "update"]: + raise subprocess.CalledProcessError(returncode=17, cmd=command) + return argparse.Namespace(returncode=0) + def fake_install(args): calls.append(("install", args.host)) assert args.refresh_existing is True assert args.marker == "kept" return 0 + monkeypatch.setattr(cli.shutil, "which", lambda name: f"/bin/{name}") + monkeypatch.setattr(cli, "_configure_reflexio_setup", lambda **_kwargs: False) monkeypatch.setattr(cli, "_run_service", fake_run_service) + monkeypatch.setattr(cli.subprocess, "run", fake_run) monkeypatch.setattr(cli, "cmd_install", fake_install) rc = cli.cmd_update(argparse.Namespace(host="claude-code", marker="kept")) diff --git a/tests/test_install_scripts.py b/tests/test_install_scripts.py index 93b71be..a718828 100644 --- a/tests/test_install_scripts.py +++ b/tests/test_install_scripts.py @@ -536,8 +536,7 @@ def test_detached_spawns_with_log_redirection_preserve_output_on_windows() -> No ) in dashboard assert ( "CLAUDE_SMART_SPAWN_KEEP_OUTPUT=1 claude_smart_spawn_detached " - '"$NEXT_BIN" start -p "$PORT" -H 127.0.0.1 >>"$LOG_FILE" 2>&1' - in dashboard + '"$NEXT_BIN" start -p "$PORT" -H 127.0.0.1 >>"$LOG_FILE" 2>&1' in dashboard ) @@ -1234,13 +1233,13 @@ def test_node_update_reads_managed_env(tmp_path: Path) -> None: ) assert result.returncode == 0, result.stderr - assert "Updating claude-smart by reinstalling from this package" in result.stdout - assert "claude-smart installed and dependencies are prepared" in result.stdout + assert "Updating claude-smart from this package" in result.stdout + assert "claude-smart updated and dependencies are prepared" in result.stdout assert "Using managed Reflexio" in result.stdout claude_log = (tmp_path / "claude.log").read_text() assert "claude plugin marketplace add" in claude_log - assert "claude plugin install claude-smart@reflexioai" in claude_log - assert "claude plugin update" not in claude_log + assert "claude plugin update claude-smart@reflexioai" in claude_log + assert "claude plugin install claude-smart@reflexioai" not in claude_log env_text = env_path.read_text() assert 'REFLEXIO_URL="https://www.reflexio.ai/"' in env_text assert 'REFLEXIO_API_KEY="rflx-test-secret"' in env_text @@ -1259,6 +1258,9 @@ def test_node_update_retries_install_after_uninstall(tmp_path: Path) -> None: claude.write_text( "#!/bin/sh\n" 'printf \'claude %s\\n\' "$*" >> "$HOME/claude.log"\n' + 'if [ "$1 $2 $3" = "plugin update claude-smart@reflexioai" ]; then\n' + " exit 19\n" + "fi\n" 'if [ "$1 $2 $3" = "plugin install claude-smart@reflexioai" ]; then\n' ' count_file="$HOME/install-count"\n' " count=0\n" @@ -1288,10 +1290,13 @@ def test_node_update_retries_install_after_uninstall(tmp_path: Path) -> None: ) assert result.returncode == 0, result.stderr + assert "falling back to reinstall" in result.stderr assert "retrying after uninstalling claude-smart@reflexioai" in result.stderr assert (tmp_path / "install-count").read_text().strip() == "2" claude_log = (tmp_path / "claude.log").read_text().splitlines() assert claude_log == [ + f"claude plugin marketplace add {REPO_ROOT}", + "claude plugin update claude-smart@reflexioai", f"claude plugin marketplace add {REPO_ROOT}", "claude plugin install claude-smart@reflexioai", "claude plugin uninstall claude-smart@reflexioai", @@ -1321,9 +1326,7 @@ def test_node_install_refreshes_same_version_cache_missing_vendor( (vendor / "pyproject.toml").write_text( '[project]\nname = "reflexio-ai"\nversion = "0.2.28"\n' ) - (vendor / "reflexio" / "__init__.py").write_text( - '__version__ = "0.2.28"\n' - ) + (vendor / "reflexio" / "__init__.py").write_text('__version__ = "0.2.28"\n') cache = ( tmp_path @@ -1348,7 +1351,7 @@ def test_node_install_refreshes_same_version_cache_missing_vendor( 'if [ "$1 $2" = "plugin uninstall" ]; then rm -rf "$dest"; exit 0; fi\n' 'if [ "$1 $2" = "plugin install" ] && [ ! -d "$dest" ]; then\n' ' mkdir -p "$(dirname "$dest")"\n' - f" cp -R {shlex.quote(str(plugin_source))} \"$dest\"\n" + f' cp -R {shlex.quote(str(plugin_source))} "$dest"\n' "fi\n" "exit 0\n", ) @@ -1417,7 +1420,9 @@ def test_node_install_ignores_cache_version_other_than_package_version( # A newer 0.2.49 is already cached and perfectly healthy — it has its own # (different) vendor bundle. Old version dirs persist in the real cache. - cache_root = tmp_path / ".claude" / "plugins" / "cache" / "reflexioai" / "claude-smart" + cache_root = ( + tmp_path / ".claude" / "plugins" / "cache" / "reflexioai" / "claude-smart" + ) newer = cache_root / "0.2.49" (newer / "scripts").mkdir(parents=True) (newer / "pyproject.toml").write_text( @@ -1436,12 +1441,12 @@ def test_node_install_ignores_cache_version_other_than_package_version( _write_executable( fake_bin / "claude", "#!/bin/sh\n" - "printf 'claude %s\\n' \"$*\" >> \"$HOME/claude.log\"\n" + 'printf \'claude %s\\n\' "$*" >> "$HOME/claude.log"\n' 'dest="$HOME/.claude/plugins/cache/reflexioai/claude-smart/0.2.48"\n' 'if [ "$1 $2" = "plugin uninstall" ]; then rm -rf "$dest"; exit 0; fi\n' 'if [ "$1 $2" = "plugin install" ] && [ ! -d "$dest" ]; then\n' ' mkdir -p "$(dirname "$dest")"\n' - f" cp -R {shlex.quote(str(plugin_source))} \"$dest\"\n" + f' cp -R {shlex.quote(str(plugin_source))} "$dest"\n' "fi\n" "exit 0\n", ) @@ -1575,7 +1580,9 @@ def test_backend_service_verifies_bundled_reflexio_runtime_identity() -> None: assert "--claude-smart-backend=1" in backend assert '"--claude-smart-plugin-root=$PLUGIN_ROOT_CANONICAL"' in backend assert '"--claude-smart-version=$PLUGIN_VERSION"' in backend - assert '"--claude-smart-reflexio-vendor-root=$VENDORED_REFLEXIO_FOR_PYTHON"' in backend + assert ( + '"--claude-smart-reflexio-vendor-root=$VENDORED_REFLEXIO_FOR_PYTHON"' in backend + ) assert "CLAUDE_SMART_BACKEND=1" in backend assert 'CLAUDE_SMART_PLUGIN_ROOT="$PLUGIN_ROOT_CANONICAL"' in backend assert 'CLAUDE_SMART_VERSION="$PLUGIN_VERSION"' in backend @@ -1688,11 +1695,9 @@ def test_node_update_reinstalls_by_host() -> None: assert "await runUpdateCodex(args)" in node_installer assert "async function runUpdateCodex(args)" in node_installer assert 'findCodexPluginRoot() || join(PACKAGE_ROOT, "plugin")' in node_installer - assert ( - "await runInstall(args, { retryInstallAfterUninstall: true })" in node_installer - ) + assert 'runClaude(["plugin", "update", PLUGIN_SPEC]' in node_installer + assert "falling back to reinstall" in node_installer assert "await runInstallCodex(args)" in node_installer - assert 'runClaude(["plugin", "update", PLUGIN_SPEC]' not in node_installer assert "update --host codex" in node_installer @@ -1921,7 +1926,10 @@ def test_integration_harness_defaults_to_isolated_runtime() -> None: assert 'BACKEND_PORT="${BACKEND_PORT:-18071}"' in integration assert 'EMBEDDING_PORT="${EMBEDDING_PORT:-18072}"' in integration assert 'DASHBOARD_PORT="${DASHBOARD_PORT:-13001}"' in integration - assert 'EMBEDDING_HEALTH_TIMEOUT_SECONDS="${EMBEDDING_HEALTH_TIMEOUT_SECONDS:-240}"' in integration + assert ( + 'EMBEDDING_HEALTH_TIMEOUT_SECONDS="${EMBEDDING_HEALTH_TIMEOUT_SECONDS:-240}"' + in integration + ) assert "within ${EMBEDDING_HEALTH_TIMEOUT_SECONDS}s" in integration assert "CLAUDE_SMART_INTEGRATION_ALLOW_PROD_PORTS" in integration assert "refusing to use production claude-smart port" in integration @@ -1959,7 +1967,11 @@ def test_integration_harness_rejects_production_ports_without_opt_in( env = _isolated_env(tmp_path) env[env_key] = port result = subprocess.run( - ["bash", str(REPO_ROOT / "tests" / "integration" / "integration.sh"), "cleanup"], + [ + "bash", + str(REPO_ROOT / "tests" / "integration" / "integration.sh"), + "cleanup", + ], env=env, text=True, capture_output=True, @@ -1980,7 +1992,11 @@ def test_integration_harness_rejects_non_numeric_ports(tmp_path: Path) -> None: env = _isolated_env(tmp_path) env["BACKEND_PORT"] = "8071abc" result = subprocess.run( - ["bash", str(REPO_ROOT / "tests" / "integration" / "integration.sh"), "cleanup"], + [ + "bash", + str(REPO_ROOT / "tests" / "integration" / "integration.sh"), + "cleanup", + ], env=env, text=True, capture_output=True, @@ -2019,7 +2035,11 @@ def test_integration_harness_sanitizes_reused_managed_env( env["CLAUDE_SMART_INTEG_HOME"] = str(home) env_file.chmod(0o644) result = subprocess.run( - ["bash", str(REPO_ROOT / "tests" / "integration" / "integration.sh"), "cleanup"], + [ + "bash", + str(REPO_ROOT / "tests" / "integration" / "integration.sh"), + "cleanup", + ], env=env, text=True, capture_output=True, @@ -2333,7 +2353,13 @@ def test_smart_install_fails_loudly_when_host_cache_has_no_vendor( succeeding and leaving the failure to the first session start. """ cache_root = ( - tmp_path / ".claude" / "plugins" / "cache" / "reflexioai" / "claude-smart" / "0.2.49" + tmp_path + / ".claude" + / "plugins" + / "cache" + / "reflexioai" + / "claude-smart" + / "0.2.49" ) _plugin_root, smart_install, env = _prepare_smart_install_sync_fixture( tmp_path, mode="fresh", plugin_root=cache_root @@ -2806,7 +2832,7 @@ def test_backend_service_restarts_stale_owned_listener_even_when_unhealthy( ) _write_executable( bin_dir / "sleep", - "#!/bin/sh\nrm -f \"$HOME/stale-listener\"\nexit 0\n", + '#!/bin/sh\nrm -f "$HOME/stale-listener"\nexit 0\n', ) env = _backend_service_env( tmp_path, @@ -2833,9 +2859,10 @@ def test_backend_service_restarts_stale_owned_listener_even_when_unhealthy( assert result.stdout.strip() == '{"continue":true}' stale.wait(timeout=5) assert "spawned backend" in (tmp_path / "python.log").read_text() - assert "older than plugin" in ( - tmp_path / ".claude-smart" / "backend.log" - ).read_text() + assert ( + "older than plugin" + in (tmp_path / ".claude-smart" / "backend.log").read_text() + ) finally: if stale.poll() is None: stale.terminate() @@ -3375,9 +3402,7 @@ def test_backend_start_waits_for_existing_service_lock_without_spawn( (lock_dir / "pid").write_text(f"{locker.pid}\n") _write_fake_plugin_python( plugin_root, - "#!/bin/sh\n" - 'printf "python %s\\n" "$*" >> "$HOME/python.log"\n' - "exit 0\n", + '#!/bin/sh\nprintf "python %s\\n" "$*" >> "$HOME/python.log"\nexit 0\n', ) bin_dir = tmp_path / "bin" bin_dir.mkdir() @@ -3427,9 +3452,7 @@ def test_backend_start_honors_startup_grace_for_recorded_pid( ) _write_fake_plugin_python( plugin_root, - "#!/bin/sh\n" - 'printf "python %s\\n" "$*" >> "$HOME/python.log"\n' - "exit 0\n", + '#!/bin/sh\nprintf "python %s\\n" "$*" >> "$HOME/python.log"\nexit 0\n', ) bin_dir = tmp_path / "bin" bin_dir.mkdir() @@ -3467,9 +3490,10 @@ def test_backend_start_honors_startup_grace_for_recorded_pid( assert "spawned backend" not in ( python_log.read_text() if python_log.exists() else "" ) - assert "recorded backend process is still running but not healthy yet" in ( - state_dir / "backend.log" - ).read_text() + assert ( + "recorded backend process is still running but not healthy yet" + in (state_dir / "backend.log").read_text() + ) finally: if warming.poll() is None: warming.terminate() @@ -3494,7 +3518,7 @@ def test_service_lock_recovers_dead_stale_lock(tmp_path: Path) -> None: ( f'. "{LIB}"; ' "CLAUDE_SMART_SERVICE_LOCK_STALE_SECONDS=60 " - "claude_smart_service_lock backend; cat \"$HOME/.claude-smart/backend.start.lock/pid\"" + 'claude_smart_service_lock backend; cat "$HOME/.claude-smart/backend.start.lock/pid"' ), ], env=env, @@ -3641,9 +3665,10 @@ def test_backend_service_preflight_rejects_non_bundled_reflexio( assert "hookSpecificOutput" in result.stdout python_log = (tmp_path / "python.log").read_text() assert "spawned backend" not in python_log - assert "bundled Reflexio import preflight failed" in ( - tmp_path / ".claude-smart" / "backend.log" - ).read_text() + assert ( + "bundled Reflexio import preflight failed" + in (tmp_path / ".claude-smart" / "backend.log").read_text() + ) def test_backend_service_preflight_missing_vendor_preserves_services( @@ -3864,7 +3889,9 @@ def test_codex_hook_recovers_missing_dependencies_without_cli_command() -> None: assert "function runServiceScript(root, scriptName, action, logName)" in script assert 'path.join(root, "scripts", scriptName)' in script assert "[script, action]" in script - assert 'runServiceScript(root, "backend-service.sh", "start", "backend.log")' in script + assert ( + 'runServiceScript(root, "backend-service.sh", "start", "backend.log")' in script + ) assert ( 'runServiceScript(root, "dashboard-service.sh", "start", "dashboard.log")' in script @@ -4937,7 +4964,9 @@ def test_package_tarball_ships_generated_marketplace_manifest(tmp_path: Path) -> archive.extractall(extract_dir) manifest_path = extract_dir / "package" / ".claude-plugin" / "marketplace.json" - assert manifest_path.is_file(), "npm tarball is missing the Claude marketplace manifest" + assert manifest_path.is_file(), ( + "npm tarball is missing the Claude marketplace manifest" + ) manifest = json.loads(manifest_path.read_text()) expected = json.loads((REPO_ROOT / "package.json").read_text())["version"]