Skip to content

Commit 044c088

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

25 files changed

+639
-924
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:*)",

.github/actions/setup/action.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ description: 'Common setup steps for pgflow CI workflow (run after checkout)'
44
runs:
55
using: 'composite'
66
steps:
7+
# Configure Docker daemon to allow containers to set high ulimits
8+
# This prevents "ulimit: open files: cannot modify limit: Operation not permitted" errors
9+
# when Supabase containers (pooler, realtime) try to set RLIMIT_NOFILE to 100000.
10+
# The error occurs because GitHub Actions runners have restrictive ulimit defaults
11+
# that prevent containers from increasing their file descriptor limits.
12+
# See: https://github.com/orgs/supabase/discussions/18228
13+
- name: Configure Docker ulimits for Supabase
14+
shell: bash
15+
run: |
16+
sudo mkdir -p /etc/docker
17+
echo '{"default-ulimits":{"nofile":{"Name":"nofile","Hard":100000,"Soft":100000}}}' | sudo tee /etc/docker/daemon.json
18+
sudo systemctl restart docker
19+
720
- uses: pnpm/action-setup@v4
821
with:
922
version: '10.20.0'

apps/demo/supabase/functions/article_flow_worker/deno.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/demo/supabase/functions/article_flow_worker/tasks/summarize-article.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ async function summarizeWithGroq(content: string, apiKey: string) {
4040
},
4141
{
4242
role: 'user',
43-
content: `Summarize this article in 2-3 sentences and determine its sentiment:\n\n${content.slice(0, 4000)}`
43+
content: `Summarize this article in 2-3 sentences and determine its sentiment:\n\n${content.slice(
44+
0,
45+
4000
46+
)}`
4447
}
4548
],
4649
temperature: 0.7,
@@ -84,7 +87,10 @@ async function summarizeWithOpenAI(content: string, apiKey: string) {
8487
},
8588
{
8689
role: 'user',
87-
content: `Summarize this article in 2-3 sentences and determine its sentiment:\n\n${content.slice(0, 4000)}`
90+
content: `Summarize this article in 2-3 sentences and determine its sentiment:\n\n${content.slice(
91+
0,
92+
4000
93+
)}`
8894
}
8995
],
9096
temperature: 0.7,
Lines changed: 90 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { assert } from 'https://deno.land/std@0.208.0/assert/mod.ts';
2+
import { stub } from 'https://deno.land/std@0.208.0/testing/mock.ts';
23
import { fetchArticle } from '../tasks/fetch-article.ts';
34
import { load } from 'https://deno.land/std@0.208.0/dotenv/mod.ts';
45

@@ -7,47 +8,113 @@ await load({ envPath: '../.env', export: true }).catch(() => {
78
console.log('No .env file found, using environment variables');
89
});
910

10-
Deno.test('fetchArticle - fetches real article from Hacker News', async () => {
11-
// Use a stable HN article URL that should always exist
12-
const url = 'https://news.ycombinator.com/item?id=35629516';
11+
const mockJinaResponse = `# Mock Article Title
1312
14-
const result = await fetchArticle(url);
13+
This is mock content with enough text to pass validation tests.
14+
It has multiple lines and is over 100 characters long.
15+
`;
1516

16-
// Verify we got a result with both content and title
17-
assert(result.content, 'Should have content');
18-
assert(result.title, 'Should have title');
19-
assert(result.content.length > 100, 'Content should be substantial');
20-
assert(result.title !== 'Untitled Article', 'Should extract a real title');
17+
function stubFetch(response: { status: number; statusText: string; body?: string }) {
18+
return stub(globalThis, 'fetch', () =>
19+
Promise.resolve(
20+
new Response(response.body || '', {
21+
status: response.status,
22+
statusText: response.statusText
23+
})
24+
)
25+
);
26+
}
2127

22-
console.log(`✓ Fetched article: "${result.title}" (${result.content.length} chars)`);
23-
});
28+
function stubFetchError(error: Error) {
29+
return stub(globalThis, 'fetch', () => Promise.reject(error));
30+
}
2431

