E2E Test Data Setup Runbook
E2E Test Data Setup Runbook
Section titled “E2E Test Data Setup Runbook”Practical guide for creating test data to exercise Flows 4-7 (bid creation, response drafting, export, and template completion) without requiring real tender documents or AI API calls.
Key insight: Flows 6 (Export) and 7 (Template Completion) are fully deterministic. By inserting responses directly via SQL, you can test export and template completion with zero AI API calls.
1. Prerequisites
Section titled “1. Prerequisites”| Requirement | How to verify |
|---|---|
| Dev server running | bun dev — visit http://localhost:3000 |
| Python worker (for template ops only) | PYTHONUNBUFFERED=1 python3 scripts/bid_worker.py |
| Supabase connection | .env.local has NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY |
| Authenticated session | Logged in at http://localhost:3000/login as admin or editor |
| Browser cookies | All API calls below require the Supabase auth cookie from the browser |
For API calls from the command line, you need an active session token. The
easiest approach is to use the browser’s fetch() from the DevTools console, or
use a tool like curl with the Supabase auth headers.
Environment variables
Section titled “Environment variables”NEXT_PUBLIC_SUPABASE_URL=https://rovrymhhffssilaftdwd.supabase.coNEXT_PUBLIC_SUPABASE_ANON_KEY=<your-anon-key>ANTHROPIC_API_KEY=<only-needed-for-Flow-5-AI-drafting>2. Test Data Strategy
Section titled “2. Test Data Strategy”Naming convention
Section titled “Naming convention”All E2E test data uses a distinctive naming convention so it can be easily identified and cleaned up:
| Entity | Convention | Example |
|---|---|---|
| Bid name | [E2E Test] prefix | [E2E Test] IT Support Services |
| Buyer | E2E Test Corp | E2E Test Corp |
| Template name | [E2E Test] prefix | [E2E Test] Response Template |
Differentiation from client data
Section titled “Differentiation from client data”- Test data lives alongside real data in the same Supabase project
- The
[E2E Test]prefix makes it visually distinct in the bid list - Cleanup queries (see Section 8) filter on this prefix
3. Creating a Test Bid
Section titled “3. Creating a Test Bid”Step 3a: Create the bid
Section titled “Step 3a: Create the bid”// Browser DevTools console (or equivalent HTTP client)const res = await fetch('/api/bids', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: '[E2E Test] IT Support Services', buyer: 'E2E Test Corp', deadline: new Date(Date.now() + 14 * 86400000).toISOString().split('T')[0], description: 'E2E test bid for automated testing', }),});const bid = await res.json();const BID_ID = bid.id;console.log('Bid ID:', BID_ID);Expected response: HTTP 201 with the bid object. Status will be draft.
Step 3b: Add questions (batch)
Section titled “Step 3b: Add questions (batch)”const questionsRes = await fetch(`/api/bids/${BID_ID}/questions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ questions: [ { section_name: 'Technical', section_sequence: 1, question_sequence: 1, question_text: 'Describe your approach to providing IT support services including response times and escalation procedures.', word_limit: 500, category: 'technical', }, { section_name: 'Technical', section_sequence: 1, question_sequence: 2, question_text: 'What experience does your organisation have delivering similar IT support contracts in the public sector?', word_limit: 400, category: 'experience', }, { section_name: 'Social Value', section_sequence: 2, question_sequence: 1, question_text: 'Describe how you will deliver social value through this contract, including apprenticeships and local employment.', word_limit: 300, category: 'social_value', }, { section_name: 'Commercial', section_sequence: 3, question_sequence: 1, question_text: 'Provide a breakdown of your pricing model including any assumptions or exclusions.', word_limit: 600, category: 'commercial', }, ], }),});const { questions, count } = await questionsRes.json();console.log(`Created ${count} questions`);// Save question IDs for later useconst QUESTION_IDS = questions.map((q) => q.id);console.log('Question IDs:', QUESTION_IDS);Expected response: HTTP 201 with { questions: [...], count: 4 }.
Step 3c: Advance bid state to drafting
Section titled “Step 3c: Advance bid state to drafting”The bid state machine requires sequential transitions. Advance through the required states:
// draft -> questions_extractedawait fetch(`/api/bids/${BID_ID}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'questions_extracted' }),});
// questions_extracted -> matchingawait fetch(`/api/bids/${BID_ID}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'matching' }),});
// matching -> draftingawait fetch(`/api/bids/${BID_ID}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'drafting' }),});
console.log('Bid advanced to drafting');Each PATCH returns the updated bid object. The state machine enforces valid transitions:
draft -> questions_extracted -> matching -> drafting -> in_review -> ready_for_export -> submitted4. Creating Responses WITHOUT AI
Section titled “4. Creating Responses WITHOUT AI”This is the key technique for testing Flows 6 and 7 without any AI API calls. Instead of calling the drafting endpoint (which invokes Claude), insert responses directly via SQL.
Option A: Direct SQL (via Supabase MCP or Dashboard)
Section titled “Option A: Direct SQL (via Supabase MCP or Dashboard)”-- Insert test responses for each question-- Replace the UUIDs with your actual BID_ID and QUESTION_IDS
INSERT INTO bid_responses (question_id, response_text, review_status, drafted_by, metadata)VALUES ( '<QUESTION_ID_1>', '<p>Our IT support approach provides a comprehensive three-tier response structure. <strong>Priority 1 (Critical)</strong> issues affecting business operations receive a 15-minute response time with continuous work until resolution. <strong>Priority 2 (High)</strong> issues receive a 1-hour response with resolution within 4 hours. <strong>Priority 3 (Standard)</strong> requests are acknowledged within 4 hours and resolved within 2 business days.</p><p>Our escalation procedure follows a clear chain: Service Desk Analyst to Technical Lead (30 minutes), Technical Lead to Service Delivery Manager (1 hour), and Service Delivery Manager to Account Director (2 hours).</p>', 'approved', '<YOUR_USER_UUID>', '{"quality_data": {"word_count": 95, "overall_score": 85, "word_limit_compliance": true}}'::jsonb ), ( '<QUESTION_ID_2>', '<p>We have delivered IT support contracts across the public sector for over 10 years. Key examples include a 5-year contract with NHS Digital providing service desk and desktop support to 3,000 users, and a 3-year contract with the Department for Education covering infrastructure monitoring and incident management.</p><p>These contracts have consistently achieved 99.5% SLA compliance and maintained customer satisfaction scores above 4.5/5.</p>', 'approved', '<YOUR_USER_UUID>', '{"quality_data": {"word_count": 75, "overall_score": 80, "word_limit_compliance": true}}'::jsonb ), ( '<QUESTION_ID_3>', '<p>Social value is integral to our delivery model. We commit to creating a minimum of 2 apprenticeship positions per year of contract operation, recruiting from the local area within a 30-mile radius of the delivery site.</p><p>We will partner with local colleges to provide work experience placements and contribute 40 hours of pro-bono IT support to local charities annually.</p>', 'approved', '<YOUR_USER_UUID>', '{"quality_data": {"word_count": 65, "overall_score": 78, "word_limit_compliance": true}}'::jsonb ), ( '<QUESTION_ID_4>', '<p>Our pricing model is based on a fixed monthly service charge covering all BAU support activities. The monthly charge includes: Service Desk (8am-6pm weekdays), on-site engineering (2 FTE), infrastructure monitoring (24/7), and quarterly service reviews.</p><p><strong>Assumptions:</strong> Maximum 500 supported users, standard desktop/laptop estate, existing network infrastructure in place. <strong>Exclusions:</strong> Hardware procurement, project-based work, and out-of-hours support (available as an add-on).</p>', 'approved', '<YOUR_USER_UUID>', '{"quality_data": {"word_count": 85, "overall_score": 82, "word_limit_compliance": true}}'::jsonb );Option B: Via API (if you want responses visible in the response editor)
Section titled “Option B: Via API (if you want responses visible in the response editor)”If you prefer to create responses via the API, you can use the response update
endpoint after creating empty records. However, the drafting endpoints require
ANTHROPIC_API_KEY. The SQL approach above is simpler for testing.
Step 4b: Approve responses via API
Section titled “Step 4b: Approve responses via API”If you inserted responses with review_status: 'approved' in the SQL above,
this step is already done. If you used a different status, approve each
response:
// Fetch responses to get their IDsconst respRes = await fetch(`/api/bids/${BID_ID}/questions`);const { questions: qs } = await respRes.json();
for (const q of qs) { if (q.response?.id) { await fetch(`/api/bids/${BID_ID}/responses/${q.response.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ review_status: 'approved' }), }); }}console.log('All responses approved');Step 4c: Advance to ready_for_export
Section titled “Step 4c: Advance to ready_for_export”// drafting -> in_reviewawait fetch(`/api/bids/${BID_ID}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'in_review' }),});
// in_review -> ready_for_exportawait fetch(`/api/bids/${BID_ID}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'ready_for_export' }),});
console.log('Bid ready for export');5. Running Flow 6: Export
Section titled “5. Running Flow 6: Export”With the bid in ready_for_export state and approved responses in place, export
is fully deterministic — no AI calls needed.
Export as DOCX
Section titled “Export as DOCX”const docxRes = await fetch(`/api/bids/${BID_ID}/export/docx`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),});
// Download the fileconst blob = await docxRes.blob();const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = 'test-export.docx';a.click();Verification checklist:
- File downloads without errors
- Document contains bid name as title
- All 4 questions are present with section headings
- All 4 approved responses are rendered
- Word count metadata is present
- Formatting (bold, paragraphs) is preserved from HTML
Export as XLSX
Section titled “Export as XLSX”const xlsxRes = await fetch(`/api/bids/${BID_ID}/export/xlsx`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),});
const blob = await xlsxRes.blob();const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = 'test-export.xlsx';a.click();Verification checklist:
- File downloads without errors
- Spreadsheet has headers: Section, Question, Word Limit, Response, Status
- All 4 questions and responses are present
- Data is correctly aligned in columns
Export options
Section titled “Export options”Both export endpoints accept optional configuration:
// DOCX with all optionsawait fetch(`/api/bids/${BID_ID}/export/docx`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ include_cover: true, include_toc: true, include_citations: false, include_unanswered: false, company_name: 'E2E Test Corp', }),});6. Running Flow 7: Template Completion
Section titled “6. Running Flow 7: Template Completion”Template completion requires the Python worker for analysis and filling steps. Ensure it is running before starting.
Step 6a: Upload a template
Section titled “Step 6a: Upload a template”Use the test fixture at __tests__/fixtures/simple-template.docx:
const formData = new FormData();// In a browser, use a file input or fetch the fixtureconst fileInput = document.querySelector('input[type="file"]');formData.append('file', fileInput.files[0]);formData.append('name', '[E2E Test] Response Template');
const uploadRes = await fetch(`/api/bids/${BID_ID}/templates`, { method: 'POST', body: formData, // No Content-Type header -- browser sets multipart boundary});const template = await uploadRes.json();const TEMPLATE_ID = template.id;console.log('Template ID:', TEMPLATE_ID);Expected response: HTTP 201 with template object, status: 'uploaded'.
Step 6b: Trigger analysis
Section titled “Step 6b: Trigger analysis”const analyseRes = await fetch( `/api/bids/${BID_ID}/templates/${TEMPLATE_ID}/analyse`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), },);const analyseJob = await analyseRes.json();console.log('Analysis job:', analyseJob);Expected response: HTTP 202 with { job_id, status: 'queued' }.
The Python worker (bid_worker.py) picks up the job from processing_queue,
downloads the template from Supabase Storage, identifies table cells that
contain questions, and writes the results to template_fields.
Wait for completion: Poll the template status or watch the worker logs. Analysis typically takes 5-15 seconds.
// Poll until status changes from 'analysing'const poll = async () => { const r = await fetch(`/api/bids/${BID_ID}/templates`); const { templates } = await r.json(); const t = templates.find((t) => t.id === TEMPLATE_ID); console.log('Template status:', t?.status, 'Fields:', t?.field_count); return t?.status;};// Call poll() every few seconds until status is 'analysed'Step 6c: Auto-map fields to questions
Section titled “Step 6c: Auto-map fields to questions”const mapRes = await fetch( `/api/bids/${BID_ID}/templates/${TEMPLATE_ID}/auto-map`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threshold: 0.5 }), // Lower threshold for test data },);const mapResult = await mapRes.json();console.log(`Mapped ${mapResult.mapped}/${mapResult.total} fields`);Expected response: { mapped: N, unmapped: M, total: T, mappings: [...] }.
Auto-mapping uses Dice coefficient text similarity (no AI). It matches template field question text against bid question text.
Step 6d: Confirm mappings
Section titled “Step 6d: Confirm mappings”After auto-mapping, fields have mapping_status: 'unreviewed'. Confirm them so
they are included in the fill:
// Fetch the mappings from auto-map resultconst confirmRes = await fetch( `/api/bids/${BID_ID}/templates/${TEMPLATE_ID}/fields/bulk-update`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mappings: mapResult.mappings.map((m) => ({ field_id: m.field_id, question_id: m.question_id, mapping_status: 'confirmed', })), }), },);const confirmResult = await confirmRes.json();console.log(`Confirmed ${confirmResult.updated} mappings`);Step 6e: Fill the template
Section titled “Step 6e: Fill the template”const fillRes = await fetch( `/api/bids/${BID_ID}/templates/${TEMPLATE_ID}/fill`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ skip_unmapped: true, skip_unapproved: false, fallback_to_draft: true, response_variant: 'standard', }), },);const fillJob = await fillRes.json();console.log('Fill job:', fillJob);Expected response: HTTP 202 with
{ job_id, status: 'queued', fields_to_fill: N }.
Wait for completion: The Python worker fills the template by writing response text into the identified table cells, preserving the original formatting. This typically takes 10-30 seconds.
// After fill completes, fetch completionsconst completionsRes = await fetch(`/api/bids/${BID_ID}/templates`);const { templates } = await completionsRes.json();const filled = templates.find((t) => t.id === TEMPLATE_ID);console.log('Template status:', filled?.status);console.log('Completions count:', filled?.completions_count);Step 6f: Download the completed document
Section titled “Step 6f: Download the completed document”// Get the completion ID from template_completions// (The fill job creates a completion record with the filled document)// You can query this via the templates list or directly:
// First, find the latest completion for this templateconst tplRes = await fetch(`/api/bids/${BID_ID}/templates`);const { templates: tpls } = await tplRes.json();const tpl = tpls.find((t) => t.id === TEMPLATE_ID);
// If completions_count > 0, get the download URL// The completion ID is available from the template_completions table// For now, use the UI at /bid/{BID_ID}/templates to downloadVerification checklist:
- Template analysis identifies the correct number of table cells
- Auto-map matches fields to the right questions
- Fill completes without errors
- Downloaded document has responses in the correct cells
- Original formatting (fonts, colours, borders) is preserved
- Word limits are respected (truncation warnings if exceeded)
7. AI Boundary Analysis
Section titled “7. AI Boundary Analysis”Understanding where AI is and is not involved helps determine what can be tested without API keys.
| Operation | AI involved? | Details |
|---|---|---|
| Bid creation | No | Simple CRUD |
| Question extraction from tender doc | Yes | Claude extracts questions from uploaded documents |
| Adding questions manually | No | Direct database insert |
| KB matching | Yes | Uses embeddings for semantic search |
| Response drafting | Yes | lib/bid-drafting.ts calls Claude |
| Response editing/approval | No | Direct database update |
| Export DOCX/XLSX | No | Deterministic document generation |
| Template upload | No | File storage |
| Template analysis | No | Python analyse_template.py — regex/heuristic |
| Template auto-map | No | Dice coefficient text similarity |
| Template fill | No | Python fill_template.py — python-docx cell writes |
Summary: To test the full export and template completion flows, you only need to bypass two AI-dependent steps (question extraction and response drafting) by inserting data directly via SQL.
8. Cleanup
Section titled “8. Cleanup”Delete test templates (cleans storage)
Section titled “Delete test templates (cleans storage)”// Delete via the API (cascades to template_fields, template_completions)await fetch(`/api/bids/${BID_ID}/templates/${TEMPLATE_ID}`, { method: 'DELETE',});Delete test bid (cascades to questions and responses)
Section titled “Delete test bid (cascades to questions and responses)”// Requires admin roleawait fetch(`/api/bids/${BID_ID}`, { method: 'DELETE',});The DELETE /api/bids/:id endpoint deletes the project record. Due to cascading
foreign keys, this also removes:
bid_questions(viaproject_id)bid_responses(viaquestion_id->bid_questions)templates(viaproject_id)template_fields(viatemplate_id->templates)template_completions(viatemplate_id->templates)
Find remaining test data
Section titled “Find remaining test data”-- Find any test bids that were not cleaned upSELECT id, name, domain_metadata->>'status' as status, created_atFROM projectsWHERE type = 'bid' AND name LIKE '[E2E Test]%'ORDER BY created_at DESC;
-- Find orphaned test responses (if questions were deleted but responses remain)SELECT r.id, r.question_id, r.review_statusFROM bid_responses rLEFT JOIN bid_questions q ON r.question_id = q.idWHERE q.id IS NULL;
-- Find test templatesSELECT id, name, project_id, statusFROM templatesWHERE name LIKE '[E2E Test]%';Storage cleanup
Section titled “Storage cleanup”Template files are stored in the templates Supabase Storage bucket. Deleting
the template record via the API also removes the storage files. If storage files
are orphaned, clean them up manually:
-- List storage objects for a specific bidSELECT name, created_atFROM storage.objectsWHERE bucket_id = 'templates' AND name LIKE '<BID_ID>/%';9. Complete Script: End-to-End Happy Path
Section titled “9. Complete Script: End-to-End Happy Path”This script runs all the steps above in sequence. Paste it into the browser DevTools console while logged in as an admin/editor.
// ============================================// E2E Test: Full bid -> export happy path// ============================================// Prerequisites: dev server running, logged in
(async () => { const BASE = ''; // relative URLs work in DevTools
// 1. Create bid console.log('Step 1: Creating bid...'); const bidRes = await fetch(`${BASE}/api/bids`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: `[E2E Test] IT Support ${Date.now()}`, buyer: 'E2E Test Corp', deadline: new Date(Date.now() + 14 * 86400000) .toISOString() .split('T')[0], }), }); if (!bidRes.ok) throw new Error(`Create bid failed: ${bidRes.status}`); const bid = await bidRes.json(); console.log(' Bid ID:', bid.id);
// 2. Add questions console.log('Step 2: Adding questions...'); const qRes = await fetch(`${BASE}/api/bids/${bid.id}/questions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ questions: [ { section_name: 'Technical', section_sequence: 1, question_sequence: 1, question_text: 'Describe your IT support approach and response times.', word_limit: 500, }, { section_name: 'Experience', section_sequence: 2, question_sequence: 1, question_text: 'What public sector IT support experience do you have?', word_limit: 400, }, { section_name: 'Social Value', section_sequence: 3, question_sequence: 1, question_text: 'How will you deliver social value through this contract?', word_limit: 300, }, { section_name: 'Commercial', section_sequence: 4, question_sequence: 1, question_text: 'Provide your pricing model breakdown.', word_limit: 600, }, ], }), }); if (!qRes.ok) throw new Error(`Add questions failed: ${qRes.status}`); const { questions } = await qRes.json(); console.log(` Created ${questions.length} questions`);
// 3. Advance state: draft -> questions_extracted -> matching -> drafting console.log('Step 3: Advancing bid state...'); for (const status of ['questions_extracted', 'matching', 'drafting']) { const r = await fetch(`${BASE}/api/bids/${bid.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }), }); if (!r.ok) throw new Error(`State transition to ${status} failed: ${r.status}`); } console.log(' Bid now in drafting state');
// 4. NOTE: Response insertion requires direct SQL or Supabase MCP // See Section 4 of e2e-test-setup.md for the INSERT statements. // After inserting responses, continue with steps 5-6 below. console.log('Step 4: Insert responses via SQL (see runbook Section 4)'); console.log(' Question IDs for SQL:'); questions.forEach((q, i) => console.log(` Q${i + 1}: ${q.id}`));
// 5. After responses are inserted, advance to ready_for_export // Uncomment the following once responses exist: /* for (const status of ['in_review', 'ready_for_export']) { await fetch(`${BASE}/api/bids/${bid.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); } console.log(' Bid ready for export');
// 6. Export DOCX console.log('Step 6: Exporting DOCX...'); const docxRes = await fetch(`${BASE}/api/bids/${bid.id}/export/docx`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); if (!docxRes.ok) throw new Error(`DOCX export failed: ${docxRes.status}`); const blob = await docxRes.blob(); console.log(` DOCX generated: ${(blob.size / 1024).toFixed(1)} KB`);
// Download it const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'e2e-test-export.docx'; a.click(); URL.revokeObjectURL(url); console.log(' Download triggered'); */
console.log('\nDone! Bid ID for cleanup:', bid.id);})();10. Future: Automated E2E with Playwright
Section titled “10. Future: Automated E2E with Playwright”When automated E2E tests are implemented, the test data setup described here will serve as the seed script. Key considerations:
- Supabase service role client can insert responses directly without auth cookies, bypassing RLS
- Playwright fixtures can wrap the create/cleanup cycle in
beforeAll/afterAll - AI mocking: The only AI boundary is
lib/bid-drafting.ts(generateBidResponse). Mocking this function with static responses enables fully deterministic E2E tests including the drafting flow - Template analysis and filling are already deterministic (Python worker uses regex/heuristics, not AI)
- Export generation is already deterministic (docx/xlsx library calls)
11. Test User Provisioning (rebuild flow)
Section titled “11. Test User Provisioning (rebuild flow)”E2E tests require three test users in auth.users and public.user_roles
before the suite runs. e2e/global-setup.ts hard-fails if any are missing or
have the wrong role.
Required env vars
Section titled “Required env vars”TEST_USER_1_PASSWORD=<password for admin>TEST_USER_2_PASSWORD=<password for editor>TEST_USER_3_PASSWORD=<password for viewer>These are already set in .env/.env.local for the live project. For new demo
or Phew DB rebuilds, generate fresh passwords and add them to the new project’s
secrets store.
Provisioning command
Section titled “Provisioning command”bun run seed:e2e-users # provision or verify all 3 usersbun run seed:e2e-users --check # verify-only, exit 1 on mismatchbun run seed:e2e-users --dry-run # preview without writingThe script uses supabase.auth.admin.createUser() (NOT raw SQL) so the
resulting auth.users rows have the correct GoTrue shape — token columns
initialised to '', accompanying auth.identities rows. This is the same
gotcha that caused the S156 incident with the pipeline service account.
When to run it
Section titled “When to run it”- After
supabase db reset && supabase db pushon a fresh project - Before the first
bun run test:e2eon any new environment - As part of the demo DB / Phew re-ingest rebuild runbook
- Whenever
bun run test:e2efails with “test users not found in auth.users”
The script is idempotent — running it on an environment where users already exist is a no-op aside from re-asserting the role assignments.
Manual fallback
Section titled “Manual fallback”If seed:e2e-users cannot run (e.g. service key not available), the operator
can create the three users manually via the Supabase dashboard:
- Authentication → Users → Add user (NOT raw SQL — use the UI)
- Set the email + a password matching
TEST_USER_N_PASSWORD - After all three exist, run this SQL to assign roles:
INSERT INTO public.user_roles (user_id, role)SELECT id, CASEWHEN email = 'test.user1@test-kb-aish.co.uk' THEN 'admin'WHEN email = 'test.user2@test-kb-aish.co.uk' THEN 'editor'WHEN email = 'test.user3@test-kb-aish.co.uk' THEN 'viewer'ENDFROM auth.usersWHERE email IN ('test.user1@test-kb-aish.co.uk','test.user2@test-kb-aish.co.uk','test.user3@test-kb-aish.co.uk')ON CONFLICT (user_id) DO UPDATE SET role = EXCLUDED.role;
Generated: Session 46, 5 March 2026 Updated: Session 156, 8 April 2026 — added §11 test user provisioning