gabriel / musehub public
test_proposal_comment_schema.py python
237 lines 8.5 KB
Raw
sha256:94ef169c149a452bff7c604ded8b280b19bd477c2dabcb56972780b0b784c7aa Merge 'fix/assignee-sigil-inline' into 'dev' — proposal: As… Human 1 day ago
1 """TDD — proposal comment schema additions (migration 0069).
2
3 Fields added to musehub_proposal_comments:
4 author_user_id — content-addressed identity ID; enables sigil without DB lookup
5 and is resilient to handle changes
6 agent_id — AI authorship provenance (empty string = human)
7 model_id — which model authored the comment
8 updated_at — set on edit; None = never edited
9 is_deleted — soft-delete flag (consistent with issue comments)
10
11 Tests:
12 CS-1 New comments persist all five new fields
13 CS-2 author_user_id backfill populates existing rows from musehub_identities
14 CS-3 Human comments have empty agent_id and model_id
15 CS-4 ProposalCommentResponse includes author_user_id
16 CS-5 is_deleted=True excludes the comment from list results
17 """
18 from __future__ import annotations
19
20 import datetime
21 import pytest
22 from sqlalchemy import select
23 from sqlalchemy.ext.asyncio import AsyncSession
24
25 from muse.core.types import fake_id
26 from musehub.core.genesis import compute_comment_id, compute_identity_id, compute_repo_id
27 from musehub.db.musehub_repo_models import MusehubCommitRef
28 from musehub.db.musehub_social_models import MusehubProposal, MusehubProposalComment
29 from tests.factories import create_repo
30
31
32 # ---------------------------------------------------------------------------
33 # Helpers
34 # ---------------------------------------------------------------------------
35
36 def _now() -> datetime.datetime:
37 return datetime.datetime.now(tz=datetime.timezone.utc)
38
39
40 async def _make_comment(
41 session: AsyncSession,
42 repo_id: str,
43 proposal_id: str,
44 author: str = "gabriel",
45 author_user_id: str | None = None,
46 agent_id: str = "",
47 model_id: str = "",
48 body: str = "test comment",
49 is_deleted: bool = False,
50 ) -> MusehubProposalComment:
51 comment_id = compute_comment_id(proposal_id, author, _now().isoformat())
52 c = MusehubProposalComment(
53 comment_id=comment_id,
54 proposal_id=proposal_id,
55 repo_id=repo_id,
56 author=author,
57 author_user_id=author_user_id,
58 agent_id=agent_id,
59 model_id=model_id,
60 body=body,
61 is_deleted=is_deleted,
62 created_at=_now(),
63 )
64 session.add(c)
65 await session.flush()
66 return c
67
68
69 async def _make_proposal(session: AsyncSession, repo_id: str) -> MusehubProposal:
70 from musehub.core.genesis import compute_proposal_id
71 author_id = compute_identity_id(b"gabriel")
72 pid = compute_proposal_id(repo_id, author_id, "feat/x", "dev", _now().isoformat())
73 p = MusehubProposal(
74 proposal_id=pid,
75 repo_id=repo_id,
76 proposal_number=1,
77 title="test proposal",
78 body="",
79 from_branch="feat/x",
80 to_branch="dev",
81 author="gabriel",
82 state="open",
83 created_at=_now(),
84 updated_at=_now(),
85 )
86 session.add(p)
87 await session.flush()
88 return p
89
90
91 # ---------------------------------------------------------------------------
92 # CS-1 — new fields persisted
93 # ---------------------------------------------------------------------------
94
95 @pytest.mark.asyncio
96 async def test_cs1_new_fields_persisted(db_session: AsyncSession) -> None:
97 """All five new fields are written and read back correctly."""
98 repo = await create_repo(db_session)
99 proposal = await _make_proposal(db_session, repo.repo_id)
100 user_id = compute_identity_id(b"gabriel")
101
102 comment = await _make_comment(
103 db_session,
104 repo_id=repo.repo_id,
105 proposal_id=proposal.proposal_id,
106 author="gabriel",
107 author_user_id=user_id,
108 agent_id="claude-code",
109 model_id="claude-sonnet-4-6",
110 is_deleted=False,
111 )
112 await db_session.commit()
113
114 row = await db_session.get(MusehubProposalComment, comment.comment_id)
115 assert row is not None
116 assert row.author_user_id == user_id
117 assert row.agent_id == "claude-code"
118 assert row.model_id == "claude-sonnet-4-6"
119 assert row.updated_at is None
120 assert row.is_deleted is False
121
122
123 # ---------------------------------------------------------------------------
124 # CS-2 — backfill author_user_id from musehub_identities
125 # ---------------------------------------------------------------------------
126
127 @pytest.mark.asyncio
128 async def test_cs2_backfill_author_user_id(db_session: AsyncSession) -> None:
129 """Backfill populates author_user_id for existing rows where it is NULL."""
130 from musehub.db.musehub_identity_models import MusehubIdentity
131
132 repo = await create_repo(db_session)
133 proposal = await _make_proposal(db_session, repo.repo_id)
134 user_id = compute_identity_id(b"gabriel")
135
136 # Insert identity row so backfill can look it up.
137 identity = MusehubIdentity(
138 identity_id=user_id,
139 handle="gabriel",
140 )
141 db_session.add(identity)
142
143 # Comment with no author_user_id (simulates pre-migration row).
144 comment_id = compute_comment_id(proposal.proposal_id, "gabriel", _now().isoformat())
145 c = MusehubProposalComment(
146 comment_id=comment_id,
147 proposal_id=proposal.proposal_id,
148 repo_id=repo.repo_id,
149 author="gabriel",
150 author_user_id=None, # pre-migration
151 body="old comment",
152 created_at=_now(),
153 )
154 db_session.add(c)
155 await db_session.flush()
156
157 # Run backfill.
158 from musehub.services.musehub_proposals import backfill_comment_author_user_ids
159 updated = await backfill_comment_author_user_ids(db_session, repo_id=repo.repo_id)
160 await db_session.commit()
161
162 row = await db_session.get(MusehubProposalComment, comment_id)
163 assert row.author_user_id == user_id, (
164 f"backfill must populate author_user_id from musehub_identities; got {row.author_user_id!r}"
165 )
166 assert updated >= 1
167
168
169 # ---------------------------------------------------------------------------
170 # CS-3 — human comments have empty agent_id / model_id
171 # ---------------------------------------------------------------------------
172
173 @pytest.mark.asyncio
174 async def test_cs3_human_comments_have_empty_provenance(db_session: AsyncSession) -> None:
175 """Human-authored comments default to empty agent_id and model_id."""
176 repo = await create_repo(db_session)
177 proposal = await _make_proposal(db_session, repo.repo_id)
178
179 comment = await _make_comment(
180 db_session, repo.repo_id, proposal.proposal_id,
181 # no agent_id / model_id — human defaults
182 )
183
184 row = await db_session.get(MusehubProposalComment, comment.comment_id)
185 assert row.agent_id == "" or row.agent_id is None
186 assert row.model_id == "" or row.model_id is None
187
188
189 # ---------------------------------------------------------------------------
190 # CS-4 — ProposalCommentResponse includes author_user_id
191 # ---------------------------------------------------------------------------
192
193 @pytest.mark.asyncio
194 async def test_cs4_response_includes_author_user_id(db_session: AsyncSession) -> None:
195 """list_proposal_comments returns author_user_id on each comment."""
196 from musehub.services import musehub_proposals
197
198 repo = await create_repo(db_session)
199 proposal = await _make_proposal(db_session, repo.repo_id)
200 user_id = compute_identity_id(b"gabriel")
201
202 await _make_comment(
203 db_session, repo.repo_id, proposal.proposal_id,
204 author="gabriel", author_user_id=user_id,
205 )
206 await db_session.flush()
207
208 result = await musehub_proposals.list_proposal_comments(
209 db_session, proposal.proposal_id, repo.repo_id
210 )
211 assert result.total == 1
212 comment = result.comments[0]
213 assert hasattr(comment, "author_user_id"), "ProposalCommentResponse must expose author_user_id"
214 assert comment.author_user_id == user_id
215
216
217 # ---------------------------------------------------------------------------
218 # CS-5 — is_deleted=True excluded from list
219 # ---------------------------------------------------------------------------
220
221 @pytest.mark.asyncio
222 async def test_cs5_deleted_comments_excluded_from_list(db_session: AsyncSession) -> None:
223 """Comments with is_deleted=True do not appear in list results."""
224 from musehub.services import musehub_proposals
225
226 repo = await create_repo(db_session)
227 proposal = await _make_proposal(db_session, repo.repo_id)
228
229 await _make_comment(db_session, repo.repo_id, proposal.proposal_id, body="visible")
230 await _make_comment(db_session, repo.repo_id, proposal.proposal_id, body="deleted", is_deleted=True)
231 await db_session.flush()
232
233 result = await musehub_proposals.list_proposal_comments(
234 db_session, proposal.proposal_id, repo.repo_id
235 )
236 assert result.total == 1
237 assert result.comments[0].body == "visible"
File History 1 commit
sha256:94ef169c149a452bff7c604ded8b280b19bd477c2dabcb56972780b0b784c7aa Merge 'fix/assignee-sigil-inline' into 'dev' — proposal: As… Human 1 day ago