uncommit .agent
This commit is contained in:
@@ -1,61 +0,0 @@
|
||||
# Story: Chat RAG Answer (happy path)
|
||||
|
||||
**Phase:** `03_story_chat_rag.md` · **E2E:** `tests/e2e/test_chat_rag.py`
|
||||
|
||||
## Narrative
|
||||
|
||||
As **a user** (friend, colleague, future me), I want to ask Brain a question
|
||||
about Reese's setup and get a grounded, chippy answer that points me at the
|
||||
exact documentation — so I can actually *do* the thing.
|
||||
|
||||
- **Given** the knowledge base is imported and I type "How is my Kubernetes
|
||||
cluster set up?"
|
||||
- **When** Brain embeds the question, retrieves the top chunks by cosine
|
||||
similarity, maps them to their parent documents, and feeds the **full
|
||||
document text** to `turbo`
|
||||
- **Then** I see a streamed, upbeat answer that cites the source
|
||||
(`Homelab/kubernetes.md`), grounded in the doc's specifics (Talos,
|
||||
Cilium, the node list) — and never in anything the docs don't say.
|
||||
|
||||
## Acceptance criteria
|
||||
1. `POST /api/chat` streams SSE: `delta` events then a final `done` event
|
||||
carrying `{deflected, sources[], suggestions[]}` (PLAN §4).
|
||||
2. Retrieval: top-4 chunks (`BOR_TOP_K_CHUNKS`), cosine via pgvector
|
||||
`<=>`, score = 1 − distance.
|
||||
3. Context assembly: top-2 **distinct documents** by best-chunk score, full
|
||||
content, capped at `BOR_MAX_CONTEXT_CHARS` with truncation marker.
|
||||
4. System prompt = locked persona + HONESTY GATE rules (PLAN §6), with
|
||||
`<relevance>HIGH</relevance>` and `<documents>…</documents>`.
|
||||
5. The answer arrives **streamed** (multiple deltas), rendered live.
|
||||
6. Source chips (mono, `source/path`) render under the answer bubble.
|
||||
7. Per-turn log line emitted (PLAN §9) and a `query_log` row inserted
|
||||
(`deflected=false`, top_score, sources, latency).
|
||||
8. LLM/embedding failure → JSON/SSE error the UI turns into the error banner
|
||||
(no hang, no stale button).
|
||||
|
||||
## UI Visualization & Structure
|
||||
- Chat column centered at 46rem (PLAN §7.1); user bubble right (brand
|
||||
indigo, white text ≥4.5:1), Brain bubble left (white, ink text, avatar 🧠).
|
||||
- While generating: typing indicator → live-appended text (see
|
||||
loading-feedback story for the full state machine — this story only needs
|
||||
"deltas render as they arrive and the button is busy throughout").
|
||||
- **Source chips:** pill, `font-family: mono`, `bg --brand-soft`,
|
||||
`color --brand-ink` (6.3:1), `max-width` + ellipsis; each shows
|
||||
`Homelab/kubernetes.md`. `aria-label` when truncated.
|
||||
- Bubble content is safe-rendered markdown (escape-first local renderer —
|
||||
`<script>` in an LLM answer must NOT execute).
|
||||
- On mobile the bubbles expand to ~92% width; chips wrap.
|
||||
|
||||
## Playwright Mapping Rule
|
||||
**Test Scenario → `tests/e2e/test_chat_rag.py`** (mock LLM, seeded KB):
|
||||
1. `test_on_topic_question_streams_grounded_answer` — type "How is my
|
||||
Kubernetes cluster set up?", submit; assert: answer bubble appears with
|
||||
streamed content (mock's answer references the question), a `.source-chip`
|
||||
containing `kubernetes.md` is present, send button returns to enabled
|
||||
"Send".
|
||||
2. `test_chat_logs_query` — after the turn, `GET /api/health` is still ok AND
|
||||
(via a test-only detail: query the DB directly) a `query_log` row exists
|
||||
with `deflected=false` and sources including `kubernetes.md`.
|
||||
3. `test_sse_stream_shape` — raw `httpx` streaming request to `/api/chat`:
|
||||
assert multiple `data:` delta events precede a `done` event with
|
||||
`deflected: false` and a non-empty `sources` list.
|
||||
@@ -1,57 +0,0 @@
|
||||
# Story: Honest Deflection
|
||||
|
||||
**Phase:** `04_story_honest_deflection.md` · **E2E:** `tests/e2e/test_honest_deflection.py`
|
||||
|
||||
## Narrative
|
||||
|
||||
As **a user**, when I ask something Brain genuinely has no notes about, I
|
||||
want it to **admit it plainly** and still be helpful — so I never walk away
|
||||
with a confident-sounding hallucination.
|
||||
|
||||
- **Given** the knowledge base is about homelab/infra topics
|
||||
- **When** I ask "How do I bake sourdough bread?"
|
||||
- **Then** retrieval's best similarity is below `BOR_RELEVANCE_THRESHOLD`,
|
||||
Brain switches to deflection mode, opens with a variant of
|
||||
**"I haven't done anything like that"**, stays chippy, and offers 2–3
|
||||
alternative questions about things it *does* know (from the weak hits).
|
||||
|
||||
## Acceptance criteria
|
||||
1. Gate: `max(1 − cosine_distance) < BOR_RELEVANCE_THRESHOLD` ⇒
|
||||
`<relevance>LOW</relevance>` + `DEFLECT_MODE` system prompt (weak-hit
|
||||
**titles only**, no full docs).
|
||||
2. The LLM is still called (voice stays chippy); the prompt forces the
|
||||
honesty phrasing + alternative suggestions (PLAN §6).
|
||||
3. `done` event carries `deflected: true` and `suggestions[]` (2–3 strings).
|
||||
4. `query_log` row has `deflected=true` + the weak `top_score`.
|
||||
5. UI: the deflected bubble is visually distinct (amber border/background),
|
||||
and "Maybe try:" chips render below it; clicking a chip asks that
|
||||
question (delegated to the suggestion-chips story for chip behavior;
|
||||
here only rendering).
|
||||
6. Threshold is env-tunable; lowering it to ~0 makes every question an
|
||||
"answer" (documented in README troubleshooting).
|
||||
7. Unit tests cover the gate boundary (score == threshold → answer mode;
|
||||
just below → deflect) using a fake retriever — no LLM needed.
|
||||
|
||||
## UI Visualization & Structure
|
||||
- Deflected brain bubble: `background: var(--accent-bg) #fff7e8`,
|
||||
`border: 1px solid var(--accent-line) #f59e0b`, text stays `var(--ink)`
|
||||
(or accent-ink for emphasis ≥4.5:1) — clearly "different" from a normal
|
||||
answer without being alarm-red (it's honesty, not an error).
|
||||
- Below the bubble: `Maybe try:` label (visually hidden for SR, `aria-label`
|
||||
on the chip group) + 2–3 `.suggestion-chip` pills (same chip component as
|
||||
onboarding: ≥44px height, brand-soft bg, brand-ink text).
|
||||
- Bubble may include the model's alternative list in text too; chips are the
|
||||
one-click affordance.
|
||||
- Contrast audit: `#92400e` on `#fff7e8` ≈ 8.7:1 ✓; chip text on chip bg ≥6:1 ✓.
|
||||
|
||||
## Playwright Mapping Rule
|
||||
**Test Scenario → `tests/e2e/test_honest_deflection.py`** (mock LLM, seeded KB):
|
||||
1. `test_off_topic_question_deflects_honestly` — ask "How do I bake
|
||||
sourdough bread?"; assert the answer bubble is `.is-deflected`, its text
|
||||
matches /haven't done anything like that/i, and ≥2 "Maybe try:" chips
|
||||
render below it.
|
||||
2. `test_deflection_suggestions_are_clickable` — click the first deflection
|
||||
chip; assert the input is populated/focus behavior per chip contract and a
|
||||
new user bubble is created.
|
||||
3. `test_threshold_gate_unit_boundary` is a **unit** test (not Playwright):
|
||||
retriever returns score 0.30 → HIGH; 0.2999 → LOW (mocked components).
|
||||
@@ -1,62 +0,0 @@
|
||||
# Story: Import Documents
|
||||
|
||||
**Phase:** `02_story_import_documents.md` · **E2E:** `tests/e2e/test_import_documents.py`
|
||||
|
||||
## Narrative
|
||||
|
||||
As **Reese** (the owner), I want to point the importer at one or more
|
||||
directories of markdown files and have them chunked, embedded, and stored in
|
||||
Postgres — so that Brain's answers always reflect my *current* documentation.
|
||||
|
||||
- **Given** the `~/Homelab` and `~/Deployments` trees (or any `--source` dirs)
|
||||
- **When** I run `uv run python -m scripts.import_docs`
|
||||
- **Then** every `*.md` file (after the exclusion list) is present in the
|
||||
`documents` table with its full content, a sha256 hash, and chunk rows with
|
||||
768-dim embeddings; unchanged files are skipped on re-runs; and the
|
||||
Sources page in the browser shows the indexed documents.
|
||||
|
||||
## Acceptance criteria
|
||||
1. `scripts/import_docs.py` accepts repeatable `--source PATH` (default
|
||||
`~/Homelab` `~/Deployments`), `--prune`, and `--limit N` (debug).
|
||||
2. Only `*.md` files are imported; excluded dirs: `.venv`, `node_modules`,
|
||||
`.git`, `__pycache__`, `.pytest_cache`, `dist`, `build` (PLAN A9).
|
||||
3. Delta detection by sha256 on `(source, path)`: unchanged → skipped
|
||||
(no re-embedding); changed → re-chunked + re-embedded, old chunks
|
||||
replaced atomically.
|
||||
4. Embeddings are batched (`BOR_EMBED_BATCH_SIZE`) against `aipi /v1/embeddings`
|
||||
(`embed`); a dimension mismatch fails loudly with an actionable message.
|
||||
5. Rich per-file logging (`added|updated|unchanged|pruned`) + summary.
|
||||
6. `GET /api/docs` returns the document list; the Sources page renders it
|
||||
(stat cards + table) or the designed empty state when none exist.
|
||||
7. The whole flow works against the **mock LLM** in E2E (deterministic),
|
||||
and against real aipi for manual runs.
|
||||
|
||||
## UI Visualization & Structure
|
||||
- **Sources page (`/sources.html`), desktop:** header row (h1 + sub), then
|
||||
stat cards in `repeat(auto-fit, minmax(170px,1fr))` (documents / chunks /
|
||||
last indexed), then a **full-width table** inside a scroll wrapper
|
||||
(min-width 640px → horizontal scroll, never a squeezed hairline list).
|
||||
Columns: Source · Path (mono, ellipsized w/ `title`) · Title · Chunks ·
|
||||
Indexed. Uses ≥85% of the 72rem container width.
|
||||
- **Empty state (no docs):** centered card with 📂, "Nothing indexed yet",
|
||||
and the exact import command in a `<code>` pill. No dead links, no
|
||||
placeholder tables.
|
||||
- **Accessibility:** `<caption class="visually-hidden">` on the table,
|
||||
`scope="col"` on headers, `role="region"` + `tabindex="0"` on the scroll
|
||||
wrapper (keyboard scrollable), stat values have visible labels.
|
||||
- **Mobile:** stat cards stack (auto-fit), table scrolls horizontally,
|
||||
no content below the fold is unreachable.
|
||||
|
||||
## Playwright Mapping Rule
|
||||
**Test Scenario → `tests/e2e/test_import_documents.py`** (one isolated
|
||||
Playwright suite for this story):
|
||||
1. *Seeding:* run the import function in-process against
|
||||
`tests/fixtures/docs/` (mock embeddings, temp DB state) — a fixture, not
|
||||
the test's subject.
|
||||
2. `test_sources_page_lists_indexed_docs` — goto `/sources.html`, assert stat
|
||||
cards show the fixture counts and the table rows include
|
||||
`homelab/kubernetes.md`, `homelab/backups.md`, `deployments/new-service.md`.
|
||||
3. `test_sources_table_layout` — table wrapper width ≥80% of container;
|
||||
`caption` present; on a 375px viewport the wrapper scrolls horizontally.
|
||||
4. `test_empty_state_when_no_docs` (fresh/truncated DB) — empty state visible
|
||||
with the import command; table hidden.
|
||||
@@ -1,61 +0,0 @@
|
||||
# Story: Loading Feedback & Progress
|
||||
|
||||
**Phase:** `06_story_loading_feedback.md` · **E2E:** `tests/e2e/test_loading_feedback.py`
|
||||
|
||||
## Narrative
|
||||
|
||||
As **a user**, local LLM answers can take 10–30+ seconds. I want to *always*
|
||||
know Brain is working — a clear "thinking" state, live progress as tokens
|
||||
arrive, and a definitive end — so I never stare at a stale Send button
|
||||
wondering if it's stuck.
|
||||
|
||||
- **Given** I submit a question
|
||||
- **When** the answer is in flight (pre-token, streaming, or erroring)
|
||||
- **Then** the UI shows an unambiguous in-progress state, transitions
|
||||
cleanly to done/error, and the send button is never left in a zombie state.
|
||||
|
||||
## Acceptance criteria
|
||||
1. **Pre-token:** typing-indicator bubble (3 animated dots, `role="status"`,
|
||||
`aria-label="Brain of Reese is thinking"`) + send button disabled with
|
||||
spinner and label "Thinking…".
|
||||
2. **Streaming:** first delta replaces the typing indicator; text appends
|
||||
live; button stays busy until `done`.
|
||||
3. **Done:** button re-enabled, label "Send", input focused back.
|
||||
4. **Error paths:** (a) LLM/DB error → red banner `role="alert"` with retry
|
||||
hint, button re-enabled; (b) **120s client timeout** → same error state
|
||||
(guard against a hung stream); (c) page reload mid-stream loses the
|
||||
stream but the composer is usable again (state is turn-local).
|
||||
5. **Slow-model E2E:** the mock LLM's 3s warm-up (message containing
|
||||
"pretend to think slowly") must show the typing indicator for ≥2s before
|
||||
any text appears.
|
||||
6. Server side: per-turn log includes `embed_ms` / total `total_ms` (PLAN
|
||||
§9) so "slow" is diagnosable.
|
||||
7. `prefers-reduced-motion`: dots/spinner still visible (slower/static) —
|
||||
feedback is never removed, only calmed.
|
||||
|
||||
## UI Visualization & Structure
|
||||
- State machine (single source of truth in `app.js`):
|
||||
`idle → thinking → streaming → done | error → idle`.
|
||||
- Typing indicator: 8px dots, `--ink-soft`, staggered 1.2s bounce; inside a
|
||||
normal brain bubble (same geometry as answers) so the layout doesn't jump.
|
||||
- Send button busy style: `background: #a5b4fc` (disabled contrast still
|
||||
fine — it's a disabled state), 16px spinner (2.5px ring, white top
|
||||
arc), label swap "Send" ↔ "Thinking…".
|
||||
- Error banner: `--err-bg/--err-ink/--err-line`, top of chat shell,
|
||||
`role="alert"`, includes the actionable hint ("Try again — if this
|
||||
persists, check the LLM is reachable").
|
||||
- Elapsed-time hint: after 10s still pre-token, the typing bubble's aria
|
||||
label becomes "…still thinking (12s)" — SR users are never left guessing.
|
||||
|
||||
## Playwright Mapping Rule
|
||||
**Test Scenario → `tests/e2e/test_loading_feedback.py`** (mock LLM):
|
||||
1. `test_typing_indicator_during_slow_think` — ask "pretend to think slowly
|
||||
then tell me about kubernetes"; assert `#typing-indicator` visible within
|
||||
500ms of submit, still visible at ~2s, gone by the time the answer text
|
||||
is present.
|
||||
2. `test_button_state_machine` — during the in-flight turn: `#send-btn`
|
||||
disabled + label "Thinking…"; after done: enabled + "Send".
|
||||
3. `test_streaming_appends_live` — capture bubble text at two timestamps
|
||||
during the stream; second length > first (progress is visible).
|
||||
4. `test_error_banner_on_llm_down` (fixture stops the mock) — submit;
|
||||
assert `role=alert` banner visible and button re-enabled within timeout.
|
||||
@@ -1,34 +0,0 @@
|
||||
# Story: Long Answers (No Truncation)
|
||||
|
||||
**Phase:** `11_long_answers.md` · **E2E:** `tests/e2e/test_long_answers.py`
|
||||
|
||||
## Narrative
|
||||
|
||||
As **a user**, I want Brain to be able to answer at full length (up to
|
||||
32 768 output tokens) so complex questions ("walk me through the whole
|
||||
setup", "list every service and its config") get a **complete** answer
|
||||
instead of one that stops mid-sentence.
|
||||
|
||||
- **Given** any question that deserves a long answer
|
||||
- **When** Brain streams its reply
|
||||
- **Then** the reply runs to its natural end — the model is allowed up to
|
||||
32 768 output tokens, not a hard 700-token cap.
|
||||
|
||||
## Acceptance criteria
|
||||
1. `LLMClient.chat_stream` sends `max_tokens` from settings
|
||||
(`BOR_MAX_OUTPUT_TOKENS`, default **32 768**) — the hard-coded 700 is
|
||||
gone.
|
||||
2. A genuinely long streamed answer (several thousand words) arrives
|
||||
**complete** in the browser — final line intact (E2E).
|
||||
3. Setting is overridable via env; unit-tested.
|
||||
4. Unit + integration green, `app/` coverage >90%, story E2E green in
|
||||
isolation, one `--no-gpg-sign` commit.
|
||||
|
||||
## Playwright Mapping Rule
|
||||
**Test Scenario → `tests/e2e/test_long_answers.py`** (mock LLM, seeded KB):
|
||||
1. `test_long_answer_streams_to_completion` — question with the
|
||||
"write a long answer" trigger → mock emits a ~4 000-word deterministic
|
||||
answer and **honors `max_tokens`** (word-based) → the browser shows the
|
||||
final line of the answer; under the old 700 cap the tail is missing.
|
||||
2. `test_normal_answer_unaffected` — a normal question still streams a
|
||||
complete, short answer.
|
||||
@@ -1,65 +0,0 @@
|
||||
# Story: Responsive, Polished, Accessible UI
|
||||
|
||||
**Phase:** `07_story_responsive_polish.md` · **E2E:** `tests/e2e/test_responsive_polish.py`
|
||||
|
||||
## Narrative
|
||||
|
||||
As **a user on any device** — phone at the coffee shop, laptop at the
|
||||
desk — I want the chat to be comfortable to read and drive: no pinched
|
||||
layout, no tiny tap targets, no contrast failures, no wasted whitespace —
|
||||
so asking Brain feels effortless everywhere.
|
||||
|
||||
- **Given** any viewport from 360px to 1600px+
|
||||
- **When** I use the chat and the Sources page
|
||||
- **Then** the layout follows the PLAN §7 standards (containers, chat
|
||||
column, full-width table), all interactive elements are reachable by
|
||||
keyboard, and every color pair meets WCAG 2.1 AA.
|
||||
|
||||
## Acceptance criteria
|
||||
1. **Layout:** container 72rem centered with side padding; chat column
|
||||
capped at 46rem centered; Sources table uses full container width with
|
||||
horizontal scroll below 640px (never a squeezed single hairline column).
|
||||
2. **Mobile (375px):** header condenses, composer reachable above the home
|
||||
indicator (`safe-area-inset-bottom`), chips scroll horizontally, bubbles
|
||||
≤92% width, no horizontal page overflow (document `scrollWidth ==
|
||||
clientWidth`).
|
||||
3. **A11y sweep:** landmarks present on both pages (`header/nav/main/
|
||||
footer`); skip link works (focus `#main`); all inputs have labels
|
||||
(visible or programmatically associated); all icon-only buttons have
|
||||
`aria-label`; `:focus-visible` outline on every control (Tab through).
|
||||
4. **Contrast:** automated check of the key pairs (ink/surface,
|
||||
ink-soft/surface, white/brand, chip-ink/chip-bg, deflection pairs) ≥4.5:1
|
||||
(test computes from computed styles; PLAN §7.2 table is the baseline).
|
||||
5. **No-CDN re-verification** on both pages (no `http(s)://` src/href
|
||||
except same-origin `/…`).
|
||||
6. **Reduced motion:** with `prefers-reduced-motion`, typing dots and
|
||||
spinner do not animate (computed `animation: none` or duration ≥2s).
|
||||
7. Long words/paths (e.g. a 60-char file path) wrap or ellipsize without
|
||||
breaking the bubble (overflow-wrap anywhere).
|
||||
|
||||
## UI Visualization & Structure
|
||||
- This phase is the **visual audit + fix pass**: it does not add features,
|
||||
it enforces PLAN §7 end-to-end on chat + sources.
|
||||
- Desktop 1440px screenshot pass: header 64px, chat centered with balanced
|
||||
margins, sources table edge-to-edge within the container.
|
||||
- Tablet 768px: chat column uses most of the width (≤46rem cap), no
|
||||
mid-column dead zones; stat cards 3-across.
|
||||
- Phone 375px: one-column flow, 44px+ targets, thumb-zone composer.
|
||||
- Any deviation found → fix in `frontend/assets/styles.css` (tokens first),
|
||||
re-verify with the E2E below.
|
||||
|
||||
## Playwright Mapping Rule
|
||||
**Test Scenario → `tests/e2e/test_responsive_polish.py`**:
|
||||
1. `test_no_horizontal_overflow_at_viewports` — for 360/375/768/1280/1600:
|
||||
`document.documentElement.scrollWidth <= clientWidth` on both pages.
|
||||
2. `test_chat_column_capped_and_centered` — at 1600px, `.chat-shell`
|
||||
width ≤ 46rem (736px) + 2% and horizontally centered (±2%).
|
||||
3. `test_sources_table_full_width` — at 1280px, `.table-wrap` width ≥ 80%
|
||||
of `.container` width.
|
||||
4. `test_a11y_landmarks_and_labels` — both pages: landmarks present,
|
||||
skip link target `#main` focusable, `#message-input` has an associated
|
||||
label, no `<img>`/icon buttons without accessible name.
|
||||
5. `test_contrast_pairs_pass_aa` — computed-color contrast assertions for
|
||||
the PLAN §7.2 pairs (helper computes WCAG relative luminance).
|
||||
6. `test_reduced_motion_respected` — emulate `reducedMotion: 'reduce'`;
|
||||
typing dots have no running animation (or ≥2s duration).
|
||||
@@ -1,58 +0,0 @@
|
||||
# Story: Suggestion Chips
|
||||
|
||||
**Phase:** `05_story_suggestion_chips.md` · **E2E:** `tests/e2e/test_suggestion_chips.py`
|
||||
|
||||
## Narrative
|
||||
|
||||
As **a user who opens the chat for the first time** (or after a deflection),
|
||||
I want a few **concrete example questions** right in front of me — so I
|
||||
immediately understand what Brain is good at and can start with zero
|
||||
friction.
|
||||
|
||||
- **Given** I land on the chat page
|
||||
- **When** the app is healthy
|
||||
- **Then** I see 3–4 suggestion chips drawn from `GET /api/suggestions`
|
||||
(defaults in settings, tuned to the real Homelab topics), and clicking one
|
||||
fills the composer and submits it.
|
||||
|
||||
## Acceptance criteria
|
||||
1. `GET /api/suggestions` returns the configured list (settings-driven,
|
||||
overridable via `BOR_SUGGESTIONS` JSON env).
|
||||
2. Chips render in the empty state as `<button class="suggestion-chip">`
|
||||
(real buttons, not links/divs) with `role="list"` container +
|
||||
`role="listitem"` items; `aria-label="Suggested questions"` on the group.
|
||||
3. Click behavior: fills `#message-input`, focuses it, **and submits**
|
||||
(one tap → answer). Keyboard: Tab to chip, Enter activates.
|
||||
4. After the first user message the empty state (and its chips) is replaced
|
||||
by the conversation; chips re-appear only on deflection (see
|
||||
honest-deflection story).
|
||||
5. If `/api/suggestions` fails, the chat still works (progressive
|
||||
enhancement — no chips, no error spam).
|
||||
6. Mobile: chips become a horizontally scrollable single row
|
||||
(no wrapping into the composer's territory).
|
||||
|
||||
## UI Visualization & Structure
|
||||
- Chips: pill (`border-radius: 999px`), `bg --brand-soft`, `text --brand-ink`
|
||||
(≥6:1), 1px `--line` border, **min-height 44px**, comfortable
|
||||
`padding 0.55rem 1rem`; hover deepens bg; `:active` scales 0.98.
|
||||
- Desktop: `flex-wrap: wrap`, centered under the empty-state subcopy, gap 0.5rem.
|
||||
- Mobile (≤640px): `flex-wrap: nowrap; overflow-x: auto` single row,
|
||||
`scrollbar-width: thin`, chips `flex: 0 0 auto` (thumb-friendly, no
|
||||
accidental double-tap on wrapped lines).
|
||||
- Default suggestion copy (tune to real docs in this phase):
|
||||
1. "How is my Kubernetes cluster set up?"
|
||||
2. "What's my backup strategy?"
|
||||
3. "How do I deploy a new service?"
|
||||
4. "What's currently running in the homelab?"
|
||||
|
||||
## Playwright Mapping Rule
|
||||
**Test Scenario → `tests/e2e/test_suggestion_chips.py`**:
|
||||
1. `test_onboarding_chips_render` — goto `/`, assert ≥3 `.suggestion-chip`
|
||||
visible inside `#suggestions` (role=list) with non-empty text.
|
||||
2. `test_chip_click_submits` — click the first chip; assert a user bubble
|
||||
with the chip's exact text appears and the brain reply (mock) follows.
|
||||
3. `test_chips_keyboard_accessible` — Tab from the page start reaches the
|
||||
first chip; Enter submits it.
|
||||
4. `test_chips_mobile_row` — at 375px viewport, the chip row is
|
||||
horizontally scrollable (`scrollWidth > clientWidth` or single-line
|
||||
height check) and no chip is cut vertically.
|
||||
Reference in New Issue
Block a user