feat(500): reply shapes are delivered - the core every turn through the ledger, each slice at its moment, reply mounts before the reply (#5495)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Failing after 1m31s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Failing after 1m31s
CI & Build / Build & push image (push) Skipped
- Every turn (/api/plugin/retrieve, UserPromptSubmit): the core reply shape leads the payload - in full the first time, as its one-line reminder after that - followed by whatever is mounted on reply.report, under the shared rule ledger. Fresh keys come back as shape_keys. - The ledger is <sid>.shapes.ids in scribe-priorart (scribe_shapes_file / _seen / _append), so the compaction sweep that clears every .ids ledger is what brings the full core back after one. - At a moment (/api/plugin/moment): the slice for that reply - completion on work.finish, asks on reply.ask, plan on work.plan - ahead of the mounted rules. reachable_tools now lists tools reaching a shaped moment even on an install with nothing mounted. - Scribe's own tools (attach_moment_rules): reply_shape in the response, in full, since that door has no ledger. enter_project carries the core for clients with no prompt hook. - Telemetry: one AppLog row per delivery (plugin / reply_shape), each shape with full or pointer and the door (turn, hook, mcp). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,9 @@ import pytest
|
||||
from scribe.services import moments, reply_shapes
|
||||
from tests.helpers import FakeMCP, dev_only_hits
|
||||
|
||||
# Bound before conftest's autouse stub replaces the module attribute.
|
||||
_REAL_RECORD = reply_shapes.record_delivery
|
||||
|
||||
|
||||
def test_there_is_a_core_and_at_least_one_slice():
|
||||
"""The sweeps below are vacuous over an empty catalog (rule 167)."""
|
||||
@@ -148,3 +151,92 @@ async def test_the_route_returns_the_catalog():
|
||||
g.user = SimpleNamespace(id=7)
|
||||
resp = await routes.reply_shapes_route.__wrapped__()
|
||||
assert await resp.get_json() == reply_shapes.catalog()
|
||||
|
||||
|
||||
# ── delivery (milestone 500 step 3) ─────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", list(reply_shapes.SHAPES))
|
||||
def test_every_shape_has_a_one_line_reminder(key):
|
||||
shape = reply_shapes.SHAPES[key]
|
||||
assert shape.reminder.strip() and "\n" not in shape.reminder
|
||||
assert not dev_only_hits(shape.reminder), key
|
||||
|
||||
|
||||
def test_the_full_form_carries_the_text_and_says_a_preference_wins():
|
||||
core = reply_shapes.core()
|
||||
out = reply_shapes.render(core, full=True)
|
||||
assert out.endswith(core.text)
|
||||
assert core.title in out and "preference" in out
|
||||
|
||||
|
||||
def test_the_pointer_form_is_one_line_carrying_the_reminder():
|
||||
core = reply_shapes.core()
|
||||
out = reply_shapes.render(core, full=False)
|
||||
assert "\n" not in out
|
||||
assert core.reminder in out and "list_reply_shapes" in out
|
||||
assert core.text not in out
|
||||
|
||||
|
||||
def test_a_shape_the_session_holds_goes_out_as_its_pointer():
|
||||
shapes = [reply_shapes.core(), reply_shapes.SHAPES["completion"]]
|
||||
blocks, forms = reply_shapes.deliver(shapes, frozenset({"core"}))
|
||||
assert forms == {"core": reply_shapes.POINTER, "completion": reply_shapes.FULL}
|
||||
assert reply_shapes.core().text not in blocks[0]
|
||||
assert reply_shapes.SHAPES["completion"].text in blocks[1]
|
||||
|
||||
|
||||
def test_a_door_with_no_ledger_sends_everything_in_full():
|
||||
_blocks, forms = reply_shapes.deliver([reply_shapes.core()], frozenset())
|
||||
assert forms == {"core": reply_shapes.FULL}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw,keys", [
|
||||
("core", {"core"}),
|
||||
("core, asks,core", {"core", "asks"}),
|
||||
("", set()),
|
||||
(None, set()),
|
||||
("core,nonsense,../x", {"core"}),
|
||||
])
|
||||
def test_the_ledger_keeps_only_shapes_that_exist(raw, keys):
|
||||
assert reply_shapes.parse_seen(raw) == frozenset(keys)
|
||||
|
||||
|
||||
async def test_a_delivery_is_one_plugin_row_naming_each_shape_and_its_form():
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
added = []
|
||||
session = MagicMock()
|
||||
session.add = added.append
|
||||
|
||||
async def _commit():
|
||||
return None
|
||||
session.commit = _commit
|
||||
|
||||
class _Ctx:
|
||||
async def __aenter__(self):
|
||||
return session
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return False
|
||||
|
||||
with patch("scribe.models.async_session", lambda: _Ctx()):
|
||||
await _REAL_RECORD(7, {"core": "pointer", "asks": "full"}, via="turn")
|
||||
assert len(added) == 1
|
||||
row = added[0]
|
||||
assert (row.category, row.action, row.user_id) == ("plugin", "reply_shape", 7)
|
||||
import json
|
||||
assert json.loads(row.details) == {"shapes": {"core": "pointer", "asks": "full"},
|
||||
"via": "turn"}
|
||||
|
||||
|
||||
async def test_the_delivery_row_fails_open_and_skips_an_empty_delivery():
|
||||
from unittest.mock import patch
|
||||
|
||||
def _boom():
|
||||
raise RuntimeError("db down")
|
||||
|
||||
with patch("scribe.models.async_session", _boom):
|
||||
await _REAL_RECORD(7, {"core": "full"}, via="turn") # no raise
|
||||
await _REAL_RECORD(7, {}, via="turn")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user