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:
@@ -102,6 +102,20 @@ def _no_supersession():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_reply_shape_log():
|
||||
"""Stub the reply-shape delivery row (milestone 500 step 3).
|
||||
|
||||
Autouse for _no_system_labels' reason: every turn's retrieval, every
|
||||
moment request and every Scribe tool that reaches a shaped moment writes
|
||||
one AppLog row, and those paths are tested without a database.
|
||||
tests/test_reply_shapes.py binds the real function at import time,
|
||||
before this patch runs.
|
||||
"""
|
||||
with patch("scribe.services.reply_shapes.record_delivery", AsyncMock()):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_system_labels():
|
||||
"""Stub the menu's "which System is each line about?" lookup (#4364).
|
||||
|
||||
+130
-14
@@ -185,18 +185,23 @@ async def _reachable(mounted, mappings=()):
|
||||
return await md.reachable_tools(1)
|
||||
|
||||
|
||||
async def test_an_install_with_nothing_mounted_keeps_the_hook_off_the_wire():
|
||||
listing = AsyncMock()
|
||||
with patch.object(rulebooks, "mounted_moments", AsyncMock(return_value=set())), \
|
||||
patch.object(moment_actions, "list_mappings", listing):
|
||||
assert await md.reachable_tools(1) == []
|
||||
listing.assert_not_awaited()
|
||||
# The tools the shipped defaults map onto a moment that carries a default reply
|
||||
# shape (milestone 500): closing work, a structured question, opening a plan.
|
||||
SHAPED = ["askuserquestion", "create_milestone", "enterplanmode", "exitplanmode",
|
||||
"skill", "start_planning", "update_milestone", "update_task"]
|
||||
|
||||
|
||||
async def test_only_the_tools_whose_moments_carry_a_mount_are_listed():
|
||||
assert await _reachable({"work.deliver"}) == ["bash", "skill"]
|
||||
assert await _reachable({"work.finish"}) == ["skill", "update_milestone", "update_task"]
|
||||
assert await _reachable({"skill.release"}) == ["skill"]
|
||||
async def test_an_install_with_nothing_mounted_still_asks_about_the_shaped_moments():
|
||||
"""The reply shapes are product, so every install has them: the hook asks
|
||||
about the acts that bring one, and about nothing else."""
|
||||
assert await _reachable(set()) == SHAPED
|
||||
|
||||
|
||||
async def test_only_the_tools_whose_moments_carry_a_mount_or_a_shape_are_listed():
|
||||
assert await _reachable({"work.deliver"}) == sorted([*SHAPED, "bash"])
|
||||
assert await _reachable({"work.finish"}) == SHAPED
|
||||
assert await _reachable({"skill.release"}) == SHAPED
|
||||
assert "bash" not in await _reachable(set())
|
||||
|
||||
|
||||
async def test_the_skill_loader_counts_whenever_anything_is_mounted():
|
||||
@@ -232,7 +237,8 @@ async def test_the_route_reads_the_event_and_the_ledger():
|
||||
with patch.object(routes.moment_delivery_svc, "deliver_for_act", deliver):
|
||||
resp = await routes.moment.__wrapped__()
|
||||
body = await resp.get_json()
|
||||
assert body == {"context": "a line", "rule_ids": [5], "moments": ["work.deliver"]}
|
||||
assert body == {"context": "a line", "rule_ids": [5], "moments": ["work.deliver"],
|
||||
"shape_keys": []}
|
||||
args, kw = deliver.await_args
|
||||
assert args == (7, "Bash", {"command": "git push"})
|
||||
assert kw == {"project_id": 3, "exclude": frozenset({9}), "held": frozenset({4})}
|
||||
@@ -245,7 +251,8 @@ async def test_the_route_answers_an_empty_event_with_nothing():
|
||||
async with app.test_request_context("/api/plugin/moment", method="POST", json={}):
|
||||
g.user = SimpleNamespace(id=7)
|
||||
resp = await routes.moment.__wrapped__()
|
||||
assert await resp.get_json() == {"context": "", "rule_ids": [], "moments": []}
|
||||
assert await resp.get_json() == {"context": "", "rule_ids": [], "moments": [],
|
||||
"shape_keys": []}
|
||||
|
||||
|
||||
def test_both_routes_are_on_the_app():
|
||||
@@ -273,13 +280,15 @@ def test_the_hook_and_the_routes_agree_on_every_name():
|
||||
assert "scribe_rules_live" in src and "scribe_rules_append" in src
|
||||
# The SHARED ledger, not one of its own.
|
||||
assert '"${TMPDIR:-/tmp}/scribe-priorart"' in src and ".rules.ids" in src
|
||||
for field in (".tools", ".rule_ids", ".context"):
|
||||
for field in (".tools", ".rule_ids", ".context", ".shape_keys"):
|
||||
assert f"'{field}'" in src
|
||||
assert "shapes_seen" in src and "scribe_shapes_append" in src
|
||||
|
||||
from scribe.routes import plugin as routes
|
||||
|
||||
route = inspect.getsource(routes.moment)
|
||||
for name in ("exclude_rule_ids", "held_rule_ids", "tool_name", "tool_input"):
|
||||
for name in ("exclude_rule_ids", "held_rule_ids", "tool_name", "tool_input",
|
||||
"shapes_seen", "shape_keys"):
|
||||
assert name in route
|
||||
|
||||
|
||||
@@ -380,3 +389,110 @@ def test_every_scribe_tool_the_defaults_name_attaches_its_moment_rules():
|
||||
f"attach its mounted rules — a client without the plugin would never "
|
||||
f"receive them"
|
||||
)
|
||||
|
||||
|
||||
# ── reply shapes ride the moments (milestone 500 step 3) ────────────────
|
||||
|
||||
from scribe.services import reply_shapes # noqa: E402
|
||||
|
||||
FINISH = [{"moment": "work.finish", "tool": "update_task", "match": "status=done", "via": "default"}]
|
||||
ASK = [{"moment": "reply.ask", "tool": "AskUserQuestion", "match": "", "via": "default"}]
|
||||
|
||||
|
||||
def test_an_act_brings_the_shape_for_the_reply_it_comes_before():
|
||||
blocks, forms = md.shapes_for_act(FINISH)
|
||||
assert forms == {"completion": reply_shapes.FULL}
|
||||
assert reply_shapes.SHAPES["completion"].text in blocks[0]
|
||||
_blocks, forms = md.shapes_for_act(ASK, frozenset({"asks"}))
|
||||
assert forms == {"asks": reply_shapes.POINTER}
|
||||
|
||||
|
||||
def test_an_act_at_an_unshaped_moment_brings_no_shape():
|
||||
assert md.shapes_for_act(PUSH) == ([], {})
|
||||
|
||||
|
||||
async def test_a_turn_carries_the_core_in_full_until_the_session_holds_it():
|
||||
with patch.object(md, "deliver_moments", AsyncMock(return_value=rp.RuleResult())):
|
||||
first = await md.deliver_for_turn(1)
|
||||
later = await md.deliver_for_turn(1, seen=frozenset({"core"}))
|
||||
assert first["shape_forms"] == {"core": reply_shapes.FULL}
|
||||
assert reply_shapes.core().text in first["context"]
|
||||
assert later["shape_forms"] == {"core": reply_shapes.POINTER}
|
||||
assert reply_shapes.core().text not in later["context"]
|
||||
assert reply_shapes.core().reminder in later["context"]
|
||||
|
||||
|
||||
async def test_a_turn_brings_what_is_mounted_on_reply_report_after_the_core():
|
||||
deliver = AsyncMock(return_value=rp.RuleResult(lines=["Preference that may apply"], rule_ids=[173]))
|
||||
with patch.object(md, "deliver_moments", deliver):
|
||||
out = await md.deliver_for_turn(1, project_id=2, exclude=frozenset({9}), held=frozenset({4}))
|
||||
assert out["context"].index(reply_shapes.core().title) < out["context"].index("Preference")
|
||||
assert out["rule_ids"] == [173]
|
||||
reached = deliver.await_args.args[1]
|
||||
assert [hit["moment"] for hit in reached] == ["reply.report"]
|
||||
assert deliver.await_args.kwargs == {"project_id": 2, "exclude": frozenset({9}),
|
||||
"held": frozenset({4})}
|
||||
|
||||
|
||||
async def test_a_failed_mount_lookup_still_delivers_the_core():
|
||||
with patch.object(md, "deliver_moments", AsyncMock(side_effect=RuntimeError("db down"))):
|
||||
out = await md.deliver_for_turn(1)
|
||||
assert reply_shapes.core().text in out["context"] and out["rule_ids"] == []
|
||||
|
||||
|
||||
async def test_a_turn_records_its_delivery():
|
||||
with patch.object(md, "deliver_moments", AsyncMock(return_value=rp.RuleResult())):
|
||||
await md.deliver_for_turn(1, seen=frozenset({"core"}))
|
||||
reply_shapes.record_delivery.assert_awaited_with(1, {"core": reply_shapes.POINTER}, via="turn")
|
||||
|
||||
|
||||
async def test_a_task_closed_through_the_tool_carries_the_completion_shape():
|
||||
data = {"id": 40, "status": "done", "project_id": 2}
|
||||
out = await _with(_stub_db([]), lambda: md.attach_moment_rules(
|
||||
1, "update_task", {"status": "done"}, data,
|
||||
))
|
||||
assert reply_shapes.SHAPES["completion"].text in out["reply_shape"]
|
||||
assert "moment_rules" not in out # nothing mounted, still shaped
|
||||
|
||||
|
||||
async def test_an_unshaped_act_through_a_tool_carries_no_shape():
|
||||
out = await _with(_stub_db([]), lambda: md.attach_moment_rules(
|
||||
1, "create_note", {"project_id": 2}, {"id": 3},
|
||||
))
|
||||
assert "reply_shape" not in out
|
||||
|
||||
|
||||
async def test_the_route_puts_the_shape_ahead_of_the_rules_and_returns_its_key():
|
||||
from scribe.routes import plugin as routes
|
||||
|
||||
deliver = AsyncMock(return_value=(ASK, rp.RuleResult(lines=["a line"], rule_ids=[5])))
|
||||
app = Quart(__name__)
|
||||
event = {"tool_name": "AskUserQuestion", "tool_input": {}}
|
||||
async with app.test_request_context("/api/plugin/moment", method="POST", json=event):
|
||||
g.user = SimpleNamespace(id=7)
|
||||
with patch.object(routes.moment_delivery_svc, "deliver_for_act", deliver):
|
||||
body = await (await routes.moment.__wrapped__()).get_json()
|
||||
assert body["shape_keys"] == ["asks"]
|
||||
assert body["context"].index("Who decides what") < body["context"].index("a line")
|
||||
|
||||
async with app.test_request_context("/api/plugin/moment", method="POST", json=event,
|
||||
query_string={"shapes_seen": "asks"}):
|
||||
g.user = SimpleNamespace(id=7)
|
||||
with patch.object(routes.moment_delivery_svc, "deliver_for_act", deliver):
|
||||
body = await (await routes.moment.__wrapped__()).get_json()
|
||||
assert body["shape_keys"] == []
|
||||
assert "Who decides what" not in body["context"]
|
||||
|
||||
|
||||
def test_the_hook_carries_the_shape_ledger_between_calls(tmp_path):
|
||||
replies = {"/api/plugin/moment-tools": b'{"tools":["askuserquestion"]}',
|
||||
"/api/plugin/moment": json.dumps(
|
||||
{"context": "Reply shape", "rule_ids": [], "moments": ["reply.ask"],
|
||||
"shape_keys": ["asks"]}).encode()}
|
||||
with http_sink(by_path=replies) as (port, seen):
|
||||
_hook(tmp_path, port, tool="AskUserQuestion")
|
||||
_hook(tmp_path, port, tool="AskUserQuestion")
|
||||
moment_calls = [e for e in seen if e["_path"] == "/api/plugin/moment"]
|
||||
assert "shapes_seen" not in moment_calls[0]
|
||||
assert moment_calls[1]["shapes_seen"] == ["asks"]
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""The core reply shape rides every turn (milestone 500 step 3).
|
||||
|
||||
Every turn ends in a reply, and the operator's prompt is the last point before
|
||||
it is written, so the per-turn retrieval carries the core shape ahead of
|
||||
everything else: in full the first time a session sees it, as its one-line
|
||||
reminder after that, and in full again once a compaction has swept the
|
||||
ledger. What is pinned here is that round trip across the shell/Python seam —
|
||||
the route's order and keys, and the hook's ledger.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from quart import Quart, g
|
||||
|
||||
from scribe.services import reply_shapes
|
||||
from tests.helpers import http_sink, need_tools
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
HOOK = ROOT / "plugin" / "hooks" / "scribe_autoinject.sh"
|
||||
SESSION_START = ROOT / "plugin" / "hooks" / "scribe_session_context.sh"
|
||||
|
||||
|
||||
# ── the route ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def _retrieve(query_string, turn_context="CORE SHAPE"):
|
||||
from scribe.routes import plugin as routes
|
||||
|
||||
turn = AsyncMock(return_value={"context": turn_context, "rule_ids": [173],
|
||||
"shape_forms": {"core": reply_shapes.FULL}})
|
||||
app = Quart(__name__)
|
||||
async with app.test_request_context("/api/plugin/retrieve", query_string=query_string):
|
||||
g.user = SimpleNamespace(id=7)
|
||||
with patch.object(routes.plugin_ctx_svc, "build_prompt_rule_hint",
|
||||
AsyncMock(return_value={"context": "RULE LINE", "rule_ids": [11],
|
||||
"shown_rule_ids": [11]})), \
|
||||
patch.object(routes.plugin_ctx_svc, "build_autoinject_hint",
|
||||
AsyncMock(return_value={"context": "NOTES MENU", "note_ids": []})), \
|
||||
patch.object(routes.lesson_rules_svc, "co_surfaced", AsyncMock(return_value="")), \
|
||||
patch.object(routes.moment_delivery_svc, "deliver_for_turn", turn):
|
||||
resp = await inspect.unwrap(routes.autoinject_retrieve)()
|
||||
return await resp.get_json(), turn
|
||||
|
||||
|
||||
async def test_the_turns_shape_leads_the_payload_and_its_key_comes_back():
|
||||
body, _turn = await _retrieve({"q": "fix the flaky thing"})
|
||||
ctx = body["context"]
|
||||
assert ctx.index("CORE SHAPE") < ctx.index("RULE LINE") < ctx.index("NOTES MENU")
|
||||
assert body["shape_keys"] == ["core"]
|
||||
# Both arms' fresh rules reach the shared ledger.
|
||||
assert body["rule_ids"] == [11, 173]
|
||||
|
||||
|
||||
async def test_the_route_hands_the_ledger_and_the_rules_just_named_to_the_turn():
|
||||
_body, turn = await _retrieve({"q": "x", "shapes_seen": "core,bogus",
|
||||
"exclude_rule_ids": "9", "held_rule_ids": "4"})
|
||||
kw = turn.await_args.kwargs
|
||||
assert kw["seen"] == frozenset({"core"})
|
||||
# A rule the prompt arm just named is not quoted again by the turn.
|
||||
assert kw["exclude"] == frozenset({9, 11})
|
||||
assert kw["held"] == frozenset({4})
|
||||
|
||||
|
||||
# ── the hook ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _env(tmp_path, port):
|
||||
return {"PATH": os.environ["PATH"], "SCRIBE_URL": f"http://127.0.0.1:{port}",
|
||||
"SCRIBE_TOKEN": "t", "TMPDIR": str(tmp_path), "HOME": str(tmp_path)}
|
||||
|
||||
|
||||
def _prompt(tmp_path, port, session="s-shape"):
|
||||
need_tools("bash", "curl", "awk")
|
||||
out = subprocess.run(
|
||||
["bash", str(HOOK)],
|
||||
input=json.dumps({"session_id": session, "cwd": str(tmp_path),
|
||||
"prompt": "please fix the importer"}),
|
||||
capture_output=True, text=True, env=_env(tmp_path, port), timeout=30,
|
||||
)
|
||||
assert out.returncode == 0, out.stderr
|
||||
return out.stdout
|
||||
|
||||
|
||||
def _compact(tmp_path, session="s-shape"):
|
||||
out = subprocess.run(
|
||||
["bash", str(SESSION_START)],
|
||||
input=json.dumps({"session_id": session, "source": "compact"}),
|
||||
capture_output=True, text=True,
|
||||
env={"PATH": os.environ["PATH"], "HOME": str(tmp_path), "TMPDIR": str(tmp_path)},
|
||||
timeout=60,
|
||||
)
|
||||
assert out.returncode == 0, out.stderr
|
||||
|
||||
|
||||
REPLY = json.dumps({"context": "Reply shape · Every reply", "note_ids": [], "rule_ids": [],
|
||||
"shape_keys": ["core"]}).encode()
|
||||
|
||||
|
||||
def test_the_core_goes_whole_once_then_as_a_reminder_then_whole_after_a_compaction(tmp_path):
|
||||
with http_sink(by_path={"/api/plugin/retrieve": REPLY}) as (port, seen):
|
||||
out = _prompt(tmp_path, port)
|
||||
_prompt(tmp_path, port)
|
||||
_compact(tmp_path)
|
||||
_prompt(tmp_path, port)
|
||||
asks = [e for e in seen if e["_path"] == "/api/plugin/retrieve"]
|
||||
assert len(asks) == 3
|
||||
assert "shapes_seen" not in asks[0]
|
||||
assert asks[1]["shapes_seen"] == ["core"]
|
||||
# The compaction swept the ledger: the session no longer holds the shape.
|
||||
assert "shapes_seen" not in asks[2]
|
||||
assert "Every reply" in json.loads(out)["hookSpecificOutput"]["additionalContext"]
|
||||
|
||||
|
||||
def test_the_hook_and_the_route_agree_on_the_ledgers_names():
|
||||
"""Rule 33 across the seam: a renamed field fails silently."""
|
||||
from scribe.routes import plugin as routes
|
||||
|
||||
hook = HOOK.read_text()
|
||||
assert "shapes_seen" in hook and "'.shape_keys'" in hook
|
||||
assert "scribe_shapes_file" in hook and "scribe_shapes_append" in hook
|
||||
route = inspect.getsource(routes.autoinject_retrieve)
|
||||
assert 'request.args.get("shapes_seen")' in route and '"shape_keys"' in route
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ DEFS = HOOKS / "scribe_defs.sh"
|
||||
# the convention tests below are what keep this roster honest as it grows.
|
||||
LEDGERS = {
|
||||
"scribe-priorart": (".ids", ".rules.ids", ".opened.ids", ".sync.ids",
|
||||
".derive.ids"),
|
||||
".derive.ids", ".shapes.ids"),
|
||||
"scribe-autoinject": (".ids",),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user