feat(family): an idea links to the rule topic holding its norms - set_family_topic, the readouts name the rules, the idea row shows the link (milestone 463 step 1 gap, found in step 7, #4993)
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 1m9s
CI & Build / integration (push) Successful in 1m21s
CI & Build / Python tests (push) Successful in 2m2s
CI & Build / Build & push image (push) Successful in 1m29s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 1m9s
CI & Build / integration (push) Successful in 1m21s
CI & Build / Python tests (push) Successful in 2m2s
CI & Build / Build & push image (push) Successful in 1m29s
Step 1 added family_ideas.topic_id but nothing could set it, so the note<->topic link the milestone promised existed only as a column. set_family_topic links or unlinks (0) a topic in a rulebook the caller owns, one idea per topic; the version does not move. get_family_idea and get_family_adoption list the topic's rules; list_family_ideas and the Family page name the topic. The model docstrings no longer claim the topic's rules are platform-scoped in retrieval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ test here (`_no_meaning`); the repeat tests stub it to one hit, to pin what
|
||||
the trigger does WITH a hit. The ranking itself is the shared semantic
|
||||
search, tested where that lives.
|
||||
"""
|
||||
import uuid
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -24,6 +25,7 @@ from scribe.models.family import (
|
||||
from scribe.models.milestone import Milestone
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.rulebook import Rule, Rulebook, RulebookTopic
|
||||
from scribe.models.user import User
|
||||
from scribe.services import family as family_svc
|
||||
from tests.helpers import ensure_user
|
||||
@@ -291,3 +293,86 @@ async def test_a_milestone_closing_on_a_platform_asks_and_one_off_platform_does_
|
||||
assert hint and "Android app" in hint
|
||||
assert await family_svc.milestone_trigger(family["owner"], off_platform) is None
|
||||
assert await family_svc.milestone_is_open(family["owner"], on_platform.id) is True
|
||||
|
||||
|
||||
# --- the note↔topic link ----------------------------------------------------------
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def topics(family):
|
||||
"""A topic of the owner's with two rules, a second empty one, and one in
|
||||
the outsider's rulebook. Titles carry a nonce: rulebooks are not purged
|
||||
with the notes, and a topic's title is unique within its rulebook."""
|
||||
nonce = uuid.uuid4().hex[:8]
|
||||
async with async_session() as s:
|
||||
mine = Rulebook(owner_user_id=family["owner"], title=f"family norms {nonce}")
|
||||
theirs = Rulebook(owner_user_id=family["outsider"], title=f"their norms {nonce}")
|
||||
s.add_all([mine, theirs])
|
||||
await s.flush()
|
||||
norms = RulebookTopic(rulebook_id=mine.id, title="distribution")
|
||||
spare = RulebookTopic(rulebook_id=mine.id, title="spare")
|
||||
foreign = RulebookTopic(rulebook_id=theirs.id, title="theirs")
|
||||
s.add_all([norms, spare, foreign])
|
||||
await s.flush()
|
||||
s.add_all([
|
||||
Rule(topic_id=norms.id, title="One release key", statement="Sign with one key.",
|
||||
order_index=0),
|
||||
Rule(topic_id=norms.id, title="No debug signer", statement="Check the signer.",
|
||||
order_index=1),
|
||||
])
|
||||
await s.commit()
|
||||
return {"norms": norms.id, "spare": spare.id, "foreign": foreign.id,
|
||||
"rulebook": mine.id}
|
||||
|
||||
|
||||
async def test_an_idea_links_to_its_topic_and_its_reads_name_the_rules(family, topics):
|
||||
await _promote(family)
|
||||
out = await family_svc.set_topic(family["owner"], family["note"], topics["norms"])
|
||||
assert out["topic_id"] == topics["norms"]
|
||||
assert [r["title"] for r in out["topic"]["rules"]] == ["One release key", "No debug signer"]
|
||||
# Where the norms are found is not what they say: the version holds.
|
||||
assert out["canon_version"] == 1
|
||||
|
||||
idea = await family_svc.get_idea(family["owner"], family["note"])
|
||||
assert idea["topic"]["rulebook_id"] == topics["rulebook"]
|
||||
assert len(idea["topic"]["rules"]) == 2
|
||||
[listed] = [i for i in await family_svc.list_ideas(family["owner"])
|
||||
if i["note_id"] == family["note"]]
|
||||
assert listed["topic"] == {"id": topics["norms"], "title": "distribution",
|
||||
"rulebook_id": topics["rulebook"]}
|
||||
|
||||
|
||||
async def test_zero_unlinks_the_topic(family, topics):
|
||||
await _promote(family)
|
||||
await family_svc.set_topic(family["owner"], family["note"], topics["norms"])
|
||||
out = await family_svc.set_topic(family["owner"], family["note"], 0)
|
||||
assert out["topic_id"] is None and out["topic"] is None
|
||||
|
||||
|
||||
async def test_a_topic_outside_the_callers_rulebooks_is_refused(family, topics):
|
||||
await _promote(family)
|
||||
with pytest.raises(ValueError, match="rulebook you own"):
|
||||
await family_svc.set_topic(family["owner"], family["note"], topics["foreign"])
|
||||
assert (await _idea(family["note"])).topic_id is None
|
||||
|
||||
|
||||
async def test_one_topic_carries_one_ideas_norms(family, topics):
|
||||
await _promote(family)
|
||||
await family_svc.set_topic(family["owner"], family["note"], topics["norms"])
|
||||
async with async_session() as s:
|
||||
other = Note(user_id=family["owner"], project_id=family["b"], title="Another idea",
|
||||
body="something else every Android app needs")
|
||||
s.add(other)
|
||||
await s.flush()
|
||||
s.add(FamilyIdea(note_id=other.id, status="candidate"))
|
||||
await s.commit()
|
||||
other_id = other.id
|
||||
with pytest.raises(ValueError, match="already carries the norms"):
|
||||
await family_svc.set_topic(family["owner"], other_id, topics["norms"])
|
||||
# Re-linking the same idea to its own topic is not a conflict.
|
||||
await family_svc.set_topic(family["owner"], family["note"], topics["norms"])
|
||||
|
||||
|
||||
async def test_someone_who_cannot_write_the_idea_cannot_link_it(family, topics):
|
||||
await _promote(family)
|
||||
with pytest.raises(ValueError, match="no write access"):
|
||||
await family_svc.set_topic(family["outsider"], family["note"], topics["spare"])
|
||||
|
||||
Reference in New Issue
Block a user