CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 44s
Step 2 of the Scribe -> Grimoire rename. - src/scribe -> src/grimoire, and every import, patch target, module path, hypercorn entry point and `python -m grimoire.restore` with it; pyproject, uv.lock and the frontend package are named grimoire. - Image git.fabledsword.com/bvandeusen/fabledgrimoire; CI's Postgres service runs as grimoire / grimoire_test. - Compose files default the DB user, database and (prod) volume to grimoire, each overridable from .env, so an install created as Scribe keeps its data by setting POSTGRES_USER, POSTGRES_DB and PGDATA_VOLUME. - Backups are written as grimoire-<ts>.tar with format grimoire-backup-tar. Archives named scribe-* with the scribe-backup-tar manifest still restore and are still counted at the destination (folder and S3), with tests. - Exports, the MCP auth realm, the ASGI scope key and the persistent logger carry the new name. Kept on purpose: the secret-sealing key label stays "scribe.secrets.v1", because every stored secret was derived under it. Plugin identity (plugin name, hooks, env vars, .scribe marker, skills) is step 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
3.2 KiB
Python
88 lines
3.2 KiB
Python
"""get_knowledge_counts — one number per facet, and an honest total.
|
|
|
|
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
|
|
|
|
import pytest
|
|
from tests.helpers import make_mock_session
|
|
|
|
|
|
def _grouped(rows):
|
|
r = MagicMock()
|
|
r.all.return_value = rows
|
|
return r
|
|
|
|
|
|
async def _counts(non_task_rows, kind_rows, **kwargs):
|
|
session = make_mock_session()
|
|
# 1) non-task rows grouped by note_type, 2) task rows grouped by task_kind
|
|
session.execute = AsyncMock(
|
|
side_effect=[_grouped(non_task_rows), _grouped(kind_rows)]
|
|
)
|
|
with patch("grimoire.services.knowledge.async_session") as cls:
|
|
cls.return_value = session
|
|
from grimoire.services.knowledge import get_knowledge_counts
|
|
return await get_knowledge_counts(user_id=1, **kwargs), session
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_every_facet_gets_a_number_including_the_kinds():
|
|
counts, _ = await _counts(
|
|
[("note", 395), ("process", 3), ("snippet", 90)],
|
|
[("work", 2104), ("issue", 435), ("spike", 1), ("plan", 90)],
|
|
)
|
|
assert counts["note"] == 395
|
|
assert counts["process"] == 3
|
|
assert counts["snippet"] == 90
|
|
assert counts["issue"] == 435
|
|
assert counts["spike"] == 1
|
|
assert counts["plan"] == 90
|
|
assert counts["work"] == 2104
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_task_is_the_sum_of_its_kinds():
|
|
"""`task` is not counted separately any more — it is what the kinds add up
|
|
to, so the two can't disagree."""
|
|
counts, _ = await _counts([], [("work", 2104), ("issue", 435), ("spike", 1)])
|
|
assert counts["task"] == 2540
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_total_counts_snippets_and_counts_no_task_twice():
|
|
"""The All chip labels a feed that contains every kind, so it has to count
|
|
every kind — and exactly once. Kinds are subsets of `task`; adding them
|
|
would count each issue a second time."""
|
|
counts, _ = await _counts(
|
|
[("note", 10), ("process", 2), ("snippet", 5)],
|
|
[("work", 20), ("issue", 4)],
|
|
)
|
|
assert counts["task"] == 24
|
|
assert counts["total"] == 10 + 2 + 5 + 24
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_absent_facets_report_zero_rather_than_missing():
|
|
counts, _ = await _counts([("note", 1)], [])
|
|
assert counts["note"] == 1
|
|
# 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
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_tag_filter_narrows_both_axes():
|
|
"""A tag has to reach both queries, or the chips would disagree with each
|
|
other under a filter — tasks narrowed, notes not."""
|
|
_, session = await _counts([], [], tags=["python"])
|
|
assert session.execute.await_count == 2
|
|
for call in session.execute.await_args_list:
|
|
assert "notes.tags" in str(call.args[0])
|