Scribe is renamed Fabled Grimoire #212
@@ -99,14 +99,13 @@ def test_a_sentence_about_a_definition_is_not_a_definition(repo):
|
||||
assert "in 1 other file(s)" in out
|
||||
|
||||
|
||||
def test_the_repos_own_prose_no_longer_defines_with():
|
||||
"""Not a fixture — this repo's actual corpus, which is where the defect
|
||||
was found.
|
||||
def test_the_repos_own_prose_does_not_define_with():
|
||||
"""Not a fixture — this repo's actual corpus, the realistic case.
|
||||
|
||||
Before the confirmation pass the arm named four files for `with`:
|
||||
`…record type with no vector…`, `A class with no rules anywhere…` and two
|
||||
more, all of them prose in docstrings and comments. It now names none,
|
||||
and none is the correct answer: nothing here defines a symbol called
|
||||
Without the confirmation pass the arm names files for `with` on prose in
|
||||
docstrings and comments — `…record type with no vector…`, `A class with
|
||||
no rules anywhere…`. With it the arm names none, and none is the correct
|
||||
answer: nothing here defines a symbol called
|
||||
`with`, and in several of the languages in this tree it could not, because
|
||||
it is a keyword.
|
||||
|
||||
|
||||
@@ -150,8 +150,9 @@ async def test_a_repeat_is_not_recorded_as_a_fresh_surfacing():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_header_no_longer_promises_once_per_session():
|
||||
"""A contract stated in the prose is a contract, and this one changed.
|
||||
async def test_the_header_explains_the_seen_marker():
|
||||
"""A contract stated in the prose is a contract: the header explains the
|
||||
`seen` marker and does not promise a record once per session.
|
||||
|
||||
Cheap to forget and invisible when wrong: the menu would carry a marker
|
||||
the header had never explained, and a reader would have to guess.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""A lesson's name is one claim on one line (#4797).
|
||||
"""A lesson's name is one claim on one line.
|
||||
|
||||
`what` is the title every listing and menu prints. Written as the incident
|
||||
instead of the claim, it turned a menu line into 1,416 characters with the
|
||||
claim somewhere in the middle. Two halves are pinned: the doors refuse a new
|
||||
instead of the claim, it turns a menu line into a paragraph with the claim
|
||||
somewhere in the middle. Two halves are pinned: the doors refuse a new
|
||||
one, and the menu shows one line for a name stored before they did.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Convergence named at the write (milestone 440, #4634).
|
||||
"""Convergence named at the write.
|
||||
|
||||
When a lesson is answered "no rule fits", the response looks for other
|
||||
no-rule lessons in the same situation and, once there are enough, names the
|
||||
@@ -53,7 +53,7 @@ def test_one_incident_written_up_three_times_is_not_a_recurring_situation():
|
||||
assert links_svc.convergence_group(same) is None
|
||||
|
||||
|
||||
# ── The bar is relative to each lesson's own register (#5193) ──────────────
|
||||
# ── The bar is relative to each lesson's own register ──────────────────────
|
||||
|
||||
|
||||
def test_too_little_background_says_nothing_stands_out():
|
||||
@@ -138,7 +138,7 @@ async def test_lessons_that_stand_out_together_are_named():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_large_no_rule_pool_in_one_flat_band_names_nothing():
|
||||
"""The #5193 failure: every lesson answered "no rule", every score above
|
||||
"""The flat-band failure: every lesson answered "no rule", every score above
|
||||
a fixed bar, and a group the size of the pool. Nothing stands out of a
|
||||
flat band, so nothing is named however large the pool grows."""
|
||||
a, b, c = _run(no_rule=set(_NOTES), sim=lambda x, y: 0.66 + 0.01 * ((x + y) % 5))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The lesson → rule link at its doors (milestone 440 step 1, #4630).
|
||||
"""The lesson → rule link at its doors.
|
||||
|
||||
The link's own behaviour — states, cascade, ACL — is tested against Postgres in
|
||||
tests/test_integration_lesson_rule_links.py. These pin the wiring a mock CAN
|
||||
@@ -112,7 +112,7 @@ def test_verdicts_name_real_states():
|
||||
|
||||
|
||||
def test_the_migration_check_and_the_model_agree_on_the_states():
|
||||
"""Rule 36's drift, guarded: the CHECK is written from the migration's
|
||||
"""Model/migration drift, guarded: the CHECK is written from the migration's
|
||||
tuple and the code from the model's, so they must be the same tuple."""
|
||||
path = Path(__file__).resolve().parents[1] / "alembic" / "versions" / "0111_lesson_rule_links.py"
|
||||
spec = importlib.util.spec_from_file_location("m0111", path)
|
||||
@@ -145,7 +145,7 @@ def test_backup_keeps_an_unjudged_link_unjudged():
|
||||
assert built.judged_at is None
|
||||
|
||||
|
||||
# ── #4631: judged when written ───────────────────────────────────────────────
|
||||
# ── judged when written ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Both records show the lesson → rule link in the web UI (milestone 440, #4635).
|
||||
"""Both records show the lesson → rule link in the web UI.
|
||||
|
||||
The frontend has no component test runner, so these read the source — and
|
||||
each is written against something the backend owns, so a drift on either
|
||||
@@ -122,7 +122,7 @@ def test_no_page_spells_its_own_write_check():
|
||||
assert offenders == []
|
||||
|
||||
|
||||
# ── #4658: the web editor records the answer ─────────────────────────────────
|
||||
# ── the web editor records the answer ────────────────────────────────────────
|
||||
|
||||
|
||||
def _payload_fn(view: str) -> str:
|
||||
@@ -131,7 +131,7 @@ def _payload_fn(view: str) -> str:
|
||||
|
||||
|
||||
def test_the_editor_sends_the_answer_under_the_names_both_routes_read():
|
||||
"""Rule 33's contract check: a field the form sends under another name is
|
||||
"""The contract check: a field the form sends under another name is
|
||||
dropped without an error, and the lesson saves unjudged."""
|
||||
from scribe.routes import lessons as routes
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Soft links — the counting rules (milestone 440, #4637).
|
||||
"""Soft links — the counting rules.
|
||||
|
||||
What a suggested link's evidence counts is a design decision with a failure
|
||||
mode on each side: count every arrival and two similar records "prove" a
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""List tools return rows that say what a record is, never what it says (#4061).
|
||||
"""List tools return rows that say what a record is, never what it says.
|
||||
|
||||
list_tasks once returned every row's to_dict(), body included. A project's todo
|
||||
list came to 93-165k characters, past what an MCP client accepts as a tool
|
||||
result, so the list arrived as a file to page through — the failure #4045 fixed
|
||||
for enter_project, one call over. A list is for choosing which record to open;
|
||||
A list that returns every row's to_dict(), body included, runs a project's todo
|
||||
list to 93-165k characters, past what an MCP client accepts as a tool result,
|
||||
so the list arrives as a file to page through — the same failure enter_project
|
||||
avoids by carrying a summary. A list is for choosing which record to open;
|
||||
get_task / get_note read one in full.
|
||||
"""
|
||||
import json
|
||||
@@ -41,7 +41,7 @@ def test_a_task_row_names_what_it_is_and_where_it_sits_but_not_its_body():
|
||||
assert row["milestone_title"] == "An existing plan is found"
|
||||
assert {"id", "title", "status", "task_kind", "priority", "milestone_id",
|
||||
"tags", "updated_at", "project_id"} <= set(row)
|
||||
# Empty optional fields are left off rather than sent as null (#2483).
|
||||
# Empty optional fields are left off rather than sent as null.
|
||||
assert not {"description", "parent_id", "due_date"} & set(row)
|
||||
assert row["updated_at"] == WHEN.isoformat()
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Every semantic search hands on the passage that matched (#4243, #4250).
|
||||
"""Every semantic search hands on the passage that matched.
|
||||
|
||||
Three searches collapse chunk rows to the best one per record, so each of them
|
||||
KNOWS which passage earned the hit. Each used to drop it, leaving every door to
|
||||
preview the head of the document instead — a span the search had already scored
|
||||
KNOWS which passage earned the hit. A search that drops it leaves every door to
|
||||
preview the head of the document instead — a span the search has already scored
|
||||
lower, with nothing saying so.
|
||||
|
||||
These pin the mechanism (`report["best_chunk"]` from all three searches) and
|
||||
@@ -26,7 +26,7 @@ from tests.helpers import make_mock_session
|
||||
|
||||
def _searching(rows):
|
||||
"""A patched session whose one query returns `rows` — the shared
|
||||
make_mock_session (#2834) rather than a third local copy of the
|
||||
make_mock_session rather than a third local copy of the
|
||||
__aenter__/__aexit__ dance."""
|
||||
session = make_mock_session()
|
||||
result = MagicMock()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Cross-family contracts for the `list_*` MCP tools (#2278, shape 4).
|
||||
"""Cross-family contracts for the `list_*` MCP tools.
|
||||
|
||||
Per-tool tests cover what each list tool does. Nothing covered what the FAMILY
|
||||
owes its callers, which is where the missing-sibling shape hides: a capability
|
||||
|
||||
@@ -110,7 +110,7 @@ async def test_a_real_tools_refusal_reaches_the_caller():
|
||||
assert "at least one verdict" in str(exc.value)
|
||||
|
||||
|
||||
async def test_the_original_regression_create_note_with_content():
|
||||
async def test_the_real_server_refuses_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."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The batch create door, and the guessed-id refusal at every create/update door (#4016).
|
||||
"""The batch create door, and the guessed-id refusal at every create/update door.
|
||||
|
||||
What the unit lane can pin: the door refuses a guessed id BEFORE the duplicate
|
||||
gate or any write, a batch is refused whole, and a malformed record fails with
|
||||
|
||||
@@ -181,8 +181,8 @@ async def test_resolve_returns_serialised_tokens_with_their_provenance():
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_token_can_clear_supersedes_with_an_empty_list():
|
||||
"""`[]` means "this token replaces nothing after all" — a real edit. Guarded
|
||||
on `is not None` so it isn't swallowed as "unchanged", the same trap #2077
|
||||
recorded for update_snippet."""
|
||||
on `is not None` so it isn't swallowed as "unchanged", the same trap
|
||||
update_snippet guards against."""
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.update_token = AsyncMock(return_value=_fake_token())
|
||||
from scribe.mcp.tools.design_systems import update_design_token
|
||||
@@ -203,7 +203,7 @@ async def test_update_token_leaves_supersedes_alone_when_omitted():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_design_system_carries_the_chain_merged_guidance():
|
||||
"""enter_project carries only a summary and points here (#4045). The
|
||||
"""enter_project carries only a summary and points here. The
|
||||
system's own `guidance` field is only its departures; a session building
|
||||
UI needs the house style too, so the merged form rides alongside."""
|
||||
from scribe.mcp.tools.design_systems import get_design_system
|
||||
|
||||
@@ -126,7 +126,7 @@ async def test_get_milestone_returns_body_steps_and_rules():
|
||||
assert out["milestone"]["body"] == "## Goal"
|
||||
assert out["milestone"]["total"] == 1
|
||||
assert [(s["id"], s["title"], s["status"]) for s in out["steps"]] == [(9, "step 1", "todo")]
|
||||
# A step's body is get_task's job (#4061); its placement is not.
|
||||
# A step's body is get_task's job; its placement is not.
|
||||
assert "body" not in out["steps"][0]
|
||||
assert out["steps"][0]["milestone_title"] == m.title
|
||||
assert out["applicable_rules"] == [{"id": 1, "title": "r"}]
|
||||
|
||||
@@ -32,9 +32,8 @@ async def test_start_planning_tool_delegates_to_service():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_planning_returns_the_plan_that_already_covers_it():
|
||||
"""The FabledLibrarian failure (milestone 415): a session that could not see
|
||||
the existing plan made a second one. Now it is handed the first, and
|
||||
nothing is created."""
|
||||
"""A session that cannot see the existing plan makes a second one. It is
|
||||
handed the first instead, and nothing is created."""
|
||||
match = {"duplicate": True, "existing_id": 12}
|
||||
svc = AsyncMock()
|
||||
with patch("scribe.mcp.tools.tasks.dedup_svc.plan_gate",
|
||||
|
||||
@@ -36,7 +36,7 @@ async def test_create_process_sets_note_type():
|
||||
async def test_create_process_blocks_a_near_duplicate():
|
||||
"""The gate matters more for processes than for other kinds: each one becomes
|
||||
a skill file that auto-surfaces, so two near-identical procedures don't just
|
||||
bloat the corpus — they compete to be followed (#2250)."""
|
||||
bloat the corpus — they compete to be followed."""
|
||||
from scribe.services.dedup import DuplicateMatch
|
||||
|
||||
# The real dataclass, not a MagicMock: a mock answers every attribute, so it
|
||||
@@ -166,7 +166,7 @@ async def test_delete_process_refuses_a_plain_note():
|
||||
mock_delete.assert_not_awaited()
|
||||
|
||||
|
||||
# ── the moments a procedure is for (milestone 458 step 5) ────────────────
|
||||
# ── the moments a procedure is for ───────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_process_stores_the_moments_it_is_for():
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""update_task closes a task with a one-line reminder of what the report needs.
|
||||
|
||||
The in-band half of milestone 409 step 3: the skill and static context only
|
||||
This is the in-band half of reporting back: the skill and static context only
|
||||
exist in the Claude Code plugin, and a tool response reaches every MCP client
|
||||
at the moment a piece of work closes. Pinned on the response, not the wording.
|
||||
|
||||
The operator's own completion-report preferences used to ride here too, from
|
||||
a fixed-question search (409 step 4). Milestone 500 retired that: they are
|
||||
The operator's own completion-report preferences do not ride here: they are
|
||||
mounted on finishing work and arrive in `moment_rules` beside the delivered
|
||||
reply shape, so this response carries no key of its own for them.
|
||||
"""
|
||||
@@ -55,7 +54,7 @@ _OWED = {"idea_id": 9, "idea_title": "Signed APK lane", "project_id": 4,
|
||||
|
||||
|
||||
async def test_closing_names_the_owed_adoptions_filed_while_the_task_was_open():
|
||||
"""The finishing moment of milestone 463 step 6: owed family work filed
|
||||
"""The finishing moment of family canon: owed family work filed
|
||||
into a project during this task comes back for the report to name."""
|
||||
from scribe.services.family_adoption import OWED_CUE
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ async def test_delete_snippet_retires_or_raises():
|
||||
|
||||
doomed = MagicMock()
|
||||
doomed.title = "debounce helper"
|
||||
# The confirmation names what went (#3273) — after the delete there is
|
||||
# The confirmation names what went — after the delete there is
|
||||
# nothing left to look the title up from.
|
||||
with patch("scribe.services.snippets.delete_snippet", AsyncMock(return_value=True)), \
|
||||
patch("scribe.services.snippets.get_snippet", AsyncMock(return_value=doomed)):
|
||||
|
||||
@@ -37,7 +37,7 @@ async def test_list_tasks_kind_empty_means_no_filter():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_passes_spike():
|
||||
"""The kind a failed rule-check asks for (milestone 312).
|
||||
"""The kind a failed rule-check asks for.
|
||||
|
||||
Time-boxed, and its output is knowledge rather than a change — filing one
|
||||
as `work` makes a finished investigation look like an abandoned change.
|
||||
@@ -51,12 +51,12 @@ async def test_create_task_passes_spike():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_task_can_re_file_a_kind():
|
||||
"""A task's kind must be CORRECTABLE, not write-once (#3129).
|
||||
"""A task's kind must be CORRECTABLE, not write-once.
|
||||
|
||||
What a piece of work turns out to be is often clear only once it is under
|
||||
way. Before this, `update_task` had no `kind` parameter at all and the
|
||||
REST route dropped the field the editor was already sending — the save
|
||||
reported success and reverted on reload.
|
||||
way. Without a `kind` parameter on `update_task`, and with a REST route that
|
||||
drops the field the editor sends, the save reports success and reverts on
|
||||
reload.
|
||||
"""
|
||||
mock = AsyncMock(return_value=fake_note(task_kind="spike"))
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.update_note", mock):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The DRY close-out measure (#4745).
|
||||
"""The DRY close-out measure.
|
||||
|
||||
Stdlib-only and kept in scripts/ so any project can run a scratch copy of it,
|
||||
so these tests import it by path rather than as a package.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Which actions reach which moment (milestone 458 step 2) — the pure half.
|
||||
"""Which actions reach which moment — the pure half.
|
||||
|
||||
Matching and resolution take no database: the defaults are code and an
|
||||
install's mappings arrive as rows, so every case below hands `resolve` the
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The moment catalog — the vocabulary rules mount on (milestone 458 step 1).
|
||||
"""The moment catalog — the vocabulary rules mount on.
|
||||
|
||||
What this pins is what a mount depends on: every name is well-formed and
|
||||
resolvable, an unknown name is refused rather than stored, the catalog speaks
|
||||
@@ -16,7 +16,7 @@ _NAME = re.compile(r"^[a-z]+\.[a-z]+$")
|
||||
|
||||
|
||||
def test_the_catalog_is_not_empty():
|
||||
"""The sweeps below are vacuous over an empty catalog (rule 167)."""
|
||||
"""The sweeps below are vacuous over an empty catalog."""
|
||||
assert len(moments.MOMENTS) >= 10
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ def test_the_tools_are_registered_and_classified():
|
||||
|
||||
|
||||
def test_both_doors_read_one_catalog():
|
||||
"""Rule 33 parity: the tool and the routes call the same services, so the
|
||||
"""Door parity: the tool and the routes call the same services, so the
|
||||
session and the Settings view cannot name different moments or disagree
|
||||
about what a mapping does."""
|
||||
from scribe.mcp.tools import moments as tool
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""A record the operator names by number is looked up, not left to ranking (#4796).
|
||||
"""A record the operator names by number is looked up, not left to ranking.
|
||||
|
||||
"yes go ahead with 4448" says exactly which record is meant, and the prompt
|
||||
menu could not use it: a number means nothing to an embedding, so the menu
|
||||
filled with whatever resembled the words around it. The parser cases below are
|
||||
drawn from real operator prompts, and the half that must NOT be read as a
|
||||
"yes go ahead with 4448" says exactly which record is meant, and a menu
|
||||
built by ranking cannot use it: a number means nothing to an embedding, so the
|
||||
menu fills with whatever resembles the words around it. The parser cases below
|
||||
are drawn from real operator prompts, and the half that must NOT be read as a
|
||||
reference matters as much as the half that must: every misread number is a
|
||||
stranger's record dropped in front of the reader.
|
||||
"""
|
||||
@@ -181,8 +181,8 @@ async def test_an_id_the_reader_cannot_open_is_dropped():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_number_in_the_assistants_reply_is_not_named():
|
||||
"""The query is enriched with the reply before a short prompt is searched
|
||||
(#4364); only the operator's own words can name a record."""
|
||||
"""The query is enriched with the reply before a short prompt is searched;
|
||||
only the operator's own words can name a record."""
|
||||
rec = _task(4448, "the named one")
|
||||
out, _, _ = await _hint(
|
||||
"sounds good", records={4448: rec},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Platforms without a database (milestone 463 step 2).
|
||||
"""Platforms without a database.
|
||||
|
||||
Detection is pure — a catalog and a list of repo paths in, platform ids out —
|
||||
so it is pinned here against fixture trees. What it writes, and what it must
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The PreCompact hook steers the summary and never blocks the compaction (#3680).
|
||||
"""The PreCompact hook steers the summary and never blocks the compaction.
|
||||
|
||||
The mechanism this pins was read out of the installed Claude Code build, not
|
||||
out of the documentation, which describes a different one. Three facts decide
|
||||
@@ -49,7 +49,7 @@ EVENT = {"session_id": "s1", "transcript_path": "/tmp/t.jsonl", "cwd": "/repo",
|
||||
def _run(event: dict, tmpdir: str | None = None) -> subprocess.CompletedProcess:
|
||||
"""Run the hook with its ledger directory ISOLATED.
|
||||
|
||||
The hook reads this session's rule ledgers since #4216, and they live under
|
||||
The hook reads this session's rule ledgers, and they live under
|
||||
`$TMPDIR/scribe-priorart`. Without an override these tests would read the
|
||||
machine's real /tmp: on a developer box mid-session that is not empty, and
|
||||
the output would depend on what some other session happened to leave
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
THE PROBLEM THIS IS ABOUT. The agent has the record open; the operator does
|
||||
not. `#3244` reads as complete to the writer and as homework to the reader,
|
||||
who has to look it up to know what their own conversation is about. The
|
||||
operator's words: *"I don't know what a note, task, or milestone is by its ID
|
||||
number."*
|
||||
who has to look it up to know what their own conversation is about. Nobody
|
||||
knows what a note, task or milestone is by its id number.
|
||||
|
||||
PRODUCT, NOT A RULE (rule 119). Every Scribe user hits this, so the fix is in
|
||||
PRODUCT, NOT A RULE. Every Scribe user hits this, so the fix is in
|
||||
the surfaces the product ships — the skill that shapes how an agent writes,
|
||||
and the tool responses that hand a record back. A per-instance rule would fix
|
||||
it for one operator and leave the behaviour wrong for everyone else.
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Tests for task lifecycle timestamps and recurrence logic.
|
||||
|
||||
The note stand-ins come from `fake_note`, not a bare MagicMock. These four
|
||||
predated that helper and broke the moment `update_note` started reading a
|
||||
column they did not set (milestone 317): a fresh MagicMock is truthy on every
|
||||
attribute, so `verify_with` read as "this record carries a check" and the
|
||||
guard refused the write. That is note 2109's lesson, and the reason fake_note
|
||||
exists — a stand-in has to be able to say NO.
|
||||
The note stand-ins come from `fake_note`, not a bare MagicMock. A fresh
|
||||
MagicMock is truthy on every attribute, so once `update_note` reads a column
|
||||
the stand-in did not set, `verify_with` reads as "this record carries a check"
|
||||
and the guard refuses the write. That is the reason fake_note exists — a
|
||||
stand-in has to be able to say NO.
|
||||
"""
|
||||
from datetime import date, datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""The reply moment: the finished reply, checked before it goes out (milestone
|
||||
458 step 4, folded in from milestone 456 step 8).
|
||||
"""The reply moment: the finished reply, checked before it goes out.
|
||||
|
||||
The operator's ruling: an unopened rule above the stop bar HOLDS the reply
|
||||
An unopened rule above the stop bar HOLDS the reply
|
||||
once, in the server's words — the backstop for whatever the earlier arms
|
||||
missed — and the rewrite is never held. These pin, with the database stubbed:
|
||||
- which moments a reply reaches;
|
||||
@@ -228,7 +227,7 @@ async def test_a_turn_that_closed_nothing_is_not_section_checked():
|
||||
|
||||
|
||||
async def test_a_closing_turn_is_section_checked_and_both_holds_fold_into_one_reason():
|
||||
"""One end-of-turn request (milestone 500 step 4): the section check and
|
||||
"""One end-of-turn request: the section check and
|
||||
the rule hold answer together, in one `reason`."""
|
||||
body, _hold, check = await _reply_route(
|
||||
{"reply": "done", "closed": 1, "closed_task_ids": [41, True, "x"], "rewrite": False},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The core reply shape rides every turn (milestone 500 step 3).
|
||||
"""The core reply shape rides every turn.
|
||||
|
||||
Every turn ends in a reply, and the operator's prompt is the last point before
|
||||
it is written, so the per-turn retrieval carries the core shape ahead of
|
||||
@@ -119,7 +119,7 @@ def test_the_core_goes_whole_once_then_as_a_reminder_then_whole_after_a_compacti
|
||||
|
||||
|
||||
def test_the_hook_and_the_route_agree_on_the_ledgers_names():
|
||||
"""Rule 33 across the seam: a renamed field fails silently."""
|
||||
"""A contract across the seam: a renamed field fails silently."""
|
||||
from scribe.routes import plugin as routes
|
||||
|
||||
hook = HOOK.read_text()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Carrying a floor across a calibration change (#4104).
|
||||
"""Carrying a floor across a calibration change.
|
||||
|
||||
WHAT THIS PINS
|
||||
|
||||
@@ -15,7 +15,7 @@ WHAT THIS PINS
|
||||
mid-backfill would end up with every bar at zero.
|
||||
4. **Every registry surface can be migrated.** A seventh arm that nobody adds
|
||||
a re-scorer for is one whose floor silently cannot survive a model change.
|
||||
5. **A half-migrated corpus is a refusal (#4132).** While any row the surface
|
||||
5. **A half-migrated corpus is a refusal.** While any row the surface
|
||||
searches is stamped with another model, a re-score measures a blend of two
|
||||
geometries — so nothing is sampled until the backfill has finished.
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The review pass: judging whether an injected line RELATED (#4772).
|
||||
"""The review pass: judging whether an injected line RELATED.
|
||||
|
||||
Unit tests pin the refusals and the re-run's fidelity to the arm it replays.
|
||||
Integration tests pin the parts a mock would make true by construction: which
|
||||
@@ -60,7 +60,7 @@ async def test_the_re_run_searches_the_way_the_arm_does():
|
||||
a menu nobody was shown. Both searches are RUN and their keywords
|
||||
compared, so a change to the arm's spec fails here until the review
|
||||
follows it — and the arm has to have filters for the check to mean
|
||||
anything (rule 167)."""
|
||||
anything."""
|
||||
from scribe.services import retrieval_pipeline as rp
|
||||
|
||||
arm_kw: dict = {}
|
||||
|
||||
@@ -167,8 +167,8 @@ async def test_injected_menu_labels_the_record_kind():
|
||||
assert "[snippet]" in by_id[1]
|
||||
assert "[process]" in by_id[2]
|
||||
# Task-ness wins over note_type, and an issue says so rather than "task".
|
||||
# A task also carries its status (#4154): #4 is DONE, and a line that said
|
||||
# only "[task]" is what let a finished step be cited as an open one.
|
||||
# A task also carries its status: #4 is DONE, and a line that says only
|
||||
# "[task]" lets a finished step be cited as an open one.
|
||||
assert "[issue (todo)]" in by_id[3]
|
||||
assert "[task (done)]" in by_id[4]
|
||||
assert "[note]" in by_id[5]
|
||||
@@ -181,7 +181,7 @@ async def test_injected_menu_labels_the_record_kind():
|
||||
def test_list_shaped_services_use_the_shared_visibility_clause():
|
||||
"""Every service that LISTS notes must declare its reach through
|
||||
notes_visibility_clause / browsable_notes_clause — never a bare owner
|
||||
filter (#47, #2462).
|
||||
filter.
|
||||
|
||||
Source inspection, because this is the shape no behavioural test catches:
|
||||
an owner-only list returns correct-looking rows and simply omits the shared
|
||||
@@ -204,7 +204,7 @@ def test_list_shaped_services_use_the_shared_visibility_clause():
|
||||
or "browsable_notes_clause" in source
|
||||
), (
|
||||
f"{label} does not reference a shared visibility clause — a bare "
|
||||
f"owner filter silently hides shared-project records (#2462)"
|
||||
f"owner filter silently hides shared-project records"
|
||||
)
|
||||
# And it must not ALSO carry a bare owner filter on the main query,
|
||||
# which would quietly re-narrow whatever the clause granted.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""The specs are the registry (milestone 456 step 5).
|
||||
"""The specs are the registry.
|
||||
|
||||
Three lists used to be kept by hand beside the arms — the tuning table, the
|
||||
ranked rows of the telemetry registry, and the rule-usage denominator — with
|
||||
tests to make them agree. They are now read off the pipeline's specs. These
|
||||
Three lists sit beside the arms — the tuning table, the ranked rows of the
|
||||
telemetry registry, and the rule-usage denominator — and all three are read
|
||||
off the pipeline's specs rather than kept by hand. These
|
||||
pin the derivation itself: every arm is tunable, every ranked source is
|
||||
measured with the declaration its spec carries, and the rule-usage
|
||||
denominator counts exactly the rule surfaces a ranker chose.
|
||||
@@ -34,7 +34,7 @@ def test_every_ranked_source_is_measured_as_its_spec_declares():
|
||||
point = POINTS[spec.source]
|
||||
assert point.kind == UNBIDDEN
|
||||
assert point.what == d.what
|
||||
# A quiet source must say why, and only a quiet source may (#2475).
|
||||
# A quiet source must say why, and only a quiet source may.
|
||||
assert point.expects_traffic == (not d.quiet_because)
|
||||
assert point.quiet_because == d.quiet_because
|
||||
|
||||
@@ -59,7 +59,7 @@ def test_the_rule_denominator_is_every_rule_surface_a_ranker_chose():
|
||||
|
||||
|
||||
def test_an_arm_added_without_tuning_would_be_caught():
|
||||
"""Rule 167: the first test has to be able to bite. SURFACES skips an arm
|
||||
"""The first test has to be able to bite. SURFACES skips an arm
|
||||
with no tuning rather than raising, so an arm added without one would
|
||||
silently be untunable — and the order-equality assertion is what notices,
|
||||
as this replays with one such arm appended."""
|
||||
|
||||
@@ -77,11 +77,11 @@ def test_service_functions_take_user_id():
|
||||
|
||||
def test_agent_and_web_surfaces_stay_at_parity():
|
||||
"""The MCP tools and the REST routes are two callers of one service; a
|
||||
capability on one has to exist on the other (rule #33).
|
||||
capability on one has to exist on the other.
|
||||
|
||||
This guard exists because the snippet surfaces drifted apart once — MCP had
|
||||
no delete, the web side had no near-duplicate gate — and neither failed
|
||||
anything until someone went looking.
|
||||
Two surfaces drift apart without failing anything — one door with no
|
||||
delete, the other with no near-duplicate gate — until someone goes
|
||||
looking.
|
||||
"""
|
||||
from scribe.mcp.tools import design_systems as tools
|
||||
from scribe.routes import design_systems as routes
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Structural tests for the family blueprint (milestone 463 steps 3 and 4) — every
|
||||
"""Structural tests for the family blueprint — every
|
||||
endpoint is routed, and every write hands the service its caller, where the
|
||||
note's write gate lives. What the writes do is in
|
||||
tests/test_integration_family_promotion.py."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Structural tests for the platforms blueprint (milestone 463 step 2) —
|
||||
"""Structural tests for the platforms blueprint —
|
||||
registration and the route-to-service contract. What the writes do is in
|
||||
tests/test_integration_platforms.py."""
|
||||
import inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Both rule-creation tools run the propose-then-approve loop (#3557).
|
||||
"""Both rule-creation tools run the propose-then-approve loop.
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
@@ -38,8 +38,8 @@ prevents a rule earns more than any question that improves one's wording.
|
||||
|
||||
WHAT THIS PINS, AND WHAT IT DOES NOT
|
||||
|
||||
STRUCTURE, never wording — the same bargain the disambiguator guard (#3123)
|
||||
strikes next door. Each element matches a family of synonyms, so the prose
|
||||
STRUCTURE, never wording — the same bargain the disambiguator guard
|
||||
(test_create_tools_disambiguate) strikes next door. Each element matches a family of synonyms, so the prose
|
||||
stays free to be rewritten, reordered or sharpened; only DELETING one fails.
|
||||
Pinning phrasing would make every improvement a red build, and a test that
|
||||
punishes editing is a test someone deletes.
|
||||
@@ -76,7 +76,7 @@ _ELEMENTS = {
|
||||
),
|
||||
"how it would be enforced": ("enforc",),
|
||||
"the answers offered back": ("as written", "talk about it", "discuss"),
|
||||
# The fourth and fifth answers (#3733, #3896). They are what makes the
|
||||
# The fourth and fifth answers. They are what makes the
|
||||
# question a ROUTING question rather than a yes/no one: an observation
|
||||
# that turns out to be a preference or a lesson has found its home, not
|
||||
# failed to become a rule. Written in one pass, because two people each
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Every door that writes a rule can mount it on moments (milestone 458 step 3).
|
||||
"""Every door that writes a rule can mount it on moments.
|
||||
|
||||
THE SHAPE, NOT ONE FUNCTION. test_system_tagging_door_parity records how a
|
||||
capability goes missing: whichever door nobody exercised for a kind never
|
||||
@@ -80,7 +80,7 @@ def test_every_rest_rule_door_takes_moments_and_checks_them_first(handler, write
|
||||
|
||||
|
||||
def test_the_guard_can_fail():
|
||||
"""A door with no check at all must be reported, not skipped (rule 167)."""
|
||||
"""A door with no check at all must be reported, not skipped."""
|
||||
fn = ast.parse("async def f():\n await svc.create_rule()\n").body[0]
|
||||
assert _first_line_calling(fn, "require_moments") is None
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Moment proposals and judgments — what a mock can see (milestone 458 step 7).
|
||||
"""Moment proposals and judgments — what a mock can see.
|
||||
|
||||
The behaviour against real tables is tests/test_integration_rule_moment_judgments.py.
|
||||
These pin the pieces that need no database: that the migration's CHECKs and
|
||||
the model agree (rule 36), which moments count as evidence, and that the line
|
||||
the model agree, which moments count as evidence, and that the line
|
||||
the signal hands a session names the tool that answers it.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -61,7 +61,7 @@ def test_the_proposal_line_names_the_tool_that_answers_it():
|
||||
assert "judge_rule_moments" in line and '"reject"' in line
|
||||
|
||||
|
||||
# ── The misfire (step 7b) ────────────────────────────────────────────────────
|
||||
# ── The misfire ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_a_day_is_one_situation_and_days_are_distinct():
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The plugin half of the rule↔moment signal (milestone 458 step 7), run for real.
|
||||
"""The plugin half of the rule↔moment signal, run for real.
|
||||
|
||||
`scribe_moment.sh` writes every call to an acts ledger; `scribe_record_opened.sh`
|
||||
sends the last few minutes of it with the rule a session opened; the server
|
||||
@@ -158,7 +158,7 @@ def test_session_end_removes_the_sessions_moment_files_and_no_others(sandbox):
|
||||
|
||||
|
||||
def test_the_hook_and_the_route_agree_on_the_body():
|
||||
"""Rule 33 across the shell/Python seam: a renamed field fails silently."""
|
||||
"""A contract across the shell/Python seam: a renamed field fails silently."""
|
||||
src = (HOOKS / "scribe_record_opened.sh").read_text()
|
||||
route = inspect.getsource(plugin_routes.rule_opened)
|
||||
assert "/api/plugin/rule-opened" in src
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Rule and preference creates surface the records they overlap (#4134).
|
||||
"""Rule and preference creates surface the records they overlap.
|
||||
|
||||
Rules and preferences share one table and one ranking, so a preference that
|
||||
restates a rule splits one instruction in two — and when only the preference
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""A rule cannot be created, or edited into, having no trigger (#4099).
|
||||
"""A rule cannot be created, or edited into, having no trigger.
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
@@ -18,10 +18,10 @@ WHAT THIS PINS
|
||||
The guard lives in the SERVICE, because both doors reach it: the MCP tools and
|
||||
the frontend's fast path in `routes/rulebooks.py`. A guard written in either
|
||||
one alone would leave the other able to create a rule that never fires, which
|
||||
is the failure mode this whole change exists to remove.
|
||||
is the failure mode this guard exists to remove.
|
||||
|
||||
These call the service directly and assert on the refusal, so an empty guard
|
||||
cannot make them pass by accident (rule 167).
|
||||
cannot make them pass by accident.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""A rule reached through its lessons (milestone 440, #4633).
|
||||
"""A rule reached through its lessons.
|
||||
|
||||
The step runs after each of the three rule arms. These pin, with the database
|
||||
and the embedder stubbed: that nothing is searched when no lesson carries a
|
||||
@@ -125,7 +125,7 @@ async def test_a_failure_brings_no_rule_and_raises_nothing():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_step_follows_the_arm_on_its_query_and_skips_what_it_named():
|
||||
"""One composer for all three rule builders (milestone 456 step 6): the
|
||||
"""One composer for all three rule builders: the
|
||||
via-lesson step searches with the query the arm searched with, skips the
|
||||
session's ledger and every rule the arm's band named, and its lines and
|
||||
fresh ids join the arm's — while `shown_rule_ids` stays the direct band."""
|
||||
@@ -153,9 +153,9 @@ async def test_a_blank_prompt_runs_neither_the_arm_nor_the_step():
|
||||
|
||||
|
||||
def test_only_the_composer_runs_a_rule_arm_or_the_via_lesson_step():
|
||||
"""Structural (rule 167): every rule builder goes through `_rule_moment`,
|
||||
so a builder that runs the arm or the via-lesson step by hand again is the
|
||||
copy this step removed coming back — and a count is what lets it fail."""
|
||||
"""Structural: every rule builder goes through `_rule_moment`, so a
|
||||
builder that runs the arm or the via-lesson step by hand is a second copy
|
||||
of the composer — and a count is what lets it fail."""
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""`.scribe` — the second key a directory can be scoped by (#4085).
|
||||
"""`.scribe` — the second key a directory can be scoped by.
|
||||
|
||||
Every hook in `plugin/hooks/` scopes its request to a project, and until this
|
||||
existed all six turned on one key: `git remote get-url origin`. That key does
|
||||
not exist outside a git repo, so a session in a plain directory was unscoped
|
||||
everywhere at once — silently, because a missing remote is indistinguishable
|
||||
Every hook in `plugin/hooks/` scopes its request to a project. With only one
|
||||
key, `git remote get-url origin`, a session outside a git repo has no key at
|
||||
all, so a session in a plain directory is unscoped everywhere at once — silently, because a missing remote is indistinguishable
|
||||
from a remote nobody bound.
|
||||
|
||||
`scribe_scope_query` is the shared answer, so these run the real shell rather
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""/api/search — the kind filter it offers, and what it does with a bad one.
|
||||
|
||||
This route used to carry its own two-value map (`note`/`task`/everything else
|
||||
is `all`), which made it the third hand-kept copy of a vocabulary that lives in
|
||||
one table. Both halves of #4250 are pinned here: every declared kind is
|
||||
The route reads its kinds from the one table the vocabulary lives in, rather
|
||||
than keeping a map of its own (`note`/`task`/everything else is `all`). Both
|
||||
halves are pinned here: every declared kind is
|
||||
reachable, and a kind that does not exist is refused rather than widened.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Every semantic search scopes first, then ranks (#4958, #4961).
|
||||
"""Every semantic search scopes first, then ranks.
|
||||
|
||||
Ordered straight off a `*_embeddings` table, the planner walks the HNSW index:
|
||||
it takes ~`hnsw.ef_search` (40) nearest chunks from every owner and project
|
||||
@@ -59,8 +59,8 @@ def test_every_semantic_search_ranks_through_the_scoped_shape():
|
||||
assert "order_by" not in calls, f"{name} orders by distance itself"
|
||||
|
||||
|
||||
def test_the_old_shape_would_be_caught():
|
||||
"""Rule 167: replay the guard against the query every search used to run."""
|
||||
def test_a_rank_then_scope_query_is_caught():
|
||||
"""Replay the guard against a query that ranks first and scopes after."""
|
||||
old = ast.parse(
|
||||
"async def semantic_search_x():\n"
|
||||
" rows = await session.execute(select(Note, distance).select_from(E)"
|
||||
|
||||
@@ -5,7 +5,7 @@ list query can't use. These express the same resolution as set membership. They
|
||||
are pure SQL builders — group membership is a subquery rather than a fetched
|
||||
list, so they need no session and callers' unit tests need not know they exist.
|
||||
|
||||
Two scopes, deliberately different (decision note 2094):
|
||||
Two scopes, deliberately different:
|
||||
readable_* — everything the ACL permits, for explicit acts (a typed search, a
|
||||
fetch by id).
|
||||
browsable_* — owner + project access only, for passive surfaces (browse lists,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""The canonical-slug matcher — the line between what maps mechanically and
|
||||
what a human is asked to confirm (milestone 307, note 3026).
|
||||
what a human is asked to confirm.
|
||||
|
||||
These cases are the real drift found across the author's own instance: one area
|
||||
carrying three spellings, and two areas that LOOK alike and are not the same.
|
||||
These cases are real-world drift: one area carrying three spellings, and two areas that LOOK alike and are not the same.
|
||||
Getting the boundary wrong in either direction is a silent failure — a missed
|
||||
mapping nobody thinks to make again, or a cross-project record surfacing in the
|
||||
wrong project.
|
||||
|
||||
@@ -18,7 +18,7 @@ def test_task_row_maps_fields():
|
||||
n.priority = None
|
||||
# Named, not left to MagicMock: an unset attribute is a truthy Mock, so
|
||||
# the assertion would pass on a field the row never really carried
|
||||
# (note 2109 — the reason fake_note exists).
|
||||
# (the reason fake_note exists).
|
||||
n.task_kind = "spike"
|
||||
assert _task_row(n) == {
|
||||
"id": 5, "title": "Wire reminders", "status": "in_progress",
|
||||
|
||||
@@ -185,7 +185,7 @@ async def test_clearing_a_projects_design_system_skips_the_system_acl():
|
||||
acc.can_read_design_system.assert_not_awaited()
|
||||
|
||||
|
||||
# --- resolution (step 2) ----------------------------------------------------
|
||||
# --- resolution -------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_denied_returns_none_not_an_empty_set():
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""get_knowledge_counts — one number per facet, and an honest total.
|
||||
|
||||
Two grouped queries, one per typing axis. It used to be three: a grouped count
|
||||
over note_type restricted to ("note", "process"), a scalar count of tasks, and
|
||||
a second scalar just for plans. That shape is why `issue` had no number —
|
||||
every kind needed a query of its own and nobody added one — and why the "All"
|
||||
chip sat ~90 below the list it labelled, since snippets were in the feed but
|
||||
in no count (#3128).
|
||||
Two grouped queries, one per typing axis. A query per kind — a grouped count
|
||||
over note_type restricted to ("note", "process"), a scalar count of tasks, a
|
||||
second scalar just for plans — leaves every new kind without a number until
|
||||
someone adds its query, and puts the "All" chip below the list it labels,
|
||||
since snippets are in the feed but in no count.
|
||||
"""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -71,9 +70,8 @@ async def test_total_counts_snippets_and_counts_no_task_twice():
|
||||
async def test_absent_facets_report_zero_rather_than_missing():
|
||||
counts, _ = await _counts([("note", 1)], [])
|
||||
assert counts["note"] == 1
|
||||
# `lesson` is here from milestone 385 step 2: a kind added to the facet
|
||||
# table and not to the counts would show an empty chip beside a feed that
|
||||
# has rows in it, which is defect 3b in #3161 repeating itself.
|
||||
# A kind added to the facet table and not to the counts would show an
|
||||
# empty chip beside a feed that has rows in it.
|
||||
for key in ("process", "snippet", "lesson", "task", "work", "issue", "spike", "plan"):
|
||||
assert counts[key] == 0, key
|
||||
assert counts["total"] == 1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The note staleness sweep (milestone 317 step 3).
|
||||
"""The note staleness sweep.
|
||||
|
||||
The query is a sibling of the rules sweep, not a shared implementation — a
|
||||
rule scopes by rulebook ownership, a note by the note ACL, so only the
|
||||
@@ -103,7 +103,7 @@ async def test_trashed_notes_are_excluded():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_scope_is_browse_not_read():
|
||||
"""Decision note 2094: a sweep is a PASSIVE surface, so a record shared
|
||||
"""A sweep is a PASSIVE surface, so a record shared
|
||||
one-to-one must not arrive in one unasked. `note_shares` is the tell —
|
||||
its presence would mean the read scope leaked in."""
|
||||
sql = await _sweep_sql()
|
||||
@@ -207,7 +207,7 @@ async def test_a_note_with_no_check_cannot_be_verified():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_reader_cannot_stamp():
|
||||
"""Stamping is a mutation (rules 47/78) — an editor-share holder may make
|
||||
"""Stamping is a mutation — an editor-share holder may make
|
||||
it, a viewer may not."""
|
||||
note = fake_note(verify_with="curl the docs", verified_at=None)
|
||||
out, _ = await _mark(note, writable=False)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""A note's own check — verify_with / expires_when (milestone 317 step 2).
|
||||
"""A note's own check — verify_with / expires_when.
|
||||
|
||||
Three behaviours the rules path (#3096) had to get right and this one inherits:
|
||||
Three behaviours this path shares with a rule's check:
|
||||
empty means NULL, clearing is explicit, and rewriting the check drops the
|
||||
stamp. Plus one this path adds: not every record may carry a check, and the
|
||||
rule is an INVARIANT over the record rather than a filter on the write.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Batched project summaries (#2384).
|
||||
"""Batched project summaries.
|
||||
|
||||
The bug was not wrong output — it was CONNECTION COUNT. A per-project summary
|
||||
opened its own session and then one more per milestone, and the route fanned
|
||||
that out with asyncio.gather. For 25 projects that asked for roughly 250
|
||||
checkouts against a pool of 15, so most waited out the 30-second timeout and
|
||||
every other route on the instance queued behind them:
|
||||
The property is CONNECTION COUNT, not output. A per-project summary that opens
|
||||
its own session and then one more per milestone, fanned out with
|
||||
asyncio.gather, asks 25 projects for roughly 250 checkouts against a pool of
|
||||
15, so most wait out the 30-second timeout and every other route on the
|
||||
instance queues behind them:
|
||||
|
||||
QueuePool limit of size 5 overflow 10 reached, connection timed out
|
||||
GET /api/settings 500 30584.0ms
|
||||
@@ -12,7 +12,7 @@ every other route on the instance queued behind them:
|
||||
|
||||
So these tests assert the number of sessions opened, not only the values
|
||||
returned. A version that produced identical output while opening a session per
|
||||
project would pass a correctness test and reproduce the outage.
|
||||
project would pass a correctness test and exhaust the pool.
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
@@ -118,7 +118,7 @@ async def test_milestone_summaries_for_many_projects_open_ONE_session():
|
||||
|
||||
assert opened[0] == 1
|
||||
# Touched is the later of the milestone's own edit and its newest step
|
||||
# update (#4045): steps closing today count, an old step doesn't pull it back.
|
||||
# update: steps closing today count, an old step doesn't pull it back.
|
||||
assert out[1][0]["last_touched_at"] == step_closed.isoformat()
|
||||
assert out[2][0]["last_touched_at"] == written.isoformat()
|
||||
assert out[1][0]["completed"] == 2 and out[1][0]["total"] == 3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Guessed record ids are refused; `{{ref:N}}` placeholders resolve (#4016).
|
||||
"""Guessed record ids are refused; `{{ref:N}}` placeholders resolve.
|
||||
|
||||
The defect: a session predicts the ids its next creates will get and writes
|
||||
The failure: a session predicts the ids its next creates will get and writes
|
||||
them into a body before the records exist; any other session's create takes
|
||||
those numbers. These pin the recogniser — what counts as a `#N`, where a guess
|
||||
can sit relative to the highest id — and the placeholder rewrite the batch
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""The report-shape check (milestone 409 step 5): which completion sections a
|
||||
task-closing reply lacks, the words it is sent back with, and the outcome
|
||||
record. Since milestone 500 step 4 the check itself runs here, inside the one
|
||||
end-of-turn request, rather than in a Stop hook of its own."""
|
||||
"""The report-shape check: which completion sections a task-closing reply
|
||||
lacks, the words it is sent back with, and the outcome record. The check runs
|
||||
here, inside the one end-of-turn request, rather than in a Stop hook of its
|
||||
own."""
|
||||
import json
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The server half of the end-of-turn shape check (milestone 439): what the
|
||||
"""The server half of the end-of-turn shape check: what the
|
||||
agent is asked, what the hook's ledger may say, and the outcome record."""
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -61,7 +61,7 @@ async def test_count_open_issues_denied_returns_zero():
|
||||
assert result == 0
|
||||
|
||||
|
||||
# ── assess_system_name — the one gate both doors call (milestone 307) ──
|
||||
# ── assess_system_name — the one gate both doors call ──
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_assess_matches_an_existing_system_on_case_and_spacing():
|
||||
@@ -128,7 +128,7 @@ async def test_assess_says_nothing_about_a_nameless_system():
|
||||
assert await assess_system_name(1, 5, " ") == {"duplicate": None, "canonical": None}
|
||||
|
||||
|
||||
# ── path patterns — the files that ARE the area (milestone 444, #4756) ──
|
||||
# ── path patterns — the files that ARE the area ──
|
||||
|
||||
def test_patterns_are_tidied_to_repo_relative_and_deduplicated():
|
||||
from scribe.services.systems import normalize_path_patterns
|
||||
|
||||
@@ -49,7 +49,6 @@ async def test_delete_project_cascades_to_notes_milestones_and_project_rules():
|
||||
# exists-check + 4 cascade ops:
|
||||
# notes (soft) → milestones (soft) → project-scoped rules (soft) →
|
||||
# project (soft)
|
||||
# The two suppression hard-DELETEs went with their tables (milestone 414).
|
||||
session.execute = AsyncMock(side_effect=[
|
||||
_exists_result(True),
|
||||
MagicMock(), MagicMock(), MagicMock(),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""A SessionStart that loads no project says why, and names the first move (#4366).
|
||||
"""A SessionStart that loads no project says why, and names the first move.
|
||||
|
||||
The fetch used to be `curl -f … || body=""`: a timeout, an HTTP error and a
|
||||
refused key all produced one sentence, which ended by suggesting
|
||||
`enter_project()` "as needed". A session that read it as optional started with
|
||||
no recent milestones or open tasks, and so had no way to know which prior work
|
||||
existed to look for. These pin the two halves of the fix on the path that needs
|
||||
no server: the cause is named, and the fallback is a concrete first step.
|
||||
A fetch written as `curl -f … || body=""` collapses a timeout, an HTTP error
|
||||
and a refused key into one sentence, and a fallback that suggests
|
||||
`enter_project()` "as needed" reads as optional. A session that skips it starts
|
||||
with no recent milestones or open tasks, and so has no way to know which prior
|
||||
work exists to look for. These pin both halves on the path that needs no
|
||||
server: the cause is named, and the fallback is a concrete first step.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The end-of-turn shape check (milestone 439): the write hooks note what each
|
||||
"""The end-of-turn shape check: the write hooks note what each
|
||||
write defined, and the Stop hook asks the instance which of those nobody has
|
||||
judged, blocking once in the server's words.
|
||||
|
||||
|
||||
@@ -311,10 +311,10 @@ def test_a_resemblance_score_must_positively_agree() -> None:
|
||||
assert not _stamp_allowed(1, "type", "fn")
|
||||
|
||||
|
||||
def test_the_portal_case_no_longer_stamps_one_canon_onto_a_whole_file() -> None:
|
||||
def test_one_canon_is_not_stamped_onto_a_whole_file() -> None:
|
||||
"""A class, an async method and a sync helper in one file, against one
|
||||
payload-level score. Previously all three were stamped; now the score
|
||||
alone cannot carry any of them, and only a matching form could."""
|
||||
payload-level score. The score alone cannot carry any of them; only a
|
||||
matching form can."""
|
||||
canon = "async-fn"
|
||||
written = [("class SessionAbsent(RuntimeError):", False),
|
||||
("async def attach(self) -> None:", True),
|
||||
|
||||
@@ -85,7 +85,7 @@ async def test_merge_requires_write_on_target():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_skips_sources_owned_by_someone_else():
|
||||
"""Cross-owner merge stays out of scope (#231): a source belonging to a
|
||||
"""Cross-owner merge stays out of scope: a source belonging to a
|
||||
different owner than the target is skipped, not silently folded in and
|
||||
trashed."""
|
||||
from scribe.services import snippets as svc
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The bundled skills declare the moments they are for (milestone 458 step 5).
|
||||
"""The bundled skills declare the moments they are for.
|
||||
|
||||
Loading the reporting procedure is a report; loading the planning procedure
|
||||
is planning. Each SKILL.md says which moment it is for in its frontmatter
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for the drift check (#2086) — does a snippet still match its source?
|
||||
"""Tests for the drift check — does a snippet still match its source?
|
||||
|
||||
The check itself runs agent-side (Scribe has no checkout), so what's testable
|
||||
here is everything around it: the verdict's shape, the rule that a verdict
|
||||
@@ -175,7 +175,7 @@ def test_python_dialect_on_a_row_with_no_data_at_all():
|
||||
[("ok", False), ("attention", True), ("drifted", False), ("unverified", False)],
|
||||
)
|
||||
def test_python_dialect_on_a_push_invalidated_ok_verdict(value, expected):
|
||||
"""#2691: a push touched the recorded location since the verdict. Like the
|
||||
"""A push touched the recorded location since the verdict. Like the
|
||||
expired case, it is neither drifted (nothing found wrong) nor unverified (a
|
||||
check happened) — but the repo moved under the blessing, so `attention`
|
||||
must include it and `ok` must not."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Merge records what it absorbed (#2087).
|
||||
"""Merge records what it absorbed.
|
||||
|
||||
Merging keeps the target's scalar fields, unions locations + tags, and trashes
|
||||
the sources. Without provenance the fact that a variant ever existed survives
|
||||
@@ -59,7 +59,7 @@ async def test_merge_accumulates_across_merges():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_does_not_record_a_skipped_cross_owner_source():
|
||||
"""A source owned by someone else is skipped, not folded in (#231) — so it
|
||||
"""A source owned by someone else is skipped, not folded in — so it
|
||||
must not appear in the provenance either, or the record would claim to
|
||||
contain something it never absorbed."""
|
||||
kwargs = await _run_merge(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""A snippet's notes — the explanation that had nowhere to go (#4378).
|
||||
"""A snippet's notes — where the explanation goes.
|
||||
|
||||
Before this field, a snippet had name, code, signature, locations and
|
||||
`when_to_use`, and nothing for prose. What a session learned about a snippet
|
||||
went into `when_to_use` — the trigger, joined onto the title of every chunk the
|
||||
snippet is embedded as — so a multi-paragraph write-up there blurred the one
|
||||
situation it is ranked on.
|
||||
A snippet has name, code, signature, locations and `when_to_use`. Without a
|
||||
place for prose, what a session learned about a snippet goes into
|
||||
`when_to_use` — the trigger, joined onto the title of every chunk the snippet
|
||||
is embedded as — so a multi-paragraph write-up there blurs the one situation
|
||||
it is ranked on.
|
||||
|
||||
The rules pinned here, each with a way to rot silently:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for un-merge (#2165) — reversing one source out of a merged survivor.
|
||||
"""Tests for un-merge — reversing one source out of a merged survivor.
|
||||
|
||||
The design question this task said to settle first was how to subtract without
|
||||
stripping call sites the survivor legitimately owns. The answer is per-source
|
||||
The design question is how to subtract without stripping call sites the
|
||||
survivor legitimately owns. The answer is per-source
|
||||
attribution recorded AT MERGE TIME: each `merged_from` entry holds only what
|
||||
that source actually added. These tests pin that rule, and the refusal that
|
||||
guards the case where the attribution isn't there.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""An area's rulings reach the work that touches its files (milestone 444, #4757).
|
||||
"""An area's rulings reach the work that touches its files.
|
||||
|
||||
These pin:
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ and means nothing. The signal is not "has a check" — it is "has a check AND
|
||||
almost nothing else does".
|
||||
|
||||
So the guidance is not decoration on this feature, it IS the feature's
|
||||
precondition, and rule 119 puts it in the tool docstrings and the skill rather
|
||||
than in a Scribe rule. That makes it exactly the kind of prose a later
|
||||
precondition, and product guidance lives in the tool docstrings and the skill
|
||||
rather than in a Scribe rule. That makes it exactly the kind of prose a later
|
||||
docstring tidy-up deletes without noticing what it was for.
|
||||
|
||||
WHAT THIS PINS, AND WHAT IT DOES NOT
|
||||
@@ -94,7 +94,7 @@ def test_the_skill_carries_the_test_a_writer_can_actually_apply():
|
||||
from tests.helpers import skill_text
|
||||
|
||||
# Reference files included: the full statement sits in writing-records.md
|
||||
# and SKILL.md keeps the pointer (#4398).
|
||||
# and SKILL.md keeps the pointer.
|
||||
text = " ".join(skill_text("using-scribe").split())
|
||||
assert "could this note become false without anyone editing it" in text.lower(), (
|
||||
"the using-scribe skill no longer carries the one-question test. That "
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Tests for the prune logic — both rolling (pin_kind IS NULL) and the
|
||||
auto-pin bucket (pin_kind='auto').
|
||||
|
||||
Design: docs/superpowers/specs/2026-05-13-note-version-pinning-design.md
|
||||
"""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from tests.helpers import make_mock_session
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""The write-path design arm: a UI write is told which design system binds it (#4256).
|
||||
"""The write-path design arm: a UI write is told which design system binds it.
|
||||
|
||||
A design system binds like a rule, and before this it reached a session only
|
||||
through the session-start block — complete for a session that knows to ask,
|
||||
silent for one writing a component. These tests pin:
|
||||
A design system binds like a rule. Delivered only through the session-start
|
||||
block, it is complete for a session that knows to ask and silent for one
|
||||
writing a component. These tests pin:
|
||||
|
||||
- THE TRIGGER is the file, not a search: a UI path in a project that has a
|
||||
design system. No vectors, no score, no slot taken from the ranked menu.
|
||||
|
||||
@@ -1776,14 +1776,15 @@ async def test_a_record_this_arm_withheld_itself_is_not_a_near_miss():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_ledger_repeat_no_longer_produces_that_case_at_all():
|
||||
async def test_a_ledger_repeat_is_counted_not_withheld():
|
||||
"""The other direction: a ledger repeat is not withheld.
|
||||
|
||||
A record on the session ledger now clears the search, is rendered with a
|
||||
A record on the session ledger clears the search, is rendered with a
|
||||
marker, and is counted in `suppressed` — so nothing is withheld and the
|
||||
reported score describes the bar. Pinned because the tempting shortcut is
|
||||
to null the score whenever the ledger matches, which would delete the near-miss measurement on exactly the calls
|
||||
where the bar is most worth reading.
|
||||
to null the score whenever the ledger matches, which would delete the
|
||||
near-miss measurement on exactly the calls where the bar is most worth
|
||||
reading.
|
||||
"""
|
||||
rec = MagicMock()
|
||||
row = await _write_path_row(rec, exclude_ids=[7])
|
||||
|
||||
Reference in New Issue
Block a user