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:
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user