Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fba4b46
test: live interaction coverage gate for the chat surface
sakibsadmanshajib Aug 9, 2026
3cf62b2
test: prove chat controls live, and teach live-auth the chat OIDC hop
sakibsadmanshajib Aug 10, 2026
2b9588d
test: record the live chat coverage run and prove the detector on a f…
sakibsadmanshajib Aug 10, 2026
ad3087c
fix: stop the chat coverage gate counting failures as proof, and floo…
sakibsadmanshajib Aug 10, 2026
84dba92
test: record the corrected chat coverage run and stop recording empty…
sakibsadmanshajib Aug 11, 2026
33a4832
docs: record the 504s and the unstable denominator in the chat covera…
sakibsadmanshajib Aug 11, 2026
dbab865
refactor: make distinct control identities the headline coverage figure
sakibsadmanshajib Aug 11, 2026
3c88ff7
fix: stop the coverage gate manufacturing proof, and redact what it w…
sakibsadmanshajib Aug 11, 2026
64265b6
test: run the coverage gate in CI, and stop it moving its own bar
sakibsadmanshajib Aug 11, 2026
245249c
fix: close the leak paths the bot review found in the coverage prover
sakibsadmanshajib Aug 11, 2026
15ee811
docs: name the earlier ledger by the day it was actually taken
sakibsadmanshajib Aug 11, 2026
cb71862
fix: require a server verdict for network proof, and stop laundering …
sakibsadmanshajib Aug 12, 2026
e33949a
fix: keep framework-generated text out of the coverage artifact, and …
sakibsadmanshajib Aug 12, 2026
5f5c37c
docs: withdraw a coverage figure no shipped code path can produce
sakibsadmanshajib Aug 12, 2026
3d23e41
fix: stop the chat list floor turning a groomed account into a red gate
sakibsadmanshajib Aug 12, 2026
21eab6f
fix: keep a presence bar on the chat list instead of no floor at all
sakibsadmanshajib Aug 12, 2026
09d3992
fix: match a response to the request the click made, and redact the l…
sakibsadmanshajib Aug 12, 2026
4f712ff
refactor: narrow DOM nodes with instanceof instead of casting
sakibsadmanshajib Aug 12, 2026
3a75aa8
fix: address the bot review on the rebased head
sakibsadmanshajib Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions .github/workflows/chat-coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: Chat interaction coverage

# Two jobs, deliberately gated differently.
#
# self-check Runs everywhere, on every pull request that touches the gate.
# Needs no deployment, no session and no secrets: it serves its
# own fixture from an intercepted origin and asserts the prover
# still calls a dead button dead. This is the part that keeps
# the gate from rotting into something that cannot go red.
#
# live-sweep The real sweep, against the deployed chat surface. It needs a
# running deployment and a minted session, so it never fires on
# a plain pull request: run it by hand, or label a pull request
# run-chat-coverage. The same reason owui-nightly is not a
# per-pull-request gate.

on:
pull_request:
types: [opened, synchronize, reopened, labeled]
Comment thread
sakibsadmanshajib marked this conversation as resolved.
push:
branches: [main]
workflow_dispatch:
inputs:
surfaces:
description: 'Surface filter (a regular expression, blank for a full sweep)'
required: false
default: ''

concurrency:
group: chat-coverage-${{ github.ref }}
cancel-in-progress: true

# Read only, and narrowed to what is actually read: the self-check job asks the
# issues API whether a surface exclusion's blocking issue has closed. Nothing
# here writes to the repository, and a job that walks a live OAuth flow should
# not be holding a token that could.
permissions:
contents: read
issues: read

jobs:
self-check:
name: Chat coverage self-check (no deployment needed)
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: apps/web-console
steps:
- uses: actions/checkout@v4
with:
# Leave no usable git credential in the runner's config for the rest
# of the job, which runs a browser against a live deployment.
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: apps/web-console/package-lock.json
- name: Install dependencies
run: npm ci
- name: Install Chromium
run: npx playwright install --with-deps chromium
- name: Prover break-proof, registry and exclusion expiry
# GITHUB_TOKEN is what the exclusion-expiry check reads issue state
# with. It fails rather than skips without one, on purpose: an
# exclusion whose blocking issue has closed has to become a failure
# the day it closes, and a check that skips itself cannot do that.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: npm run e2e:chat-coverage:self-check

