From 7bdc85163b5bbe3a976e25c645849525cdc9c891 Mon Sep 17 00:00:00 2001 From: Wojtek Majewski Date: Sun, 2 Nov 2025 21:10:59 +0100 Subject: [PATCH] chore: add comprehensive pgTAP testing guide, helper functions, and documentation updates Includes new SKILL.md with detailed testing patterns, helper functions for database management, flow setup, task operations, realtime testing, and timestamp manipulation. Updates schema-dev SKILL.md to reference the new testing resources for improved testing workflows. --- .claude/settings.json | 7 +- .claude/skills/pgtap-testing/SKILL.md | 282 +++++++++++++++++++++++ .claude/skills/pgtap-testing/helpers.md | 289 ++++++++++++++++++++++++ .claude/skills/schema-dev/SKILL.md | 2 + 4 files changed, 578 insertions(+), 2 deletions(-) create mode 100644 .claude/skills/pgtap-testing/SKILL.md create mode 100644 .claude/skills/pgtap-testing/helpers.md diff --git a/.claude/settings.json b/.claude/settings.json index cc7f71e02..f804281c5 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -46,10 +46,13 @@ "Read(./.notes/**)", "Write(./.notes/**)", "Edit(./.notes/**)", - "Skill(scratch-capture)", - "Skill(scratch-review)", "Skill(idea-refine)", + "Skill(migration-management)", "Skill(notes-sync)", + "Skill(pgtap-testing)", + "Skill(schema-dev)", + "Skill(scratch-capture)", + "Skill(scratch-review)", "Skill(unblock)", "AskUserQuestion", "WebSearch", diff --git a/.claude/skills/pgtap-testing/SKILL.md b/.claude/skills/pgtap-testing/SKILL.md new file mode 100644 index 000000000..04b4ab67d --- /dev/null +++ b/.claude/skills/pgtap-testing/SKILL.md @@ -0,0 +1,282 @@ +--- +name: pgtap-testing +description: Guide pgTAP test writing in pgflow. Use when user asks to write pgTAP test, add test for feature, test SQL function, or asks how to test database scenarios. Provides test patterns and helper functions. +--- + +# pgTAP Testing Guide + +**CRITICAL**: Tests live in `pkgs/core/supabase/tests/`, helpers in `pkgs/core/supabase/seed.sql` + +## Quick Reference + +**Test Structure:** +```sql +begin; +select plan(N); -- Declare number of tests +select pgflow_tests.reset_db(); -- Clean state +-- ... setup and assertions ... +select finish(); +rollback; +``` + +**Common Assertions:** +- `is(actual, expected, description)` - Equality check +- `results_eq(query1, query2, description)` - Compare query results +- `ok(boolean, description)` - Boolean check +- `throws_ok(query, description)` - Expect error + +**Running Tests:** +```bash +# Single test +./scripts/run-test-with-colors pkgs/core/supabase/tests/path/to/test.sql + +# All tests +pnpm nx test:pgtap core +``` + +## Test Structure + +All pgTAP tests follow this pattern: + +```sql +begin; -- Start transaction +select plan(N); -- Declare number of tests + +-- Setup phase +select pgflow_tests.reset_db(); +select pgflow_tests.setup_flow('sequential'); + +-- Test assertions +select is( + (select count(*) from pgflow.runs), + 1::bigint, + 'Should create one run' +); + +select finish(); -- Complete tests +rollback; -- Roll back transaction +``` + +**Key points:** +- Transaction ensures isolation (BEGIN...ROLLBACK) +- `plan(N)` must match exact number of assertions +- `reset_db()` cleans state between test runs +- All changes are rolled back + +## Common Patterns + +### Testing Single Functions + +Test a function's return value or side effects: + +```sql +begin; +select plan(2); +select pgflow_tests.reset_db(); +select pgflow_tests.setup_flow('sequential'); + +-- Execute function +select pgflow.start_flow('sequential', '"hello"'::jsonb); + +-- Test: Check created run +select results_eq( + $$ SELECT flow_slug, status from pgflow.runs $$, + $$ VALUES ('sequential', 'started') $$, + 'Run should be created with correct status' +); + +select finish(); +rollback; +``` + +### Testing Workflows (Setup → Execute → Assert) + +Test complete workflows with multiple steps: + +```sql +begin; +select plan(3); +select pgflow_tests.reset_db(); +select pgflow_tests.setup_flow('sequential'); + +-- Start flow +select pgflow.start_flow('sequential', '"hello"'::jsonb); + +-- Poll and start task +select pgflow_tests.read_and_start('sequential'); + +-- Complete task +select pgflow.complete_task( + (select run_id from pgflow.runs limit 1), + 'first', + 0, + '{"result": "done"}'::jsonb +); + +-- Test: Task completed +select is( + (select status from pgflow.step_tasks + where step_slug = 'first' limit 1), + 'completed', + 'Task should be completed' +); + +select finish(); +rollback; +``` + +### Testing Error Conditions + +Verify functions throw expected errors: + +```sql +begin; +select plan(1); +select pgflow_tests.reset_db(); + +-- Test: Invalid flow slug +select throws_ok( + $$ SELECT pgflow.start_flow('nonexistent', '{}') $$, + 'Flow not found: nonexistent' +); + +select finish(); +rollback; +``` + +### Testing Realtime Events + +Verify realtime notifications are sent: + +```sql +begin; +select plan(3); + +-- CRITICAL: Create partition before testing realtime +select pgflow_tests.create_realtime_partition(); + +select pgflow_tests.reset_db(); +select pgflow.create_flow('sequential'); +select pgflow.add_step('sequential', 'first'); + +-- Capture run_id in temporary table +with flow as ( + select * from pgflow.start_flow('sequential', '{}') +) +select run_id into temporary run_ids from flow; + +-- Test: Event was sent +select is( + pgflow_tests.count_realtime_events( + 'run:started', + (select run_id from run_ids) + ), + 1::int, + 'Should send run:started event' +); + +-- Test: Event payload is correct +select is( + (select payload->>'status' + from pgflow_tests.get_realtime_message( + 'run:started', + (select run_id from run_ids) + )), + 'started', + 'Event should have correct status' +); + +-- Cleanup +drop table if exists run_ids; + +select finish(); +rollback; +``` + +## Common Assertions + +**Equality checks:** +```sql +select is(actual, expected, 'description'); +``` + +**Query result comparison:** +```sql +select results_eq( + $$ SELECT col1, col2 FROM table1 $$, + $$ VALUES ('val1', 'val2') $$, + 'description' +); +``` + +**Boolean checks:** +```sql +select ok(boolean_expression, 'description'); +``` + +**Error handling:** +```sql +select throws_ok($$ SELECT function_call() $$, 'error message'); +select lives_ok($$ SELECT function_call() $$, 'should not error'); +``` + +**Pattern matching:** +```sql +select alike(actual, 'pattern%', 'description'); +``` + +## Helper Functions + +pgflow provides test helpers in `pgflow_tests` schema. See [helpers.md](helpers.md) for complete reference. + +**Most commonly used:** +- `reset_db()` - Clean all pgflow data and queues +- `setup_flow(slug)` - Create predefined test flow +- `read_and_start(flow_slug)` - Poll and start tasks +- `poll_and_complete(flow_slug)` - Poll and complete task +- `poll_and_fail(flow_slug)` - Poll and fail task +- `create_realtime_partition()` - Required for realtime tests +- `count_realtime_events(type, run_id)` - Count events sent +- `get_realtime_message(type, run_id)` - Get full event message + +## Test Organization + +**Directory structure:** +``` +pkgs/core/supabase/tests/ +├── start_flow/ # Tests for starting flows +├── complete_task/ # Tests for task completion +├── fail_task/ # Tests for task failures +├── realtime/ # Tests for realtime events +└── [feature]/ # Group related tests by feature +``` + +**Naming convention:** +- Test files: `descriptive_name.test.sql` +- One test file per specific behavior +- Group related tests in directories + +## Running Tests + +**Single test:** +```bash +./scripts/run-test-with-colors pkgs/core/supabase/tests/start_flow/creates_run.test.sql +``` + +**All tests:** +```bash +pnpm nx test:pgtap core +``` + +**Watch mode:** +```bash +pnpm nx test:pgtap:watch core +``` + +## Example Test Files + +See existing tests for patterns: +- `tests/start_flow/creates_run.test.sql` - Basic workflow +- `tests/complete_task/completes_task_and_updates_dependents.test.sql` - Complex workflow +- `tests/realtime/start_flow_events.test.sql` - Realtime testing +- `tests/type_violations/*.test.sql` - Error testing diff --git a/.claude/skills/pgtap-testing/helpers.md b/.claude/skills/pgtap-testing/helpers.md new file mode 100644 index 000000000..94bf4aab8 --- /dev/null +++ b/.claude/skills/pgtap-testing/helpers.md @@ -0,0 +1,289 @@ +# pgTAP Test Helpers Reference + +All helpers are in the `pgflow_tests` schema. Source: `pkgs/core/supabase/seed.sql` + +## Database Management + +### reset_db() + +Cleans all pgflow data and drops all pgmq queues. Use at the start of every test. + +```sql +select pgflow_tests.reset_db(); +``` + +Deletes from: +- `pgflow.step_tasks` +- `pgflow.step_states` +- `pgflow.runs` +- `pgflow.deps` +- `pgflow.steps` +- `pgflow.flows` +- `realtime.messages` +- Drops all pgmq queues + +### create_realtime_partition() + +Creates partition for `realtime.messages` table. **Required before testing realtime events.** + +```sql +select pgflow_tests.create_realtime_partition(); +``` + +**Why needed:** `realtime.send()` silently fails without proper partition. Always call this before realtime tests. + +## Flow Setup + +### setup_flow(flow_slug) + +Creates predefined test flows with common patterns. + +```sql +select pgflow_tests.setup_flow('sequential'); +``` + +**Available flows:** +- `'sequential'` - Three steps: first → second → last +- `'two_roots'` - Two root steps merging into one: root_a, root_b → last +- `'two_roots_left_right'` - Root with diverging paths +- `'sequential_other'` - Same as sequential but named 'other' + +### ensure_worker(queue_name, worker_uuid, function_name) + +Creates or updates a test worker. Usually called internally by `read_and_start()`. + +```sql +select pgflow_tests.ensure_worker( + queue_name => 'my_flow', + worker_uuid => '11111111-1111-1111-1111-111111111111'::uuid, + function_name => 'test_worker' +); +``` + +**Default values:** +- `worker_uuid`: `'11111111-1111-1111-1111-111111111111'::uuid` +- `function_name`: `'test_worker'` + +## Task Operations + +### read_and_start(flow_slug, vt, qty, worker_uuid, function_name) + +Polls messages from queue and starts tasks in one operation. + +```sql +-- Start one task with default settings +select pgflow_tests.read_and_start('sequential'); + +-- Start multiple tasks +select pgflow_tests.read_and_start('sequential', vt => 1, qty => 3); +``` + +**Parameters:** +- `flow_slug` - Queue name (required) +- `vt` - Visibility timeout in seconds (default: 1) +- `qty` - Number of tasks to start (default: 1) +- `worker_uuid` - Worker ID (default: test UUID) +- `function_name` - Worker function name (default: 'test_worker') + +**Returns:** `setof pgflow.step_task_record` + +### poll_and_complete(flow_slug, vt, qty) + +Polls for a task and immediately completes it. Useful for testing downstream effects. + +```sql +select pgflow_tests.poll_and_complete('sequential'); +``` + +**Returns:** `setof pgflow.step_tasks` + +### poll_and_fail(flow_slug, vt, qty) + +Polls for a task and immediately fails it. Useful for testing error handling. + +```sql +select pgflow_tests.poll_and_fail('sequential'); +``` + +**Returns:** `setof pgflow.step_tasks` + +## Realtime Testing + +### count_realtime_events(event_type, run_id, step_slug) + +Counts realtime events matching criteria. + +```sql +-- Count run-level events +select pgflow_tests.count_realtime_events( + 'run:started', + run_id +); + +-- Count step-level events +select pgflow_tests.count_realtime_events( + 'step:completed', + run_id, + 'first' +); +``` + +**Returns:** `integer` + +### get_realtime_message(event_type, run_id, step_slug) + +Returns full realtime message record including topic and event fields. + +```sql +-- Get run event message +select pgflow_tests.get_realtime_message( + 'run:started', + run_id +); + +-- Get step event message +select pgflow_tests.get_realtime_message( + 'step:completed', + run_id, + 'first' +); +``` + +**Returns:** `realtime.messages` (includes id, inserted_at, event, topic, payload) + +**Common usage:** +```sql +-- Extract payload field +select payload->>'status' +from pgflow_tests.get_realtime_message('run:started', run_id); + +-- Check topic +select topic +from pgflow_tests.get_realtime_message('run:started', run_id); +``` + +### find_realtime_event(event_type, run_id, step_slug) + +Returns just the JSONB payload of a matching event. + +```sql +-- Find run event payload +select pgflow_tests.find_realtime_event( + 'run:started', + run_id +); +``` + +**Returns:** `jsonb` (payload only) + +## Timing Helpers + +### message_timing(step_slug, queue_name) + +Returns message timing information including visibility timeout in seconds. + +```sql +select * from pgflow_tests.message_timing('first', 'sequential'); +``` + +**Returns:** Table with columns: +- `msg_id` - Message ID +- `read_ct` - Read count +- `enqueued_at` - When message was queued +- `vt` - Visibility timeout timestamp +- `message` - Message JSONB +- `vt_seconds` - Delay in seconds (calculated) + +### reset_message_visibility(queue_name) + +Makes all hidden messages immediately visible for testing retry logic without waiting. + +```sql +select pgflow_tests.reset_message_visibility('sequential'); +``` + +**Returns:** `integer` (number of messages made visible) + +### assert_retry_delay(queue_name, step_slug, expected_delay, description) + +Asserts that retry delay matches expected value. + +```sql +select pgflow_tests.assert_retry_delay( + 'sequential', + 'first', + 5, + 'First retry should have 5 second delay' +); +``` + +**Returns:** `text` (pgTAP assertion result) + +## Timestamp Manipulation + +These helpers set timestamps for completed/failed/running flows to simulate age. + +### set_completed_flow_timestamps(flow_slug, days_old) + +Sets timestamps to make completed run appear N days old. + +```sql +select pgflow_tests.set_completed_flow_timestamps('sequential', 30); +``` + +### set_failed_flow_timestamps(flow_slug, days_old) + +Sets timestamps to make failed run appear N days old. + +```sql +select pgflow_tests.set_failed_flow_timestamps('sequential', 30); +``` + +### set_running_flow_timestamps(flow_slug, days_old) + +Sets timestamps to make running flow appear N days old. + +```sql +select pgflow_tests.set_running_flow_timestamps('sequential', 30); +``` + +**Use case:** Testing pruning/cleanup logic that operates on old data. + +## Realtime Assertion Helpers + +### assert_realtime_event_sent(event_type, description) + +Asserts at least one event of given type was sent. + +```sql +select pgflow_tests.assert_realtime_event_sent( + 'run:started', + 'Should send run:started event' +); +``` + +### assert_step_event_sent(event_type, step_slug, description) + +Asserts event was sent for specific step. + +```sql +select pgflow_tests.assert_step_event_sent( + 'step:completed', + 'first', + 'Should send step:completed for first step' +); +``` + +### assert_run_event_sent(event_type, flow_slug, description) + +Asserts event was sent for specific flow. + +```sql +select pgflow_tests.assert_run_event_sent( + 'run:started', + 'sequential', + 'Should send run:started for sequential flow' +); +``` + +**Note:** These helpers query `pgflow_tests.realtime_calls` table (mock mode). For testing actual realtime.messages, use `count_realtime_events()` and `get_realtime_message()` instead. diff --git a/.claude/skills/schema-dev/SKILL.md b/.claude/skills/schema-dev/SKILL.md index 7f79c3fb3..b80e0c183 100644 --- a/.claude/skills/schema-dev/SKILL.md +++ b/.claude/skills/schema-dev/SKILL.md @@ -69,6 +69,8 @@ SELECT * FROM finish(); ROLLBACK; ``` +**For test patterns and helpers**, see pgtap-testing skill. + ### Step 2: Run Test (Should Fail) ```bash