Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 42 additions & 16 deletions .STATUS
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,30 @@
## Project: flow-cli
## Type: zsh-plugin
## Status: active
## Focus: post-release
## Focus: --help
## Phase: Released
## Priority: 2
## Progress: 100

## Current Session (2026-05-13)

**Session activity:**
- feat(doctor): wired lib/doctor-cache.zsh into legacy GitHub token validation — fingerprint cache key (sha256 prefix of token, 1h TTL), `--no-cache` flag, JSON envelope `{http_code, username}`
- refactor(doctor): extracted `_doctor_check_github_token(no_cache)` helper; doctor() now dispatches via one line; helper is independently testable
- test isolation: `DOCTOR_CACHE_DIR` exported pre-source (cache lib marks it readonly post-source)
- tests: +4 functions calling helper directly (not full doctor) — cuts test runtime by ~14s
- empirical timing: cold flow doctor 11.3s → warm 10.8s (~5%, smaller than orchestrate estimate because `_dots_doctor_integration` was already provider-cached)
- result: 53 test files pass, 1 expected interactive timeout; test-doctor 25s standalone, exit 0 under `timeout 30`
- discovered (filed): test-framework `create_mock` save/restore is broken for binaries — `tail -n +2` strips opening `{` but keeps closing `}`, eval parse error on second mock
- discovered (filed): `_doctor_cache_acquire_lock` mkdir-fallback leaks state across in-process invocations; cache_set rc=1 on second call with same key

**Correction (2026-05-13):** Commit `0880f924` (first push of this branch) introduced a test-doctor timeout regression that the commit message and .STATUS both misreported as "52/52 passing". Run-all.sh actually reported "2 timeout" — only one (e2e-em-dispatcher) is documented as expected; the other was test-doctor running ~40s standalone under the 30s `timeout` wrapper in `tests/run-all.sh`. Caught during the optional Step 4 follow-up review when re-checking `time timeout 30 zsh tests/test-doctor.zsh`. Fixed in the follow-up refactor commit by extracting `_doctor_check_github_token` so cache tests skip the slow system-check section.

**Previous Session (2026-05-13 earlier):**
- fix(tests): test-doctor timeout under run-all.sh — cached `doctor --verbose` output in setup() and reused for both `--verbose` and `-v` tests (which test identical output since `-v` aliases `--verbose` per commands/doctor.zsh:39)
- root cause: 3 redundant `$(doctor ...)` calls in test, each hitting `curl https://api.github.com/user` (commands/doctor.zsh:405) for ~5-8s of network wait
- timing: 38.86s standalone → 23.51s wrapped (40% reduction), 6.5s headroom under 30s ceiling
- root cause: 3 redundant `$(doctor ...)` calls in test, each hitting `curl https://api.github.com/user` for ~5-8s of network wait
- timing: 38.86s standalone → 23.51s wrapped (40% reduction)
- result: 23/23 passing, exit 0 under `timeout 30 zsh ...`
- filed pending: commands/doctor.zsh:405 bypasses lib/doctor-cache.zsh

## Previous Session (2026-03-11)

Expand Down Expand Up @@ -241,12 +252,26 @@
- Current coverage: ~50% (348 functions documented)
- Target: 80%

### Doctor command bypasses its own cache (filed 2026-05-13)
- `lib/doctor-cache.zsh` (25KB) provides a file-based cache at `~/.flow/cache/doctor/`
- `commands/doctor.zsh:405` calls `curl https://api.github.com/user` directly, no cache check
- Impact: every `flow doctor` invocation hits the network (~5-8s) for a result that's stable for hours
- Discovered while debugging test-doctor timeout; fix shape: wrap the curl with `_doctor_cache_get`/`_doctor_cache_set` (key e.g. `token-github-validation`, TTL ~1 hour)
- Would also let test-doctor.zsh be simpler (fewer manual caching workarounds)
### Test framework `create_mock` parse error for binaries (filed 2026-05-13)
- `tests/test-framework.zsh:351-368` — `whence -f $fn | tail -n +2` strips opening `{` but keeps trailing `}`, producing unbalanced eval on second mock cycle
- Affects any test that mocks a binary (curl, etc.) via create_mock
- Workaround: use `${functions[name]}` for save/restore (see test-doctor.zsh:_test_install_curl_mock)
- Fix: replace eval-based save with `_ORIGINAL_FUNCTIONS[fn]="${functions[fn]}"` pattern

### Doctor cache lock leaks across in-process invocations (filed 2026-05-13)
- `lib/doctor-cache.zsh:139-192` mkdir-fallback `_doctor_cache_acquire_lock` leaves stale state when called multiple times in the same shell
- Manifests as `_doctor_cache_set` returning rc=1 on second-and-later calls with the same key
- Hypothesis: lock dir cleanup `rm -rf "$lock_dir"` either fails silently or races with subsequent acquire
- Impact: silent cache write failures during rapid sequential doctor calls (e.g., test setup, scripted automation)
- Workaround in test-doctor.zsh: use unique cache keys per test

### CI runs smoke tests only — full suite not gated (filed 2026-05-13)
- `.github/workflows/test.yml:39-44` runs only `test-flow.zsh` + `test-install.sh`
- Comment is explicit: "Smoke tests only - run full suite locally with: `./tests/run-all.sh`"
- The required status check "ZSH Plugin Tests" on `main` therefore does NOT verify the 205-file suite
- Surfaced during PR #446: the test-doctor regression from commit `0880f924` would have passed CI green despite test-doctor exiting 124 under the harness
- Impact: relies on developer discipline to run `./tests/run-all.sh` locally before opening PRs
- Options to consider: (B) add `test-doctor.zsh` specifically (~30s job time), or (C) add full `./tests/run-all.sh` as a separate slower job (~4min). Either is a separate workflow PR, not bundled with feature work

### Future Enhancements
- Token automation Phases 2-4 (multi-token, gamification)
Expand All @@ -258,14 +283,15 @@

## Next Action

1. API documentation push (50% → 80%)
2. Wire `lib/doctor-cache.zsh` into `commands/doctor.zsh:405` (see Pending above)
3. Code workspace manager — spec ready on dev
1. Open PR for `feature/wire-doctor-cache` → dev (this branch)
2. After merge: optional follow-up — drop `CACHED_DOCTOR_VERBOSE` workaround from test-doctor.zsh setup() now that the production cache also benefits second invocations
3. API documentation push (50% → 80%)
4. Code workspace manager — spec ready on dev

---

**Last Updated:** 2026-05-13
**Status:** v7.6.0 | 52/52 tests passing (1 expected interactive/tmux timeout) | 15 dispatchers + at bridge | 205 test files | 12000+ test functions | 0 lint errors | 0 broken links
## wins: Fixed the regression bug (2026-05-04), --category fix squashed the bug (2026-05-04), fixed the bug (2026-05-04), Fixed the regression bug (2026-05-04), --category fix squashed the bug (2026-05-04)
**Status:** v7.6.0 | 53/53 tests passing (1 expected interactive/tmux timeout) | 15 dispatchers + at bridge | 205 test files | 12000+ test functions | 0 lint errors | 0 broken links
## wins: Fixed the regression bug (2026-05-13), --category fix squashed the bug (2026-05-13), fixed the bug (2026-05-13), Fixed the regression bug (2026-05-13), --category fix squashed the bug (2026-05-13)
## streak: 1
## last_active: 2026-05-04 12:36
## last_active: 2026-05-13 12:27
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`flow doctor` GitHub token validation cache** — Subsequent `flow doctor` invocations within 1 hour skip the GitHub `/user` API call (~5–8s saved per warm run). Fingerprint-based cache key (sha256 prefix of token) auto-invalidates on rotation.
- **`flow doctor --no-cache`** — Bypasses the cache and forces a fresh API validation. Useful when troubleshooting "why is my token broken?".

---

## [7.6.0] — 2026-02-27 — em --prompt + Scholar Config Sync
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ Update: `MASTER-DISPATCHER-GUIDE.md`, `QUICK-REFERENCE.md`, `mkdocs.yml`

## Testing

**205 test files, 12000+ test functions.** Run: `./tests/run-all.sh` (52/52 passing, 2 expected interactive/tmux timeouts) or individual suites in `tests/`.
**205 test files, 12000+ test functions.** Run: `./tests/run-all.sh` (53/53 passing, 1 expected interactive/tmux timeout) or individual suites in `tests/`.

See `docs/guides/TESTING.md` for patterns, mocks, assertions, TDD workflow.

Expand Down Expand Up @@ -289,7 +289,7 @@ export FLOW_DEBUG=1 # Debug mode

## Current Status

**Version:** v7.6.0 | **Tests:** 12000+ (52/52 suite, 2 interactive timeouts) | **Docs:** https://Data-Wise.github.io/flow-cli/
**Version:** v7.6.0 | **Tests:** 12000+ (53/53 suite, 1 interactive timeout) | **Docs:** https://Data-Wise.github.io/flow-cli/

---

Expand Down
186 changes: 114 additions & 72 deletions commands/doctor.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ doctor() {
# Task 4: Verbosity levels
local verbosity_level="normal" # quiet, normal, verbose

# Cache bypass: --no-cache forces fresh GitHub token validation
local no_cache=false

# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
Expand All @@ -38,6 +41,7 @@ doctor() {
--yes|-y) auto_yes=true; shift ;;
--verbose|-v) verbose=true; verbosity_level="verbose"; shift ;;
--quiet|-q) verbosity_level="quiet"; shift ;;
--no-cache) no_cache=true; shift ;;

