@@ -0,0 +1,697 @@
""" Phase 155 task 04 E2E (Playwright, mock-only): the new voice, end to end.
The dedicated phase suite for ``155_folder_summary_voice`` (A16 — one
Playwright file per phase, run in isolation): the owner ' s audit found
that every ``lite`` folder summary opened with the formulaic " This
folder contains… " catalogue. The fix under test, proven here end to
end: a KB change triggers the sync ' s folder regeneration (the
change-gated path, phase 127) under the deterministic mock, the
STORED ``folder_summaries`` row for the regenerated folder carries the
mock ' s new-voice canned text — ``Documentation for <folder>.`` (a
natural opener naming the folder, NEVER the two forbidden " This folder
contains " / " The <folder> folder contains " openers — phase 155 ' s pin)
— and the RAG catalogue (the phase-97 view) renders that stored voice
in its Description cells.
Run in isolation (DB must be up: ``podman compose up -d db``):
uv run pytest tests/e2e/test_folder_summary_voice.py -v --no-cov
MOCK-ONLY suite: ``E2E_REAL_LLM=1`` is not supported — the gate is the
deterministic mock (the ``FOLDER_SUMMARY_MODE`` branch ' s canned
``Documentation for <folder>.`` line, ``tests/e2e/mock_llm.py``): the
stored rows are a pure function of the request (the label is the
``Folder: …`` header tail — ``<source>`` for the source root,
``<source>/<folder>`` for a folder), so the exact-text and the
forbidden-opener assertions are deterministic only against the mock.
No slow-LLM proxy: the assertions are on the POST-terminal DB state
(the rows ' ``summary`` AND ``updated_at``) plus the terminal status
(the keep-final-counts convention — the terminal keeps
``summaries_done == summaries_total == <affected count>``) and the
rendered RAG view, never on in-flight sampling, so the fast mock is
dense enough by construction.
KB fixture — one host temp dir (``tmp_path_factory``; the app runs on
the same host) registered as a ``kind=local`` source (the
``test_local_directory_sources.py`` / phase-94 API-registration
pattern; no git anywhere): ``voice/`` with TWO 2-doc folders —
``alpha/`` (the folder every test changes) and ``bravo/`` (the
unchanged sibling — the phase-127 regression guard) — plus ONE
root-level doc (5 documents total). The generator ' s candidate set is
therefore exactly 3: the source root (``folder_path " " ``) + the two
folders (the recursive-subtree ≥ 1 rule).
Autouse cleanup (the phase-96/127 pattern): before each test wait for
no running sync on the module app, then truncate the shared registry +
KB tables (documents/chunks/query_log/steering_notes/kb_overview/
git_sources/folder_summaries) — each test seeds its own source and
runs its own syncs; after each test the same truncate. (The temp tree
itself persists for the module; the tests ' disk mutations are content
revisions of existing fixture files — no file is ever added or
removed — so every test ' s syncs are deltas against its own truncated
KB and the counts below hold independently of the tree state the
previous test left behind.)
Test → observable mapping (Playwright Mapping Rule):
1. ``test_regen_summary_avoids_forbidden_openers`` — a first (full)
sync settles ``success``; ONE doc inside ``alpha/`` is content-
revised and the second sync (the change-gated path, phase 127)
settles ``success`` with the single-file detail and
``summaries_done == summaries_total == 2`` (the AFFECTED count —
alpha + the source root): the stored ``folder_summaries`` row for
``alpha`` EQUALS the mock ' s canned reply for that folder
(``Documentation for voice/alpha.`` — the exact string, a pure
function of the request) AND does not start with " This folder
contains " or " The voice/alpha folder contains " (the phase-155
shape pin — the regenerated voice, not the formulaic catalogue),
while the source-root row — the other affected row — likewise
carries its exact canned line.
2. ``test_rag_table_renders_the_stored_voice`` — the same
sync-#1 + alpha-revision + sync-#2 sequence (the regenerated
folder ' s row is re-stamped under the new instruction), then the
REAL admin login (the ``auth_helpers`` form pattern) opens the RAG
view: the top-level source row ' s Description cell renders the
stored source-root voice (the cell text AND its ``title`` — the
phase-99 one-line clamp keeps the FULL text in the DOM and on the
hover escape hatch, so the assertion holds whether or not the
display ellipsizes); drilling into the source, the ``alpha`` row ' s
Description cell renders ``Documentation for voice/alpha.`` (text +
``title``) and the ``bravo`` row its own canned line, and the
level block shows the stored source-root voice — the stored voice
is what the catalogue renders, end to end.
3. ``test_unchanged_folders_keep_their_rows`` (the phase-127
regression guard) — sync #1 (full), capturing every row ' s
``(summary, manually_edited, updated_at)``; ONE doc inside
``alpha/`` is content-revised and sync #2 settles ``success`` with
``summaries_done == summaries_total == 2`` (the AFFECTED count):
the ``bravo`` row is BYTE-IDENTICAL — same ``summary`` AND same
``updated_at`` (a KB change in folder A does not touch folder B ' s
stored row), while the ``alpha`` AND the source-root rows are
re-stamped (``updated_at`` strictly after the captured stamp, the
canned text, still AI-written) — asserted through the DB the way
the phase-127 suite does.
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import time
from collections . abc import Iterator
from datetime import datetime
from pathlib import Path
from typing import Any
import httpx
import pytest
from playwright . sync_api import Page , expect
from sqlalchemy import select , text
from app . config import Settings as _Settings
from app . db import SessionLocal
from app . models import FolderSummary
from e2e . auth_helpers import login
from e2e . conftest import (
ADMIN_PASSWORD ,
SESSION_SECRET ,
USE_REAL_LLM ,
_wait_http ,
)
REPO = Path ( __file__ ) . resolve ( ) . parents [ 2 ]
# Phase 79 (task 04, full inventory): the conftest session app owns its
# port in a combined run — this module's app binds its own port instead
# (a same-port second uvicorn dies on bind and would drive the wrong
# server). Env-overridable.
APP_PORT = int ( os . environ . get ( " E2E_APP_PORT_FVOICE " , " 8158 " ) )
APP_URL = f " http://127.0.0.1: { APP_PORT } "
# --------------------------------------------------------------------------
# Fixture constants (deterministic, token-controlled)
# --------------------------------------------------------------------------
#: The local source — the temp directory's basename (``kind=local`` →
#: the directory's basename is the source name, phase 38).
SOURCE = " voice "
#: The folder every test changes (A) and the unchanged sibling (B —
#: the phase-127 regression guard's keeper).
FOLDER_A = " alpha "
FOLDER_B = " bravo "
ROOT_NOTE = " root-note.md " # the one root-level doc
N_FILES = 5 # two folders × 2 docs + the root-level doc
N_CANDIDATES = 3 # the source root + the two folders (the ≥ 1-doc rule)
#: The mock's byte-stable ``FOLDER_SUMMARY_MODE`` lines for this fixture
#: (the phase-94 template — the label is the ``FOLDER_HEADER_PREFIX``
#: tail: ``<source>`` for the root, ``<source>/<folder>`` for a folder;
#: the phase-155 voice: a natural opener naming the folder, never the
#: forbidden "This folder contains…" openers).
SUMMARY_FOR = " Documentation for {} . "
EXPECTED : dict [ str , str ] = {
" " : SUMMARY_FOR . format ( SOURCE ) ,
FOLDER_A : SUMMARY_FOR . format ( f " { SOURCE } / { FOLDER_A } " ) ,
FOLDER_B : SUMMARY_FOR . format ( f " { SOURCE } / { FOLDER_B } " ) ,
}
#: The phase-155 shape pin (test 1): the forbidden openers the new
#: instruction bans — the generic "This folder contains" opener and the
#: "The <folder name> folder contains" form with <folder name> bound to
#: the mock's own label (``voice/alpha``) and to the bare folder name
#: (``alpha``) — the stored voice must open with none of them.
FORBIDDEN_PREFIXES : tuple [ str , . . . ] = (
" This folder contains " ,
f " The { SOURCE } / { FOLDER_A } folder contains " ,
f " The { FOLDER_A } folder contains " ,
)
def _forbidden_hits ( summary : str ) - > list [ str ] :
""" The forbidden openers the text starts with (``[]`` = the shape
pin holds — the summary opens in the varied voice). """
return [ p for p in FORBIDDEN_PREFIXES if summary . startswith ( p ) ]
#: Test 1's single-file revision — a revised body whose hash differs
#: from the fixture's (the importer's sha256 delta reports ``updated``)
#: and which no other fixture text contains.
ALPHA_A_REVISED_1 = (
" Voice alpha fixture note A, REVISED-1: the only file test 1 changed "
" — the change-gated regeneration must re-voice exactly this file ' s "
" chain (alpha + the source root) under the new instruction. "
" (RESE-FVOICE-01) "
)
#: Test 2's single-file revision (a fresh distinctive body — the temp
#: tree persists across the module, so the second revision is a delta
#: against the state test 1's revision left behind).
ALPHA_A_REVISED_2 = (
" Voice alpha fixture note A, REVISED-2: the one changed file of "
" test 2 ' s sync — the regenerated folder whose stored voice the RAG "
" catalogue must render. (RESE-FVOICE-02) "
)
#: Test 3's single-file revision (``alpha-b.md`` — a different file, so
#: the revision is a delta against test 3's own truncated KB).
ALPHA_B_REVISED_3 = (
" Voice alpha fixture note B, REVISED-3: the one changed file of "
" test 3 ' s sync — bravo ' s row must stay byte-identical through it. "
" (RESE-FVOICE-03) "
)
def _md ( title : str , body : str ) - > str :
return f " # { title } \n \n { body } \n "
# --------------------------------------------------------------------------
# Fixtures
# --------------------------------------------------------------------------
@pytest.fixture ( scope = " module " )
def fvoice_dir ( tmp_path_factory : pytest . TempPathFactory ) - > Path :
""" The one-source temp tree (see the module docstring): the app
server runs on the same host, so the paths are visible to it. The
directory NAME is the source name (``kind=local``, phase 38). """
root = tmp_path_factory . mktemp ( " bor_fvoice " )
src = root / SOURCE
for folder in ( FOLDER_A , FOLDER_B ) :
( src / folder ) . mkdir ( parents = True )
for letter in ( " a " , " b " ) :
( src / folder / f " { folder } - { letter } .md " ) . write_text (
_md (
f " Voice { folder . title ( ) } { letter . upper ( ) } " ,
f " Voice { folder } fixture note { letter . upper ( ) } : covers "
f " topic { letter . upper ( ) } of the { SOURCE } source tree. " ,
) ,
encoding = " utf-8 " ,
)
( src / ROOT_NOTE ) . write_text (
_md (
" Voice Root Note " ,
f " This file sits directly under the { SOURCE } source, not in "
" any folder. " ,
) ,
encoding = " utf-8 " ,
)
assert len ( list ( src . rglob ( " *.md " ) ) ) == N_FILES
return src
def _app_env (
base_url : str , tmp_path_factory : pytest . TempPathFactory
) - > dict [ str , str ] :
""" The per-module app env (the conftest pattern, cf.
``test_incremental_folder_summaries.py``): the ``base_url`` is the
mock LLM directly (the fast leg is the only leg — see the module
docstring), ``BOR_GIT_SOURCES`` forced empty (the sync source is
this suite ' s own DB-registered local row), and the leak-guarded
code defaults. """
env = dict ( os . environ )
env . pop ( " DEBUGPY " , None )
env [ " BOR_ENVIRONMENT " ] = " e2e "
env [ " BOR_STATIC_DIR " ] = str ( REPO / " frontend " )
env [ " BOR_LLM_BASE_URL " ] = (
" https://aipi.reeseapps.com/v1 " if USE_REAL_LLM else base_url
)
# Mock-calibrated threshold (conftest pattern): this suite never
# asks the chat model anything — the gate is never on a path.
env [ " BOR_RELEVANCE_THRESHOLD " ] = " 0.30 "
# Phase 67: instant retry waits + the code-default budget (the
# conftest leak-guard pattern).
env [ " BOR_LLM_RETRY_DELAY " ] = " 0 "
env [ " BOR_LLM_RETRIES " ] = str ( _Settings . model_fields [ " llm_retries " ] . default )
env . setdefault (
" BOR_DATABASE_URL " ,
" postgresql+psycopg://reese:reese@localhost:5432/brain_of_reese " ,
)
# Phase 16: admin auth must be set or create_app() refuses to boot.
env [ " BOR_ADMIN_PASSWORD " ] = ADMIN_PASSWORD
env [ " BOR_SESSION_SECRET " ] = SESSION_SECRET
# The repo's .env file carries the owner's BOR_GIT_SOURCES (the app
# reads it from cwd) — override it with an EMPTY value (the env var
# beats the .env file): the registry must hold EXACTLY the local
# directory this suite registers.
env [ " BOR_GIT_SOURCES " ] = " "
# Leak guards (conftest pattern): an operator's local (gitignored)
# .env cannot leak corpus-specific settings into the app under test.
env [ " BOR_DOCS_REPO " ] = " "
env [ " BOR_SUGGESTIONS " ] = json . dumps (
_Settings . model_fields [ " suggestions " ] . default
)
env [ " BOR_INPUT_PLACEHOLDER " ] = _Settings . model_fields [ " input_placeholder " ] . default
env [ " BOR_FOOTER_TEXT " ] = _Settings . model_fields [ " footer_text " ] . default
env [ " BOR_SOURCES_DIR " ] = str ( tmp_path_factory . mktemp ( " bor_checkouts " ) )
return env
@pytest.fixture ( scope = " module " )
def app_server (
mock_llm : int ,
fvoice_dir : Path ,
tmp_path_factory : pytest . TempPathFactory ,
) - > Iterator [ str ] :
""" The app under test (all three tests): ``BOR_LLM_BASE_URL`` is
the mock directly — the canned ``FOLDER_SUMMARY_MODE`` lines are
byte-stable there, and the exact-text and shape assertions key on
that exact text (no slow proxy: the assertions are post-terminal,
see the module docstring). ``fvoice_dir`` is a dependency only for
the fixture ordering (the temp tree exists before the app boots —
the sync reads it). """
env = _app_env ( f " http://127.0.0.1: { mock_llm } /v1 " , tmp_path_factory )
proc = subprocess . Popen (
[ sys . executable , " -m " , " uvicorn " , " app.main:app " ,
" --host " , " 127.0.0.1 " , " --port " , str ( APP_PORT ) , " --log-level " , " warning " ] ,
cwd = REPO ,
env = env ,
)
try :
_wait_http ( f " { APP_URL } /api/health " )
yield APP_URL
finally :
proc . terminate ( )
try :
proc . wait ( timeout = 10 )
except subprocess . TimeoutExpired :
proc . kill ( )
@pytest.fixture ( scope = " module " )
def app_url ( app_server : str ) - > str :
return app_server
def _truncate_all ( ) - > None :
""" Fresh registry + KB (the E2E isolation pattern): the E2E suites
share one Postgres, so a leftover git_sources row or document would
pollute the candidate count and the counts the syncs and the
folder-rows assertions pin exactly. """
with SessionLocal ( ) as db :
db . execute (
text (
" TRUNCATE chunks, documents, query_log, steering_notes, "
" kb_overview, git_sources, folder_summaries "
)
)
db . commit ( ) # without the commit the TRUNCATE rolls back (the house pattern)
def _wait_sync_settled ( base_url : str , timeout_s : float = 180.0 ) - > None :
""" No background sync may leak across tests (the run state lives in
the app ' s memory, and a still-running run would keep importing into
the NEXT test ' s truncated KB): wait for the app ' s sync status to
be non-running BEFORE the truncate. Own admin session (the
endpoint is admin-only) — usually a no-op: the tests settle only
after their run ' s terminal state. """
with httpx . Client ( base_url = base_url , timeout = 5.0 ) as client :
r = client . post ( " /api/login " , json = { " password " : ADMIN_PASSWORD } )
assert r . status_code == 204 , r . text
deadline = time . monotonic ( ) + timeout_s
while time . monotonic ( ) < deadline :
r = client . get ( " /api/sync/status " )
assert r . status_code == 200 , r . text
if r . json ( ) [ " state " ] != " running " :
return
time . sleep ( 0.2 )
raise AssertionError ( f " a sync was still running at a test boundary: { base_url } " )
@pytest.fixture ( autouse = True )
def _clean ( app_url : str , db_ready : None ) - > Iterator [ None ] :
""" Per-test KB isolation (the phase-96/127 cleanup pattern): the
shared E2E Postgres is truncated before AND after every test —
each test seeds its own source and runs its own syncs, so the KB
rows + folder_summaries rows of the temp source are exactly this
test ' s own. The pre-wait keeps a leaked run from the previous test
(the module app is shared) from racing the truncate. """
_wait_sync_settled ( app_url )
_truncate_all ( )
yield
_truncate_all ( )
# --------------------------------------------------------------------------
# Helpers
# --------------------------------------------------------------------------
def _register_source ( base_url : str , path : str ) - > None :
""" Register the temp directory through the authenticated API (the
``test_local_directory_sources.py`` / phase-94 pattern — a plain
directory is an API/DB-only operation). """
with httpx . Client ( base_url = base_url , timeout = 30.0 ) as client :
r = client . post ( " /api/login " , json = { " password " : ADMIN_PASSWORD } )
assert r . status_code == 204 , r . text
r = client . post (
" /api/git-sources " , json = { " kind " : " local " , " path " : path }
)
assert r . status_code == 201 , r . text
def _run_sync ( base_url : str , timeout_s : float = 120.0 ) - > dict [ str , Any ] :
""" Login + ``POST /api/sync`` + poll the status endpoint until THIS
run reaches a terminal state (the ``test_local_directory_sources``
pattern, over plain httpx — with the stale-terminal freshness gate:
the module app is shared, so the PREVIOUS run ' s terminal lives in
the app ' s memory until this run supersedes it; a terminal is
accepted only after a running tick was observed or its
``started_at`` post-dates this POST). """
with httpx . Client ( base_url = base_url , timeout = 30.0 ) as client :
r = client . post ( " /api/login " , json = { " password " : ADMIN_PASSWORD } )
assert r . status_code == 204 , r . text
t0 = time . time ( )
r = client . post ( " /api/sync " )
assert r . status_code == 202 , r . text
saw_running = False
deadline = time . monotonic ( ) + timeout_s
body : dict [ str , Any ] = { }
while time . monotonic ( ) < deadline :
r = client . get ( " /api/sync/status " )
assert r . status_code == 200 , r . text
body = r . json ( )
state = body [ " state " ]
if state == " running " :
saw_running = True
elif state in ( " success " , " failed " ) and (
saw_running
or _started_at ( body ) > = t0 - 1.0 # same-host clock; 1 s tolerance
) :
return body
time . sleep ( 0.1 )
raise AssertionError ( f " sync did not reach a terminal state: { body } " )
def _started_at ( body : dict [ str , Any ] ) - > float :
""" The status body ' s ``started_at`` as a wall-clock epoch (the
server stamps it in UTC; this process runs on the same host —
same clock, the phase-64 recorder ' s freshness convention). """
started = body . get ( " started_at " )
if not started :
return 0.0
try :
return datetime . fromisoformat ( str ( started ) ) . timestamp ( )
except ValueError :
return 0.0
def _folder_rows ( ) - > dict [ str , tuple [ str , bool , datetime ] ] :
""" The source ' s stored folder summaries
`` { folder_path: (summary, manually_edited, updated_at)}``
(`` " " `` = the source root) — the test process ' s direct DB access
(the E2E ' s other established lens). """
with SessionLocal ( ) as db :
rows = db . execute (
select (
FolderSummary . folder_path ,
FolderSummary . summary ,
FolderSummary . manually_edited ,
FolderSummary . updated_at ,
) . where ( FolderSummary . source == SOURCE )
) . all ( )
return {
folder : ( summary , manually , updated )
for folder , summary , manually , updated in rows
}
def _first_sync ( app_url : str , fvoice_dir : Path ) - > dict [ str , Any ] :
""" The suite ' s precondition (every test): register the temp source
and run the first (full) sync under the deterministic mock —
settled ``success`` with all 5 docs added and the 3 candidate rows
generated (the phase-127 suite ' s sequence). """
_register_source ( app_url , str ( fvoice_dir ) )
body = _run_sync ( app_url )
assert body [ " state " ] == " success " , body
detail = body [ " detail " ]
assert detail [ " added " ] == N_FILES , detail
assert detail [ " updated " ] == 0 and detail [ " unchanged " ] == 0 , detail
assert detail [ " pruned " ] == 0 , detail
assert detail [ " overview " ] is True , detail
return body
def _revise_alpha_a ( fvoice_dir : Path , body : str ) - > None :
""" Test 1 ' s / test 2 ' s KB change: ONE doc inside ``alpha/`` gets
new content (the importer ' s sha256 delta reports ``updated`` — no
add, no prune). """
target = fvoice_dir / FOLDER_A / f " { FOLDER_A } -a.md "
target . write_text ( _md ( f " Voice { FOLDER_A . title ( ) } A (revised) " , body ) , encoding = " utf-8 " )
def _assert_change_terminal ( body : dict [ str , Any ] ) - > None :
""" The second sync ' s terminal (the keep-final-counts convention):
the AFFECTED count — alpha + the source root — the phase-127
observable at the wire, plus the single-file detail. """
assert body [ " state " ] == " success " , body
detail = body [ " detail " ]
assert detail [ " added " ] == 0 , detail
assert detail [ " updated " ] == 1 , detail
assert detail [ " unchanged " ] == N_FILES - 1 , detail
assert detail [ " pruned " ] == 0 , detail
assert detail [ " overview " ] is True , detail
assert body [ " summaries_done " ] == 2 , body
assert body [ " summaries_total " ] == 2 , body
assert body [ " phase " ] is None , body
assert body [ " current_summary " ] is None , body
def _drill ( page : Page , * names : str ) - > None :
""" Drill one level at a time (client-side — no fetch, no URL
change): each name is the EXACT text of the source/folder link at
the current level (the row builders ' link text: the source name,
or the folder ' s last path segment). """
for name in names :
page . click ( f ' #folders-tbody a.folder-link:text-is( " { name } " ) ' )
def _expect_desc_cell (
page : Page , row_index : int , full_text : str
) - > None :
""" The RAG view ' s Description cell (the phase-97 catalogue idiom —
``td:nth-child(4)``; the phase-99 one-line clamp): the cell ' s text
span carries the STORED text (``textContent`` only — the house
rule; the clamp is VISUAL only, so the full text stays in the DOM)
and its ``title`` carries the FULL unclamped string (the hover
escape hatch, ``makeDescCell`` — set whenever the description is
non-empty, whether or not the display ellipsizes). """
span = page . locator ( f " #folders-tbody tr:nth-child( { row_index + 1 } ) "
" td:nth-child(4) span " )
expect ( span ) . to_have_text ( full_text )
expect ( span ) . to_have_attribute ( " title " , full_text )
# --------------------------------------------------------------------------
# 1. The change-gated regeneration (phase 127's path) stores the mock's
# new-voice canned text — exact match, and the shape pin: none of
# the forbidden openers
# --------------------------------------------------------------------------
def test_regen_summary_avoids_forbidden_openers (
app_url : str , fvoice_dir : Path , db_ready : None
) - > None :
""" A first sync of the temp source settles ``success`` with all 5
docs added; ONE doc inside ``alpha/`` is content-revised and the
second sync (the CHANGE-GATED path — phase 127: the run ' s changed
document set reaches ``generate_folder_summaries``) settles
``success`` with the single-file detail and
``summaries_done == summaries_total == 2`` (the AFFECTED count —
alpha + the source root): the stored ``folder_summaries`` row for
``alpha`` EQUALS the mock ' s canned reply for that folder (the
EXACT string — the mock ' s line is a pure function of the request,
so the match is byte-exact) AND does not start with " This folder
contains " or " The voice/alpha folder contains " (the phase-155
shape pin); the source-root row — the other affected row —
likewise carries its exact canned line. """
_first_sync ( app_url , fvoice_dir )
_revise_alpha_a ( fvoice_dir , ALPHA_A_REVISED_1 )
body = _run_sync ( app_url )
_assert_change_terminal ( body )
rows = _folder_rows ( )
assert set ( rows ) == { " " } | { FOLDER_A , FOLDER_B } , rows
# The regenerated folder: the EXACT canned string (deterministic) +
# the shape pin (no forbidden opener).
summary , manually , _updated_at = rows [ FOLDER_A ]
assert summary == EXPECTED [ FOLDER_A ] , ( FOLDER_A , summary )
assert _forbidden_hits ( summary ) == [ ] , ( FOLDER_A , summary )
assert manually is False , ( FOLDER_A , manually )
# The other affected row (the source root) — the new voice there too.
assert rows [ " " ] [ 0 ] == EXPECTED [ " " ] , rows [ " " ]
assert _forbidden_hits ( rows [ " " ] [ 0 ] ) == [ ] , rows [ " " ]
# The unaffected sibling keeps its canned line (phase 127 — full
# guard in test 3; here only the text, the shape pin's context).
assert rows [ FOLDER_B ] [ 0 ] == EXPECTED [ FOLDER_B ] , rows [ FOLDER_B ]
# --------------------------------------------------------------------------
# 2. The RAG view (the phase-97 catalogue) renders the stored voice in
# the Description cells — the exact canned text, title attribute
# carrying the full string
# --------------------------------------------------------------------------
def test_rag_table_renders_the_stored_voice (
page : Page ,
app_url : str ,
fvoice_dir : Path ,
db_ready : None ,
) - > None :
""" The same sync-#1 + alpha-revision + sync-#2 sequence (the
regenerated folder ' s row is re-stamped under the new instruction —
the DB state test 1 pins), then the REAL admin login opens the
RAG view (the phase-97 catalogue): the top-level source row ' s
Description cell renders the stored source-root voice (the cell
text AND its ``title`` — the phase-99 clamp keeps the full text in
the DOM and on the hover escape hatch, so the assertion holds
whether or not the display ellipsizes); drilling into the source,
the ``alpha`` row ' s Description cell renders the regenerated
folder ' s exact canned line (text + ``title``) and the ``bravo``
row its own canned line, and the level block shows the stored
source-root voice — the stored voice is what the catalogue
renders, end to end. """
page . set_default_timeout ( 30_000 )
_first_sync ( app_url , fvoice_dir )
_revise_alpha_a ( fvoice_dir , ALPHA_A_REVISED_2 )
_assert_change_terminal ( _run_sync ( app_url ) )
# The regenerated folder's DB state (the precondition the view
# renders — test 1's exact-text pin, re-asserted here so the view
# assertion stands on a verified store).
rows = _folder_rows ( )
assert rows [ FOLDER_A ] [ 0 ] == EXPECTED [ FOLDER_A ] , rows [ FOLDER_A ]
login ( page , app_url ) # → /sources.html (the RAG view)
# The top level: the one source row (the ls() equivalence — the
# tree's single fetch settled) and its stored source-root voice in
# the Description cell (the exact canned line, text + title).
page . wait_for_selector ( " #folders-tbody tr " , timeout = 30_000 )
expect ( page . locator ( " #folders-tbody tr " ) ) . to_have_count ( 1 )
expect ( page . locator ( " #folders-tbody tr td:nth-child(1) a.folder-link " )
) . to_have_text ( SOURCE )
_expect_desc_cell ( page , 0 , EXPECTED [ " " ] )
# Drill into the source: the two folder rows (path order) and the
# regenerated folder's stored voice in its Description cell — the
# exact canned line (the DB state above is what the cell renders),
# plus the sibling's own canned line; the level block carries the
# source root's stored voice (the phase-97 ``#kb-level`` idiom).
_drill ( page , SOURCE )
rows_visible = page . locator ( " #folders-tbody tr " )
expect ( rows_visible ) . to_have_count ( 2 )
expect ( rows_visible . nth ( 0 ) . locator ( " a.folder-link " ) ) . to_have_text ( FOLDER_A )
_expect_desc_cell ( page , 0 , EXPECTED [ FOLDER_A ] )
expect ( rows_visible . nth ( 1 ) . locator ( " a.folder-link " ) ) . to_have_text ( FOLDER_B )
_expect_desc_cell ( page , 1 , EXPECTED [ FOLDER_B ] )
expect ( page . locator ( " #kb-level " ) ) . to_be_visible ( )
expect ( page . locator ( " #kb-level-summary " ) ) . to_have_text ( EXPECTED [ " " ] )
# The rendered voice is the new one: no Description cell in the
# level shows a forbidden opener (the shape pin, view-side).
for cell in page . locator ( " #folders-tbody td:nth-child(4) span " ) . all ( ) :
rendered = cell . text_content ( ) or " "
for prefix in FORBIDDEN_PREFIXES :
assert not rendered . startswith ( prefix ) , rendered
# --------------------------------------------------------------------------
# 3. The phase-127 regression guard: a KB change in folder A does not
# touch folder B's stored row (text AND updated_at byte-identical)
# --------------------------------------------------------------------------
def test_unchanged_folders_keep_their_rows (
app_url : str , fvoice_dir : Path , db_ready : None
) - > None :
""" Re-seed (the autouse truncate) + sync #1 (full), capturing every
row ' s ``(summary, manually_edited, updated_at)``; then ONE doc
inside ``alpha/`` is content-revised (``alpha-b.md`` — a different
file, so the revision is a delta against this test ' s own truncated
KB) and sync #2 settles ``success`` with the single-file detail
and ``summaries_done == summaries_total == 2`` (the AFFECTED
count — alpha + the source root): the ``bravo`` row is
BYTE-IDENTICAL — same ``summary`` AND same ``updated_at`` (a KB
change in folder A does not touch folder B ' s stored row — the
phase-127 contract), while the ``alpha`` AND the source-root rows
are re-stamped (``updated_at`` strictly after the captured stamp,
the canned text, still AI-written). """
_first_sync ( app_url , fvoice_dir )
before = _folder_rows ( )
assert set ( before ) == { " " } | { FOLDER_A , FOLDER_B } , before
target = fvoice_dir / FOLDER_A / f " { FOLDER_A } -b.md "
target . write_text (
_md ( f " Voice { FOLDER_A . title ( ) } B (revised) " , ALPHA_B_REVISED_3 ) ,
encoding = " utf-8 " ,
)
body = _run_sync ( app_url )
_assert_change_terminal ( body )
after = _folder_rows ( )
assert set ( after ) == { " " } | { FOLDER_A , FOLDER_B } , after
# The unchanged sibling: BYTE-IDENTICAL — same summary text AND
# same updated_at (never re-stamped, never re-generated).
assert after [ FOLDER_B ] == before [ FOLDER_B ] , (
FOLDER_B ,
before [ FOLDER_B ] ,
after [ FOLDER_B ] ,
)
# The changed chain (alpha + the source root): RE-STAMPED — a new
# stamp strictly after the captured one, the canned text, still
# AI-written (the mock labels from the folder header only, so the
# text is the same deterministic line).
for folder in ( " " , FOLDER_A ) :
summary , manually , updated_at = after [ folder ]
assert summary == EXPECTED [ folder ] , ( folder , summary )
assert manually is False , ( folder , manually )
assert updated_at > before [ folder ] [ 2 ] , (
folder ,
before [ folder ] ,
after [ folder ] ,
)