Phase 8: Gitea CI — runner probe + PR gating jobs (fast-checks + api) #3

Merged
luckberg merged 66 commits from gsd/phase-08-gitea-ci into main 2026-06-11 16:11:42 -04:00
Showing only changes of commit c564fc67a1 - Show all commits
+29 -17
View File
@@ -38,19 +38,33 @@ test.describe('TEST-02 preconditions — auth bypass and SW block', () => {
)
})
test('no service-worker controller (serviceWorkers: block enforced)', async ({ page }) => {
test('no service-worker registration (serviceWorkers: block enforced)', async ({ page }) => {
await page.goto('/calendar')
// serviceWorkers: 'block' in playwright.config.ts prevents SW registration.
// navigator.serviceWorker.controller is null when no SW is controlling the page.
// Note: navigator.serviceWorker may be undefined in some contexts (e.g. non-https),
// but in the dev server context it is defined. Treat undefined as no-controller (safe).
const controller = await page.evaluate(() => {
if (typeof navigator === 'undefined') return null
if (!('serviceWorker' in navigator)) return null
return navigator.serviceWorker.controller
})
expect(controller, 'Service worker controller should be null (serviceWorkers:block enforced)').toBeNull()
//
// WR-07: asserting `navigator.serviceWorker.controller === null` is near-vacuous —
// (a) on WebKit over plain http://localhost, `serviceWorker` is often *absent* from
// navigator (secure-context strictness), so the old guard returned null and the
// assertion passed without ever proving the block worked; and
// (b) `controller` is null on a first uncontrolled load even when SW *is* available,
// regardless of the block setting.
// Instead probe getRegistration() — when SW is available and `block` is in effect, no
// registration exists, so it resolves to undefined. Where `serviceWorker` is absent
// entirely (WebKit/http), skip rather than let an unavailable API masquerade as a pass.
const swAvailable = await page.evaluate(
() => typeof navigator !== 'undefined' && 'serviceWorker' in navigator,
)
test.skip(
!swAvailable,
'navigator.serviceWorker is unavailable in this context (e.g. WebKit over http://localhost) — block is unobservable here',
)
const registration = await page.evaluate(() => navigator.serviceWorker.getRegistration())
expect(
registration,
'No service worker should be registered (serviceWorkers:block enforced)',
).toBeUndefined()
})
})
@@ -116,9 +130,9 @@ test.describe('Rule 5 — calendar error state (API mocked to 500)', () => {
const retryBtn = page.getByRole('button', { name: 'Retry' })
await expect(retryBtn).toBeVisible()
// Unroute so the mock does not leak to subsequent tests (T-07-11)
await page.unroute('/api/events*')
// No manual unroute (WR-05): Playwright gives each test a fresh page/context, so route
// handlers do not leak across tests. A trailing unroute also never runs if an `expect`
// above throws — it was misleading "cleanup" that guaranteed nothing.
})
test('Retry button meets 44px touch-target minimum in error state (Rule 1)', async ({
@@ -137,8 +151,7 @@ test.describe('Rule 5 — calendar error state (API mocked to 500)', () => {
const box = await retryBtn.boundingBox()
expect(box, 'Retry button bounding box must not be null').not.toBeNull()
expect(box!.height, 'Retry button height must be ≥ 44px (Rule 1)').toBeGreaterThanOrEqual(44)
await page.unroute('/api/events*')
// No manual unroute (WR-05): per-test context isolation handles route cleanup.
})
test('no horizontal overflow in error state (Rule 2)', async ({ page }) => {
@@ -160,7 +173,6 @@ test.describe('Rule 5 — calendar error state (API mocked to 500)', () => {
overflow.scrollWidth,
`scrollWidth (${overflow.scrollWidth}) must be ≤ clientWidth (${overflow.clientWidth}) in error state`,
).toBeLessThanOrEqual(overflow.clientWidth)
await page.unroute('/api/events*')
// No manual unroute (WR-05): per-test context isolation handles route cleanup.
})
})