1+ /**
2+ * Connectivity smoke test for Supabase services required by pgflow client.
3+ * This test validates all the connectivity patterns used by the client:
4+ * 1. Realtime channel subscription and message broadcasting
5+ * 2. Database queries (from() method)
6+ * 3. RPC calls
7+ *
8+ * Run this test first in CI to quickly identify connectivity issues
9+ * before running the full test suite.
10+ */
11+
12+ import { describe , it , expect , beforeAll } from 'vitest' ;
13+ import { createClient , SupabaseClient , RealtimeChannel } from '@supabase/supabase-js' ;
14+ import { createTestSupabaseClient } from '../helpers/setup.js' ;
15+ import { withPgNoTransaction } from '../helpers/db.js' ;
16+ import { grantMinimalPgflowPermissions } from '../helpers/permissions.js' ;
17+
18+ describe ( 'Supabase Connectivity Check' , ( ) => {
19+ let supabase : SupabaseClient ;
20+
21+ beforeAll ( async ( ) => {
22+ // Use the same setup as other integration tests - service_role key
23+ supabase = createTestSupabaseClient ( ) ;
24+
25+ console . log ( '🔍 Supabase connectivity check starting...' ) ;
26+ console . log ( ` URL: http://localhost:50521` ) ;
27+ console . log ( ` Using: service_role key (same as other integration tests)` ) ;
28+
29+ // Grant permissions like other integration tests do
30+ await withPgNoTransaction ( async ( sql ) => {
31+ await grantMinimalPgflowPermissions ( sql ) ;
32+ } ) ( ) ;
33+ } ) ;
34+
35+ it ( 'has valid Supabase configuration' , ( ) => {
36+ const url = process . env . SUPABASE_URL || 'http://localhost:50521' ;
37+ const key = process . env . SUPABASE_ANON_KEY ||
38+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' ;
39+
40+ expect ( url ) . toBeTruthy ( ) ;
41+ expect ( key ) . toBeTruthy ( ) ;
42+ expect ( url ) . toMatch ( / ^ h t t p s ? : \/ \/ / ) ;
43+ expect ( key ) . toMatch ( / ^ e y J / ) ; // JWT tokens start with eyJ
44+ } ) ;
45+
46+ it ( 'can establish Realtime channel subscription' , async ( ) => {
47+ const channelName = `connectivity-test-${ Date . now ( ) } ` ;
48+ let channel : RealtimeChannel | null = null ;
49+
50+ try {
51+ channel = supabase . channel ( channelName ) ;
52+
53+ // Track connection status
54+ const statusPromise = new Promise < string > ( ( resolve , reject ) => {
55+ const timeout = setTimeout ( ( ) => {
56+ const currentStatus = channel ?. state || 'unknown' ;
57+ reject ( new Error (
58+ `Realtime subscription timeout after 10s. ` +
59+ `Final status: ${ currentStatus } . ` +
60+ `This suggests Supabase Realtime is not accessible in CI.`
61+ ) ) ;
62+ } , 10000 ) ;
63+
64+ channel ! . subscribe ( ( status ) => {
65+ console . log ( ` Channel status: ${ status } ` ) ;
66+
67+ if ( status === 'SUBSCRIBED' ) {
68+ clearTimeout ( timeout ) ;
69+ resolve ( status ) ;
70+ } else if ( status === 'CHANNEL_ERROR' || status === 'TIMED_OUT' ) {
71+ clearTimeout ( timeout ) ;
72+ reject ( new Error (
73+ `Channel subscription failed with status: ${ status } . ` +
74+ `This indicates Supabase Realtime connection issues.`
75+ ) ) ;
76+ }
77+ } ) ;
78+ } ) ;
79+
80+ const status = await statusPromise ;
81+ expect ( status ) . toBe ( 'SUBSCRIBED' ) ;
82+ console . log ( ' ✓ Realtime subscription established' ) ;
83+ } finally {
84+ if ( channel ) {
85+ await supabase . removeChannel ( channel ) ;
86+ }
87+ }
88+ } , 15000 ) ;
89+
90+ it ( 'can send and receive broadcast messages' , async ( ) => {
91+ const channelName = `broadcast-test-${ Date . now ( ) } ` ;
92+ const testMessage = { test : 'connectivity' , timestamp : Date . now ( ) } ;
93+ let channel : RealtimeChannel | null = null ;
94+
95+ try {
96+ channel = supabase . channel ( channelName ) ;
97+
98+ // Set up message reception
99+ const messagePromise = new Promise < any > ( ( resolve , reject ) => {
100+ const timeout = setTimeout ( ( ) => {
101+ reject ( new Error (
102+ 'Broadcast message timeout after 10s. ' +
103+ 'Channel subscribed but messages not being received.'
104+ ) ) ;
105+ } , 10000 ) ;
106+
107+ // Listen for broadcast messages (pgflow client pattern)
108+ channel ! . on ( 'broadcast' , { event : '*' } , ( payload ) => {
109+ console . log ( ' Received broadcast:' , payload ) ;
110+ if ( payload . event === 'test-event' ) {
111+ clearTimeout ( timeout ) ;
112+ resolve ( payload . payload ) ;
113+ }
114+ } ) ;
115+
116+ // Subscribe and send message
117+ channel ! . subscribe ( async ( status ) => {
118+ if ( status === 'SUBSCRIBED' ) {
119+ console . log ( ' Sending test broadcast...' ) ;
120+ // Send using the pattern from SupabaseBroadcastAdapter
121+ await channel ! . send ( {
122+ type : 'broadcast' ,
123+ event : 'test-event' ,
124+ payload : testMessage
125+ } ) ;
126+ }
127+ } ) ;
128+ } ) ;
129+
130+ const received = await messagePromise ;
131+ expect ( received ) . toEqual ( testMessage ) ;
132+ console . log ( ' ✓ Broadcast messages working' ) ;
133+ } finally {
134+ if ( channel ) {
135+ await supabase . removeChannel ( channel ) ;
136+ }
137+ }
138+ } , 15000 ) ;
139+
140+ it ( 'can query pgflow database tables' , async ( ) => {
141+ // Test the same queries used by SupabaseBroadcastAdapter
142+ const { data : flowsData , error : flowsError } = await supabase
143+ . schema ( 'pgflow' )
144+ . from ( 'flows' )
145+ . select ( 'flow_slug' )
146+ . limit ( 1 ) ;
147+
148+ if ( flowsError ) {
149+ throw new Error (
150+ `Failed to query pgflow.flows table: ${ flowsError . message } . ` +
151+ `This suggests database connectivity or schema issues.`
152+ ) ;
153+ }
154+
155+ // We don't need any flows to exist, just that the query works
156+ expect ( flowsError ) . toBeNull ( ) ;
157+ console . log ( ' ✓ Database query to pgflow.flows working' ) ;
158+
159+ // Test steps table query
160+ const { data : stepsData , error : stepsError } = await supabase
161+ . schema ( 'pgflow' )
162+ . from ( 'steps' )
163+ . select ( 'step_slug' )
164+ . limit ( 1 ) ;
165+
166+ if ( stepsError ) {
167+ throw new Error (
168+ `Failed to query pgflow.steps table: ${ stepsError . message } . ` +
169+ `This suggests database schema issues.`
170+ ) ;
171+ }
172+
173+ expect ( stepsError ) . toBeNull ( ) ;
174+ console . log ( ' ✓ Database query to pgflow.steps working' ) ;
175+ } ) ;
176+
177+ it ( 'can call pgflow RPC functions' , async ( ) => {
178+ // Test calling the RPC used by PgflowClient (with a fake run_id that won't exist)
179+ const testRunId = '00000000-0000-0000-0000-000000000000' ;
180+
181+ const { data, error } = await supabase
182+ . schema ( 'pgflow' )
183+ . rpc ( 'get_run_with_states' , { run_id : testRunId } ) ;
184+
185+ // We expect no data for a fake run_id, but the RPC should execute without error
186+ if ( error && ! error . message . includes ( 'not found' ) ) {
187+ throw new Error (
188+ `Failed to call pgflow.get_run_with_states RPC: ${ error . message } . ` +
189+ `This suggests RPC connectivity or permission issues.`
190+ ) ;
191+ }
192+
193+ // The RPC should return null/empty for non-existent run, not an error
194+ expect ( error ) . toBeNull ( ) ;
195+ console . log ( ' ✓ RPC call to pgflow.get_run_with_states working' ) ;
196+ } ) ;
197+
198+ it ( 'validates full PgflowClient connection pattern' , async ( ) => {
199+ // This test simulates the exact pattern PgflowClient uses:
200+ // 1. Create channel with specific naming pattern
201+ // 2. Subscribe to channel
202+ // 3. Listen for events with wildcard pattern
203+
204+ const runId = `test-${ Date . now ( ) } ` ;
205+ const channelName = `pgflow:run:${ runId } ` ;
206+ let channel : RealtimeChannel | null = null ;
207+
208+ try {
209+ console . log ( ` Testing PgflowClient pattern with channel: ${ channelName } ` ) ;
210+ channel = supabase . channel ( channelName ) ;
211+
212+ const eventReceived = new Promise < boolean > ( ( resolve , reject ) => {
213+ const timeout = setTimeout ( ( ) => {
214+ reject ( new Error (
215+ `PgflowClient pattern timeout. Channel: ${ channelName } , ` +
216+ `Status: ${ channel ?. state } . ` +
217+ `This is the exact pattern failing in the test suite.`
218+ ) ) ;
219+ } , 10000 ) ;
220+
221+ // Use the exact event pattern from SupabaseBroadcastAdapter
222+ channel ! . on ( 'broadcast' , { event : '*' } , ( payload ) => {
223+ clearTimeout ( timeout ) ;
224+ resolve ( true ) ;
225+ } ) ;
226+
227+ channel ! . subscribe ( async ( status ) => {
228+ console . log ( ` PgflowClient channel status: ${ status } ` ) ;
229+ if ( status === 'SUBSCRIBED' ) {
230+ // Send a test event matching pgflow patterns
231+ await channel ! . send ( {
232+ type : 'broadcast' ,
233+ event : 'run:started' ,
234+ payload : { run_id : runId , status : 'Started' }
235+ } ) ;
236+ } else if ( status === 'TIMED_OUT' || status === 'CHANNEL_ERROR' ) {
237+ clearTimeout ( timeout ) ;
238+ reject ( new Error ( `Channel failed with status: ${ status } ` ) ) ;
239+ }
240+ } ) ;
241+ } ) ;
242+
243+ await expect ( eventReceived ) . resolves . toBe ( true ) ;
244+ console . log ( ' ✓ PgflowClient connection pattern working' ) ;
245+ } finally {
246+ if ( channel ) {
247+ await supabase . removeChannel ( channel ) ;
248+ }
249+ }
250+ } , 15000 ) ;
251+
252+ // Summary test that provides actionable feedback
253+ it ( 'provides connectivity summary' , ( ) => {
254+ console . log ( '\n✅ All Supabase connectivity checks passed!' ) ;
255+ console . log ( ' The client can successfully:' ) ;
256+ console . log ( ' - Subscribe to Realtime channels' ) ;
257+ console . log ( ' - Send and receive broadcast messages' ) ;
258+ console . log ( ' - Query database tables' ) ;
259+ console . log ( ' - Call RPC functions' ) ;
260+ console . log ( ' - Use PgflowClient connection patterns' ) ;
261+ } ) ;
262+ } ) ;
0 commit comments