Repository navigation
Web UI smoke check for the Darling pages and Web Admin (#5701, phase 1) - #5732
Conversation
tools/ui-smoke walks the Overview and five busiest tabs of a seeded SQL Server, Fleet and Web Admin's servers tab in Chrome. Adds data-panel and data-grid hooks to the web JS and the darling-ui-smoke workflow.
…nked grids to prove a red run
…rt rows; drift-guard test
…n; TEMPORARY push trigger
…ows and empty panels of every tab
…up; load #/ag and #/triage (#5701)
| Add-Content -Path "$dataDir\postgresql.conf" -Value "timescaledb.max_background_workers = 74" | ||
| Add-Content -Path "$dataDir\postgresql.conf" -Value "max_worker_processes = 85" |
There was a problem hiding this comment.
A third hand-written copy of the #1888 cluster sizing, and nothing pins this one.
CiClusterWorkerSizingTests.ClusterWorkflows() is a roster of exactly two files:
public static TheoryData<string> ClusterWorkflows() => new() { "build.yml", "nightly.yml" };and Darling.Tests.csproj:122-123 copies exactly those two beside the test binary. This workflow is now a third copy of the same Add-Content -Path "$dataDir\postgresql.conf" block with 74 / 85 baked in, and the roster does not see it.
Failure scenario: a collector is added, so TimescaleSupport.HypertableCount goes to 73. ClusterWorkflow_SizesWorkersFromTheProductsFormula reds and names build.yml and nightly.yml; both get bumped to 75/86. This file keeps 74/85 forever, and the UI-smoke store runs one background worker short of what every hypertable policy needs — the "failed to start a background worker" shape #1888 exists to keep out of CI, now with no test that can report it. That is the test's own stated failure mode: "Both, not just build.yml: the PR leg and the nightly leg are separate copies of the same steps, and a fix applied to one of them is the ordinary way these two drift apart."
The comment above at line 82-83 ("worker numbers pinned by CiClusterWorkerSizingTests there; copied here exactly") is accurate about build.yml and is exactly why the copy here needs the same treatment.
Fix is two lines: add "darling-ui-smoke.yml" to ClusterWorkflows() and a <None Include="..\..\.github\workflows\darling-ui-smoke.yml" Link="Fixtures\darling-ui-smoke.yml" .../> to the csproj. The Add-Content regex already matches this file's spelling verbatim, so no parser change is needed. Note that doing so also makes .github/workflows/darling-ui-smoke.yml a file Darling.Tests parses, which is the stated reason nightly.yml and darling-upgrade-shapes.yml are entries in .github/darling-paths-filter.yml — it would need one too.
| Assert.Equal(360, await CountAsync("collect.cpu_utilization_stats", sql)); | ||
| Assert.Equal(360, await CountAsync("collect.memory_stats", sql)); |
There was a problem hiding this comment.
This guard cannot run on an edit to the file it guards: tools/ui-smoke/** is not in the Darling path gate.
These 21 literals are the counts in tools/ui-smoke/seed.sql, and the class only runs when DARLING_TEST_PG and DARLING_TEST_PGRUNTIME are both set. The only job that sets them is build.yml's darling-pg job (build.yml:1936), which is gated on steps.filter.outputs.darling — i.e. on .github/darling-paths-filter.yml's darling filter. That filter's entries are Darling/**, the seven shared libraries, build.yml, the filter file, nightly.yml, the two shard scripts and the two darling-upgrade-shapes files. tools/ui-smoke/seed.sql matches none of them.
Failure scenario, and it is the one the README prescribes: a contributor follows "A table: one more section in seed.sql … add the new table's count there too" and touches only tools/ui-smoke/seed.sql (plus pages.json). The darling filter is false, the darling-pg job is skipped, this test never runs, and the new/changed count literal is never checked. The UI-smoke workflow does fire on that path and does apply the seed — but it asserts page expectations, not these counts, so a count literal left stale here reports clean and then surfaces as an unrelated red on the next Darling PR that happens to flip the filter.
The filter file already states this exact requirement for its own entries, twice — "nightly.yml is not a build input, but Darling.Tests PARSES it … so an edit to it has to reach the suite" and the same sentence for darling-upgrade-shapes.yml. Adding
- 'tools/ui-smoke/**/!(*.md)'to the darling filter (extglob form, for the dorny v4 quantifier reason the file documents) closes it. The guard's reverse direction — a Darling schema change breaking the seed — is already covered, since that edit matches Darling/**.
| $registered = $false | ||
| while ((Get-Date) -lt $deadline) { | ||
| $n = & $psql -h 127.0.0.1 -p $PgPort -U darling -d darling -X -q -t -A -c "SELECT count(*) FROM config.config_monitored_servers WHERE name = 'ui-sql'" 2>$null | ||
| if ($n -eq '1') { $registered = $true; break } | ||
| Start-Sleep -Seconds 1 | ||
| } | ||
| if (-not $registered) { throw "the service did not register ui-sql in the store in time" } |
There was a problem hiding this comment.
The registration wait has no budget of its own — it inherits whatever the ping wait left of the 90 s, which can be nothing.
$deadline is computed once at line 71 and both loops test against it. The second loop's while ((Get-Date) -lt $deadline) is evaluated before its first probe, so if /api/ping answered late the registration check can run zero times and throw "the service did not register ui-sql in the store in time" without ever having asked.
The two waits are for different things and the second is the slower one: /api/ping answers as soon as the web host is up, while config.config_monitored_servers is written after PgMigrations.MigrateAsync has walked the whole ladder against an empty store. A cold runner that spends 70 s on the host coming up leaves 20 s for ~170 migration rungs plus the config seed, and the failure it reports blames registration for a slow start. Even in the ordinary case the budget is whatever is left over rather than a stated one.
Give the second wait its own deadline and make it probe at least once:
| $registered = $false | |
| while ((Get-Date) -lt $deadline) { | |
| $n = & $psql -h 127.0.0.1 -p $PgPort -U darling -d darling -X -q -t -A -c "SELECT count(*) FROM config.config_monitored_servers WHERE name = 'ui-sql'" 2>$null | |
| if ($n -eq '1') { $registered = $true; break } | |
| Start-Sleep -Seconds 1 | |
| } | |
| if (-not $registered) { throw "the service did not register ui-sql in the store in time" } | |
| $registered = $false | |
| $regDeadline = (Get-Date).AddSeconds(120) | |
| do { | |
| if ($serviceProc.HasExited) { throw "the service exited early (code $($serviceProc.ExitCode)); see service.out.log" } | |
| $n = & $psql -h 127.0.0.1 -p $PgPort -U darling -d darling -X -q -t -A -c "SELECT count(*) FROM config.config_monitored_servers WHERE name = 'ui-sql'" 2>$null | |
| if ($n -eq '1') { $registered = $true; break } | |
| Start-Sleep -Seconds 1 | |
| } while ((Get-Date) -lt $regDeadline) | |
| if (-not $registered) { throw "the service did not register ui-sql in the store in 120 s (migrations still running? see service.out.log)" } |
The HasExited check is worth carrying into this loop too: the ping loop has it, and a service that dies during the migrations is exactly what this wait sits through today, spending the full remaining budget before reporting the wrong cause.
| const body = el("div", { class: "panel-body" }, [loadingStrip()]); | ||
| const panel = el("div", { class: "panel card" + (span === 2 ? " span-2" : "") }, [ | ||
| /* #5701: `data-panel` (the read) and `data-title` are the hooks the web UI smoke check finds a panel by. */ | ||
| const panel = el("div", { class: "panel card" + (span === 2 ? " span-2" : ""), "data-panel": read || title, "data-title": title }, [ |
There was a problem hiding this comment.
Non-blocking: data-panel is not unique on a fanout tab. fanout hands the same read to every shell —
const shells = specs.map((s) => panelShell(s.title, s.subtitle, s.span ?? 2, read, s.dbScope));— so e.g. the Blocking tab draws two cards both carrying data-panel="get_current_waits_trend" (Waiting Tasks, Blocked Sessions) and two more both carrying get_blocking_stats. smoke.mjs's lookup is panels.find((p) => want.panel ? p.panel === want.panel : p.title === want.title), which takes the first match, so a panel:-keyed expectation on a fanout read silently grades whichever card the tab happens to build first.
Nothing in pages.json hits this today (the three panel: entries — get_top_queries_by_cpu, get_top_procedures_by_cpu, get_cpu_scheduler_pressure — are single-panel reads), so this is a latent sharp edge rather than a defect in the current run. The README's advice to "take the panel names from a run's log" will walk someone into it. Worth a sentence in pages.json's _comment saying panel: is only unambiguous for a read that feeds one card, and to key a fanout panel by title.
| const main = document.querySelector("#main"); | ||
| /* A panel that answered "not collected" and asked to be hidden (Instance CPU on a PostgreSQL server that is not Aurora, | ||
| panels.js hideWhenNotCollected) keeps its loading strip inside a hidden card: not a panel still loading. */ | ||
| const loading = [...(main ? main.querySelectorAll(".strip.loading") : [])].filter((e) => !e.closest("[hidden]")); |
There was a problem hiding this comment.
Non-blocking, and this one checks out — but the reason it checks out is narrower than the comment above says, so it is worth writing down.
There are two hide paths for a panel card and they do different things:
panels.js:146-149(hideWhenNotCollected) sets bothstyle.display = "none"andhidden = true, and returns without mounting, so the loading strip stays. That is the case this filter exists for, andclosest("[hidden]")sees it because thehiddenproperty reflects to the attribute.server-tabs.js:199(hidesPanel, forhideWhenNoRows) sets onlystyle.display = "none"— nohiddenattribute — but it also callsmount(shell.body, []), which clears the strip. So this filter never has to see it.
So the [hidden]-only predicate is correct today purely because the one path that orphans a strip is also the one that sets the attribute. A third hide path that sets display:none and leaves the body alone would hang settle() for the full SETTLE_MS and report "did not settle" on a page that is in fact fine. Matching what readPanels already does for the same question (card.style.display === "none" || card.hidden) would make it independent of that coincidence:
const loading = [...(main ? main.querySelectorAll(".strip.loading") : [])].filter((e) => {
const card = e.closest(".panel.card");
return !(card && (card.hidden || card.style.display === "none"));
});Line 84's copy of the same predicate would want the same treatment.
There was a problem hiding this comment.
Verdict: CHANGES REQUESTED
The product diff is genuinely additive — I read every gridTable branch and every hide path that could orphan a loading strip. The two defects are in the CI wiring: a third hand-written copy of the #1888 cluster sizing that no roster sees, and a drift guard that cannot run on edits to the file it guards. See the inline comments.
Claims
- VERIFIED —
servers: []is refused with that message —DarlingConfig.cs:547(body cites:542) - VERIFIED — the DOM change is
data-panel/data-titleon panel cards anddata-gridon.grid-boxplus the bare wrapper when a grid has no tools —panels.js:96,:293-301;server-tabs.js:149; all fourgridTablereturns keep their prior node shape - VERIFIED —
pages.jsoncarries 34 pages — counted the"name":entries - VERIFIED — the CSP already allows
img-src data:—DarlingWebSecurityHeaders.cs:66, precedent at:39 - VERIFIED —
IconLinkHtml's href is byte-identical toindex.html's — the extracteddata:URL hashes the same out ofDarlingWebHostService.cs:2344andindex.html:14 - VERIFIED — the host source holds exactly three
<headliterals, so the census is not vacuous —:2326,:2371,:2434 - VERIFIED — the sign-in page's hashes are unaffected —
IconLinkHtmlholds no script or style block - VERIFIED —
hideWhenNotCollectedleaves a strip inside ahiddencard and the settle wait skips it —panels.js:146-149;smoke.mjs:73,:84 - VERIFIED —
#/aggrid #0 is the 4-row database grid —ag.js:170-171chips,:223-234the oneVIZ.table - VERIFIED — the
authored/cpuread cells are exactly the three reads named, and carrydata-panel—AlertNotebookTemplateCpuTests.cs:75-87;views.js:813 - VERIFIED — the seed needs
psqlfor\gset— both\gsetblocks inseed.sql - VERIFIED — only the darling-pg job sets
DARLING_TEST_PGRUNTIME—build.yml:1936 - VERIFIED — the nightly job is gated like
darling-upgrade-shapesand absent frompublish's needs —nightly.yml:155-159vs:1516 - VERIFIED — the air-gap scan still passes on the new
index.htmlline — the onlyhttpliteral is the allowlisted SVG namespace andExtractUrlstops at the' - VERIFIED — the artifact collects logs,
out/*and the PG log for 7 days —darling-ui-smoke.yml:125-135 - VERIFIED — the CRLF restoration landed —
file(1)reports CRLF on every changed and added file - UNVERIFIED — the green runs (8/8, then 32/32 with 0 flakes) — live windows-latest runs
- UNVERIFIED — the two red proofs — those commits are not in this squashed tree
- UNVERIFIED — the live seed guard passing on 55499 / 55505 — needs PostgreSQL and a pg-runtime
- UNVERIFIED — the local guard and
check-web-js.shcounts — local runs - UNVERIFIED — that the draft run concluded
skipped— thepush:trigger's absence I confirmed (:20-29); the conclusion needs Actions history - UNVERIFIED — that the 404 was
/favicon.ico— the body labels this inference, not a printed path - UNVERIFIED —
#/agand#/triagechecked by hand withplaywright-cli— a live session - UNVERIFIED — the timing estimates — measurements
- REFUTED — "asserts 18 row counts" —
UiSmokeSeedAppliesLiveTests.cs:103-134carries 21 count assertions plus fourDarlingAgReaderones; stronger than advertised, so no code consequence, but the figure is cited as its coverage - REFUTED — the guard run as evidence for this change ("every class that reads …
darling-ui-smoke",CiClusterWorkerSizingTestsgreen) — no class readsdarling-ui-smoke(a grep of both test projects returns only the new seed guard, which readsseed.sql), and that test's roster namesbuild.ymlandnightly.ymlonly
Riskiest lines
.github/workflows/darling-ui-smoke.yml:95-96— the74/85pair, a third copy of a derivation whose siblings are pinned toTimescaleSupport.HypertableCount— UNPINNED (roster is two files;Darling.Tests.csproj:122-123copies two)Darling/Darling.Tests/UiSmokeSeedAppliesLiveTests.cs:103-134— 21 count literals mirroringseed.sql— pinned by its own test, but unreachable from an edit toseed.sql, the edit the README prescribeswwwroot/js/panels.js:296-301—gridTable's return split from two branches to three — UNPINNED (no JS test; verified by reading that nothing inwwwrootreads these attributes and the hook never nests)
Traps near this diff
- "A roster-based census does not see the file it does not roster (#3699)." — applies twice:
ClusterWorkflows()vsdarling-ui-smoke.yml:95-96, and thedarlingpath filter vs the newtools/ui-smoke/** - "Two copies pinned EQUAL to each other fail only after BOTH have drifted (#3684)." — applies, worse: the new pair is pinned to nothing
- "dorny/paths-filter v4 evaluates each pattern as an INDEPENDENT predicate." — does not apply:
darling-ui-smoke.yml:23-26is nativepathswith no negation - "A bare
$(gh api …)underset -euo pipefailkills the step (#2309)." — does not apply: all pwsh, nogh,$LASTEXITCODEchecked at:97,:106,:108 - "
gh api --paginate --jqfilters once PER PAGE." — does not apply: nogh api - "A summary helper reading only the first positional arg truncates a wrapped
echo." — does not apply: the tail at:115-123is pwsh - "
claude-code-actionrefuses whenclaude-review.ymldiffers from the default branch (#2229)." — does not apply: not touched - "Lite and Darling ship in parity." — does not apply:
Lite/has nowwwrootand no*.html - "
eol=crlf: a round-trip converts the WHOLE file's endings." — does not apply: small hunks, all files CRLF - "A test that reads the clock twice measures the CI scheduler (#3676)." — does not apply to the seed:
now()in six statements inside oneBEGIN/COMMITis one anchor. It is the shape of therun-ui-smoke.ps1comment, where one$deadlineserves two waits
Divergence (blind read vs PR body)
The guard run is offered as evidence this change is pinned, and for the new CI it cannot be. No test reads darling-ui-smoke.yml, and CiClusterWorkerSizingTests is green here precisely because its roster stops at two files — the same run would stay green had the new workflow written max_worker_processes = 8. The same gap reaches the seed guard the body leans on: it does not fire on a seed-only edit, since tools/ui-smoke/** is in neither the Darling path gate nor any roster.
Lane A's figures are superseded by lane B and not retracted inline: 8 pages, "Admin servers exactly 1", "Product change (the only one)" vs a tree with 34 pages, exactRows: 2, and a product diff that also carries the icon link and a private-to-internal widening. Lane B states all three correctly, so this is a stale section rather than a wrong account — worth collapsing.
Otherwise the body matches the diff, including where it marks a conclusion as inference.
Reviewer checklist
- Mark ready and confirm the
pull_requestleg reports: the green runs came from a temporarypush:trigger now removed, and a draft is skipped by the job's ownif, so this workflow has never run under the trigger it ships with - Watch
triage: twoauthored/cpucells are composedpanelcells (includingPostgreSQL CPUagainst a SQL Server target) rendered throughcompose.js, which carries nodata-panel— invisible to assertions 4 and 5, but able to trip assertion 3 with a.strip.errorif/api/compose/runrefuses that source - Confirm
collect.servers.server_nameforui-pg: the seed writes the host, butServerIdHelper.BuildStorageNameyields127.0.0.1:pg:1for a PG target with a port. Every read I traced scopes byserver_id, so likely cosmetic — but the guard cannot catch it, since it mimics the registration rather than letting the service perform it - Time
/api/pinganswering vsui-sqlappearing inconfig_monitored_serverson a cold store, against the shared 90 s budget — the migration ladder runs between those two events - Verify Chrome is on the
windows-latestimage:smoke.mjslaunches thechromechannel undernpm ci --ignore-scripts, so nothing installs a browser if it moves
There was a problem hiding this comment.
Verdict: LGTM
Claims
- VERIFIED — "
darling-ui-smoke.ymlis now inCiClusterWorkerSizingTests.ClusterWorkflows()and is copied beside the test binary byDarling.Tests.csproj, soClusterWorkflow_SizesWorkersFromTheProductsFormulachecks its74/85againstTimescaleSupport.HypertableCount" — confirmed in the diff (CiClusterWorkerSizingTests.cs:54, Darling.Tests.csproj) and the values satisfy the formula (HypertableCount+2=74⇒HypertableCount=72;3+74+8=85). - VERIFIED — "
.github/darling-paths-filter.ymldarlingfilter gainstools/ui-smoke/**/!(*.md)(the extglob form the file requires) and.github/workflows/darling-ui-smoke.yml" — matches the diff; the!(*.md)carve-out sits inside the include, the formDarlingPathFilterGateTests.cs's own doc comment (lines ~36-40) requires, not the bare-!**/*.mdshape the traps file flags. - VERIFIED — "A new test,
DarlingPathFilterGateTests.TheDarlingGate_CoversTheUiSmokeWorkflowAndHarness, pins both entries" — present in the diff, asserting both the workflow path and thetools/ui-smoke/**/!(*.md)pattern. - VERIFIED — "
run-ui-smoke.ps1: its own 120 s deadline, ado { } whileso it probes at least once, and theHasExitedcheck carried in. The ping wait keeps its 90 s." — confirmed;$deadline(90s, ping) and$regDeadline(120s, registration) are now distinct and each used exactly once. - VERIFIED — "
smoke.mjs: both copies of the loading-strip predicate now treat a card withstyle.display === "none"as hidden... as well as any[hidden]ancestor" — confirmed in both thewaitForFunctionand polling-loop copies; traced the real need toserver-tabs.js'sfanout/hidesPanel(shell.panel.style.display = "none"with nohiddenattribute forhideWhenNoRowscards), which the old[hidden]-only filter could not see. - VERIFIED — "One sentence in
pages.json_commentand in the README: apanelkey is unambiguous only for a read that feeds one card; key a fanout panel bytitle" — matches the diff, and cross-checked againstpages.json's actual entries: every multi-card fanout read already keys bytitle, so this is documentation catching up to existing-correct usage, not a fix to a live bug. - UNVERIFIED — the "Revert check" (injecting
max_background_workers = 73fails the test) and all "Local results" test-run counts (1591 passed, etc.) andnode --check— these are live run outputs that a read-only tree can't reproduce.
Riskiest lines
tools/ui-smoke/run-ui-smoke.ps1:84-91— the new registration wait's own deadline and early-exit check — pinned by none directly (no unit test exercises this script; it's only exercised by thedarling-ui-smoke.ymlCI run itself), but the logic was traced by hand and is sound (do-while probes once before the clock check,$deadline/$regDeadlineno longer alias).tools/ui-smoke/smoke.mjs:73-76,87-90— the two loading-strip filters must stay in lockstep with each other and withreadPanels'shidden = card.style.display === "none" || card.hiddendefinition — UNPINNED by any unit test (smoke.mjs has none), but hand-verified both copies now match and.panel.card/[data-panel]are confirmed to be the same element viapanelShell/renderPanel..github/darling-paths-filter.yml:58-63— new gate entries — pinned byDarlingPathFilterGateTests.TheDarlingGate_CoversTheUiSmokeWorkflowAndHarness.
Traps near this diff
- "dorny/paths-filter v4 evaluates each pattern as an INDEPENDENT predicate under
predicate-quantifier: some: a bare!**/*.mdline is not a subtraction... Keep the carve-out INSIDE the include:dir/**/!(*.md)." — does not apply; the new line istools/ui-smoke/**/!(*.md), the carve-out form, at.github/darling-paths-filter.yml:61, not a standalone exclude. - "Lite and Darling ship in parity... a fix to one SKU's reader, tool, alert or description without the counterpart's is a finding." — does not apply; this diff is Darling-only CI/test/harness plumbing (worker sizing, path gate, ui-smoke harness) with no Lite-side analog to keep in parity.
Divergence (blind read vs PR body)
None — the body's "Review round (lane D, head ab4ceb4)" section's description of both commits (worker sizing, path filter, registration wait, settle filter, panel: key docs) matches what the diff actually does, including the detail that the [hidden]-ancestor check was kept alongside the new .panel.card check rather than replaced by it.
Reviewer checklist
- Let the
darling-ui-smoke.ymlpull_requestrun execute once for real (a draft PR skips it) and confirm the registration wait's new 120 s budget and theHasExitedearly-exit actually fire as intended on a live run, not just by code trace.
Claude posting for Erik Darling.
Fixes #5701 (phase 1 of 3; phases 2 and 3 get their own issues). Lane A: harness, seed, workflow. Lane B (PG seed and tabs, remaining pages, drift-guard test, nightly wiring, artifacts, README) is the last section.
Why
Nothing loads the Darling web pages in a browser in CI, so a broken panel, a failed
/api/*read or a console error reaches dev unseen.What the check does
tools/ui-smoke/run-ui-smoke.ps1starts the Darling service (Release) on a throwaway PostgreSQL store, appliesseed.sql, and runssmoke.mjs(playwright-core1.64.0, lockfile, the runner's installed Chrome). Pages inpages.json: Fleet,ui-sqlOverview, Wait Stats, CPU, Memory, I/O, Queries, and Web Admin's servers tab. Per page, any failure exits 1:/api/*(or asset) answer >= 400; the allow-list starts empty and each entry needs an issue number#mainfills; the sidebar listsui-sql; the route's nav link (or the server) is active.strip.erroror[role=alert]in#mainstate: empty)A failing page keeps a screenshot; a page that fails is retried once, and a retry that passes is printed as a
FLAKEline, not hidden.Product change (the only one):
data-panel(read name) anddata-titleon the panel cards (panelShellinserver-tabs.jsandrenderPanelinpanels.js, which most panels use), anddata-gridon.grid-box(and on the bare table wrapper when a grid has no tools).check-web-js.sh: 68 files, 0 failures, 68 modules linked.Spike (the 20-minute box, by reading the code)
servers: []does NOT start:DarlingConfig.Validatereturns "servers must contain at least one entry" (DarlingConfig.cs:542), fatal. Fallback used: the config registers one SQL Server,ui-sqlat127.0.0.1,1(integrated auth), an address nothing listens on. The connect fails at once, the worker backs off quietly (a warning in the log), and the service never writes over the seeded rows. Alerts, analysis and MCP are off in the config, so a failed connect sends nothing. I did not disable the server in the store:collect.servers.is_enabledmirrors the config row and every Fleet read filters on it, so a disabled server would vanish from Fleet.seed.sqlwrites thecollect.serversrow (storage name = host) and the collector rows; Fleet, the sidebar and the tabs show it (see the run below).web.networkblock), so the harness signs in to nothing and no token exists.Run (the push run on this branch, windows-latest)
First run: green in 2 m 54 s, all 8 pages clean. Panels seen with data: Overview (Overview, Server Properties, CPU, Memory, File I/O, Daily Summary), Wait Stats (4 rows), CPU and Queries Top Queries by CPU (12 rows), Memory trend, I/O (File I/O Stats 2 rows, Database Sizes 3 rows), Query Duration and Executions per Second. Everything else shows its empty text.
Draft skip: the
pull_requestrun on this draft PR concludedskipped. (gh workflow run ... --refreturns 404 until the workflow file is on the default branch, so the run above came from a TEMPORARYpush:trigger for this branch; it is removed in the last commit.)Red proof: a commit that blanked every grid (
gridTablereturning the empty strip) failed the job: "panel Wait Stats has 0 rows, wanted >= 4", "panel get_top_queries_by_cpu has 0 rows, wanted >= 12", "grid #0 not found (page has 0)", 5/8 pages clean. Reverted in the next commit; the run after the revert is the final green one.Finding: a first-load 404 on Fleet
Fixed by lane B, see the lane-B section.
Expected minutes
About 3 minutes measured warm (pg-runtime cache hit, service build, store, 8 page loads); plan estimate 8 to 10 for the full ~40 pages, +3 on a cold pg-runtime cache. Cap 20.
Run it locally
See
tools/ui-smoke/README.md(start a throwaway cluster by hand, thenrun-ui-smoke.ps1 -PgBin ... -PgPort ...).Not done / for the coordinator
See the lane-B section: the guards lane A could not run were run by lane B.
Lane B: coverage, drift guard, nightly, guards
Pages: 32 (was 8; 34 with the part below), each with the six checks above. Fleet;
ui-sqlall 12 tabs (overview, waits, cpu, memory, blocking, io, queries, config, changes, activity, events, health);ui-pgall 8 tabs (overview, activity, vacuum, waits, io, replication, storage, config); Alert History, Alert Rules, FinOps, Fleet Sweeps, Custom Views, Mute Rules, Manage Tags, Job History; Admin servers, routes and settings. The sidebar must list both servers.#/agand#/triageare covered in the "Icons, AG and triage" part below.Seed coverage (
tools/ui-smoke/seed.sql, header lists every count).ui-sql: the lane-A tables plus 2 blocked-process reports, 1 deadlock, 2 analysis findings, 1 alert-history row.ui-pg(registered indarling.ui-smoke.jsonas a PostgreSQL target at127.0.0.1port 1, unreachable likeui-sql):pg_database_stats2 databases x 360,pg_wait_stats3 events x 360,pg_statement_stats6 queries x 36 withpg_statement_text6,collection_log3 collectors x 72. (There is nopg_top_queriestable: the Top Query Shapes panel readspg_statement_statsandpg_statement_text.) Row-count assertions: Blocking 2, Collectors 5, Top Query Shapes 6, By Database 2, Wait Events 3, Admin servers exactly 2, Alert History at least 1; the panels the seed leaves empty (Severe Errors, Trace Flag Changes, Sessions, Autovacuum Backlog, I/O Summary, Slots, By Table, Settings and others) must show their empty text.The 404 (assertion 1). Every response of 400 or more,
/api/*or not, is recorded with method and path in the problem text. The first load made one request that answered 404 and was retried green as aFLAKE.index.htmlhad no icon link, so Chrome asked for/favicon.ico. Fix in the product:index.htmlnow has adata:SVG icon link (the CSP already allowsimg-src data:, andapp.cssuses the same kind of URL), so the request is not made. The allow-list stays empty. Evidence: both lane-A runs flaked on Fleet; every run since has 0 flakes. I did not capture the 404's URL itself (the fix landed before a run printed it), so "favicon" is the cause by inference, not by a printed path.Hidden-panel settle.
ui-pg-overviewfirst timed out: the Instance CPU panel answers "not collected" on a non-Aurora target and hides its card (hideWhenNotCollected,panels.js) but leaves its loading strip inside. The settle wait now ignores a loading strip inside a[hidden]card. No product change.Drift guard.
Darling.Tests/UiSmokeSeedAppliesLiveTests.cs: migrates a scratch store (ScratchPostgres,#1776 own-store, not the sharedlive-postgresstore, so the seed's server rows cannot leak into other live classes), inserts the twoconfig_monitored_serversrows the service would write from the config, appliesseed.sqlwith the runtime's ownpsql(the seed uses\gset, which Npgsql cannot run; skips whenDARLING_TEST_PGRUNTIMEis unset, which CI'sdarling-pgjob sets), and asserts 21 row-count literals plus 4DarlingAgReaderones. Ran on a throwaway cluster on 55499: 1 passed, not skipped.Nightly.
nightly.ymljobdarling-ui-smoke(uses: ./.github/workflows/darling-ui-smoke.yml,needs: check, sameifasdarling-upgrade-shapes), not inpublish'sneeds. The workflow gainedworkflow_call. Artifactdarling-ui-smoke(service log, PostgreSQL log,report.json, failing pages' screenshots and<page>.trace.zip), 7 days. Each page records a Playwright trace and keeps it only when the page fails.-StartPgremoved fromrun-ui-smoke.ps1(unproven; CI starts its own cluster). The README describes starting PostgreSQL by hand.Runs on the branch (windows-latest, push trigger, since removed): 32/32 pages clean, 0 flakes, in about 3.5 minutes; one deliberate single-panel break (the Top Query Shapes
rowsKey) failed exactly that page ("panel Top Query Shapes is empty, wanted data" and "has 0 rows, wanted >= 6"), 31/32; reverted, and the run after the revert is green.Guards run locally (Darling.Tests Debug build, 0 warnings):
-trait Stage=Guard1584 passed, 0 failed; 70 classes (LockedModeRestoreCoverageTests,CrossAppGuardCiGateTests,ProductVersionDeclarationTests,CiClusterWorkerSizingTests, every class that readsindex.html,nightly.ymlor the ui-smoke files, every Census and Ratchet class,UiSmokeSeedAppliesLiveTests) 828 passed, 0 failed;check-web-js.sh68 files, 0 failures, 68 modules linked. I changed no line ofpanels.jsorserver-tabs.jsthat stays (the break was reverted), so the classes that read only those files were not separately run. The full suite was not run; CI decides.For the coordinator. The first-load fix is a user-visible product change (the tab icon). The PR still needs a non-draft run of the workflow once it is ready (a draft skips
pull_request). The sign-in page and the two small pages the host builds by hand now carry the icon too (see the part below).Icons, AG and triage
Icons on the host's own pages.
DarlingWebHostService.csbuilds three pages with their own<head>: the sign-in page (served whenweb.networkis set), the signed-in landing page and the sign-in error page. Each now carries the samedata:SVG icon link asindex.html, from one shared constant (DarlingWebHostService.IconLinkHtml), so a signed-out browser no longer asks for/favicon.icoand gets a 404. The default policy already allowsimg-src data:, so no policy changed (the sign-in page's script and style hashes are unaffected: the link holds neither).WriteSignInErrorAsyncwent fromprivatetointernalso the test can render it. NewWebPageIconLinkTests(Guard): everywwwroot/*.htmlhead has an icon link; the sign-in page (plain and SSO), the landing page and the error page each carry the shared link; the shared link is adata:URL and the same href asindex.html; a census that the host source holds exactly three<headliterals, so a fourth page fails the test until it is added.#/ag.seed.sqlnow writescollect.ag_replica_states(2 rows: groupSalesAG, replicasUISQL01primary and local,UISQL02secondary) andcollect.ag_database_replica_states(4 rows:SalesDbandOrdersDb, each on both replicas), newest minute, forui-sql. These are the tablesDarlingAgReader(the web/api/agandget_ag_health) reads throughDarlingAgStatesReader.pages.jsonpageag: navagactive, heading "Availability Groups", grid #0 exactly 4 rows.UiSmokeSeedAppliesLiveTestsnow also asserts the two table counts and reads the seed back throughDarlingAgReader.GetAvailabilityGroupCountAsync(1) andGetAgHealthAsync(one groupSalesAG, 2 replicas, 4 databases).#/triage. Pagetriage, hash#/triage?server=ui-sql&metric=High%20CPU(the route needsserverandmetric;atis left out so the anchor is now and the seeded alert, an hour old, is inside the 24-hour match window). The seeded alert'smetric_namechanged fromcputoHigh CPU, the name theauthored/cpunotebook template is keyed on (nothing asserted the old name). Assertions: heading "Triage"; panelget_top_queries_by_cpudata with at least 10 rows;get_top_procedures_by_cpuandget_cpu_scheduler_pressureempty (the seed holds no procedure or scheduler rows).Checked in a browser, not by the smoke script. I ran the Debug service on a throwaway cluster (port 55505), seeded it, and looked at the two pages with
playwright-cli(4 page-level commands, no screenshots).#/ag: heading, navagactive, one card, 2 replica chips, one grid of 4 rows, 0 error strips, 0 loading strips.#/triage: the alert notebook renders (header, status, two chart cells, three read cells); panelsget_top_queries_by_cpu10 rows with a notice strip, the other twostrip empty, none hidden. The smoke script itself was not run locally, as asked. This PR's ownpull_requestrun is the proof for all 34 pages.Local results (Debug build, 0 warnings, throwaway cluster on 55505, stopped afterwards):
UiSmokeSeedAppliesLiveTestsandWebPageIconLinkTests8 passed, 0 skipped; 49 classes that readDarlingWebHostService.cs,index.htmlor the ui-smoke files plus every Census and Ratchet class: 2157 passed, 0 failed;-trait Stage=Guard: 1591 passed, 0 failed;check-web-js.sh: 68 modules linked, 0 failures. One class (DarlingStoreLoginsTests) failed once because my edit had leftDarlingWebHostService.cswith LF endings (the repo'seol=crlf; the test matches CRLF text); fixed by restoring CRLF and rerun green. Nopush:trigger was added.Review round (lane D, head ab4ceb4)
Fixes for the automated review (id 5498827903):
darling-ui-smoke.ymlis now inCiClusterWorkerSizingTests.ClusterWorkflows()and is copied beside the test binary byDarling.Tests.csproj, soClusterWorkflow_SizesWorkersFromTheProductsFormulachecks its74/85againstTimescaleSupport.HypertableCount. That test, not the Guard run, is what pins the workflow's cluster. Revert check: withmax_background_workers = 73in the new workflow the class fails (ClusterWorkflow_SizesWorkersFromTheProductsFormula(workflowFileName: "darling-ui-smoke.yml") [FAIL], 5 run, 1 failed); restored, it passes..github/darling-paths-filter.ymldarlingfilter gainstools/ui-smoke/**/!(*.md)(the extglob form the file requires) and.github/workflows/darling-ui-smoke.yml(Darling.Tests now parses it), each with a comment. A new test,DarlingPathFilterGateTests.TheDarlingGate_CoversTheUiSmokeWorkflowAndHarness, pins both entries, the same way the upgrade-shape entries are pinned.run-ui-smoke.ps1: its own 120 s deadline, ado { } whileso it probes at least once, and theHasExitedcheck carried in. The ping wait keeps its 90 s.smoke.mjs: both copies of the loading-strip predicate now treat a card withstyle.display === "none"as hidden (thereadPanelstest), as well as any[hidden]ancestor, so a hide path that sets onlydisplay:nonecannot hang settle. I kept the[hidden]ancestor test as well as the card test, so a hidden non-card container still counts as hidden.panel:keys (ab4ceb4). One sentence inpages.json_commentand in the README: apanelkey is unambiguous only for a read that feeds one card; key a fanout panel bytitle.Local results (Debug build, 0 warnings):
CiClusterWorkerSizingTests,DarlingPathFilterGateTests,CommentFilterAdoptionTests20 passed; those three plus every Census and Ratchet class 581 passed, 0 failed, 2 skipped (the live-store classes, no cluster running);-trait Stage=Guard1591 passed, 0 failed.node --check tools/ui-smoke/smoke.mjsclean. Nowwwrootchange, socheck-web-js.shwas not rerun. The smoke check itself was not run locally and nopush:trigger was added; this PR's own run covers it.SECTION: Fixed
ENTRY:
/favicon.icoon first load ([Web UI smoke check for the Darling pages and Web Admin (#5701, phase 1) #5732]) - the pages had no icon link, so every browser's first request answered 404;index.html, the sign-in page, the signed-in landing page and the sign-in error page now carry an inline icon.REF:
[Web UI smoke check for the Darling pages and Web Admin (#5701, phase 1) #5732]: Web UI smoke check for the Darling pages and Web Admin (#5701, phase 1) #5732