Skip to content

test: Cover FDv2 request shapes in instance-id and tags tests#340

Merged
keelerm84 merged 10 commits into
feat/fdv2from
mkeeler/SDK-2372/fdv2-header-coverage
Jun 1, 2026
Merged

test: Cover FDv2 request shapes in instance-id and tags tests#340
keelerm84 merged 10 commits into
feat/fdv2from
mkeeler/SDK-2372/fdv2-header-coverage

Conversation

@keelerm84
Copy link
Copy Markdown
Member

@keelerm84 keelerm84 commented May 15, 2026

Summary

CommonInstanceIDTests and CommonTagsTests previously verified the X-LaunchDarkly-Instance-Id and X-LaunchDarkly-Tags headers only on a single streaming synchronizer, a single polling synchronizer, and the events endpoint. FDv2 introduced three new request shapes that the suite did not exercise:

  • Polling Initializers that run before the synchronizer (DataSystemOptionPollingInitializer).
  • Secondary Synchronizers reached after the Primary is permanently removed (the FDv2 fallback chain in CommonStreamingTests.FDv2).
  • The FDv1 Fallback Synchronizer reached via the server-directed FDv1 Fallback Directive (feat: Add FDv1 Fallback Directive tests and config #336).

A regression that dropped either header from any of these new request shapes would not have been caught by the existing tests.

This PR adds three subtests to each of the two files, standing up each request shape and asserting the relevant header is present:

  • polling initializer requests -- builds a data system with a polling initializer plus a no-op (xfer-none / up-to-date) synchronizer; asserts on the initializer endpoint.
  • secondary synchronizer requests after permanent fallback -- primary returns 401 (non-recoverable, immediate permanent removal); secondary serves an empty FDv2 stream; asserts on the secondary endpoint.
  • FDv1 fallback directive requests -- FDv2 streaming returns 403 + X-LD-FD-Fallback: true; FDv1 endpoint serves an empty {"flags":{},"segments":{}} body so init completes along the fallback path; asserts on the FDv1 endpoint.

The new subtests are server-side only -- the FDv2 features they exercise are server-side today, matching the gating used in server_side_stream_all.go. The FDv1 directive subtest is additionally gated on CapabilityFDv1Fallback so SDKs that have not yet implemented the directive can opt out.

The tags variant uses one representative tags config for the new subtests rather than iterating over makeValidTagsTestParams -- endpoint coverage and tag-value variation are orthogonal properties, and the existing subtests already cover the latter.


Note

Low Risk
Test-only changes to the SDK test harness; no production SDK or service behavior is modified.

Overview
Expands the SDK test harness so X-LaunchDarkly-Instance-Id and X-LaunchDarkly-Tags are verified on FDv2-related request paths that were previously untested (streaming/poll/events only).

Instance ID: Replaces one-off header checks with newInstanceIDChecker, which requires a non-empty header and the same value across every endpoint touched by one client. Adds a subtest that distinct clients get distinct instance IDs (skipped when CapabilitySingleton). Adds server-side subtests for polling initializer, secondary synchronizer after permanent 401 fallback, and FDv1 fallback directive (gated on CapabilityFDv1Fallback), including assertions that abandoned endpoints stop receiving traffic.

Tags: Mirrors the same three FDv2 request-shape subtests using one representative tags config (endpoint coverage vs tag permutations).

Shared: Adds emptyFDv1FallbackBody on commonTestsBase so FDv1 fallback mocks return server- vs client-side-shaped empty JSON for init-only scenarios.

Reviewed by Cursor Bugbot for commit 48e0fd9. Bugbot is set up for automated code reviews on this repo. Configure here.

keelerm84 added 4 commits May 13, 2026 15:49
CommonInstanceIDTests and CommonTagsTests previously asserted the
X-LaunchDarkly-Instance-Id and X-LaunchDarkly-Tags headers only on a
single streaming synchronizer, a single polling synchronizer, and the
events endpoint. FDv2 introduces three new request shapes that the
suite did not exercise: polling Initializers that run before the
synchronizer, Secondary Synchronizers reached after the Primary is
permanently removed, and the FDv1 Fallback Synchronizer reached via
the server-directed FDv1 Fallback Directive.

Add subtests in both files that stand up each new request shape and
assert the relevant header is present. The new subtests are
server-side only, since the FDv2 features they exercise are
server-side today; the FDv1 directive subtest is additionally gated
on CapabilityFDv1Fallback so SDKs that have not yet implemented the
directive can opt out.

The tags variant uses one representative tags config for the new
subtests rather than iterating over makeValidTagsTestParams --
endpoint coverage and tag-value variation are orthogonal properties.
Each subtest in CommonInstanceIDTests previously asserted only that
the header was non-empty. Two endpoints in the same subtest could
return different non-empty values and the test would still pass,
which would mask a regression where the SDK assigns a new instance-id
per request shape (or, for example, uses a separate HTTP transport
for events vs data and forgets to plumb the shared instance-id
through).

Replace the per-call non-empty check with a per-subtest checker that
latches the first observed value and asserts every subsequent request
carries the same value. The checker is scoped to one SDK client
lifetime: each subtest constructs its own checker, since each subtest
creates its own client and so legitimately receives a different
instance-id.

Where the SDK naturally contacts more than one endpoint in a subtest,
check both:
- event posts: the SDK hits the data source during init and the
  events endpoint on flush.
- polling initializer requests: initializer runs before the
  synchronizer.
- secondary synchronizer requests: primary receives the 401 before
  the SDK falls through to the secondary.
- FDv1 fallback directive requests: FDv2 stream returns the 403 +
  directive before the SDK transitions to the FDv1 endpoint.

Tags tests already verify consistency implicitly by comparing every
endpoint against the same static expectedHeaderValue, so no change
there.
The earlier consistency check guarantees that within one SDK client
all requests carry the same instance-id. The complementary property
-- that two distinct clients in the same process produce different
instance-ids -- was not covered. Without that check, an SDK that
seeded the instance-id from process-level state and reused it across
client instances would still pass every existing assertion, even
though the resulting telemetry would be unable to distinguish the
clients.

Add a subtest that stands up two independent SDK clients back to
back, captures the instance-id from each, and asserts the values
differ. Gated on !CapabilitySingleton since the test creates a
second client while the first still exists.
…subtests

Multi-agent review caught a PROVEN bug in the tags FDv1 fallback
subtest. The configurer list was

  withTagsConfig(fdv2TagParams.tags),
  WithConfig({StartWaitTimeMS: 5000}),
  WithStreamingSynchronizer(...),
  WithFDv1Fallback(...),

WithConfig performs *configOut = config, which overwrites every field
previously set -- including Tags. The SDK would have been started
without tags configured, would not have sent X-LaunchDarkly-Tags on
any request, and the assertion against the expected header value
would have failed every run.

Replace WithConfig with WithWaitToStart, which mutates only
StartWaitTimeMS and InitCanFail. The same substitution is applied to
the instance-id FDv1 fallback subtest; the bug was latent there
(WithConfig was the first option in the chain), but the pattern is
the same fragility waiting for a future option to be prepended.

While in the area, bring tags FDv2 subtests up to instance-id parity
by checking both endpoints exercised in each flow:
- polling initializer: initializer + synchronizer
- secondary synchronizer: primary + secondary
- FDv1 fallback: FDv2 stream + FDv1
@keelerm84 keelerm84 marked this pull request as ready for review May 15, 2026 20:24
@keelerm84 keelerm84 requested a review from a team as a code owner May 15, 2026 20:24
Comment thread sdktests/common_tests_instance_id.go
Comment thread sdktests/common_tests_instance_id.go
// disambiguate them. Stand up two independent clients back to back and
// assert their instance-ids differ. Gated on !CapabilitySingleton since
// the test requires creating a second client while the first still exists.
if !t.Capabilities().Has(servicedef.CapabilitySingleton) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not to be that guy, but an SDK could have the ability to make a singleton and the ability to make instances. I supposed we don't do that nor do we expect to do that?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In that case, the SDK wouldn't specify a capability of singleton like this. It's one of the earliest capabilities, and just designates SDKs that can ONLY create a single instance at a time, not that it has the ability to create a singleton. It should have been CapabilityMultiClient or something instead.

Comment thread sdktests/common_tests_tags.go Outdated
}
fdv2TagParams.expectedHeaderValue = c.makeExpectedTagsHeader(fdv2TagParams.tags)

