Skip to content

Commit 30bc0af

Browse files
committed
Add shared round-trip corpus + wire-fidelity fixes (Swift/Rust/Kotlin)
Introduce a language-agnostic round-trip test corpus under types/test-cases/round-trips/ (24 fixtures) and wire each client up to decode → re-encode every fixture through its real generated types. The corpus pins forward-compatibility and exact-bit fidelity across clients. Fidelity fixes driven by the corpus: - Rust: SessionStatus becomes a u32 bitset newtype (was a #[repr(u32)] enum) so combined flags and unknown forward-compat bits round-trip instead of being dropped or rejected. Generated via generate-rust.ts. - Kotlin: SessionStatus.rawValue widens Int → Long so an unknown bit at or above 2^31 round-trips as a plain JSON integer instead of throwing. Generated via generate-kotlin.ts. - Swift: open unions (StateAction, Customization, ResponsePart, ToolCallState, TerminalClaim, …) decode an unknown discriminant to a raw passthrough and re-encode it verbatim instead of throwing or emitting {}; ChangesetOperation*Target now encode their kind discriminant. Generated via generate-swift.ts; hand-written NativeReducer / ToolCallStateExtensions handle the .unknown variant. Tests: round-trip corpus runners for Go, Kotlin, Swift, and TypeScript, plus Kotlin bitset tests covering the Long high-bit case. CHANGELOG entries for the Swift, Rust, and Kotlin clients.
1 parent 0dfdcf8 commit 30bc0af

49 files changed

Lines changed: 3987 additions & 71 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

clients/go/ahptypes/roundtrip_fixture_test.go

Lines changed: 872 additions & 0 deletions
Large diffs are not rendered by default.

clients/kotlin/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ Implements AHP 0.3.0.
7979
with `Client(clientId)` and `Mcp(customizationId)` variants).
8080
`SessionToolCallStartAction` carries the new `contributor` field as
8181
well.
82+
### Changed
83+
84+
- **BREAKING:** `SessionStatus.rawValue` is now a `Long` (was `Int`), and the
85+
named flag constants are `Long` literals. `SessionStatus` is an unsigned
86+
32-bit bitset on the wire; a signed `Int` could not hold a forward-compat bit
87+
at or above `2^31`.
88+
89+
### Fixed
90+
91+
- `SessionStatus` decode fidelity: an unknown forward-compat bit at or above
92+
`2^31` (e.g. `2147483720`) now round-trips as a plain JSON integer instead of
93+
throwing `JsonDecodingException` and dropping the bit.
94+
8295
## [0.2.0] — 2026-05-28
8396

8497
Implements AHP `0.2.0`.

clients/kotlin/build.gradle.kts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ tasks.withType<Test>().configureEach {
5656
.resolve("../../types/test-cases/reducers")
5757
.canonicalPath,
5858
)
59+
// Same wiring for the shared round-trip corpus consumed by
60+
// `TypesRoundTripFixtureTest` — the language-agnostic wire-fidelity
61+
// fixtures shared with the .NET / Swift / Rust clients.
62+
systemProperty(
63+
"ahp.roundTripFixturesDir",
64+
rootProject.projectDir
65+
.resolve("../../types/test-cases/round-trips")
66+
.canonicalPath,
67+
)
5968
}
6069

