diff --git a/docs/STATUS.md b/docs/STATUS.md index 397efcd..f1e9bba 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -117,9 +117,19 @@ Everything here needs hardware or a session this branch was written without. ## Open — older, still true -- **Never exercised, still:** `StopFailure` · `Elicitation` · - permission prompts in the terminal CLI · alert stack compression · click-to-focus - for anything other than VS Code. +- **Never exercised, still:** `StopFailure` · `Elicitation` · alert stack + compression · click-to-focus for anything other than VS Code. +- **Permission prompts in the terminal CLI: verified 2026-08-20.** A CLI session + on `gl-login4` was made to edit a tracked source file, `Notification` fired with + `notification_type=permission_prompt`, the event reached `events.log`, and the + Mac drew the 🔑 alert. That is release 2's one distinct capability — the chat + panel has no event for this — confirmed end to end for the first time. + + **Two earlier attempts failed and both were the test's fault, not the code's.** + `uptime` and `touch /tmp/…` are auto-approved by Claude Code without raising a + permission event at all, so nothing fired and the run looked like a tailbell + bug. Anyone re-testing this must pick an action Claude Code will genuinely + stop on — editing a tracked file works. - The prototype this grew out of still sits in `~/.claude/hooks/` (`cc-notify.sh`, `cc-hammerspoon.lua`, `gl-listen.py`, …). It is dead code on a live machine — easy to edit by mistake. One part of it is **not** dead: `probe.sh` is still diff --git a/docs/roadmap.md b/docs/roadmap.md index c9c85ca..93200a2 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -14,7 +14,7 @@ would be wrong. | --- | --- | --- | --- | | 0 | `v0-ssh-vscode-chatbox` | Claude Code over SSH in the VS Code chat panel → notification on macOS. Zero third-party software or service. | ✅ done | | 1 | `v1-rich-presentation` | Centered, clickable overlay: close button, fade in/out, click to focus the originating editor window. Hammerspoon as an **optional** dependency; release 0's zero-dependency path stays the guarantee. | small | -| 2 | `v2-ssh-terminal-cli` | Claude Code CLI in an SSH terminal. Mostly already working — the addition is `Notification`, which fires in the CLI and covers permission prompts that the chat panel cannot report. | small | +| 2 | `v2-ssh-terminal-cli` | Claude Code CLI in an SSH terminal. The addition is `Notification`, which fires in the CLI and covers permission prompts the chat panel cannot report. **Verified end to end 2026-08-20** — a permission prompt on `gl-login4` drew the 🔑 alert on the Mac. | ✅ done | | 3 | `v3-ssh-any-frontend` | Any editor, terminal or MCP frontend over SSH. Largely documentation and a test matrix rather than new code, for the same reason: hooks are process-level. | small | | 4 | `v4-local-and-remote` | Same behaviour whether Claude Code runs on the cluster or on the Mac. The local path already exists; this release is about making one install cover both without branching config. | medium | | 5 | `v5-claude-and-codex` | Codex as well as Claude Code. **First real architectural step:** an adapter layer, because each agent's event model differs. Needs a stable internal event schema first. | large | diff --git a/tests/fake-desktop b/tests/fake-desktop new file mode 100755 index 0000000..f9d2fe4 --- /dev/null +++ b/tests/fake-desktop @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Stands in for anything that would reach the user's actual screen or speakers: +# osascript, afplay, say. Records the call and returns success. +# +# This exists because the suite drew real alerts on a real Mac. On the cluster +# where most of it was written, the desktop sink is unreachable and every macOS +# path is dead code, so nothing was visible; on a Mac the same cases pop an +# overlay and play a sound at whoever is running the tests. A recorder here is +# the same seam TAILBELL_CURL already provides for the network. +# +# FAKE_DESKTOP_RECORD names the log; without it the call is simply swallowed. +set -u +if [ -n "${FAKE_DESKTOP_RECORD:-}" ]; then + { printf 'CALL|%s|' "$(basename "$0")" + for a in ${@+"$@"}; do printf '%s ' "$a"; done + printf '\n' + } >> "$FAKE_DESKTOP_RECORD" 2>/dev/null +fi +exit 0 diff --git a/tests/run-tests.sh b/tests/run-tests.sh index a06dfec..8c240e1 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -24,6 +24,15 @@ export TAILBELL_LOG="$TMP/events.log" export TAILBELL_DEBUG_LOG="$TMP/debug.log" export TAILBELL_STATE_DIR="$TMP/state" export TAILBELL_MIN_SECONDS=60 +# Nothing in this suite may reach the screen or the speakers. On the cluster that +# was free -- every macOS path is dead code there -- so it went unnoticed until +# the suite was run on a real Mac and popped overlays at the person running it. +# Same seam as TAILBELL_CURL, for the same reason. +export TAILBELL_OSASCRIPT="$REPO/tests/fake-desktop" +export TAILBELL_AFPLAY="$REPO/tests/fake-desktop" +export TAILBELL_SAY="$REPO/tests/fake-desktop" +export TAILBELL_RENDERER="osascript" # never probe for a live Hammerspoon +export FAKE_DESKTOP_RECORD="$TMP/desktop.log" mkdir -p "$TAILBELL_STATE_DIR" : > "$TAILBELL_LOG" @@ -392,6 +401,15 @@ export TAILBELL_CHANNELS="file" ######################################################################## echo echo "── 声音与语音 ──" +# The seams must be pointed at the recorder for the whole run, not just inside +# the cases below. Without this the suite passes identically on the cluster and +# draws real overlays on a Mac -- which is exactly how it shipped. +if [ "$TAILBELL_OSASCRIPT" = "$REPO/tests/fake-desktop" ] \ + && [ "$TAILBELL_AFPLAY" = "$REPO/tests/fake-desktop" ] \ + && [ "$TAILBELL_SAY" = "$REPO/tests/fake-desktop" ] \ + && [ -x "$REPO/tests/fake-desktop" ]; then + ok "屏幕与扬声器的三个出口全程指向录音器" +else no "有出口没被拦住 —— 在 Mac 上跑会弹真弹窗"; fi # # tailbell-show is macOS code, and this suite runs on the cluster. What IS # testable here is every decision it makes before touching a Mac API: which @@ -674,12 +692,30 @@ else no "install.sh 没生成 cluster-id —— 监听器会静默跳过这台 # point with no coverage at all. Cloned from this working tree at this commit, so # it needs no network either. OH="$TMP/onelinerhome"; mkdir -p "$OH" +# ⚠️ The one-liner branches on `uname`, and on Darwin it runs mac/install.sh, +# which writes a LaunchAgent and `launchctl load`s it. Run unguarded on a Mac +# this test installed a listener into the tester's own login session, pointed at +# this throwaway HOME. The temp directory was later cleaned; the loaded job was +# not, and went on drawing test fixtures onto a real screen for days. +# +# So the machine is told it is Linux for this one call. What is under test here +# is the agent side the one-liner installs -- the workstation installer has its +# own coverage and must never be executed by the suite. +mkdir -p "$TMP/unamebin" +printf '#!/bin/sh\necho Linux\n' > "$TMP/unamebin/uname" +chmod +x "$TMP/unamebin/uname" HOME="$OH" TAILBELL_REPO="$REPO" TAILBELL_DIR="$OH/tailbell" \ TAILBELL_BRANCH="$(git -C "$REPO" rev-parse HEAD)" \ + PATH="$TMP/unamebin:$PATH" \ bash "$REPO/packaging/get-tailbell.sh" >/dev/null 2>&1 if [ -s "$OH/.tailbell/cluster-id" ] && [ -x "$OH/tailbell/bin/tailbell-notify" ]; then ok "一行安装脚本能装出一个完整的 agent 侧" else no "一行安装脚本没装成"; fi +# The bug this guards: a LaunchAgent loaded into the tester's session, pointing +# at a temp directory, outliving both the test and the directory. +if [ -z "$(ls -A "$OH/Library/LaunchAgents" 2>/dev/null)" ]; then + ok "并且没有往用户会话里装 LaunchAgent" +else no "测试装了一个 LaunchAgent —— 它会活过这次测试"; fi # And that what it installed actually notifies, which is the claim being made. s=$(sid o001) mkdir -p "$OH/.tailbell/state"; echo $(( $(date +%s) - 400 )) > "$OH/.tailbell/state/$s.start"