Skip to content

Commit a62061e

Browse files
authored
Merge branch 'main' into scanner/fix-19276
2 parents c61d8b1 + 78aa4f1 commit a62061e

7 files changed

Lines changed: 86 additions & 47 deletions

File tree

CONTRIBUTING.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ Starts backend on `:8080` and frontend on `:5174` with a mock `dev-user` account
104104
- Update documentation when changing behavior
105105
- Keep PRs focused on a single concern
106106

107+
### Review and Auto-QA Triage
108+
109+
The project tracks proposed review and triage expectations in [docs/plans/PR-TRIAGE-SLA.md](docs/plans/PR-TRIAGE-SLA.md).
110+
Issues or PRs labeled `ai-needs-human` need a human decision rather than more automation. For Auto-QA issues, the expected decision is to accept, defer, or close the finding so contributors know whether follow-up work, including PRs, is welcome.
111+
107112
### Netlify Functions parity for API changes
108113

109114
If your PR changes shared API behavior, update both sides of the production architecture:

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ This index covers the Markdown and YAML documentation files in `docs/` and group
3232
| [components/component-criteria.md](components/component-criteria.md) | Defines the criteria and review checklist for dashboard components. |
3333
| [plans/PLUGIN-ARCHITECTURE-RFC.md](plans/PLUGIN-ARCHITECTURE-RFC.md) | RFC defining plugin scope, extension points, security constraints, and phased rollout. |
3434
| [plans/GITOPS-INTEGRATION-RFC.md](plans/GITOPS-INTEGRATION-RFC.md) | Concrete mid-term RFC for Flux + Argo CD integration, declarative Console config, and Mission Control deep links. |
35+
| [plans/PR-TRIAGE-SLA.md](plans/PR-TRIAGE-SLA.md) | Proposed review and Auto-QA triage SLA for `ai-needs-human` PRs and issues. |
3536
| [plans/UNIFIED-DEMO-SKELETON-PLAN.md](plans/UNIFIED-DEMO-SKELETON-PLAN.md) | Implementation plan for the unified demo-data and loading-skeleton system. |
3637
| [plans/planjan21.md](plans/planjan21.md) | Plan for console filtering and data-consistency improvements from January 2026. |
3738
| [qa/AI-UX-ISSUE-AGENT-BRIEF.md](qa/AI-UX-ISSUE-AGENT-BRIEF.md) | Operating brief for the agent that turns Playwright UX findings into issues. |

docs/plans/PR-TRIAGE-SLA.md

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22

33
> Status: proposed
44
> Horizon: v0.4 — AI-Native Observability (Q3 2026)
5-
> Related issue: #17587
5+
> Related issues: #17587, #19256
66
77
## Problem statement
88

9-
KubeStellar Console uses Claude Code GitHub Actions for AI-assisted PR review and issue triage. PRs labeled `ai-needs-human` indicate cases where the AI agent has identified issues that require human judgment, but without a defined service level agreement (SLA) or escalation path, these PRs can remain stuck indefinitely. This creates contributor friction, slows down the review pipeline, and undermines the effectiveness of the AI triage system.
9+
KubeStellar Console uses Claude Code GitHub Actions for AI-assisted PR review and issue triage. PRs and issues labeled `ai-needs-human` indicate cases where the AI agent has identified work that requires human judgment, but without a defined service level agreement (SLA) or escalation path, these items can remain stuck indefinitely. This creates contributor friction, slows down the review pipeline, and undermines the effectiveness of the AI triage system.
1010

11-
Without clear expectations for human review latency, contributors cannot estimate when their PRs will be merged, and maintainers lack prioritization signals to focus their limited review bandwidth.
11+
Without clear expectations for human review latency, contributors cannot estimate when their PRs will be merged or when Auto-QA findings will be accepted, deferred, or closed. Maintainers also lack prioritization signals to focus their limited review bandwidth.
1212

1313
## Goals
1414

