Skip to content

Commit 03c2baf

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

File tree

18 files changed

+549
-850
lines changed

18 files changed

+549
-850
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__/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: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -137,36 +137,19 @@
137137
"parallel": false
138138
}
139139
},
140-
"db:ensure": {
141-
"executor": "nx:run-commands",
142-
"local": true,
143-
"dependsOn": ["supabase:prepare"],
144-
"options": {
145-
"cwd": "{projectRoot}",
146-
"commands": ["./scripts/ensure-db"],
147-
"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
161-
},
162140
"test:integration": {
163141
"executor": "nx:run-commands",
164142
"local": true,
165-
"dependsOn": ["db:ensure", "build"],
166-
"inputs": ["default", "^production"],
143+
"cache": false,
144+
"dependsOn": ["supabase:prepare", "build"],
167145
"options": {
168146
"cwd": "{projectRoot}",
169-
"commands": ["vitest run __tests__/integration/"],
147+
"commands": [
148+
"../../scripts/supabase-start-locked.sh .",
149+
"psql 'postgresql://postgres:postgres@localhost:50522/postgres' -c 'SELECT pgflow_tests.reset_db()'",
150+
"psql 'postgresql://postgres:postgres@localhost:50522/postgres' -c 'SELECT pgflow_tests.create_realtime_partition()'",
151+
"vitest run __tests__/integration/"
152+
],
170153
"parallel": false
171154
}
172155
},
@@ -183,11 +166,16 @@
183166
"test:vitest": {
184167
"executor": "nx:run-commands",
185168
"local": true,
186-
"dependsOn": ["db:ensure", "build"],
187-
"inputs": ["default", "^production"],
169+
"cache": false,
170+
"dependsOn": ["supabase:prepare", "build"],
188171
"options": {
189172
"cwd": "{projectRoot}",
190-
"commands": ["vitest run __tests__/"],
173+
"commands": [
174+
"../../scripts/supabase-start-locked.sh .",
175+
"psql 'postgresql://postgres:postgres@localhost:50522/postgres' -c 'SELECT pgflow_tests.reset_db()'",
176+
"psql 'postgresql://postgres:postgres@localhost:50522/postgres' -c 'SELECT pgflow_tests.create_realtime_partition()'",
177+
"vitest run __tests__/"
178+
],
191179
"parallel": false
192180
}
193181
},
@@ -199,11 +187,16 @@
199187
"benchmark": {
200188
"executor": "nx:run-commands",
201189
"local": true,
202-
"dependsOn": ["db:ensure", "build"],
203-
"inputs": ["default", "^production"],
190+
"cache": false,
191+
"dependsOn": ["supabase:prepare", "build"],
204192
"options": {
205193
"cwd": "{projectRoot}",
206-
"commands": ["node scripts/performance-benchmark.mjs"],
194+
"commands": [
195+
"../../scripts/supabase-start-locked.sh .",
196+
"psql 'postgresql://postgres:postgres@localhost:50522/postgres' -c 'SELECT pgflow_tests.reset_db()'",
197+
"psql 'postgresql://postgres:postgres@localhost:50522/postgres' -c 'SELECT pgflow_tests.create_realtime_partition()'",
198+
"node scripts/performance-benchmark.mjs"
199+
],
207200
"parallel": false
208201
}
209202
},
@@ -214,7 +207,7 @@
214207
"inputs": ["default", "^production"],
215208
"options": {
216209
"cwd": "{projectRoot}",
217-
"command": "pnpm vitest --typecheck.only --run"
210+
"command": "pnpm vitest --typecheck.only --run --config vitest.typecheck.config.ts"
218211
}
219212
},
220213
"test:types:strict": {

pkgs/client/scripts/ensure-db

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)