25-
Deno.test('fetchArticle - fetches real article from TechCrunch', async () => {
26-
// Use TechCrunch homepage which should always work
27-
const url = 'https://techcrunch.com';
32+
// Mocked tests (always run)
33+
Deno.test('fetchArticle - fetches article with mocked fetch', async () => {
34+
const fetchStub = stubFetch({ status: 200, statusText: 'OK', body: mockJinaResponse });
2835

29-
const result = await fetchArticle(url);
36+
try {
37+
const url = 'https://example.com/article';
38+
const result = await fetchArticle(url);
3039

31-
assert(result.content, 'Should have content');
32-
assert(result.title, 'Should have title');
33-
assert(result.content.length > 100, 'Content should be substantial');
40+
assert(result.content, 'Should have content');
41+
assert(result.title, 'Should have title');
42+
assert(result.content.length > 100, 'Content should be substantial');
43+
assert(result.title === 'Mock Article Title', 'Should extract title from mock response');
3444

35-
console.log(`✓ Fetched article: "${result.title}" (${result.content.length} chars)`);
45+
console.log('✓ Mock fetch works');
46+
} finally {
47+
fetchStub.restore();
48+
}
49+
});
50+
51+
Deno.test('fetchArticle - handles non-OK response with mocked fetch', async () => {
52+
const fetchStub = stubFetch({
53+
status: 451,
54+
statusText: 'Unavailable For Legal Reasons',
55+
body: 'Unavailable'
56+
});
57+
58+
try {
59+
const url = 'https://example.com/blocked-article';
60+
await fetchArticle(url);
61+
assert(false, 'Should have thrown an error');
62+
} catch (error) {
63+
assert(error instanceof Error);
64+
assert(error.message.includes('Failed to fetch'));
65+
assert(error.message.includes('451'));
66+
console.log('✓ Properly handles non-OK responses');
67+
} finally {
68+
fetchStub.restore();
69+
}
3670
});
3771

3872
Deno.test({
39-
name: 'fetchArticle - handles non-existent URL gracefully',
40-
sanitizeResources: false, // Disable resource leak check for this test
73+
name: 'fetchArticle - handles network errors with mocked fetch',
74+
sanitizeResources: false,
4175
fn: async () => {
42-
const url = 'https://this-domain-definitely-does-not-exist-12345.com/article';
76+
const fetchStub = stubFetchError(new TypeError('Network error'));
4377

4478
try {
79+
const url = 'https://example.com/article';
4580
await fetchArticle(url);
4681
assert(false, 'Should have thrown an error');
4782
} catch (error) {
4883
assert(error instanceof Error);
4984
assert(error.message.includes('Failed to fetch'));
50-
console.log('✓ Properly handles fetch errors');
85+
console.log('✓ Properly handles network errors');
86+
} finally {
87+
fetchStub.restore();
5188
}
5289
}
5390
});
91+
92+
// Real HTTP tests (only run when USE_REAL_HTTP=true)
93+
if (Deno.env.get('USE_REAL_HTTP') === 'true') {
94+
Deno.test('fetchArticle - fetches real article from Hacker News', async () => {
95+
const url = 'https://news.ycombinator.com/item?id=35629516';
96+
97+
const result = await fetchArticle(url);
98+
99+
assert(result.content, 'Should have content');
100+
assert(result.title, 'Should have title');
101+
assert(result.content.length > 100, 'Content should be substantial');
102+
assert(result.title !== 'Untitled Article', 'Should extract a real title');
103+
104+
console.log(`✓ Fetched real article: "${result.title}" (${result.content.length} chars)`);
105+
});
106+
107+
Deno.test('fetchArticle - fetches real article from TechCrunch', async () => {
108+
const url = 'https://techcrunch.com';
109+
110+
const result = await fetchArticle(url);
111+
112+
assert(result.content, 'Should have content');
113+
assert(result.title, 'Should have title');
114+
assert(result.content.length > 100, 'Content should be substantial');
115+
116+
console.log(`✓ Fetched real article: "${result.title}" (${result.content.length} chars)`);
117+
});
118+
} else {
119+
console.log('ℹ Skipping real HTTP tests (set USE_REAL_HTTP=true to run them)');
120+
}

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__/PgflowClient.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ describe('PgflowClient', () => {
2424

2525
test('initializes correctly', () => {
2626
const { client } = createMockClient();
27-
const pgflowClient = new PgflowClient(client);
27+
const pgflowClient = new PgflowClient(client, { realtimeStabilizationDelayMs: 0 });
2828

2929
expect(pgflowClient).toBeDefined();
3030
});
@@ -40,7 +40,7 @@ describe('PgflowClient', () => {
4040
);
4141
mockRpcCall(mocks, response);
4242

43-
const pgflowClient = new PgflowClient(client);
43+
const pgflowClient = new PgflowClient(client, { realtimeStabilizationDelayMs: 0 });
4444
const run = await pgflowClient.startFlow(FLOW_SLUG, input);
4545

4646
// Check RPC call
@@ -66,7 +66,7 @@ describe('PgflowClient', () => {
6666
const error = new Error('RPC error');
6767
mockRpcCall(mocks, { data: null, error });
6868

69-
const pgflowClient = new PgflowClient(client);
69+
const pgflowClient = new PgflowClient(client, { realtimeStabilizationDelayMs: 0 });
7070

7171
// The startFlow call should reject with the error
7272
await expect(
@@ -84,7 +84,7 @@ describe('PgflowClient', () => {
8484
);
8585
mockRpcCall(mocks, response);
8686

87-
const pgflowClient = new PgflowClient(client);
87+
const pgflowClient = new PgflowClient(client, { realtimeStabilizationDelayMs: 0 });
8888

8989
// First call should fetch from DB
9090
const run1 = await pgflowClient.getRun(RUN_ID);
@@ -105,7 +105,7 @@ describe('PgflowClient', () => {
105105
// Mock the RPC call to return no run
106106
mockRpcCall(mocks, { data: { run: null, steps: [] }, error: null });
107107

108-
const pgflowClient = new PgflowClient(client);
108+
const pgflowClient = new PgflowClient(client, { realtimeStabilizationDelayMs: 0 });
109109

110110
const result = await pgflowClient.getRun('nonexistent-id');
111111

@@ -120,7 +120,7 @@ describe('PgflowClient', () => {
120120
mockRpcCall(mocks, response);
121121

122122
// Create test client
123-
const pgflowClient = new PgflowClient(client);
123+
const pgflowClient = new PgflowClient(client, { realtimeStabilizationDelayMs: 0 });
124124

125125
// Get a run to create an instance
126126
const run = await pgflowClient.getRun(RUN_ID);
@@ -154,7 +154,7 @@ describe('PgflowClient', () => {
154154
const response = createRunResponse({ run_id: RUN_ID, input });
155155
mockRpcCall(mocks, response);
156156

157-
const pgflowClient = new PgflowClient(client);
157+
const pgflowClient = new PgflowClient(client, { realtimeStabilizationDelayMs: 0 });
158158

159159
// Start a flow to create a run instance
160160
const run = await pgflowClient.startFlow(FLOW_SLUG, input);
@@ -189,7 +189,7 @@ describe('PgflowClient', () => {
189189
mockRpcCall(mocks, response1);
190190
mockRpcCall(mocks, response2);
191191

192-
const pgflowClient = new PgflowClient(client);
192+
const pgflowClient = new PgflowClient(client, { realtimeStabilizationDelayMs: 0 });
193193

194194
// Get two different runs
195195
await pgflowClient.getRun('1');
@@ -215,7 +215,7 @@ describe('PgflowClient', () => {
215215
const response = createRunResponse({ run_id: RUN_ID, input }, []);
216216
mockRpcCall(mocks, response);
217217

218-
const pgflowClient = new PgflowClient(client);
218+
const pgflowClient = new PgflowClient(client, { realtimeStabilizationDelayMs: 0 });
219219

220220
// Start a flow
221221
const run = await pgflowClient.startFlow(FLOW_SLUG, input);

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

0 commit comments

Comments
 (0)