Skip to content

Commit de5ebab

Browse files
fix: improve SSE conformance test validation (SEP-1699)
Enhance timing validation and event ordering checks: Client test (sse-retry): - Add "very late" threshold (>2x retry value) as FAILURE - Distinguish between slightly late (WARNING) and very late (FAILURE) Server test (sse-polling): - Check that priming event is sent immediately (first event) - Warn if server disconnects without sending event ID first
1 parent be3d929 commit de5ebab

File tree

2 files changed

+53
-15
lines changed

2 files changed

+53
-15
lines changed

src/scenarios/client/sse-retry.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export class SSERetryScenario implements Scenario {
2626
// Tolerances for timing validation
2727
private readonly EARLY_TOLERANCE = 50; // Allow 50ms early for scheduler variance
2828
private readonly LATE_TOLERANCE = 200; // Allow 200ms late for network/event loop
29+
private readonly VERY_LATE_MULTIPLIER = 2; // If >2x retry value, client is likely ignoring it
2930

3031
async start(): Promise<ScenarioUrls> {
3132
return new Promise((resolve, reject) => {
@@ -237,8 +238,10 @@ export class SSERetryScenario implements Scenario {
237238
const maxExpected = this.retryValue + this.LATE_TOLERANCE;
238239

239240
const tooEarly = actualDelay < minExpected;
240-
const tooLate = actualDelay > maxExpected;
241-
const withinTolerance = !tooEarly && !tooLate;
241+
const slightlyLate = actualDelay > maxExpected;
242+
const veryLate =
243+
actualDelay > this.retryValue * this.VERY_LATE_MULTIPLIER;
244+
const withinTolerance = !tooEarly && !slightlyLate;
242245

243246
let status: 'SUCCESS' | 'FAILURE' | 'WARNING' = 'SUCCESS';
244247
let errorMessage: string | undefined;
@@ -247,10 +250,14 @@ export class SSERetryScenario implements Scenario {
247250
// Client reconnected too soon - MUST violation
248251
status = 'FAILURE';
249252
errorMessage = `Client reconnected too early (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). Client MUST respect the retry field and wait the specified time.`;
250-
} else if (tooLate) {
251-
// Client reconnected too late - not a spec violation but suspicious
253+
} else if (veryLate) {
254+
// Client reconnected way too late - likely ignoring retry field entirely
255+
status = 'FAILURE';
256+
errorMessage = `Client reconnected very late (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). Client appears to be ignoring the retry field and using its own backoff strategy.`;
257+
} else if (slightlyLate) {
258+
// Client reconnected slightly late - not a spec violation but suspicious
252259
status = 'WARNING';
253-
errorMessage = `Client reconnected late (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). This is acceptable but may indicate the client is ignoring the retry field and using its own backoff.`;
260+
errorMessage = `Client reconnected slightly late (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). This is acceptable but may indicate network delays.`;
254261
}
255262

256263
this.checks.push({
@@ -272,11 +279,13 @@ export class SSERetryScenario implements Scenario {
272279
actualDelayMs: Math.round(actualDelay),
273280
minAcceptableMs: minExpected,
274281
maxAcceptableMs: maxExpected,
282+
veryLateThresholdMs: this.retryValue * this.VERY_LATE_MULTIPLIER,
275283
earlyToleranceMs: this.EARLY_TOLERANCE,
276284
lateToleranceMs: this.LATE_TOLERANCE,
277285
withinTolerance,
278286
tooEarly,
279-
tooLate,
287+
slightlyLate,
288+
veryLate,
280289
connectionCount: this.connectionTimestamps.length
281290
}
282291
});

src/scenarios/server/sse-polling.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export class ServerSSEPollingScenario implements ClientScenario {
7171
// Parse SSE stream
7272
let hasEventId = false;
7373
let hasPrimingEvent = false;
74+
let primingEventIsFirst = false;
7475
let hasRetryField = false;
7576
let retryValue: number | undefined;
7677
let firstEventId: string | undefined;
@@ -130,6 +131,10 @@ export class ServerSSEPollingScenario implements ClientScenario {
130131
event.data.trim() === ''
131132
) {
132133
hasPrimingEvent = true;
134+
// Check if priming event is the first event (SEP-1699 says "immediately")
135+
if (eventCount === 1) {
136+
primingEventIsFirst = true;
137+
}
133138
}
134139
}
135140

@@ -143,13 +148,26 @@ export class ServerSSEPollingScenario implements ClientScenario {
143148
clearTimeout(timeout);
144149
}
145150

146-
// Check 1: Server SHOULD send priming event with ID
151+
// Check 1: Server SHOULD send priming event with ID immediately
152+
let primingStatus: 'SUCCESS' | 'WARNING' = 'SUCCESS';
153+
let primingErrorMessage: string | undefined;
154+
155+
if (!hasPrimingEvent) {
156+
primingStatus = 'WARNING';
157+
primingErrorMessage =
158+
'Server did not send priming event with id and empty data. This is a SHOULD requirement for SEP-1699.';
159+
} else if (!primingEventIsFirst) {
160+
primingStatus = 'WARNING';
161+
primingErrorMessage =
162+
'Priming event was not sent immediately (not the first event). SEP-1699 says server SHOULD immediately send the priming event.';
163+
}
164+
147165
checks.push({
148166
id: 'server-sse-priming-event',
149167
name: 'ServerSendsPrimingEvent',
150168
description:
151-
'Server SHOULD send SSE event with id and empty data to prime client for reconnection',
152-
status: hasPrimingEvent ? 'SUCCESS' : 'WARNING',
169+
'Server SHOULD immediately send SSE event with id and empty data to prime client for reconnection',
170+
status: primingStatus,
153171
timestamp: new Date().toISOString(),
154172
specReferences: [
155173
{
@@ -159,13 +177,12 @@ export class ServerSSEPollingScenario implements ClientScenario {
159177
],
160178
details: {
161179
hasPrimingEvent,
180+
primingEventIsFirst,
162181
hasEventId,
163182
firstEventId,
164183
eventCount
165184
},
166-
errorMessage: !hasPrimingEvent
167-
? 'Server did not send priming event with id and empty data. This is a SHOULD requirement for SEP-1699.'
168-
: undefined
185+
errorMessage: primingErrorMessage
169186
});
170187

171188
// Check 2: Server SHOULD send retry field before disconnect
@@ -191,13 +208,23 @@ export class ServerSSEPollingScenario implements ClientScenario {
191208
: undefined
192209
});
193210

194-
// Check 3: Server MAY close connection (informational)
211+
// Check 3: Server MAY close connection after sending event ID
212+
// Per SEP-1699, server can only close "if it has sent an SSE event with an event ID"
213+
let disconnectStatus: 'SUCCESS' | 'WARNING' | 'INFO' = 'INFO';
214+
let disconnectMessage: string | undefined;
215+
216+
if (disconnected && !hasEventId) {
217+
disconnectStatus = 'WARNING';
218+
disconnectMessage =
219+
'Server closed connection without sending an event ID first. SEP-1699 allows disconnect only after sending event ID.';
220+
}
221+
195222
checks.push({
196223
id: 'server-sse-disconnect',
197224
name: 'ServerDisconnectBehavior',
198225
description:
199226
'Server MAY close connection after sending event ID (informational)',
200-
status: 'INFO',
227+
status: disconnectStatus,
201228
timestamp: new Date().toISOString(),
202229
specReferences: [
203230
{
@@ -207,10 +234,12 @@ export class ServerSSEPollingScenario implements ClientScenario {
207234
],
208235
details: {
209236
disconnected,
237+
hasEventId,
210238
eventCount,
211239
hasRetryField,
212240
retryValue
213-
}
241+
},
242+
errorMessage: disconnectMessage
214243
});
215244
} catch (error) {
216245
checks.push({

0 commit comments

Comments
 (0)