Skip to content

Commit 616435f

Browse files
mydeaclaude
andauthored
test(cloudflare-integration-tests): Type-check test suites in CI (#22068)
The `suites/**` files in `cloudflare-integration-tests` were never type-checked in CI: there was no `type-check` script, and vitest transpiles with esbuild (no type-checking). The suites are already covered by `tsconfig.json`, so this adds a gate and fixes the ~30 latent type errors it surfaced. **All fixes are type-only — no runtime behavior changed.** ## Changes - Add a `type-check` script (`tsc --noEmit`) and run `yarn type-check` for the package in the **Lint** CI job. - **Worker fixtures** (`suites/tracing/propagation/**`): drop an incorrect `implements RpcTarget` (Durable Objects expose their public methods over RPC inherently), and let the `WorkerEntrypoint` classes use the ambient `Cloudflare.Env` — which is what `withSentry`'s `WorkerEntrypointConstructor` requires — casting `this.env` to the local `Env` for binding access. (A global `Cloudflare.Env` augmentation would be more idiomatic but collides across fixtures that declare the same binding name with different DO classes into one tsc program.) - **AI assertions** (`google-genai` / `langchain` / `langgraph` test files): type the `span` callback params as `SerializedStreamedSpan`. - Narrow envelope items before accessing `.spans` in the durable-object tests, and fix the runner's `Expected` argument typing. - Targeted, commented casts for version-skewed AI mocks (LangChain callback handler, OpenAI mock client, vercel AI mock language model). - Exclude `suites/prisma/**` from the check — it imports a Prisma-generated client (`./generated`) that only exists after codegen. Part of a small series adding suite type-checking to the integration-test packages (alongside `node-integration-tests` and `bun-integration-tests` #22059). `deno-integration-tests` is intentionally skipped — it's a Deno project (`deno test --no-check`) that `tsc` can't type-check (URL imports, `Deno` globals). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f0fc230 commit 616435f

20 files changed

Lines changed: 67 additions & 41 deletions

File tree

.github/workflows/build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,9 @@ jobs:
337337
run: yarn lint
338338
- name: Lint for ES compatibility
339339
run: yarn lint:es-compatibility
340+
- name: Type check cloudflare integration test suites
341+
working-directory: dev-packages/cloudflare-integration-tests
342+
run: yarn type-check
340343

341344
job_check_lockfile:
342345
name: Check lockfile

dev-packages/cloudflare-integration-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"scripts": {
1010
"lint": "OXLINT_TSGOLINT_DANGEROUSLY_SUPPRESS_PROGRAM_DIAGNOSTICS=true oxlint . --type-aware",
1111
"lint:fix": "OXLINT_TSGOLINT_DANGEROUSLY_SUPPRESS_PROGRAM_DIAGNOSTICS=true oxlint . --fix --type-aware",
12+
"type-check": "tsc --noEmit",
1213
"test": "vitest run",
1314
"test:watch": "yarn test --watch"
1415
},

dev-packages/cloudflare-integration-tests/runner.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,10 @@ export function createRunner(...paths: string[]) {
408408
expected: Expected | Expected[],
409409
options: { headers?: Record<string, string>; data?: BodyInit; expectError?: boolean } = {},
410410
): Promise<T | undefined> {
411-
const expectations = Array.isArray(expected) ? expected : [expected];
411+
// `Expected` includes `Envelope`, which is itself an array, so `Array.isArray` can't
412+
// distinguish a single `Envelope` from an `Expected[]`. Callers pass expectation
413+
// callbacks (or an array of them), so the narrowed value is always `Expected[]`.
414+
const expectations = (Array.isArray(expected) ? expected : [expected]) as Expected[];
412415
const envelopePromises = expectations.map(e => waitForEnvelope(e));
413416
const result = await this.makeRequest<T>(method, path, options);
414417
await Promise.all(envelopePromises);

dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Envelope } from '@sentry/core';
1+
import type { Envelope, TransactionEvent } from '@sentry/core';
22
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
33
import { expect, it } from 'vitest';
44
import { createRunner } from '../../../runner';
@@ -14,7 +14,7 @@ const flushMarkerMatcher = (envelope: Envelope): void => {
1414
it('instruments SQL exec operations on Durable Object storage', async ({ signal }) => {
1515
const runner = createRunner(__dirname)
1616
.expect(envelope => {
17-
const transactionEvent = envelope[1]?.[0]?.[1];
17+
const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent | undefined;
1818
const spans = transactionEvent?.spans ?? [];
1919

2020
expect(transactionEvent).toEqual(
@@ -24,9 +24,7 @@ it('instruments SQL exec operations on Durable Object storage', async ({ signal
2424
}),
2525
);
2626

27-
const sqlSpans = (spans as Array<Record<string, unknown>>).filter(
28-
s => s.origin === 'auto.db.cloudflare.durable_object.sql',
29-
);
27+
const sqlSpans = spans.filter(s => s.origin === 'auto.db.cloudflare.durable_object.sql');
3028

3129
expect(sqlSpans).toHaveLength(3);
3230
expect(sqlSpans).toEqual(

dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Envelope } from '@sentry/core';
1+
import type { Envelope, TransactionEvent } from '@sentry/core';
22
import { expect, it } from 'vitest';
33
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
44
import { createRunner } from '../../../runner';
@@ -14,7 +14,7 @@ const flushMarkerMatcher = (envelope: Envelope): void => {
1414
it('instruments sync KV operations on Durable Object storage', async ({ signal }) => {
1515
const runner = createRunner(__dirname)
1616
.expect(envelope => {
17-
const transactionEvent = envelope[1]?.[0]?.[1];
17+
const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent | undefined;
1818
const spans = transactionEvent?.spans ?? [];
1919

2020
expect(transactionEvent).toEqual(

dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, it } from 'vitest';
2+
import type { SerializedStreamedSpan } from '@sentry/core';
23
import {
34
GEN_AI_OPERATION_NAME_ATTRIBUTE,
45
GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE,
@@ -29,13 +30,13 @@ it('traces Google GenAI chat creation and message sending', async ({ signal }) =
2930
const container = envelope[1]?.[1]?.[1] as any;
3031
expect(container).toBeDefined();
3132
expect(container.items).toHaveLength(3);
32-
expect(container.items.map(span => span.name).sort()).toEqual([
33+
expect(container.items.map((span: SerializedStreamedSpan) => span.name).sort()).toEqual([
3334
'chat gemini-1.5-pro',
3435
'embeddings text-embedding-004',
3536
'generate_content gemini-1.5-flash',
3637
]);
3738

38-
const chatSpan = container.items.find(span => span.name === 'chat gemini-1.5-pro');
39+
const chatSpan = container.items.find((span: SerializedStreamedSpan) => span.name === 'chat gemini-1.5-pro');
3940
expect(chatSpan).toBeDefined();
4041
expect(chatSpan!.status).toBe('ok');
4142
expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' });
@@ -50,7 +51,9 @@ it('traces Google GenAI chat creation and message sending', async ({ signal }) =
5051
expect(chatSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 });
5152
expect(chatSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 });
5253

53-
const generateContentSpan = container.items.find(span => span.name === 'generate_content gemini-1.5-flash');
54+
const generateContentSpan = container.items.find(
55+
(span: SerializedStreamedSpan) => span.name === 'generate_content gemini-1.5-flash',
56+
);
5457
expect(generateContentSpan).toBeDefined();
5558
expect(generateContentSpan!.status).toBe('ok');
5659
expect(generateContentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({
@@ -95,7 +98,9 @@ it('traces Google GenAI chat creation and message sending', async ({ signal }) =
9598
value: 20,
9699
});
97100

98-
const embeddingsSpan = container.items.find(span => span.name === 'embeddings text-embedding-004');
101+
const embeddingsSpan = container.items.find(
102+
(span: SerializedStreamedSpan) => span.name === 'embeddings text-embedding-004',
103+
);
99104
expect(embeddingsSpan).toBeDefined();
100105
expect(embeddingsSpan!.status).toBe('ok');
101106
expect(embeddingsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({

dev-packages/cloudflare-integration-tests/suites/tracing/langchain/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as Sentry from '@sentry/cloudflare';
2-
import { MockChain, MockChatModel, MockTool } from './mocks';
2+
import { type CallbackHandler, MockChain, MockChatModel, MockTool } from './mocks';
33

44
interface Env {
55
SENTRY_DSN: string;
@@ -14,10 +14,11 @@ export default Sentry.withSentry(
1414
{
1515
async fetch(_request, _env, _ctx) {
1616
// Create LangChain callback handler
17+
// The mock models accept their own local `CallbackHandler` shape, which differs from the SDK's handler type.
1718
const callbackHandler = Sentry.createLangChainCallbackHandler({
1819
recordInputs: false,
1920
recordOutputs: false,
20-
});
21+
}) as unknown as CallbackHandler;
2122

2223
// Test 1: Chat model invocation
2324
const chatModel = new MockChatModel({

dev-packages/cloudflare-integration-tests/suites/tracing/langchain/test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, it } from 'vitest';
2+
import type { SerializedStreamedSpan } from '@sentry/core';
23
import {
34
GEN_AI_OPERATION_NAME_ATTRIBUTE,
45
GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE,
@@ -29,13 +30,15 @@ it('traces langchain chat model, chain, and tool invocations', async ({ signal }
2930
const container = envelope[1]?.[1]?.[1] as any;
3031
expect(container).toBeDefined();
3132
expect(container.items).toHaveLength(3);
32-
expect(container.items.map(span => span.name).sort()).toEqual([
33+
expect(container.items.map((span: SerializedStreamedSpan) => span.name).sort()).toEqual([
3334
'chain my_test_chain',
3435
'chat claude-3-5-sonnet-20241022',
3536
'execute_tool search_tool',
3637
]);
3738

38-
const chatSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022');
39+
const chatSpan = container.items.find(
40+
(span: SerializedStreamedSpan) => span.name === 'chat claude-3-5-sonnet-20241022',
41+
);
3942
expect(chatSpan).toBeDefined();
4043
expect(chatSpan!.status).toBe('ok');
4144
expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' });
@@ -52,14 +55,14 @@ it('traces langchain chat model, chain, and tool invocations', async ({ signal }
5255
expect(chatSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 });
5356
expect(chatSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 });
5457

55-
const chainSpan = container.items.find(span => span.name === 'chain my_test_chain');
58+
const chainSpan = container.items.find((span: SerializedStreamedSpan) => span.name === 'chain my_test_chain');
5659
expect(chainSpan).toBeDefined();
5760
expect(chainSpan!.status).toBe('ok');
5861
expect(chainSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langchain' });
5962
expect(chainSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.invoke_agent' });
6063
expect(chainSpan!.attributes['langchain.chain.name']).toEqual({ type: 'string', value: 'my_test_chain' });
6164

62-
const toolSpan = container.items.find(span => span.name === 'execute_tool search_tool');
65+
const toolSpan = container.items.find((span: SerializedStreamedSpan) => span.name === 'execute_tool search_tool');
6366
expect(toolSpan).toBeDefined();
6467
expect(toolSpan!.status).toBe('ok');
6568
expect(toolSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langchain' });

dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, it } from 'vitest';
2+
import type { SerializedStreamedSpan } from '@sentry/core';
23
import {
34
GEN_AI_AGENT_NAME_ATTRIBUTE,
45
GEN_AI_INPUT_MESSAGES_ATTRIBUTE,
@@ -29,12 +30,14 @@ it('traces langgraph compile and invoke operations', async ({ signal }) => {
2930
expect(container).toBeDefined();
3031

3132
expect(container.items).toHaveLength(2);
32-
expect(container.items.map(span => span.name).sort()).toEqual([
33+
expect(container.items.map((span: SerializedStreamedSpan) => span.name).sort()).toEqual([
3334
'create_agent weather_assistant',
3435
'invoke_agent weather_assistant',
3536
]);
3637

37-
const createAgentSpan = container.items.find(span => span.name === 'create_agent weather_assistant');
38+
const createAgentSpan = container.items.find(
39+
(span: SerializedStreamedSpan) => span.name === 'create_agent weather_assistant',
40+
);
3841
expect(createAgentSpan).toBeDefined();
3942
expect(createAgentSpan!.status).toBe('ok');
4043
expect(createAgentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({
@@ -48,7 +51,9 @@ it('traces langgraph compile and invoke operations', async ({ signal }) => {
4851
value: 'weather_assistant',
4952
});
5053

51-
const invokeAgentSpan = container.items.find(span => span.name === 'invoke_agent weather_assistant');
54+
const invokeAgentSpan = container.items.find(
55+
(span: SerializedStreamedSpan) => span.name === 'invoke_agent weather_assistant',
56+
);
5257
expect(invokeAgentSpan).toBeDefined();
5358
expect(invokeAgentSpan!.status).toBe('ok');
5459
expect(invokeAgentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({

dev-packages/cloudflare-integration-tests/suites/tracing/openai/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ export default Sentry.withSentry(
1919
}),
2020
{
2121
async fetch(_request, _env, _ctx) {
22-
const response = await client.chat?.completions?.create({
22+
// The mock client types `chat` loosely (`Record<string, unknown>`), so narrow it to the shape used here.
23+
const completions = (
24+
client.chat as { completions?: { create: (args: Record<string, unknown>) => Promise<unknown> } }
25+
)?.completions;
26+
const response = await completions?.create({
2327
model: 'gpt-3.5-turbo',
2428
messages: [
2529
{ role: 'system', content: 'You are a helpful assistant.' },

0 commit comments

Comments
 (0)