6170
mavenPublishing {

clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ private fun now(): Long = currentTimestampProvider()
213213
// ─── Status Bitset Helpers ──────────────────────────────────────────────────
214214

215215
/** Bitmask covering the mutually-exclusive activity bits (bits 0–4). */
216-
private const val STATUS_ACTIVITY_MASK: Int = (1 shl 5) - 1
216+
private const val STATUS_ACTIVITY_MASK: Long = (1L shl 5) - 1L
217217

218218
/** Sets or clears a metadata flag on a status value. */
219219
private fun withStatusFlag(status: SessionStatus, flag: SessionStatus, set: Boolean): SessionStatus =

clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ enum class SessionLifecycle {
123123
*/
124124
@Serializable(with = SessionStatusSerializer::class)
125125
@JvmInline
126-
value class SessionStatus(val rawValue: Int) {
126+
value class SessionStatus(val rawValue: Long) {
127127
operator fun contains(other: SessionStatus): Boolean =
128128
(rawValue and other.rawValue) == other.rawValue
129129

@@ -134,38 +134,38 @@ value class SessionStatus(val rawValue: Int) {
134134
/**
135135
* Session is idle — no turn is active.
136136
*/
137-
val IDLE: SessionStatus = SessionStatus(1)
137+
val IDLE: SessionStatus = SessionStatus(1L)
138138
/**
139139
* Session ended with an error.
140140
*/
141-
val ERROR: SessionStatus = SessionStatus(2)
141+
val ERROR: SessionStatus = SessionStatus(2L)
142142
/**
143143
* A turn is actively streaming.
144144
*/
145-
val IN_PROGRESS: SessionStatus = SessionStatus(8)
145+
val IN_PROGRESS: SessionStatus = SessionStatus(8L)
146146
/**
147147
* A turn is in progress but blocked waiting for user input or tool confirmation.
148148
*/
149-
val INPUT_NEEDED: SessionStatus = SessionStatus(24)
149+
val INPUT_NEEDED: SessionStatus = SessionStatus(24L)
150150
/**
151151
* The client has viewed this session since its last modification.
152152
*/
153-
val IS_READ: SessionStatus = SessionStatus(32)
153+
val IS_READ: SessionStatus = SessionStatus(32L)
154154
/**
155155
* The session has been archived by the client.
156156
*/
157-
val IS_ARCHIVED: SessionStatus = SessionStatus(64)
157+
val IS_ARCHIVED: SessionStatus = SessionStatus(64L)
158158
}
159159
}
160160

161161
internal object SessionStatusSerializer : KSerializer<SessionStatus> {
162162
override val descriptor: SerialDescriptor =
163-
PrimitiveSerialDescriptor("SessionStatus", PrimitiveKind.INT)
163+
PrimitiveSerialDescriptor("SessionStatus", PrimitiveKind.LONG)
164164
override fun serialize(encoder: Encoder, value: SessionStatus) {
165-
encoder.encodeInt(value.rawValue)
165+
encoder.encodeLong(value.rawValue)
166166
}
167167
override fun deserialize(decoder: Decoder): SessionStatus =
168-
SessionStatus(decoder.decodeInt())
168+
SessionStatus(decoder.decodeLong())
169169
}
170170

171171
/**

clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/BitsetEnumTest.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,31 @@ class BitsetEnumTest {
5252
// preserve it so subsequent re-encoding doesn't drop the unknown
5353
// capability.
5454
val withFutureBit = json.decodeFromString(SessionStatus.serializer(), "129")
55-
assertEquals(129, withFutureBit.rawValue)
55+
assertEquals(129L, withFutureBit.rawValue)
5656
assertTrue(SessionStatus.IDLE in withFutureBit)
5757

5858
val reencoded = json.encodeToString(SessionStatus.serializer(), withFutureBit)
5959
assertEquals("129", reencoded)
6060
}
6161

62+
@Test
63+
fun `high bits above signed int32 range survive round trip`() {
64+
// SessionStatus is an unsigned 32-bit bitset on the wire (the .NET
65+
// reference models it as `uint`). A forward-compat unknown bit at or
66+
// above the sign bit 2^31 (2147483648) is a positive value that does
67+
// NOT fit a signed 32-bit Int — backing rawValue with Long is what lets
68+
// it round-trip. Mirrors the shared corpus fixture
69+
// 005-session-status-unknown-bits-preserved: 8|64|2^31 = 2147483720.
70+
val wire = "2147483720"
71+
val status = json.decodeFromString(SessionStatus.serializer(), wire)
72+
assertEquals(2147483720L, status.rawValue)
73+
assertTrue(SessionStatus.IN_PROGRESS in status)
74+
assertTrue(SessionStatus.IS_ARCHIVED in status)
75+
76+
val reencoded = json.encodeToString(SessionStatus.serializer(), status)
77+
assertEquals(wire, reencoded)
78+
}
79+
6280
@Test
6381
fun `bitset wire value is a plain JSON number`() {
6482
val parsed = json.parseToJsonElement("64") as JsonPrimitive

clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/GeneratedStructsTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ class GeneratedStructsTest {
139139
// Sanity check that Ahp object initializes lazily and produces the
140140
// SessionStatus reference (just to ensure the import graph compiles).
141141
assertNotNull(Ahp.json)
142-
assertEquals(8, SessionStatus.IN_PROGRESS.rawValue)
142+
assertEquals(8L, SessionStatus.IN_PROGRESS.rawValue)
143143
}
144144

145145
@Test

0 commit comments

Comments
 (0)