docs(6209): preference door, systems and notes tools, lesson surfacing, rule versions, backup round trip, JSON reader, charter search, provenance, location filter
Ten more test files: issue, milestone, rule, decision and snippet numbers come out of docstrings, comments and messages; the preference door states what a preference is instead of quoting the framing; the JSON reader says why jq is not assumed instead of what every hook used to do; the backup round trip's column warning no longer claims the import side is unguarded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,10 @@
|
|||||||
"""JSON, read and written by the hooks without jq (#4107).
|
"""JSON, read and written by the hooks without jq.
|
||||||
|
|
||||||
WHAT THIS REPLACED, AND WHY IT NEEDS TESTING AT ALL. Every hook used to open
|
WHY NOT jq, AND WHY THIS NEEDS TESTING AT ALL. jq is absent by default on
|
||||||
`command -v jq >/dev/null 2>&1 || exit 0`, so on a machine without jq the
|
macOS, on the Debian/Ubuntu slim images, on Alpine and in most CI containers,
|
||||||
operator got no session context, no rules, no prior art and no process sync,
|
and a hook that exits quietly without it gives the operator no session context,
|
||||||
and not one word saying why — `exit 0` is indistinguishable from "ran fine,
|
no rules, no prior art and no process sync, with not one word saying why. The
|
||||||
nothing to say". jq is absent by default on macOS, on the Debian/Ubuntu slim
|
parser is `plugin/hooks/scribe_json.awk`, written in POSIX awk, and a parser is exactly
|
||||||
images, on Alpine and in most CI containers. The parser that replaced it is
|
|
||||||
`plugin/hooks/scribe_json.awk`, written in POSIX awk, and a parser is exactly
|
|
||||||
the kind of thing that works on the six documents you tried it on.
|
the kind of thing that works on the six documents you tried it on.
|
||||||
|
|
||||||
So the centre of this file is a DIFFERENTIAL against Python's `json`: the same
|
So the centre of this file is a DIFFERENTIAL against Python's `json`: the same
|
||||||
@@ -16,7 +14,7 @@ is one the hooks can be handed an arbitrary event with.
|
|||||||
|
|
||||||
The rest pins the three jobs around it — writing the hook envelope,
|
The rest pins the three jobs around it — writing the hook envelope,
|
||||||
percent-encoding a URL, and bounding a turn in a transcript — and that each
|
percent-encoding a URL, and bounding a turn in a transcript — and that each
|
||||||
guard here can fail (rule 167).
|
guard here can fail.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -74,7 +72,7 @@ def reference_flatten(value, path="", out=None):
|
|||||||
shell reads it back with. Every array also reports its LENGTH at `[#]`,
|
shell reads it back with. Every array also reports its LENGTH at `[#]`,
|
||||||
including an empty one, and an empty object reports `{#}` 0 — that is what
|
including an empty one, and an empty object reports `{#}` 0 — that is what
|
||||||
lets a caller tell "the server answered with zero notes" from "the server
|
lets a caller tell "the server answered with zero notes" from "the server
|
||||||
did not answer", which #2932 built an outage marker around.
|
did not answer", which the outage marker is built around.
|
||||||
"""
|
"""
|
||||||
out = [] if out is None else out
|
out = [] if out is None else out
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
@@ -221,7 +219,7 @@ def test_an_absent_field_and_a_null_field_both_read_as_empty():
|
|||||||
def test_an_absent_array_length_is_empty_not_zero():
|
def test_an_absent_array_length_is_empty_not_zero():
|
||||||
"""NOT the same answer as 0, and the distinction is load-bearing: the
|
"""NOT the same answer as 0, and the distinction is load-bearing: the
|
||||||
record nudge fires when the server said "zero notes" and must not fire
|
record nudge fires when the server said "zero notes" and must not fire
|
||||||
when the server said nothing at all (#2932)."""
|
when the server said nothing at all."""
|
||||||
out = sh(r"""flat=$(scribe_json_flat)
|
out = sh(r"""flat=$(scribe_json_flat)
|
||||||
printf '[%s][%s]\n' "$(scribe_json_len "$flat" '.note_ids')" \
|
printf '[%s][%s]\n' "$(scribe_json_len "$flat" '.note_ids')" \
|
||||||
"$(scribe_json_len "$flat" '.missing')" """,
|
"$(scribe_json_len "$flat" '.missing')" """,
|
||||||
@@ -272,7 +270,7 @@ def test_urlenc_percent_encodes_exactly_the_unreserved_set(value):
|
|||||||
|
|
||||||
|
|
||||||
def test_urlenc_and_the_envelope_can_fail():
|
def test_urlenc_and_the_envelope_can_fail():
|
||||||
"""Rule 167 — a guard that cannot fail is decoration. If the escaper were a
|
"""A guard that cannot fail is decoration. If the escaper were a
|
||||||
no-op, this pair would be the assertion that noticed."""
|
no-op, this pair would be the assertion that noticed."""
|
||||||
assert sh("scribe_urlenc", stdin="a b") != "a b"
|
assert sh("scribe_urlenc", stdin="a b") != "a b"
|
||||||
raw = sh('scribe_json_out PreToolUse "$(cat)"', stdin='he said "hi"')
|
raw = sh('scribe_json_out PreToolUse "$(cat)"', stdin='he said "hi"')
|
||||||
@@ -376,7 +374,7 @@ def test_a_tool_result_does_not_read_as_a_prompt():
|
|||||||
|
|
||||||
|
|
||||||
def test_the_turn_guards_can_fail():
|
def test_the_turn_guards_can_fail():
|
||||||
"""Rule 167. If the analyzer counted everything, or nothing, these are the
|
"""If the analyzer counted everything, or nothing, these are the
|
||||||
assertions that would notice."""
|
assertions that would notice."""
|
||||||
assert _turn([_prompt(), _close("t1", 41), _text("r")])["closed"] == "1"
|
assert _turn([_prompt(), _close("t1", 41), _text("r")])["closed"] == "1"
|
||||||
assert _turn([_prompt(), _text("r")])["closed"] == "0"
|
assert _turn([_prompt(), _text("r")])["closed"] == "0"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Real-Postgres round trip for rule_usage_events (milestone 333 step 1).
|
"""Real-Postgres round trip for rule_usage_events.
|
||||||
|
|
||||||
**This file is the reason the table exists.** `rule_usage_events` could have
|
**This file is the reason the table exists.** `rule_usage_events` could have
|
||||||
been a `rule_id` column on `note_usage_events` — the row carries no
|
been a `rule_id` column on `note_usage_events` — the row carries no
|
||||||
note-specific field and the readout is the same shape, which is the strongest
|
note-specific field and the readout is the same shape, which is the strongest
|
||||||
case for sharing that note #3163 admits. What decided against it is identity at
|
case for sharing. What decides against it is identity at
|
||||||
restore, and that is a claim only a real round trip can support.
|
restore, and that is a claim only a real round trip can support.
|
||||||
|
|
||||||
The failure it guards is the quiet kind. `note_usage_events`'s importer maps
|
The failure it guards is the quiet kind. `note_usage_events`'s importer maps
|
||||||
@@ -17,7 +17,7 @@ So the assertions below are about WHICH MAP resolved the id, and they are
|
|||||||
written to fail if the answer ever becomes "the note one" or "neither".
|
written to fail if the answer ever becomes "the note one" or "neither".
|
||||||
|
|
||||||
Same shape as `test_integration_backup_rule_version_roundtrip.py`, which guards
|
Same shape as `test_integration_backup_rule_version_roundtrip.py`, which guards
|
||||||
`rule_versions` against #3182's `arose_from_id` trap on the same seam.
|
`rule_versions` against the same id-remapping trap on the same seam.
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
@@ -143,7 +143,7 @@ async def source():
|
|||||||
user_id=None, rule_id=rule.id,
|
user_id=None, rule_id=rule.id,
|
||||||
event=SURFACED, source="write_path_rule",
|
event=SURFACED, source="write_path_rule",
|
||||||
),
|
),
|
||||||
# A departure and its reason (#4212). Seeded here because
|
# A departure and its reason. Seeded here because
|
||||||
# `detail` is the ONE field on this table that cannot be
|
# `detail` is the ONE field on this table that cannot be
|
||||||
# recomputed: a fresh install re-earns its counts by being used,
|
# recomputed: a fresh install re-earns its counts by being used,
|
||||||
# but a stated reason exists once and is gone if a restore drops
|
# but a stated reason exists once and is gone if a restore drops
|
||||||
@@ -290,7 +290,7 @@ async def test_the_actor_is_remapped_and_a_missing_one_survives(restored):
|
|||||||
attributed = [e for e in restored["events"] if e.user_id is not None]
|
attributed = [e for e in restored["events"] if e.user_id is not None]
|
||||||
orphaned = [e for e in restored["events"] if e.user_id is None]
|
orphaned = [e for e in restored["events"] if e.user_id is None]
|
||||||
# Four attributed: the surfacing, the pull, and the two outcome rows
|
# Four attributed: the surfacing, the pull, and the two outcome rows
|
||||||
# added with `detail` (#4212). One orphaned, deliberately.
|
# that carry `detail`. One orphaned, deliberately.
|
||||||
assert len(attributed) == 4
|
assert len(attributed) == 4
|
||||||
assert len(orphaned) == 1, (
|
assert len(orphaned) == 1, (
|
||||||
"the event with no actor did not come back. Telemetry outlives the "
|
"the event with no actor did not come back. Telemetry outlives the "
|
||||||
@@ -325,10 +325,9 @@ async def test_a_departures_reason_survives_the_round_trip(restored):
|
|||||||
419 was opened to create, undone silently at the one moment nobody is
|
419 was opened to create, undone silently at the one moment nobody is
|
||||||
watching.
|
watching.
|
||||||
|
|
||||||
#4197 is the standing warning behind this test: the backup column guard
|
A column added to the model and to the exporter and NOT to the importer
|
||||||
watches the export side only, so a column added to the model and to the
|
round-trips as null with nothing to say so; this round trip is where that
|
||||||
exporter and NOT to the importer round-trips as null with nothing to say
|
shows up in the data.
|
||||||
so.
|
|
||||||
"""
|
"""
|
||||||
departures = [e for e in restored["events"] if e.event == DEPARTED]
|
departures = [e for e in restored["events"] if e.event == DEPARTED]
|
||||||
assert len(departures) == 1
|
assert len(departures) == 1
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"""Real-Postgres tests for the rule-history write path (milestone 323 step 2).
|
"""Real-Postgres tests for the rule-history write path.
|
||||||
|
|
||||||
Every assertion here is about ORDERING or ABSENCE, which is why none of them
|
Every assertion here is about ORDERING or ABSENCE, which is why none of them
|
||||||
can be made with mocks.
|
can be made with mocks.
|
||||||
|
|
||||||
What a version is for: it holds what the rule USED TO SAY. The edit destroys
|
What a version is for: it holds what the rule USED TO SAY. The edit destroys
|
||||||
that, and nothing can recompute it — the rescoping of rule 79 had to be
|
that, and nothing can recompute it — without a version, a rescoped rule's old
|
||||||
hand-copied into a task log to survive (#3237), which is not a process, it is
|
wording survives only if a person copies it somewhere, which is not a
|
||||||
a person remembering.
|
process.
|
||||||
|
|
||||||
Two of these pin a DELIBERATE ABSENCE. `note_versions` carries a 300-second
|
Two of these pin a DELIBERATE ABSENCE. `note_versions` carries a 300-second
|
||||||
interval gate and a 50-version cap, both defences against note autosave. Rules
|
interval gate and a 50-version cap, both defences against note autosave. Rules
|
||||||
@@ -199,9 +199,9 @@ async def test_history_reads_newest_first(constraint):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# ── The read path is ACL-scoped (milestone 323 step 3) ─────────────────
|
# ── The read path is ACL-scoped ───────────────────────────────────────
|
||||||
#
|
#
|
||||||
# Rule 47: every read of user data is scoped by owner. A version carries a
|
# Every read of user data is scoped by access. A version carries a
|
||||||
# `user_id`, which makes it tempting to scope the history by it — that would
|
# `user_id`, which makes it tempting to scope the history by it — that would
|
||||||
# be wrong in both directions, and these pin which way round it goes.
|
# be wrong in both directions, and these pin which way round it goes.
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"""A lesson reaches the session it applies to — milestone 385, step 5.
|
"""A lesson reaches the session it applies to.
|
||||||
|
|
||||||
Three decisions are guarded here, and each has a way of going quietly wrong
|
Three decisions are guarded here, and each has a way of going quietly wrong
|
||||||
that these tests are shaped to catch rather than to describe:
|
that these tests are shaped to catch rather than to describe:
|
||||||
|
|
||||||
WHICH ARM. The two note arms, and no new one. An arm that FILTERS kinds does
|
WHICH ARM. The two note arms, and no new one. An arm that FILTERS kinds does
|
||||||
not merely outrank a kind it omits — it makes it unreachable, and nothing
|
not merely outrank a kind it omits — it makes it unreachable, and nothing
|
||||||
reports an arm that never had the candidate (#3702). The write path filters;
|
reports an arm that never had the candidate. The write path filters;
|
||||||
the prompt menu does not, but is project-scoped, which for a kind whose whole
|
the prompt menu does not, but is project-scoped, which for a kind whose whole
|
||||||
claim is that it transfers amounts to the same silence.
|
claim is that it transfers amounts to the same silence.
|
||||||
|
|
||||||
@@ -115,8 +115,8 @@ async def test_the_write_path_asks_for_lessons_at_all():
|
|||||||
async def test_a_lesson_takes_a_reserved_slot_and_the_query_is_logged():
|
async def test_a_lesson_takes_a_reserved_slot_and_the_query_is_logged():
|
||||||
"""The slot is a claim that a kind deserves a guaranteed line. A claim like
|
"""The slot is a claim that a kind deserves a guaranteed line. A claim like
|
||||||
that has to be falsifiable from the first deploy, which means its own query
|
that has to be falsifiable from the first deploy, which means its own query
|
||||||
in `retrieval_logs` — #2463's finding, recorded about the slot that shipped
|
in `retrieval_logs`; a slot without one cannot be judged against what it
|
||||||
without one."""
|
displaced."""
|
||||||
rec = MagicMock()
|
rec = MagicMock()
|
||||||
out, calls = await _menu(
|
out, calls = await _menu(
|
||||||
[(0.70, fake_note(id=1, title="Badge layout task", user_id=1, is_task=True))],
|
[(0.70, fake_note(id=1, title="Badge layout task", user_id=1, is_task=True))],
|
||||||
@@ -143,7 +143,7 @@ async def test_the_reserved_lesson_extends_and_never_evicts():
|
|||||||
"""It sides with `preference_slot` over `reuse_slot`, and the reason is the
|
"""It sides with `preference_slot` over `reuse_slot`, and the reason is the
|
||||||
ledger rather than taste: an evicted hit was RETURNED by the general search
|
ledger rather than taste: an evicted hit was RETURNED by the general search
|
||||||
and sits in that call's log row, so displacing it makes the two tables
|
and sits in that call's log row, so displacing it makes the two tables
|
||||||
disagree about one call for a reason nothing in the data explains (#3668).
|
disagree about one call for a reason nothing in the data explains.
|
||||||
The voice argument points the same way — a record that binds nothing should
|
The voice argument points the same way — a record that binds nothing should
|
||||||
not be able to throw a better-scoring one off the menu."""
|
not be able to throw a better-scoring one off the menu."""
|
||||||
main = [(0.72, fake_note(id=1, title="a", user_id=1)),
|
main = [(0.72, fake_note(id=1, title="a", user_id=1)),
|
||||||
@@ -190,7 +190,7 @@ async def test_a_weak_lesson_does_not_buy_the_slot():
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_the_slot_surfacing_matches_its_own_retrieval_row():
|
async def test_the_slot_surfacing_matches_its_own_retrieval_row():
|
||||||
"""#3668's identity, on the new arm: what a source says it retrieved and
|
"""The two-table identity, on this arm: what a source says it retrieved and
|
||||||
what it says it showed must be the same records. The reserved lesson is
|
what it says it showed must be the same records. The reserved lesson is
|
||||||
fetched by `lesson_slot`'s query, so it is `lesson_slot`'s surfacing —
|
fetched by `lesson_slot`'s query, so it is `lesson_slot`'s surfacing —
|
||||||
counting it under `auto_inject` as well would book one delivery twice and
|
counting it under `auto_inject` as well would book one delivery twice and
|
||||||
@@ -275,7 +275,7 @@ async def test_the_write_path_names_the_register_for_a_lesson_too():
|
|||||||
|
|
||||||
|
|
||||||
def test_the_surfacing_guards_can_fail():
|
def test_the_surfacing_guards_can_fail():
|
||||||
"""Rule 167: each assertion above has to be able to bite. The three shapes
|
"""Each assertion above has to be able to bite. The three shapes
|
||||||
it would take to break this feature silently, each checked here against the
|
it would take to break this feature silently, each checked here against the
|
||||||
condition the tests actually assert on."""
|
condition the tests actually assert on."""
|
||||||
# An arm that stops asking for the kind.
|
# An arm that stops asking for the kind.
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ pytestmark = pytest.mark.usefixtures("_bind_user")
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _no_supersession():
|
def _no_supersession():
|
||||||
"""Every note read/write now asks for its supersession relations (#278).
|
"""Every note read/write asks for its supersession relations.
|
||||||
|
|
||||||
These are unit tests of the TOOL layer and this job has no database — the
|
These are unit tests of the TOOL layer and this job has no database — the
|
||||||
same hazard the `_fake_note` comment below records for note 2109. Stubbed
|
same hazard the `_fake_note` comment below records. Stubbed
|
||||||
to "no relations", which is the state of essentially every note; the
|
to "no relations", which is the state of essentially every note; the
|
||||||
relation's own behaviour is covered in test_services_supersession.py, and
|
relation's own behaviour is covered in test_services_supersession.py, and
|
||||||
the attachment is covered explicitly below.
|
the attachment is covered explicitly below.
|
||||||
@@ -120,7 +120,7 @@ async def test_get_note_returns_dict():
|
|||||||
# Own record: no provenance noise.
|
# Own record: no provenance noise.
|
||||||
assert "shared" not in out
|
assert "shared" not in out
|
||||||
# No supersession relations: both keys ABSENT, not present-and-empty. A
|
# No supersession relations: both keys ABSENT, not present-and-empty. A
|
||||||
# field that always says nothing trains readers to skip fields (#2483).
|
# field that always says nothing trains readers to skip fields.
|
||||||
assert "supersedes" not in out
|
assert "supersedes" not in out
|
||||||
assert "superseded_by" not in out
|
assert "superseded_by" not in out
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ async def test_update_note_only_sends_non_default_fields():
|
|||||||
|
|
||||||
`clear` is always forwarded and is not a field: it is how this door says
|
`clear` is always forwarded and is not a field: it is how this door says
|
||||||
"unset these", and an empty one says "unset nothing". Same shape the rules
|
"unset these", and an empty one says "unset nothing". Same shape the rules
|
||||||
door took in #3096, and the same test that had to learn about it there."""
|
door takes, and the same test."""
|
||||||
fake = fake_note()
|
fake = fake_note()
|
||||||
mock = AsyncMock(return_value=fake)
|
mock = AsyncMock(return_value=fake)
|
||||||
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
||||||
@@ -232,7 +232,7 @@ async def test_update_note_only_sends_non_default_fields():
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_note_forwards_a_check_but_not_an_empty_one():
|
async def test_update_note_forwards_a_check_but_not_an_empty_one():
|
||||||
""""" means "leave this alone" at this door — an agent updating a body must
|
""""" means "leave this alone" at this door — an agent updating a body must
|
||||||
not wipe a check it was never asked about (milestone 317)."""
|
not wipe a check it was never asked about."""
|
||||||
mock = AsyncMock(return_value=fake_note())
|
mock = AsyncMock(return_value=fake_note())
|
||||||
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
||||||
await update_note(note_id=1, verify_with="curl the docs")
|
await update_note(note_id=1, verify_with="curl the docs")
|
||||||
@@ -305,7 +305,7 @@ async def test_delete_note_soft_deletes_and_returns_batch():
|
|||||||
):
|
):
|
||||||
result = await delete_note(note_id=7)
|
result = await delete_note(note_id=7)
|
||||||
assert result["deleted_batch_id"] == "batch-1"
|
assert result["deleted_batch_id"] == "batch-1"
|
||||||
# The confirmation NAMES what went (#3273). After the delete the row is
|
# The confirmation NAMES what went. After the delete the row is
|
||||||
# trashed, so this line is the last chance to say which note it was.
|
# trashed, so this line is the last chance to say which note it was.
|
||||||
assert result["title"] == "the note that went"
|
assert result["title"] == "the note that went"
|
||||||
assert "the note that went" in result["message"]
|
assert "the note that went" in result["message"]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import pytest
|
|||||||
from tests.helpers import fake_note, fake_system, fake_task
|
from tests.helpers import fake_note, fake_system, fake_task
|
||||||
|
|
||||||
|
|
||||||
# The name assessment both doors run before minting (milestone 307). A test
|
# The name assessment both doors run before minting. A test
|
||||||
# that patches systems_svc wholesale must stub it, or the awaited MagicMock
|
# that patches systems_svc wholesale must stub it, or the awaited MagicMock
|
||||||
# raises — this shape is the "nothing matched" answer.
|
# raises — this shape is the "nothing matched" answer.
|
||||||
_NO_MATCH = {"duplicate": None, "canonical": None}
|
_NO_MATCH = {"duplicate": None, "canonical": None}
|
||||||
@@ -179,8 +179,8 @@ async def test_untagged_hint_zero_systems_prompts_first_create_and_fails_open():
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_untagged_hint_escalates_in_a_mature_zero_systems_project():
|
async def test_untagged_hint_escalates_in_a_mature_zero_systems_project():
|
||||||
"""#2683: at 282 records and zero Systems (Minstrel), the generic question
|
"""In a mature project with zero Systems the generic question does not
|
||||||
had demonstrably never converted. The escalated ask must carry the
|
convert. The escalated ask must carry the
|
||||||
project's OWN evidence — record count and recent titles — and demand a
|
project's OWN evidence — record count and recent titles — and demand a
|
||||||
concrete deliverable, because that is the property separating the nudges
|
concrete deliverable, because that is the property separating the nudges
|
||||||
that convert from the prose that doesn't."""
|
that convert from the prose that doesn't."""
|
||||||
@@ -190,7 +190,7 @@ async def test_untagged_hint_escalates_in_a_mature_zero_systems_project():
|
|||||||
patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
||||||
svc.list_systems = AsyncMock(return_value=[])
|
svc.list_systems = AsyncMock(return_value=[])
|
||||||
notes.list_notes = AsyncMock(return_value=(recent, 282))
|
notes.list_notes = AsyncMock(return_value=(recent, 282))
|
||||||
# The standard names come from the GLOBAL catalog now (milestone 307),
|
# The standard names come from the GLOBAL catalog,
|
||||||
# not a module constant — so the ask reads them through the service.
|
# not a module constant — so the ask reads them through the service.
|
||||||
# That the SEEDED vocabulary is these eight is migration 0087's
|
# That the SEEDED vocabulary is these eight is migration 0087's
|
||||||
# business, asserted against a real database in the inception
|
# business, asserted against a real database in the inception
|
||||||
@@ -203,7 +203,7 @@ async def test_untagged_hint_escalates_in_a_mature_zero_systems_project():
|
|||||||
assert "282 records" in hint
|
assert "282 records" in hint
|
||||||
assert "Fix scrape retry backoff" in hint # the project's own evidence
|
assert "Fix scrape retry backoff" in hint # the project's own evidence
|
||||||
assert "3-6" in hint and "create_system" in hint
|
assert "3-6" in hint and "create_system" in hint
|
||||||
# NOT an approval flow (#2798): the agent mints directly, and the
|
# NOT an approval flow: the agent mints directly, and the
|
||||||
# standard cross-project names carry the consistency instead.
|
# standard cross-project names carry the consistency instead.
|
||||||
assert "without asking permission" in hint
|
assert "without asking permission" in hint
|
||||||
assert "propose" not in hint and "confirmed" not in hint
|
assert "propose" not in hint and "confirmed" not in hint
|
||||||
@@ -262,7 +262,7 @@ async def test_populated_vocabulary_never_counts_records():
|
|||||||
|
|
||||||
# The name gate's own cases (case/whitespace normalisation, exact-over-overlap,
|
# The name gate's own cases (case/whitespace normalisation, exact-over-overlap,
|
||||||
# fail-open) moved to tests/test_services_systems.py with the logic itself —
|
# fail-open) moved to tests/test_services_systems.py with the logic itself —
|
||||||
# services/systems.assess_system_name, so both doors share one answer (#2482).
|
# services/systems.assess_system_name, so both doors share one answer.
|
||||||
# What stays the TOOL's job — rendering a duplicate, applying an exact area,
|
# What stays the TOOL's job — rendering a duplicate, applying an exact area,
|
||||||
# offering an overlap — is covered at the top of this file.
|
# offering an overlap — is covered at the top of this file.
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ async def test_add_task_log_on_untagged_project_task_asks_the_question():
|
|||||||
notes_svc.get_note_for_user = AsyncMock(return_value=(task, "owner"))
|
notes_svc.get_note_for_user = AsyncMock(return_value=(task, "owner"))
|
||||||
seam_svc.list_record_systems = AsyncMock(return_value=[])
|
seam_svc.list_record_systems = AsyncMock(return_value=[])
|
||||||
seam_svc.list_systems = AsyncMock(return_value=[])
|
seam_svc.list_systems = AsyncMock(return_value=[])
|
||||||
# Young project: below the #2683 bootstrap threshold, so the mild
|
# Young project: below the bootstrap threshold, so the mild
|
||||||
# question is the expected form here.
|
# question is the expected form here.
|
||||||
seam_notes.list_notes = AsyncMock(return_value=([], 2))
|
seam_notes.list_notes = AsyncMock(return_value=([], 2))
|
||||||
from scribe.mcp.tools.tasks import add_task_log
|
from scribe.mcp.tools.tasks import add_task_log
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
"""The preference write path, and how it differs from a rule's (milestone 399).
|
"""The preference write path, and how it differs from a rule's.
|
||||||
|
|
||||||
WHY TWO DOORS AT ALL
|
WHY TWO DOORS AT ALL
|
||||||
|
|
||||||
`create_rule`'s docstring IS the approval gate (#3557): it tells its caller to
|
`create_rule`'s docstring IS the approval gate: it tells its caller to
|
||||||
propose, offer three answers, and wait. That is right for a rule — the person
|
propose, offer three answers, and wait. That is right for a rule — the person
|
||||||
a rule binds should have agreed to be bound.
|
a rule binds should have agreed to be bound.
|
||||||
|
|
||||||
A preference inverts it. The operator's framing: *"preferences are rules that
|
A preference inverts it: it is a rule Scribe can and should update during
|
||||||
scribe can and should update during use."* A preference that asks every time
|
use. A preference that asks every time never drifts, and drifting is the whole feature. Reaching one through
|
||||||
never drifts, and drifting is the whole feature. Reaching one through
|
|
||||||
`create_rule(kind=..., when_to_apply="when the moment this fixture stands in for arises")` would mean reading it through the gate's prose, and
|
`create_rule(kind=..., when_to_apply="when the moment this fixture stands in for arises")` would mean reading it through the gate's prose, and
|
||||||
the caller would hesitate over exactly the act this kind exists to make
|
the caller would hesitate over exactly the act this kind exists to make
|
||||||
routine.
|
routine.
|
||||||
@@ -18,7 +17,7 @@ So the asymmetry is the product, and these tests pin it.
|
|||||||
TWO PRESENCE CHECKS, NEVER AN ABSENCE
|
TWO PRESENCE CHECKS, NEVER AN ABSENCE
|
||||||
|
|
||||||
The tempting guard is "create_preference's docstring does NOT run the approval
|
The tempting guard is "create_preference's docstring does NOT run the approval
|
||||||
loop". That is the shape snippet #3352 warns against: an absence check passes
|
loop". That is a trap: an absence check passes
|
||||||
against a docstring that has been deleted, emptied, or rewritten into
|
against a docstring that has been deleted, emptied, or rewritten into
|
||||||
something else entirely, and it reads as coverage while proving nothing.
|
something else entirely, and it reads as coverage while proving nothing.
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ from tests.helpers import fake_rule, plain_rule_detail as _plain_detail
|
|||||||
from tests.helpers import tool_doc as _doc
|
from tests.helpers import tool_doc as _doc
|
||||||
|
|
||||||
# The tool layer reads its caller from a ContextVar the HTTP transport sets.
|
# The tool layer reads its caller from a ContextVar the HTTP transport sets.
|
||||||
# With no request in flight, the module binds it itself (snippet #2836).
|
# With no request in flight, the module binds it itself.
|
||||||
pytestmark = pytest.mark.usefixtures("_bind_user")
|
pytestmark = pytest.mark.usefixtures("_bind_user")
|
||||||
|
|
||||||
MODULE = "scribe.mcp.tools.rulebooks"
|
MODULE = "scribe.mcp.tools.rulebooks"
|
||||||
@@ -176,7 +175,7 @@ def test_the_preference_door_says_to_write_it():
|
|||||||
"""Half two. The inverting instruction has to be PRESENT, not merely
|
"""Half two. The inverting instruction has to be PRESENT, not merely
|
||||||
unaccompanied by a gate.
|
unaccompanied by a gate.
|
||||||
|
|
||||||
An agent that has internalised #3557 will hesitate to write or rewrite a
|
An agent that has internalised the rule gate will hesitate to write or rewrite a
|
||||||
preference unless told plainly that this door is different. Silence here
|
preference unless told plainly that this door is different. Silence here
|
||||||
does not read as permission — it reads as an omission, and the caller
|
does not read as permission — it reads as an omission, and the caller
|
||||||
falls back on the behaviour it already knows.
|
falls back on the behaviour it already knows.
|
||||||
@@ -194,7 +193,7 @@ def test_the_preference_door_says_to_write_it():
|
|||||||
|
|
||||||
|
|
||||||
def test_the_preference_door_names_the_force_distinction():
|
def test_the_preference_door_names_the_force_distinction():
|
||||||
"""The routing test, stated positively (rule 165).
|
"""The routing test, stated positively.
|
||||||
|
|
||||||
The confusion this milestone exists to fix is that a session cannot tell
|
The confusion this milestone exists to fix is that a session cannot tell
|
||||||
which kind it is holding. If the docstring stops drawing the line, the
|
which kind it is holding. If the docstring stops drawing the line, the
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""Tests for the snippet location reverse lookup (#2083).
|
"""Tests for the snippet location reverse lookup.
|
||||||
|
|
||||||
Covers the pure half of the predicate — `location_parts`, `location_matches`,
|
Covers the pure half of the predicate — `location_parts`, `location_matches`,
|
||||||
`location_jsonpath` — plus the parameter plumbing across the three surfaces
|
`location_jsonpath` — plus the parameter plumbing across the three surfaces
|
||||||
(service / REST route / MCP tool), which rule #33 requires to agree on names and
|
(service / REST route / MCP tool), which have to agree on names and
|
||||||
defaults.
|
defaults.
|
||||||
|
|
||||||
The SQL half can only be proved against real Postgres — see
|
The SQL half can only be proved against real Postgres — see
|
||||||
@@ -56,7 +56,7 @@ def test_no_parts_matches_everything():
|
|||||||
def test_matches_exact_repo_path_and_symbol():
|
def test_matches_exact_repo_path_and_symbol():
|
||||||
data = _data({"repo": "Scribe", "path": "src/scribe/x.py", "symbol": "helper"})
|
data = _data({"repo": "Scribe", "path": "src/scribe/x.py", "symbol": "helper"})
|
||||||
assert location_matches(data, {"repo": "Scribe"})
|
assert location_matches(data, {"repo": "Scribe"})
|
||||||
assert location_matches(data, {"repo": "scribe"}) # repo names: case never distinguishes (#2874)
|
assert location_matches(data, {"repo": "scribe"}) # repo names: case never distinguishes
|
||||||
assert location_matches(data, {"path": "src/scribe/x.py"})
|
assert location_matches(data, {"path": "src/scribe/x.py"})
|
||||||
assert location_matches(data, {"symbol": "helper"})
|
assert location_matches(data, {"symbol": "helper"})
|
||||||
assert location_matches(data, {"repo": "Scribe", "symbol": "helper"})
|
assert location_matches(data, {"repo": "Scribe", "symbol": "helper"})
|
||||||
@@ -102,7 +102,7 @@ def test_blank_recorded_part_does_not_match_a_requested_one():
|
|||||||
def test_jsonpath_filters_within_one_locations_entry():
|
def test_jsonpath_filters_within_one_locations_entry():
|
||||||
expr = location_jsonpath({"repo": "Scribe", "symbol": "helper"})
|
expr = location_jsonpath({"repo": "Scribe", "symbol": "helper"})
|
||||||
assert expr.startswith("$.locations[*] ? (")
|
assert expr.startswith("$.locations[*] ? (")
|
||||||
# repo: anchored, case-insensitive (#2874) — mirrors location_matches.
|
# repo: anchored, case-insensitive — mirrors location_matches.
|
||||||
assert '@.repo like_regex "^Scribe$" flag "i"' in expr
|
assert '@.repo like_regex "^Scribe$" flag "i"' in expr
|
||||||
assert '@.symbol == "helper"' in expr
|
assert '@.symbol == "helper"' in expr
|
||||||
assert " && " in expr
|
assert " && " in expr
|
||||||
@@ -149,7 +149,7 @@ def test_location_clause_renders_a_jsonpath_containment():
|
|||||||
assert "data" in sql
|
assert "data" in sql
|
||||||
|
|
||||||
|
|
||||||
# --- surface plumbing (rule #33) ---------------------------------------------
|
# --- surface plumbing ---------------------------------------------------------
|
||||||
|
|
||||||
def test_agent_and_service_surfaces_take_the_same_location_params():
|
def test_agent_and_service_surfaces_take_the_same_location_params():
|
||||||
from scribe.mcp.tools.snippets import list_snippets as mcp_list
|
from scribe.mcp.tools.snippets import list_snippets as mcp_list
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""Body provenance — the cache-with-provenance half of the pointer model (#2688).
|
"""Body provenance — the cache-with-provenance half of the pointer model.
|
||||||
|
|
||||||
Decision #2686: the recorded location is the source of truth for a snippet's
|
The recorded location is the source of truth for a snippet's
|
||||||
code and the stored body is a CACHE of it. `data.provenance` records what that
|
code and the stored body is a CACHE of it. `data.provenance` records what that
|
||||||
cache is a cache OF — the commit the body was read at, and when — so staleness
|
cache is a cache OF — the commit the body was read at, and when — so staleness
|
||||||
becomes judgeable instead of guessed, and so the forge fetch (milestone 288
|
becomes judgeable instead of guessed, and so the forge fetch has something to
|
||||||
step 5) has something to refresh.
|
refresh.
|
||||||
|
|
||||||
The rules under test, because each has a way to rot silently:
|
The rules under test, because each has a way to rot silently:
|
||||||
|
|
||||||
@@ -18,8 +18,8 @@ The rules under test, because each has a way to rot silently:
|
|||||||
cached body matches the source there.
|
cached body matches the source there.
|
||||||
|
|
||||||
Unit tests cover the compose/carry logic; the integration section runs the
|
Unit tests cover the compose/carry logic; the integration section runs the
|
||||||
same rules through the real service paths on real Postgres (the #2663 lesson —
|
same rules through the real service paths on real Postgres (a DB-touching path
|
||||||
a DB-touching path with only mocked coverage is a path with no coverage).
|
with only mocked coverage is a path with no coverage).
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
@@ -49,7 +49,7 @@ def test_compose_provenance_stamps_sha_and_time():
|
|||||||
|
|
||||||
|
|
||||||
def test_compose_provenance_without_a_sha_is_none_not_an_empty_record():
|
def test_compose_provenance_without_a_sha_is_none_not_an_empty_record():
|
||||||
# Absent provenance must stay ABSENT (the pre-#2688 semantics), never an
|
# Absent provenance must stay ABSENT, never an
|
||||||
# empty dict that readers would have to distinguish from a real one.
|
# empty dict that readers would have to distinguish from a real one.
|
||||||
assert compose_provenance(commit_sha="") is None
|
assert compose_provenance(commit_sha="") is None
|
||||||
assert compose_provenance(commit_sha=" ") is None
|
assert compose_provenance(commit_sha=" ") is None
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Systems become findable by meaning — the charter as an ANSWER (#4251).
|
"""Systems become findable by meaning — the charter as an ANSWER.
|
||||||
|
|
||||||
A System's `description` is a charter: several hundred words saying what
|
A System's `description` is a charter: several hundred words saying what
|
||||||
belongs in that area and what does not. It is the answer to "where does this
|
belongs in that area and what does not. It is the answer to "where does this
|
||||||
@@ -20,7 +20,7 @@ from tests.helpers import make_mock_session
|
|||||||
|
|
||||||
def _system(system_id=3, name="Retrieval & recall", description="the charter"):
|
def _system(system_id=3, name="Retrieval & recall", description="the charter"):
|
||||||
s = MagicMock(id=system_id, project_id=2, description=description)
|
s = MagicMock(id=system_id, project_id=2, description=description)
|
||||||
s.name = name # never via the constructor — see note #2833
|
s.name = name # never via the constructor: `name` names the mock
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
@@ -65,8 +65,7 @@ def _rows_ctx(rows):
|
|||||||
async def test_the_search_collapses_to_best_chunk_and_reports_which_one_won():
|
async def test_the_search_collapses_to_best_chunk_and_reports_which_one_won():
|
||||||
"""A charter is long, and a result shows its NAME — so the paragraph that
|
"""A charter is long, and a result shows its NAME — so the paragraph that
|
||||||
actually decides where a record belongs has to come back with it, or the
|
actually decides where a record belongs has to come back with it, or the
|
||||||
caller judges from two words that cannot say (#4243). Published from the
|
caller judges from two words that cannot say."""
|
||||||
start here rather than retrofitted, which is what #4251 asked for."""
|
|
||||||
a, b = _system(3), _system(4, name="Data Model")
|
a, b = _system(3), _system(4, name="Data Model")
|
||||||
rows = [
|
rows = [
|
||||||
(a, 0.10, 2, "retrieval telemetry belongs here"),
|
(a, 0.10, 2, "retrieval telemetry belongs here"),
|
||||||
@@ -94,7 +93,7 @@ async def test_the_search_collapses_to_best_chunk_and_reports_which_one_won():
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_a_project_the_caller_cannot_read_returns_nothing():
|
async def test_a_project_the_caller_cannot_read_returns_nothing():
|
||||||
"""Rule 78 — the charter of a project someone was not given is not an
|
"""The charter of a project someone was not given is not an
|
||||||
answer to any question they are entitled to ask."""
|
answer to any question they are entitled to ask."""
|
||||||
_session, ctx = _rows_ctx([])
|
_session, ctx = _rows_ctx([])
|
||||||
with (
|
with (
|
||||||
|
|||||||
Reference in New Issue
Block a user