Files
FabledScribe/tests/test_integration_family_adoption.py
T
bvandeusenandClaude Opus 5.5 f006f15058
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m13s
CI & Build / Python tests (push) Successful in 1m54s
CI & Build / Build & push image (push) Successful in 40s
fix(family): a decision read as precedent says its reason once - to_brief for precedents and idea history; the decision log keeps the full row (#5131)
Each decision stored its reason three times (reason, before.reason, after.reason)
and a promotion carried the whole criteria reasoning in evidence. Precedents
(assess, promote, get_family_adoption, get_family_idea) and get_family_idea's
history now serve FamilyDecision.to_brief(): reason once, before/after as status,
canon_version and platforms, evidence as the cited list. list_family_decisions
and the decision echoed back by a write keep to_dict. The precedent count was
already bounded at five.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:30:48 -04:00

581 lines
28 KiB
Python

"""The adoption ledger against real Postgres (milestone 463 step 4).
What the unit lane can't show: an answer moves the row and logs exactly one
decision; an owed answer files its task in the OWING project, tagged to the
System matching the idea's, and the task follows the answer from there; a
version bump leaves every older answer reading as needing a recheck; each
branch of the conflict order folds the losing side into the note and settles
both rows; and the same assessment given twice records nothing the second
time.
Precedent search ranks by meaning and the lane has no embedding model, so
the search is stubbed to "nothing similar" (`_no_meaning`). The same-idea
precedents — another project's answer to this idea — need no embedder, and
are tested here.
"""
from unittest.mock import AsyncMock, patch
import pytest
import pytest_asyncio
from sqlalchemy import select
from scribe.models import async_session
from scribe.models.canonical_system import CanonicalSystem
from scribe.models.family import FamilyAdoption, FamilyDecision, FamilyIdea, Platform, ProjectPlatform
from scribe.models.note import Note
from scribe.models.project import Project
from scribe.models.system import RecordSystem, System
from scribe.models.task_log import TaskLog
from scribe.models.user import User
from scribe.services import family as family_svc
from scribe.services import family_adoption as adoption_svc
from tests.helpers import ensure_user
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
OWNER = "family_adoption_owner"
OUTSIDER = "family_adoption_outsider"
CRITERIA = {
"platform_terms": "stated for any Android app that distributes its own APK",
"platform_problem": "signature continuity is the platform's rule, not one app's",
"proven": "shipped and updated in place on a device",
}
async def _purge(username: str) -> None:
"""SETUP ONLY, as the promotion siblings do: a database call after a
`yield` in an autouse fixture orphans a pooled connection."""
async with async_session() as s:
for user in (await s.execute(select(User).where(User.username == username))).scalars():
for note in (await s.execute(select(Note).where(Note.user_id == user.id))).scalars():
await s.delete(note)
for project in (await s.execute(select(Project).where(Project.user_id == user.id))).scalars():
await s.delete(project)
await s.commit()
@pytest_asyncio.fixture(autouse=True)
async def _clean():
await _purge(OWNER)
await _purge(OUTSIDER)
@pytest.fixture(autouse=True)
def _no_meaning():
with patch("scribe.services.embeddings.semantic_search_notes", AsyncMock(return_value=[])):
yield
async def _platform_id(slug: str) -> int:
async with async_session() as s:
return await s.scalar(select(Platform.id).where(
Platform.slug == slug, Platform.deleted_at.is_(None)))
@pytest_asyncio.fixture
async def family():
"""Two Android apps and a Go service. The idea is a note in the first app,
filed under a System mapped to a canonical area; the second app has its
own System mapped to the same area, and one that is not. Promoted, so
both apps hold an `unassessed` row."""
android, go = await _platform_id("android-app"), await _platform_id("go")
async with async_session() as s:
canonical = await s.scalar(select(CanonicalSystem.id).where(
CanonicalSystem.deleted_at.is_(None)).order_by(CanonicalSystem.id).limit(1))
owner = await ensure_user(s, OWNER)
outsider = await ensure_user(s, OUTSIDER)
a = Project(user_id=owner.id, title="android one")
b = Project(user_id=owner.id, title="android two")
c = Project(user_id=owner.id, title="go service")
s.add_all([a, b, c])
await s.flush()
s.add_all([
ProjectPlatform(project_id=a.id, platform_id=android, state="declared"),
ProjectPlatform(project_id=b.id, platform_id=android, state="declared"),
ProjectPlatform(project_id=c.id, platform_id=go, state="declared"),
])
a_sys = System(user_id=owner.id, project_id=a.id, name="Release", canonical_id=canonical)
b_sys = System(user_id=owner.id, project_id=b.id, name="Shipping", canonical_id=canonical)
b_other = System(user_id=owner.id, project_id=b.id, name="Unrelated")
pattern = Note(user_id=owner.id, project_id=a.id, title="Signed APK lane",
body="One keystore, two channels, in-place update.")
s.add_all([a_sys, b_sys, b_other, pattern])
await s.flush()
s.add(RecordSystem(note_id=pattern.id, system_id=a_sys.id))
await s.commit()
ids = {"owner": owner.id, "outsider": outsider.id, "a": a.id, "b": b.id, "c": c.id,
"note": pattern.id, "b_sys": b_sys.id, "b_other": b_other.id}
out = await family_svc.promote(
ids["owner"], ids["note"], applies_when="any Android app that ships its own APK",
platforms=["android-app"], criteria=CRITERIA,
evidence=["CI green on the release lane"], reason="proven, stated for the platform",
)
assert out["promoted"] is True
return ids
async def _assess(f, project: str, outcome: str, reason: str = "the gap", **kw):
if outcome == "adopted":
kw.setdefault("evidence", ["app/build.gradle: the release lane"])
return await adoption_svc.assess(f["owner"], f[project], f["note"],
outcome=outcome, reason=reason, **kw)
async def _row(f, project: str) -> FamilyAdoption:
async with async_session() as s:
return (await s.execute(select(FamilyAdoption).where(
FamilyAdoption.project_id == f[project], FamilyAdoption.idea_id == f["note"]))).scalars().one()
async def _task(task_id: int) -> Note:
async with async_session() as s:
return await s.get(Note, task_id)
async def _decisions(f, project: str) -> list[FamilyDecision]:
async with async_session() as s:
return list((await s.execute(select(FamilyDecision).where(
FamilyDecision.idea_id == f["note"], FamilyDecision.project_id == f[project])
.order_by(FamilyDecision.id))).scalars().all())
# --- each state change ----------------------------------------------------------------
@pytest.mark.parametrize("outcome", ("adopted", "variant", "exempt"))
async def test_an_answer_moves_the_row_and_logs_one_decision(family, outcome):
out = await _assess(family, "b", outcome, reason="a fact about android two")
row = await _row(family, "b")
assert (row.status, row.reason, row.canon_version, row.decided_via) == (
outcome, "a fact about android two", 1, "agent")
assert row.assessed_at is not None and row.owed_task_id is None
[decision] = await _decisions(family, "b")
assert decision.action == "assess"
assert decision.before == {"status": "unassessed", "reason": "", "canon_version": None}
assert decision.after["status"] == outcome
assert out["changed"] is True and out["owed_task"] is None
async def test_owed_files_a_task_in_the_owing_project_tagged_to_the_matching_system(family):
out = await _assess(family, "b", "owed", reason="no in-place update yet")
row = await _row(family, "b")
assert row.status == "owed" and row.owed_task_id == out["owed_task"]["id"]
task = await _task(row.owed_task_id)
# Filed where the work is owed — never in the idea's own project.
assert task.project_id == family["b"] and task.status == "todo"
assert f"#{family['note']}" in task.body and "no in-place update yet" in task.body
assert "any Android app that ships its own APK" in task.body
async with async_session() as s:
tagged = set((await s.execute(select(RecordSystem.system_id).where(
RecordSystem.note_id == task.id))).scalars().all())
assert tagged == {family["b_sys"]}
async def test_the_owed_task_follows_the_answer(family):
await _assess(family, "b", "owed", reason="not built")
task_id = (await _row(family, "b")).owed_task_id
await _assess(family, "b", "variant", reason="installs through a managed store")
assert (await _task(task_id)).status == "cancelled"
out = await _assess(family, "b", "owed", reason="the store plan fell through")
assert out["owed_task"] == {"id": task_id, "title": (await _task(task_id)).title,
"status": "todo", "action": "reopened"}
await _assess(family, "b", "adopted", reason="built")
assert (await _task(task_id)).status == "done"
async with async_session() as s:
logs = (await s.execute(select(TaskLog.content).where(TaskLog.task_id == task_id)
.order_by(TaskLog.id))).scalars().all()
assert [("variant" in logs[0]), ("owed again" in logs[1]), ("adopted" in logs[2])] == [True] * 3
# One task across the whole life of the answer.
async with async_session() as s:
filed = (await s.execute(select(Note.id).where(
Note.project_id == family["b"], Note.title.like("Adopt the family idea%")))).scalars().all()
assert filed == [task_id]
async def test_the_same_assessment_twice_records_nothing_the_second_time(family):
first = await _assess(family, "b", "owed", reason="not built")
second = await _assess(family, "b", "owed", reason="not built")
assert first["changed"] is True and second["changed"] is False
assert second["decision"] is None
assert second["owed_task"]["id"] == first["owed_task"]["id"]
assert len(await _decisions(family, "b")) == 1
async def test_a_departure_without_a_reason_writes_nothing(family):
with pytest.raises(ValueError, match="exempt needs a reason"):
await _assess(family, "b", "exempt", reason=" ")
with pytest.raises(ValueError, match="adopted needs evidence"):
await _assess(family, "b", "adopted", evidence=[])
assert (await _row(family, "b")).status == "unassessed"
assert await _decisions(family, "b") == []
async def test_an_idea_that_does_not_reach_the_project_is_refused(family):
with pytest.raises(ValueError, match="not on any of"):
await _assess(family, "c", "exempt", reason="it is a Go service")
async def test_only_canon_is_assessed(family):
await family_svc.retire(family["owner"], family["note"], reason="superseded")
with pytest.raises(ValueError, match="not family canon"):
await _assess(family, "a", "adopted")
async def test_someone_who_cannot_write_the_project_cannot_answer_for_it(family):
with pytest.raises(ValueError, match="no write access"):
await adoption_svc.assess(family["outsider"], family["b"], family["note"],
outcome="exempt", reason="not mine to say")
async def test_another_projects_answer_is_recorded_as_precedent(family):
first = await _assess(family, "a", "adopted", reason="the source app")
second = await _assess(family, "b", "owed", reason="not built")
assert first["decision"]["id"] in second["decision"]["precedent_ids"]
assert second["precedents"][0]["relation"] == "same idea, another project"
assert second["precedents"][0]["project_title"] == "android one"
# A precedent says its reason once: before/after are state only (#5131).
assert second["precedents"][0]["reason"] == "the source app"
assert "reason" not in second["precedents"][0]["after"]
# --- recheck --------------------------------------------------------------------------
async def test_a_revision_leaves_every_older_answer_needing_a_recheck(family):
await _assess(family, "a", "adopted")
await _assess(family, "b", "exempt", reason="ships through a store")
out = await family_svc.revise(family["owner"], family["note"],
reason="the keystore now rotates", evidence=["incident"])
assert out["idea"]["canon_version"] == 2
rows = await adoption_svc.list_adoptions(family["owner"], idea_id=family["note"])
assert {r["project_title"]: r["needs_recheck"] for r in rows} == {
"android one": True, "android two": True}
assert [r["project_title"] for r in await adoption_svc.list_adoptions(
family["owner"], idea_id=family["note"], recheck_only=True)] == ["android one", "android two"]
await _assess(family, "a", "adopted", reason="rotation added")
rows = {r["project_title"]: r for r in await adoption_svc.list_adoptions(
family["owner"], idea_id=family["note"])}
assert rows["android one"]["needs_recheck"] is False
assert rows["android one"]["canon_version"] == 2
assert rows["android two"]["needs_recheck"] is True
async def test_a_revision_needs_canon_and_a_reason(family):
with pytest.raises(ValueError, match="needs a reason"):
await family_svc.revise(family["owner"], family["note"], reason="")
with pytest.raises(ValueError, match="at least one platform"):
await family_svc.revise(family["owner"], family["note"], reason="x", platforms=[])
# --- undo ------------------------------------------------------------------------------
async def test_undoing_an_owed_answer_restores_the_row_and_closes_its_task(family):
out = await _assess(family, "b", "owed", reason="not built")
task_id = out["owed_task"]["id"]
decisions = await family_svc.list_decisions(family["owner"], project_id=family["b"])
assert decisions[0]["undoable"] is True and decisions[0]["project_title"] == "android two"
await family_svc.undo(family["owner"], out["decision"]["id"], reason="assessed too early")
row = await _row(family, "b")
assert (row.status, row.reason, row.canon_version, row.assessed_at) == (
"unassessed", None, None, None)
assert (await _task(task_id)).status == "cancelled"
undo_row = (await _decisions(family, "b"))[-1]
assert undo_row.action == "undo" and undo_row.precedent_ids == [out["decision"]["id"]]
async def test_only_the_latest_answer_is_undoable(family):
first = await _assess(family, "b", "owed", reason="not built")
await _assess(family, "b", "adopted", reason="built")
with pytest.raises(ValueError, match="came after it"):
await family_svc.undo(family["owner"], first["decision"]["id"], reason="x")
# --- the conflict order -------------------------------------------------------------
def _checked(ground: str) -> dict:
keys = adoption_svc.CONFLICT_KEYS
return {k: f"{k} does not apply here" for k in keys[:keys.index(ground)]}
async def _resolve(f, ground: str, **kw):
kw.setdefault("evidence", ["named in the record"])
return await adoption_svc.resolve_conflict(
f["owner"], f["note"], canon_project_id=f["a"], other_project_id=f["b"],
ground=ground, grounds_checked=_checked(ground),
fold="Android two signs in CI with a throwaway key.", reason="settled", **kw)
@pytest.mark.parametrize("ground,heading,other_outcome", [
("operator_stance", "### Alternative — android two's approach", "owed"),
("covers_failure", "### Trap — android two's approach", "owed"),
("most_recent_complete", "### Alternative — android two's approach", "owed"),
])
async def test_a_side_that_loses_is_folded_in_and_owes_the_canon(family, ground, heading, other_outcome):
out = await _resolve(family, ground)
async with async_session() as s:
body = (await s.get(Note, family["note"])).body
idea = await s.get(FamilyIdea, family["note"])
assert heading in body and "throwaway key" in body
assert idea.canon_version == 2
assert out["decision"]["action"] == "revise"
assert out["decision"]["evidence"]["conflict"]["ground"] == ground
a, b = await _row(family, "a"), await _row(family, "b")
assert (a.status, a.canon_version) == ("adopted", 2)
assert (b.status, b.canon_version) == (other_outcome, 2)
assert (await _task(b.owed_task_id)).project_id == family["b"]
# Each answer names the revision as the decision it followed.
assert (await _decisions(family, "b"))[-1].precedent_ids == [out["decision"]["id"]]
async def test_a_split_makes_each_side_canon_under_its_condition(family):
out = await _resolve(family, "split_by_condition", evidence=None,
conditions={"canon": "the app updates itself",
"other": "a managed store installs it"})
async with async_session() as s:
body = (await s.get(Note, family["note"])).body
assert "### When a managed store installs it — android two's approach" in body
assert "When the app updates itself, android one's approach above is the canon" in body
a, b = await _row(family, "a"), await _row(family, "b")
assert (a.status, b.status) == ("adopted", "adopted")
assert b.owed_task_id is None and out["owed_task"] is None
async def test_a_ground_cannot_skip_the_order_and_nothing_is_written(family):
async with async_session() as s:
body_before = (await s.get(Note, family["note"])).body
with pytest.raises(ValueError, match="'operator_stance' comes before 'covers_failure'"):
await adoption_svc.resolve_conflict(
family["owner"], family["note"], canon_project_id=family["a"],
other_project_id=family["b"], ground="covers_failure", grounds_checked={},
fold="x", evidence=["incident"], reason="settled")
async with async_session() as s:
assert (await s.get(Note, family["note"])).body == body_before
assert (await s.get(FamilyIdea, family["note"])).canon_version == 1
# --- the matrix -----------------------------------------------------------------------
async def test_the_matrix_shows_every_member_and_who_has_not_been_asked(family):
android = await _platform_id("android-app")
async with async_session() as s:
late = Project(user_id=family["owner"], title="android three")
s.add(late)
await s.flush()
s.add(ProjectPlatform(project_id=late.id, platform_id=android, state="detected"))
await s.commit()
late_id = late.id
await _assess(family, "b", "owed", reason="not built")
matrix = await adoption_svc.adoption_matrix(family["owner"])
cells = {(c["project_title"]): c for c in matrix["cells"] if c["idea_id"] == family["note"]}
assert set(cells) == {"android one", "android two", "android three"}
assert cells["android two"]["owed_task"]["status"] == "todo"
assert cells["android three"]["reached"] is False and cells["android three"]["status"] == "unassessed"
assert [o["key"] for o in matrix["outcomes"]] == list(adoption_svc.OUTCOME_KEYS)
# A late member's first answer creates its row.
await adoption_svc.assess(family["owner"], late_id, family["note"],
outcome="exempt", reason="ships through a store")
one = await adoption_svc.adoption_matrix(family["owner"], project_id=late_id)
assert [c["status"] for c in one["cells"]] == ["exempt"]
# Filtered to a platform no canon idea is for, there is nothing to show.
assert (await adoption_svc.adoption_matrix(family["owner"], platform="go"))["ideas"] == []
async def test_the_matrix_hides_what_the_caller_cannot_read(family):
matrix = await adoption_svc.adoption_matrix(family["outsider"])
assert all(i["note_id"] != family["note"] for i in matrix["ideas"])
# --- the shape ledger as evidence (step 5) -------------------------------------------
REPO = "git.example/android-two"
KOTLIN_BODY = (
"fun installUpdate(context: Context, apk: File) {\n"
" val installer = context.packageManager.packageInstaller\n"
" val session = installer.openSession(installer.createSession(params()))\n"
" apk.inputStream().use { src -> session.openWrite(\"apk\", 0, apk.length()).use(src::copyTo) }\n"
"}\n"
)
B_SHAPES = [
("app/src/main/kotlin/Updater.kt", "sym", "selfUpdate"),
("tools/release.py", "sym", "push_update"),
]
async def _reference(f, *, language: str = "kotlin", name: str = "installUpdate") -> int:
from scribe.services import snippets as snippets_svc
snippet = await snippets_svc.create_snippet(
f["owner"], name=f"{name} ({language})", code=KOTLIN_BODY, language=language,
project_id=f["a"],
)
return int(snippet.id)
@pytest_asyncio.fixture
async def shapes(family):
"""Android two's ledger has two live shapes, and the idea has a Kotlin
reference in android one."""
from scribe.services.shape_ledger import sync_repo_shapes
await sync_repo_shapes(family["b"], REPO, B_SHAPES, seen_marker="main")
family["kotlin"] = await _reference(family)
await adoption_svc.set_references(family["owner"], family["note"], [family["kotlin"]])
return family
async def _classify(f, project: str, symbol: str, status: str, **item):
from scribe.services import shape_ledger
path = next(p for p, _, s in B_SHAPES if s == symbol)
return await shape_ledger.classify_shapes(
f["owner"], f[project], [{"path": path, "symbol": symbol, "status": status, **item}])
async def test_a_shape_classified_against_the_idea_answers_it_adopted(shapes):
"""Across languages: a Python shape is an instance of an idea whose only
reference is Kotlin, and the project's answer moves with it."""
out = await _classify(shapes, "b", "push_update", "instance", idea_id=shapes["note"])
assert out["classified"] == 1
assert [m["status"] for m in out["family"]] == ["adopted"]
row = await _row(shapes, "b")
assert (row.status, row.decided_via, row.canon_version) == ("adopted", "system", 1)
[decision] = await _decisions(shapes, "b")
assert decision.evidence["source"] == adoption_svc.SHAPE_SOURCE
assert decision.evidence["evidence"] == ["tools/release.py::push_update"]
assert decision.evidence["shapes"][0]["snippet_id"] == shapes["kotlin"]
# The matrix shows the code that answers the cell.
matrix = await adoption_svc.adoption_matrix(shapes["owner"], project_id=shapes["b"])
[cell] = [c for c in matrix["cells"] if c["idea_id"] == shapes["note"]]
assert cell["shapes"] == [
{"path": "tools/release.py", "symbol": "push_update", "status": "instance"}]
# A second instance agrees with the standing answer: nothing more is logged.
await _classify(shapes, "b", "selfUpdate", "instance", idea_id=shapes["note"])
assert len(await _decisions(shapes, "b")) == 1
async def test_idea_id_prefers_the_reference_in_the_shapes_language(shapes):
from scribe.services.shape_ledger import live_rows
python_ref = await _reference(shapes, language="python", name="install_update")
await adoption_svc.set_references(shapes["owner"], shapes["note"],
[shapes["kotlin"], python_ref])
await _classify(shapes, "b", "push_update", "instance", idea_id=shapes["note"])
await _classify(shapes, "b", "selfUpdate", "instance", idea_id=shapes["note"])
by_symbol = {r.symbol: r.snippet_id for r in await live_rows(shapes["b"])}
assert by_symbol == {"push_update": python_ref, "selfUpdate": shapes["kotlin"]}
async def test_an_idea_with_no_reference_is_refused_and_nothing_applies(family):
from scribe.services.shape_ledger import live_rows, sync_repo_shapes
await sync_repo_shapes(family["b"], REPO, B_SHAPES, seen_marker="main")
with pytest.raises(ValueError, match="no reference implementation"):
await _classify(family, "b", "push_update", "instance", idea_id=family["note"])
assert {r.status for r in await live_rows(family["b"])} == {"unclassified"}
async def test_a_variant_shape_answers_variant_and_withdrawing_it_unassesses(shapes):
await _classify(shapes, "b", "selfUpdate", "variant", idea_id=shapes["note"],
reason="installs through the device-owner API: it is a kiosk")
row = await _row(shapes, "b")
assert row.status == "variant" and "device-owner API" in row.reason
out = await _classify(shapes, "b", "selfUpdate", "unclassified")
assert [(m["idea_id"], m["status"]) for m in out["family"]] == [
(shapes["note"], "unassessed")]
row = await _row(shapes, "b")
assert (row.status, row.canon_version, row.decided_via) == ("unassessed", None, None)
assert (await _decisions(shapes, "b"))[-1].evidence["source"] == adoption_svc.SHAPE_SOURCE
async def test_the_shapes_close_an_owed_task_as_done(shapes):
owed = await _assess(shapes, "b", "owed", reason="no in-place update yet")
await _classify(shapes, "b", "push_update", "instance", idea_id=shapes["note"])
assert (await _task(owed["owed_task"]["id"])).status == "done"
async def test_an_answer_the_shapes_contradict_is_refused(shapes):
await _classify(shapes, "b", "push_update", "instance", idea_id=shapes["note"])
with pytest.raises(ValueError, match="never disagree"):
await _assess(shapes, "b", "owed", reason="the agent thinks otherwise")
# The agreeing answer is not refused.
await _assess(shapes, "b", "adopted", reason="release.py does it",
evidence=["tools/release.py"])
async def test_an_undo_that_would_contradict_the_shapes_is_refused(shapes):
await _assess(shapes, "b", "owed", reason="not yet")
await _classify(shapes, "b", "push_update", "instance", idea_id=shapes["note"])
latest = (await _decisions(shapes, "b"))[-1]
with pytest.raises(ValueError, match="never disagree"):
await family_svc.undo(shapes["owner"], latest.id, reason="go back")
async def test_an_answer_given_on_other_evidence_survives_the_shapes_leaving(shapes):
"""No shape is not the same as owed: withdrawing a shape only withdraws
an answer the shape ledger gave."""
await _assess(shapes, "b", "adopted", reason="CI proves it",
evidence=["ci run 12"])
await _classify(shapes, "b", "push_update", "instance", idea_id=shapes["note"])
out = await _classify(shapes, "b", "push_update", "unclassified")
assert "family" not in out
assert (await _row(shapes, "b")).status == "adopted"
async def test_nothing_is_answered_or_proposed_across_projects_sharing_no_platform(shapes):
"""The Go service shares no platform with the idea: its shape judged
against the reference is plain canon use, and the proposer never offers
the reference to it."""
from scribe.services import shape_ledger
await shape_ledger.sync_repo_shapes(shapes["c"], "git.example/go", [
("cmd/update.go", "sym", "PushUpdate")], seen_marker="main")
out = await shape_ledger.classify_shapes(shapes["owner"], shapes["c"], [
{"path": "cmd/update.go", "symbol": "PushUpdate", "status": "instance",
"snippet_id": shapes["kotlin"]}])
assert out["classified"] == 1 and "family" not in out
async with async_session() as s:
assert (await s.execute(select(FamilyAdoption).where(
FamilyAdoption.project_id == shapes["c"]))).first() is None
assert await adoption_svc.family_reference_ids(shapes["c"]) == set()
assert await adoption_svc.family_reference_ids(shapes["b"]) == {shapes["kotlin"]}
async def test_the_proposer_offers_the_family_reference_as_the_top_hit(shapes):
from scribe.services import shape_ledger
body = ("def push_update(apk_path: str) -> None:\n"
" session = installer.open_session(installer.create_session())\n"
" session.write_stream('apk', open(apk_path, 'rb'))\n"
" session.commit()\n")
defs = [("tools/release.py", "sym", "push_update", "def push_update(apk_path: str) -> None:",
"sha-push", body)]
ref = type("Hit", (), {"id": shapes["kotlin"]})()
with patch("scribe.services.embeddings.semantic_search_notes",
AsyncMock(return_value=[(0.73, ref)])):
await shape_ledger.propose_for_repo(shapes["owner"], shapes["b"], REPO, defs)
row = next(r for r in await shape_ledger.live_rows(shapes["b"]) if r.symbol == "push_update")
assert (row.proposed_snippet_id, row.proposal_basis) == (shapes["kotlin"], "family")
# Confirming it answers the idea.
out = await shape_ledger.confirm_proposals(shapes["owner"], shapes["b"], basis="family")
assert out["confirmed"] == 1 and out["family"][0]["status"] == "adopted"
async def test_a_conflict_brings_the_losing_sides_variant_shapes_back_to_the_todo(shapes):
from scribe.services.shape_ledger import live_rows
await _classify(shapes, "b", "selfUpdate", "variant", idea_id=shapes["note"],
reason="signs in CI with a throwaway key")
out = await _resolve(shapes, "covers_failure")
assert out["shapes_rejudged"] == {str(shapes["b"]): 1}
b = await _row(shapes, "b")
assert b.status == "owed"
row = next(r for r in await live_rows(shapes["b"]) if r.symbol == "selfUpdate")
assert row.status == "unclassified"