From 00f66ba48d1a99ffd75c82927e7509f07fc48b43 Mon Sep 17 00:00:00 2001 From: pjay Date: Sat, 18 Apr 2026 18:00:12 +0700 Subject: [PATCH] fix: pre-v0.1 review feedback (#14) * fix(sdk-android): add @JvmOverloads to Uatu.start Java callers can now invoke start(application) without supplying a Configuration, matching the Kotlin default-arg ergonomics. * feat(agent): add protocol_version to HELLO handshake ProtocolVersion=1 lives on Message and is set by Hello(). Server.Accept rejects mismatches with a clear error. SDK upgrades that don't change the wire format keep the same protocol_version; bump on breaking changes. * test(agent): assert protocol_version in Hello round-trip * feat(sdk-android): send protocol_version=1 in HELLO Mirrors agent.ProtocolVersion on the Go side. Bump in lockstep with the Go constant when the wire format breaks. * chore(sample-app): pull @uatu/spec from npm next tag Replaces the file: dep. Copy-paste users can now npm install against the registry. The release workflow publishes pre-release tags to npm dist-tag 'next', so the sample tracks the latest rc without manual version bumps. Lockfile currently resolves to 0.0.1-rc3. --- examples/sample-app/package-lock.json | 20 +++++------------ examples/sample-app/package.json | 2 +- internal/agent/protocol.go | 19 ++++++++++++---- internal/agent/protocol_test.go | 3 +++ internal/agent/server.go | 4 ++++ internal/agent/server_test.go | 22 +++++++++++++++++++ .../src/main/kotlin/dev/uatu/sdk/Protocol.kt | 14 +++++++++++- .../src/main/kotlin/dev/uatu/sdk/Uatu.kt | 1 + .../test/kotlin/dev/uatu/sdk/ProtocolTest.kt | 2 ++ 9 files changed, 67 insertions(+), 20 deletions(-) diff --git a/examples/sample-app/package-lock.json b/examples/sample-app/package-lock.json index 53b04fe..d04d4ab 100644 --- a/examples/sample-app/package-lock.json +++ b/examples/sample-app/package-lock.json @@ -6,23 +6,15 @@ "": { "name": "uatu-sample", "devDependencies": { - "@uatu/spec": "file:../../pkg/spec-api" - } - }, - "../../pkg/spec-api": { - "name": "@uatu/spec", - "version": "0.0.0-dev", - "dev": true, - "license": "Apache-2.0", - "devDependencies": { - "@types/node": "^22.7.5", - "tsx": "^4.21.0", - "typescript": "^5.9.3" + "@uatu/spec": "next" } }, "node_modules/@uatu/spec": { - "resolved": "../../pkg/spec-api", - "link": true + "version": "0.0.1-rc3", + "resolved": "https://registry.npmjs.org/@uatu/spec/-/spec-0.0.1-rc3.tgz", + "integrity": "sha512-mCayssOqM2wfzrQDnFGE8FAlUlDzBl1fx2L/MLDIuPrQHBMlUC350xBB8Z7I/DYI98eCWJMqjtw3iU6yVWKtIQ==", + "dev": true, + "license": "Apache-2.0" } } } diff --git a/examples/sample-app/package.json b/examples/sample-app/package.json index 477525b..29edc03 100644 --- a/examples/sample-app/package.json +++ b/examples/sample-app/package.json @@ -3,6 +3,6 @@ "private": true, "description": "Minimal uatu integration example.", "devDependencies": { - "@uatu/spec": "file:../../pkg/spec-api" + "@uatu/spec": "next" } } diff --git a/internal/agent/protocol.go b/internal/agent/protocol.go index 39d6664..5e4615c 100644 --- a/internal/agent/protocol.go +++ b/internal/agent/protocol.go @@ -21,13 +21,18 @@ const ( const MaxFrameSize = 16 * 1024 * 1024 +// ProtocolVersion is the wire-format version. Bump on any breaking change +// to the message schema or framing. Independent of the SDK release version. +const ProtocolVersion = 1 + type Message struct { Type MessageType `json:"type"` ID uint64 `json:"id,omitempty"` - Version string `json:"version,omitempty"` - Platform string `json:"platform,omitempty"` - AppPackage string `json:"app_package,omitempty"` + ProtocolVersion int `json:"protocol_version,omitempty"` + Version string `json:"version,omitempty"` + Platform string `json:"platform,omitempty"` + AppPackage string `json:"app_package,omitempty"` Snapshots map[string]json.RawMessage `json:"snapshots,omitempty"` @@ -39,7 +44,13 @@ type Message struct { } func Hello(version, platform, appPackage string) Message { - return Message{Type: MessageTypeHello, Version: version, Platform: platform, AppPackage: appPackage} + return Message{ + Type: MessageTypeHello, + ProtocolVersion: ProtocolVersion, + Version: version, + Platform: platform, + AppPackage: appPackage, + } } func Pause(id uint64) Message { return Message{Type: MessageTypePause, ID: id} } diff --git a/internal/agent/protocol_test.go b/internal/agent/protocol_test.go index 9275a15..19509ad 100644 --- a/internal/agent/protocol_test.go +++ b/internal/agent/protocol_test.go @@ -28,6 +28,9 @@ func TestRoundTrip_Hello(t *testing.T) { if got.Type != MessageTypeHello || got.Version != "0.0.1" || got.Platform != "android" || got.AppPackage != "in.okcredit.merchant" { t.Fatalf("hello round-trip failed: %+v", got) } + if got.ProtocolVersion != ProtocolVersion { + t.Errorf("protocol_version: got %d, want %d", got.ProtocolVersion, ProtocolVersion) + } } func TestRoundTrip_PauseResume(t *testing.T) { diff --git a/internal/agent/server.go b/internal/agent/server.go index af3d0ad..93ea20c 100644 --- a/internal/agent/server.go +++ b/internal/agent/server.go @@ -41,6 +41,10 @@ func (s *Server) Accept(ctx context.Context) (*Conn, error) { rawConn.Close() return nil, fmt.Errorf("expected HELLO, got %q", hello.Type) } + if hello.ProtocolVersion != ProtocolVersion { + rawConn.Close() + return nil, fmt.Errorf("protocol version mismatch: host=%d sdk=%d", ProtocolVersion, hello.ProtocolVersion) + } return &Conn{rawConn: rawConn, hello: hello}, nil } diff --git a/internal/agent/server_test.go b/internal/agent/server_test.go index 1e63c24..11a5ab3 100644 --- a/internal/agent/server_test.go +++ b/internal/agent/server_test.go @@ -141,6 +141,28 @@ func TestServer_SnapshotAndRelease(t *testing.T) { wg.Wait() } +func TestServer_AcceptRejectsProtocolVersionMismatch(t *testing.T) { + server := newLoopbackServer(t) + + go func() { + client, err := net.Dial("tcp", server.Addr().String()) + if err != nil { + return + } + defer client.Close() + mismatched := Hello("0.0.1", "android", "com.x") + mismatched.ProtocolVersion = ProtocolVersion + 99 + _ = WriteMessage(client, mismatched) + }() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + _, err := server.Accept(ctx) + if err == nil || !strings.Contains(err.Error(), "protocol version mismatch") { + t.Fatalf("expected protocol-version-mismatch error, got %v", err) + } +} + func TestServer_AcceptRequiresHello(t *testing.T) { server := newLoopbackServer(t) diff --git a/sdk/android/src/main/kotlin/dev/uatu/sdk/Protocol.kt b/sdk/android/src/main/kotlin/dev/uatu/sdk/Protocol.kt index 21ffe48..8ad9f94 100644 --- a/sdk/android/src/main/kotlin/dev/uatu/sdk/Protocol.kt +++ b/sdk/android/src/main/kotlin/dev/uatu/sdk/Protocol.kt @@ -26,6 +26,7 @@ enum class MessageType(val wire: String) { data class Message( val type: MessageType, val id: Long = 0L, + val protocolVersion: Int = 0, val version: String? = null, val platform: String? = null, val appPackage: String? = null, @@ -37,7 +38,13 @@ data class Message( ) { companion object { fun hello(version: String, platform: String, appPackage: String): Message = - Message(MessageType.HELLO, version = version, platform = platform, appPackage = appPackage) + Message( + MessageType.HELLO, + protocolVersion = Protocol.PROTOCOL_VERSION, + version = version, + platform = platform, + appPackage = appPackage, + ) fun pause(id: Long): Message = Message(MessageType.PAUSE, id = id) @@ -56,6 +63,9 @@ data class Message( object Protocol { const val MAX_FRAME_SIZE: Int = 16 * 1024 * 1024 + // Wire-format version. Must match agent.ProtocolVersion on the Go side. + const val PROTOCOL_VERSION: Int = 1 + @Throws(IOException::class) fun write(output: OutputStream, message: Message) { val bytes = toJson(message).toString().toByteArray(Charsets.UTF_8) @@ -85,6 +95,7 @@ object Protocol { val json = JSONObject() json.put("type", message.type.wire) if (message.id != 0L) json.put("id", message.id) + if (message.protocolVersion != 0) json.put("protocol_version", message.protocolVersion) message.version?.let { json.put("version", it) } message.platform?.let { json.put("platform", it) } message.appPackage?.let { json.put("app_package", it) } @@ -108,6 +119,7 @@ object Protocol { return Message( type = MessageType.fromWire(typeString), id = json.optLong("id", 0L), + protocolVersion = json.optInt("protocol_version", 0), version = json.optStringOrNull("version"), platform = json.optStringOrNull("platform"), appPackage = json.optStringOrNull("app_package"), diff --git a/sdk/android/src/main/kotlin/dev/uatu/sdk/Uatu.kt b/sdk/android/src/main/kotlin/dev/uatu/sdk/Uatu.kt index 5225a35..5083e8a 100644 --- a/sdk/android/src/main/kotlin/dev/uatu/sdk/Uatu.kt +++ b/sdk/android/src/main/kotlin/dev/uatu/sdk/Uatu.kt @@ -14,6 +14,7 @@ object Uatu { @Volatile private var runtime: UatuRuntime? = null + @JvmOverloads @Synchronized fun start(application: Application, configuration: Configuration = Configuration()) { if (runtime != null) return diff --git a/sdk/android/src/test/kotlin/dev/uatu/sdk/ProtocolTest.kt b/sdk/android/src/test/kotlin/dev/uatu/sdk/ProtocolTest.kt index 78ec50d..3782575 100644 --- a/sdk/android/src/test/kotlin/dev/uatu/sdk/ProtocolTest.kt +++ b/sdk/android/src/test/kotlin/dev/uatu/sdk/ProtocolTest.kt @@ -24,6 +24,7 @@ class ProtocolTest { @Test fun roundTripHello() { val got = roundTrip(Message.hello("0.0.1", "android", "in.okcredit.merchant")) assertEquals(MessageType.HELLO, got.type) + assertEquals(Protocol.PROTOCOL_VERSION, got.protocolVersion) assertEquals("0.0.1", got.version) assertEquals("android", got.platform) assertEquals("in.okcredit.merchant", got.appPackage) @@ -140,6 +141,7 @@ class ProtocolTest { Protocol.write(output, Message.hello("0.0.1", "android", "com.x")) val payload = String(output.toByteArray().copyOfRange(4, output.size()), Charsets.UTF_8) assertTrue(payload.contains("\"type\":\"HELLO\"")) + assertTrue(payload.contains("\"protocol_version\":1")) assertTrue(payload.contains("\"version\":\"0.0.1\"")) assertTrue(payload.contains("\"platform\":\"android\"")) assertTrue(payload.contains("\"app_package\":\"com.x\""))