From f1ccfbeadce45f580cd42f81c3a2130e8a42eac0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 02:15:38 +0000 Subject: [PATCH 1/8] Initial plan From fa48c47ec23be95bc0393a7a7ac3e546f0d10f65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 02:27:39 +0000 Subject: [PATCH 2/8] Add fractional duration int32-seconds spector scenarios Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- packages/http-specs/spec-summary.md | 41 +++++++ .../http-specs/specs/encode/duration/main.tsp | 56 ++++++++++ .../specs/encode/duration/mockapi.ts | 100 ++++++++++++++++++ 3 files changed, 197 insertions(+) diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index c623a91cc95..d50e47264f1 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -888,6 +888,15 @@ Expected header `duration: 180000` Test int32 seconds encode for a duration header. Expected header `duration: 36` +### Encode_Duration_Header_int32SecondsFractional + +- Endpoint: `get /encode/duration/header/int32-seconds-fractional` + +Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. +The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. +Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number), e.g. `duration: 36`. +Expected header to be an integer (the fractional part is dropped during serialization). + ### Encode_Duration_Header_int32SecondsLargerUnit - Endpoint: `get /encode/duration/header/int32-seconds-larger-unit` @@ -1165,6 +1174,29 @@ Expected response body: } ``` +### Encode_Duration_Property_int32SecondsFractional + +- Endpoint: `get /encode/duration/property/int32-seconds-fractional` + +Test operation with request and response model contains a duration property with int32 seconds encode whose value has a fractional (sub-second) component. +The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. +Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number). +Expected request body: + +```json +{ + "value": 36 +} +``` + +Expected response body: + +```json +{ + "value": 36 +} +``` + ### Encode_Duration_Property_int32SecondsLargerUnit - Endpoint: `get /encode/duration/property/int32-seconds-larger-unit` @@ -1295,6 +1327,15 @@ Expected query parameter `input=36` Test int32 seconds encode for a duration array parameter. Expected query parameter `input=36,47` +### Encode_Duration_Query_int32SecondsFractional + +- Endpoint: `get /encode/duration/query/int32-seconds-fractional` + +Test int32 seconds encode for a duration parameter whose value has a fractional (sub-second) component. +The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. +Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number), e.g. `input=36`. +Expected query parameter to be an integer (the fractional part is dropped during serialization). + ### Encode_Duration_Query_int32SecondsLargerUnit - Endpoint: `get /encode/duration/query/int32-seconds-larger-unit` diff --git a/packages/http-specs/specs/encode/duration/main.tsp b/packages/http-specs/specs/encode/duration/main.tsp index cb0cf16aee0..e755137a93c 100644 --- a/packages/http-specs/specs/encode/duration/main.tsp +++ b/packages/http-specs/specs/encode/duration/main.tsp @@ -45,6 +45,20 @@ namespace Query { input: duration, ): NoContentResponse; + @route("/int32-seconds-fractional") + @scenario + @scenarioDoc(""" + Test int32 seconds encode for a duration parameter whose value has a fractional (sub-second) component. + The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. + Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number), e.g. `input=36`. + Expected query parameter to be an integer (the fractional part is dropped during serialization). + """) + op int32SecondsFractional( + @query + @encode(DurationKnownEncoding.seconds, int32) + input: duration, + ): NoContentResponse; + @route("/int32-seconds-larger-unit") @scenario @scenarioDoc(""" @@ -202,6 +216,11 @@ namespace Property { value: duration; } + model Int32SecondsFractionalDurationProperty { + @encode(DurationKnownEncoding.seconds, int32) + value: duration; + } + model FloatSecondsDurationProperty { @encode(DurationKnownEncoding.seconds, float) value: duration; @@ -320,6 +339,29 @@ namespace Property { """) op int32Seconds(@body body: Int32SecondsDurationProperty): Int32SecondsDurationProperty; + @route("/int32-seconds-fractional") + @scenario + @scenarioDoc(""" + Test operation with request and response model contains a duration property with int32 seconds encode whose value has a fractional (sub-second) component. + The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. + Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number). + Expected request body: + ```json + { + "value": 36 + } + ``` + Expected response body: + ```json + { + "value": 36 + } + ``` + """) + op int32SecondsFractional( + @body body: Int32SecondsFractionalDurationProperty, + ): Int32SecondsFractionalDurationProperty; + @route("/float-seconds") @scenario @scenarioDoc(""" @@ -603,6 +645,20 @@ namespace Header { duration: duration, ): NoContentResponse; + @route("/int32-seconds-fractional") + @scenario + @scenarioDoc(""" + Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. + The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. + Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number), e.g. `duration: 36`. + Expected header to be an integer (the fractional part is dropped during serialization). + """) + op int32SecondsFractional( + @header + @encode(DurationKnownEncoding.seconds, int32) + duration: duration, + ): NoContentResponse; + @route("/int32-seconds-larger-unit") @scenario @scenarioDoc(""" diff --git a/packages/http-specs/specs/encode/duration/mockapi.ts b/packages/http-specs/specs/encode/duration/mockapi.ts index c74a77d300b..5013dc544da 100644 --- a/packages/http-specs/specs/encode/duration/mockapi.ts +++ b/packages/http-specs/specs/encode/duration/mockapi.ts @@ -23,6 +23,36 @@ function createBodyServerTests(uri: string, data: any, value: any) { kind: "MockApiDefinition", }); } + +// Validates that a duration with a fractional (sub-second) component is serialized as an integer. +function createBodyIntServerTests(uri: string) { + return passOnSuccess({ + uri, + method: "post", + request: { + body: json({ value: 36 }), + }, + response: { + status: 200, + body: json({ value: 36 }), + }, + handler: (req: MockRequest) => { + const value = req.body?.value; + if (typeof value !== "number" || !Number.isInteger(value)) { + throw new ValidationError( + `Expected body property "value" to be serialized as an integer but got ${value}`, + "an integer", + value, + ); + } + return { + status: 200, + body: json({ value }), + }; + }, + kind: "MockApiDefinition", + }); +} Scenarios.Encode_Duration_Property_default = createBodyServerTests( "/encode/duration/property/default", { @@ -51,6 +81,9 @@ Scenarios.Encode_Duration_Property_int32Seconds = createBodyServerTests( }, 36, ); +Scenarios.Encode_Duration_Property_int32SecondsFractional = createBodyIntServerTests( + "/encode/duration/property/int32-seconds-fractional", +); Scenarios.Encode_Duration_Property_iso8601 = createBodyServerTests( "/encode/duration/property/iso8601", { @@ -175,6 +208,34 @@ function createQueryFloatServerTests(uri: string, paramData: any, value: number) kind: "MockApiDefinition", }); } + +// Validates that a duration with a fractional (sub-second) component is serialized as an integer. +function createQueryIntServerTests(uri: string, paramData: any) { + return passOnSuccess({ + uri, + method: "get", + request: { + query: paramData, + }, + response: { + status: 204, + }, + handler: (req: MockRequest) => { + const actual = req.query["input"] as string; + if (!/^[-+]?\d+$/.test(actual)) { + throw new ValidationError( + `Expected query param input to be serialized as an integer but got ${actual}`, + "an integer", + actual, + ); + } + return { + status: 204, + }; + }, + kind: "MockApiDefinition", + }); +} Scenarios.Encode_Duration_Query_default = createQueryServerTests( "/encode/duration/query/default", { @@ -196,6 +257,12 @@ Scenarios.Encode_Duration_Query_int32Seconds = createQueryServerTests( }, "36", ); +Scenarios.Encode_Duration_Query_int32SecondsFractional = createQueryIntServerTests( + "/encode/duration/query/int32-seconds-fractional", + { + input: 36, + }, +); Scenarios.Encode_Duration_Query_int32SecondsArray = createQueryServerTests( "/encode/duration/query/int32-seconds-array", { @@ -321,6 +388,36 @@ function createHeaderFloatServerTests(uri: string, value: number) { }); } +// Validates that a duration with a fractional (sub-second) component is serialized as an integer. +function createHeaderIntServerTests(uri: string) { + return passOnSuccess({ + uri, + method: "get", + request: { + headers: { + duration: "36", + }, + }, + response: { + status: 204, + }, + handler: (req: MockRequest) => { + const actual = req.headers["duration"]; + if (!/^[-+]?\d+$/.test(actual)) { + throw new ValidationError( + `Expected header duration to be serialized as an integer but got ${actual}`, + "an integer", + actual, + ); + } + return { + status: 204, + }; + }, + kind: "MockApiDefinition", + }); +} + Scenarios.Encode_Duration_Header_default = createHeaderServerTests( "/encode/duration/header/default", { @@ -342,6 +439,9 @@ Scenarios.Encode_Duration_Header_int32Seconds = createHeaderServerTests( }, "36", ); +Scenarios.Encode_Duration_Header_int32SecondsFractional = createHeaderIntServerTests( + "/encode/duration/header/int32-seconds-fractional", +); Scenarios.Encode_Duration_Header_floatSeconds = createHeaderServerTests( "/encode/duration/header/float-seconds", { From 9aabc3f1d4cf789e5bd92cf58dd454cfbc4b51e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:25:41 +0000 Subject: [PATCH 3/8] Clarify fractional duration scenarios validate integer type, not rounding Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- packages/http-specs/spec-summary.md | 26 +++++-------------- .../http-specs/specs/encode/duration/main.tsp | 23 +++++----------- 2 files changed, 12 insertions(+), 37 deletions(-) diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index d50e47264f1..92bc3a9a58e 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -894,8 +894,8 @@ Expected header `duration: 36` Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. -Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number), e.g. `duration: 36`. -Expected header to be an integer (the fractional part is dropped during serialization). +Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `35.625`). +Whether the fractional part is rounded or truncated is an emitter implementation detail; the test only validates that an integer is sent (e.g. `duration: 35` or `duration: 36`). ### Encode_Duration_Header_int32SecondsLargerUnit @@ -1180,22 +1180,8 @@ Expected response body: Test operation with request and response model contains a duration property with int32 seconds encode whose value has a fractional (sub-second) component. The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. -Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number). -Expected request body: - -```json -{ - "value": 36 -} -``` - -Expected response body: - -```json -{ - "value": 36 -} -``` +Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `35.625`). +Whether the fractional part is rounded or truncated is an emitter implementation detail; the test only validates that an integer is sent (e.g. `35` or `36`). ### Encode_Duration_Property_int32SecondsLargerUnit @@ -1333,8 +1319,8 @@ Expected query parameter `input=36,47` Test int32 seconds encode for a duration parameter whose value has a fractional (sub-second) component. The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. -Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number), e.g. `input=36`. -Expected query parameter to be an integer (the fractional part is dropped during serialization). +Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `35.625`). +Whether the fractional part is rounded or truncated is an emitter implementation detail; the test only validates that an integer is sent (e.g. `input=35` or `input=36`). ### Encode_Duration_Query_int32SecondsLargerUnit diff --git a/packages/http-specs/specs/encode/duration/main.tsp b/packages/http-specs/specs/encode/duration/main.tsp index e755137a93c..6c1fd062de0 100644 --- a/packages/http-specs/specs/encode/duration/main.tsp +++ b/packages/http-specs/specs/encode/duration/main.tsp @@ -50,8 +50,8 @@ namespace Query { @scenarioDoc(""" Test int32 seconds encode for a duration parameter whose value has a fractional (sub-second) component. The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. - Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number), e.g. `input=36`. - Expected query parameter to be an integer (the fractional part is dropped during serialization). + Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `35.625`). + Whether the fractional part is rounded or truncated is an emitter implementation detail; the test only validates that an integer is sent (e.g. `input=35` or `input=36`). """) op int32SecondsFractional( @query @@ -344,19 +344,8 @@ namespace Property { @scenarioDoc(""" Test operation with request and response model contains a duration property with int32 seconds encode whose value has a fractional (sub-second) component. The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. - Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number). - Expected request body: - ```json - { - "value": 36 - } - ``` - Expected response body: - ```json - { - "value": 36 - } - ``` + Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `35.625`). + Whether the fractional part is rounded or truncated is an emitter implementation detail; the test only validates that an integer is sent (e.g. `35` or `36`). """) op int32SecondsFractional( @body body: Int32SecondsFractionalDurationProperty, @@ -650,8 +639,8 @@ namespace Header { @scenarioDoc(""" Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. - Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number), e.g. `duration: 36`. - Expected header to be an integer (the fractional part is dropped during serialization). + Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `35.625`). + Whether the fractional part is rounded or truncated is an emitter implementation detail; the test only validates that an integer is sent (e.g. `duration: 35` or `duration: 36`). """) op int32SecondsFractional( @header From fa87ef47186c343946deaa10af4289b46198124f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:34:49 +0000 Subject: [PATCH 4/8] Use 36.25s duration so rounding/truncating both yield 36, assert exact value Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- packages/http-specs/spec-summary.md | 33 ++++++++++++++----- .../http-specs/specs/encode/duration/main.tsp | 30 ++++++++++++----- .../specs/encode/duration/mockapi.ts | 16 +++++++++ 3 files changed, 61 insertions(+), 18 deletions(-) diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 92bc3a9a58e..a672f6ee5f9 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -893,9 +893,9 @@ Expected header `duration: 36` - Endpoint: `get /encode/duration/header/int32-seconds-fractional` Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. -The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. -Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `35.625`). -Whether the fractional part is rounded or truncated is an emitter implementation detail; the test only validates that an integer is sent (e.g. `duration: 35` or `duration: 36`). +The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. +Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). +The value is chosen so that rounding and truncating both yield the same integer, so the expected header is `duration: 36`. ### Encode_Duration_Header_int32SecondsLargerUnit @@ -1179,9 +1179,24 @@ Expected response body: - Endpoint: `get /encode/duration/property/int32-seconds-fractional` Test operation with request and response model contains a duration property with int32 seconds encode whose value has a fractional (sub-second) component. -The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. -Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `35.625`). -Whether the fractional part is rounded or truncated is an emitter implementation detail; the test only validates that an integer is sent (e.g. `35` or `36`). +The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. +Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). +The value is chosen so that rounding and truncating both yield the same integer. +Expected request body: + +```json +{ + "value": 36 +} +``` + +Expected response body: + +```json +{ + "value": 36 +} +``` ### Encode_Duration_Property_int32SecondsLargerUnit @@ -1318,9 +1333,9 @@ Expected query parameter `input=36,47` - Endpoint: `get /encode/duration/query/int32-seconds-fractional` Test int32 seconds encode for a duration parameter whose value has a fractional (sub-second) component. -The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. -Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `35.625`). -Whether the fractional part is rounded or truncated is an emitter implementation detail; the test only validates that an integer is sent (e.g. `input=35` or `input=36`). +The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. +Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). +The value is chosen so that rounding and truncating both yield the same integer, so the expected query parameter is `input=36`. ### Encode_Duration_Query_int32SecondsLargerUnit diff --git a/packages/http-specs/specs/encode/duration/main.tsp b/packages/http-specs/specs/encode/duration/main.tsp index 6c1fd062de0..830af94a211 100644 --- a/packages/http-specs/specs/encode/duration/main.tsp +++ b/packages/http-specs/specs/encode/duration/main.tsp @@ -49,9 +49,9 @@ namespace Query { @scenario @scenarioDoc(""" Test int32 seconds encode for a duration parameter whose value has a fractional (sub-second) component. - The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. - Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `35.625`). - Whether the fractional part is rounded or truncated is an emitter implementation detail; the test only validates that an integer is sent (e.g. `input=35` or `input=36`). + The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. + Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). + The value is chosen so that rounding and truncating both yield the same integer, so the expected query parameter is `input=36`. """) op int32SecondsFractional( @query @@ -343,9 +343,21 @@ namespace Property { @scenario @scenarioDoc(""" Test operation with request and response model contains a duration property with int32 seconds encode whose value has a fractional (sub-second) component. - The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. - Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `35.625`). - Whether the fractional part is rounded or truncated is an emitter implementation detail; the test only validates that an integer is sent (e.g. `35` or `36`). + The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. + Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). + The value is chosen so that rounding and truncating both yield the same integer. + Expected request body: + ```json + { + "value": 36 + } + ``` + Expected response body: + ```json + { + "value": 36 + } + ``` """) op int32SecondsFractional( @body body: Int32SecondsFractionalDurationProperty, @@ -638,9 +650,9 @@ namespace Header { @scenario @scenarioDoc(""" Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. - The duration is 35.625 seconds, e.g. TimeSpan.FromSeconds(35.625) in C#. - Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `35.625`). - Whether the fractional part is rounded or truncated is an emitter implementation detail; the test only validates that an integer is sent (e.g. `duration: 35` or `duration: 36`). + The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. + Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). + The value is chosen so that rounding and truncating both yield the same integer, so the expected header is `duration: 36`. """) op int32SecondsFractional( @header diff --git a/packages/http-specs/specs/encode/duration/mockapi.ts b/packages/http-specs/specs/encode/duration/mockapi.ts index 5013dc544da..668affd0489 100644 --- a/packages/http-specs/specs/encode/duration/mockapi.ts +++ b/packages/http-specs/specs/encode/duration/mockapi.ts @@ -25,6 +25,7 @@ function createBodyServerTests(uri: string, data: any, value: any) { } // Validates that a duration with a fractional (sub-second) component is serialized as an integer. +// The duration (36.25s) is chosen so rounding and truncating both yield 36. function createBodyIntServerTests(uri: string) { return passOnSuccess({ uri, @@ -45,6 +46,13 @@ function createBodyIntServerTests(uri: string) { value, ); } + if (value !== 36) { + throw new ValidationError( + `Expected body property "value" to be 36 but got ${value}`, + "36", + value, + ); + } return { status: 200, body: json({ value }), @@ -210,6 +218,7 @@ function createQueryFloatServerTests(uri: string, paramData: any, value: number) } // Validates that a duration with a fractional (sub-second) component is serialized as an integer. +// The duration (36.25s) is chosen so rounding and truncating both yield 36. function createQueryIntServerTests(uri: string, paramData: any) { return passOnSuccess({ uri, @@ -229,6 +238,9 @@ function createQueryIntServerTests(uri: string, paramData: any) { actual, ); } + if (actual !== "36") { + throw new ValidationError(`Expected query param input=36 but got ${actual}`, "36", actual); + } return { status: 204, }; @@ -389,6 +401,7 @@ function createHeaderFloatServerTests(uri: string, value: number) { } // Validates that a duration with a fractional (sub-second) component is serialized as an integer. +// The duration (36.25s) is chosen so rounding and truncating both yield 36. function createHeaderIntServerTests(uri: string) { return passOnSuccess({ uri, @@ -410,6 +423,9 @@ function createHeaderIntServerTests(uri: string) { actual, ); } + if (actual !== "36") { + throw new ValidationError(`Expected header duration=36 but got ${actual}`, "36", actual); + } return { status: 204, }; From b9a6161a343ad64c974eaeaef5e0a2e12160b222 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:58:04 +0000 Subject: [PATCH 5/8] Add chronus changeset for http-specs fractional duration scenarios Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- ...lot-add-spector-case-serialization-2026-5-1-18-57-48.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/copilot-add-spector-case-serialization-2026-5-1-18-57-48.md diff --git a/.chronus/changes/copilot-add-spector-case-serialization-2026-5-1-18-57-48.md b/.chronus/changes/copilot-add-spector-case-serialization-2026-5-1-18-57-48.md new file mode 100644 index 00000000000..eb8e6b51f73 --- /dev/null +++ b/.chronus/changes/copilot-add-spector-case-serialization-2026-5-1-18-57-48.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-specs" +--- + +Add encode/duration int32-seconds-fractional Spector scenarios verifying a fractional duration is serialized as an integer \ No newline at end of file From 7a11acd7eeeccbb0696b02614c026fbe766813a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:36:23 +0000 Subject: [PATCH 6/8] Clarify lossy encode scenario in duration test docs Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- packages/http-specs/spec-summary.md | 3 +++ packages/http-specs/specs/encode/duration/main.tsp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index a672f6ee5f9..1de93b04486 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -895,6 +895,7 @@ Expected header `duration: 36` Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). +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 header is `duration: 36`. ### Encode_Duration_Header_int32SecondsLargerUnit @@ -1181,6 +1182,7 @@ Expected response body: Test operation with request and response model contains a duration property with int32 seconds encode whose value has a fractional (sub-second) component. The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). +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. Expected request body: @@ -1335,6 +1337,7 @@ Expected query parameter `input=36,47` Test int32 seconds encode for a duration parameter whose value has a fractional (sub-second) component. The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). +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`. ### Encode_Duration_Query_int32SecondsLargerUnit diff --git a/packages/http-specs/specs/encode/duration/main.tsp b/packages/http-specs/specs/encode/duration/main.tsp index 830af94a211..92a033d3dcb 100644 --- a/packages/http-specs/specs/encode/duration/main.tsp +++ b/packages/http-specs/specs/encode/duration/main.tsp @@ -51,6 +51,7 @@ namespace Query { Test int32 seconds encode for a duration parameter whose value has a fractional (sub-second) component. The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). + 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( @@ -345,6 +346,7 @@ namespace Property { Test operation with request and response model contains a duration property with int32 seconds encode whose value has a fractional (sub-second) component. The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). + 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. Expected request body: ```json @@ -652,6 +654,7 @@ namespace Header { Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). + 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 header is `duration: 36`. """) op int32SecondsFractional( From bd267b10dc839109f50fee8476bd3c3f402be766 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:01:59 +0000 Subject: [PATCH 7/8] Move duration lossy-encode tests to dedicated Lossy category, add milliseconds, allow ceil Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- ...or-case-serialization-2026-5-1-18-57-48.md | 2 +- packages/http-specs/spec-summary.md | 99 +++++----- .../http-specs/specs/encode/duration/main.tsp | 169 +++++++++++------- .../specs/encode/duration/mockapi.ts | 92 ++++++---- 4 files changed, 224 insertions(+), 138 deletions(-) diff --git a/.chronus/changes/copilot-add-spector-case-serialization-2026-5-1-18-57-48.md b/.chronus/changes/copilot-add-spector-case-serialization-2026-5-1-18-57-48.md index eb8e6b51f73..6a25bdb8a4a 100644 --- a/.chronus/changes/copilot-add-spector-case-serialization-2026-5-1-18-57-48.md +++ b/.chronus/changes/copilot-add-spector-case-serialization-2026-5-1-18-57-48.md @@ -4,4 +4,4 @@ packages: - "@typespec/http-specs" --- -Add encode/duration int32-seconds-fractional Spector scenarios verifying a fractional duration is serialized as an integer \ No newline at end of file +Add encode/duration lossy Spector scenarios verifying a duration whose value carries more precision than the target integer encoding (fractional seconds and sub-millisecond milliseconds) is serialized as an integer \ No newline at end of file diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 1de93b04486..f5dee827e0b 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -888,16 +888,6 @@ Expected header `duration: 180000` Test int32 seconds encode for a duration header. Expected header `duration: 36` -### Encode_Duration_Header_int32SecondsFractional - -- Endpoint: `get /encode/duration/header/int32-seconds-fractional` - -Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. -The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. -Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). -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 header is `duration: 36`. - ### Encode_Duration_Header_int32SecondsLargerUnit - Endpoint: `get /encode/duration/header/int32-seconds-larger-unit` @@ -920,6 +910,60 @@ Expected header `duration: P40D` Test iso8601 encode for a duration array header. Expected header `duration: [P40D,P50D]` +### Encode_Duration_Lossy_Header_int32Milliseconds + +- Endpoint: `get /encode/duration/lossy/header/int32-milliseconds` + +Test int32 milliseconds encode for a duration header whose value has a sub-millisecond fractional component. +The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) in C#. +The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. +Because emitters may floor, round, or ceil when discarding that precision, the expected header is `duration: 36250` or `duration: 36251`. + +### Encode_Duration_Lossy_Header_int32Seconds + +- Endpoint: `get /encode/duration/lossy/header/int32-seconds` + +Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. +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 header is `duration: 36` or `duration: 37`. + +### Encode_Duration_Lossy_Property_int32Milliseconds + +- Endpoint: `get /encode/duration/lossy/property/int32-milliseconds` + +Test int32 milliseconds encode for a duration property whose value has a sub-millisecond fractional component. +The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) in C#. +The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. +Because emitters may floor, round, or ceil when discarding that precision, the expected request body `value` is `36250` or `36251`. + +### Encode_Duration_Lossy_Property_int32Seconds + +- Endpoint: `get /encode/duration/lossy/property/int32-seconds` + +Test int32 seconds encode for a duration property whose value has a fractional (sub-second) component. +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 request body `value` is `36` or `37`. + +### Encode_Duration_Lossy_Query_int32Milliseconds + +- Endpoint: `get /encode/duration/lossy/query/int32-milliseconds` + +Test int32 milliseconds encode for a duration query parameter whose value has a sub-millisecond fractional component. +The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) in C#. +The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. +Because emitters may floor, round, or ceil when discarding that precision, the expected query parameter is `input=36250` or `input=36251`. + +### Encode_Duration_Lossy_Query_int32Seconds + +- Endpoint: `get /encode/duration/lossy/query/int32-seconds` + +Test int32 seconds encode for a duration query parameter whose value has a fractional (sub-second) component. +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`. + ### Encode_Duration_Property_default - Endpoint: `post /encode/duration/property/default` @@ -1175,31 +1219,6 @@ Expected response body: } ``` -### Encode_Duration_Property_int32SecondsFractional - -- Endpoint: `get /encode/duration/property/int32-seconds-fractional` - -Test operation with request and response model contains a duration property with int32 seconds encode whose value has a fractional (sub-second) component. -The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. -Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). -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. -Expected request body: - -```json -{ - "value": 36 -} -``` - -Expected response body: - -```json -{ - "value": 36 -} -``` - ### Encode_Duration_Property_int32SecondsLargerUnit - Endpoint: `get /encode/duration/property/int32-seconds-larger-unit` @@ -1330,16 +1349,6 @@ Expected query parameter `input=36` Test int32 seconds encode for a duration array parameter. Expected query parameter `input=36,47` -### Encode_Duration_Query_int32SecondsFractional - -- Endpoint: `get /encode/duration/query/int32-seconds-fractional` - -Test int32 seconds encode for a duration parameter whose value has a fractional (sub-second) component. -The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. -Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). -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`. - ### Encode_Duration_Query_int32SecondsLargerUnit - Endpoint: `get /encode/duration/query/int32-seconds-larger-unit` diff --git a/packages/http-specs/specs/encode/duration/main.tsp b/packages/http-specs/specs/encode/duration/main.tsp index 92a033d3dcb..1d01a2a2086 100644 --- a/packages/http-specs/specs/encode/duration/main.tsp +++ b/packages/http-specs/specs/encode/duration/main.tsp @@ -45,21 +45,6 @@ namespace Query { input: duration, ): NoContentResponse; - @route("/int32-seconds-fractional") - @scenario - @scenarioDoc(""" - Test int32 seconds encode for a duration parameter whose value has a fractional (sub-second) component. - The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. - Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). - 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( - @query - @encode(DurationKnownEncoding.seconds, int32) - input: duration, - ): NoContentResponse; - @route("/int32-seconds-larger-unit") @scenario @scenarioDoc(""" @@ -217,11 +202,6 @@ namespace Property { value: duration; } - model Int32SecondsFractionalDurationProperty { - @encode(DurationKnownEncoding.seconds, int32) - value: duration; - } - model FloatSecondsDurationProperty { @encode(DurationKnownEncoding.seconds, float) value: duration; @@ -340,31 +320,6 @@ namespace Property { """) op int32Seconds(@body body: Int32SecondsDurationProperty): Int32SecondsDurationProperty; - @route("/int32-seconds-fractional") - @scenario - @scenarioDoc(""" - Test operation with request and response model contains a duration property with int32 seconds encode whose value has a fractional (sub-second) component. - The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. - Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). - 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. - Expected request body: - ```json - { - "value": 36 - } - ``` - Expected response body: - ```json - { - "value": 36 - } - ``` - """) - op int32SecondsFractional( - @body body: Int32SecondsFractionalDurationProperty, - ): Int32SecondsFractionalDurationProperty; - @route("/float-seconds") @scenario @scenarioDoc(""" @@ -648,21 +603,6 @@ namespace Header { duration: duration, ): NoContentResponse; - @route("/int32-seconds-fractional") - @scenario - @scenarioDoc(""" - Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. - The duration is 36.25 seconds, e.g. TimeSpan.FromSeconds(36.25) in C#. - Even though the underlying value is fractional, the client must serialize it as an integer (not a floating point number such as `36.25`). - 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 header is `duration: 36`. - """) - op int32SecondsFractional( - @header - @encode(DurationKnownEncoding.seconds, int32) - duration: duration, - ): NoContentResponse; - @route("/int32-seconds-larger-unit") @scenario @scenarioDoc(""" @@ -789,3 +729,112 @@ namespace Header { duration: Int32MillisecondsDuration[], ): NoContentResponse; } + +/** + * Lossy encode scenarios. + * + * These scenarios cover the case where the source `duration` carries more precision than the + * target encoding can represent (e.g. a sub-second value encoded as integer seconds). 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 round-trip scenarios above. + */ +@route("/lossy") +namespace Lossy { + @route("/query") + namespace Query { + @route("/int32-seconds") + @scenario + @scenarioDoc(""" + Test int32 seconds encode for a duration query parameter whose value has a fractional (sub-second) component. + 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; + + @route("/int32-milliseconds") + @scenario + @scenarioDoc(""" + Test int32 milliseconds encode for a duration query parameter whose value has a sub-millisecond fractional component. + The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) in C#. + The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. + Because emitters may floor, round, or ceil when discarding that precision, the expected query parameter is `input=36250` or `input=36251`. + """) + op int32Milliseconds( + @query + @encode(DurationKnownEncoding.milliseconds, int32) + input: duration, + ): NoContentResponse; + } + + @route("/property") + namespace Property { + model Int32SecondsDurationProperty { + @encode(DurationKnownEncoding.seconds, int32) + value: duration; + } + + model Int32MillisecondsDurationProperty { + @encode(DurationKnownEncoding.milliseconds, int32) + value: duration; + } + + @route("/int32-seconds") + @scenario + @scenarioDoc(""" + Test int32 seconds encode for a duration property whose value has a fractional (sub-second) component. + 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 request body `value` is `36` or `37`. + """) + op int32Seconds(@body body: Int32SecondsDurationProperty): Int32SecondsDurationProperty; + + @route("/int32-milliseconds") + @scenario + @scenarioDoc(""" + Test int32 milliseconds encode for a duration property whose value has a sub-millisecond fractional component. + The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) in C#. + The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. + Because emitters may floor, round, or ceil when discarding that precision, the expected request body `value` is `36250` or `36251`. + """) + op int32Milliseconds( + @body body: Int32MillisecondsDurationProperty, + ): Int32MillisecondsDurationProperty; + } + + @route("/header") + namespace Header { + @route("/int32-seconds") + @scenario + @scenarioDoc(""" + Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. + 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 header is `duration: 36` or `duration: 37`. + """) + op int32Seconds( + @header + @encode(DurationKnownEncoding.seconds, int32) + duration: duration, + ): NoContentResponse; + + @route("/int32-milliseconds") + @scenario + @scenarioDoc(""" + Test int32 milliseconds encode for a duration header whose value has a sub-millisecond fractional component. + The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) in C#. + The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. + Because emitters may floor, round, or ceil when discarding that precision, the expected header is `duration: 36250` or `duration: 36251`. + """) + op int32Milliseconds( + @header + @encode(DurationKnownEncoding.milliseconds, int32) + duration: duration, + ): NoContentResponse; + } +} diff --git a/packages/http-specs/specs/encode/duration/mockapi.ts b/packages/http-specs/specs/encode/duration/mockapi.ts index 668affd0489..3c74452b8bc 100644 --- a/packages/http-specs/specs/encode/duration/mockapi.ts +++ b/packages/http-specs/specs/encode/duration/mockapi.ts @@ -24,18 +24,19 @@ function createBodyServerTests(uri: string, data: any, value: any) { }); } -// Validates that a duration with a fractional (sub-second) component is serialized as an integer. -// The duration (36.25s) is chosen so rounding and truncating both yield 36. -function createBodyIntServerTests(uri: string) { +// Validates that a duration whose value carries more precision than the target encoding (a lossy +// encode) is serialized as an integer. The allowed values cover floor, round and ceil so the test +// does not take a position on an emitter's rounding mode while still rejecting floating point output. +function createLossyBodyServerTests(uri: string, allowed: number[]) { return passOnSuccess({ uri, method: "post", request: { - body: json({ value: 36 }), + body: json({ value: allowed[0] }), }, response: { status: 200, - body: json({ value: 36 }), + body: json({ value: allowed[0] }), }, handler: (req: MockRequest) => { const value = req.body?.value; @@ -46,10 +47,10 @@ function createBodyIntServerTests(uri: string) { value, ); } - if (value !== 36) { + if (!allowed.includes(value)) { throw new ValidationError( - `Expected body property "value" to be 36 but got ${value}`, - "36", + `Expected body property "value" to be one of ${allowed.join(", ")} but got ${value}`, + allowed.join(" | "), value, ); } @@ -89,9 +90,6 @@ Scenarios.Encode_Duration_Property_int32Seconds = createBodyServerTests( }, 36, ); -Scenarios.Encode_Duration_Property_int32SecondsFractional = createBodyIntServerTests( - "/encode/duration/property/int32-seconds-fractional", -); Scenarios.Encode_Duration_Property_iso8601 = createBodyServerTests( "/encode/duration/property/iso8601", { @@ -217,14 +215,17 @@ function createQueryFloatServerTests(uri: string, paramData: any, value: number) }); } -// Validates that a duration with a fractional (sub-second) component is serialized as an integer. -// The duration (36.25s) is chosen so rounding and truncating both yield 36. -function createQueryIntServerTests(uri: string, paramData: any) { +// Validates that a duration whose value carries more precision than the target encoding (a lossy +// encode) is serialized as an integer. The allowed values cover floor, round and ceil so the test +// does not take a position on an emitter's rounding mode while still rejecting floating point output. +function createLossyQueryServerTests(uri: string, allowed: number[]) { return passOnSuccess({ uri, method: "get", request: { - query: paramData, + query: { + input: allowed[0], + }, }, response: { status: 204, @@ -238,8 +239,12 @@ function createQueryIntServerTests(uri: string, paramData: any) { actual, ); } - if (actual !== "36") { - throw new ValidationError(`Expected query param input=36 but got ${actual}`, "36", actual); + if (!allowed.map(String).includes(actual)) { + throw new ValidationError( + `Expected query param input to be one of ${allowed.join(", ")} but got ${actual}`, + allowed.join(" | "), + actual, + ); } return { status: 204, @@ -269,12 +274,6 @@ Scenarios.Encode_Duration_Query_int32Seconds = createQueryServerTests( }, "36", ); -Scenarios.Encode_Duration_Query_int32SecondsFractional = createQueryIntServerTests( - "/encode/duration/query/int32-seconds-fractional", - { - input: 36, - }, -); Scenarios.Encode_Duration_Query_int32SecondsArray = createQueryServerTests( "/encode/duration/query/int32-seconds-array", { @@ -400,15 +399,16 @@ function createHeaderFloatServerTests(uri: string, value: number) { }); } -// Validates that a duration with a fractional (sub-second) component is serialized as an integer. -// The duration (36.25s) is chosen so rounding and truncating both yield 36. -function createHeaderIntServerTests(uri: string) { +// Validates that a duration whose value carries more precision than the target encoding (a lossy +// encode) is serialized as an integer. The allowed values cover floor, round and ceil so the test +// does not take a position on an emitter's rounding mode while still rejecting floating point output. +function createLossyHeaderServerTests(uri: string, allowed: number[]) { return passOnSuccess({ uri, method: "get", request: { headers: { - duration: "36", + duration: String(allowed[0]), }, }, response: { @@ -423,8 +423,12 @@ function createHeaderIntServerTests(uri: string) { actual, ); } - if (actual !== "36") { - throw new ValidationError(`Expected header duration=36 but got ${actual}`, "36", actual); + if (!allowed.map(String).includes(actual)) { + throw new ValidationError( + `Expected header duration to be one of ${allowed.join(", ")} but got ${actual}`, + allowed.join(" | "), + actual, + ); } return { status: 204, @@ -455,9 +459,6 @@ Scenarios.Encode_Duration_Header_int32Seconds = createHeaderServerTests( }, "36", ); -Scenarios.Encode_Duration_Header_int32SecondsFractional = createHeaderIntServerTests( - "/encode/duration/header/int32-seconds-fractional", -); Scenarios.Encode_Duration_Header_floatSeconds = createHeaderServerTests( "/encode/duration/header/float-seconds", { @@ -524,3 +525,30 @@ Scenarios.Encode_Duration_Header_floatMillisecondsLargerUnit = createHeaderFloat "/encode/duration/header/float-milliseconds-larger-unit", 210000, ); + +// Lossy encode scenarios: the source duration carries more precision than the target integer +// encoding, so floor/round/ceil are all acceptable results (e.g. 36.25s -> 36 or 37). +Scenarios.Encode_Duration_Lossy_Query_int32Seconds = createLossyQueryServerTests( + "/encode/duration/lossy/query/int32-seconds", + [36, 37], +); +Scenarios.Encode_Duration_Lossy_Query_int32Milliseconds = createLossyQueryServerTests( + "/encode/duration/lossy/query/int32-milliseconds", + [36250, 36251], +); +Scenarios.Encode_Duration_Lossy_Property_int32Seconds = createLossyBodyServerTests( + "/encode/duration/lossy/property/int32-seconds", + [36, 37], +); +Scenarios.Encode_Duration_Lossy_Property_int32Milliseconds = createLossyBodyServerTests( + "/encode/duration/lossy/property/int32-milliseconds", + [36250, 36251], +); +Scenarios.Encode_Duration_Lossy_Header_int32Seconds = createLossyHeaderServerTests( + "/encode/duration/lossy/header/int32-seconds", + [36, 37], +); +Scenarios.Encode_Duration_Lossy_Header_int32Milliseconds = createLossyHeaderServerTests( + "/encode/duration/lossy/header/int32-milliseconds", + [36250, 36251], +); From a96701da5b594a4587d000bdfa12f71b587ed424 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 22:50:24 +0000 Subject: [PATCH 8/8] Flatten Lossy namespace to avoid client name collisions; mark property ops @post Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- packages/http-specs/spec-summary.md | 16 +- .../http-specs/specs/encode/duration/main.tsp | 175 +++++++++--------- .../specs/encode/duration/mockapi.ts | 12 +- 3 files changed, 98 insertions(+), 105 deletions(-) diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index f5dee827e0b..44cb91ad026 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -910,7 +910,7 @@ Expected header `duration: P40D` Test iso8601 encode for a duration array header. Expected header `duration: [P40D,P50D]` -### Encode_Duration_Lossy_Header_int32Milliseconds +### Encode_Duration_Lossy_headerInt32Milliseconds - Endpoint: `get /encode/duration/lossy/header/int32-milliseconds` @@ -919,7 +919,7 @@ The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. Because emitters may floor, round, or ceil when discarding that precision, the expected header is `duration: 36250` or `duration: 36251`. -### Encode_Duration_Lossy_Header_int32Seconds +### Encode_Duration_Lossy_headerInt32Seconds - Endpoint: `get /encode/duration/lossy/header/int32-seconds` @@ -928,25 +928,25 @@ 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 header is `duration: 36` or `duration: 37`. -### Encode_Duration_Lossy_Property_int32Milliseconds +### Encode_Duration_Lossy_propertyInt32Milliseconds -- Endpoint: `get /encode/duration/lossy/property/int32-milliseconds` +- Endpoint: `post /encode/duration/lossy/property/int32-milliseconds` Test int32 milliseconds encode for a duration property whose value has a sub-millisecond fractional component. The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) in C#. The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. Because emitters may floor, round, or ceil when discarding that precision, the expected request body `value` is `36250` or `36251`. -### Encode_Duration_Lossy_Property_int32Seconds +### Encode_Duration_Lossy_propertyInt32Seconds -- Endpoint: `get /encode/duration/lossy/property/int32-seconds` +- Endpoint: `post /encode/duration/lossy/property/int32-seconds` Test int32 seconds encode for a duration property whose value has a fractional (sub-second) component. 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 request body `value` is `36` or `37`. -### Encode_Duration_Lossy_Query_int32Milliseconds +### Encode_Duration_Lossy_queryInt32Milliseconds - Endpoint: `get /encode/duration/lossy/query/int32-milliseconds` @@ -955,7 +955,7 @@ The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. Because emitters may floor, round, or ceil when discarding that precision, the expected query parameter is `input=36250` or `input=36251`. -### Encode_Duration_Lossy_Query_int32Seconds +### Encode_Duration_Lossy_queryInt32Seconds - Endpoint: `get /encode/duration/lossy/query/int32-seconds` diff --git a/packages/http-specs/specs/encode/duration/main.tsp b/packages/http-specs/specs/encode/duration/main.tsp index 1d01a2a2086..faeffbd1c85 100644 --- a/packages/http-specs/specs/encode/duration/main.tsp +++ b/packages/http-specs/specs/encode/duration/main.tsp @@ -741,100 +741,93 @@ namespace Header { */ @route("/lossy") namespace Lossy { - @route("/query") - namespace Query { - @route("/int32-seconds") - @scenario - @scenarioDoc(""" - Test int32 seconds encode for a duration query parameter whose value has a fractional (sub-second) component. - 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; - - @route("/int32-milliseconds") - @scenario - @scenarioDoc(""" - Test int32 milliseconds encode for a duration query parameter whose value has a sub-millisecond fractional component. - The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) in C#. - The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. - Because emitters may floor, round, or ceil when discarding that precision, the expected query parameter is `input=36250` or `input=36251`. - """) - op int32Milliseconds( - @query - @encode(DurationKnownEncoding.milliseconds, int32) - input: duration, - ): NoContentResponse; + model Int32SecondsDurationProperty { + @encode(DurationKnownEncoding.seconds, int32) + value: duration; } - @route("/property") - namespace Property { - model Int32SecondsDurationProperty { - @encode(DurationKnownEncoding.seconds, int32) - value: duration; - } + model Int32MillisecondsDurationProperty { + @encode(DurationKnownEncoding.milliseconds, int32) + value: duration; + } - model Int32MillisecondsDurationProperty { - @encode(DurationKnownEncoding.milliseconds, int32) - value: duration; - } + @route("/query/int32-seconds") + @scenario + @scenarioDoc(""" + Test int32 seconds encode for a duration query parameter whose value has a fractional (sub-second) component. + 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 queryInt32Seconds( + @query + @encode(DurationKnownEncoding.seconds, int32) + input: duration, + ): NoContentResponse; - @route("/int32-seconds") - @scenario - @scenarioDoc(""" - Test int32 seconds encode for a duration property whose value has a fractional (sub-second) component. - 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 request body `value` is `36` or `37`. - """) - op int32Seconds(@body body: Int32SecondsDurationProperty): Int32SecondsDurationProperty; - - @route("/int32-milliseconds") - @scenario - @scenarioDoc(""" - Test int32 milliseconds encode for a duration property whose value has a sub-millisecond fractional component. - The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) in C#. - The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. - Because emitters may floor, round, or ceil when discarding that precision, the expected request body `value` is `36250` or `36251`. - """) - op int32Milliseconds( - @body body: Int32MillisecondsDurationProperty, - ): Int32MillisecondsDurationProperty; - } + @route("/query/int32-milliseconds") + @scenario + @scenarioDoc(""" + Test int32 milliseconds encode for a duration query parameter whose value has a sub-millisecond fractional component. + The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) in C#. + The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. + Because emitters may floor, round, or ceil when discarding that precision, the expected query parameter is `input=36250` or `input=36251`. + """) + op queryInt32Milliseconds( + @query + @encode(DurationKnownEncoding.milliseconds, int32) + input: duration, + ): NoContentResponse; - @route("/header") - namespace Header { - @route("/int32-seconds") - @scenario - @scenarioDoc(""" - Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. - 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 header is `duration: 36` or `duration: 37`. - """) - op int32Seconds( - @header - @encode(DurationKnownEncoding.seconds, int32) - duration: duration, - ): NoContentResponse; - - @route("/int32-milliseconds") - @scenario - @scenarioDoc(""" - Test int32 milliseconds encode for a duration header whose value has a sub-millisecond fractional component. - The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) in C#. - The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. - Because emitters may floor, round, or ceil when discarding that precision, the expected header is `duration: 36250` or `duration: 36251`. - """) - op int32Milliseconds( - @header - @encode(DurationKnownEncoding.milliseconds, int32) - duration: duration, - ): NoContentResponse; - } + @route("/property/int32-seconds") + @scenario + @scenarioDoc(""" + Test int32 seconds encode for a duration property whose value has a fractional (sub-second) component. + 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 request body `value` is `36` or `37`. + """) + @post + op propertyInt32Seconds(@body body: Int32SecondsDurationProperty): Int32SecondsDurationProperty; + + @route("/property/int32-milliseconds") + @scenario + @scenarioDoc(""" + Test int32 milliseconds encode for a duration property whose value has a sub-millisecond fractional component. + The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) in C#. + The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. + Because emitters may floor, round, or ceil when discarding that precision, the expected request body `value` is `36250` or `36251`. + """) + @post + op propertyInt32Milliseconds( + @body body: Int32MillisecondsDurationProperty, + ): Int32MillisecondsDurationProperty; + + @route("/header/int32-seconds") + @scenario + @scenarioDoc(""" + Test int32 seconds encode for a duration header whose value has a fractional (sub-second) component. + 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 header is `duration: 36` or `duration: 37`. + """) + op headerInt32Seconds( + @header + @encode(DurationKnownEncoding.seconds, int32) + duration: duration, + ): NoContentResponse; + + @route("/header/int32-milliseconds") + @scenario + @scenarioDoc(""" + Test int32 milliseconds encode for a duration header whose value has a sub-millisecond fractional component. + The duration is 36250.25 milliseconds, e.g. TimeSpan.FromMilliseconds(36250.25) in C#. + The client must serialize the value as an integer (not a floating point number such as `36250.25`), discarding the sub-millisecond precision. + Because emitters may floor, round, or ceil when discarding that precision, the expected header is `duration: 36250` or `duration: 36251`. + """) + op headerInt32Milliseconds( + @header + @encode(DurationKnownEncoding.milliseconds, int32) + duration: duration, + ): NoContentResponse; } diff --git a/packages/http-specs/specs/encode/duration/mockapi.ts b/packages/http-specs/specs/encode/duration/mockapi.ts index 3c74452b8bc..868f5c54947 100644 --- a/packages/http-specs/specs/encode/duration/mockapi.ts +++ b/packages/http-specs/specs/encode/duration/mockapi.ts @@ -528,27 +528,27 @@ Scenarios.Encode_Duration_Header_floatMillisecondsLargerUnit = createHeaderFloat // Lossy encode scenarios: the source duration carries more precision than the target integer // encoding, so floor/round/ceil are all acceptable results (e.g. 36.25s -> 36 or 37). -Scenarios.Encode_Duration_Lossy_Query_int32Seconds = createLossyQueryServerTests( +Scenarios.Encode_Duration_Lossy_queryInt32Seconds = createLossyQueryServerTests( "/encode/duration/lossy/query/int32-seconds", [36, 37], ); -Scenarios.Encode_Duration_Lossy_Query_int32Milliseconds = createLossyQueryServerTests( +Scenarios.Encode_Duration_Lossy_queryInt32Milliseconds = createLossyQueryServerTests( "/encode/duration/lossy/query/int32-milliseconds", [36250, 36251], ); -Scenarios.Encode_Duration_Lossy_Property_int32Seconds = createLossyBodyServerTests( +Scenarios.Encode_Duration_Lossy_propertyInt32Seconds = createLossyBodyServerTests( "/encode/duration/lossy/property/int32-seconds", [36, 37], ); -Scenarios.Encode_Duration_Lossy_Property_int32Milliseconds = createLossyBodyServerTests( +Scenarios.Encode_Duration_Lossy_propertyInt32Milliseconds = createLossyBodyServerTests( "/encode/duration/lossy/property/int32-milliseconds", [36250, 36251], ); -Scenarios.Encode_Duration_Lossy_Header_int32Seconds = createLossyHeaderServerTests( +Scenarios.Encode_Duration_Lossy_headerInt32Seconds = createLossyHeaderServerTests( "/encode/duration/lossy/header/int32-seconds", [36, 37], ); -Scenarios.Encode_Duration_Lossy_Header_int32Milliseconds = createLossyHeaderServerTests( +Scenarios.Encode_Duration_Lossy_headerInt32Milliseconds = createLossyHeaderServerTests( "/encode/duration/lossy/header/int32-milliseconds", [36250, 36251], );