Skip to content

Commit b23f159

Browse files
committed
chore: failfast client tests if connectivity issues
1 parent 0adc276 commit b23f159

File tree

13 files changed

+393
-768
lines changed

13 files changed

+393
-768
lines changed

.claude/settings.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@
3333
"Bash(tree:*)",
3434
"Bash(vim:*)",
3535
"Bash(test:*)",
36-
"Bash(rm:./.notes/scratch/*)",
37-
"Bash(mv:./.notes/*)",
38-
"Bash(cat:./.notes/*)",
36+
"Bash(rm ./.notes/scratch/:*)",
37+
"Bash(mv ./.notes/:*)",
38+
"Bash(cat ./.notes/:*)",
3939
"Bash(./.claude/skills/_shared/notes/search-notes.sh:*)",
4040
"Bash(./.claude/skills/_shared/notes/list-titles.sh:*)",
4141
"Bash(./.claude/skills/_shared/notes/list-topics.sh:*)",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"eslint-config-prettier": "10.1.5",
3939
"jiti": "2.4.2",
4040
"jsdom": "~22.1.0",
41+
"jsonc-eslint-parser": "^2.4.1",
4142
"jsr": "^0.13.4",
4243
"netlify-cli": "^22.1.3",
4344
"nx": "21.2.1",

pkgs/client/__tests__/SupabaseBroadcastAdapter.simple.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('SupabaseBroadcastAdapter - Simple Tests', () => {
3737
*/
3838
test('subscribes to a run and configures channel correctly', async () => {
3939
const { client, mocks } = createMockClient();
40-
const adapter = new SupabaseBroadcastAdapter(client);
40+
const adapter = new SupabaseBroadcastAdapter(client, { stabilizationDelayMs: 0 });
4141

4242
// Setup realistic channel subscription
4343
mockChannelSubscription(mocks);
@@ -71,7 +71,7 @@ describe('SupabaseBroadcastAdapter - Simple Tests', () => {
7171
*/
7272
test('properly routes events to registered callbacks', () => {
7373
const { client, mocks } = createMockClient();
74-
const adapter = new SupabaseBroadcastAdapter(client);
74+
const adapter = new SupabaseBroadcastAdapter(client, { stabilizationDelayMs: 0 });
7575

7676
// Set up event listeners
7777
const runSpy = vi.fn();
@@ -112,7 +112,7 @@ describe('SupabaseBroadcastAdapter - Simple Tests', () => {
112112
error: null,
113113
});
114114

115-
const adapter = new SupabaseBroadcastAdapter(client);
115+
const adapter = new SupabaseBroadcastAdapter(client, { stabilizationDelayMs: 0 });
116116

117117
// Call method directly
118118
const result = await adapter.getRunWithStates(RUN_ID);
@@ -135,7 +135,7 @@ describe('SupabaseBroadcastAdapter - Simple Tests', () => {
135135
*/
136136
test('properly cleans up on unsubscribe', async () => {
137137
const { client, mocks } = createMockClient();
138-
const adapter = new SupabaseBroadcastAdapter(client);
138+
const adapter = new SupabaseBroadcastAdapter(client, { stabilizationDelayMs: 0 });
139139

140140
// Setup realistic channel subscription
141141
mockChannelSubscription(mocks);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
describe('Dummy', () => {
4+
it('passes', () => {
5+
expect(true).toBe(true);
6+
});
7+
});

pkgs/client/project.json

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -140,24 +140,13 @@
140140
"db:ensure": {
141141
"executor": "nx:run-commands",
142142
"local": true,
143+
"cache": false,
143144
"dependsOn": ["supabase:prepare"],
144145
"options": {
145146
"cwd": "{projectRoot}",
146147
"commands": ["./scripts/ensure-db"],
147148
"parallel": false
148-
},
149-
"inputs": [
150-
"{projectRoot}/scripts/ensure-db",
151-
"{workspaceRoot}/pkgs/core/supabase/migrations/**/*.sql",
152-
"{workspaceRoot}/pkgs/core/supabase/seed.sql",
153-
"{projectRoot}/supabase/config.toml",
154-
"{projectRoot}/tests/helpers/db.ts",
155-
"{projectRoot}/tests/helpers/permissions.ts"
156-
],
157-
"outputs": [
158-
"{projectRoot}/.nx-inputs/db-ready.txt"
159-
],
160-
"cache": true
149+
}
161150
},
162151
"test:integration": {
163152
"executor": "nx:run-commands",
@@ -214,7 +203,7 @@
214203
"inputs": ["default", "^production"],
215204
"options": {
216205
"cwd": "{projectRoot}",
217-
"command": "pnpm vitest --typecheck.only --run"
206+
"command": "pnpm vitest --typecheck.only --run --config vitest.typecheck.config.ts"
218207
}
219208
},
220209
"test:types:strict": {

pkgs/client/scripts/ensure-db

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ echo "Ensuring test database is ready..."
1111

1212
# Reset database (migrations should already be prepared by supabase:prepare target)
1313
echo "Resetting database..."
14-
supabase db reset
14+
pnpm exec supabase db reset
1515

1616
# One-time setup for integration tests
1717
echo "Setting up test database for integration tests..."

pkgs/client/test-stability.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/bin/bash
2+
3+
# Script to test channel stabilization delay reliability
4+
# Usage: ./test-stability.sh <number_of_iterations> [delay_ms]
5+
6+
iterations=${1:-10}
7+
delay_ms=${2:-"current"}
8+
9+
if [ "$delay_ms" != "current" ]; then
10+
echo "Note: To change delay, you need to manually edit vitest.global-setup.ts"
11+
echo "This script will test with the current delay setting"
12+
fi
13+
14+
echo "Testing channel stabilization with $iterations iterations"
15+
echo "Current delay: Check vitest.global-setup.ts for actual value"
16+
echo "================================================"
17+
18+
success_count=0
19+
fail_count=0
20+
21+
for i in $(seq 1 $iterations); do
22+
echo -n "Run $i/$iterations: "
23+
24+
# Run test and capture output
25+
output=$(pnpm vitest run __tests__/dummy.test.ts 2>&1)
26+
27+
# Check if test passed
28+
if echo "$output" | grep -q "Test Files 1 passed"; then
29+
echo "✓ PASS"
30+
success_count=$((success_count + 1))
31+
else
32+
echo "✗ FAIL"
33+
fail_count=$((fail_count + 1))
34+
35+
# Show error if failed
36+
if echo "$output" | grep -q "Supabase check failed"; then
37+
error_msg=$(echo "$output" | grep "Supabase check failed" | head -1)
38+
echo " Error: $error_msg"
39+
fi
40+
fi
41+
done
42+
43+
echo "================================================"
44+
echo "Results:"
45+
echo " Success: $success_count/$iterations ($(( success_count * 100 / iterations ))%)"
46+
echo " Failed: $fail_count/$iterations ($(( fail_count * 100 / iterations ))%)"
47+
48+
# Exit with non-zero if any failures
49+
if [ $fail_count -gt 0 ]; then
50+
exit 1
51+
fi

pkgs/client/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export default defineConfig({
5151
'__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'
5252
],
5353
setupFiles: ['__tests__/setup.ts'],
54+
globalSetup: './vitest.global-setup.ts',
5455
reporters: ['default'],
5556
coverage: {
5657
reportsDirectory: '../../coverage/pkgs/client',

pkgs/client/vitest.global-setup.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { createClient } from '@supabase/supabase-js';
2+
import postgres from 'postgres';
3+
import { createHash } from 'crypto';
4+
5+
export async function setup() {
6+
// Create a hash-based channel name with only alphanumeric characters
7+
const timestamp = Date.now().toString();
8+
const random = Math.random().toString();
9+
const hash = createHash('sha1').update(timestamp + random).digest('hex');
10+
const channelName = `setup${hash.substring(0, 16)}`; // Use first 16 chars of hash
11+
console.log(`[GLOBAL SETUP] Using random channel: ${channelName}`);
12+
13+
const supabaseUrl = 'http://localhost:50521';
14+
const pgUrl = 'postgresql://postgres:postgres@localhost:50522/postgres';
15+
16+
console.log('[GLOBAL SETUP] Checking Supabase availability...');
17+
18+
// Check if Supabase is reachable
19+
try {
20+
const response = await fetch(`${supabaseUrl}/rest/v1/`, { method: 'HEAD' });
21+
console.log(`[GLOBAL SETUP] Supabase REST API response: ${response.status}`);
22+
} catch (fetchError) {
23+
console.error('[GLOBAL SETUP] ❌ Failed to reach Supabase REST API:', fetchError instanceof Error ? fetchError.message : fetchError);
24+
console.error('[GLOBAL SETUP] Is Supabase running on port 50521?');
25+
process.exit(1);
26+
}
27+
28+
console.log('[GLOBAL SETUP] Creating Supabase client...');
29+
const supabase = createClient(supabaseUrl, 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU');
30+
31+
console.log('[GLOBAL SETUP] Creating PostgreSQL connection...');
32+
// eslint-disable-next-line @typescript-eslint/no-empty-function
33+
const sql = postgres(pgUrl, { prepare: false, onnotice: () => {} });
34+
35+
const channel = supabase.channel(channelName);
36+
const events: unknown[] = [];
37+
38+
try {
39+
console.log('[GLOBAL SETUP] Testing PostgreSQL connection...');
40+
try {
41+
await sql`SELECT 1`;
42+
console.log('[GLOBAL SETUP] ✓ PostgreSQL connection successful');
43+
} catch (pgError) {
44+
console.error('[GLOBAL SETUP] ❌ Failed to connect to PostgreSQL:', pgError instanceof Error ? pgError.message : pgError);
45+
console.error('[GLOBAL SETUP] Is PostgreSQL running on port 50522?');
46+
throw pgError;
47+
}
48+
49+
console.log('[GLOBAL SETUP] Creating realtime partition...');
50+
await sql`SELECT pgflow_tests.create_realtime_partition()`;
51+
console.log('[GLOBAL SETUP] ✓ Realtime partition created');
52+
53+
console.log('[GLOBAL SETUP] Setting up broadcast listener...');
54+
channel.on('broadcast', { event: '*' }, (p) => {
55+
console.log('[GLOBAL SETUP] Received broadcast event:', p);
56+
events.push(p);
57+
});
58+
59+
console.log('[GLOBAL SETUP] Subscribing to channel...');
60+
await new Promise<void>((ok, fail) => {
61+
const t = setTimeout(() => {
62+
console.error('[GLOBAL SETUP] ❌ Channel subscription timed out after 10s');
63+
fail(new Error('Channel subscription timeout after 10s'));
64+
}, 10000);
65+
66+
channel.subscribe((s) => {
67+
console.log(`[GLOBAL SETUP] Channel status: ${s}`);
68+
if (s === 'SUBSCRIBED') {
69+
console.log('[GLOBAL SETUP] ✓ Channel subscribed successfully');
70+
clearTimeout(t);
71+
ok();
72+
}
73+
if (s === 'TIMED_OUT') {
74+
console.error('[GLOBAL SETUP] ❌ Channel subscription timed out (status: TIMED_OUT)');
75+
clearTimeout(t);
76+
fail(new Error('Channel status: TIMED_OUT'));
77+
}
78+
if (s === 'CHANNEL_ERROR') {
79+
console.error('[GLOBAL SETUP] ❌ Channel error occurred (status: CHANNEL_ERROR)');
80+
console.error('[GLOBAL SETUP] This usually means the realtime server is not accessible');
81+
console.error('[GLOBAL SETUP] Check if Supabase realtime is running on ws://localhost:50521');
82+
clearTimeout(t);
83+
fail(new Error('Channel status: CHANNEL_ERROR'));
84+
}
85+
});
86+
});
87+
88+
// Add stabilization delay for cold channels to fully establish routing
89+
console.log('[GLOBAL SETUP] Channel subscribed, waiting 75ms for stabilization...');
90+
await new Promise(resolve => setTimeout(resolve, 75));
91+
console.log('[GLOBAL SETUP] Stabilization complete');
92+
93+
console.log('[GLOBAL SETUP] Sending test message via realtime.send()...');
94+
await sql`SELECT realtime.send('{}', 'e', ${channelName}, false)`;
95+
console.log('[GLOBAL SETUP] ✓ Message sent');
96+
97+
console.log('[GLOBAL SETUP] Waiting for broadcast event (timeout: 10s)...');
98+
const start = Date.now();
99+
while (events.length === 0 && Date.now() - start < 10000) {
100+
await new Promise((ok) => setTimeout(ok, 100));
101+
}
102+
103+
if (events.length === 0) {
104+
console.error('[GLOBAL SETUP] ❌ No events received after 10s');
105+
console.error('[GLOBAL SETUP] Message was sent but not received - realtime routing may be broken');
106+
throw new Error('realtime.send() failed - no events received');
107+
}
108+
109+
console.log(`[GLOBAL SETUP] ✓ Received ${events.length} event(s)`);
110+
console.log('[GLOBAL SETUP] ✅ All connectivity checks passed');
111+
} catch (e) {
112+
console.error('\n❌ Supabase connectivity check failed');
113+
console.error('Error:', e instanceof Error ? e.message : e);
114+
console.error('\nTroubleshooting:');
115+
console.error(' 1. Check if Supabase is running: docker ps | grep supabase');
116+
console.error(' 2. Check Supabase logs: supabase status');
117+
console.error(' 3. Try restarting: supabase stop && supabase start');
118+
console.error(' 4. Verify ports 50521 (API) and 50522 (PostgreSQL) are accessible\n');
119+
process.exit(1);
120+
} finally {
121+
console.log('[GLOBAL SETUP] Cleaning up...');
122+
await supabase.removeChannel(channel);
123+
await sql.end();
124+
console.log('[GLOBAL SETUP] Cleanup complete');
125+
}
126+
}
127+
128+
export async function teardown() {
129+
// Nothing to clean up globally
130+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference types='vitest' />
2+
import { defineConfig } from 'vitest/config';
3+
4+
// Separate config for type tests - NO global setup needed
5+
export default defineConfig({
6+
root: __dirname,
7+
cacheDir: '../../node_modules/.vite/pkgs/client',
8+
test: {
9+
watch: false,
10+
globals: true,
11+
environment: 'node',
12+
include: [
13+
'__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'
14+
],
15+
// NO setupFiles or globalSetup - type tests don't need runtime setup
16+
typecheck: {
17+
enabled: true,
18+
},
19+
reporters: ['default'],
20+
},
21+
});

0 commit comments

Comments
 (0)