Compare commits
2
Commits
d0d278dcce
...
63e97a6f0f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63e97a6f0f | ||
|
|
ba0b56018e |
@@ -1,4 +1,4 @@
|
||||
"""The PostToolUse after-write hook (#2901): code written through Bash — sed,
|
||||
"""The PostToolUse after-write hook: code written through Bash — sed,
|
||||
heredocs, scripts — gets the same prior-art / ledger checks as a Write/Edit.
|
||||
|
||||
Runs the real shell against a temp git repo and a throwaway HTTP sink, like
|
||||
@@ -103,7 +103,7 @@ def test_after_write_is_silent_where_it_has_nothing_to_say(tmp_path):
|
||||
assert _run(loose, env, session="s-loose") == ""
|
||||
# A change that defines nothing (prose, a call-site edit) → nothing, even
|
||||
# with the server unreachable (port 9 refuses): no definitions, no call
|
||||
# owed, so not even the #2932 outage line. (a.css above is removed first:
|
||||
# owed, so not even the outage line. (a.css above is removed first:
|
||||
# it DOES define a shape, and an unanswered call for it would rightly speak.)
|
||||
(repo / "a.css").unlink()
|
||||
(repo / "README.md").write_text("# notes\n")
|
||||
@@ -112,9 +112,9 @@ def test_after_write_is_silent_where_it_has_nothing_to_say(tmp_path):
|
||||
|
||||
|
||||
def test_after_write_local_arm_works_without_a_server_and_says_the_server_did_not_answer(tmp_path):
|
||||
"""The local by-name arm needs no instance (#2280). A configured instance
|
||||
that does not ANSWER (a refused connection stands in for it) is said, once
|
||||
per outage (#2932) — and the record nudge, which claims "nothing recorded",
|
||||
"""The local by-name arm needs no instance. A configured instance that
|
||||
does not ANSWER (a refused connection stands in for it) is said, once per
|
||||
outage — and the record nudge, which claims "nothing recorded",
|
||||
is withheld: no answer backs that claim."""
|
||||
env = _env(tmp_path)
|
||||
repo = _repo(tmp_path, env)
|
||||
@@ -138,7 +138,7 @@ def test_after_write_local_arm_works_without_a_server_and_says_the_server_did_no
|
||||
|
||||
def test_after_write_unconfigured_install_owes_no_call_and_keeps_the_record_nudge(tmp_path):
|
||||
"""No URL/token → no call was owed, so nothing is "unreached"; the local
|
||||
arm and the record nudge (#2664) stand on their own, as before."""
|
||||
arm and the record nudge stand on their own."""
|
||||
env = {k: v for k, v in _env(tmp_path).items() if k not in ("SCRIBE_URL", "SCRIBE_TOKEN")}
|
||||
repo = _repo(tmp_path, env)
|
||||
(repo / "d.py").write_text("def slug(t):\n return t.lower()\n")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""A backfill repairs to current truth; it does not replay a snapshot (#4262).
|
||||
"""A backfill repairs to current truth; it does not replay a snapshot.
|
||||
|
||||
The bug these pin: every backfill used to SELECT the text alongside the id and
|
||||
then iterate that snapshot, sleeping between records. A run over a real corpus
|
||||
@@ -90,8 +90,8 @@ async def test_a_record_deleted_between_the_scan_and_the_loop_is_skipped():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_record_whose_text_outran_its_vectors_is_re_embedded():
|
||||
"""The repair half (#4202 — a guard that refuses to write a bad value does
|
||||
not undo the bad values already stored). Note 5 IS at the current version,
|
||||
"""The repair half: a guard that refuses to write a bad value does not
|
||||
undo the bad values already stored. Note 5 IS at the current version,
|
||||
so the version comparison alone would skip it forever; its text is newer
|
||||
than its vectors, which is the only signal that separates "embedded from
|
||||
stale text" from "embedded correctly"."""
|
||||
@@ -131,7 +131,7 @@ async def test_a_task_logged_since_its_vectors_is_re_embedded():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_corpus_that_is_genuinely_current_embeds_nothing():
|
||||
"""The guard must be able to stay quiet, or it is not a guard (#167) — and
|
||||
"""The guard must be able to stay quiet, or it is not a guard — and
|
||||
a backfill that re-embeds everything every boot is a different bug."""
|
||||
scan = _scan_session(all_ids=[1, 2, 3], current_ids=[1, 2, 3])
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""A tuned number carries the space it was measured in — and only that (#4104).
|
||||
"""A tuned number carries the space it was measured in — and only that.
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
Milestone 416 step 6 answers *"a path for thresholds to be inherited by the next
|
||||
version or different model so that they don't have to recalibrate a lot."* The
|
||||
mechanism is a stamp: every tuning event records the embedding model and
|
||||
A threshold should be inheritable by the next version or a different model
|
||||
without recalibrating from scratch. The mechanism is a stamp: every tuning event records the embedding model and
|
||||
document shape the number was chosen under, and a mismatch is reported rather
|
||||
than left to be noticed.
|
||||
|
||||
@@ -49,7 +48,7 @@ def test_calibration_stamp_names_the_embedder_and_the_shape():
|
||||
"embedding_model": emb.EMBEDDING_MODEL,
|
||||
"shape_version": emb.CHUNKER_VERSION,
|
||||
}
|
||||
# Two keys, never one fused string (rule 149): a mismatch has to be able to
|
||||
# Two keys, never one fused string: a mismatch has to be able to
|
||||
# say WHICH half moved, because a new embedder and a re-cut document
|
||||
# invalidate the same numbers for different reasons.
|
||||
assert len(stamp) == 2
|
||||
@@ -136,7 +135,7 @@ def test_a_dial_moved_before_stamps_existed_is_unknown_not_fine():
|
||||
|
||||
|
||||
def test_a_model_change_and_a_shape_change_are_reported_apart():
|
||||
"""Rule 149. Two conditions, two answers — they call for different work."""
|
||||
"""Two conditions, two answers — they call for different work."""
|
||||
s = get_surface("prompt_rule")
|
||||
live = {"embedding_model": "new/model", "shape_version": 1}
|
||||
cal = rt._calibration(_row(model="old/model", shape=1), s, live)
|
||||
@@ -182,8 +181,7 @@ def test_the_tool_says_nothing_auto_retunes():
|
||||
"""The contract an agent reads. A stale stamp is a prompt to MEASURE.
|
||||
|
||||
Without this, the obvious next move on seeing `stale: true` is to move the
|
||||
dial — which is tuning from a statistic, the exact failure #4102 measured
|
||||
pointing the wrong way.
|
||||
dial — which is tuning from a statistic, which can point the wrong way.
|
||||
"""
|
||||
doc = tool_doc("scribe.mcp.tools.retrieval_tuning", "retrieval_surfaces")
|
||||
assert "calibration" in doc
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""The shape ledger judging against family ideas, without a database
|
||||
(milestone 463 step 5).
|
||||
"""The shape ledger judging against family ideas, without a database.
|
||||
|
||||
Pinned here: what a project's shapes say about an idea, when an answer
|
||||
contradicts them, which reference a shape in a given language is judged
|
||||
against, and the family arm's rule — measured on 2026-10-06 (#4991), and
|
||||
replayed below as the table it was chosen from. The two ledgers moving
|
||||
against, and the family arm's rule — measured, and replayed below as the
|
||||
table it was chosen from. The two ledgers moving
|
||||
together against Postgres are in tests/test_integration_family_adoption.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -95,7 +94,7 @@ OWN, REF, OTHER = 1, 2, 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,hits,expected", [
|
||||
# #4991's pairs, the reference as REF and the best other hit as OTHER.
|
||||
# The measured pairs, the reference as REF and the best other hit as OTHER.
|
||||
("A py->go", [(0.728, REF), (0.703, OTHER)], "family"),
|
||||
("B py->go", [(0.715, REF), (0.675, OTHER)], "family"),
|
||||
("D kt->kt", [(0.816, REF), (0.734, OTHER)], "family"),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Project inception (milestone 297) — step 1: the record's shape.
|
||||
"""Project inception — the record's shape.
|
||||
|
||||
The WHY a project inherits what it does lives on projects.inception. Pure
|
||||
validation is pinned here; the effects are step 3's integration tests.
|
||||
validation is pinned here; the effects are the integration tests.
|
||||
|
||||
Rules left inception in two steps: the always-on opt-out with the tier
|
||||
(milestone 394), and the rulebook subscription with subscriptions (milestone
|
||||
414). A rule in a rulebook is global, so there is nothing to choose.
|
||||
Rules are not an inception choice: a rule in a rulebook is global, so there is
|
||||
nothing to choose.
|
||||
"""
|
||||
from scribe.models.project import Project
|
||||
from scribe.services.inception import (
|
||||
@@ -39,7 +38,7 @@ def test_validate_inception_pins_the_choice_vocabulary():
|
||||
# believe a rulebook had been declined.
|
||||
assert "unknown inception choice" in validate_inception(
|
||||
{"exclude_always_on_rulebooks": [1]})
|
||||
# Same for the subscription choice since milestone 414.
|
||||
# Same for a rulebook subscription choice.
|
||||
assert "unknown inception choice" in validate_inception({"subscribe_rulebooks": [2]})
|
||||
assert "positive id or null" in validate_inception({"design_system_id": 0})
|
||||
assert "positive id or null" in validate_inception({"design_system_id": True})
|
||||
@@ -71,8 +70,8 @@ def test_normalize_choices_is_canonical_and_complete():
|
||||
|
||||
|
||||
def test_standard_systems_vocabulary_reads_the_catalog_not_a_constant():
|
||||
"""The vocabulary moved from a module constant to the global catalog table
|
||||
(milestone 307): a constant cannot be a foreign key, so nothing outside a
|
||||
"""The vocabulary is the global catalog table, not a module constant: a
|
||||
constant cannot be a foreign key, so nothing outside a
|
||||
project could reference an area. The seed and the bootstrap ask must both
|
||||
read the table, or the list they show and the list they mint diverge."""
|
||||
import inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Real-Postgres round trip for the note fields #3182 restored.
|
||||
"""Real-Postgres round trip for the note fields a restore carries.
|
||||
|
||||
The unit lane can prove a serialiser EMITS a key. It cannot prove a restore
|
||||
puts the value back on the right row, and the note->note edges are exactly
|
||||
@@ -7,8 +7,6 @@ the SOURCE database, so a restore that writes them straight through succeeds,
|
||||
reports success, and silently points every edge at whatever record happens to
|
||||
hold that number here.
|
||||
|
||||
That is why #3182 was not fixed in passing.
|
||||
|
||||
These drive the REAL `restore_full_backup`, not a reimplementation of its
|
||||
loop. A test that re-derives the remap it is checking would agree with
|
||||
whatever the product does, including nothing.
|
||||
@@ -162,10 +160,10 @@ async def _no_leftover_restored_user():
|
||||
|
||||
|
||||
async def test_a_restored_record_keeps_what_it_IS(restored):
|
||||
"""#3182's headline. Without note_type and task_kind a restore reported
|
||||
success and handed back a corpus where every snippet and process was a
|
||||
plain note and every issue and spike was `work` — the whole vocabulary
|
||||
milestone 312 and #3128 were about, gone, with nothing to notice it by."""
|
||||
"""Without note_type and task_kind a restore reports success and hands
|
||||
back a corpus where every snippet and process is a plain note and every
|
||||
issue and spike is `work` — the record vocabulary gone, with nothing to
|
||||
notice it by."""
|
||||
by_title, _ = restored
|
||||
|
||||
assert by_title["debounce — rate-limit"].note_type == "snippet"
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"""#3262 against a real Postgres: the embedder loses the race, it doesn't run it.
|
||||
"""Against a real Postgres: the embedder loses the race, it doesn't run it.
|
||||
|
||||
The reported failure was a deadlock — `DELETE FROM rulebooks` killed by the
|
||||
server while a detached `upsert_rule_embedding` held the other half of the
|
||||
cycle. It cannot be reproduced with mocks, because there is nothing to
|
||||
The failure is a deadlock — `DELETE FROM rulebooks` killed by the server
|
||||
while a detached `upsert_rule_embedding` holds the other half of the cycle.
|
||||
It cannot be reproduced with mocks, because there is nothing to
|
||||
deadlock: the whole bug lives in the ORDER two transactions take two row
|
||||
locks, which only a lock manager can adjudicate.
|
||||
|
||||
So each test here holds a real delete open in one transaction and calls the
|
||||
embedder in another. What is being pinned is that the embedder RETURNS —
|
||||
promptly, having written nothing. Before the fix it would sit on the chunk
|
||||
rows waiting for a delete that is itself waiting on the insert's foreign key,
|
||||
and the test would hang rather than fail, which is why every call carries a
|
||||
deadline (rule 156).
|
||||
promptly, having written nothing. An embedder that sits on the chunk rows
|
||||
waiting for a delete that is itself waiting on the insert's foreign key would
|
||||
hang the test rather than fail it, which is why every call carries a
|
||||
deadline.
|
||||
"""
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, patch
|
||||
@@ -53,8 +53,7 @@ SETTLE_DEADLINE_SECONDS = 30
|
||||
async def seeded():
|
||||
"""A rule and a note to race against.
|
||||
|
||||
CLEANED AT SETUP, NOT TEARDOWN — the same constraint #3241 hit and the
|
||||
reason this file exists. `create_rule` fires its own detached embedding
|
||||
CLEANED AT SETUP, NOT TEARDOWN. `create_rule` fires its own detached embedding
|
||||
task; a teardown that deleted the rulebook would be racing exactly the
|
||||
thing under test, on a loop that is closing.
|
||||
"""
|
||||
@@ -141,7 +140,7 @@ async def test_a_rule_refresh_yields_to_a_delete_cascading_from_its_rulebook(see
|
||||
|
||||
|
||||
async def test_a_note_refresh_yields_to_a_delete_of_the_note(seeded):
|
||||
"""The note twin, which #3262 recorded as unverified. Notes are soft-deleted
|
||||
"""The note twin. Notes are soft-deleted
|
||||
day to day, so the hard delete a trash purge issues is the one that can put
|
||||
a lock on the row while a refresh is in flight."""
|
||||
async with async_session() as blocker:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Real-Postgres tests for the drift surface (milestone 399 step 5, #3895).
|
||||
"""Real-Postgres tests for the drift surface.
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
@@ -20,8 +20,8 @@ subquery picking the newest version per rule, an ownership clause spanning two
|
||||
paths, and a write that has to leave the history longer than it found it. A
|
||||
stand-in session returns whatever the test handed it, so it could confirm none
|
||||
of those. `test_the_restore_is_recorded_as_an_edit` is the one that matters
|
||||
most: it is the whole basis for allowing a one-click revert here when
|
||||
milestone 323 refused one for rules, and if it were ever to pass while the
|
||||
most: it is the whole basis for allowing a one-click revert here when rules
|
||||
have none, and if it were ever to pass while the
|
||||
restore silently overwrote history, the carve-out would be indefensible.
|
||||
"""
|
||||
import pytest
|
||||
@@ -228,8 +228,8 @@ async def test_restoring_puts_the_old_wording_back(world):
|
||||
async def test_the_restore_is_recorded_as_an_edit(world):
|
||||
"""THE ONE THAT MATTERS MOST — it is why this carve-out is defensible.
|
||||
|
||||
Milestone 323 refused a one-click restore for rules because "a silent
|
||||
revert would erase the only record of why the rewrite happened". The
|
||||
Rules have no one-click restore because a silent revert would erase the
|
||||
only record of why the rewrite happened. The
|
||||
preference carve-out survives that objection only while nothing is
|
||||
erased: the restore goes through `update_rule`, so it takes its own
|
||||
snapshot and the history GAINS an entry.
|
||||
@@ -262,7 +262,7 @@ async def test_the_restore_is_recorded_as_an_edit(world):
|
||||
|
||||
|
||||
async def test_a_rule_refuses_the_one_click_restore(world):
|
||||
"""Milestone 323 stands for rules, and the kind check is in the service
|
||||
"""Rules still have no restore, and the kind check is in the service
|
||||
rather than only in the route — so a caller reaching this directly cannot
|
||||
forget it."""
|
||||
await rulebooks_svc.update_rule(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Real-Postgres tests for `rules.kind` and its CHECK (0098, #3849 step 1).
|
||||
"""Real-Postgres tests for `rules.kind` and its CHECK (migration 0098).
|
||||
|
||||
Rule 36 exists because a value and its constraint drift apart: the code
|
||||
A value and its constraint drift apart: the code
|
||||
starts writing a new kind while the database still refuses it, and nothing
|
||||
catches it until a write fails in front of someone. A mock cannot show that —
|
||||
it has no CHECK — so the constraint gets a real-DB test, exactly as 0091's
|
||||
@@ -94,8 +94,7 @@ async def test_an_unknown_kind_is_refused(topic_id):
|
||||
"""The half that proves the constraint is there at all.
|
||||
|
||||
Without this, every other assertion in this file would pass against a
|
||||
table whose CHECK had been dropped — the exact failure rule 36 is written
|
||||
against.
|
||||
table whose CHECK had been dropped — the exact drift this guards.
|
||||
"""
|
||||
_uid, topic = topic_id
|
||||
with pytest.raises(IntegrityError):
|
||||
@@ -119,8 +118,8 @@ async def test_a_rule_can_become_a_preference_after_the_fact(topic_id):
|
||||
"""The read-back is the whole test.
|
||||
|
||||
A version asserting only that the update call succeeded would pass against
|
||||
code that accepted `kind` and dropped it — which is how the same bug
|
||||
survived on `task_kind` long enough to be found by hand (#3129).
|
||||
code that accepted `kind` and dropped it — a bug that otherwise surfaces
|
||||
only when someone finds it by hand.
|
||||
|
||||
This is also the migration path the always-on triage needs: the rules that
|
||||
turn out to be preferences change one column and keep their id, history
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Real-Postgres tests for rule mounts (milestone 458 step 3).
|
||||
"""Real-Postgres tests for rule mounts.
|
||||
|
||||
A mount is the claim "this rule arrives at this moment". These pin what that
|
||||
claim depends on:
|
||||
@@ -7,7 +7,7 @@ claim depends on:
|
||||
- the rule's detail reads its mounts back;
|
||||
- a backup carries them, attached to the RESTORED rule's new id rather than
|
||||
the old number;
|
||||
- the delivery lookup (step 4) answers by home: a project's mounted rule
|
||||
- the delivery lookup answers by home: a project's mounted rule
|
||||
reaches only work bound to that project, and nobody else's mount arrives.
|
||||
"""
|
||||
import pytest
|
||||
@@ -150,7 +150,7 @@ async def test_a_backup_carries_the_mounts_to_the_restored_rule(world):
|
||||
assert sorted(r["moment"] for r in payload["rule_moments"]) == [
|
||||
"reply.report", "work.finish",
|
||||
]
|
||||
# The mount was an edit, so it was also recorded as two judgments (step 7)
|
||||
# The mount was an edit, so it was also recorded as two judgments
|
||||
# — and those travel too, onto the restored rule.
|
||||
payload["rule_moment_judgments"] = [
|
||||
row for row in exported["rule_moment_judgments"] if row["rule_id"] == rule.id
|
||||
@@ -238,7 +238,7 @@ async def test_the_delivery_lookup_answers_by_home(world):
|
||||
|
||||
|
||||
async def test_moment_usage_counts_deliveries_and_the_agent_opens_that_followed(world):
|
||||
"""The per-moment readout (step 6), on real SQL: an EXISTS inside a
|
||||
"""The per-moment readout, on real SQL: an EXISTS inside a
|
||||
COUNT(CASE …) is a shape no mock can vouch for, and the block's own guard
|
||||
would turn a database refusal into a quiet `moment_usage_failed`."""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Real-Postgres tests for WHERE a rule reaches (milestone 414, step 1).
|
||||
"""Real-Postgres tests for WHERE a rule reaches.
|
||||
|
||||
A rule lives in a rulebook topic (global) or on one project. Retrieval used to
|
||||
ignore that and search every rule the user owned, so each project's rules were
|
||||
@@ -78,7 +78,7 @@ async def _found(user_id: int, **scope) -> set[int]:
|
||||
"""The rule ids a search finds — and a failure, not an empty set, when the
|
||||
search never ran. `semantic_search_rules` fails open (an exception becomes
|
||||
[]), so without `report["searched"]` an error and a scoping miss read the
|
||||
same (#4958)."""
|
||||
same."""
|
||||
report: dict = {}
|
||||
raised: list[str] = []
|
||||
with patch("scribe.services.embeddings.get_embedding",
|
||||
@@ -109,7 +109,7 @@ async def test_retrieval_reaches_a_rule_only_from_its_home(homes):
|
||||
|
||||
|
||||
async def test_a_shared_project_brings_its_rules_to_a_collaborator(homes):
|
||||
"""Readability goes through access.can_read_project (rule 78): a viewer on
|
||||
"""Readability goes through access.can_read_project: a viewer on
|
||||
A gets A's rules. Not the owner's global rules — rulebooks are the
|
||||
owner's — and not B's, which is not shared."""
|
||||
collaborator = homes["collaborator"]
|
||||
@@ -118,9 +118,9 @@ async def test_a_shared_project_brings_its_rules_to_a_collaborator(homes):
|
||||
|
||||
|
||||
async def test_an_in_scope_rule_is_found_however_many_closer_rules_are_out_of_scope():
|
||||
"""#4958. Ordered through the HNSW index, the search took the ~40 nearest
|
||||
chunks from every owner and filtered them AFTERWARDS — so a reader's own
|
||||
rule ranked past the 40th chunk overall was never returned. Here another
|
||||
"""A search ordered through the HNSW index takes the ~40 nearest chunks
|
||||
from every owner, and filtering them AFTERWARDS loses a reader's own rule
|
||||
ranked past the 40th chunk overall. Here another
|
||||
user's 80 rules all sit nearer the query than the reader's one rule; the
|
||||
reader's search must still find it."""
|
||||
tag = uuid.uuid4().hex[:8]
|
||||
|
||||
@@ -10,12 +10,9 @@ planning read carries — and what mocks can't prove about it:
|
||||
comparison, never a similarity score.
|
||||
2. An UNTAGGED global rule is not listed: it applies everywhere and arrives by
|
||||
retrieval, and listing every one under every project would say nothing.
|
||||
3. A `co_surfaces` partner arrives with its other half — the failure that made
|
||||
merging rule 144 into rule 46 look like the only fix — unless the partner
|
||||
lives on a different project, because an edge is not a way in.
|
||||
|
||||
Milestone 414 dropped the suppression claim ("an explicit suppression outranks
|
||||
an edge") with suppressions themselves.
|
||||
3. A `co_surfaces` partner arrives with its other half — without it, merging
|
||||
two rules that must arrive together looks like the only fix — unless the
|
||||
partner lives on a different project, because an edge is not a way in.
|
||||
"""
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
@@ -102,9 +99,9 @@ async def test_a_tagged_global_rule_is_listed_for_a_project_that_works_in_its_ar
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_co_surfaces_drags_in_the_half_that_would_have_been_missed(world):
|
||||
"""Rule 144 was split off rule 46 and folded back the same day because
|
||||
"either rule could surface without the other". This is the edge that makes
|
||||
that unnecessary: the partner arrives even though nothing else selected
|
||||
"""Two rules that must arrive together get folded into one because either
|
||||
could surface without the other. This is the edge that makes that
|
||||
unnecessary: the partner arrives even though nothing else selected
|
||||
it, and says why it is here."""
|
||||
partner = await rulebooks_svc.create_rule(
|
||||
world["topic"], world["owner"], "Version names are labels",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The document a lesson is embedded as (milestone 385 step 3; milestone 427).
|
||||
"""The document a lesson is embedded as.
|
||||
|
||||
WHY THIS IS THE STEP THAT DECIDES THE MILESTONE
|
||||
|
||||
@@ -9,17 +9,16 @@ anywhere would report that.
|
||||
|
||||
WHERE THE SHARP SHAPE LIVES
|
||||
|
||||
A snippet, which note #2485 measured as the only sharp record in the corpus (a
|
||||
0.153 top-to-second gap against 0.010–0.023 for everything else), is sharp
|
||||
A snippet, measured as the sharpest record kind in a corpus (a 0.153
|
||||
top-to-second gap against 0.010–0.023 for everything else), is sharp
|
||||
because its document states its purpose twice: `name — when to use` as the
|
||||
title, and again as the body's first line. A lesson follows the snippet.
|
||||
|
||||
Until milestone 427 that `subject — trigger` title was also the STORED title,
|
||||
so every listing, menu and search row showed the trigger too — kilobytes of it.
|
||||
Now the stored title is the subject, and `embeddings.document_title` joins the
|
||||
trigger back from `data` at embed time. The embedded TEXT is what it always
|
||||
was, which is the property these guards pin: nothing re-embeds, and the floors
|
||||
tuned against these vectors stay calibrated.
|
||||
The STORED title is the subject alone, so listings, menus and search rows do
|
||||
not carry kilobytes of trigger; `embeddings.document_title` joins the trigger
|
||||
back from `data` at embed time. The embedded TEXT keeps the `subject —
|
||||
trigger` shape, which is the property these guards pin: the floors tuned
|
||||
against these vectors stay calibrated.
|
||||
|
||||
No similarity number is asserted anywhere: a threshold pins the embedder's
|
||||
behaviour rather than this code's, and breaks on a model change that is not a
|
||||
@@ -44,7 +43,7 @@ def _embedded(what: str, trigger: str, insight: str) -> tuple[str, str]:
|
||||
|
||||
|
||||
def test_the_stored_title_is_the_subject_alone():
|
||||
"""Milestone 427. The trigger lives in `data` and the body's first line; the
|
||||
"""The trigger lives in `data` and the body's first line; the
|
||||
title a listing shows is what the lesson is ABOUT."""
|
||||
title, _ = lessons_svc.lesson_document(SUBJECT, TRIGGER, INSIGHT)
|
||||
assert title == SUBJECT
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Reading a lesson's insight back and re-composing it returns the same body (#4272).
|
||||
"""Reading a lesson's insight back and re-composing it returns the same body.
|
||||
|
||||
THE PROPERTY. `update_lesson` is field-wise: an edit that touches only
|
||||
`system_ids`, or only `what`, passes `insight=None`, and the service then
|
||||
@@ -24,11 +24,9 @@ insight", the full trigger was prepended again, and the stored lesson gained
|
||||
(n-1) paragraphs per update — silently, compounding, and in the half of the
|
||||
document that does the retrieving.
|
||||
|
||||
Measured on the one real row this damaged (lesson #4207, a three-paragraph
|
||||
trigger): the body grew by exactly 607 characters per edit — the length of
|
||||
paragraphs 2 and 3 with their separators — taking it from 1291 characters to
|
||||
7361 over ten edits, at which point the trigger appears eleven times and the
|
||||
insight is 17% of the record.
|
||||
On a three-paragraph trigger the body grows by the length of paragraphs 2 and
|
||||
3 with their separators on every edit — after ten edits the trigger appears
|
||||
eleven times and the insight is a sixth of the record.
|
||||
|
||||
WHY A MARKER COULD NOT HAVE FIXED IT. There is no end-of-trigger marker in
|
||||
the body and adding one would not repair a row already written without it.
|
||||
@@ -99,7 +97,7 @@ def _stored(trigger: str, insight: str, sources: list[int]):
|
||||
MagicMock auto-creates truthy and coerces to `1`. A no-sources shape then
|
||||
recomposes with `**Learned from:** #1` and the round trip fails for a
|
||||
reason that has nothing to do with the trigger. `fake_lesson` pins that
|
||||
field to None — note #2109's point, which is why the fixture exists.
|
||||
field to None, which is why the fixture exists.
|
||||
"""
|
||||
title, body = lessons_svc.lesson_document(WHAT, trigger, insight, sources)
|
||||
data = lessons_svc.compose_data(WHAT, trigger, sources)
|
||||
@@ -137,7 +135,7 @@ def test_an_edit_that_changes_nothing_changes_nothing(_name, trigger, insight, s
|
||||
# Reported as a size delta rather than left to a diff of two multi-kilobyte
|
||||
# strings: the failure this catches is duplication, and the number of
|
||||
# characters gained says which paragraph came back far faster than an
|
||||
# equality dump does (rule 167 — a guard has to name what broke).
|
||||
# equality dump does (a guard has to name what broke).
|
||||
assert (title, body) == (note.title, note.body), (
|
||||
f"a no-op edit rewrote the lesson: title {len(title) - len(note.title):+d} "
|
||||
f"chars, body {len(body) - len(note.body):+d} chars"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Creating a lesson, and tying it to what taught it (milestone 385 step 4).
|
||||
"""Creating a lesson, and tying it to what taught it.
|
||||
|
||||
THE TRIGGER IS THE RECORD, so the door refuses a lesson without one.
|
||||
|
||||
@@ -136,8 +136,8 @@ async def test_the_gate_compares_the_composed_document_not_the_raw_fields():
|
||||
|
||||
|
||||
def test_a_lesson_is_judged_at_the_trigger_dominated_bar():
|
||||
"""#2518 measured deliberately-parallel siblings at 0.92 on a document that
|
||||
is mostly prose ABOUT the thing. A lesson's document is that shape, so the
|
||||
"""Deliberately-parallel siblings measure 0.92 on a document that is
|
||||
mostly prose ABOUT the thing. A lesson's document is that shape, so the
|
||||
bar sits above that band — otherwise two different lessons about one area
|
||||
block each other, which is the failure step 4 asked to check for."""
|
||||
from scribe.services.dedup import (
|
||||
@@ -153,7 +153,7 @@ def test_a_lesson_is_judged_at_the_trigger_dominated_bar():
|
||||
)
|
||||
assert _LESSON_SEMANTIC_THRESHOLD > _SEMANTIC_THRESHOLD
|
||||
# The carve-out is per kind, not a loosening of the gate: a lesson's bar is
|
||||
# its own, apart from the note's copy band (#4306).
|
||||
# its own, apart from the note's copy band.
|
||||
from scribe.services.dedup import _NOTE_COPY_THRESHOLD
|
||||
|
||||
assert _semantic_threshold("note") == _NOTE_COPY_THRESHOLD
|
||||
@@ -163,7 +163,7 @@ def test_a_lesson_is_judged_at_the_trigger_dominated_bar():
|
||||
@pytest.mark.asyncio
|
||||
async def test_an_update_recomposes_both_halves_of_the_document():
|
||||
"""A new trigger has to reach the mirror AND the head of the body — the two
|
||||
places the embedded document reads it from (milestone 427). Patching one
|
||||
places the embedded document reads it from. Patching one
|
||||
would leave a lesson that reads correctly and ranks on the old situation —
|
||||
the failure mode with no symptom. The title stays the subject, even when
|
||||
the stored one was an un-migrated composed title."""
|
||||
|
||||
@@ -108,16 +108,15 @@ def test_every_registered_tool_is_classified_exactly_once():
|
||||
"""Every tool must be declared a read, a write, or a read-shaped write.
|
||||
|
||||
`_READ_ONLY_TOOLS` is hand-maintained, and default-deny means a read
|
||||
omitted from it fails CLOSED — safe, but silent. That is how a read key
|
||||
ended up able to `get_note` and not `get_snippet` (#2496), and how
|
||||
`rules_due_for_verification` and `rule_history` sat denied (#3191).
|
||||
omitted from it fails CLOSED — safe, but silent: a read key able to
|
||||
`get_note` and not `get_snippet`.
|
||||
|
||||
The fix keeps the DECISION explicit and derives the CANDIDATES. Deriving
|
||||
So the DECISION stays explicit and the CANDIDATES are derived. Deriving
|
||||
the decision would make a security boundary follow a naming convention, so
|
||||
any future `get_*` grants itself access. This test used to derive
|
||||
candidates from names too — tools starting `get_`, `list_`, `search`… — and
|
||||
that narrowing is exactly what let the two #3191 reads through: neither
|
||||
name looked like a read. So the candidates are now EVERY registered tool.
|
||||
any future `get_*` grants itself access. Deriving the candidates from names
|
||||
— tools starting `get_`, `list_`, `search`… — misses reads whose names do
|
||||
not look like reads (`rules_due_for_verification`, `rule_history`). So the
|
||||
candidates are EVERY registered tool.
|
||||
"""
|
||||
from scribe.mcp.server import (
|
||||
_DELIBERATELY_WRITE_SCOPED, _READ_ONLY_TOOLS, _WRITE_TOOLS,
|
||||
@@ -156,7 +155,7 @@ def test_every_registered_tool_is_classified_exactly_once():
|
||||
|
||||
def test_the_completeness_check_can_fail():
|
||||
"""A guard that cannot fail is indistinguishable from one that is broken
|
||||
(rule 167). Drop a real tool from its set and the check must notice."""
|
||||
Drop a real tool from its set and the check must notice."""
|
||||
from scribe.mcp.server import _READ_ONLY_TOOLS, _WRITE_TOOLS
|
||||
|
||||
registered = _registered_tool_names()
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"""Unknown tool arguments are rejected, never silently dropped (#2709).
|
||||
"""Unknown tool arguments are rejected, never silently dropped.
|
||||
|
||||
The failure this pins: MCPServer validates tool arguments with a pydantic model
|
||||
whose extra-field policy is "ignore", so `create_note(content=...)` — a
|
||||
plausible near-miss for `body=`, primed by add_task_log's `content` — ran
|
||||
successfully, stored `body: ""`, and left a record embedding/search cannot
|
||||
see. The call REPORTED SUCCESS. Two real notes were persisted body-less
|
||||
before the pattern was noticed.
|
||||
plausible near-miss for `body=`, primed by add_task_log's `content` — would
|
||||
run, store `body: ""`, and leave a record embedding/search cannot see, while
|
||||
REPORTING SUCCESS.
|
||||
|
||||
The fix is at the dispatch seam, not per-tool: StrictArgsMCPServer rejects any
|
||||
The rejection is at the dispatch seam, not per-tool: StrictArgsMCPServer rejects any
|
||||
call carrying arguments the tool does not declare, with a did-you-mean when
|
||||
one is close. An error the caller sees once beats data half-written forever.
|
||||
"""
|
||||
@@ -59,11 +58,11 @@ async def test_unknown_tool_keeps_the_upstream_error():
|
||||
assert "no_such_tool" in str(exc.value)
|
||||
|
||||
|
||||
# --- a refusal's text reaches the agent (#4794) -------------------------------
|
||||
# --- a refusal's text reaches the agent ---------------------------------------
|
||||
#
|
||||
# SDK 2.x passes on only a ToolError's text; anything else is wrapped as
|
||||
# "Error executing tool X" and its message is kept on the server. ValueError is
|
||||
# how every Scribe tool refuses, so for weeks every refusal arrived bare.
|
||||
# how every Scribe tool refuses, so without this every refusal arrives bare.
|
||||
|
||||
|
||||
def _raising_server() -> StrictArgsMCPServer:
|
||||
@@ -112,7 +111,7 @@ async def test_a_real_tools_refusal_reaches_the_caller():
|
||||
|
||||
|
||||
async def test_the_original_regression_create_note_with_content():
|
||||
"""Pin #2709 itself against the REAL server: `content=` on create_note
|
||||
"""Against the REAL server: `content=` on create_note
|
||||
must raise before dispatch — naming the bad argument and listing `body`
|
||||
among the accepted ones — instead of creating a body-less note."""
|
||||
from scribe.mcp.server import build_mcp_server
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Delivering mounted rules when their moment happens (milestone 458 step 4).
|
||||
"""Delivering mounted rules when their moment happens.
|
||||
|
||||
The database is stubbed: the lookup's scoping is pinned against Postgres in
|
||||
tests/test_integration_rule_moments.py. These pin what is decided above it:
|
||||
@@ -186,7 +186,7 @@ async def _reachable(mounted, mappings=()):
|
||||
|
||||
|
||||
# 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.
|
||||
# shape: closing work, a structured question, opening a plan.
|
||||
SHAPED = ["askuserquestion", "create_milestone", "enterplanmode", "exitplanmode",
|
||||
"skill", "start_planning", "update_milestone", "update_task"]
|
||||
|
||||
@@ -273,7 +273,7 @@ def test_the_hook_is_registered_on_every_tool():
|
||||
|
||||
|
||||
def test_the_hook_and_the_routes_agree_on_every_name():
|
||||
"""Rule 33 across the shell/Python seam: a renamed arg fails silently."""
|
||||
"""Parity across the shell/Python seam: a renamed arg fails silently."""
|
||||
src = HOOK.read_text()
|
||||
assert "/api/plugin/moment-tools" in src and "/api/plugin/moment" in src
|
||||
assert "exclude_rule_ids" in src and "scribe_held_query" in src
|
||||
@@ -391,7 +391,7 @@ def test_every_scribe_tool_the_defaults_name_attaches_its_moment_rules():
|
||||
)
|
||||
|
||||
|
||||
# ── reply shapes ride the moments (milestone 500 step 3) ────────────────
|
||||
# ── reply shapes ride the moments ───────────────────────────────────────
|
||||
|
||||
from scribe.services import reply_shapes # noqa: E402
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The prompt boundary retrieves against PROMPTS, not against plumbing (#4200).
|
||||
"""The prompt boundary retrieves against PROMPTS, not against plumbing.
|
||||
|
||||
Claude Code submits more than typed words through `UserPromptSubmit`. A task
|
||||
notification, the echo of a slash command and the caveat banner a local command
|
||||
@@ -10,11 +10,11 @@ never returns anything. The cost that matters is not the wasted embedding, it
|
||||
is the LOG ROW. Every one of those calls counts in the denominator of every
|
||||
prompt-boundary surface, so delivery rate reads low for a reason unrelated to
|
||||
retrieval quality; and each refusal lands in `near_misses`, where a later
|
||||
tuning decision reads it as demand. Measured on #3898: 15 of the top 20
|
||||
preference near-misses were `<task-notification>` blocks, all matching one
|
||||
record, all within thousandths of the floor. A floor lowered to serve that
|
||||
apparent demand would have injected that record into every notification — an
|
||||
instrument arguing for the wrong fix, which is exactly #379.
|
||||
tuning decision reads it as demand. Notification blocks can fill most of a
|
||||
preference arm's top near-misses, all matching one record, all within
|
||||
thousandths of the floor. A floor lowered to serve that apparent demand would
|
||||
inject that record into every notification — an instrument arguing for the
|
||||
wrong fix.
|
||||
|
||||
So the assertions split in two, and the SECOND half is the one guarding
|
||||
against the obvious wrong implementation:
|
||||
@@ -52,7 +52,7 @@ SYNTHETIC = [
|
||||
"<local-command-stderr>Failed to reconnect</local-command-stderr>",
|
||||
"<local-command-caveat>Caveat: The messages below were generated…</local-command-caveat>",
|
||||
" \n<task-notification>\n<task-id>abc</task-id>", # whitespace must not walk the filter
|
||||
# A subagent's hand-back (#4785) — it carries attributes, so the tag is
|
||||
# A subagent's hand-back — it carries attributes, so the tag is
|
||||
# matched up to its name, not to a closing `>`.
|
||||
'<agent-message from="a93b1b587d7a718b0">\n[Subagent hand-back] The text below is the final report',
|
||||
"<agent-message>\nreport</agent-message>",
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
"""Every typed record kind gets the same doors (#4164).
|
||||
"""Every typed record kind gets the same doors.
|
||||
|
||||
WHY THIS IS ONE GUARD AND NOT FIFTEEN
|
||||
|
||||
Milestone 385 shipped `lesson` across four steps and it still arrived with
|
||||
three of five tools, no way to list it, and no duplicate report. Nothing was
|
||||
red, because every test asked "does what I built work" and none asked "is the
|
||||
kind finished". A per-tool test cannot catch a MISSING tool.
|
||||
A kind can ship across several steps and still arrive with three of five
|
||||
tools, no way to list it, and no duplicate report — with nothing red, because
|
||||
every test asks "does what I built work" and none asks "is the kind
|
||||
finished". A per-tool test cannot catch a MISSING tool.
|
||||
|
||||
So this asserts the property directly — a kind with its own `note_type` has a
|
||||
full create/read/update/delete plus a listing, each one registered and each one
|
||||
classified for auth — and derives the expectation from the kinds themselves. A
|
||||
fourth kind added later inherits the bar without anyone remembering to.
|
||||
|
||||
#2250 is the same failure one kind earlier: processes could always be deleted
|
||||
through `delete_note`, but nothing said so, and "a kind whose own tools offer
|
||||
create/read/update reads as one you cannot retire".
|
||||
A kind deletable only through `delete_note` is the same failure: "a kind whose
|
||||
own tools offer create/read/update reads as one you cannot retire".
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -103,7 +102,7 @@ def test_every_kind_can_be_asked_whether_it_holds_duplicates(singular):
|
||||
|
||||
|
||||
def test_the_surface_guards_can_fail():
|
||||
"""Rule 167: falsify the shape these assert against, so a guard that has
|
||||
"""Falsify the shape these assert against, so a guard that has
|
||||
quietly stopped describing anything cannot pass by describing nothing."""
|
||||
assert _expected_tools("lesson", "lessons") == [
|
||||
"list_lessons", "create_lesson", "get_lesson",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""The operator's half of the tuning surface (#4102).
|
||||
"""The operator's half of the tuning surface.
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
The model moves these dials — that is the operator's decision for milestone
|
||||
416 — so the browser's job is no longer "set the number". It is to show what
|
||||
The model moves these dials, so the browser's job is not "set the number". It is to show what
|
||||
was set, by whom, and on what argument, and to let the operator disagree.
|
||||
|
||||
The load-bearing guard here is the last one. `/api/settings` is a generic
|
||||
@@ -51,7 +50,7 @@ def test_every_endpoint_is_reachable_on_the_app():
|
||||
|
||||
|
||||
def test_the_browser_and_the_agent_call_the_same_service():
|
||||
"""Rule 33 parity. Two callers, one service — or the two surfaces drift and
|
||||
"""Parity. Two callers, one service — or the two surfaces drift and
|
||||
the guard that a reason is required exists on only one of them."""
|
||||
from scribe.mcp.tools import retrieval_tuning as tool
|
||||
from scribe.routes import retrieval as routes
|
||||
@@ -138,7 +137,7 @@ async def test_setting_a_dial_to_the_value_it_already_has_records_nothing():
|
||||
setter.assert_not_called()
|
||||
|
||||
|
||||
# ── the moments view (milestone 458 step 6) ─────────────────────────────
|
||||
# ── the moments view ────────────────────────────────────────────────────
|
||||
|
||||
async def _moments_call(handler_name, path, method="GET", **kw):
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -69,7 +69,7 @@ def test_create_project_rule_route_exists():
|
||||
|
||||
|
||||
def test_subscriptions_and_suppressions_are_gone():
|
||||
"""Milestone 414: a rule's home is its scope. Nothing subscribes a project
|
||||
"""A rule's home is its scope. Nothing subscribes a project
|
||||
to a rulebook or mutes a rule for one — not a route, a service function or
|
||||
a table. A partial removal would leave a door that 500s on a missing table.
|
||||
"""
|
||||
@@ -94,14 +94,14 @@ def test_rule_handlers_callable():
|
||||
for name in (
|
||||
"list_rules", "create_rule", "get_rule", "update_rule", "delete_rule",
|
||||
"get_project_rules", "move_rule",
|
||||
# The typed edges — both doors carry them (rule 33).
|
||||
# The typed edges — both doors carry them.
|
||||
"relate_rules", "unrelate_rules",
|
||||
):
|
||||
assert callable(getattr(rb_routes, name))
|
||||
|
||||
|
||||
def test_the_rule_list_zero_fills_usage_on_every_row():
|
||||
"""Milestone 333 step 5, asserted the only way this harness allows.
|
||||
"""Asserted the only way this harness allows.
|
||||
|
||||
There is no live-HTTP fixture here (see this module's docstring), so this
|
||||
reads the handler's source. What it can still prove is the property that
|
||||
@@ -110,7 +110,7 @@ def test_the_rule_list_zero_fills_usage_on_every_row():
|
||||
existing install predates `rule_usage_events`, so a route that only
|
||||
attached the key when it found something would leave the badge component
|
||||
reading `undefined` on almost every row — and the difference between "no
|
||||
events" and "no field" is exactly the distinction #2663 is about.
|
||||
events" and "no field" is exactly the distinction a readout must keep.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""The supersession claim — who may make it, and what it refuses.
|
||||
|
||||
Step 2 of #278. Ranking behaviour is step 3; this covers only recording and
|
||||
reading the relation.
|
||||
Ranking behaviour is in test_supersession_ranking.py; this covers only
|
||||
recording and reading the relation.
|
||||
|
||||
The cycle tests are the ones worth reading. Under FLAT demotion a ring of
|
||||
records that supersede each other claims every member is obsolete, so all of
|
||||
@@ -53,7 +53,7 @@ async def test_superseding_a_note_you_can_only_READ_is_refused_not_dropped():
|
||||
|
||||
Demoting someone else's record out of their retrieval is damage that is
|
||||
invisible from the outside — the caller would believe it worked, and the
|
||||
owner would have no symptom to trace. Rule #47.
|
||||
owner would have no symptom to trace.
|
||||
"""
|
||||
# writable for the superseder (id 1), not for the target (id 2)
|
||||
writable = AsyncMock(side_effect=lambda uid, nid: nid == 1)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The SessionStart hook clears the rule ledger exactly when context dies (#3749).
|
||||
"""The SessionStart hook clears the rule ledger exactly when context dies.
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
@@ -109,28 +109,16 @@ def test_the_rules_ledger_survives_exactly_when_the_context_does(tmp_path):
|
||||
|
||||
|
||||
def test_the_note_ledgers_are_cleared_too_now_that_a_repeat_is_shown(tmp_path):
|
||||
"""The decision this test used to pin, reversed — and why it could be.
|
||||
"""The note ledgers are cleared on a compaction too.
|
||||
|
||||
It read: the note arms were deliberately left out of #3749, whether a
|
||||
surfaced NOTE should come back after a compaction is a different question,
|
||||
and a glob over `<sid>.*` would decide it by accident. That was right when
|
||||
it was written, and the reason was the note arms' behaviour rather than the
|
||||
note arms' nature: a note repeat was WITHHELD, so clearing their ledger
|
||||
meant re-injecting whole menu lines the session had already been given.
|
||||
Against that, leaving them was the cheaper mistake.
|
||||
|
||||
#4101 removed the premise. A note repeat is now rendered again with a
|
||||
`seen` marker instead of dropped from the search, so the ledger no longer
|
||||
decides whether a record is reachable — only whether its line says the
|
||||
A note repeat is rendered again with a `seen` marker rather than dropped
|
||||
from the search, so the ledger does not decide whether a record is
|
||||
reachable — only whether its line says the
|
||||
session has met it before. Across a compaction that marker becomes a false
|
||||
statement: it tells a freshly-summarised session it has already seen a
|
||||
record that is nowhere in its context. Keeping the ledger buys nothing and
|
||||
asserts something untrue.
|
||||
|
||||
Kept as a named test rather than deleted, because the roster it guards
|
||||
moved to tests/test_session_ledger_clear.py and what is worth keeping HERE
|
||||
is the record that the answer changed, with the reason attached — a
|
||||
deleted test takes the old decision's reasoning with it.
|
||||
asserts something untrue. The full roster is guarded in
|
||||
tests/test_session_ledger_clear.py.
|
||||
"""
|
||||
env = _env(tmp_path)
|
||||
sid = "sess-scope"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Every bundled skill keeps the shape Agent Skills can load well (#4398).
|
||||
"""Every bundled skill keeps the shape Agent Skills can load well.
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
@@ -8,11 +8,10 @@ keep the SKILL.md body under 500 lines, split into reference files as it
|
||||
nears that, and link each reference file ONE level deep from SKILL.md —
|
||||
Claude may only preview a file reached through another reference file.
|
||||
|
||||
using-scribe reached 478 lines because every new practice lands there, the
|
||||
same squeeze `_INSTRUCTIONS` was in before #4389. It was split: what matters
|
||||
on most turns stays in SKILL.md, what matters at one moment (placing a rule,
|
||||
a missed retrieval, starting a project) moved to files SKILL.md names with
|
||||
the moment to open them. These tests keep that shape from quietly undoing
|
||||
using-scribe grows because every new practice lands there. What matters on
|
||||
most turns stays in SKILL.md; what matters at one moment (placing a rule, a
|
||||
missed retrieval, starting a project) lives in files SKILL.md names with the
|
||||
moment to open them. These tests keep that shape from quietly undoing
|
||||
itself.
|
||||
|
||||
WHAT IT PINS
|
||||
@@ -71,8 +70,8 @@ def test_every_skill_md_fits_its_budget():
|
||||
SKILL_LINE_BUDGET)
|
||||
assert not found, (
|
||||
f"SKILL.md over {SKILL_LINE_BUDGET} lines: {found}. Move what matters at "
|
||||
f"one moment into a reference file SKILL.md links with that moment "
|
||||
f"(#4398), rather than raising the budget toward 500."
|
||||
f"one moment into a reference file SKILL.md links with that moment, "
|
||||
f"rather than raising the budget toward 500."
|
||||
)
|
||||
|
||||
|
||||
@@ -102,7 +101,7 @@ def test_long_reference_files_open_with_contents():
|
||||
|
||||
|
||||
def test_the_guards_can_fail():
|
||||
"""Rule 167: each guard bites on the failure it exists for."""
|
||||
"""Each guard bites on the failure it exists for."""
|
||||
assert over_budget({"s": "x\n" * 400}, SKILL_LINE_BUDGET) == ["s: 400 lines"]
|
||||
assert unlinked("see [a](a.md)", ["a.md", "b.md"]) == ["b.md"]
|
||||
assert nested({"a.md": "then [b](b.md)"}) == ["a.md → b.md"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for the near-duplicate finder (#2088).
|
||||
"""Tests for the near-duplicate finder.
|
||||
|
||||
The SQL half needs a database and lives in the integration lane; what's covered
|
||||
here is the grouping rule — which is where the interesting decisions are — plus
|
||||
@@ -98,13 +98,13 @@ def test_the_snippet_report_threshold_is_looser_than_the_write_gate():
|
||||
report only suggests a merge the operator reviews, so it has to reach
|
||||
further or it would never surface the pairs the gate already let through.
|
||||
Snippet-only on purpose: note/task floors sit ABOVE the gate because chunk
|
||||
grain (#280) lifts related families over it — see the module comment."""
|
||||
grain lifts related families over it — see the module comment."""
|
||||
assert DUPLICATE_DEFAULT_THRESHOLDS["snippet"] < dedup_svc._SEMANTIC_THRESHOLD
|
||||
|
||||
|
||||
def test_the_note_and_task_floors_are_stricter_than_the_snippet_floor():
|
||||
"""At chunk grain, 0.82 is a related-families view; the report's default
|
||||
must point at genuinely-alike records (measured 2026-08-09, note #2551)."""
|
||||
must point at genuinely-alike records."""
|
||||
assert DUPLICATE_DEFAULT_THRESHOLDS["note"] == 0.93
|
||||
assert DUPLICATE_DEFAULT_THRESHOLDS["task"] == 0.93
|
||||
|
||||
@@ -119,7 +119,7 @@ async def test_a_failed_scan_returns_an_empty_report_not_an_error():
|
||||
patch.object(dedup_svc, "async_session", side_effect=RuntimeError("boom")),
|
||||
):
|
||||
out = await dedup_svc.find_duplicate_snippets(1)
|
||||
# The empty report still carries its per-kind `suggestion` (#2547) — a
|
||||
# The empty report still carries its per-kind `suggestion` — a
|
||||
# failed scan must be indistinguishable in SHAPE from a clean one, or every
|
||||
# consumer needs a second code path for the degraded case.
|
||||
assert out["groups"] == [] and out["pairs"] == []
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"""Demotion: superseded records rank behind their equals, and are never hidden.
|
||||
|
||||
Step 3 of #278. The distinction these tests exist to protect is DEMOTE vs
|
||||
FILTER. The operator was explicit:
|
||||
|
||||
"the failure is pollution, not existence"
|
||||
The distinction these tests exist to protect is DEMOTE vs FILTER: the failure
|
||||
is pollution, not existence.
|
||||
|
||||
Hiding a superseded record would turn every one of them into something you must
|
||||
already know exists in order to find, and would destroy "what did we think
|
||||
@@ -131,11 +129,10 @@ async def test_an_empty_candidate_set_short_circuits():
|
||||
def test_the_penalty_is_sized_to_reorder_a_cluster_not_shuffle_within_it():
|
||||
"""Sized against a measurement, not by feel.
|
||||
|
||||
Measured 2026-08-07 (#2486): the notes competing on a dev-log's own title
|
||||
phrase sat within ~0.014 of each other, spanning 0.6506 down to 0.6120. A
|
||||
penalty smaller than that spread would move a record within a tie without
|
||||
changing which one wins — the failure #2486 already proved cannot be tuned
|
||||
away, because the neighbours are not barely passing, they are tied.
|
||||
Notes competing on a dev-log's own title phrase sit within ~0.014 of each
|
||||
other (0.6506 down to 0.6120 on one measured cluster). A penalty smaller
|
||||
than that spread moves a record within a tie without changing which one
|
||||
wins — a failure that cannot be tuned away, because the neighbours are not barely passing, they are tied.
|
||||
|
||||
The upper bound is the operator's constraint, not an optimisation: a penalty
|
||||
large enough to bury a superseded record outright is hiding by another name.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Rulings are counted where someone will read them (#4769).
|
||||
"""Rulings are counted where someone will read them.
|
||||
|
||||
`system_usage_events` was written from milestone 444 step 4 and read by
|
||||
nothing. These pin the two readouts:
|
||||
`system_usage_events` is read in two places, and these pin both readouts:
|
||||
|
||||
- PER SYSTEM: `usage_for_systems` — one GROUP BY, a zero shape for a System
|
||||
nothing touched — carried by the Systems list, the REST detail and MCP
|
||||
@@ -10,7 +9,7 @@ nothing. These pin the two readouts:
|
||||
the areas named, and deliberately NO pull-through ratio.
|
||||
|
||||
Integration for both aggregates, not mocked: a GROUP BY the database rejects
|
||||
is swallowed by the fail-open guard and reads as zero, which is #2663 exactly.
|
||||
is swallowed by the fail-open guard and reads as zero.
|
||||
"""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""A task's claim — which session is working it, and how it dies (milestone 381 step 2).
|
||||
"""A task's claim — which session is working it, and how it dies.
|
||||
|
||||
WHAT THIS PINS
|
||||
|
||||
1. **The write that is the work stamps it.** Reaching `in_progress` — by
|
||||
update OR by a create that names it (#3683) — claims the task for whoever
|
||||
update OR by a create that names it — claims the task for whoever
|
||||
made the change. Nothing asks the model to claim anything.
|
||||
2. **Ending or un-starting the work releases it**, and releasing is
|
||||
idempotent.
|
||||
@@ -287,8 +287,8 @@ async def test_ending_a_session_releases_only_that_sessions_claims(users):
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_a_collaborator_with_write_access_can_log_and_so_claims(users):
|
||||
"""create_log used to filter on the task's OWNER (a rule #78 violation), so
|
||||
a collaborator on a shared task could not log on it — nor, since logging
|
||||
"""create_log filters on who may write the TASK, not on its owner, so a
|
||||
collaborator on a shared task can log on it — nor, since logging
|
||||
stamps the claim, ever be seen working it."""
|
||||
from scribe.services import sharing, task_logs
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"""The work log is READABLE from the tools an agent opens a task with (#4241).
|
||||
"""The work log is READABLE from the tools an agent opens a task with.
|
||||
|
||||
`add_task_log` wrote to a surface no agent could read back: the entries
|
||||
reached the web UI through routes/task_logs.py and nothing else. So a session
|
||||
opening a task saw only the body — a claim written once, before the work —
|
||||
with the record written during it invisible beside it. A stale body had
|
||||
nothing to contradict it, and shipped work got rebuilt.
|
||||
A session that opens a task and sees only the body sees a claim written once,
|
||||
before the work, with the record written during it invisible beside it. A
|
||||
stale body has nothing to contradict it, and shipped work gets rebuilt.
|
||||
|
||||
These pin the read, not the write. The payload shape is tested against the
|
||||
real `work_log_payload`; the three tools are tested by re-patching the arm
|
||||
@@ -235,7 +233,7 @@ async def test_log_limit_zero_asks_for_every_entry_and_skips_the_count_query():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_task_docstring_tells_a_reader_the_log_outranks_the_body():
|
||||
"""#2846: the docstring IS the agent-facing contract. A `work_log` key
|
||||
"""The docstring IS the agent-facing contract. A `work_log` key
|
||||
that nothing tells the reader to prefer over a stale body is the same
|
||||
failure one layer up."""
|
||||
doc = (get_task.__doc__ or "").lower()
|
||||
@@ -305,7 +303,7 @@ def test_brief_row_without_a_mapping_is_unchanged():
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The service read — scoped by who may read the TASK (rule #78)
|
||||
# The service read — scoped by who may read the TASK
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -332,7 +330,7 @@ async def test_logs_for_task_is_not_filtered_by_who_wrote_the_entry():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logs_for_task_refuses_a_task_the_caller_cannot_read():
|
||||
"""Unscoped would have been the mirror-image hole: rule #78 is about
|
||||
"""Unscoped would be the mirror-image hole: access control is about
|
||||
routing the question through the access layer, in both directions."""
|
||||
opened = MagicMock()
|
||||
with patch("scribe.services.task_logs.can_read_note",
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
"""One seam attaches `usage`, and every door that shows it uses that seam (#4230).
|
||||
"""One seam attaches `usage`, and every door that shows it uses that seam.
|
||||
|
||||
WHAT WENT WRONG. `usage_for_notes` is named for notes and works on every note
|
||||
row. Yet the surfaced-vs-opened chip reached snippets and rules only: notes had
|
||||
it nowhere, and lessons had it collected but shown nowhere a person could
|
||||
reach, because the only lesson LIST in the UI is the Knowledge browse and that
|
||||
route never attached it.
|
||||
|
||||
The cause was not any one missing line. Seven call sites carried their own copy
|
||||
of the same few lines — two REST lists, two REST details, two MCP lists, one
|
||||
MCP detail — and each read perfectly well on its own. Nobody was comparing
|
||||
them, so "which doors attach usage?" had no answer anywhere in the code. That
|
||||
is the same failure `test_system_tagging_door_parity.py` records for System
|
||||
tagging (#4249): whichever door nobody exercised for a kind is the one that
|
||||
never grew the feature, and a human reviewer does not reliably catch it because
|
||||
each door is only ever read alone.
|
||||
WHY. `usage_for_notes` is named for notes and works on every note row, and
|
||||
seven doors show its result — two REST lists, two REST details, two MCP
|
||||
lists, one MCP detail. Seven copies of the same few lines each read perfectly
|
||||
well on their own, and nobody compares them, so "which doors attach usage?"
|
||||
has no answer anywhere in the code. That is the same failure
|
||||
`test_system_tagging_door_parity.py` records for System tagging: whichever
|
||||
door nobody exercises for a kind is the one that
|
||||
never grows the feature, and a human reviewer does not reliably catch it
|
||||
because each door is only ever read alone.
|
||||
|
||||
So this file asserts the PROPERTY, not the behaviour of one route: the attach
|
||||
logic exists once, and no door re-implements it. A kind added next month either
|
||||
@@ -156,7 +151,7 @@ def test_no_door_calls_the_aggregate_directly() -> None:
|
||||
|
||||
Keyed on the CALL, not on the text, so a module that merely names the
|
||||
function in a comment explaining the seam is not a false positive — and a
|
||||
hand-kept skip list, which would itself go stale, is not needed (rule 167).
|
||||
hand-kept skip list, which would itself go stale, is not needed.
|
||||
"""
|
||||
offenders = []
|
||||
for path in _door_modules():
|
||||
|
||||
Reference in New Issue
Block a user