From c7d3b20ca034ce6e0425dec4f16d809f8c77d651 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Sat, 11 Jul 2026 22:17:47 +0200 Subject: [PATCH] Isolate the comments e2e pack in its own pond (#92) The shared fixture pond accumulates grants from earlier packs in the same CI job, so "fixture-editor is only a reader" did not hold there and the policy test failed in CI while passing locally. A Site Admin now lifts fixture-user's additional-pond quota for the run, the pack provisions a fresh pond and page, and tears both down afterwards; the policy PATCH is also asserted. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 --- apps/web/e2e/comments.spec.ts | 69 +++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/apps/web/e2e/comments.spec.ts b/apps/web/e2e/comments.spec.ts index 0e0d855..5e4d034 100644 --- a/apps/web/e2e/comments.spec.ts +++ b/apps/web/e2e/comments.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type BrowserContext } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { contextForUser } from './helpers'; @@ -13,54 +13,56 @@ const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; let pondId: string; let pageUrl: string; -let pageId: string; -let readerUserId: string; - -async function pondOf(owner: BrowserContext): Promise<{ id: string; slug: string }> { - const ponds = (await (await owner.request.get('/api/v1/ponds')).json()) as { - id: string; - slug: string; - type: string; - }[]; - const shared = ponds.find((p) => p.type === 'shared'); - expect(shared).toBeTruthy(); - return shared!; -} +let ownerId: string; +// The pack provisions its OWN pond: the shared fixture pond accumulates +// grants from earlier packs in the same CI job (access-rules & friends), so +// "fixture-editor is only a reader here" would not hold there. A Site Admin +// lifts fixture-user's additional-pond quota for the run. test.beforeAll(async ({ browser }) => { + const admin = await contextForUser(browser, BASE_URL, 'fixture-admin'); + const lookup = (await (await admin.request.get('/api/v1/admin/users?q=fixture-user')).json()) as { + users: { id: string; username: string }[]; + }; + ownerId = lookup.users.find((u) => u.username === 'fixture-user')!.id; + const quota = await admin.request.put(`/api/v1/admin/quotas/user/${ownerId}/additional_ponds`, { + data: { value: 10 }, + }); + expect(quota.ok()).toBe(true); + await admin.close(); + const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); - const pond = await pondOf(owner); + const pondRes = await owner.request.post('/api/v1/ponds', { + data: { name: `Comments stage ${Date.now()}` }, + }); + expect(pondRes.ok()).toBe(true); + const pond = (await pondRes.json()) as { id: string; slug: string }; pondId = pond.id; - // Fresh page per run — never depends on fixture pages' trash state. const created = await owner.request.post(`/api/v1/ponds/${pondId}/pages`, { - data: { title: `Comment stage ${Date.now()}` }, + data: { title: 'Discussion' }, }); expect(created.ok()).toBe(true); - const page = (await created.json()) as { id: string; slug: string }; - pageId = page.id; + const page = (await created.json()) as { slug: string }; pageUrl = `/p/${pond.slug}/${page.slug}`; - // fixture-editor becomes a reader-member of the pond (the second user); - // a leftover membership from an aborted run just yields a conflict we ignore. - await owner.request.post(`/api/v1/ponds/${pondId}/members`, { + // fixture-editor is exactly a reader in this fresh pond — nothing else. + const member = await owner.request.post(`/api/v1/ponds/${pondId}/members`, { data: { usernameOrEmail: 'fixture-editor', role: 'reader' }, }); - const members = (await (await owner.request.get(`/api/v1/ponds/${pondId}/members`)).json()) as { - members: { id: string; username: string }[]; - }; - readerUserId = members.members.find((m) => m.username === 'fixture-editor')!.id; - // Deterministic starting policy. - await owner.request.patch(`/api/v1/ponds/${pondId}`, { data: { commentPolicy: 'readers' } }); + expect(member.ok()).toBe(true); await owner.close(); }); test.afterAll(async ({ browser }) => { const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); - await owner.request.patch(`/api/v1/ponds/${pondId}`, { data: { commentPolicy: 'readers' } }); - if (readerUserId) await owner.request.delete(`/api/v1/ponds/${pondId}/members/${readerUserId}`); - if (pageId) await owner.request.delete(`/api/v1/pages/${pageId}`); + if (pondId) await owner.request.delete(`/api/v1/ponds/${pondId}`); await owner.close(); + const admin = await contextForUser(browser, BASE_URL, 'fixture-admin'); + if (ownerId) { + await admin.request.delete(`/api/v1/admin/quotas/user/${ownerId}/additional_ponds`); + } + await admin.close(); }); test('two users run the full comment lifecycle with resolve/unresolve', async ({ browser }) => { @@ -130,7 +132,10 @@ test('two users run the full comment lifecycle with resolve/unresolve', async ({ test('the composer hides with a hint when the policy bars readers', async ({ browser }) => { const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); - await owner.request.patch(`/api/v1/ponds/${pondId}`, { data: { commentPolicy: 'editors' } }); + const patched = await owner.request.patch(`/api/v1/ponds/${pondId}`, { + data: { commentPolicy: 'editors' }, + }); + expect(patched.ok()).toBe(true); await owner.close(); const reader = await contextForUser(browser, BASE_URL, 'fixture-editor');