test_social_cli.py
python
sha256:ff478cfdcdd4b7fd6de89cb68896601a981f945634463275ec333bd20ca36402
Merge branch 'dev' into main
Human
20 days ago
| 1 | """Phase 01 TDD — ``muse social`` CLI surface. |
| 2 | |
| 3 | RED → GREEN cycle. Run before implementation to confirm failures, then |
| 4 | implement until all pass. |
| 5 | |
| 6 | Test tiers |
| 7 | ---------- |
| 8 | TestSocialCliShape — command registered, all subcommands in --help, |
| 9 | all run_* importable, module docstring present. |
| 10 | TestSocialStateHelpers — pure state-layer logic: write_post, write_follow, |
| 11 | write_reaction, read_profile — no repo, no network. |
| 12 | TestSocialCliPost — muse social post: JSON output, file written, |
| 13 | body stored, reply_to wired, dedup by content. |
| 14 | TestSocialCliFollow — follow/unfollow: file created/deleted, idempotent. |
| 15 | TestSocialCliTimeline — timeline: sorted by created_at desc, --limit, |
| 16 | reply_to threading, empty feed. |
| 17 | TestSocialCliReact — react: file written, emoji stored. |
| 18 | TestSocialCliProfile — profile show (no args) and profile set. |
| 19 | TestSocialCliDocstrings — all public run_* and helpers carry docstrings. |
| 20 | """ |
| 21 | from __future__ import annotations |
| 22 | |
| 23 | import hashlib |
| 24 | import json |
| 25 | import pathlib |
| 26 | import sys |
| 27 | import time |
| 28 | import types |
| 29 | |
| 30 | import pytest |
| 31 | |
| 32 | |
| 33 | # --------------------------------------------------------------------------- |
| 34 | # Invoke helper — same pattern as test_mist_cli.py |
| 35 | # --------------------------------------------------------------------------- |
| 36 | |
| 37 | def invoke_social(args: list[str]) -> tuple[int, str, str]: |
| 38 | """Run ``muse social <args>`` in-process and capture stdout/stderr.""" |
| 39 | from io import StringIO |
| 40 | |
| 41 | old_stdout, old_stderr = sys.stdout, sys.stderr |
| 42 | sys.stdout = out = StringIO() |
| 43 | sys.stderr = err = StringIO() |
| 44 | exit_code = 0 |
| 45 | try: |
| 46 | from muse.cli.app import main |
| 47 | main(["social"] + args) |
| 48 | except SystemExit as exc: |
| 49 | exit_code = int(exc.code) if exc.code is not None else 0 |
| 50 | finally: |
| 51 | sys.stdout = old_stdout |
| 52 | sys.stderr = old_stderr |
| 53 | return exit_code, out.getvalue(), err.getvalue() |
| 54 | |
| 55 | |
| 56 | # --------------------------------------------------------------------------- |
| 57 | # Fixtures |
| 58 | # --------------------------------------------------------------------------- |
| 59 | |
| 60 | @pytest.fixture() |
| 61 | def state_dir(tmp_path: pathlib.Path) -> pathlib.Path: |
| 62 | """Minimal social repo state directory.""" |
| 63 | (tmp_path / "posts").mkdir() |
| 64 | (tmp_path / "reactions").mkdir() |
| 65 | (tmp_path / "graph" / "follows").mkdir(parents=True) |
| 66 | return tmp_path |
| 67 | |
| 68 | |
| 69 | # =========================================================================== |
| 70 | # Shape — command registered, subcommands visible, symbols importable |
| 71 | # =========================================================================== |
| 72 | |
| 73 | class TestSocialCliShape: |
| 74 | |
| 75 | def test_social_registered_in_app(self) -> None: |
| 76 | code, out, err = invoke_social(["--help"]) |
| 77 | # --help exits 0 and prints usage; if unregistered it exits non-zero |
| 78 | assert code == 0 |
| 79 | |
| 80 | def test_help_lists_post_subcommand(self) -> None: |
| 81 | _, out, _ = invoke_social(["--help"]) |
| 82 | assert "post" in out |
| 83 | |
| 84 | def test_help_lists_follow_subcommand(self) -> None: |
| 85 | _, out, _ = invoke_social(["--help"]) |
| 86 | assert "follow" in out |
| 87 | |
| 88 | def test_help_lists_unfollow_subcommand(self) -> None: |
| 89 | _, out, _ = invoke_social(["--help"]) |
| 90 | assert "unfollow" in out |
| 91 | |
| 92 | def test_help_lists_timeline_subcommand(self) -> None: |
| 93 | _, out, _ = invoke_social(["--help"]) |
| 94 | assert "timeline" in out |
| 95 | |
| 96 | def test_help_lists_react_subcommand(self) -> None: |
| 97 | _, out, _ = invoke_social(["--help"]) |
| 98 | assert "react" in out |
| 99 | |
| 100 | def test_help_lists_profile_subcommand(self) -> None: |
| 101 | _, out, _ = invoke_social(["--help"]) |
| 102 | assert "profile" in out |
| 103 | |
| 104 | def test_run_functions_importable(self) -> None: |
| 105 | from muse.cli.commands.social import ( |
| 106 | run_post, |
| 107 | run_follow, |
| 108 | run_unfollow, |
| 109 | run_timeline, |
| 110 | run_react, |
| 111 | run_profile, |
| 112 | ) |
| 113 | for fn in (run_post, run_follow, run_unfollow, run_timeline, run_react, run_profile): |
| 114 | assert callable(fn) |
| 115 | |
| 116 | def test_register_importable(self) -> None: |
| 117 | from muse.cli.commands.social import register |
| 118 | assert callable(register) |
| 119 | |
| 120 | def test_module_has_docstring(self) -> None: |
| 121 | import muse.cli.commands.social as mod |
| 122 | assert mod.__doc__ and len(mod.__doc__.strip()) > 20 |
| 123 | |
| 124 | |
| 125 | # =========================================================================== |
| 126 | # State helpers — pure logic, no repo, no network |
| 127 | # =========================================================================== |
| 128 | |
| 129 | class TestSocialStateHelpers: |
| 130 | |
| 131 | def test_build_post_returns_dict_with_required_keys(self) -> None: |
| 132 | from muse.cli.commands.social import _build_post |
| 133 | post = _build_post(body="hello muse") |
| 134 | assert "body" in post |
| 135 | assert "created_at" in post |
| 136 | assert "post_id" in post |
| 137 | |
| 138 | def test_build_post_body_stored(self) -> None: |
| 139 | from muse.cli.commands.social import _build_post |
| 140 | post = _build_post(body="hello muse") |
| 141 | assert post["body"] == "hello muse" |
| 142 | |
| 143 | def test_build_post_post_id_is_sha256_prefix(self) -> None: |
| 144 | from muse.cli.commands.social import _build_post |
| 145 | post = _build_post(body="hello muse") |
| 146 | assert post["post_id"].startswith("sha256:") |
| 147 | |
| 148 | def test_build_post_reply_to_stored(self) -> None: |
| 149 | from muse.cli.commands.social import _build_post |
| 150 | post = _build_post(body="reply text", reply_to="sha256:abc123") |
| 151 | assert post["reply_to"] == "sha256:abc123" |
| 152 | |
| 153 | def test_build_post_reply_to_defaults_none(self) -> None: |
| 154 | from muse.cli.commands.social import _build_post |
| 155 | post = _build_post(body="hello") |
| 156 | assert post.get("reply_to") is None |
| 157 | |
| 158 | def test_build_post_deterministic(self) -> None: |
| 159 | from muse.cli.commands.social import _build_post |
| 160 | p1 = _build_post(body="same text", created_at="2026-05-01T00:00:00Z") |
| 161 | p2 = _build_post(body="same text", created_at="2026-05-01T00:00:00Z") |
| 162 | assert p1["post_id"] == p2["post_id"] |
| 163 | |
| 164 | def test_write_post_creates_file( |
| 165 | self, state_dir: pathlib.Path |
| 166 | ) -> None: |
| 167 | from muse.cli.commands.social import _build_post, _write_post |
| 168 | post = _build_post(body="hello muse", created_at="2026-05-01T00:00:00Z") |
| 169 | path = _write_post(state_dir, post) |
| 170 | assert path.exists() |
| 171 | |
| 172 | def test_write_post_file_is_valid_json( |
| 173 | self, state_dir: pathlib.Path |
| 174 | ) -> None: |
| 175 | from muse.cli.commands.social import _build_post, _write_post |
| 176 | post = _build_post(body="hello muse", created_at="2026-05-01T00:00:00Z") |
| 177 | path = _write_post(state_dir, post) |
| 178 | data = json.loads(path.read_text()) |
| 179 | assert data["body"] == "hello muse" |
| 180 | |
| 181 | def test_write_post_file_under_posts_dir( |
| 182 | self, state_dir: pathlib.Path |
| 183 | ) -> None: |
| 184 | from muse.cli.commands.social import _build_post, _write_post |
| 185 | post = _build_post(body="hello", created_at="2026-05-01T00:00:00Z") |
| 186 | path = _write_post(state_dir, post) |
| 187 | assert path.parent.parent.name == "posts" |
| 188 | |
| 189 | def test_write_post_idempotent(self, state_dir: pathlib.Path) -> None: |
| 190 | from muse.cli.commands.social import _build_post, _write_post |
| 191 | post = _build_post(body="hello", created_at="2026-05-01T00:00:00Z") |
| 192 | p1 = _write_post(state_dir, post) |
| 193 | p2 = _write_post(state_dir, post) |
| 194 | assert p1 == p2 |
| 195 | assert len(list((state_dir / "posts").iterdir())) == 1 |
| 196 | |
| 197 | def test_write_follow_creates_file(self, state_dir: pathlib.Path) -> None: |
| 198 | from muse.cli.commands.social import _write_follow |
| 199 | path = _write_follow(state_dir, "alice") |
| 200 | assert path.exists() |
| 201 | |
| 202 | def test_write_follow_filename_contains_handle( |
| 203 | self, state_dir: pathlib.Path |
| 204 | ) -> None: |
| 205 | from muse.cli.commands.social import _write_follow |
| 206 | path = _write_follow(state_dir, "alice") |
| 207 | assert "alice" in path.name |
| 208 | |
| 209 | def test_write_follow_content_has_handle( |
| 210 | self, state_dir: pathlib.Path |
| 211 | ) -> None: |
| 212 | from muse.cli.commands.social import _write_follow |
| 213 | path = _write_follow(state_dir, "alice") |
| 214 | data = json.loads(path.read_text()) |
| 215 | assert data["handle"] == "alice" |
| 216 | |
| 217 | def test_delete_follow_removes_file(self, state_dir: pathlib.Path) -> None: |
| 218 | from muse.cli.commands.social import _write_follow, _delete_follow |
| 219 | _write_follow(state_dir, "alice") |
| 220 | _delete_follow(state_dir, "alice") |
| 221 | assert not (state_dir / "graph" / "follows" / "alice.json").exists() |
| 222 | |
| 223 | def test_delete_follow_nonexistent_is_noop( |
| 224 | self, state_dir: pathlib.Path |
| 225 | ) -> None: |
| 226 | from muse.cli.commands.social import _delete_follow |
| 227 | # Should not raise |
| 228 | _delete_follow(state_dir, "nobody") |
| 229 | |
| 230 | def test_write_reaction_creates_file(self, state_dir: pathlib.Path) -> None: |
| 231 | from muse.cli.commands.social import _write_reaction |
| 232 | path = _write_reaction(state_dir, post_id="sha256:aaa", emoji="❤️") |
| 233 | assert path.exists() |
| 234 | |
| 235 | def test_write_reaction_content_has_emoji( |
| 236 | self, state_dir: pathlib.Path |
| 237 | ) -> None: |
| 238 | from muse.cli.commands.social import _write_reaction |
| 239 | path = _write_reaction(state_dir, post_id="sha256:aaa", emoji="❤️") |
| 240 | data = json.loads(path.read_text()) |
| 241 | assert data["emoji"] == "❤️" |
| 242 | |
| 243 | def test_write_reaction_content_has_post_id( |
| 244 | self, state_dir: pathlib.Path |
| 245 | ) -> None: |
| 246 | from muse.cli.commands.social import _write_reaction |
| 247 | path = _write_reaction(state_dir, post_id="sha256:aaa", emoji="❤️") |
| 248 | data = json.loads(path.read_text()) |
| 249 | assert data["post_id"] == "sha256:aaa" |
| 250 | |
| 251 | def test_read_profile_returns_none_when_missing( |
| 252 | self, state_dir: pathlib.Path |
| 253 | ) -> None: |
| 254 | from muse.cli.commands.social import _read_profile |
| 255 | assert _read_profile(state_dir) is None |
| 256 | |
| 257 | def test_write_read_profile_roundtrip(self, state_dir: pathlib.Path) -> None: |
| 258 | from muse.cli.commands.social import _write_profile, _read_profile |
| 259 | _write_profile(state_dir, {"handle": "gabriel", "bio": "building the future"}) |
| 260 | profile = _read_profile(state_dir) |
| 261 | assert profile is not None |
| 262 | assert profile["handle"] == "gabriel" |
| 263 | assert profile["bio"] == "building the future" |
| 264 | |
| 265 | def test_load_posts_empty_dir(self, state_dir: pathlib.Path) -> None: |
| 266 | from muse.cli.commands.social import _load_posts |
| 267 | posts = _load_posts(state_dir) |
| 268 | assert posts == [] |
| 269 | |
| 270 | def test_load_posts_returns_list(self, state_dir: pathlib.Path) -> None: |
| 271 | from muse.cli.commands.social import _build_post, _write_post, _load_posts |
| 272 | post = _build_post(body="hello", created_at="2026-05-01T00:00:00Z") |
| 273 | _write_post(state_dir, post) |
| 274 | posts = _load_posts(state_dir) |
| 275 | assert len(posts) == 1 |
| 276 | |
| 277 | def test_load_posts_sorted_newest_first(self, state_dir: pathlib.Path) -> None: |
| 278 | from muse.cli.commands.social import _build_post, _write_post, _load_posts |
| 279 | older = _build_post(body="older", created_at="2026-01-01T00:00:00Z") |
| 280 | newer = _build_post(body="newer", created_at="2026-06-01T00:00:00Z") |
| 281 | _write_post(state_dir, older) |
| 282 | _write_post(state_dir, newer) |
| 283 | posts = _load_posts(state_dir) |
| 284 | assert posts[0]["body"] == "newer" |
| 285 | assert posts[1]["body"] == "older" |
| 286 | |
| 287 | |
| 288 | # =========================================================================== |
| 289 | # CLI: post |
| 290 | # =========================================================================== |
| 291 | |
| 292 | class TestSocialCliPost: |
| 293 | |
| 294 | def test_post_help_exits_zero(self) -> None: |
| 295 | code, _, _ = invoke_social(["post", "--help"]) |
| 296 | assert code == 0 |
| 297 | |
| 298 | def test_post_json_output_has_post_id(self, tmp_path: pathlib.Path) -> None: |
| 299 | from muse.cli.commands.social import _build_post, _write_post |
| 300 | post = _build_post(body="test post", created_at="2026-05-01T00:00:00Z") |
| 301 | (tmp_path / "posts").mkdir() |
| 302 | (tmp_path / "reactions").mkdir() |
| 303 | (tmp_path / "graph" / "follows").mkdir(parents=True) |
| 304 | path = _write_post(tmp_path, post) |
| 305 | data = json.loads(path.read_text()) |
| 306 | assert "post_id" in data |
| 307 | |
| 308 | def test_post_json_has_body(self, tmp_path: pathlib.Path) -> None: |
| 309 | from muse.cli.commands.social import _build_post, _write_post |
| 310 | post = _build_post(body="hello world", created_at="2026-05-01T00:00:00Z") |
| 311 | (tmp_path / "posts").mkdir() |
| 312 | path = _write_post(tmp_path, post) |
| 313 | data = json.loads(path.read_text()) |
| 314 | assert data["body"] == "hello world" |
| 315 | |
| 316 | def test_post_with_reply_to_stored(self, tmp_path: pathlib.Path) -> None: |
| 317 | from muse.cli.commands.social import _build_post, _write_post |
| 318 | post = _build_post( |
| 319 | body="great point", reply_to="sha256:abc", created_at="2026-05-01T00:00:00Z" |
| 320 | ) |
| 321 | (tmp_path / "posts").mkdir() |
| 322 | path = _write_post(tmp_path, post) |
| 323 | data = json.loads(path.read_text()) |
| 324 | assert data["reply_to"] == "sha256:abc" |
| 325 | |
| 326 | def test_post_missing_body_arg_exits_nonzero(self) -> None: |
| 327 | code, _, err = invoke_social(["post"]) |
| 328 | assert code != 0 |
| 329 | |
| 330 | |
| 331 | # =========================================================================== |
| 332 | # CLI: follow / unfollow |
| 333 | # =========================================================================== |
| 334 | |
| 335 | class TestSocialCliFollow: |
| 336 | |
| 337 | def test_follow_help_exits_zero(self) -> None: |
| 338 | code, _, _ = invoke_social(["follow", "--help"]) |
| 339 | assert code == 0 |
| 340 | |
| 341 | def test_unfollow_help_exits_zero(self) -> None: |
| 342 | code, _, _ = invoke_social(["unfollow", "--help"]) |
| 343 | assert code == 0 |
| 344 | |
| 345 | def test_write_follow_idempotent(self, state_dir: pathlib.Path) -> None: |
| 346 | from muse.cli.commands.social import _write_follow |
| 347 | p1 = _write_follow(state_dir, "alice") |
| 348 | p2 = _write_follow(state_dir, "alice") |
| 349 | assert p1 == p2 |
| 350 | assert len(list((state_dir / "graph" / "follows").iterdir())) == 1 |
| 351 | |
| 352 | def test_follow_multiple_handles(self, state_dir: pathlib.Path) -> None: |
| 353 | from muse.cli.commands.social import _write_follow |
| 354 | _write_follow(state_dir, "alice") |
| 355 | _write_follow(state_dir, "bob") |
| 356 | _write_follow(state_dir, "carol") |
| 357 | follows = list((state_dir / "graph" / "follows").iterdir()) |
| 358 | assert len(follows) == 3 |
| 359 | |
| 360 | def test_unfollow_after_follow_removes_file( |
| 361 | self, state_dir: pathlib.Path |
| 362 | ) -> None: |
| 363 | from muse.cli.commands.social import _write_follow, _delete_follow |
| 364 | _write_follow(state_dir, "alice") |
| 365 | _delete_follow(state_dir, "alice") |
| 366 | assert not (state_dir / "graph" / "follows" / "alice.json").exists() |
| 367 | |
| 368 | def test_follow_missing_handle_exits_nonzero(self) -> None: |
| 369 | code, _, _ = invoke_social(["follow"]) |
| 370 | assert code != 0 |
| 371 | |
| 372 | def test_unfollow_missing_handle_exits_nonzero(self) -> None: |
| 373 | code, _, _ = invoke_social(["unfollow"]) |
| 374 | assert code != 0 |
| 375 | |
| 376 | |
| 377 | # =========================================================================== |
| 378 | # CLI: timeline |
| 379 | # =========================================================================== |
| 380 | |
| 381 | class TestSocialCliTimeline: |
| 382 | |
| 383 | def test_timeline_help_exits_zero(self) -> None: |
| 384 | code, _, _ = invoke_social(["timeline", "--help"]) |
| 385 | assert code == 0 |
| 386 | |
| 387 | def test_load_posts_empty_returns_empty_list( |
| 388 | self, state_dir: pathlib.Path |
| 389 | ) -> None: |
| 390 | from muse.cli.commands.social import _load_posts |
| 391 | assert _load_posts(state_dir) == [] |
| 392 | |
| 393 | def test_timeline_sorted_newest_first( |
| 394 | self, state_dir: pathlib.Path |
| 395 | ) -> None: |
| 396 | from muse.cli.commands.social import _build_post, _write_post, _load_posts |
| 397 | for i, ts in enumerate( |
| 398 | ["2026-01-01T00:00:00Z", "2026-03-01T00:00:00Z", "2026-06-01T00:00:00Z"] |
| 399 | ): |
| 400 | _write_post(state_dir, _build_post(body=f"post {i}", created_at=ts)) |
| 401 | posts = _load_posts(state_dir) |
| 402 | timestamps = [p["created_at"] for p in posts] |
| 403 | assert timestamps == sorted(timestamps, reverse=True) |
| 404 | |
| 405 | def test_timeline_limit(self, state_dir: pathlib.Path) -> None: |
| 406 | from muse.cli.commands.social import _build_post, _write_post, _load_posts |
| 407 | for i in range(5): |
| 408 | _write_post( |
| 409 | state_dir, |
| 410 | _build_post(body=f"post {i}", created_at=f"2026-0{i+1}-01T00:00:00Z"), |
| 411 | ) |
| 412 | posts = _load_posts(state_dir, limit=3) |
| 413 | assert len(posts) == 3 |
| 414 | |
| 415 | def test_replies_have_reply_to_field(self, state_dir: pathlib.Path) -> None: |
| 416 | from muse.cli.commands.social import _build_post, _write_post, _load_posts |
| 417 | root = _build_post(body="root post", created_at="2026-01-01T00:00:00Z") |
| 418 | reply = _build_post( |
| 419 | body="reply", |
| 420 | created_at="2026-01-01T01:00:00Z", |
| 421 | reply_to=root["post_id"], |
| 422 | ) |
| 423 | _write_post(state_dir, root) |
| 424 | _write_post(state_dir, reply) |
| 425 | posts = _load_posts(state_dir) |
| 426 | reply_post = next(p for p in posts if p["body"] == "reply") |
| 427 | assert reply_post["reply_to"] == root["post_id"] |
| 428 | |
| 429 | |
| 430 | # =========================================================================== |
| 431 | # CLI: react |
| 432 | # =========================================================================== |
| 433 | |
| 434 | class TestSocialCliReact: |
| 435 | |
| 436 | def test_react_help_exits_zero(self) -> None: |
| 437 | code, _, _ = invoke_social(["react", "--help"]) |
| 438 | assert code == 0 |
| 439 | |
| 440 | def test_react_creates_file_in_reactions_dir( |
| 441 | self, state_dir: pathlib.Path |
| 442 | ) -> None: |
| 443 | from muse.cli.commands.social import _write_reaction |
| 444 | _write_reaction(state_dir, post_id="sha256:aaa", emoji="❤️") |
| 445 | reactions = list((state_dir / "reactions").iterdir()) |
| 446 | assert len(reactions) == 1 |
| 447 | |
| 448 | def test_react_different_emojis_different_files( |
| 449 | self, state_dir: pathlib.Path |
| 450 | ) -> None: |
| 451 | from muse.cli.commands.social import _write_reaction |
| 452 | _write_reaction(state_dir, post_id="sha256:aaa", emoji="❤️") |
| 453 | _write_reaction(state_dir, post_id="sha256:aaa", emoji="🔥") |
| 454 | reactions = list((state_dir / "reactions").rglob("*.json")) |
| 455 | assert len(reactions) == 2 |
| 456 | |
| 457 | def test_react_same_emoji_idempotent(self, state_dir: pathlib.Path) -> None: |
| 458 | from muse.cli.commands.social import _write_reaction |
| 459 | _write_reaction(state_dir, post_id="sha256:aaa", emoji="❤️") |
| 460 | _write_reaction(state_dir, post_id="sha256:aaa", emoji="❤️") |
| 461 | reactions = list((state_dir / "reactions").rglob("*.json")) |
| 462 | assert len(reactions) == 1 |
| 463 | |
| 464 | def test_react_missing_args_exits_nonzero(self) -> None: |
| 465 | code, _, _ = invoke_social(["react"]) |
| 466 | assert code != 0 |
| 467 | |
| 468 | |
| 469 | # =========================================================================== |
| 470 | # CLI: profile |
| 471 | # =========================================================================== |
| 472 | |
| 473 | class TestSocialCliProfile: |
| 474 | |
| 475 | def test_profile_help_exits_zero(self) -> None: |
| 476 | code, _, _ = invoke_social(["profile", "--help"]) |
| 477 | assert code == 0 |
| 478 | |
| 479 | def test_write_profile_creates_file(self, state_dir: pathlib.Path) -> None: |
| 480 | from muse.cli.commands.social import _write_profile |
| 481 | _write_profile(state_dir, {"handle": "gabriel", "bio": "hi"}) |
| 482 | assert (state_dir / "profile.json").exists() |
| 483 | |
| 484 | def test_write_profile_content_stored(self, state_dir: pathlib.Path) -> None: |
| 485 | from muse.cli.commands.social import _write_profile, _read_profile |
| 486 | _write_profile(state_dir, {"handle": "gabriel", "bio": "building the future"}) |
| 487 | profile = _read_profile(state_dir) |
| 488 | assert profile["bio"] == "building the future" |
| 489 | |
| 490 | def test_profile_set_updates_bio(self, state_dir: pathlib.Path) -> None: |
| 491 | from muse.cli.commands.social import _write_profile, _read_profile |
| 492 | _write_profile(state_dir, {"handle": "gabriel", "bio": "old bio"}) |
| 493 | existing = _read_profile(state_dir) |
| 494 | existing["bio"] = "new bio" |
| 495 | _write_profile(state_dir, existing) |
| 496 | assert _read_profile(state_dir)["bio"] == "new bio" |
| 497 | |
| 498 | |
| 499 | # =========================================================================== |
| 500 | # Docstrings |
| 501 | # =========================================================================== |
| 502 | |
| 503 | class TestSocialCliDocstrings: |
| 504 | |
| 505 | def test_module_has_docstring(self) -> None: |
| 506 | import muse.cli.commands.social as mod |
| 507 | assert mod.__doc__ and len(mod.__doc__.strip()) > 20 |
| 508 | |
| 509 | def test_run_functions_have_docstrings(self) -> None: |
| 510 | from muse.cli.commands import social |
| 511 | for name in ("run_post", "run_follow", "run_unfollow", "run_timeline", |
| 512 | "run_react", "run_profile", "register"): |
| 513 | fn = getattr(social, name) |
| 514 | assert fn.__doc__ and len(fn.__doc__.strip()) > 10, \ |
| 515 | f"{name}() missing docstring" |
| 516 | |
| 517 | def test_helper_functions_have_docstrings(self) -> None: |
| 518 | from muse.cli.commands import social |
| 519 | for name in ("_build_post", "_write_post", "_write_follow", |
| 520 | "_delete_follow", "_write_reaction", "_read_profile", |
| 521 | "_write_profile", "_load_posts"): |
| 522 | fn = getattr(social, name) |
| 523 | assert fn.__doc__ and len(fn.__doc__.strip()) > 10, \ |
| 524 | f"{name}() missing docstring" |
File History
1 commit
sha256:ff478cfdcdd4b7fd6de89cb68896601a981f945634463275ec333bd20ca36402
Merge branch 'dev' into main
Human
20 days ago