Isolate the comments e2e pack in its own pond (#92)
Some checks failed
CD / Build and push images (push) Successful in 1m5s
CD / Deploy to Test (push) Successful in 10s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 3m17s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Failing after 2m45s
CI / Import/export fidelity gate (push) Has been skipped

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
This commit is contained in:
Claude Fable 5 2026-07-11 22:17:47 +02:00
parent e54aaf76f9
commit c7d3b20ca0

View File

@ -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');