feat(500): Settings shows the shipped reply shapes, what adjusts each, and how often each arrives
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m19s
CI & Build / Python tests (push) Successful in 1m58s
CI & Build / Build & push image (push) Successful in 39s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m19s
CI & Build / Python tests (push) Successful in 1m58s
CI & Build / Build & push image (push) Successful in 39s
Settings > General > Reply shapes. One menu row per shape (core, completion, asks, plan): its title and when it arrives, how many of the operator's preferences adjust it, and its deliveries over 30 days (in full / as a reminder, "—" when the counts cannot be read). A verdict line leads. Opening a row shows the shipped text, read-only because it is product, and the preferences mounted on its moment; each opens in the rule editor, and "Adjust this" opens a new preference already mounted on the shape's moment with a starting trigger. The rule editor takes an optional preset (kind, moments, trigger) and, opened outside the Rules view, asks where a new record lives instead of dropping it. GET /api/retrieval/reply-shapes now returns reply_shapes.overview: the catalog plus mounted preferences (the same lookup that delivers them) and counts from the reply_shape delivery rows. #5497 (step 5 of milestone 500). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -139,18 +139,91 @@ def test_both_doors_read_one_catalog():
|
||||
assert routes.shapes_svc is reply_shapes
|
||||
|
||||
|
||||
async def test_the_route_returns_the_catalog():
|
||||
async def test_the_route_returns_the_operators_overview_with_a_bounded_window():
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from quart import Quart, g
|
||||
|
||||
from scribe.routes import retrieval as routes
|
||||
|
||||
view = AsyncMock(return_value={"shapes": [], "total": 0})
|
||||
app = Quart(__name__)
|
||||
async with app.test_request_context("/api/retrieval/reply-shapes"):
|
||||
g.user = SimpleNamespace(id=7)
|
||||
resp = await routes.reply_shapes_route.__wrapped__()
|
||||
assert await resp.get_json() == reply_shapes.catalog()
|
||||
for raw, days in (("", 30), ("7", 7), ("900", 90), ("x", 30)):
|
||||
async with app.test_request_context("/api/retrieval/reply-shapes", query_string={"days": raw}):
|
||||
g.user = SimpleNamespace(id=7)
|
||||
with patch.object(routes.shapes_svc, "overview", view):
|
||||
resp = await routes.reply_shapes_route.__wrapped__()
|
||||
assert await resp.get_json() == {"shapes": [], "total": 0}
|
||||
assert view.await_args.kwargs == {"days": days}
|
||||
|
||||
|
||||
# ── the operator's view (milestone 500 step 5) ──────────────────────────
|
||||
|
||||
|
||||
async def test_the_overview_lists_the_preferences_mounted_on_each_shapes_moment():
|
||||
"""What adjusts a shape is what is mounted beside it — and only
|
||||
preferences: a rule on the same moment binds, it does not reshape."""
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
pref = SimpleNamespace(id=173, title="Problem first", statement="…", kind="preference")
|
||||
rule = SimpleNamespace(id=11, title="Definition of done", statement="…", kind="rule")
|
||||
|
||||
async def mounted(user_id, moments, project_id=None):
|
||||
return [(pref, "reply.report"), (rule, "reply.report")] if moments == ["reply.report"] else []
|
||||
|
||||
counts = {k: {"full": 0, "pointer": 0} for k in reply_shapes.SHAPES}
|
||||
counts["core"] = {"full": 2, "pointer": 9}
|
||||
with patch("scribe.services.rulebooks.rules_on_moments", mounted), \
|
||||
patch.object(reply_shapes, "delivery_counts", AsyncMock(return_value=counts)):
|
||||
data = await reply_shapes.overview(7, days=30)
|
||||
rows = {r["key"]: r for r in data["shapes"]}
|
||||
assert rows["core"]["preferences"] == [{"id": 173, "title": "Problem first", "statement": "…"}]
|
||||
assert rows["core"]["deliveries"] == {"full": 2, "pointer": 9}
|
||||
assert rows["completion"]["preferences"] == []
|
||||
assert data["days"] == 30 and data["deliveries_failed"] is False
|
||||
|
||||
|
||||
async def test_counts_that_fail_read_as_unknown_not_zero():
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
async def mounted(user_id, moments, project_id=None):
|
||||
return []
|
||||
|
||||
with patch("scribe.services.rulebooks.rules_on_moments", mounted), \
|
||||
patch.object(reply_shapes, "delivery_counts", AsyncMock(side_effect=RuntimeError("db"))):
|
||||
data = await reply_shapes.overview(7)
|
||||
assert data["deliveries_failed"] is True
|
||||
assert all(r["deliveries"] is None for r in data["shapes"])
|
||||
|
||||
|
||||
async def test_delivery_counts_tally_each_shape_by_form_and_skip_what_does_not_parse():
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
rows = [json.dumps({"shapes": {"core": "full"}, "via": "turn"}),
|
||||
json.dumps({"shapes": {"core": "pointer", "completion": "full"}, "via": "hook"}),
|
||||
json.dumps({"shapes": {"core": "pointer", "bogus": "full"}, "via": "turn"}),
|
||||
"not json", None]
|
||||
result = MagicMock()
|
||||
result.scalars.return_value.all.return_value = rows
|
||||
|
||||
class _Session:
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return False
|
||||
|
||||
async def execute(self, _stmt):
|
||||
return result
|
||||
|
||||
with patch("scribe.models.async_session", lambda: _Session()):
|
||||
counts = await reply_shapes.delivery_counts(7, days=30)
|
||||
assert counts["core"] == {"full": 1, "pointer": 2}
|
||||
assert counts["completion"] == {"full": 1, "pointer": 0}
|
||||
assert "bogus" not in counts
|
||||
|
||||
|
||||
# ── delivery (milestone 500 step 3) ─────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user