Skip to content

Introduce SSE test helpers#891

Merged
kibertoad merged 2 commits intomainfrom
feat/sse-helpers
Mar 17, 2026
Merged

Introduce SSE test helpers#891
kibertoad merged 2 commits intomainfrom
feat/sse-helpers

Conversation

@kibertoad
Copy link
Copy Markdown
Collaborator

Changes

Simplify client-side SSE testing

Checklist

  • Apply one of following labels; major, minor, patch or skip-release
  • I've updated the documentation, or no changes were necessary
  • I've updated the tests, or no changes were necessary

AI Assistance Tracking

We're running a metric to understand where AI assists our engineering work. Please select exactly one of the options below:

Mark "Yes" if AI helped in any part of this work, for example: generating code, refactoring, debugging support,
explaining something, reviewing an idea, or suggesting an approach.

  • Yes, AI assisted with this PR
  • No, AI did not assist with this PR

Params extends z.ZodTypeAny = z.ZodTypeAny,
Query extends z.ZodTypeAny = z.ZodTypeAny,
RequestHeaders extends z.ZodTypeAny = z.ZodTypeAny,
Params extends z.ZodTypeAny | undefined = undefined,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was inconsistent with simple REST contracts for no good reason, which made using them harder

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 17, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: lokalise/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a81b8b47-44dc-4a49-bf0a-52b296098f29

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR introduces optional schema parameters across SSE and Dual-mode contract definitions, allowing Params, Query, and RequestHeaders to default to undefined. Path resolution is conditionally typed: contracts with Params use RoutePathResolver<z.infer<Params>>, while contracts without Params use a parameterless () => string function. SSE mocking support is added to MockttpHelper and MswHelper via new mockSseResponse methods and supporting types. Test contracts are created for multiple SSE scenarios (GET, POST, with path/query params, dual-mode). Type test coverage is expanded to verify inference behavior with optional schemas, and documentation is added for SSE mocking usage.

Sequence Diagram

sequenceDiagram
    participant Client as Test Client
    participant Helper as MockttpHelper/<br/>MswHelper
    participant Contract as SSE Contract<br/>Definition
    participant Server as Mock Server
    participant Stream as Text/Event<br/>Stream Response

    Client->>Helper: mockSseResponse(contract, {<br/>pathParams?, events })
    Helper->>Contract: Extract requestPathParamsSchema<br/>and HTTP method
    Helper->>Helper: Resolve path using pathParams<br/>or pathResolver()
    Helper->>Server: Register handler for<br/>resolved path & method
    
    Client->>Server: Send HTTP request<br/>(GET/POST/PATCH/PUT)
    Server->>Helper: Invoke registered handler
    Helper->>Helper: formatSseResponse(events)<br/>converts to text/event-stream format
    Helper->>Stream: Return response with<br/>content-type: text/event-stream
    Stream->>Client: Stream SSE events
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Introduce SSE test helpers' is clear, concise, and accurately describes the primary change: adding Server-Sent Events testing helpers across multiple modules.
Description check ✅ Passed The PR description is partially complete; it addresses the core objective but lacks detail. While it confirms checklist items and AI assistance, the 'Changes' section is minimal ('Simplify client-side SSE testing') compared to the extensive file modifications shown in the summary.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sse-helpers
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/app/api-contracts/src/sse/sseContractBuilders.types.spec.ts (1)

137-147: ⚠️ Potential issue | 🟡 Minor

Fix lint error: unused variable contract.

The CI pipeline reports an unused variable lint error at line 138. The variable appears to be used in the type assertions below, but the linter is flagging it. This could be because expectTypeOf only uses the variable at the type level, not at runtime.

Consider using the _ prefix convention or restructuring to satisfy the linter:

🔧 Proposed fix
   it('omitted schemas accept undefined', () => {
-    const contract = buildSseContract({
+    const _contract = buildSseContract({
       method: 'get' as const,
       pathResolver: () => '/api/stream',
       serverSentEventSchemas: { message: z.object({ text: z.string() }) },
     })

-    expectTypeOf<undefined>().toMatchTypeOf<typeof contract.requestPathParamsSchema>()
-    expectTypeOf<undefined>().toMatchTypeOf<typeof contract.requestQuerySchema>()
-    expectTypeOf<undefined>().toMatchTypeOf<typeof contract.requestHeaderSchema>()
+    expectTypeOf<undefined>().toMatchTypeOf<typeof _contract.requestPathParamsSchema>()
+    expectTypeOf<undefined>().toMatchTypeOf<typeof _contract.requestQuerySchema>()
+    expectTypeOf<undefined>().toMatchTypeOf<typeof _contract.requestHeaderSchema>()
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/api-contracts/src/sse/sseContractBuilders.types.spec.ts` around
lines 137 - 147, The variable contract created by buildSseContract is only used
in type-level assertions so the linter flags it as unused; rename it to a
deliberately-unused-prefixed identifier (e.g. _contract) or otherwise reference
it in a no-op runtime expression so the linter is satisfied—update the
declaration of contract in the it block (where buildSseContract is called)
accordingly and keep the existing expectTypeOf checks that reference that
identifier (e.g. _contract) so buildSseContract, contract/_contract, and
expectTypeOf usages remain aligned.
🧹 Nitpick comments (4)
packages/app/universal-testing-utils/README.md (1)

336-443: Consider showing queryParams in the new SSE docs too.

packages/app/universal-testing-utils/test/testContracts.ts now includes sseGetContractWithQueryParams, but this section only demonstrates no-param and path-param calls. One short mockSseResponse(..., { queryParams: ... }) example would make that capability easier to discover.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/universal-testing-utils/README.md` around lines 336 - 443, The
docs for SSE mock support are missing an example demonstrating queryParams
usage; add a concise example near the other mockSseResponse snippets showing
mockSseResponse called with a contract that expects query params (referencing
sseGetContractWithQueryParams from test/testContracts.ts) and pass a queryParams
object (e.g., { page: '1' }) in the options for MockttpHelper and MswHelper
calls so readers can discover query param support alongside pathParams and
responseCode; keep the example minimal and consistent with the existing event
array and formatSseResponse usage.
packages/app/universal-testing-utils/src/MockttpHelper.ts (2)

40-42: Consider adding trailing newline for SSE spec compliance.

The SSE specification recommends ending the stream with a blank line. The current implementation joins events with \n but the final event only has one trailing newline, not two (which would create the blank line separator).

♻️ Optional fix for stricter SSE compliance
 export function formatSseResponse(events: { event: string; data: unknown }[]): string {
-  return events.map((e) => `event: ${e.event}\ndata: ${JSON.stringify(e.data)}\n`).join('\n')
+  return events.map((e) => `event: ${e.event}\ndata: ${JSON.stringify(e.data)}\n\n`).join('')
 }

This produces event: foo\ndata: {...}\n\n per event, which matches the SSE spec where events are separated by blank lines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/universal-testing-utils/src/MockttpHelper.ts` around lines 40 -
42, The formatSseResponse function currently joins events with single newlines
which leaves the final event without the required blank-line separator; update
formatSseResponse so each serialized event ends with an extra newline (e.g.,
make each event string end with "\n\n" or append a final "\n" after join) so the
output yields a blank line after the last event to comply with the SSE spec.

199-201: Path resolution should match contract's pathResolver signature.

Same issue as in MswHelper: when requestPathParamsSchema is undefined, the contract's pathResolver is typed as () => string. Calling it with {} as any works but is semantically incorrect.

♻️ Proposed fix
     const path = contract.requestPathParamsSchema
       ? contract.pathResolver(pathParams)
-      : contract.pathResolver({} as any)
+      : (contract.pathResolver as () => string)()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/universal-testing-utils/src/MockttpHelper.ts` around lines 199 -
201, The path resolution currently calls contract.pathResolver({} as any) when
contract.requestPathParamsSchema is falsy, which mismatches the resolver's () =>
string signature; update the logic in MockttpHelper.ts to call
contract.pathResolver(pathParams) when contract.requestPathParamsSchema is
present and call contract.pathResolver() with no arguments when it is absent,
ensuring you reference the pathResolver and requestPathParamsSchema checks
around the pathParams variable to satisfy the correct function signature.
packages/app/universal-testing-utils/src/MswHelper.ts (1)

290-292: Path resolution should match contract's pathResolver signature.

When requestPathParamsSchema is undefined, the contract's pathResolver is typed as () => string (parameterless). Calling it with {} as any works but is semantically incorrect.

♻️ Proposed fix for cleaner path resolution
     const path = contract.requestPathParamsSchema
       ? contract.pathResolver(pathParams)
-      : contract.pathResolver({} as any)
+      : (contract.pathResolver as () => string)()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/universal-testing-utils/src/MswHelper.ts` around lines 290 -
292, The current call always invokes contract.pathResolver with an argument
(using {} as any when requestPathParamsSchema is absent), which violates the
resolver's typed signature; update the logic so that when
contract.requestPathParamsSchema is present you call
contract.pathResolver(pathParams), otherwise call contract.pathResolver() with
no arguments, keeping the use of contract.requestPathParamsSchema,
contract.pathResolver and pathParams to locate and fix the expression.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/app/api-contracts/src/sse/sseContractBuilders.types.spec.ts`:
- Around line 137-147: The variable contract created by buildSseContract is only
used in type-level assertions so the linter flags it as unused; rename it to a
deliberately-unused-prefixed identifier (e.g. _contract) or otherwise reference
it in a no-op runtime expression so the linter is satisfied—update the
declaration of contract in the it block (where buildSseContract is called)
accordingly and keep the existing expectTypeOf checks that reference that
identifier (e.g. _contract) so buildSseContract, contract/_contract, and
expectTypeOf usages remain aligned.

---

Nitpick comments:
In `@packages/app/universal-testing-utils/README.md`:
- Around line 336-443: The docs for SSE mock support are missing an example
demonstrating queryParams usage; add a concise example near the other
mockSseResponse snippets showing mockSseResponse called with a contract that
expects query params (referencing sseGetContractWithQueryParams from
test/testContracts.ts) and pass a queryParams object (e.g., { page: '1' }) in
the options for MockttpHelper and MswHelper calls so readers can discover query
param support alongside pathParams and responseCode; keep the example minimal
and consistent with the existing event array and formatSseResponse usage.

In `@packages/app/universal-testing-utils/src/MockttpHelper.ts`:
- Around line 40-42: The formatSseResponse function currently joins events with
single newlines which leaves the final event without the required blank-line
separator; update formatSseResponse so each serialized event ends with an extra
newline (e.g., make each event string end with "\n\n" or append a final "\n"
after join) so the output yields a blank line after the last event to comply
with the SSE spec.
- Around line 199-201: The path resolution currently calls
contract.pathResolver({} as any) when contract.requestPathParamsSchema is falsy,
which mismatches the resolver's () => string signature; update the logic in
MockttpHelper.ts to call contract.pathResolver(pathParams) when
contract.requestPathParamsSchema is present and call contract.pathResolver()
with no arguments when it is absent, ensuring you reference the pathResolver and
requestPathParamsSchema checks around the pathParams variable to satisfy the
correct function signature.

In `@packages/app/universal-testing-utils/src/MswHelper.ts`:
- Around line 290-292: The current call always invokes contract.pathResolver
with an argument (using {} as any when requestPathParamsSchema is absent), which
violates the resolver's typed signature; update the logic so that when
contract.requestPathParamsSchema is present you call
contract.pathResolver(pathParams), otherwise call contract.pathResolver() with
no arguments, keeping the use of contract.requestPathParamsSchema,
contract.pathResolver and pathParams to locate and fix the expression.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: lokalise/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3a19ddb4-ce57-40ce-8a02-f0c9239746a7

📥 Commits

Reviewing files that changed from the base of the PR and between 713caa7 and de603de.

📒 Files selected for processing (14)
  • packages/app/api-contracts/src/contractBuilder.types.spec.ts
  • packages/app/api-contracts/src/sse/dualModeContracts.ts
  • packages/app/api-contracts/src/sse/sseContractBuilders.spec.ts
  • packages/app/api-contracts/src/sse/sseContractBuilders.ts
  • packages/app/api-contracts/src/sse/sseContractBuilders.types.spec.ts
  • packages/app/api-contracts/src/sse/sseContracts.ts
  • packages/app/universal-testing-utils/README.md
  • packages/app/universal-testing-utils/package.json
  • packages/app/universal-testing-utils/src/MockttpHelper.spec.ts
  • packages/app/universal-testing-utils/src/MockttpHelper.ts
  • packages/app/universal-testing-utils/src/MswHelper.spec.ts
  • packages/app/universal-testing-utils/src/MswHelper.ts
  • packages/app/universal-testing-utils/src/index.ts
  • packages/app/universal-testing-utils/test/testContracts.ts
💤 Files with no reviewable changes (1)
  • packages/app/api-contracts/src/contractBuilder.types.spec.ts

Copy link
Copy Markdown
Contributor

@leonaves leonaves left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good :) can definitely make use of this!

@kibertoad kibertoad merged commit 999e08f into main Mar 17, 2026
8 checks passed
@kibertoad kibertoad deleted the feat/sse-helpers branch March 17, 2026 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants