myclaude -r: 交互式 resume picker(方向键/翻页/即时过滤)
This commit is contained in:
+126
-22
@@ -2,6 +2,7 @@
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -150,32 +151,135 @@ class TestResumePickerClaude:
|
||||
assert "Not a terminal" in err
|
||||
assert "resume directly with myclaude -r <id>" in err
|
||||
|
||||
def test_selecting_number_launches_resume(
|
||||
self, claude_env, monkeypatch
|
||||
) -> None:
|
||||
def test_routing_select_launches(self, claude_env, monkeypatch) -> None:
|
||||
"""Picking a session resumes via ``claude --resume <id>``."""
|
||||
_write_jsonl(claude_env.proj_dir, "aaa", "2026-08-15T04:00:00Z")
|
||||
_write_jsonl(claude_env.proj_dir, "bbb", "2026-08-15T03:00:00Z")
|
||||
|
||||
launched: list[list[str]] = []
|
||||
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
|
||||
monkeypatch.setattr(L, "_pick_session", lambda rows, cwd: rows[0])
|
||||
monkeypatch.setattr(
|
||||
L, "_launch", lambda *_a, **_k: launched.append(_a[2])
|
||||
)
|
||||
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
|
||||
monkeypatch.setattr(L.Prompt, "ask", lambda *a, **k: "2")
|
||||
|
||||
L._resume_picker_claude(claude_env.cwd)
|
||||
assert launched == [["--resume", "bbb"]]
|
||||
|
||||
def test_enter_picks_latest(self, claude_env, monkeypatch) -> None:
|
||||
_write_jsonl(claude_env.proj_dir, "aaa", "2026-08-15T04:00:00Z")
|
||||
_write_jsonl(claude_env.proj_dir, "bbb", "2026-08-15T03:00:00Z")
|
||||
|
||||
launched: list[list[str]] = []
|
||||
monkeypatch.setattr(
|
||||
L, "_launch", lambda *_a, **_k: launched.append(_a[2])
|
||||
)
|
||||
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
|
||||
monkeypatch.setattr(L.Prompt, "ask", lambda *a, **k: "")
|
||||
|
||||
L._resume_picker_claude(claude_env.cwd)
|
||||
assert launched == [["--resume", "aaa"]]
|
||||
|
||||
def test_routing_quit_exits(self, claude_env, monkeypatch) -> None:
|
||||
"""Quitting the picker exits 0 without launching."""
|
||||
_write_jsonl(claude_env.proj_dir, "aaa", "2026-08-15T04:00:00Z")
|
||||
launched: list[list[str]] = []
|
||||
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
|
||||
monkeypatch.setattr(L, "_pick_session", lambda rows, cwd: None)
|
||||
monkeypatch.setattr(
|
||||
L, "_launch", lambda *_a, **_k: launched.append(_a[2])
|
||||
)
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
L._resume_picker_claude(claude_env.cwd)
|
||||
assert exc.value.code == 0
|
||||
assert launched == []
|
||||
|
||||
|
||||
def _make_rows(claude_env, n: int):
|
||||
"""n sessions, s000 newest → rows[0] == s000."""
|
||||
for i in range(n):
|
||||
_write_jsonl(
|
||||
claude_env.proj_dir,
|
||||
f"s{i:03d}",
|
||||
f"2026-08-15T{23 - i:02d}:00:00Z",
|
||||
)
|
||||
return L._claude_session_rows(claude_env.cwd)
|
||||
|
||||
|
||||
class TestPickerAdvance:
|
||||
"""Windowed picker state machine (no tty needed)."""
|
||||
|
||||
def test_enter_selects_latest(self, claude_env) -> None:
|
||||
rows = _make_rows(claude_env, 3)
|
||||
state = L._PickerState(rows=rows)
|
||||
state, action, payload = L._picker_advance(state, "enter")
|
||||
assert action == "select"
|
||||
assert payload == rows[0]
|
||||
|
||||
def test_number_selects_row(self, claude_env) -> None:
|
||||
rows = _make_rows(claude_env, 3)
|
||||
state = L._PickerState(rows=rows)
|
||||
state, action, payload = L._picker_advance(state, "2")
|
||||
assert action == "none"
|
||||
state, action, payload = L._picker_advance(state, "enter")
|
||||
assert action == "select"
|
||||
assert payload == rows[1]
|
||||
|
||||
def test_number_out_of_range(self, claude_env) -> None:
|
||||
rows = _make_rows(claude_env, 3)
|
||||
state = L._PickerState(rows=rows)
|
||||
state, _, _ = L._picker_advance(state, "9")
|
||||
state, action, payload = L._picker_advance(state, "enter")
|
||||
assert action == "none"
|
||||
assert isinstance(payload, str) and "range" in payload
|
||||
|
||||
def test_scroll_window_advances(self, claude_env) -> None:
|
||||
rows = _make_rows(claude_env, 15)
|
||||
state = L._PickerState(rows=rows)
|
||||
for _ in range(10):
|
||||
state, action, payload = L._picker_advance(state, "down")
|
||||
assert action == "none"
|
||||
# cursor hits window bottom, then the window scrolls
|
||||
assert state.offset == 1
|
||||
assert state.cursor == 9
|
||||
|
||||
def test_quit(self, claude_env) -> None:
|
||||
rows = _make_rows(claude_env, 3)
|
||||
state = L._PickerState(rows=rows)
|
||||
state, action, payload = L._picker_advance(state, "q")
|
||||
assert action == "quit"
|
||||
assert payload is None
|
||||
|
||||
def test_db_only_row_not_selectable(self, claude_env) -> None:
|
||||
_write_jsonl(claude_env.proj_dir, "aaa", "2026-08-15T04:00:00Z")
|
||||
# hosted session with no jsonl on disk, dated newest (year 2033)
|
||||
claude_env.index["nojsonl"] = XiaoheSession(
|
||||
cli_session_id="nojsonl",
|
||||
xiaohe_session_id="x",
|
||||
title="Lost",
|
||||
status="ready",
|
||||
updated_at=2_000_000_000.0,
|
||||
)
|
||||
rows = L._claude_session_rows(claude_env.cwd)
|
||||
assert rows[0].jsonl_path is None
|
||||
state = L._PickerState(rows=rows)
|
||||
state, action, payload = L._picker_advance(state, "enter")
|
||||
assert action == "none"
|
||||
assert isinstance(payload, str) and "client-only" in payload
|
||||
|
||||
|
||||
def _read_key_from_bytes(data: bytes) -> str:
|
||||
"""Feed bytes through a pipe into ``_read_key``."""
|
||||
r, w = os.pipe()
|
||||
try:
|
||||
os.write(w, data)
|
||||
return L._read_key(r)
|
||||
finally:
|
||||
os.close(r)
|
||||
os.close(w)
|
||||
|
||||
|
||||
class TestReadKey:
|
||||
def test_enter(self) -> None:
|
||||
assert _read_key_from_bytes(b"\r") == "enter"
|
||||
|
||||
def test_arrow_down(self) -> None:
|
||||
assert _read_key_from_bytes(b"\x1b[B") == "down"
|
||||
|
||||
def test_arrow_up(self) -> None:
|
||||
assert _read_key_from_bytes(b"\x1b[A") == "up"
|
||||
|
||||
def test_page_down(self) -> None:
|
||||
assert _read_key_from_bytes(b"\x1b[6~") == "pgdown"
|
||||
|
||||
def test_q(self) -> None:
|
||||
assert _read_key_from_bytes(b"q") == "q"
|
||||
|
||||
def test_digit(self) -> None:
|
||||
assert _read_key_from_bytes(b"5") == "5"
|
||||
|
||||
def test_escape(self) -> None:
|
||||
assert _read_key_from_bytes(b"\x1b") == "esc"
|
||||
|
||||
Reference in New Issue
Block a user