diff --git a/packages/cloud/cloudflare/src/index.test.ts b/packages/cloud/cloudflare/src/index.test.ts index 8cbf25ee..3d7ef01a 100644 --- a/packages/cloud/cloudflare/src/index.test.ts +++ b/packages/cloud/cloudflare/src/index.test.ts @@ -125,6 +125,31 @@ describe('Cloudflare cloud adapter', () => { expect(instances.find((instance) => instance.id === 'tunnel:tun-1')?.status).toBe('running'); }); + it('continues list pagination when Cloudflare omits total_pages', async () => { + const fetchMock = vi.fn(async (url: string) => { + const { searchParams } = new URL(url); + const page = searchParams.get('page'); + if (page === '1') { + return ok(Array.from({ length: 100 }, (_, index) => ({ + queue_id: `queue-${index}`, + queue_name: `jobs-${index}`, + created_on: '2026-06-14T00:00:00Z', + }))); + } + if (page === '2') { + return ok([{ queue_id: 'queue-100', queue_name: 'jobs-100', created_on: '2026-06-14T00:00:00Z' }]); + } + throw new Error(`unexpected url ${url}`); + }); + vi.stubGlobal('fetch', fetchMock); + + const instances = await adapter.list(connectCtx(), { accountId: 'acct-1', resourceType: 'queue' }); + + expect(instances).toHaveLength(101); + expect(instances.at(-1)?.id).toBe('queue:queue-100'); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + it('checks status using the prefixed resource id', async () => { const fetchMock = vi.fn(async (url: string) => { expect(url).toBe(`${API}/accounts/acct-1/queues/queue-1`); @@ -137,6 +162,24 @@ describe('Cloudflare cloud adapter', () => { expect(instance).toMatchObject({ id: 'queue:queue-1', status: 'running', sku: 'queue' }); }); + it('maps Cloudflare tunnel active and errored statuses', async () => { + const fetchMock = vi.fn(async (url: string) => { + if (url.endsWith('/cfd_tunnel/tun-active')) { + return ok({ id: 'tun-active', name: 'edge-active', status: 'active', created_at: '2026-06-14T00:00:00Z' }); + } + if (url.endsWith('/cfd_tunnel/tun-errored')) { + return ok({ id: 'tun-errored', name: 'edge-errored', status: 'errored', created_at: '2026-06-14T00:00:00Z' }); + } + throw new Error(`unexpected url ${url}`); + }); + vi.stubGlobal('fetch', fetchMock); + + await expect(adapter.status(connectCtx(), 'tunnel:tun-active', { accountId: 'acct-1' })) + .resolves.toMatchObject({ id: 'tunnel:tun-active', status: 'running' }); + await expect(adapter.status(connectCtx(), 'tunnel:tun-errored', { accountId: 'acct-1' })) + .resolves.toMatchObject({ id: 'tunnel:tun-errored', status: 'failed' }); + }); + it('requires a caller-supplied tunnel secret when creating a tunnel', async () => { const fetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); diff --git a/packages/cloud/cloudflare/src/index.ts b/packages/cloud/cloudflare/src/index.ts index 9b65b4d2..eae82cb9 100644 --- a/packages/cloud/cloudflare/src/index.ts +++ b/packages/cloud/cloudflare/src/index.ts @@ -263,17 +263,24 @@ async function cfListAll( path: string, arrayKey: string, ): Promise { + const perPage = 100; const items: T[] = []; let page = 1; - let totalPages = 1; + let shouldContinue = true; do { const separator = path.includes('?') ? '&' : '?'; - const { result, resultInfo } = await cfRequest(ctx, config, 'GET', `${path}${separator}page=${page}&per_page=100`); - items.push(...arrayFromResult(result, arrayKey)); - totalPages = typeof resultInfo?.total_pages === 'number' ? resultInfo.total_pages : 1; + const { result, resultInfo } = await cfRequest(ctx, config, 'GET', `${path}${separator}page=${page}&per_page=${perPage}`); + const pageItems = arrayFromResult(result, arrayKey); + items.push(...pageItems); + + if (typeof resultInfo?.total_pages === 'number') { + shouldContinue = page < resultInfo.total_pages; + } else { + shouldContinue = pageItems.length >= perPage; + } page += 1; - } while (page <= totalPages); + } while (shouldContinue); return items; } @@ -477,9 +484,12 @@ function tunnelInstance(tunnel: Tunnel, kind: InstanceKind, quote: Quote, fallba } function tunnelStatus(status: string | undefined): Instance['status'] { - if (status === 'healthy') return 'running'; - if (status === 'inactive' || status === 'down') return 'stopped'; - if (status === 'degraded') return 'failed'; + const normalized = status?.toLowerCase(); + if (!normalized) return 'provisioning'; + if (normalized === 'healthy' || normalized === 'active' || normalized === 'running') return 'running'; + if (normalized === 'inactive' || normalized === 'down' || normalized === 'stopped') return 'stopped'; + if (normalized === 'degraded' || normalized === 'errored' || normalized === 'error' || normalized === 'failed' || normalized === 'unhealthy') return 'failed'; + if (normalized === 'pending' || normalized === 'provisioning' || normalized === 'initializing') return 'provisioning'; return 'provisioning'; }