live-sweep:
name: Chat coverage live sweep
runs-on: ubuntu-latest
timeout-minutes: 60
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'run-chat-coverage'))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
defaults:
run:
working-directory: apps/web-console
env:
CHAT_URL: ${{ vars.CHAT_URL || 'https://chat-hive.scubed.co' }}
CONSOLE_URL: ${{ vars.CONSOLE_URL || 'https://console-hive.scubed.co' }}
# An existing account to mint a session for. live-auth.mjs mints through
# the admin one-time-token flow, so no password is read and none is
# changed; HIVE_QA_TESTER_PASSWORD is deliberately not passed in here.
HIVE_QA_AGENT_EMAIL: ${{ secrets.HIVE_QA_AGENT_EMAIL || secrets.HIVE_QA_TESTER_EMAIL }}
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
COV_SURFACES: ${{ inputs.surfaces }}
steps:
- uses: actions/checkout@v4
with:
# Leave no usable git credential in the runner's config for the rest
# of the job, which runs a browser against a live deployment.
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: apps/web-console/package-lock.json
- name: Check the inputs the sweep cannot run without
# Loud, not quiet. A missing secret here used to leave the whole
# config matching zero files, which reports a green run of nothing.
run: |
set -euo pipefail
missing=""
for v in CHAT_URL CONSOLE_URL HIVE_QA_AGENT_EMAIL SUPABASE_URL SUPABASE_ANON_KEY SUPABASE_SERVICE_ROLE_KEY; do
[ -n "${!v:-}" ] || missing="$missing $v"
done
if [ -n "$missing" ]; then
echo "::error::the live sweep needs:$missing"
exit 1
fi
- name: Install dependencies
run: npm ci
- name: Install Chromium
run: npx playwright install --with-deps chromium
- name: Sweep the chat surface
run: npm run e2e:chat-coverage
- name: Upload the coverage ledger
if: always()
uses: actions/upload-artifact@v4
with:
name: chat-coverage-ledger
# Two named files, never a directory. Everything this suite writes
# itself goes through redactUrl, but a directory upload also sweeps
# up whatever the framework wrote beside them: Playwright's JSON and
# HTML reports carry raw error text, and a sign-in that outruns its
# ninety second wait quotes the OAuth callback with a live code and
# state in the timeout message. Those reports are configured to land
# outside this directory, and naming the files here means a future
# reporter added to the config cannot quietly rejoin the artifact.
# Traces and videos are off for the same reason and cannot be
# inspected by the text lint at all.
path: |
apps/web-console/chat-coverage-report/coverage.run.json
Comment thread
sakibsadmanshajib marked this conversation as resolved.
apps/web-console/chat-coverage-report/coverage.md
if-no-files-found: error
retention-days: 14
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ __pycache__/
# named anything other than test-results/ was landing traces and videos in
# `git status` where an `add -A` could commit them.
/apps/web-console/test-results*/
/apps/web-console/playwright-report-chat-coverage/
/apps/web-console/chat-coverage-report/
/apps/web-console/e2e/chat-coverage/.auth/
# Added by code-review-graph
.code-review-graph/

Expand Down
36 changes: 36 additions & 0 deletions apps/web-console/e2e/chat-coverage/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Signs the coverage sweep in and saves storageState.
//
// signInToChat lives in tests/e2e/support/live-auth.ts, and is the only
// implementation of the chat hop: the mint itself stays in live-auth.mjs,
// which this shells out to, so the service-role key never enters this worker's
// module graph. It mints a session the way a magic link does, so no password
// is read, written or rotated, then carries it through the real "Continue with
// Hive" OIDC hop, which is the only thing that gets Open WebUI's own session
// cookie set. See docs/live-test-auth.md.
//
// This project records no trace, video or screenshot (see the config): the
// callback URL it walks through carries a live `code` and `state`, and no text
// lint can read inside a trace or a video to catch them.
import path from "node:path";

import { test as setup } from "@playwright/test";

import { signInToChat } from "../../tests/e2e/support/live-auth";

const STATE = path.join(__dirname, ".auth", "state.json");
const CHAT = process.env.CHAT_URL ?? process.env.OWUI_URL ?? "";
const CONSOLE = process.env.CONSOLE_URL ?? "";
const EMAIL = process.env.OWUI_E2E_EMAIL ?? process.env.HIVE_QA_AGENT_EMAIL ?? "";

setup("authenticate against the chat surface", async ({ browser }) => {
setup.setTimeout(6 * 60_000);
if (!CHAT || !CONSOLE || !EMAIL) {
setup.skip(true, "CHAT_URL, CONSOLE_URL and an account are all required");
return;
}

const context = await browser.newContext({ viewport: { width: 1440, height: 950 } });
await signInToChat(context, { email: EMAIL, consoleUrl: CONSOLE, chatUrl: CHAT });
await context.storageState({ path: STATE });
await context.close();
});
Loading
Loading