Skip to content

Add Spector cases for serializing lossy duration encodings as integers#10835

Open
Copilot wants to merge 8 commits into
mainfrom
copilot/add-spector-case-serialization
Open

Add Spector cases for serializing lossy duration encodings as integers#10835
Copilot wants to merge 8 commits into
mainfrom
copilot/add-spector-case-serialization

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 29, 2026

Existing encode/duration scenarios only used whole-number payloads, so they couldn't catch the bug from #10831 where a duration with a fractional sub-second component was serialized as a floating point number (e.g. 123.45) instead of an integer.

These tests target the lossy encode case, where the source duration carries more precision than the target integer encoding can represent. The client must still serialize the value using the target number type (an integer), discarding the extra precision, rather than emitting a floating point number. This is distinct from arbitrary type mismatches, which are already covered by the existing round-trip scenarios.

Changes (packages/http-specs/specs/encode/duration/)

  • main.tsp: New dedicated Lossy namespace (routed under /encode/duration/lossy/...) containing the lossy-encode scenarios, instead of placing them under the regular Query/Property/Header encoding categories. For each transport (query, property, header) it adds:

    • int32-seconds: a 36.25-second duration encoded as int32 seconds (sub-second source precision).
    • int32-milliseconds: a 36250.25-millisecond duration encoded as int32 milliseconds (sub-millisecond source precision).
    @route("/int32-seconds")
    @scenario
    @scenarioDoc("""
      ...The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#.
      The client must serialize the value as an integer (not a floating point
      number such as `36.25`), discarding the sub-second precision.
      Because emitters may floor, round, or ceil when discarding that precision,
      the expected query parameter is `input=36` or `input=37`.
      """)
    op int32Seconds(
      @query @encode(DurationKnownEncoding.seconds, int32) input: duration,
    ): NoContentResponse;
  • mockapi.ts: New handlers (createLossyBodyServerTests, createLossyQueryServerTests, createLossyHeaderServerTests) verify that the received value is serialized as an integer (rejecting float/double/decimal output). They accept an allow-list of integers covering floor, round, and ceil results (36/37 for seconds, 36250/36251 for milliseconds), so the test verifies the core contract ("serializes as an int") without taking a position on an emitter's rounding mode.

  • spec-summary.md: Regenerated.

Note

The mock validation accepts floor/round/ceil results rather than a single exact value, so it remains agnostic to an emitter's rounding mode while still failing on floating point output.

Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
@microsoft-github-policy-service microsoft-github-policy-service Bot added the lib:http-specs For issues/prs related to the @typespec/http-specs package label May 29, 2026
Copilot AI changed the title [WIP] Add Spector case for serializing duration as integer Add Spector case for serializing fractional duration encoded as integer May 29, 2026
Copilot AI requested a review from JoshLove-msft May 29, 2026 02:28
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

from discussion on the issue I don't think we can add a test for this as floor vs round is not something TypeSpec should have an opinion here and it more of an emitter implementation detail

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.

The test can still validate that the proper number type is serialized (e.g. integer rather than double/decimal).

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the scenarios to validate only the number type. The mock handlers reject a floating point value and assert an integer is serialized, without checking an exact value. I also reworded the scenarioDocs to state that floor vs. round is an emitter implementation detail and that the test only validates that an integer is sent (e.g. 35 or 36). Done in 4b13fac.

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.

@copilot actually we can just use input data such that the rounding vs truncating decision wouldn't matter - e.g. 36.25.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

by that logic should we have a test to make sure you don't somehow send a string instead of a int as well? It just feels like this is one case you caught here but there is an infinite number of potential failures here

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

an emitter could also very well here decide that is this an error (crash) so can never get this test to work

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.

by that logic should we have a test to make sure you don't somehow send a string instead of a int as well?

These type violations are already covered by every round trip test. This case is special because the source type carries more precision than the target encoding. This is a lossy encode scenario - not arbitrary type mismatch.

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.

an emitter could also very well here decide that is this an error (crash) so can never get this test to work

The contract is "encode this duration as int32 seconds," so crashing is non-conformant behavior.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hhm alright fair enough, though do we need the same for milliseconds encoding?

…ding

Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
…t value

Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 1, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/http-specs@10835

commit: a96701d

@JoshLove-msft
Copy link
Copy Markdown
Contributor

@copilot run chronus add

Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jun 1, 2026

@copilot run chronus add

Added a changeset for @typespec/http-specs (changeKind: internal). chronus verify --since main now passes. Done in b9a6161.

@azure-sdk-automation
Copy link
Copy Markdown

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

@JoshLove-msft
Copy link
Copy Markdown
Contributor

@copilot add a line to the test description clarifying that this is specifically exercising the lossy encode scenario and not about arbitrary type mismatches.

Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jun 1, 2026

@copilot add a line to the test description clarifying that this is specifically exercising the lossy encode scenario and not about arbitrary type mismatches.

Added a line to each of the three scenario docs (Query, Property, Header) clarifying that this specifically exercises the lossy encode case where the source type carries more precision than the target encoding, and is not about arbitrary type mismatches. Done in the latest commit.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hhm alright fair enough, though do we need the same for milliseconds encoding?

This scenario specifically exercises the lossy encode case where the source type carries more precision than the target encoding; it is not about arbitrary type mismatches, which are already covered by other round-trip scenarios.
The value is chosen so that rounding and truncating both yield the same integer, so the expected query parameter is `input=36`.
""")
op int32SecondsFractional(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we move this (and potentially the milli test) to another category, this doesn't feel like this belongs under query encoding. Maybe we can have a loosy encoding test file dedicated to those kind of tests)

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.

The problem with doing this is that we still want to have header/query/property variants for the lossy encoding tests to exercise all code paths.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think we need all 3 I would just keep a single in a body.

value,
);
}
if (value !== 36) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

probably unlikely but an implementation could also choose ceil so we might want to allow 37 to be explicit that all of this is allowed.
An option for those kind of tests would also be to have a 2-phase test, first part you get a value(randomly generated for that server instance) maybe duration encoded as float then you got to send back that value.

@JoshLove-msft
Copy link
Copy Markdown
Contributor

@copilot please apply the feedback

…liseconds, allow ceil

Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jun 1, 2026

@copilot please apply the feedback

Applied the feedback:

  • Moved the lossy-encode scenarios out of Query/Property/Header into a dedicated Lossy namespace (/encode/duration/lossy/...), so they no longer live under the regular query encoding category.
  • Added matching int32-milliseconds scenarios (source value 36250.25 ms, sub-millisecond precision) alongside the int32-seconds ones (36.25 s), for query, property, and header.
  • The mock handlers now accept floor, round, or ceil results (e.g. 36/37 for seconds, 36250/36251 for ms) while still rejecting floating-point output, so the test doesn't take a position on an emitter's rounding mode.

Regenerated spec-summary.md and updated the changeset. validate-scenarios, validate-mock-apis, and client-server validation all pass.

Copilot AI changed the title Add Spector case for serializing fractional duration encoded as integer Add Spector cases for serializing lossy duration encodings as integers Jun 1, 2026
@JoshLove-msft
Copy link
Copy Markdown
Contributor

Error: test/e2e/http/encode/duration/main.test.ts(13,25): error TS2339: Property 'default_' does not exist on type 'QueryClient'.
Error: test/e2e/http/encode/duration/main.test.ts(18,25): error TS2339: Property 'iso8601' does not exist on type 'QueryClient'.
Error: test/e2e/http/encode/duration/main.test.ts(28,25): error TS2339: Property 'floatSeconds' does not exist on type 'QueryClient'.
Error: test/e2e/http/encode/duration/main.test.ts(33,25): error TS2339: Property 'float64Seconds' does not exist on type 'QueryClient'.
Error: test/e2e/http/encode/duration/main.test.ts(38,25): error TS2551: Property 'int32SecondsArray' does not exist on type 'QueryClient'. Did you mean 'int32Seconds'?
Error: test/e2e/http/encode/duration/main.test.ts(53,45): error TS2339: Property 'default_' does not exist on type 'PropertyClient'.
Error: test/e2e/http/encode/duration/main.test.ts(59,45): error TS2339: Property 'iso8601' does not exist on type 'PropertyClient'.
Error: test/e2e/http/encode/duration/main.test.ts(71,45): error TS2339: Property 'floatSeconds' does not exist on type 'PropertyClient'.
Error: test/e2e/http/encode/duration/main.test.ts(77,45): error TS2339: Property 'float64Seconds' does not exist on type 'PropertyClient'.
Error: test/e2e/http/encode/duration/main.test.ts(83,45): error TS2339: Property 'floatSecondsArray' does not exist on type 'PropertyClient'.
Error: test/e2e/http/encode/duration/main.test.ts(97,26): error TS2339: Property 'default_' does not exist on type 'HeaderClient'.
Error: test/e2e/http/encode/duration/main.test.ts(102,26): error TS2339: Property 'iso8601' does not exist on type 'HeaderClient'.
Error: test/e2e/http/encode/duration/main.test.ts(107,26): error TS2339: Property 'iso8601Array' does not exist on type 'HeaderClient'.
Error: test/e2e/http/encode/duration/main.test.ts(117,26): error TS2339: Property 'floatSeconds' does not exist on type 'HeaderClient'.
Error: test/e2e/http/encode/duration/main.test.ts(122,26): error TS2339: Property 'float64Seconds' does not exist on type 'HeaderClient'.

@copilot

…y ops @post

Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jun 1, 2026

Error: test/e2e/http/encode/duration/main.test.ts(13,25): error TS2339: Property 'default_' does not exist on type 'QueryClient'.
Error: test/e2e/http/encode/duration/main.test.ts(18,25): error TS233...

Fixed. The failure was a client-name collision: the Lossy namespace nested sub-namespaces named Query/Property/Header, and the http-client-js emitter derives client names from the namespace leaf name, so the duplicate QueryClient/PropertyClient/HeaderClient exports overwrote the originals (leaving only the lossy ops). I flattened Lossy into a single namespace with distinct operation names (queryInt32Seconds, propertyInt32Seconds, headerInt32Seconds, plus the milliseconds variants) so it generates one LossyClient and the existing clients keep all their operations. I also added @post to the lossy property ops so the endpoint method matches the mock. Regenerated spec-summary.md; validate-scenarios, validate-mock-apis, and the client/server knock all pass.

The client must serialize the value as an integer (not a floating point number such as `36.25`), discarding the sub-second precision.
Because emitters may floor, round, or ceil when discarding that precision, the expected query parameter is `input=36` or `input=37`.
""")
op queryInt32Seconds(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am not sure we really need for header, property, query, etc.

I think we should just need 2 tests

interface Lossy {
  op intSeconds(
    @query
    @encode(DurationKnownEncoding.seconds, int32)
    input: duration
   ): void;

  op intMilliseconds(
    @query
    @encode(DurationKnownEncoding.milliseconds, int32)
    input: duration
   ): void;
}

with the http route matching

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lib:http-specs For issues/prs related to the @typespec/http-specs package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Spector Case for Serializing Duration encoded as integer

3 participants