1515
1. Establish measurable SLA targets for PR triage across all states (`needs-review`, `ai-needs-human`, `changes-requested`).
16-
2. Define a clear escalation path for `ai-needs-human` PRs that exceed the SLA threshold.
16+
2. Define a clear escalation path for `ai-needs-human` PRs and issues that exceed the SLA threshold.
1717
3. Automate SLA monitoring and alerting to surface stuck PRs before they become stale.
18-
4. Provide contributors with visibility into expected review timelines.
18+
4. Provide contributors with visibility into expected review and triage timelines.
1919
5. Generate weekly triage reports to track SLA adherence and identify bottlenecks.
2020

2121
## Non-goals
@@ -24,6 +24,7 @@ Without clear expectations for human review latency, contributors cannot estimat
2424
- Guaranteeing immediate reviews for all PRs — the SLA establishes targets, not hard commitments.
2525
- Auto-merging PRs without human approval, even when AI review passes.
2626
- Applying SLAs to draft PRs or PRs explicitly marked as work-in-progress.
27+
- Requiring maintainers to implement every Auto-QA finding. The SLA requires a clear decision, not automatic acceptance.
2728

2829
## Current foundation
2930

@@ -69,6 +70,36 @@ When a PR receives the `ai-needs-human` label, the following escalation sequence
6970
4. **Day 7** — If still unreviewed, label added to bi-weekly contributor sync agenda; maintainer availability evaluated.
7071
5. **Day 14** — Project lead makes merge/close/defer decision; outcome documented in PR comment.
7172

73+
## Auto-QA issue triage SLA
74+
75+
Auto-QA issues can represent real quality gaps, noisy thresholds, or work that is too broad for a single PR. When an issue receives both `auto-qa` and `ai-needs-human`, the goal is to record a human decision within 14 days.
76+
77+
### Required decision
78+
79+
Each stuck Auto-QA issue should receive one of these outcomes:
80+
81+
- **Accept**: confirm the finding is actionable, remove `ai-processing`, assign or decompose the work, and leave the issue open.
82+
- **Defer**: keep the finding but move it to a milestone, roadmap item, or child issues with a clear follow-up path.
83+
- **Close**: close as not planned when the finding is noisy, too broad, or not worth the maintenance cost.
84+
85+
### Current backlog queue
86+
87+
Issue #19256 tracks four Auto-QA items that need this decision path:
88+
89+
| Issue | Decision needed |
90+
| --- | --- |
91+
| #18599 | Accept or defer the missing component test coverage work. If accepted, keep work in focused child issues. |
92+
| #18598 | Accept oversized test-file refactoring, tune the threshold, or close if large test files are acceptable. |
93+
| #19077 | Accept bundle-size work, tune the chunk-size threshold, or defer to a performance milestone. |
94+
| #19161 | Audit major dependency updates and decide whether to schedule, defer, or close each upgrade path. |
95+
96+
### Escalation path
97+
98+
1. **Day 0**: `ai-needs-human` is added; issue comment states the decision needed.
99+
2. **Day 7**: If no maintainer response, add the issue to the weekly triage agenda and tag a reviewer.
100+
3. **Day 14**: Maintainer records an accept/defer/close decision and removes `ai-processing`.
101+
4. **After Day 14**: If no owner exists to take the work, record **Defer** by closing as **not planned** with a short note to reopen when ownership exists.
102+
72103
## Automation implementation
73104

74105
Deliver SLA enforcement through GitHub Actions workflows:

web/e2e/Dashboard.spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const MOBILE_VIEWPORT_HEIGHT_PX = 667
3333
const TABLET_VIEWPORT_WIDTH_PX = 768
3434
const TABLET_VIEWPORT_HEIGHT_PX = 1024
3535
const KEYBOARD_FOCUS_SEQUENCE_LENGTH = 5
36+
const WEBKIT_MIN_FOCUS_SEQUENCE_LENGTH = 3
3637
const STANDARD_TAB_KEY = 'Tab'
3738
const WEBKIT_FULL_KEYBOARD_TAB_KEY = 'Alt+Tab'
3839
const REFRESH_BUTTON_TITLE = 'Refresh cluster data'
@@ -68,6 +69,13 @@ const FOCUSABLE_SELECTOR = [
6869
'textarea:not([disabled])',
6970
'[tabindex]:not([tabindex="-1"])',
7071
].join(', ')
72+
const WEBKIT_FOCUSABLE_SELECTOR = [
73+
'a[href]',
74+
'input:not([disabled])',
75+
'select:not([disabled])',
76+
'textarea:not([disabled])',
77+
'[tabindex]:not([tabindex="-1"])',
78+
].join(', ')
7179

