Skip to content

Feat/backend smoke gate - #5

Merged
saqibmanan merged 35 commits into
mainfrom
feat/backend-smoke-gate
Aug 14, 2026
Merged

Feat/backend smoke gate#5
saqibmanan merged 35 commits into
mainfrom
feat/backend-smoke-gate

Conversation

@saqibmanan

Copy link
Copy Markdown
Contributor

No description provided.

Saqib added 30 commits August 12, 2026 10:32
8 of 12 authenticated baselines captured something other than the page
they named: a 404 (the route /evaluation/288 does not exist), stuck
"Loading ..." curtains, a false-empty list, and four byte-identical
copies of the logged-out marketing homepage (bug #15). Most sat under
hard pixel-diff assertions, so they passed *because* the app was
broken - ai_maker_dashboard would have gone red the day bug #14 was
fixed.

The root cause was the wait, not the missing check: captures settled a
flat 2.5s after load while lessons.md documents curtains at 15-30s+, so
they landed mid-curtain by design. Adds wait_for_render_settled (45s)
plus a gate that refuses to save or compare a capture that is a 404, a
curtain, logged-out chrome on an auth route, or blank. It skips rather
than fails: an invalid capture means "did the pixels change?" was never
asked, so the honest result is no signal.

Also fixes the evaluation-detail route to use the completed_eval_id
fixture, and masks the account name/initials that every authenticated
baseline was embedding.
Four defects in the generated TEST_REPORT.md:

- Duration read from summary["duration"], but pytest-json-report puts it
  at the top level, so every report claimed "0.00s". Per-test durations
  were 0.00s for the same reason - they live on the setup/call/teardown
  phases, not on the test object.
- Pass rate counted skips in the denominator, so a run that skipped
  almost everything rendered as "0.0%" - a skip is the absence of a
  result, not a failed one. Now computed over executed tests, "n/a" when
  none ran.
- A run where nothing executed reported "PASSED". An environment outage
  that skipped every test looked identical to a clean pass; it now reads
  "NO TESTS EXECUTED".
- xfailed/xpassed were never counted, so on any xfail-using suite the
  rows silently failed to add up to the total.
The summary table printed needs.<job>.result per suite, which cannot
express a test failure here: every suite runs pytest with
continue-on-error: true, so the job succeeds no matter how many tests
failed. The most-read signal in the run always said "success".

Each suite now writes a named JSON report and uploads it; the summary
aggregates them into per-suite pass/fail/skip/xfail counts, flags a
suite where nothing executed, and keeps the job-status table in a
collapsed block labelled as plumbing.

Also bumps the visual baseline cache to a v2- generation. Editing
tests/visual/** changes the primary key, but restore-keys would still
fall back to the newest old cache - which holds the poisoned baselines,
so CI would have diffed correct captures against 404s and homepage
clones.
Phase 11 log plus per-baseline outcomes. Files #24 (auditor assignments
hangs past 45s on "Loading your assignments...", a route neither #13 nor
#14 covered).

Flags a caveat worth acting on: #13/#14 did not reproduce this session -
both the AI-Maker overview and the evaluations list loaded fully within
45s. That may be the merged backend perf work, or intermittency, but
either way the visual suite's evidence for those rows was unreliable
because it was capturing at 2.5s. Re-verify before removing their
xfails.
Moves DEFAULT_MASKS / AUTH_PII_MASKS into utils/visual_guards so sibling
visual suites use one definition - a new module that forgot the identity
masks would quietly start baking the test account's name back into
baselines.

Also updates app_bugs.md #15: two consecutive runs hit the logged-out
fallback on 12 of 24 authenticated captures (~50%, vs the 17-25%
recorded in July), and the fallback now renders logged-out chrome
(LOGIN / SIGN UP) rather than authenticated shell with wrong content.
Both samples are small and conditions differed, so it is noted as
materially worse under current conditions rather than a measured
regression.
Responsive: mobile/tablet baselines for 4 high-traffic auth routes
(role selector, AI-Maker overview, evaluations list, models list) plus
a no-horizontal-overflow assertion at mobile width - a deterministic,
non-pixel-diff check that catches the most common responsive bug
directly. Verified: 10 passed, sensible skip/xfail, all 4 overflow
checks passed. Surfaced a real bug: the evaluations table overflows
horizontally at 768px, cutting off the Model column and the row-count
control.

Modal: element-scoped captures for the Know More info dialog (90f38dc)
and the Start Evaluation modal's two steps, targeting the
ModelSelectionModal refactor (130b5ac). Two interaction steps
(evaluation-method radio, evaluator-type radio) intermittently time out
against the live dev UI - wrapped to skip with a clear reason rather
than fail, same pattern as completed_eval_id's UI-seeding fallback.

Masks are shared via utils.visual_guards (DEFAULT_MASKS /
AUTH_PII_MASKS) rather than redefined, so this coverage can't
accidentally omit the identity masking that visual_regression.py relies
on.
Targeted repro (poll body text until the loading-curtain regex clears
or a 60s budget expires - the disambiguation procedure this file
already prescribes) shows both routes rendering real content well
within budget: the AI-Maker overview in 28.2s with populated stat
cards, the evaluations list in 8.1s with real table headers. Second
independent confirmation after Phase 11's visual-suite runs also
failed to reproduce either hang the same day.

Xfails referencing #13/#14 in the e2e/accessibility suites are not yet
removed - that cleanup is a follow-up, per this file's own convention
of re-running the linked test before dropping the xfail.
Applies to e2e-tests, accessibility-tests, visual-tests. The visual
suite alone ran 11.5 min on a cache-cold run today (every baseline
saving for the first time under the new v2- cache generation); 30 min
was tight headroom for a slow or retry-heavy run. test-summary's
separate 10-minute timeout is untouched - it does no browser work.
Both visual suites hit the same problem: the AI-Maker overview's stat
cards (Evaluations Completed, etc.) and Recent Evaluations table
change during normal platform use, so a hard pixel-diff assertion
flapped red on ordinary re-runs. The responsive suite had already
worked around this by adding the page to its non-deterministic xfail
set - which silences diffs for the WHOLE page, including the static
layout and model-card grid a real regression would land in.

Adds route_masks() to utils.visual_guards: a per-route mask lookup,
verified against the live DOM logged in as TEST_EMAIL_2 so the
selectors aren't tied to one account's data. Masks only
.metric-card-value and the live table rows; the four stat labels,
headings, and model cards keep a real hard assertion. Wired into both
the desktop and responsive suites, and ai_maker_dashboard is removed
from the responsive suite's non-deterministic set now that it's
genuinely stable - confirmed 4/4 passed on a same-session re-run with
zero skips.
The health endpoints are now deployed to dev, so test_rejects_non_get
ran for real for the first time and failed: assumed 405 from
@require_GET, but Django's CsrfViewMiddleware intercepts a POST with
no CSRF cookie before that decorator ever runs, returning 403
('CSRF verification failed') instead. Confirmed live via direct curl.
Accept either code - the test's concern is that non-GET is rejected,
not which middleware rejects it first.
Experiment: both now depend only on lint instead of chaining
accessibility-tests after api-tests. The serial chain existed because
concurrent load against the shared dev backend used to produce pool
exhaustion and widespread ReadTimeouts (docs/app_bugs.md #3/#13/#14).
Two things changed since: the backend moved from runserver to Docker,
and #13/#14 were independently re-verified fixed today.

A local 2-suite concurrency smoke test today was inconclusive - it
showed no pool-exhaustion symptoms, but was confounded by a second,
unrelated CI run hitting the same backend at the same time (this
branch has an open PR into main, so every push auto-triggers a run).
Testing it in CI directly avoids that confound.

visual-tests and e2e-tests are deliberately not included yet - widen
gradually and watch for the ReadTimeout/stuck-curtain signature before
going further.
The concurrent experiment (previous commit) started failing in CI
within ~13 minutes of the real run. Reverting accessibility-tests to
needs: api-tests, restoring the full serial chain. The run was
cancelled rather than left to finish, so there's no captured job log
of the actual failure mode yet - documented in docs/ci_workflow_notes.md
along with what to gather before trying again.
The prior note said the concurrent run "started failing" without a
captured log. Pulled the cancelled API Tests job's partial log (GitHub
retains whatever uploaded before cancellation) and found a real,
diagnostic failure: tests/api/test_audit_detail_api.py failed in a
RERUN/RERUN/FAILED cluster with ~60s between each attempt - a timeout
signature matching bug #3's fingerprint, not an assertion bug.

Also corrects an overstatement: accessibility-tests and visual-tests
both completed successfully in the same run despite running
concurrently with the struggling api-tests job, so the slowdown was
concentrated in api-tests' fixture-heavy queries, not universal across
suites.
Visual Regression has always shown "no report found" regardless of
whether the run passed - the summary script was looking for
suite-artifacts/visual.json, but that artifact's upload step spans
three top-level folders (reports/, screenshots/, snapshots/), so
GitHub doesn't strip the reports/ prefix the way it does for
api-test-report and accessibility-report (each a single common
folder). Confirmed by downloading the actual artifact and inspecting
its real layout: reports/visual.json.
Rapid pushes to an open PR were each auto-triggering a full run, and
every run shares one dev backend - overlapping runs were contending
with each other (api-tests hit 42m26s vs a 6m31s baseline while two
runs overlapped, docs/ci_workflow_notes.md). Groups by PR number (or
ref for push/workflow_dispatch), cancel-in-progress so a new push
supersedes a stale run's results instead of letting it keep running
against the shared backend.
Verified live: a superseded run's api-tests correctly cancelled via
the new concurrency group, but accessibility-tests (if: always())
started right after anyway and kept running against the shared
backend, ignoring the cancellation entirely - the exact caveat flagged
when the concurrency group was added.

Removed always() from accessibility-tests and visual-tests. Every test
step already uses continue-on-error, so the upstream job's conclusion
is "success" even when its tests fail - default success()-gating only
actually changes behavior on a genuine infra failure or cancellation,
both cases where skipping is correct. test-summary keeps always()
(paired with !cancelled()) since it's a reporting job that must
tolerate e2e-tests' deliberate skip while paused, not a test job.

Not pushed yet - holding per instruction until current runs are
stopped or told to push.
DOM value attrs on the step-2 evaluator-type radios changed from plain
Technical/Domain/Cultural to the backend enum form (TECHNICAL_AUDIT/
DOMAIN_AUDIT/CULTURAL_AUDIT), so select_evaluator_type_in_modal timed
out 30s on every call. Updated the locators and made
get_checked_evaluator_type() map the raw enum back to the display
label so existing assertions keep reading Technical/Domain/Cultural.
Root-caused in ParakhAI-frontend (origin/dev, NewEvaluationContent.tsx
getEvaluatorLabel): the switch compares the raw AUDIT_TYPE enum against
AUDIT_TYPE_LABELS values, which can never match, so every selection
falls through to the default label. Introduced in commit 34f221c.
xfail the three e2e assertions that hit it.
DOM value attr for the Playground radio changed from 'manual' to
'playground' (bulk unchanged). select_evaluation_method/is_method_selected
kept the public 'bulk'/'manual' API used across ~20 call sites and
translate internally to the real DOM values.
Both tests navigate with the unauthenticated page fixture, which
redirects to Keycloak login before the editor's JS bundle (and its
SyntaxError) ever loads -- so they were passing without observing
anything, which fails loudly under xfail(strict=True) as a false
XPASS. Added the same reachability skip guard the CDS-005 tests in
this file already use, keyed on landing on the opub-kc auth domain.
The tab bar was replaced by a per-column 'Filter Status' popover on the
DataTable in the evaluation-table-listing redesign (~2026-08); all
STATUS_TAB_* locators now permanently match 0 elements. Added the new
FILTER_STATUS_BUTTON/FILTER_DIALOG locators and rewrote
EvaluationsPage.click_status_tab/get_status_filter_options/
is_status_filter_available to drive the popover. Updated all of
TestStatusFilterTabs plus the two pagination tests that referenced the
old tabs. Also filed bug #27: the Pending Review filter option shows
its chip as applied but doesn't actually filter rows (Draft/Completed
filtering works correctly), and xfailed the one test that catches it.
The list can render its first (cached) page of rows before a
freshly-created draft has synced in -- waiting on any EVAL_TABLE_ROW
match returns too early. Wait on the draft's own href instead.
Phase 12 marked it fixed after one clean check, but a group-2 e2e run
today genuinely XFAILed two of the four related tests while the other
two XPASSed in the same run -- and an immediate isolated re-run of the
XFAILing pair then XPASSed cleanly. That's inconsistent both ways, not
the bug #3 concurrency-noise shape. Keeping all four strict=False
xfails in place rather than removing them.
test_evaluations_list_controls.py still assumed the removed
StatusFilterTabs '(N)' count badges -- they have no equivalent in the
redesigned Filter Status popover, so narrowed those assertions the
same way the sibling test_evaluations.py fixes did. Also found: (1)
bug #28 -- the first click on a sortable column header is a no-op
(aria-sort never updates, rows don't reorder); only the second click
engages sorting. (2) Broadened bug #27 -- the Filter Status popover's
intermittent no-op isn't Pending-Review-specific, a same-day run
reproduced it for Completed/Draft/Failed too. xfailed the affected
tests referencing both rows.
test_new_evaluation_regression.py still used the removed STATUS_TAB_*
tab-bar locators directly; migrated to EvaluationsPage.click_status_tab.
While fixing it, found click_status_tab itself only checked new
checkboxes without unchecking previous ones -- since the popover is a
real multi-select group, calling it twice (Draft then Completed) would
silently accumulate both filters instead of switching. Fixed to clear
all checked boxes before checking the target one. Also xfailed a new
bug #26 recurrence in TestEvaluationTypeRadio and extended that row's
test list.
Live repro (open Add Evaluator dialog, click Cancel) confirmed no
phantom success toast appears anymore. Was XPASS(strict)-failing the
suite; verified genuinely fixed rather than trusting the single run.
Three locators mixed a text=/regex/flags selector with plain CSS in
one comma-joined string, which Playwright can't parse (documented
pattern, tasks/lessons.md 2026-05-18/2026-07-28) -- rewrote all three
using get_by_text(re.compile(...)).or_(css_locator) instead. Also
fixed _nav_to_eval_detail's fixed 3.5s wait, which was far too short
for the documented 15-30s+ curtain and only survived because most
callers here soft-retry-then-xfail on empty results; now polls for
curtain-absence AND a positive 'Evaluation Overview' content marker
(content can flash empty in between, same caveat as
wait_for_wizard_loaded).
pytest.ini sets xfail_strict=true globally. This test's xfail for
bug #15 (documented as ~50% intermittent, not always-reproducing) had
no strict=False override, so a lucky run's XPASS reported as a suite
failure -- exactly the problem bug #14's xfails were already fixed to
tolerate. Confirmed no other e2e test references bug #15 by id.
Saqib added 5 commits August 14, 2026 13:22
XPASSed (strict) in the 2026-08-14 CI run, failing the suite over an
intermittent a11y improvement rather than a regression. Had no
app_bugs.md row at all, violating this file's own convention. Filed
#29 and added strict=False, matching how #14/#15/CDS-001 were already
handled today for the same reason.
1.085% diff in the 2026-08-14 CI run, vs a 0.2% threshold. Root cause:
completed_eval_id picks whichever completed audit it discovers/seeds
each run, so the page's actual content (evaluation name, scores,
sample-issue text) varies run to run - most of the page, not a few
numbers a mask could absorb. xfail-on-diff (matching the existing
pattern for other live-data pages) is the right tool here, not masking.
The job summary showed real counts like 176 as "***76" - some repo
secret's value (unknown, secrets are write-only) collides with digit
substrings in real test counts, and GitHub masks any exact substring
match anywhere in workflow output, not just where the secret was
meant to be used. Confirmed cosmetic-only earlier (real numbers always
recoverable from the JSON artifacts), but the summary table is the
whole point of this script, so it should render correctly on its own.

Joins every printed count and duration with a zero-width space
(chr(0x200B), not a literal invisible character in source - that
version got caught in review since it's unverifiable in a diff and
fragile to whitespace-trimming tools) between characters. Breaks the
contiguous-substring match masking relies on; renders identically to
a human in both the terminal and GitHub's Markdown-rendered summary.
Confirmed with the user: its value is "1" (CivicDataLab's org id, not
sensitive). GitHub was masking every literal "1" digit anywhere in the
job summary because it exactly matched this secret's value - that's
what caused ***76/***5/***0 etc regardless of what actually ran.

Scoped narrowly to this file only: added a new repo Variable
(coexists with the existing secret under the same name, no conflict)
and changed just this one reference from secrets. to vars.. The
actual secret is untouched and still used as-is by run-smoke.yml
(the reusable workflow ParakhAI-frontend's CD pipeline calls) and
scheduled.yml - no reason to touch either for a fix scoped to this
file's own job summary rendering.

A prior attempt to defeat the masking with zero-width spaces between
digits (20b3ab9) did not work and was reverted (68d29a6) - GitHub
evidently normalizes/strips invisible characters before matching
secrets, which is the correct hardening on their end. This is the
real fix, not a workaround.
@saqibmanan
saqibmanan merged commit b7a275f into main Aug 14, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant