Compare commits
2
Commits
63e97a6f0f
...
914f31359c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
914f31359c | ||
|
|
79de8938b9 |
@@ -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.
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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,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,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,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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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,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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user