7280
type MockCluster = {
7381
name: string
@@ -479,6 +487,10 @@ test.describe('Dashboard Page', () => {
479487
await expect(page.getByTestId('dashboard-page')).toBeVisible({ timeout: ACCESSIBILITY_ASSERT_TIMEOUT_MS })
480488

481489
const tabKey = browserName === 'webkit' ? WEBKIT_FULL_KEYBOARD_TAB_KEY : STANDARD_TAB_KEY
490+
const focusableSelector = browserName === 'webkit' ? WEBKIT_FOCUSABLE_SELECTOR : FOCUSABLE_SELECTOR
491+
const requiredFocusCount = browserName === 'webkit'
492+
? WEBKIT_MIN_FOCUS_SEQUENCE_LENGTH
493+
: KEYBOARD_FOCUS_SEQUENCE_LENGTH
482494
const expectedFocusOrder = await page.evaluate(({ selector, limit }) => {
483495
const isVisible = (element: Element) => {
484496
const htmlElement = element as HTMLElement
@@ -518,11 +530,11 @@ test.describe('Dashboard Page', () => {
518530
label: getLabel(element),
519531
}))
520532
}, {
521-
selector: FOCUSABLE_SELECTOR,
533+
selector: focusableSelector,
522534
limit: KEYBOARD_FOCUS_SEQUENCE_LENGTH,
523535
})
524536

525-
expect(expectedFocusOrder.length).toBe(KEYBOARD_FOCUS_SEQUENCE_LENGTH)
537+
expect(expectedFocusOrder.length).toBeGreaterThanOrEqual(requiredFocusCount)
526538
await page.evaluate(() => {
527539
(document.activeElement as HTMLElement | null)?.blur?.()
528540
})

web/e2e/Sidebar.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,13 @@ test.describe('Sidebar Navigation', () => {
116116
test('dashboard link navigates to home', async ({ page }) => {
117117
await expect(page.getByTestId('sidebar')).toBeVisible({ timeout: SIDEBAR_TIMEOUT_MS })
118118

119-
// Navigate away first — clicking the home link while already on "/"
120-
// would not exercise any real routing behavior.
121-
await page.goto('/clusters')
119+
// Navigate away first using the real sidebar link instead of a deep-link
120+
// goto(). Firefox/WebKit nightly runs use Vite preview and can race on
121+
// direct sub-route loads, whereas in-app navigation is deterministic.
122+
const clustersLink = page.locator('[data-testid="sidebar-primary-nav"] a[href="/clusters"], [data-testid="sidebar"] a[href="/clusters"]').first()
123+
await expect(clustersLink).toBeVisible({ timeout: SIDEBAR_TIMEOUT_MS })
124+
await clustersLink.click({ force: true })
122125
await page.waitForLoadState('domcontentloaded')
123-
await expect(page.getByTestId('sidebar')).toBeVisible({ timeout: SIDEBAR_TIMEOUT_MS })
124126
await expectDashboardNavigation(page, '/clusters', 'My Clusters')
125127

126128
const dashboardLink = page.locator('[data-testid="sidebar-primary-nav"] a[href="/"], [data-testid="sidebar"] a[href="/"]').first()

web/e2e/responsive-regression.spec.ts

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { test, expect, Page } from '@playwright/test'
2+
import { mockApiFallback, mockLocalAgentUnavailable, mockApiMe } from './helpers/setup'
3+
import { setupDemoMode as setupSharedDemoMode } from './helpers/storage-setup'
24

35
/**
46
* Responsive Breakpoint Regression Tests
@@ -39,35 +41,13 @@ const KEY_ROUTES = [
3941
]
4042

4143
async function setupDemoMode(page: Page) {
42-
// Seed localStorage BEFORE any page script runs so the auth guard sees
43-
// the token on first execution. page.evaluate() runs after the page has
44-
// already parsed and executed scripts, which is too late for webkit/Safari
45-
// where the auth redirect fires synchronously on script evaluation.
46-
// page.addInitScript() injects the snippet ahead of any page code (#9096).
47-
await page.addInitScript(() => {
48-
localStorage.setItem('token', 'demo-token')
49-
localStorage.setItem('kc-demo-mode', 'true')
50-
localStorage.setItem('demo-user-onboarded', 'true')
51-
})
52-
53-
// Mock /api/me so AuthProvider has a deterministic user without a backend.
54-
// Without this, WebKit may redirect to /login before the page renders. #10200
55-
await page.route('**/api/me', (route) =>
56-
route.fulfill({
57-
status: 200,
58-
contentType: 'application/json',
59-
body: JSON.stringify({
60-
id: '1',
61-
github_id: '12345',
62-
github_login: 'testuser',
63-
email: 'test@example.com',
64-
onboarded: true,
65-
}),
66-
})
67-
)
44+
await setupSharedDemoMode(page)
45+
await mockApiFallback(page)
46+
await mockLocalAgentUnavailable(page)
47+
await mockApiMe(page)
6848

69-
// Mock MCP endpoints to prevent unmocked requests hitting a non-existent
70-
// backend, which causes WebKit CORS errors and slow timeouts. #10200
49+
// Keep the MCP override explicit so responsive tests always receive a stable
50+
// shape for cluster-backed views, regardless of helper evolution.
7151
await page.route('**/api/mcp/**', (route) =>
7252
route.fulfill({
7353
status: 200,
@@ -182,10 +162,14 @@ test.describe('Responsive Breakpoint Tests', () => {
182162
const mobileNav = page.locator('[data-testid="mobile-menu-toggle"]')
183163
.or(page.locator('button[aria-label*="menu" i]'))
184164
.or(page.locator('[data-testid="sidebar-toggle"]'))
165+
.or(page.getByTestId('navbar-home-btn'))
185166

186167
const hasHamburger = await mobileNav.first().isVisible({ timeout: MOBILE_NAV_PROBE_TIMEOUT_MS }).catch(() => false)
187168

188-
// Either hamburger menu is present OR nav items are visible
169+
// Either a primary navbar control is present OR nav items are visible.
170+
// On narrow viewports the app can collapse into a compact header with
171+
// a home button before the full nav items render, and that still
172+
// satisfies "navigation is accessible" for this regression guard.
189173
if (!hasHamburger) {
190174
const navItems = page.locator('nav a, nav button')
191175
const navCount = await navItems.count()

web/e2e/smoke.spec.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,17 @@ test.describe('Smoke Tests', () => {
154154
test('clicking navbar logo navigates to home from non-home route', async ({ page }) => {
155155
await setupDemoMode(page)
156156

157-
// Navigate to a non-home route
158-
await page.goto('/settings')
159-
160-
// Use regex-based URL assertion for cross-browser compatibility.
161-
// Glob-based waitForURL('**/settings') fails on Firefox/WebKit. (#18588)
162-
await expect(page).toHaveURL(/\/settings/, { timeout: 10000 })
163-
await expect(page.locator('h1:has-text("Settings")')).toBeVisible({ timeout: 10000 })
157+
// Navigate via the in-app sidebar instead of a deep-link goto().
158+
// The nightly workflow runs against Vite preview, where direct sub-route
159+
// entry can be flaky across Firefox/WebKit. In-app navigation exercises
160+
// the actual router path without relying on preview-server rewrites.
161+
await page.goto('/', { waitUntil: 'domcontentloaded' })
162+
const settingsLink = page.locator('[data-testid="sidebar-primary-nav"] a[href="/settings"], [data-testid="sidebar"] a[href="/settings"]').first()
163+
await expect(settingsLink).toBeVisible({ timeout: 10000 })
164+
await settingsLink.click({ force: true })
165+
166+
await expect(page).toHaveURL(/\/settings(?:[?#].*)?$/, { timeout: 10000 })
167+
await expect(page.locator('main [data-testid="settings-title"]').first()).toBeVisible({ timeout: 10000 })
164168

165169
// Click the logo button (has aria-label "Go to home dashboard").
166170
// The navbar renders two such buttons — the logo and the wordmark —

0 commit comments

Comments
 (0)