# Task 1: Token flags
--dot)
Expand Down Expand Up @@ -390,79 +394,9 @@ doctor() {
# ──────────────────────────────────────────────────────────────
# GITHUB TOKEN HEALTH
# ──────────────────────────────────────────────────────────────
# Note: This is the legacy token check. Future phases will delegate to tok expiring
# Note: legacy token check. Future phases will delegate to tok expiring.
if [[ "$dot_check" == false ]]; then
_doctor_log_quiet "${FLOW_COLORS[bold]}🔑 GITHUB TOKEN${FLOW_COLORS[reset]}"

local token=$(sec github-token 2>/dev/null)
local -a token_issues=()

if [[ -z "$token" ]]; then
_doctor_log_quiet " ${FLOW_COLORS[error]}✗${FLOW_COLORS[reset]} Not configured"
token_issues+=("missing")
else
# Validate token via API
local api_response=$(curl -s -w "\n%{http_code}" \
-H "Authorization: token $token" \
"https://api.github.com/user" 2>/dev/null)

local http_code=$(echo "$api_response" | tail -1)
local username=$(echo "$api_response" | sed '$d' | jq -r '.login // "unknown"')

if [[ "$http_code" != "200" ]]; then
_doctor_log_quiet " ${FLOW_COLORS[error]}✗${FLOW_COLORS[reset]} Invalid/Expired"
token_issues+=("invalid")
else
_doctor_log_quiet " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} Valid (@$username)"

# Check expiration
local age_days=$(_tok_age_days "github-token")
local days_remaining=$((90 - age_days))

if [[ $days_remaining -le 7 ]]; then
_doctor_log_quiet " ${FLOW_COLORS[warning]}⚠${FLOW_COLORS[reset]} Expiring in $days_remaining days"
token_issues+=("expiring")
fi

# Test token-dependent services (verbose only)
_doctor_log_verbose ""
_doctor_log_verbose " ${FLOW_COLORS[muted]}Token-Dependent Services:${FLOW_COLORS[reset]}"

# Test gh CLI
if command -v gh &>/dev/null; then
if gh auth status &>/dev/null 2>&1; then
_doctor_log_verbose " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} gh CLI authenticated"
else
_doctor_log_verbose " ${FLOW_COLORS[error]}✗${FLOW_COLORS[reset]} gh CLI not authenticated"
token_issues+=("gh-cli")
fi
else
_doctor_log_verbose " ${FLOW_COLORS[muted]}○${FLOW_COLORS[reset]} gh CLI not installed"
fi

# Test Claude Code MCP
if [[ -f "$HOME/.claude/settings.json" ]]; then
if grep -q "GITHUB_PERSONAL_ACCESS_TOKEN.*\${GITHUB_TOKEN}" "$HOME/.claude/settings.json"; then
if [[ -n "$GITHUB_TOKEN" ]]; then
_doctor_log_verbose " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} Claude Code MCP configured"
else
_doctor_log_verbose " ${FLOW_COLORS[error]}✗${FLOW_COLORS[reset]} \$GITHUB_TOKEN not exported"
token_issues+=("env-var")
fi
else
_doctor_log_verbose " ${FLOW_COLORS[warning]}⚠${FLOW_COLORS[reset]} Claude MCP not using env var"
token_issues+=("mcp-config")
fi
fi
fi
fi

# Store token issues for category selection
if [[ ${#token_issues[@]} -gt 0 ]]; then
_doctor_token_issues[github]="${token_issues[*]}"
fi

_doctor_log_quiet ""
_doctor_check_github_token "$no_cache"
fi

# ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -1763,6 +1697,112 @@ _doctor_confirm() {
esac
}

# Validate the github-token via GitHub /user API, with file-based cache.
# Side effects: prints status lines via _doctor_log_*; mutates global
# _doctor_token_issues[github] when problems are found; reads/writes to
# $DOCTOR_CACHE_DIR via lib/doctor-cache.zsh.
#
# Args:
# $1 - "true" to bypass cache, anything else uses cache (default: false)
_doctor_check_github_token() {
local no_cache="${1:-false}"

_doctor_log_quiet "${FLOW_COLORS[bold]}🔑 GITHUB TOKEN${FLOW_COLORS[reset]}"

local token=$(sec github-token 2>/dev/null)
local -a token_issues=()

if [[ -z "$token" ]]; then
_doctor_log_quiet " ${FLOW_COLORS[error]}✗${FLOW_COLORS[reset]} Not configured"
token_issues+=("missing")
else
# Cache key derives from token sha256 prefix so rotation auto-invalidates.
local http_code username
local cache_key="" cached=""

if [[ "$no_cache" == false ]] && (( $+functions[_doctor_cache_get] )) \
&& command -v shasum >/dev/null 2>&1; then
local token_fp=$(printf '%s' "$token" | shasum -a 256 | cut -c1-12)
cache_key="token-github-${token_fp}"
cached=$(_doctor_cache_get "$cache_key" 2>/dev/null)
fi

if [[ -n "$cached" ]] && command -v jq >/dev/null 2>&1; then
http_code=$(echo "$cached" | jq -r '.http_code // ""')
username=$(echo "$cached" | jq -r '.username // "unknown"')
_doctor_log_verbose " ${FLOW_COLORS[muted]}[Cache hit]${FLOW_COLORS[reset]}"
else
[[ -n "$cache_key" ]] && _doctor_log_verbose " ${FLOW_COLORS[muted]}[Cache miss - validating...]${FLOW_COLORS[reset]}"
local api_response=$(curl -s -w "\n%{http_code}" \
-H "Authorization: token $token" \
"https://api.github.com/user" 2>/dev/null)

http_code=$(echo "$api_response" | tail -1)
username=$(echo "$api_response" | sed '$d' | jq -r '.login // "unknown"')

# Cache only successful validations (don't cache transient curl failures)
if [[ -n "$cache_key" ]] && [[ "$http_code" == "200" ]] \
&& (( $+functions[_doctor_cache_set] )); then
local cache_value=$(jq -nc \
--arg http_code "$http_code" \
--arg username "$username" \
'{http_code: $http_code, username: $username}')
_doctor_cache_set "$cache_key" "$cache_value" 3600 2>/dev/null || true
fi
fi

if [[ "$http_code" != "200" ]]; then
_doctor_log_quiet " ${FLOW_COLORS[error]}✗${FLOW_COLORS[reset]} Invalid/Expired"
token_issues+=("invalid")
else
_doctor_log_quiet " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} Valid (@$username)"

local age_days=$(_tok_age_days "github-token")
local days_remaining=$((90 - age_days))

if [[ $days_remaining -le 7 ]]; then
_doctor_log_quiet " ${FLOW_COLORS[warning]}⚠${FLOW_COLORS[reset]} Expiring in $days_remaining days"
token_issues+=("expiring")
fi

# Test token-dependent services (verbose only)
_doctor_log_verbose ""
_doctor_log_verbose " ${FLOW_COLORS[muted]}Token-Dependent Services:${FLOW_COLORS[reset]}"

if command -v gh &>/dev/null; then
if gh auth status &>/dev/null 2>&1; then
_doctor_log_verbose " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} gh CLI authenticated"
else
_doctor_log_verbose " ${FLOW_COLORS[error]}✗${FLOW_COLORS[reset]} gh CLI not authenticated"
token_issues+=("gh-cli")
fi
else
_doctor_log_verbose " ${FLOW_COLORS[muted]}○${FLOW_COLORS[reset]} gh CLI not installed"
fi

if [[ -f "$HOME/.claude/settings.json" ]]; then
if grep -q "GITHUB_PERSONAL_ACCESS_TOKEN.*\${GITHUB_TOKEN}" "$HOME/.claude/settings.json"; then
if [[ -n "$GITHUB_TOKEN" ]]; then
_doctor_log_verbose " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} Claude Code MCP configured"
else
_doctor_log_verbose " ${FLOW_COLORS[error]}✗${FLOW_COLORS[reset]} \$GITHUB_TOKEN not exported"
token_issues+=("env-var")
fi
else
_doctor_log_verbose " ${FLOW_COLORS[warning]}⚠${FLOW_COLORS[reset]} Claude MCP not using env var"
token_issues+=("mcp-config")
fi
fi
fi
fi

if [[ ${#token_issues[@]} -gt 0 ]]; then
_doctor_token_issues[github]="${token_issues[*]}"
fi

_doctor_log_quiet ""
}

_doctor_help() {
echo ""
echo "${FLOW_COLORS[header]}╭─────────────────────────────────────────────╮${FLOW_COLORS[reset]}"
Expand Down Expand Up @@ -1794,6 +1834,7 @@ _doctor_help() {
echo ""
echo "${FLOW_COLORS[bold]}OTHER OPTIONS${FLOW_COLORS[reset]}"
echo " -y, --yes Skip confirmations (use with --fix)"
echo " --no-cache Bypass GitHub token validation cache (force fresh API call)"
echo " -h, --help Show this help"
echo ""
echo "${FLOW_COLORS[bold]}EXAMPLES${FLOW_COLORS[reset]}"
Expand All @@ -1805,6 +1846,7 @@ _doctor_help() {
echo " ${FLOW_COLORS[muted]}\$${FLOW_COLORS[reset]} doctor --fix-token # Fix token issues only"
echo " ${FLOW_COLORS[muted]}\$${FLOW_COLORS[reset]} doctor --quiet # Show only errors"
echo " ${FLOW_COLORS[muted]}\$${FLOW_COLORS[reset]} doctor --verbose # Show detailed info + cache status"
echo " ${FLOW_COLORS[muted]}\$${FLOW_COLORS[reset]} doctor --no-cache # Force fresh GitHub token validation"
echo " ${FLOW_COLORS[muted]}\$${FLOW_COLORS[reset]} doctor --ai # Get AI help deciding what to install"
echo " ${FLOW_COLORS[muted]}\$${FLOW_COLORS[reset]} doctor --update-docs # Regenerate documentation"
echo " ${FLOW_COLORS[muted]}\$${FLOW_COLORS[reset]} flow doctor # Also works via flow command"
Expand Down
5 changes: 5 additions & 0 deletions completions/_flow
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ _flow() {
'-y:Skip confirmations (short)'
'--verbose:Verbose output'
'-v:Verbose (short)'
'--quiet:Minimal output (errors only)'
'-q:Quiet (short)'
'--dot:Check only DOT tokens (isolated, < 3s)'
'--fix-token:Fix only token issues (< 60s)'
'--no-cache:Bypass GitHub token validation cache'
'--help:Show help'
)
_describe -t options 'option' doctor_opts
Expand Down
Loading
Loading