Fix tree rendering.
This commit is contained in:
1 parent
b64ab023ea
commit
26f04e6cb0
14 files changed
+697
-52
No files matched your search
@@ -516,5 +516,137 @@ class OrphansNotReusedTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class SpinePrefersSameAuthorTests(unittest.TestCase):
|
||||
"""The spine walker must prefer the OP's own same-handle chain
|
||||
over cross-author replies. When the OP has a self-reply plus
|
||||
other-author replies to the same post, the self-reply continues
|
||||
the spine and the cross-author replies become branches."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.tmp = Path(tempfile.mkdtemp(prefix="spine_same_author_"))
|
||||
self.input_dir = self.tmp / "input"
|
||||
self.input_dir.mkdir()
|
||||
self.vault = self.tmp / "vault"
|
||||
(self.vault / "archive" / "threads").mkdir(parents=True)
|
||||
(self.vault / "assets" / "threads").mkdir(parents=True)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def test_op_with_self_reply_continues_spine(self) -> None:
|
||||
"""OP has children including a self-reply and a cross-author
|
||||
reply that arrived FIRST chronologically. Spine should walk
|
||||
the OP's self-reply chain, not stop at the cross-author reply.
|
||||
"""
|
||||
# OP at 00:00. won3d replies at 00:01 (cross-author, earliest).
|
||||
# Lottes self-replies at 00:05 (later, same handle, continues
|
||||
# the spine).
|
||||
thread_data = {
|
||||
"root_post_id": "1000",
|
||||
"source_url": "https://x.com/i/status/1000",
|
||||
"posts": [
|
||||
{
|
||||
"post_id": "1000",
|
||||
"author": "OP",
|
||||
"handle": "OP",
|
||||
"text": "OP post",
|
||||
"timestamp": "2023-01-01 00:00:00",
|
||||
"media_urls": [],
|
||||
"reply_to_id": None,
|
||||
"quote_of_id": None,
|
||||
"metrics": {
|
||||
"reply_count": 0,
|
||||
"repost_count": 0,
|
||||
"like_count": 0,
|
||||
"view_count": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"post_id": "1001",
|
||||
"author": "won3d",
|
||||
"handle": "won3d",
|
||||
"text": "early reply from outsider",
|
||||
"timestamp": "2023-01-01 00:01:00",
|
||||
"media_urls": [],
|
||||
"reply_to_id": "1000",
|
||||
"quote_of_id": None,
|
||||
"metrics": {
|
||||
"reply_count": 0,
|
||||
"repost_count": 0,
|
||||
"like_count": 0,
|
||||
"view_count": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"post_id": "1002",
|
||||
"author": "OP",
|
||||
"handle": "OP",
|
||||
"text": "OP self-reply continues the thread",
|
||||
"timestamp": "2023-01-01 00:05:00",
|
||||
"media_urls": [],
|
||||
"reply_to_id": "1000",
|
||||
"quote_of_id": None,
|
||||
"metrics": {
|
||||
"reply_count": 0,
|
||||
"repost_count": 0,
|
||||
"like_count": 0,
|
||||
"view_count": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"post_id": "1003",
|
||||
"author": "OP",
|
||||
"handle": "OP",
|
||||
"text": "OP continues again",
|
||||
"timestamp": "2023-01-01 00:10:00",
|
||||
"media_urls": [],
|
||||
"reply_to_id": "1002",
|
||||
"quote_of_id": None,
|
||||
"metrics": {
|
||||
"reply_count": 0,
|
||||
"repost_count": 0,
|
||||
"like_count": 0,
|
||||
"view_count": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
(self.input_dir / "thread_data.json").write_text(
|
||||
json.dumps(thread_data, indent=2),
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
emit(
|
||||
input_dir=self.input_dir,
|
||||
vault=self.vault,
|
||||
slug=None,
|
||||
archived="2026-08-24",
|
||||
force=True,
|
||||
)
|
||||
|
||||
archive_root = self.vault / "archive" / "threads"
|
||||
thread_dir = next(
|
||||
d for d in (archive_root / "OP").iterdir() if d.is_dir()
|
||||
)
|
||||
text = (thread_dir / "index.md").read_text(encoding="utf-8")
|
||||
# The spine should have 3 OP posts in order: 1000, 1002, 1003.
|
||||
# won3d's post 1001 should be a branch (separate file).
|
||||
self.assertIn("**1/**", text)
|
||||
self.assertIn("**2/**", text)
|
||||
self.assertIn("**3/**", text)
|
||||
# won3d's text appears only in the branch file, not in the spine.
|
||||
won3d_spine_mentions = text.count("early reply from outsider")
|
||||
self.assertEqual(won3d_spine_mentions, 0)
|
||||
# A branch file for won3d exists.
|
||||
branch_files = [
|
||||
f for f in thread_dir.iterdir()
|
||||
if f.is_file() and f.name != "index.md"
|
||||
]
|
||||
self.assertEqual(len(branch_files), 1)
|
||||
branch_text = branch_files[0].read_text(encoding="utf-8")
|
||||
self.assertIn("early reply from outsider", branch_text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+17
-11
@@ -45,18 +45,22 @@ def spine_from_tip(thread: ThreadData, tip_id: str) -> list[str]:
|
||||
|
||||
|
||||
def spine_ids(thread: ThreadData) -> list[str]:
|
||||
"""Walk the linear reply chain from the OP (the post with
|
||||
`reply_to_id == None`) down through `children_map`, picking the
|
||||
first child at each level (already sorted by `(timestamp,
|
||||
post_id)`).
|
||||
"""Walk the OP's own chain down through `children_map`.
|
||||
|
||||
The walk crosses handle boundaries. For a chain that alternates
|
||||
authors (e.g., rianflo ↔ NOTimothyLottes), every post on the
|
||||
chain is included. The archive dir for the thread is owned by
|
||||
the OP — the first post in the returned list.
|
||||
At each level, prefer a same-handle child (to continue the OP's
|
||||
own chain) over any child. Fall back to the earliest child when
|
||||
no same-handle child exists.
|
||||
|
||||
If no post has `reply_to_id == None`, the first post in
|
||||
`thread.posts` is used as a fallback start.
|
||||
The OP is the post with `reply_to_id == None`. The returned
|
||||
list is the OP's chain: the OP at index 0, then same-author
|
||||
replies in chronological order. Cross-author replies to spine
|
||||
posts are branches (rendered as separate files by the emit).
|
||||
|
||||
For a thread where the OP has no self-replies but cross-author
|
||||
replies (e.g., a post that attracted only outside replies), the
|
||||
walker picks the earliest child to start a chain — that branch
|
||||
becomes the only spine entry after the OP, and any other
|
||||
children of the OP become branches.
|
||||
"""
|
||||
ids = by_id(thread)
|
||||
kids = children_map(thread)
|
||||
@@ -65,13 +69,15 @@ def spine_ids(thread: ThreadData) -> list[str]:
|
||||
start = ops[0]
|
||||
else:
|
||||
start = thread.posts[0].post_id
|
||||
handle = ids[start].handle
|
||||
out = [start]
|
||||
cur = start
|
||||
while True:
|
||||
children = kids.get(cur, [])
|
||||
if not children:
|
||||
break
|
||||
cur = children[0]
|
||||
same = [c for c in children if ids[c].handle == handle]
|
||||
cur = same[0] if same else children[0]
|
||||
out.append(cur)
|
||||
return out
|
||||
|
||||
|
||||
Reference in new issue
Block a user