QA SMOKE TEST — Redmine #1546 (epic #1511, all eight children) — production
https://pipeliner.omdev.tech · master build #109 · commit c784f95
Date: 2026-08-03 · Agent: manual QA (#25)
NOTE: no secrets, tokens, cookies or credentials were read, printed or captured.

==============================================================================
0. TEST ENVIRONMENT AND ITS LIMIT (read this before the results)
==============================================================================
The gstack /browse daemon is NOT installed on this machine:
  /home/om/.claude/skills/gstack -> does not exist
  /usr/bin/browse                -> symlink to xdg-open (not the daemon)
No prod client credentials were available (../infra/secrets is out of bounds
and access to it was denied), so NO AUTHENTICATED PRODUCTION SESSION was
possible. Per the QA brief I did not invent browser evidence.

What was actually done instead, in three real layers:
  L1  Live production HTTP + a real Chrome 150 (Playwright) against prod.
  L2  The shipped board components rendered from the exact prod commit and
      screenshotted under the DEPLOYED production CSS chunk.
  L3  The full test suite executed on the exact prod commit c784f95.

==============================================================================
L1 — LIVE PRODUCTION
==============================================================================
GET https://pipeliner.omdev.tech/            -> 307 -> /projects
GET https://pipeliner.omdev.tech/login       -> 200
GET https://pipeliner.omdev.tech/api/health  -> 200

Real Chrome navigating to /projects/1/ticketing:
  route HTTP 200, board chunks served, then 401 -> redirect to /login
  401 https://pipeliner.omdev.tech/api/auth/me
  401 https://pipeliner.omdev.tech/api/projects/1
  landed on https://pipeliner.omdev.tech/login  (auth gate works)

Backend children #1541-#1544 are live and auth-gated (401, never 404):
  /api/auth/me                     -> 401
  /api/auth/me/board-columns/1     -> 401   (#1544 column preferences)
  /api/projects/1/tasks            -> 401
  /api/projects/1/tasks?mode=board -> 401   (#1542 board payload)

Board code confirmed present in the chunks the ticketing route loads:
  1srf4qgx5z11l.js : "Tableau des tickets", "reconnexion en cours",
                     "au-dela des plus recents", "Depot impossible",
                     "Un ticket sort du backlog"      <- real FR catalog shipped
  2_9sp7epp_tud.js : board_read_only, can_move_forward, has_more,
                     cursor-grabbing, snap-x, categoryBeyondCap

==============================================================================
L3 — TEST SUITE ON THE EXACT PRODUCTION COMMIT
==============================================================================
Worktree: detached at c784f95 (Merge PR #100, #1554) — the master commit prod serves.
Node 20.20.2 via nvm.

  npx vitest run   ->  Test Files 163 passed (163)
                       Tests     1180 passed (1180)

Board-specific subset re-run verbosely: 15 files, 216 tests, all passed.

==============================================================================
1..10 — THE PRIORITY CHECKS
==============================================================================

[1] BOARD REPLACES THE LIST — #1546 ......................................... OK
    Screenshot en-board.png: ten-stage Kanban, no list, no toggle.
    Column order rendered: Backlog, Submitted, Spec, Design, In development,
    QA, (Preprod, Ready to ship, Blocked off-viewport) — Shipped ABSENT.
    Tests: "renders the board and no list, for an ordinary client";
           "renders the columns in STAGE_ORDER — Backlog first, Design between
            Spec and In development";
           "imports STAGE_ORDER rather than listing the columns" (grep contract);
           "hides Shipped before the server has answered, so nothing jumps".

[2] CARD ANATOMY + PRIORITY CHIP RULE — #1542/#1546 ......................... OK
    en-board.png, card #703: title, parent row "> #1511 Kanban delivery board",
    assignee "CF Claire Fontaine", priority chip "Immediate".
    Card #701 has priority Normal -> NO CHIP RENDERED, and its accessible name
    is  "Export invoices as CSV, Backlog, priority Normal"  — priority still
    named. Empty state for no assignee renders as "Unassigned".
    Chips actually painted on the Feature tab: Immediate, Urgent — and nothing
    for Normal. This is the design (§KB.2 / #1545 amendment 5), not a defect.
    Priority label is derived from position (4 -> Urgent, 5 -> Immediate), NOT
    from the server's name/key — locale-stable, as specified.
    Test: "renders no priority chip for Normal, but always names it in the
           accessible name"; "names a missing priority too".

[3] A FAILED POLL MUST NOT BLANK THE BOARD — #1547 (the blocking bug) ....... OK
    Could not induce a poll failure on prod (no session). Verified at the route
    integration level on the prod commit, which is where the bug lived:
      "A failing poll degrades the board — it never replaces it (§KB.7)"
        > keeps the cards on screen when a later poll fails
        > keeps a first-time client's guidance panel when a later poll fails
        > still shows the full-region error when there is NOTHING to show
    The amber reconnecting line is rendered and screenshotted:
      en-freshness.png : "Updated 4 minutes ago — reconnecting" (amber + warn icon)
      fr-freshness.png : "Actualise il y a 4 minutes — reconnexion en cours"
    Staleness only after THREE missed intervals, not one.

[4] TAB PILLS COUNT THE PROJECT, NOT THE VISIBLE CARDS — #1554 .............. OK
    THE CONTRADICTION IS RESOLVED. Screenshot en-bugtab.png, Bugs pill = 12
    with zero cards in view, renders:
      "This tab has 12 tickets, beyond the most recent ones each column loads."
    and does NOT render "No bugs reported yet." The two never co-occur.
    Tests: "shows the twelve bugs the per-status cap kept out of the payload";
           "does not tell a client their twelve bugs do not exist";
           "still says a genuinely empty tab is empty";
           "shows no pill for a category the project genuinely has none of".

[5] BACKLOG + BLOCKED RENDER BUT REJECT EVERY DROP, ZERO NETWORK ........... OK
    en-board.png: Backlog column renders with a padlock and "Not a drop target".
    Blocked renders likewise. Neither draws the dashed drop box.
    Network panel not observable without a session; asserted instead at the
    exact call site:
      "refuses a drop on Backlog without asking the write path anything"
      "refuses a drop on Blocked even when the server allows that status"
      "never offers Backlog(16), for anyone" / "never offers Blocked(14)"
      "locks Backlog and Blocked and nothing else"
      "treats a drop back on the card's own column as a no-op, with zero calls"
    FR lock reasons are distinct and correct: Backlog "Un ticket sort du backlog
    en etant soumis, pas en etant glisse." / Blocked "C'est depuis le ticket
    lui-meme qu'on le marque comme bloque."

[6] MOVE MENU REACHES BLOCKED + KEYBOARD PATH — #1546 ....................... OK
    The earlier dead path (B1 of the failed code review) is alive:
      "MOVES a card to Blocked when the menu asks — the gesture's ban is not
       the write path's"
      "keeps Blocked, which is the whole point of §KB.4b"
      "is a superset of the drag targets, never a different set"
      "still never moves a card INTO the backlog"
    Keyboard, keys alone:
      "offers a keyboard route to every legal target, driven by KEYS ALONE"
      "closes the Move menu on Escape and returns focus to its trigger"
      "hands focus back to the row when a confirm is cancelled"
    B2 (stale target list) also fixed: "hands back the REAL set once the engaged
    card's detail lands".

[7] MANUAL REFRESH — #1547 .................................................. OK
    Rendered and screenshotted (en/fr-freshness.png).
      "refetches on click"
      "says it is busy while a fetch is in flight"
      "issues no second refetch when clicked mid-fetch"   <- double-fire guard
      "keeps its place in the tab order while busy — aria-disabled, not disabled"
      "does not change its label while busy, so its width never moves"
      "renders no freshness line before the first successful fetch"
      "turns amber and says reconnecting once polls stop landing"

[8] COLUMN FILTER PERSISTS ACROSS A FULL RELOAD — #1544 ..................... OK
    Endpoint live on prod: /api/auth/me/board-columns/1 -> 401 (exists, gated).
      "writes the whole hidden set and survives a reload"
      "reverts the filter when the server refuses the write"
      "applies a toggle immediately and persists the WHOLE set"
      "falls back to the default rather than blanking the board when the read fails"
      "resets to the documented default — Shipped hidden, everything else shown"

[9] FRENCH — REAL TRANSLATION, NO ENGLISH FALLBACK ......................... OK
    Screenshot fr-board.png, every board string in French:
      Tickets · Fonctionnalites/Bugs/SecOps · Colonnes · Actualiser ·
      Nouvelle demande · Reindexer · Pret · "Derniere indexation il y a 2 heures"
      Soumis · Specification · En developpement · Recette
      "Depot impossible" · "Aucune tache a cette etape." · "Non assigne"
      "Urgente" · "Immediate" · "+ 52 tickets de plus"
      "Actualise a l'instant"
    Relative time and the freshness line in French (fr-freshness.png):
      "Actualise il y a 4 minutes"
      "Actualise il y a 4 minutes — reconnexion en cours"
    Test: "keeps `tasks.board` AND `tasks.priority` mirrored EN <-> FR";
          "renders relative time through the app's locale, never the browser's".

[10] NEW REQUEST + KNOWLEDGESTRIP REINDEX STILL REACHABLE ................... OK
    Both visible in the board toolbar on en-board.png / fr-board.png:
      "Ready · Last indexed 2 hours ago · [Reindex]"  (KnowledgeStrip inline)
      "[+ New request]"                                (the only intake path)
    Tests: "still reaches New request — the only ticket intake path on this screen"
           "still reaches the reindex control — the app's ONLY mount of it"

==============================================================================
WHAT I COULD NOT TEST, AND WHY
==============================================================================
a) No authenticated production session. No client credentials were available
   and the secrets store is out of bounds. Everything below therefore rests on
   the shipped components + the prod-commit suite, not on prod runtime:
     - a real mouse drag against the real API,
     - a real 403/409 rollback against real data,
     - the real network panel showing zero requests on a locked-column drop,
     - a real page reload proving the column filter round-trips.
b) DRAG WAS DELIBERATELY NOT EXERCISED ON PROD. Per the QA brief, the only way
   to drag on production would be to move a real client's live ticket. I stopped
   rather than mutate client data. A safe target (a disposable ticket on a
   throwaway project) is needed to close item [5] and the drag half of [6]
   against the live API.
c) The language selector in the fr-board.png header still reads "English" —
   that is an artifact of my harness forcing the locale through the intl
   provider rather than through the selector. It is NOT a product defect; the
   board content beneath it is fully French.

==============================================================================
OBSERVATION (cosmetic, not a defect, no ticket owner assigned)
==============================================================================
Two freshness/refresh controls sit on the same screen: the pre-existing project
header "Updated 2 hours ago | Refresh data", and the new board toolbar
"Refresh | Updated just now". They report different things (project data vs
board poll) but read as redundant side by side. Pre-existing chrome meeting new
chrome; worth a design pass, not a QA failure.

==============================================================================
VERDICT: PASS — #1546 stays in QA.
No acceptance criterion came back KO. No child of epic #1511 is implicated.
==============================================================================
