Compare commits

...
2 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 914f31359c docs(6209): the remaining test files — lessons, reply moment and shape, retrieval specs and scopes, rule doors and judgments, services, snippets, search scope; five tests renamed to their property
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 17s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m19s
CI & Build / Python tests (push) Successful in 2m2s
CI & Build / Build & push image (push) Successful in 15s
Docstrings, section headers and assertion messages drop the record number,
milestone or step that introduced the behaviour, and incident accounts are
restated as the present-tense failure shape. A dead pointer to a design doc
that is not in the repo is removed.

Renamed to the property they check:
  test_the_old_shape_would_be_caught -> test_a_rank_then_scope_query_is_caught
  test_a_ledger_repeat_no_longer_produces_that_case_at_all -> test_a_ledger_repeat_is_counted_not_withheld
  test_the_header_no_longer_promises_once_per_session -> test_the_header_explains_the_seen_marker
  test_the_portal_case_no_longer_stamps_one_canon_onto_a_whole_file -> test_one_canon_is_not_stamped_onto_a_whole_file
  test_the_repos_own_prose_no_longer_defines_with -> test_the_repos_own_prose_does_not_define_with
  test_the_original_regression_create_note_with_content -> test_the_real_server_refuses_content_on_create_note

Fixture and example ids stay: "note 5" in a supersession chain, "rule 156"
in an illustrative question, `#2275` as the hex-versus-citation case,
`#3244` as the example of a bare number, fixed dates in date-filter tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:44:58 -04:00
bvandeusenandClaude Opus 5.5 79de8938b9 docs(6209): forty test files — design cascade and stylesheet, embeddings, family canon, forge, backup round trips, inception, placement, platforms, moments, rule move and verification, search scope, facets
Module and test docstrings state what is pinned without the record number
or step that introduced it. Incident accounts become the failure shape in
the present tense: the pipefail head, the code_shapes import, the export
order, the facet dialects, the cross-kind repeat, the write-once kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:40:25 -04:00
113 changed files with 308 additions and 324 deletions
+6 -6
View File
@@ -1,8 +1,8 @@
"""The auto-inject query carries the conversation beside a thin prompt (#4364).
"""The auto-inject query carries the conversation beside a thin prompt.
The notes arm used to retrieve against the operator's typed words alone, and a
mid-session follow-up — "yes do that", "now fix the filter" — names nothing a
record can match. The hook now reads the tail of the last assistant reply out
The operator's typed words alone are a poor query: a mid-session follow-up —
"yes do that", "now fix the filter" — names nothing a record can match. The
hook reads the tail of the last assistant reply out
of the transcript and sends it as `ctx`; the server appends it to a SHORT
prompt only. Two halves, pinned separately: what the server builds, and what
the hook extracts.
@@ -103,10 +103,10 @@ def test_no_transcript_is_silence_not_failure(tmp_path):
assert recent(tmp_path, []) == ""
# --- what a menu line carries (#4364) ----------------------------------------
# --- what a menu line carries -----------------------------------------------
#
# The name, the kind and System, and the WHOLE matched passage — once. A repeat
# is a pointer. These pin the shape against the three ways it had gone wrong:
# is a pointer. These pin the shape against the three ways it goes wrong:
# a trigger-composed title rendered as the line (1,500+ chars), the passage cut
# to its head (which was the title again), and a `seen` repeat re-rendered whole.
+2 -2
View File
@@ -1,4 +1,4 @@
"""The CI-side token check (#2277).
"""The CI-side token check.
The checker is stdlib-only and lives in scripts/ so CI can run it without an
install, so these tests import it by path rather than as a package.
@@ -45,7 +45,7 @@ def test_declarations_after_a_comment_are_not_lost():
def test_the_supersedes_block_is_read_from_the_sheet_not_hardcoded():
"""Rule #115: the checker must know nothing about any install's palette.
"""The checker knows nothing about any install's palette.
Everything it enforces comes out of the stylesheet it is pointed at."""
assert check.superseded_literals(SHEET) == {
"#fff": "--fs-text-primary",
+2 -3
View File
@@ -1,12 +1,11 @@
"""Every create_* tool says what it is NOT for (#3123).
"""Every create_* tool says what it is NOT for.
WHY THIS EXISTS
Scribe's record kinds are reached for interchangeably — a note written where
a task was owed, a rule written where a snippet belonged — and the moment of
choice is the only moment a correction is cheap. The tool docstring IS the
agent-facing contract (rule 119 puts product guidance there and nowhere
else), so a docstring that only documents parameters answers "how do I call
agent-facing contract (product guidance lives there and nowhere else), so a docstring that only documents parameters answers "how do I call
this" while leaving "should I be calling this at all" unasked.
The gap was lopsided before this: `create_rule` and `start_planning` both
+3 -3
View File
@@ -1,4 +1,4 @@
"""The parent chain: walking it, and refusing to close it (milestone #254 step 1).
"""The parent chain: walking it, and refusing to close it.
Pure functions over a `{id: parent_id}` literal, so a whole hierarchy is one line
of setup and no database is involved. That is the reason the cascade lives in its
@@ -99,7 +99,7 @@ def test_the_guard_survives_a_hierarchy_that_is_already_corrupt():
# --- resolution -------------------------------------------------------------
#
# The cascade proper (milestone #254 step 2). Hierarchies are stated as literals
# The cascade proper. Hierarchies are stated as literals
# because resolve_tokens is pure and duck-typed — which is the whole reason it
# lives here rather than inside the service.
@@ -384,7 +384,7 @@ def test_rows_without_a_supersedes_attribute_at_all_still_resolve():
def test_rationale_cascades_like_purpose_and_is_a_different_question():
"""`purpose` is what the token is FOR; `rationale` is why it is this value.
Rules carry the second routinely ("Success = Moss, aligned by design") and a
token row had nowhere to put it until now."""
token row needs a place to put it."""
resolved = _by_name(resolve_tokens(
APP, PARENTS,
{
+3 -3
View File
@@ -120,8 +120,8 @@ def test_the_header_says_what_the_sheet_is_for():
# --- modes ------------------------------------------------------------------
def test_base_goes_on_the_root_selector_and_other_modes_layer_over_it():
"""Matches the convention already in the codebase, and the one-way scoping
#251 recorded: light on `:root`, dark layered on an attribute selector."""
"""Matches the convention already in the codebase, and its one-way
scoping: light on `:root`, dark layered on an attribute selector."""
css = render_stylesheet([
design_token_stub(name="--fs-bg", value_by_mode={"base": "#f5f1e8", "dark": "#14171a"}),
])
@@ -188,7 +188,7 @@ def test_a_declared_token_with_no_value_appears_as_a_comment_not_a_silence():
def test_an_empty_system_renders_a_sheet_with_no_blocks_rather_than_failing():
"""A system with no tokens is an ordinary state (rule #115), including one
"""A system with no tokens is an ordinary state, including one
that was just created."""
css = render_stylesheet([])
assert "{" not in css
+2 -2
View File
@@ -9,7 +9,7 @@ the dangerous one: a recurring task embedded to a different shape than the rest
of the corpus is ranked against documents it doesn't match, and nothing reports
it. A wrong vector returns results; it just returns the wrong ones.
It is also the precondition for #2486. A dev-log's vector separates from five
It is also the precondition for comparing document shapes. A dev-log's vector separates from five
unrelated dev-logs by 0.023 where a snippet separates by 0.153, and the leading
explanation is shape — a snippet states its purpose twice in a short document.
Testing an alternative against three copies would mean testing a shape that
@@ -77,5 +77,5 @@ def test_nothing_else_builds_the_embedding_document_itself():
f"these build the embedding document inline instead of calling "
f"embedding_text(): {offenders}. One definition — an inlined copy is "
f"ranked against a corpus it no longer matches the moment the shape "
f"changes, and nothing reports it (#2486)."
f"changes, and nothing reports it."
)
+3 -3
View File
@@ -1,4 +1,4 @@
"""The embedding refresh must LOSE to a delete, not race it (#3262).
"""The embedding refresh must LOSE to a delete, not race it.
Both upserts replace a record's vectors as delete-then-insert. That takes two
row locks — the chunk rows, then the parent row via the insert's foreign key —
@@ -32,7 +32,7 @@ def _mock_session(lock_result: object = 7, execute_side_effect=None):
session = MagicMock()
claimed = MagicMock()
claimed.scalar_one_or_none.return_value = lock_result
# A task's document carries its work logs (#4251), so the refresh reads
# A task's document carries its work logs, so the refresh reads
# task_logs before it builds chunks. Answered explicitly — left to
# autovivify, `list(result.all())` would raise and be swallowed, and these
# tests would be exercising the failure path without saying so.
@@ -72,7 +72,7 @@ async def test_a_note_refresh_claims_the_row_before_rewriting_its_vectors():
sqls = [compiled_sql(c.args[0], dialect=PG) for c in session.execute.call_args_list]
# Located by what they ARE rather than by position: the work-log read a
# task's document needs (#4251) runs before any of this, and an index is
# task's document needs runs before any of this, and an index is
# not what the claim is about.
claim = next(i for i, sql in enumerate(sqls) if "FOR KEY SHARE NOWAIT" in sql)
replace = next(
+1 -1
View File
@@ -74,7 +74,7 @@ async def test_get_embedding_propagates_model_load_failures():
await get_embedding("x")
# ─── query_embedding_memo (milestone 456 step 1) ─────────────────────────────
# ─── query_embedding_memo ────────────────────────────────────────────────────
def _counting_embeddings():
+2 -2
View File
@@ -1,4 +1,4 @@
"""The promotion engine without a database (milestone 463 step 3).
"""The promotion engine without a database.
The parts that decide — which citations claim lineage, which criteria veto —
are pure and pinned here, beside the doors' wiring: the triggers ride the
@@ -89,7 +89,7 @@ def test_proven_needs_named_evidence():
def test_the_criteria_the_agent_is_told_are_the_ones_enforced():
"""Rule 119: the criteria are product text. The promote tool's docstring
"""The criteria are product text. The promote tool's docstring
must name every criterion the service checks, by its parameter name."""
doc = family_tools.promote_family_idea.__doc__
for key in CRITERIA_KEYS:
+2 -2
View File
@@ -1,4 +1,4 @@
"""The adoption ledger without a database (milestone 463 step 4).
"""The adoption ledger without a database.
The parts that decide are pure and pinned here: what an assessment needs
before it can be recorded, what each ground of the conflict order needs —
@@ -166,7 +166,7 @@ def test_no_reference_at_all_points_at_the_idea():
# --- the product text and the doors ---------------------------------------------------
def test_the_outcomes_the_agent_is_told_are_the_ones_enforced():
"""Rule 119: the outcomes and the order are product text. The tool
"""The outcomes and the order are product text. The tool
docstrings must name every outcome and every ground the service checks,
in the service's order."""
assess_doc = tool_doc("scribe.mcp.tools.family", "assess_family_adoption")
+1 -1
View File
@@ -1,4 +1,4 @@
"""How family canon reaches a session (milestone 463 step 6), without a
"""How family canon reaches a session, without a
database: the line enter_project carries, the skill and its triggers, and the
pointers every other surface gives to it. Retrieval reach, the readout's
counts and the owed answers against Postgres are in
+1 -1
View File
@@ -1,4 +1,4 @@
"""Forge-connection tokens are stored sealed (milestone 523, #5877).
"""Forge-connection tokens are stored sealed.
Migration 0122 seals the rows that existed before, with its OWN frozen copy of
the v1 key derivation (so a later edit to services/secrets.py can never change
+2 -2
View File
@@ -1,4 +1,4 @@
"""Forge push webhook (#2691) — signature gate, payload parsing, and the
"""Forge push webhook — signature gate, payload parsing, and the
end-to-end drift flag on real Postgres.
The webhook is the seam that makes verification scale: a push names exactly
@@ -43,7 +43,7 @@ def test_signature_gate():
def test_delivered_signature_reads_both_forges_headers():
"""#2693: GitHub signs the same HMAC but ships it as
"""GitHub signs the same HMAC but ships it as
X-Hub-Signature-256: sha256=<hex> — the whole webhook payload mapping is
this header, so pin it."""
hexsig = _sign(b"{}")
+1 -1
View File
@@ -1,4 +1,4 @@
"""The create gate's similarity bars are settings, clamped and fail-open (#4385)."""
"""The create gate's similarity bars are settings, clamped and fail-open."""
from unittest.mock import AsyncMock, patch
import pytest
+6 -7
View File
@@ -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.
+5 -5
View File
@@ -1,14 +1,14 @@
"""Under `pipefail`, an early-exiting `head` must not void the output it kept (#4042).
"""Under `pipefail`, an early-exiting `head` must not void the output it kept.
The hooks run with `set -uo pipefail`. `hits=$(git grep -l … | head -4) ||
hits=""` lost its four hits whenever git grep was still writing when head
exited: SIGPIPE, a failed substitution, and the fallback wiped the result. It
only showed on big outputs, so the by-name duplicate arm went silent for
hits=""` loses its four hits whenever git grep is still writing when head
exits: SIGPIPE, a failed substitution, and the fallback wipes the result. It
only shows on big outputs, so the by-name duplicate arm goes silent for
exactly the most-duplicated names.
Two halves: the real function on a repo big enough to overflow the pipe, and
the check_plugin pattern that keeps the shape out of every hook (shown able to
fail, rule #167).
fail).
"""
from __future__ import annotations
@@ -1,11 +1,11 @@
"""Real-Postgres round trip for code_shapes (#4197).
"""Real-Postgres round trip for code_shapes.
**What this guards is a column that was simply not there.** `code_shapes`
exported `reason_code`, `recheck_at` and `diverges_from` and imported none of
them — a restore reported success and came back with every judgment's verdict
**What this guards is a column the import leaves out.** If `code_shapes`
exports `reason_code`, `recheck_at` and `diverges_from` and imports none of
them, a restore reports success and comes back with every judgment's verdict
and no code for why, every recheck flag cleared, and every divergence pointer
gone. The unit-level column guard in `test_services_backup.py` is what found
it and is what stops it recurring; this file is the behavioural half, because
gone. The unit-level column guard in `test_services_backup.py` catches a
column missing from either side; this file is the behavioural half, because
"the builder passes the kwarg" and "the value survives a real restore" are
different claims.
@@ -1,11 +1,11 @@
"""Real-Postgres check that the full export is ORDERED, so the backup file
deduplicates between runs (milestone 523).
deduplicates between runs.
Only Postgres shows this. An unordered select returns rows in heap order, and
an UPDATE writes the row's new version at the end of the heap — so before
export_full_backup ordered its queries, editing one old note moved it to the
bottom of `notes`. Every byte after its old place shifted, and a restic run
over the next backup stored most of the table again. A mocked session returns
an UPDATE writes the row's new version at the end of the heap — so without an
ORDER BY in export_full_backup, editing one old note moves it to the bottom of
`notes`. Every byte after its old place shifts, and a restic run over the next
backup stores most of the table again. A mocked session returns
whatever order it was handed and could never catch it.
"""
import io
@@ -101,7 +101,7 @@ async def test_the_tar_restores_with_every_id_remapped(written):
assert (note.project_id, note.milestone_id) == (project.id, milestone.id)
assert note.body == "From the tar, not the JSON."
assert note.status == "todo"
# The Profile page travels with its user (#5879); a field left blank stays
# The Profile page travels with its user; a field left blank stays
# blank rather than restoring as the page's default.
assert (profile.display_name, profile.tone) == ("Drill Owner", "technical")
assert profile.interests == ["backups"]
@@ -1,4 +1,4 @@
"""Real-Postgres round trip for rule_versions (milestone 323 step 1).
"""Real-Postgres round trip for rule_versions.
Two things here cannot be shown with mocks, and both are the kind that fail
QUIETLY — a restore reports success and hands back history that is wrong.
@@ -7,7 +7,7 @@ QUIETLY — a restore reports success and hands back history that is wrong.
A restore that writes it straight through succeeds and attaches every
snapshot to whatever rule happens to hold that number here — an edit
history filed against the wrong binding instruction, which is worse than
no history at all. This is #3182's `arose_from_id` trap on a new table.
no history at all. It is the same trap as a note's `arose_from_id`.
2. **A null actor does not drop the row.** `user_id` is SET NULL precisely so
history outlives the account that wrote it, so the restore deliberately
@@ -217,7 +217,7 @@ async def test_both_snapshots_come_back(restored):
async def test_the_history_attaches_to_the_RESTORED_rule(restored):
"""#3182's trap. The source rule still exists and holds a different id, so
"""The remap trap. The source rule still exists and holds a different id, so
a straight-through copy would file this history against it — or against
whatever unrelated rule owns that number."""
new_rule_id = restored["rule"].id
+3 -3
View File
@@ -1,4 +1,4 @@
"""The adoption ledger against real Postgres (milestone 463 step 4).
"""The adoption ledger against real Postgres.
What the unit lane can't show: an answer moves the row and logs exactly one
decision; an owed answer files its task in the OWING project, tagged to the
@@ -236,7 +236,7 @@ async def test_another_projects_answer_is_recorded_as_precedent(family):
assert first["decision"]["id"] in second["decision"]["precedent_ids"]
assert second["precedents"][0]["relation"] == "same idea, another project"
assert second["precedents"][0]["project_title"] == "android one"
# A precedent says its reason once: before/after are state only (#5131).
# A precedent says its reason once: before/after are state only.
assert second["precedents"][0]["reason"] == "the source app"
assert "reason" not in second["precedents"][0]["after"]
@@ -392,7 +392,7 @@ async def test_the_matrix_hides_what_the_caller_cannot_read(family):
assert all(i["note_id"] != family["note"] for i in matrix["ideas"])
# --- the shape ledger as evidence (step 5) -------------------------------------------
# --- the shape ledger as evidence ----------------------------------------------------
REPO = "git.example/android-two"
KOTLIN_BODY = (
+1 -1
View File
@@ -1,4 +1,4 @@
"""Real-Postgres checks for family canon's schema (milestone 463 step 1).
"""Real-Postgres checks for family canon's schema.
Two kinds of claim live here, and both need a database:
+4 -4
View File
@@ -1,4 +1,4 @@
"""The promotion engine against real Postgres (milestone 463 step 3).
"""The promotion engine against real Postgres.
What the unit lane can't show: a promotion opens the right ledger rows and
no others, a veto changes nothing but the log, an undo puts back exactly the
@@ -279,9 +279,9 @@ async def test_a_repeat_with_no_shared_platform_is_silent(family):
async def test_a_repeat_across_kinds_is_silent(family):
"""A snippet whose only match is a NOTE is not a repeat (#5130): a dated
release log scored 0.83 against a signed-APK CI snippet on shared
vocabulary and was opened as a candidate. The stub hands back the
"""A snippet whose only match is a NOTE is not a repeat: a dated
release log can score above the bar against a signed-APK CI snippet on
shared vocabulary alone, and would open as a candidate. The stub hands back the
cross-kind hit directly, so this pins the trigger's own check, not the
search filter in front of it."""
async with async_session() as s:
+2 -3
View File
@@ -1,7 +1,6 @@
"""A family idea reaches every project on its platform, and no other
(milestone 463 step 6).
"""A family idea reaches every project on its platform, and no other.
WHY THIS IS AN INTEGRATION TEST — the lesson-reach reasoning (#3730) holds
WHY THIS IS AN INTEGRATION TEST — the lesson-reach reasoning holds
here too: the reach is one `OR` inside the search's project filter, and only
the ROWS that come back prove it. Every note embeds identically to the query,
so scoping is the only thing that can separate them. The embedder is stubbed;
+1 -1
View File
@@ -1,4 +1,4 @@
"""Real-Postgres integration tests for the forge keyring (#2778).
"""Real-Postgres integration tests for the forge keyring.
Runs only in the CI integration lane (real Postgres, schema built by
`alembic upgrade head`, which includes migration 0078's forge_connections
+1 -1
View File
@@ -1,4 +1,4 @@
"""Real-Postgres tests for "recently touched" in the enter_project handshake (#4045).
"""Real-Postgres tests for "recently touched" in the enter_project handshake.
What a mock cannot show: that a work-log moves its task up the list even
though logging never bumps the task's updated_at, and that a milestone whose
+2 -2
View File
@@ -1,10 +1,10 @@
"""Real-Postgres integration tests for project inception (milestone 297).
"""Real-Postgres integration tests for project inception.
What mocks can't prove: a decision's effects land through the real services
(the design system points, the standard Systems seed once), the record is
written last, a bad target applies nothing.
Rules are not part of inception since milestone 414: a rule in a rulebook is
Rules are not part of inception: a rule in a rulebook is
global and applies to every project, so there is nothing to subscribe to.
"""
import pytest
+1 -1
View File
@@ -1,4 +1,4 @@
"""A lesson is reachable from a project it was not written on (step 3).
"""A lesson is reachable from a project it was not written on.
WHY THIS IS AN INTEGRATION TEST
+4 -5
View File
@@ -1,8 +1,7 @@
"""Real-Postgres tests for finding a plan by meaning (milestone 415, steps 3 and 4).
"""Real-Postgres tests for finding a plan by meaning.
A project's roadmap written as milestones was invisible to recall: `search`
covered notes, tasks and rules, so "is there already a plan for this?" had no
tool. What a mock cannot show is the join scoping the vectors to a project and
A project's roadmap is written as milestones, and `search` reaches them, so
"is there already a plan for this?" has a tool. What a mock cannot show is the join scoping the vectors to a project and
to what the caller may read, so these seed real milestones with hand-made
vectors and stub only the embedder.
"""
@@ -84,7 +83,7 @@ async def test_a_project_the_caller_cannot_read_returns_nothing(roadmap):
assert await _found(roadmap["stranger"]) == []
# ── the plan gate (step 4): start_planning finds the plan before making another ──
# ── the plan gate: start_planning finds the plan before making another ──
LONG = "Resolve works and editions against the metadata providers. " * 5
+1 -1
View File
@@ -1,4 +1,4 @@
"""Real-Postgres tests for an install's moment mappings (milestone 458 step 2).
"""Real-Postgres tests for an install's moment mappings.
What these pin is what the in-session correction promises the operator: a
mapping made is in force on the next call, a default switched off stays off,
+3 -3
View File
@@ -32,7 +32,7 @@ def _vec(*nonzero_first):
def _emb(note_id, user_id, chunk_index, vec):
"""A chunk row at the current chunker version (#280, migration 0077)."""
"""A chunk row at the current chunker version."""
from scribe.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL
return NoteEmbedding(
@@ -63,7 +63,7 @@ async def seeded():
# query vector will be [1,0,0,...]; near ~ identical (sim≈1.0),
# far is orthogonal (sim≈0.0 -> filtered by the default threshold).
# near gets a SECOND, weaker chunk (sim≈0.6) — the collapse to
# best-chunk-per-note (#280) is under test: near must come back once,
# best-chunk-per-note is under test: near must come back once,
# at its best chunk's score, not twice.
s.add(_emb(near.id, user.id, 0, _vec(1.0)))
s.add(_emb(near.id, user.id, 1, _vec(0.6, 0.8)))
@@ -94,7 +94,7 @@ async def test_semantic_search_ranks_and_thresholds_via_pgvector(seeded):
assert near_id in ids
assert far_id not in ids
assert ids[0] == near_id
# Chunk collapse (#280): near has TWO chunk rows above the floor (sim≈1.0
# Chunk collapse: near has TWO chunk rows above the floor (sim≈1.0
# and ≈0.6) and must appear exactly once, at its best chunk's score.
assert ids.count(near_id) == 1
top_score = results[0][0]
+1 -1
View File
@@ -1,4 +1,4 @@
"""Real-Postgres tests for task placement (milestone 409 step 1).
"""Real-Postgres tests for task placement.
What a mock cannot show: that step order is creation order rather than update
order, that position and progress are computed over the steps the CALLER may
+1 -1
View File
@@ -1,4 +1,4 @@
"""Real-Postgres checks for platform membership (milestone 463 step 2).
"""Real-Postgres checks for platform membership.
The one invariant: detection only ever ADDS, and only where nobody has
answered. A refresh that overwrote a "no" would put a project back into a
+1 -1
View File
@@ -1,4 +1,4 @@
"""Real-Postgres proof that a batch create cannot be interfered with (#4016).
"""Real-Postgres proof that a batch create cannot be interfered with.
What mocks cannot show: that the ids a placeholder is rewritten to are the ids
the rows actually got, that two batches running at the same time each cite
@@ -1,6 +1,6 @@
"""Real-Postgres tests for moment proposals and judgments (milestone 458 step 7).
"""Real-Postgres tests for moment proposals and judgments.
What the step promises, against the real tables:
What the feature promises, against the real tables:
- the pass reads only rules nobody has answered for, and any answer — a
proposal, a mount, "no moment fits" — takes a rule off that list;
- a proposal mounts nothing; a confirm mounts, a reject is kept, and a judged
@@ -8,7 +8,7 @@ What the step promises, against the real tables:
- an edit that takes a moment off a rule is a rejection the signal respects;
- the signal counts SESSIONS, not opens, and asks once the bar is crossed;
- only the owner can propose, judge, or feed the signal;
- the misfire (step 7b): only a mount can misfire, reports count DAYS, the
- the misfire: only a mount can misfire, reports count DAYS, the
bar makes an unmount proposal, reject takes it off, confirm keeps it and
stops the asking, and a re-mount starts the count again.
"""
@@ -196,7 +196,7 @@ async def test_the_open_resolves_acts_through_the_installs_mappings(world):
assert (await _row(done.id, "work.deliver")).source == "signal"
# ── The misfire (step 7b) ────────────────────────────────────────────────────
# ── The misfire ──────────────────────────────────────────────────────────────
@pytest.fixture
+2 -2
View File
@@ -1,4 +1,4 @@
"""Real-Postgres tests for moving a rule between homes (milestone 414, step 3).
"""Real-Postgres tests for moving a rule between homes.
A rule's home is its reach: a rulebook topic makes it global, a project makes
it that project's. The alternative to a move — recreate the rule in the other
@@ -84,7 +84,7 @@ async def test_a_project_rule_becomes_global_and_keeps_everything(homes):
async def test_a_move_writes_no_version(homes):
"""A version records what a rule SAID (milestone 323, decision 4). A move
"""A version records what a rule SAID. A move
changes where it binds, not a word of it."""
before = len(await rv_svc.list_versions(homes["rule"]))
await rulebooks_svc.move_rule(homes["rule"], homes["owner"], topic_id=homes["topic"])
+2 -2
View File
@@ -1,4 +1,4 @@
"""Real-Postgres tests for a rule's CHECK — the write half (milestone 312).
"""Real-Postgres tests for a rule's CHECK — the write half.
What mocks cannot prove, and what the staleness sweep depends on:
@@ -132,7 +132,7 @@ async def test_editing_anything_else_leaves_the_stamp_alone(constraint):
assert row.why.startswith("act_runner picks the shell")
# ── the sweep itself (step 3) ──────────────────────────────────────────
# ── the sweep itself ───────────────────────────────────────────────────
@pytest_asyncio.fixture
async def rulebook_of_three():
+6 -6
View File
@@ -1,14 +1,14 @@
"""Real-Postgres crowd tests: an in-scope record is found however many nearer
records sit outside the scope (#4961; the rule search's is #4958's, in
records sit outside the scope (the rule search's crowd test is in
test_integration_rule_scope).
Ordered straight off a `*_embeddings` table, the planner can walk the HNSW
index, which takes ~`hnsw.ef_search` (40) nearest chunks from every owner and
project and only then applies the scope. Each test here puts 80 out-of-scope
records nearer the query than the reader's one in-scope record, so the old
shape could not reach it when the index served the order. What a test cannot
force is the plan — the shape guard in test_search_scope_shape is what fails
on the old query whatever Postgres picks.
records nearer the query than the reader's one in-scope record, so a query
that scopes after the index walk cannot reach it when the index serves the
order. What a test cannot force is the plan — the shape guard in
test_search_scope_shape is what fails on that query whatever Postgres picks.
Rows are inserted directly with hand-made vectors and the embedder is stubbed,
so the scope alone decides what comes back.
@@ -57,7 +57,7 @@ def _chunk(model, key: str, record_id: int, vec: list[float], **extra):
async def _found(search, user_id: int, **scope) -> set[int]:
"""The ids a search finds. Every search fails open, so a query that raised
reads as an empty result — the swallowed traceback is captured and named
in the failure instead (#4958)."""
in the failure instead."""
raised: list[str] = []
with patch("scribe.services.embeddings.get_embedding",
AsyncMock(return_value=QUERY_VEC)), \
+1 -1
View File
@@ -1,4 +1,4 @@
"""Real-Postgres integration test for the snippet location reverse lookup (#2083).
"""Real-Postgres integration test for the snippet location reverse lookup.
Runs only in the CI integration lane (real Postgres, schema built by
`alembic upgrade head`, which includes migration 0070's `notes.data` + GIN index).
+5 -6
View File
@@ -1,6 +1,6 @@
"""Real-Postgres test that the CHECK actually accepts 'spike' (0091).
Rule 36 exists because the value and the constraint can drift apart: the
The value and the constraint can 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 its own real-DB test,
@@ -68,19 +68,18 @@ async def test_an_unknown_kind_is_still_refused(owner_id):
Without this, every assertion above would pass just as happily against a
table whose CHECK had been dropped and never re-added — which is exactly
the failure rule 36 is written against.
the drift this file is written against.
"""
with pytest.raises(IntegrityError):
await _write(owner_id, "investigation")
async def test_a_kind_can_be_corrected_after_the_fact(owner_id):
"""The write-once bug, asserted against a real column (#3129).
"""A kind is not write-once, asserted against a real column.
The read-back is the whole test. A version that only asserted the update
call succeeded would have passed against the broken code — the route
returned 200 while dropping the field, which is exactly how this survived
long enough to be found by hand.
call succeeded would pass against a route that returns 200 while dropping
the field, which is a failure nothing else reports.
"""
from scribe.services import notes as notes_svc
+8 -9
View File
@@ -3,14 +3,14 @@
The facet predicate is written twice by necessity — as SQL for rows the
database hands back, and as Python for candidates the vector search has
already fetched — plus a third time as the `is_task` pre-filter that narrows
the candidate set before it exists. Those three used to be hand-written and
agreed only by luck: adding `issue` to the SQL arm alone would have set the
pre-filter to is_task=False, handed the Python arm a candidate set with no
tasks in it, and returned an empty semantic half for the Issues facet forever
with nothing red anywhere (#3128).
the candidate set before it exists. Hand-written, those three
agree only by luck: adding `issue` to the SQL arm alone would set the
pre-filter to is_task=False, hand the Python arm a candidate set with no
tasks in it, and return an empty semantic half for the Issues facet forever
with nothing red anywhere.
They are now generated from one table. These tests pin the property that made
the trap possible, so it cannot come back by a different route.
So they are generated from one table, and these tests pin the property that
makes the trap possible, so it cannot come back by a different route.
"""
import pytest
@@ -104,8 +104,7 @@ def test_the_live_task_kinds_are_all_facets():
def test_non_task_facets_are_the_note_types_and_only_those():
"""Spelled out rather than derived, so adding a kind to `_FACETS` has to
be a deliberate edit in two places. `lesson` joined in milestone 385 step
2 (#3729) and is non-task on purpose: `is_task` IS `status is not None`,
be a deliberate edit in two places. `lesson` is non-task on purpose: `is_task` IS `status is not None`,
so a lesson that acquired a status would stop being a lesson."""
assert set(NON_TASK_FACETS) == {"note", "process", "snippet", "lesson"}
+3 -2
View File
@@ -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.
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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))
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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 -1
View File
@@ -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
+6 -6
View File
@@ -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()
+4 -4
View File
@@ -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 -1
View File
@@ -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
+1 -1
View File
@@ -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 -1
View File
@@ -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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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"}]
+2 -3
View File
@@ -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",
+2 -2
View File
@@ -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():
+3 -4
View File
@@ -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
+1 -1
View File
@@ -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)):
+5 -5
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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
+3 -3
View File
@@ -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
+7 -7
View File
@@ -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 -1
View File
@@ -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
+2 -2
View File
@@ -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
+3 -4
View File
@@ -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.
+5 -6
View File
@@ -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
+3 -4
View File
@@ -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},
+2 -2
View File
@@ -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()
+2 -2
View File
@@ -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.
"""
+2 -2
View File
@@ -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 = {}
+4 -4
View File
@@ -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.
+6 -6
View File
@@ -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."""
+4 -4
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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():
+2 -2
View File
@@ -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 -1
View File
@@ -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
+3 -3
View File
@@ -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
+5 -5
View File
@@ -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
+4 -5
View File
@@ -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
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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)"
+1 -1
View File
@@ -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,
+2 -3
View File
@@ -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.
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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():
+7 -9
View File
@@ -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
+3 -3
View File
@@ -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)
+2 -2
View File
@@ -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.
+8 -8
View File
@@ -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
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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 -1
View File
@@ -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
+2 -2
View File
@@ -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
-1
View File
@@ -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(),
+7 -7
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More