feat: add whoami, reply, quote, follow/unfollow commands and --compact mode

- whoami: fetch current authenticated user profile
- reply <id> <text>: standalone reply command
- quote <id> <text>: quote-tweet command
- follow/unfollow <handle>: follow/unfollow users
- --compact/-c: global flag for LLM-friendly minimal JSON output
- client.py: add fetch_me, quote_tweet, follow_user, unfollow_user
- serialization.py: add tweet_to_compact_dict, tweets_to_compact_json
- 7 new tests (82 total, all passing)
This commit is contained in:
jackwener
2026-03-10 20:09:08 +08:00
parent 250fca46f0
commit 49d3e237c4
5 changed files with 397 additions and 25 deletions

View File

@@ -88,3 +88,109 @@ def test_cli_bookmark_alias_works(monkeypatch) -> None:
assert result.exit_code == 0
assert calls == ["123"]
def test_cli_whoami_command(monkeypatch) -> None:
from twitter_cli.models import UserProfile
class FakeClient:
def fetch_me(self) -> UserProfile:
return UserProfile(id="42", name="Test User", screen_name="testuser")
monkeypatch.setattr("twitter_cli.cli._get_client", lambda config=None: FakeClient())
runner = CliRunner()
result = runner.invoke(cli, ["whoami"])
assert result.exit_code == 0
result_json = runner.invoke(cli, ["whoami", "--json"])
assert result_json.exit_code == 0
assert '"screenName": "testuser"' in result_json.output
def test_cli_reply_command(monkeypatch) -> None:
calls = []
class FakeClient:
def create_tweet(self, text: str, reply_to_id=None) -> str:
calls.append({"text": text, "reply_to_id": reply_to_id})
return "999"
monkeypatch.setattr("twitter_cli.cli._get_client", lambda config=None: FakeClient())
runner = CliRunner()
result = runner.invoke(cli, ["reply", "12345", "Nice tweet!"])
assert result.exit_code == 0
assert calls[0]["reply_to_id"] == "12345"
assert calls[0]["text"] == "Nice tweet!"
def test_cli_quote_command(monkeypatch) -> None:
calls = []
class FakeClient:
def quote_tweet(self, tweet_id: str, text: str) -> str:
calls.append({"tweet_id": tweet_id, "text": text})
return "888"
monkeypatch.setattr("twitter_cli.cli._get_client", lambda config=None: FakeClient())
runner = CliRunner()
result = runner.invoke(cli, ["quote", "12345", "Interesting!"])
assert result.exit_code == 0
assert calls[0]["tweet_id"] == "12345"
assert calls[0]["text"] == "Interesting!"
def test_cli_follow_command(monkeypatch) -> None:
from twitter_cli.models import UserProfile
actions = []
class FakeClient:
def fetch_user(self, screen_name: str) -> UserProfile:
return UserProfile(id="42", name="Alice", screen_name=screen_name)
def follow_user(self, user_id: str) -> bool:
actions.append(("follow", user_id))
return True
monkeypatch.setattr("twitter_cli.cli._get_client", lambda config=None: FakeClient())
runner = CliRunner()
result = runner.invoke(cli, ["follow", "alice"])
assert result.exit_code == 0
assert actions == [("follow", "42")]
def test_cli_unfollow_command(monkeypatch) -> None:
from twitter_cli.models import UserProfile
actions = []
class FakeClient:
def fetch_user(self, screen_name: str) -> UserProfile:
return UserProfile(id="42", name="Alice", screen_name=screen_name)
def unfollow_user(self, user_id: str) -> bool:
actions.append(("unfollow", user_id))
return True
monkeypatch.setattr("twitter_cli.cli._get_client", lambda config=None: FakeClient())
runner = CliRunner()
result = runner.invoke(cli, ["unfollow", "alice"])
assert result.exit_code == 0
assert actions == [("unfollow", "42")]
def test_cli_compact_mode(tmp_path, tweet_factory) -> None:
json_path = tmp_path / "tweets.json"
json_path.write_text(tweets_to_json([tweet_factory("1")]), encoding="utf-8")
runner = CliRunner()
result = runner.invoke(cli, ["-c", "feed", "--input", str(json_path)])
assert result.exit_code == 0
# Compact output should have "author" field with @ prefix
assert '"@alice"' in result.output
# Compact output should NOT have full metrics keys
assert '"metrics"' not in result.output

View File

@@ -20,3 +20,32 @@ def test_tweets_json_roundtrip(tweet_factory) -> None:
assert [tweet.id for tweet in restored] == ["1", "2"]
assert restored[1].lang == "zh"
def test_compact_serialization(tweet_factory) -> None:
from twitter_cli.serialization import tweet_to_compact_dict, tweets_to_compact_json
import json
tweet = tweet_factory(
"42",
created_at="Sat Mar 07 05:51:02 +0000 2026",
text="A" * 200,
)
compact = tweet_to_compact_dict(tweet)
assert compact["id"] == "42"
assert compact["author"] == "@alice"
assert compact["time"] == "Mar 07 05:51"
assert len(compact["text"]) <= 140
assert compact["text"].endswith("...")
assert compact["likes"] == 10
assert compact["rts"] == 2
# Should only have 6 keys
assert set(compact.keys()) == {"id", "author", "text", "likes", "rts", "time"}
# Test batch serialization
raw = tweets_to_compact_json([tweet])
parsed = json.loads(raw)
assert len(parsed) == 1
assert parsed[0]["author"] == "@alice"