From 05bb09c083c4ef0bdc1ef24e2212c6a759f62c9a Mon Sep 17 00:00:00 2001 From: Yi Lu Date: Tue, 26 May 2026 12:24:04 -0700 Subject: [PATCH] fix: start claude-smart backend during install --- bin/claude-smart.js | 20 +++++-- plugin/hooks/codex-hooks.json | 5 -- plugin/hooks/hooks.json | 10 ---- plugin/scripts/backend-service.sh | 10 ++-- plugin/scripts/codex-hook.js | 45 +++------------- plugin/scripts/dashboard-service.sh | 11 ++-- plugin/scripts/hook_entry.sh | 41 +++++---------- plugin/scripts/smart-install.sh | 12 ++++- tests/test_codex_support.py | 5 +- tests/test_install_scripts.py | 81 ++++++++++++++++++----------- 10 files changed, 110 insertions(+), 130 deletions(-) diff --git a/bin/claude-smart.js b/bin/claude-smart.js index 51cadbe..a7da038 100755 --- a/bin/claude-smart.js +++ b/bin/claude-smart.js @@ -508,14 +508,14 @@ function runChecked(command, args, options = {}) { }); } -function runPluginService(pluginRoot, scriptName, subcommand) { +function runPluginService(pluginRoot, scriptName, subcommand, envOverrides = {}) { const script = join(pluginRoot, "scripts", scriptName); if (!existsSync(script)) return false; const bash = resolveCommand(isWindows() ? ["bash.exe", "bash"] : ["bash"]); if (!bash) return false; const result = spawnSync(bash, [script, subcommand], { cwd: pluginRoot, - env: runtimeEnv(), + env: { ...runtimeEnv(), ...envOverrides }, stdio: "ignore", windowsHide: true, timeout: PLUGIN_SERVICE_TIMEOUT_MS, @@ -542,6 +542,12 @@ function refreshDashboardService(pluginRoot) { return runPluginService(pluginRoot, "dashboard-service.sh", "start"); } +function startBackendService(pluginRoot, host) { + return runPluginService(pluginRoot, "backend-service.sh", "start", { + CLAUDE_SMART_HOST: host, + }); +} + function stopClaudeSmartServices(pluginRoot) { runPluginService(pluginRoot, "dashboard-service.sh", "stop"); runPluginService(pluginRoot, "backend-service.sh", "stop"); @@ -834,9 +840,7 @@ function patchCodexHooksForNode(pluginRoot, nodePath) { const runner = join(pluginRoot, "scripts", "codex-hook.js"); const command = (...args) => [nodePath, runner, ...args].map(quoteCommandPart).join(" "); // Dispatch by command content rather than index — entries can be added or - // reordered (e.g. the SessionStart install hook at index 0) without - // breaking the patch. Entries that must run as bash (smart-install.sh) - // are left untouched. + // reordered without breaking the patch. const patchOne = (original) => { if (typeof original !== "string") return original; if (original.includes("smart-install.sh")) return original; @@ -1512,6 +1516,9 @@ async function runInstall(args) { process.stdout.write("Installed read-only hook manifest; publish interactions hooks are disabled.\n"); } process.stdout.write(`Prepared claude-smart runtime at ${pluginRoot}.\n`); + if (startBackendService(pluginRoot, "claude-code")) { + process.stdout.write("Started claude-smart backend service.\n"); + } if (refreshDashboardService(pluginRoot)) { process.stdout.write("Refreshed claude-smart dashboard service.\n"); } @@ -1592,6 +1599,9 @@ async function runInstallCodex(args) { if (readOnly) { process.stdout.write("Installed read-only hook manifest; publish interactions hooks are disabled.\n"); } + if (startBackendService(cacheDir, "codex")) { + process.stdout.write("Started claude-smart backend service.\n"); + } if (refreshDashboardService(cacheDir)) { process.stdout.write("Refreshed claude-smart dashboard service.\n"); } diff --git a/plugin/hooks/codex-hooks.json b/plugin/hooks/codex-hooks.json index 62b5430..27ecda7 100644 --- a/plugin/hooks/codex-hooks.json +++ b/plugin/hooks/codex-hooks.json @@ -5,11 +5,6 @@ { "matcher": "startup|resume", "hooks": [ - { - "type": "command", - "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/smart-install.sh\" || true", - "timeout": 300 - }, { "type": "command", "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/ensure-plugin-root.sh\" \"$_R\" || true", diff --git a/plugin/hooks/hooks.json b/plugin/hooks/hooks.json index 7688c70..9d5f626 100644 --- a/plugin/hooks/hooks.json +++ b/plugin/hooks/hooks.json @@ -14,16 +14,6 @@ } ], "SessionStart": [ - { - "matcher": "startup|clear|compact|resume", - "hooks": [ - { - "type": "command", - "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/smart-install.sh\"", - "timeout": 300 - } - ] - }, { "matcher": "startup|clear|compact|resume", "hooks": [ diff --git a/plugin/scripts/backend-service.sh b/plugin/scripts/backend-service.sh index aa406b6..64778cf 100755 --- a/plugin/scripts/backend-service.sh +++ b/plugin/scripts/backend-service.sh @@ -201,13 +201,13 @@ case "$CMD" in fi if ! command -v uv >/dev/null 2>&1; then if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" != "1" ] && [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then - claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH; running installer" - CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >>"$STATE_DIR/install.log" 2>&1 || true - claude_smart_source_login_path - claude_smart_prepend_astral_bins + claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH; starting installer in background" + claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \ + bash "$PLUGIN_ROOT/scripts/smart-install.sh" \ + >>"$STATE_DIR/install.log" 2>&1 || true fi if ! command -v uv >/dev/null 2>&1; then - claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH after installer; skipping" + claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH; installer recovery scheduled; skipping" emit_ok; exit 0 fi fi diff --git a/plugin/scripts/codex-hook.js b/plugin/scripts/codex-hook.js index 68e5ecc..4fd2c5c 100644 --- a/plugin/scripts/codex-hook.js +++ b/plugin/scripts/codex-hook.js @@ -302,33 +302,6 @@ function detached(command, args, options = {}) { return child.pid; } -function runInstaller(root, reason) { - if (process.env.CLAUDE_SMART_BOOTSTRAPPING === "1") return false; - const script = path.join(root, "scripts", "smart-install.sh"); - if (!fs.existsSync(script)) return false; - const bash = bashPath(); - if (!bash) return false; - appendLog("backend.log", `[claude-smart] ${reason}; running installer`); - const result = spawnSync(bash, [script], { - cwd: root, - env: { - ...process.env, - CLAUDE_SMART_BOOTSTRAPPING: "1", - }, - encoding: "utf8", - maxBuffer: 20 * 1024 * 1024, - windowsHide: true, - }); - const output = `${result.stdout || ""}${result.stderr || ""}`.trim(); - if (output) { - ensureDir(STATE_DIR); - fs.appendFileSync(path.join(STATE_DIR, "install.log"), `${output}\n`); - trimLog(path.join(STATE_DIR, "install.log")); - } - prependRuntimePath(); - return result.status === 0; -} - function startInstallerDetached(root, reason) { if (process.env.CLAUDE_SMART_BOOTSTRAPPING === "1") return false; const script = path.join(root, "scripts", "smart-install.sh"); @@ -410,11 +383,11 @@ async function startBackend(root) { } const uv = uvPath(); if (!uv) { - runInstaller(root, "backend: uv not on PATH"); + startInstallerDetached(root, "backend: uv not on PATH"); } const readyUv = uvPath(); if (!readyUv) { - appendLog("backend.log", "[claude-smart] backend: uv not on PATH after installer; skipping"); + appendLog("backend.log", "[claude-smart] backend: uv not on PATH; installer recovery scheduled; skipping"); emitOk(); return; } @@ -478,11 +451,11 @@ async function startDashboard(root) { } const npm = npmPath(); if (!npm) { - runInstaller(root, "dashboard: npm not on PATH"); + startInstallerDetached(root, "dashboard: npm not on PATH"); } const readyNpm = npmPath(); if (!readyNpm) { - appendLog("dashboard.log", "[claude-smart] dashboard: npm not on PATH after installer; skipping"); + appendLog("dashboard.log", "[claude-smart] dashboard: npm not on PATH; installer recovery scheduled; skipping"); emitOk(); return; } @@ -512,14 +485,10 @@ function runHook(root, event) { trimLog(path.join(STATE_DIR, "backend.log")); let uv = uvPath(); if (!uv) { - if (event === "session-start") { - runInstaller(root, "hook: uv not on PATH"); - uv = uvPath(); - } else { - startInstallerDetached(root, "hook: uv not on PATH"); - } + startInstallerDetached(root, "hook: uv not on PATH"); + uv = uvPath(); if (!uv) { - appendLog("backend.log", "[claude-smart] hook: uv not on PATH after installer; skipping"); + appendLog("backend.log", "[claude-smart] hook: uv not on PATH; installer recovery scheduled; skipping"); emitHookOk(); return 0; } diff --git a/plugin/scripts/dashboard-service.sh b/plugin/scripts/dashboard-service.sh index 26256cb..b292384 100755 --- a/plugin/scripts/dashboard-service.sh +++ b/plugin/scripts/dashboard-service.sh @@ -190,14 +190,13 @@ case "$CMD" in NPM_BIN=$(claude_smart_resolve_npm || true) if [ -z "$NPM_BIN" ] || ! "$NPM_BIN" --version >/dev/null 2>&1; then if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" != "1" ] && [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then - echo "[claude-smart] dashboard: npm is not on PATH; running installer" >>"$LOG_FILE" - CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >>"$STATE_DIR/install.log" 2>&1 || true - claude_smart_source_login_path - claude_smart_prepend_node_bins - NPM_BIN=$(claude_smart_resolve_npm || true) + echo "[claude-smart] dashboard: npm is not on PATH; starting installer in background" >>"$LOG_FILE" + claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \ + bash "$PLUGIN_ROOT/scripts/smart-install.sh" \ + >>"$STATE_DIR/install.log" 2>&1 || true fi if [ -z "$NPM_BIN" ] || ! "$NPM_BIN" --version >/dev/null 2>&1; then - reason="npm is not on PATH after installer; dashboard cannot start" + reason="npm is not on PATH; installer recovery scheduled; dashboard cannot start yet" echo "[claude-smart] dashboard: $reason; skipping" >>"$LOG_FILE" claude_smart_write_dashboard_unavailable "$reason" emit_ok; exit 0 diff --git a/plugin/scripts/hook_entry.sh b/plugin/scripts/hook_entry.sh index a70a2f9..c496dac 100755 --- a/plugin/scripts/hook_entry.sh +++ b/plugin/scripts/hook_entry.sh @@ -85,29 +85,18 @@ PY fi if ! command -v uv >/dev/null 2>&1; then - # Self-heal from skipped Setup/SessionStart bootstrap. SessionStart can - # afford to wait because it has the install budget; prompt/tool hooks start - # the same installer detached so normal work is not blocked by first-run - # dependency setup. + # Self-heal from skipped setup without blocking the first prompt. The + # installer and Setup hook own dependency bootstrap; hooks only schedule + # recovery and return so SessionStart stays lightweight. if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" = "1" ]; then claude_smart_emit_continue exit 0 fi if [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then mkdir -p "$STATE_DIR" - if [ "$EVENT" = "session-start" ]; then - CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2 - claude_smart_prepend_astral_bins - claude_smart_prepend_node_bins - if command -v uv >/dev/null 2>&1; then - bash "$HERE/backend-service.sh" start >/dev/null 2>&1 || true - bash "$HERE/dashboard-service.sh" start >/dev/null 2>&1 || true - fi - else - claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \ - bash "$PLUGIN_ROOT/scripts/smart-install.sh" \ - >>"$STATE_DIR/install.log" 2>&1 || true - fi + claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \ + bash "$PLUGIN_ROOT/scripts/smart-install.sh" \ + >>"$STATE_DIR/install.log" 2>&1 || true fi if ! command -v uv >/dev/null 2>&1; then claude_smart_emit_continue @@ -128,17 +117,13 @@ ensure_hook_package_importable() { fi mkdir -p "$STATE_DIR" - if [ "$EVENT" = "session-start" ]; then - CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2 - else - { - printf '%s\n' "[claude-smart] hook: claude_smart.hook is not importable; retrying install in background" - date 2>/dev/null || true - } >>"$STATE_DIR/install.log" 2>&1 - claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \ - bash "$PLUGIN_ROOT/scripts/smart-install.sh" \ - >>"$STATE_DIR/install.log" 2>&1 || true - fi + { + printf '%s\n' "[claude-smart] hook: claude_smart.hook is not importable; retrying install in background" + date 2>/dev/null || true + } >>"$STATE_DIR/install.log" 2>&1 + claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \ + bash "$PLUGIN_ROOT/scripts/smart-install.sh" \ + >>"$STATE_DIR/install.log" 2>&1 || true claude_smart_python_imports "$PLUGIN_ROOT" claude_smart.hook } diff --git a/plugin/scripts/smart-install.sh b/plugin/scripts/smart-install.sh index 8a0d15a..d0c7f72 100755 --- a/plugin/scripts/smart-install.sh +++ b/plugin/scripts/smart-install.sh @@ -118,6 +118,13 @@ install_complete() { return 0 } +start_backend_service() { + if [ -x "$HERE/backend-service.sh" ]; then + echo "[claude-smart] starting backend service in background" >&2 + bash "$HERE/backend-service.sh" start >/dev/null 2>&1 || true + fi +} + install_vendored_reflexio() { local VENDORED_REFLEXIO PLUGIN_PYTHON @@ -391,6 +398,7 @@ fi preflight_supported_runtime_platform if install_complete; then + start_backend_service claude_smart_emit_continue exit 0 fi @@ -624,6 +632,8 @@ if ! bash "$HERE/ensure-plugin-root.sh" "$PLUGIN_ROOT"; then echo "[claude-smart] WARNING: failed to set ~/.reflexio/plugin-root symlink — slash commands may not resolve" >&2 fi +start_backend_service + write_success_marker -echo "[claude-smart] install complete. Backend and dashboard auto-start on session start." >&2 +echo "[claude-smart] install complete. Backend started; dashboard auto-starts on session start." >&2 claude_smart_emit_continue diff --git a/tests/test_codex_support.py b/tests/test_codex_support.py index 385a018..9d49678 100644 --- a/tests/test_codex_support.py +++ b/tests/test_codex_support.py @@ -509,7 +509,10 @@ def fake_run_codex(args: list[str]): rc = cli.cmd_install(argparse.Namespace(host="codex", source="unused")) assert rc == 0 - assert not env_path.exists() + env_text = env_path.read_text() + assert "CLAUDE_SMART_USE_LOCAL_CLI=1" in env_text + assert "CLAUDE_SMART_USE_LOCAL_EMBEDDING=1" in env_text + assert 'CLAUDE_SMART_READ_ONLY="0"' in env_text config = config_path.read_text() assert "plugin_hooks = true" in config assert '[plugins."claude-smart@reflexioai"]' in config diff --git a/tests/test_install_scripts.py b/tests/test_install_scripts.py index 53960d8..b8238a2 100644 --- a/tests/test_install_scripts.py +++ b/tests/test_install_scripts.py @@ -261,20 +261,30 @@ def test_service_start_scripts_recover_missing_dependencies_without_cli_command( backend = (REPO_ROOT / "plugin" / "scripts" / "backend-service.sh").read_text() dashboard = (REPO_ROOT / "plugin" / "scripts" / "dashboard-service.sh").read_text() - assert "[claude-smart] backend: uv not on PATH; running installer" in backend assert ( - 'CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh"' + "[claude-smart] backend: uv not on PATH; starting installer in background" in backend ) - assert "[claude-smart] backend: uv not on PATH after installer; skipping" in backend assert ( - "[claude-smart] dashboard: npm is not on PATH; running installer" in dashboard + 'claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1' + in backend + ) + assert ( + "[claude-smart] backend: uv not on PATH; installer recovery scheduled; skipping" + in backend + ) + assert ( + "[claude-smart] dashboard: npm is not on PATH; starting installer in background" + in dashboard + ) + assert ( + 'claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1' + in dashboard ) assert ( - 'CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh"' + "npm is not on PATH; installer recovery scheduled; dashboard cannot start yet" in dashboard ) - assert "npm is not on PATH after installer; dashboard cannot start" in dashboard def test_backend_service_skips_local_start_for_remote_reflexio_url() -> None: @@ -842,18 +852,33 @@ def test_dashboard_service_restarts_stale_claude_smart_dashboard() -> None: assert "foreign app on 3001 is never killed" in dashboard -def test_installers_refresh_or_stop_dashboard_services() -> None: +def test_installers_start_backend_and_refresh_dashboard_services() -> None: setup = SETUP_LOCAL_DEV.read_text() node_installer = NODE_INSTALLER.read_text() + smart_install = SMART_INSTALL.read_text() assert "refresh_local_dashboard()" in setup + assert "restart_local_backend()" in setup + assert 'bash "$backend_script" start' in setup + assert 'bash "$HERE/backend-service.sh" start' in smart_install + assert "start_backend_service()" in smart_install + assert "if install_complete; then\n start_backend_service" in smart_install + assert "Backend started; dashboard auto-starts on session start." in smart_install assert 'bash "$PLUGIN_ROOT/scripts/dashboard-service.sh" stop' in setup assert 'bash "$PLUGIN_ROOT/scripts/dashboard-service.sh" start' in setup + assert "function startBackendService(pluginRoot, host)" in node_installer + assert "CLAUDE_SMART_HOST: host" in node_installer assert "function refreshDashboardService(pluginRoot)" in node_installer assert "const PLUGIN_SERVICE_TIMEOUT_MS = 15_000" in node_installer assert "timeout: PLUGIN_SERVICE_TIMEOUT_MS" in node_installer assert 'killSignal: "SIGTERM"' in node_installer assert 'result.error && result.error.code === "ETIMEDOUT"' in node_installer + assert ( + 'runPluginService(pluginRoot, "backend-service.sh", "start"' + in node_installer + ) + assert 'startBackendService(pluginRoot, "claude-code")' in node_installer + assert 'startBackendService(cacheDir, "codex")' in node_installer assert ( 'runPluginService(pluginRoot, "dashboard-service.sh", "stop")' in node_installer ) @@ -862,6 +887,7 @@ def test_installers_refresh_or_stop_dashboard_services() -> None: in node_installer ) assert "function stopClaudeSmartServices(pluginRoot)" in node_installer + assert "Started claude-smart backend service." in node_installer assert "Refreshed claude-smart dashboard service." in node_installer @@ -1255,11 +1281,10 @@ def test_backend_service_configures_shared_embedding_daemon() -> None: def test_codex_hook_recovers_missing_dependencies_without_cli_command() -> None: script = CODEX_HOOK.read_text() - assert "function runInstaller(root, reason)" in script assert "function startInstallerDetached(root, reason)" in script - assert 'runInstaller(root, "backend: uv not on PATH")' in script - assert 'runInstaller(root, "dashboard: npm not on PATH")' in script - assert 'runInstaller(root, "hook: uv not on PATH")' in script + assert 'startInstallerDetached(root, "backend: uv not on PATH")' in script + assert 'startInstallerDetached(root, "dashboard: npm not on PATH")' in script + assert 'startInstallerDetached(root, "hook: uv not on PATH")' in script def test_smart_install_waits_on_existing_lock() -> None: @@ -1357,12 +1382,15 @@ def test_smart_install_reaps_stale_portable_lock_without_flock(tmp_path: Path) - def test_claude_code_install_hook_matches_session_start_modes() -> None: hooks = json.loads((REPO_ROOT / "plugin" / "hooks" / "hooks.json").read_text()) - install_block = hooks["hooks"]["SessionStart"][0] - runtime_block = hooks["hooks"]["SessionStart"][1] + setup_block = hooks["hooks"]["Setup"][0] + runtime_block = hooks["hooks"]["SessionStart"][0] - assert install_block["matcher"] == runtime_block["matcher"] - assert install_block["matcher"] == "startup|clear|compact|resume" - assert "smart-install.sh" in install_block["hooks"][0]["command"] + assert "smart-install.sh" in setup_block["hooks"][0]["command"] + assert runtime_block["matcher"] == "startup|clear|compact|resume" + assert all( + "smart-install.sh" not in hook["command"] + for hook in runtime_block["hooks"] + ) session_start_hook = runtime_block["hooks"][1] assert "hook_entry.sh" in session_start_hook["command"] assert session_start_hook["timeout"] == 300 @@ -1397,13 +1425,10 @@ def test_codex_session_start_hook_has_install_recovery_budget() -> None: def test_hook_entry_self_heals_missing_uv_without_cli_command() -> None: script = HOOK_ENTRY.read_text() - assert ( - 'CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh"' - in script - ) assert "claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1" in script - assert 'bash "$HERE/backend-service.sh" start' in script - assert 'bash "$HERE/dashboard-service.sh" start' in script + assert 'bash "$PLUGIN_ROOT/scripts/smart-install.sh"' in script + assert 'bash "$HERE/backend-service.sh" start' not in script + assert 'bash "$HERE/dashboard-service.sh" start' not in script def test_install_complete_requires_importable_hook_package() -> None: @@ -1743,8 +1768,7 @@ def test_node_installer_bootstraps_runtime_with_private_node_and_uv( uv_bin.mkdir(parents=True) (plugin_root / "pyproject.toml").write_text("[project]\nname='claude-smart'\n") # Use realistic command fixtures so the content-based patcher in - # patchCodexHooksForNode can dispatch by script name. The install hook - # at index 0 must survive patching because it has to run as bash. + # patchCodexHooksForNode can dispatch by script name. (hooks / "codex-hooks.json").write_text( json.dumps( { @@ -1752,7 +1776,6 @@ def test_node_installer_bootstraps_runtime_with_private_node_and_uv( "SessionStart": [ { "hooks": [ - {"command": 'bash "$_R/scripts/smart-install.sh"'}, { "command": ( 'bash "$_R/scripts/ensure-plugin-root.sh" "$_R"' @@ -1858,11 +1881,7 @@ def test_node_installer_bootstraps_runtime_with_private_node_and_uv( assert "npm run build" in npm_log parsed = json.loads((hooks / "codex-hooks.json").read_text()) session_hooks = parsed["hooks"]["SessionStart"][0]["hooks"] - # Index 0 is smart-install.sh — must run as bash, not through node. - install_cmd = session_hooks[0]["command"] - assert "smart-install.sh" in install_cmd - assert "codex-hook.js" not in install_cmd - # Indices 1-4 dispatch to node by command content. Verify each maps to + # Each SessionStart hook dispatches to node by command content. Verify each maps to # the right codex-hook.js subcommand and references the private node. # Each arg is independently shell-quoted, so the sub-event appears as # `"hook" "session-start"` rather than `hook session-start`. @@ -1872,7 +1891,7 @@ def test_node_installer_bootstraps_runtime_with_private_node_and_uv( ["dashboard"], ["hook", "session-start"], ] - for idx, sub_tokens in enumerate(expected_subs, start=1): + for idx, sub_tokens in enumerate(expected_subs): cmd = session_hooks[idx]["command"] assert str(private_node / "node") in cmd, f"index {idx}: {cmd}" assert "codex-hook.js" in cmd, f"index {idx}: {cmd}"