if !c.isClientSide {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a way to positively identify the category instead of negatively? Then if another type is added, this doesn't start running accidentally.

Address Cursor Bugbot feedback on the new FDv2 header-coverage subtests:
the secondary-synchronizer and FDv1-fallback subtests verified headers on
both endpoints but never asserted the abandoned source stops receiving
connections. An SDK that falls through to the secondary/FDv1 path but keeps
retrying the removed primary/FDv2 stream in the background would have passed.

Add RequireNoMoreConnections on the abandoned endpoint at the end of all four
subtests, matching the pattern in DirectiveOnStreamingErrorEngagesFDv1.
Comment thread sdktests/common_tests_tags.go Outdated
keelerm84 added 3 commits June 1, 2026 12:27
Replace the negative `!c.isClientSide` guard on the FDv2 request-shape
subtests with the positive `c.sdkKind.IsServerSide()`. Functionally
identical today (IsClientSide is defined as !IsServerSide), but states intent
directly and means a future SDK category must opt into these server-side-only
shapes explicitly rather than inheriting them by default.

Addresses review feedback from tanderson-ld and Cursor Bugbot. There is no
FDv2/data-system capability to gate on; FDv2 support is assumed for all
server-side SDKs, matching how doServerSideFDv2StreamTests is gated.
feat/fdv2 (#325) reworked the mock data model: ServerSDKDataBuilder no
longer carries IntentCode/IntentReason, and the streaming/polling mock
services now require FDv2-format data instead of auto-converting plain
ServerSDKData. After merging the updated base, port the new subtests:

- Build the no-op synchronizer via FDv2SDKDataFromServerSDKData(..., "none",
  "up-to-date", "initial") instead of the removed builder methods.
- Serve the secondary streaming synchronizer a valid FDv2 "xfer-full" basis
  so the SDK can initialize from it; raw ServerSDKData now makes the mock
  streamer reject the request ("cannot handle non-fdv2 sdk data") and init
  times out.

Verified against go-server-sdk (v7) and ruby-server-sdk (main) contract
test services: all instance-id and tags subtests pass.
Comment thread sdktests/common_tests_instance_id.go
The existing FDv1 fallback tests in common_tests_stream_fdv2.go pair
WithFDv1Fallback with WithServiceEndpoints so the top-level streaming and
polling endpoints also point at the mocks. The new FDv1 fallback directive
subtests omitted it, so an SDK that resolves the FDv1 polling URL from
ServiceEndpoints.Polling (rather than DataSystem.FDv1Fallback.BaseURI) would
contact the real LaunchDarkly endpoint and time out. Add WithServiceEndpoints
to match the established convention.

Addresses Cursor Bugbot feedback. Re-verified against go-server-sdk (v7) and
ruby-server-sdk (main): all instance-id and tags subtests pass.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5731505. Configure here.

Comment thread sdktests/common_tests_instance_id.go
The FDv1 fallback directive subtests are gated on CapabilityFDv1Fallback but
not on server-side, yet they hardcoded the server-side FDv1 polling body
({"flags":..,"segments":..}). Client-side SDKs use a flat map of evaluations,
so a client-side SDK claiming the capability would receive an unparseable
payload and fail to initialize.

Client-side support for the FDv1 Fallback Directive is in progress, so rather
than gating these subtests to server-side only, add a commonTestsBase
emptyFDv1FallbackBody helper that returns an empty payload in the format the
SDK kind expects, and serve that. Keeps the capability-only gate valid for both
kinds, matching the format-aware fdv1FallbackBody helper in
common_tests_stream_fdv2.go.

Addresses Cursor Bugbot feedback. Re-verified against go-server-sdk (v7) and
ruby-server-sdk (main): all instance-id and tags subtests pass.
@keelerm84 keelerm84 merged commit f235ce1 into feat/fdv2 Jun 1, 2026
6 checks passed
@keelerm84 keelerm84 deleted the mkeeler/SDK-2372/fdv2-header-coverage branch June 1, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants