From b976cea7d14ea359aaae35d77c5dc51324e1d48a Mon Sep 17 00:00:00 2001 From: Michael Moffett Date: Sat, 3 Oct 2026 06:46:27 -0600 Subject: [PATCH 1/6] test(harness): Kotlin protocol runner in the mpp-protocol divergence matrix A Gradle stdin/stdout runner over the Kotlin SDK's protocol functions, plus `harness/protocol-runners/kotlin.json`, so the spawned-runner block drives Kotlin. No protocol-layer vector reaches Kotlin today. Milestone 1 of our harness proposal puts Kotlin in the divergence matrix. --- .github/workflows/harness.yml | 8 ++ harness/kotlin-protocol-runner/.gitignore | 2 + .../kotlin-protocol-runner/build.gradle.kts | 23 +++++ .../settings.gradle.kts | 16 ++++ .../com/solana/paykit/protocolrunner/Main.kt | 91 +++++++++++++++++++ .../solana/paykit/protocolrunner/MainTest.kt | 73 +++++++++++++++ harness/protocol-runners/kotlin.json | 5 + harness/test/protocol-conformance.test.ts | 70 +++++++++++--- 8 files changed, 276 insertions(+), 12 deletions(-) create mode 100644 harness/kotlin-protocol-runner/.gitignore create mode 100644 harness/kotlin-protocol-runner/build.gradle.kts create mode 100644 harness/kotlin-protocol-runner/settings.gradle.kts create mode 100644 harness/kotlin-protocol-runner/src/main/kotlin/com/solana/paykit/protocolrunner/Main.kt create mode 100644 harness/kotlin-protocol-runner/src/test/kotlin/com/solana/paykit/protocolrunner/MainTest.kt create mode 100644 harness/protocol-runners/kotlin.json diff --git a/.github/workflows/harness.yml b/.github/workflows/harness.yml index 80462416a..f5dfee41a 100644 --- a/.github/workflows/harness.yml +++ b/.github/workflows/harness.yml @@ -341,6 +341,14 @@ jobs: env: MPP_CONFORMANCE_LANGUAGES: kotlin run: pnpm exec vitest run test/conformance.test.ts + - name: Build the Kotlin protocol runner + working-directory: harness/kotlin-protocol-runner + run: gradle installDist --no-daemon + - name: Run Kotlin mpp-protocol conformance vectors + working-directory: harness + env: + MPP_CONFORMANCE_LANGUAGES: kotlin + run: pnpm exec vitest run test/protocol-conformance.test.ts - name: Pre-warm Gradle for the Kotlin harness client working-directory: harness/kotlin-client run: gradle installDist --no-daemon diff --git a/harness/kotlin-protocol-runner/.gitignore b/harness/kotlin-protocol-runner/.gitignore new file mode 100644 index 000000000..67bcc2f72 --- /dev/null +++ b/harness/kotlin-protocol-runner/.gitignore @@ -0,0 +1,2 @@ +.gradle/ +build/ diff --git a/harness/kotlin-protocol-runner/build.gradle.kts b/harness/kotlin-protocol-runner/build.gradle.kts new file mode 100644 index 000000000..3380edfe9 --- /dev/null +++ b/harness/kotlin-protocol-runner/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + kotlin("jvm") version "2.3.21" + application +} + +dependencies { + // Path-included build, see settings.gradle.kts. + implementation("com.solana.paykit:solana-pay-kit-kotlin") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") + testImplementation(kotlin("test")) +} + +kotlin { + jvmToolchain(17) +} + +application { + mainClass.set("com.solana.paykit.protocolrunner.MainKt") +} + +tasks.test { + useJUnitPlatform() +} diff --git a/harness/kotlin-protocol-runner/settings.gradle.kts b/harness/kotlin-protocol-runner/settings.gradle.kts new file mode 100644 index 000000000..80d7e332f --- /dev/null +++ b/harness/kotlin-protocol-runner/settings.gradle.kts @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + } +} + +rootProject.name = "mpp-kotlin-protocol-runner" +includeBuild("../../kotlin") diff --git a/harness/kotlin-protocol-runner/src/main/kotlin/com/solana/paykit/protocolrunner/Main.kt b/harness/kotlin-protocol-runner/src/main/kotlin/com/solana/paykit/protocolrunner/Main.kt new file mode 100644 index 000000000..58a415d3b --- /dev/null +++ b/harness/kotlin-protocol-runner/src/main/kotlin/com/solana/paykit/protocolrunner/Main.kt @@ -0,0 +1,91 @@ +package com.solana.paykit.protocolrunner + +import com.solana.paykit.protocols.mpp.core.MppHeaders +import com.solana.paykit.protocols.mpp.core.PaymentCredential +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import java.util.Base64 +import kotlin.system.exitProcess + +private val families = mapOf( + "challenge.parse" to "parse_error", + "credential.parse" to "parse_error", + "receipt.parse" to "parse_error", + "challenge.format" to "format_error", + "credential.format" to "format_error", + "receipt.format" to "format_error", + "base64url.encode" to "encoding_error", + "base64url.decode" to "encoding_error", + "challenge.id" to "generation_error", +) + +private val sdkGaps = families.keys - setOf("challenge.parse", "credential.format") + +fun main() { + val response = reply(System.`in`.readBytes().decodeToString()) + println(response) + if (response["error_type"] == JsonPrimitive("runner_error")) exitProcess(1) +} + +internal fun respond(line: String): String = reply(line).toString() + +private fun reply(line: String): JsonObject { + val (op, input) = try { + val request = Json.parseToJsonElement(line).jsonObject + request["op"]?.jsonPrimitive?.content.orEmpty() to (request["input"] ?: JsonNull) + } catch (error: IllegalArgumentException) { + return fail(error.message, "runner_error") + } + val family = families[op] ?: return fail("unknown operation: $op", "unsupported_operation") + if (op in sdkGaps) return fail("$op unsupported by the Kotlin SDK", family) + return try { + val result = if (op == "challenge.parse") parseChallenge(input) else formatCredential(input) + buildJsonObject { + put("success", true) + put("result", result) + } + } catch (error: Exception) { + fail(error.message, family) + } +} + +private fun fail(error: String?, errorType: String) = buildJsonObject { + put("success", false) + put("error", error) + put("error_type", errorType) +} + +private fun parseChallenge(input: JsonElement): JsonObject { + val challenge = MppHeaders.parseWWWAuthenticate(input.jsonObject.getValue("header").jsonPrimitive.content) + return buildJsonObject { + put("id", challenge.id) + put("realm", challenge.realm) + put("method", challenge.method) + put("intent", challenge.intent) + put("request", decodeJson(challenge.request)) + challenge.expires?.takeIf { it.isNotEmpty() }?.let { put("expires", it) } + challenge.digest?.takeIf { it.isNotEmpty() }?.let { put("digest", it) } + challenge.opaque?.let { put("opaque", decodeJson(it)) } + } +} + +private fun decodeJson(base64Url: String): JsonElement = + Json.parseToJsonElement(Base64.getUrlDecoder().decode(base64Url).decodeToString()) + +private fun formatCredential(input: JsonElement): JsonObject { + val credential = input.jsonObject + val challenge = credential.getValue("challenge").jsonObject + val request = (challenge["request"] ?: JsonObject(emptyMap())).toString().encodeToByteArray() + val echo = challenge + ("request" to JsonPrimitive(Base64.getUrlEncoder().withoutPadding().encodeToString(request))) + val wire = JsonObject(credential + ("challenge" to JsonObject(echo))) + return buildJsonObject { + put("header", MppHeaders.formatAuthorization(Json.decodeFromJsonElement(PaymentCredential.serializer(), wire))) + } +} diff --git a/harness/kotlin-protocol-runner/src/test/kotlin/com/solana/paykit/protocolrunner/MainTest.kt b/harness/kotlin-protocol-runner/src/test/kotlin/com/solana/paykit/protocolrunner/MainTest.kt new file mode 100644 index 000000000..c3ae43320 --- /dev/null +++ b/harness/kotlin-protocol-runner/src/test/kotlin/com/solana/paykit/protocolrunner/MainTest.kt @@ -0,0 +1,73 @@ +package com.solana.paykit.protocolrunner + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject +import java.util.Base64 +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class MainTest { + private fun answer(line: String): JsonObject { + val output = respond(line) + assertFalse('\n' in output) + return Json.parseToJsonElement(output).jsonObject + } + + private fun JsonObject.field(name: String) = getValue(name).jsonPrimitive.content + + @Test + fun parsesBasicChallengeWithRequestObject() { + val request = """{"amount":"1000000","currency":"0x20c0000000000000000000000000000000000001","recipient":"0x1234567890abcdef1234567890abcdef12345678"}""" + val encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(request.encodeToByteArray()) + val line = buildJsonObject { + put("op", "challenge.parse") + putJsonObject("input") { + put("header", """Payment id="ch_abc123", realm="api.example.com", method="tempo", intent="charge", request="$encoded"""") + } + }.toString() + + val response = answer(line) + + assertEquals("true", response.field("success")) + assertEquals( + Json.parseToJsonElement("""{"id":"ch_abc123","realm":"api.example.com","method":"tempo","intent":"charge","request":$request}"""), + response["result"], + ) + } + + @Test + fun refusesNonJson() { + assertEquals("runner_error", answer("not json").field("error_type")) + } + + @Test + fun unknownOpIsUnsupportedOperation() { + assertEquals("unsupported_operation", answer("""{"op":"nope.op","input":{}}""").field("error_type")) + } + + @Test + fun sdkGapAnswersItsFamilyErrorType() { + val response = answer("""{"op":"receipt.parse","input":{"header":"x"}}""") + + assertEquals("false", response.field("success")) + assertEquals("parse_error", response.field("error_type")) + assertTrue("unsupported" in response.field("error")) + } + + @Test + fun credentialPayloadHashIsAFormatError() { + val credential = """{"challenge":{"id":"ch","realm":"r","method":"tempo","intent":"charge","request":{}},"payload":{"type":"hash","hash":"0x12"}}""" + + val response = answer("""{"op":"credential.format","input":$credential}""") + + assertEquals("format_error", response.field("error_type")) + assertTrue("unknown key 'hash'" in response.field("error")) + } +} diff --git a/harness/protocol-runners/kotlin.json b/harness/protocol-runners/kotlin.json new file mode 100644 index 000000000..c4fde3360 --- /dev/null +++ b/harness/protocol-runners/kotlin.json @@ -0,0 +1,5 @@ +{ + "language": "kotlin", + "command": ["sh", "-c", "exec build/install/mpp-kotlin-protocol-runner/bin/mpp-kotlin-protocol-runner"], + "cwd": "harness/kotlin-protocol-runner" +} diff --git a/harness/test/protocol-conformance.test.ts b/harness/test/protocol-conformance.test.ts index 5468dbaf9..4d9582e39 100644 --- a/harness/test/protocol-conformance.test.ts +++ b/harness/test/protocol-conformance.test.ts @@ -9,6 +9,7 @@ import { describe, expect, it } from "vitest"; import { caseRunsOnAdapter, collectProtocolCases } from "../src/protocol/vectors"; import { runCase } from "../src/protocol/driver"; +import { parseLanguageAllowlist } from "../src/conformance/select"; import { typescriptProtocolAdapter } from "../src/protocol/runners/typescript"; import { discoverProtocolRunners, @@ -162,28 +163,61 @@ const smokeCases = (() => { // Each entry is `${op} :: ${scenario}` and is asserted to STILL diverge so the // gap fails loudly the moment the SDK conforms (mirrors KNOWN_TS_DIVERGENCES). // -// Empty: every SDK now conforms to the canonical receipt shape. The Go +// Only Kotlin's remain: every other SDK now conforms to the canonical receipt shape. The Go // (`challengeId:""` injected) and Ruby (`challengeId` hard-required) schema // mismatches on `receipt.parse :: success_receipt` were both fixed in the -// per-SDK protocol-conformance round, so there are no remaining known runner -// divergences. -const KNOWN_RUNNER_DIVERGENCES: Record> = {}; +// per-SDK protocol-conformance round. Each entry maps to the exact response the +// runner gives, so a runner that cannot start never passes as a divergence. +const kotlinUnsupported = (op: string, error_type: string) => ({ + success: false, + error: `${op} unsupported by the Kotlin SDK`, + error_type, +}); +// Kotlin's PaymentChallenge has no `description` field. +const kotlinWithoutDescription = { + success: true, + result: expect.not.objectContaining({ description: expect.anything() }), +}; +const KNOWN_RUNNER_DIVERGENCES: Record> = { + kotlin: new Map([ + ["base64url.encode :: empty_string", kotlinUnsupported("base64url.encode", "encoding_error")], + ["base64url.decode :: empty_string", kotlinUnsupported("base64url.decode", "encoding_error")], + ["challenge.id :: required_fields_only", kotlinUnsupported("challenge.id", "generation_error")], + ["challenge.format :: basic_challenge", kotlinUnsupported("challenge.format", "format_error")], + ["credential.parse :: basic_credential", kotlinUnsupported("credential.parse", "parse_error")], + ["receipt.parse :: success_receipt", kotlinUnsupported("receipt.parse", "parse_error")], + ["challenge.parse :: full_challenge", kotlinWithoutDescription], + ["challenge.parse :: escaped_quotes_in_description", kotlinWithoutDescription], + [ + "challenge.parse :: unescaped_quotes_in_description", + { success: false, error: "invalid Payment header", error_type: "parse_error" }, + ], + // Kotlin's CredentialPayload has no `hash` field. + [ + "credential.format :: credential_with_source", + { success: false, error: expect.stringContaining("unknown key 'hash'"), error_type: "format_error" }, + ], + ]), +}; -const runners = discoverProtocolRunners(); +const allowlist = parseLanguageAllowlist(process.env.MPP_CONFORMANCE_LANGUAGES); +const runners = discoverProtocolRunners().filter( + (runner) => !allowlist || allowlist.has(runner.language), +); for (const runner of runners) { - const known = KNOWN_RUNNER_DIVERGENCES[runner.language] ?? new Set(); + const known = KNOWN_RUNNER_DIVERGENCES[runner.language] ?? new Map(); + const keyOf = (testCase: (typeof cases)[number]) => `${testCase.op} :: ${testCase.scenario}`; describe(`mpp-protocol conformance (spawned ${runner.language} runner)`, () => { const adapter = spawnedProtocolAdapter(runner); - for (const testCase of smokeCases) { + for (const testCase of cases.filter((c) => smokeCases.includes(c) || known.has(keyOf(c)))) { if (!caseRunsOnAdapter(testCase, runner.language)) continue; - const key = `${testCase.op} :: ${testCase.scenario}`; + const key = keyOf(testCase); if (known.has(key)) { it(`KNOWN DIVERGENCE: ${key}`, async () => { - const result = await runCase(adapter, testCase); expect( - result.ok, - `${key} now conforms — remove from KNOWN_RUNNER_DIVERGENCES[${runner.language}]`, - ).toBe(false); + await adapter.runProtocolRequest({ op: testCase.op, input: testCase.input }), + `${key} changed — update or remove KNOWN_RUNNER_DIVERGENCES[${runner.language}]`, + ).toEqual(known.get(key)); }); continue; } @@ -192,5 +226,17 @@ for (const runner of runners) { expect(result.ok, result.detail).toBe(true); }); } + if (known.size === 0) return; + it("a runner that cannot start fails its known divergences", async () => { + const missing = spawnedProtocolAdapter({ + ...runner, + command: ["sh", "-c", "exec build/install/missing/bin/missing"], + }); + const [key, expected] = [...known][0]; + const testCase = cases.find((c) => keyOf(c) === key)!; + expect(await missing.runProtocolRequest({ op: testCase.op, input: testCase.input })).not.toEqual( + expected, + ); + }); }); } From 208fbd71c18b11869de8fb326eadd72ef91a2e1a Mon Sep 17 00:00:00 2001 From: caliperforge Date: Sat, 3 Oct 2026 06:46:29 -0600 Subject: [PATCH 2/6] greptile.json: review on request only --- greptile.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 greptile.json diff --git a/greptile.json b/greptile.json new file mode 100644 index 000000000..3e653b565 --- /dev/null +++ b/greptile.json @@ -0,0 +1 @@ +{"autoReview": []} From f400328de5e1c826ebefd5f290672c49a52393ed Mon Sep 17 00:00:00 2001 From: Michael Moffett Date: Sat, 3 Oct 2026 06:52:57 -0600 Subject: [PATCH 3/6] test(harness): Kotlin protocol runner, its manifest, the CI build-and-run steps in the kotlin harness job, and asserted Kotlin divergences in the spawned-runner matrix --- harness/test/protocol-conformance.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/harness/test/protocol-conformance.test.ts b/harness/test/protocol-conformance.test.ts index 4d9582e39..673840c6f 100644 --- a/harness/test/protocol-conformance.test.ts +++ b/harness/test/protocol-conformance.test.ts @@ -174,9 +174,10 @@ const kotlinUnsupported = (op: string, error_type: string) => ({ error_type, }); // Kotlin's PaymentChallenge has no `description` field. -const kotlinWithoutDescription = { - success: true, - result: expect.not.objectContaining({ description: expect.anything() }), +const kotlinWithoutDescription = (scenario: string) => { + const parse = cases.find((c) => c.op === "challenge.parse" && c.scenario === scenario); + const { description: _, ...result } = (parse as { golden: Record }).golden; + return { success: true, result }; }; const KNOWN_RUNNER_DIVERGENCES: Record> = { kotlin: new Map([ @@ -186,8 +187,12 @@ const KNOWN_RUNNER_DIVERGENCES: Record> = { ["challenge.format :: basic_challenge", kotlinUnsupported("challenge.format", "format_error")], ["credential.parse :: basic_credential", kotlinUnsupported("credential.parse", "parse_error")], ["receipt.parse :: success_receipt", kotlinUnsupported("receipt.parse", "parse_error")], - ["challenge.parse :: full_challenge", kotlinWithoutDescription], - ["challenge.parse :: escaped_quotes_in_description", kotlinWithoutDescription], + ["challenge.parse :: full_challenge", kotlinWithoutDescription("full_challenge")], + [ + "challenge.parse :: escaped_quotes_in_description", + kotlinWithoutDescription("escaped_quotes_in_description"), + ], + // Kotlin's parser rejects text after a closing quote; the canonical parser truncates there. [ "challenge.parse :: unescaped_quotes_in_description", { success: false, error: "invalid Payment header", error_type: "parse_error" }, From 17dd9a290b5764bd9444e9ae95abcf0c813c5888 Mon Sep 17 00:00:00 2001 From: Michael Moffett Date: Sat, 3 Oct 2026 13:46:12 -0600 Subject: [PATCH 4/6] test(harness): Rebuilt and retested the Kotlin protocol runner from scratch (--rerun-tasks); 5/5 MainTest cases pass, the vitest suite is still unrun --- harness/test/protocol-conformance.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harness/test/protocol-conformance.test.ts b/harness/test/protocol-conformance.test.ts index 673840c6f..8f03cada9 100644 --- a/harness/test/protocol-conformance.test.ts +++ b/harness/test/protocol-conformance.test.ts @@ -167,7 +167,7 @@ const smokeCases = (() => { // (`challengeId:""` injected) and Ruby (`challengeId` hard-required) schema // mismatches on `receipt.parse :: success_receipt` were both fixed in the // per-SDK protocol-conformance round. Each entry maps to the exact response the -// runner gives, so a runner that cannot start never passes as a divergence. +// runner gives. const kotlinUnsupported = (op: string, error_type: string) => ({ success: false, error: `${op} unsupported by the Kotlin SDK`, From e2eeda59f1f1c2f0b096088674d71eb697a74b98 Mon Sep 17 00:00:00 2001 From: Michael Moffett Date: Sat, 3 Oct 2026 13:51:36 -0600 Subject: [PATCH 5/6] test(harness): Kotlin block runs credential.format :: basic_credential as a must-pass case (TS reference re-parses Kotlin's header) and lists receipt.format as a known unsupported divergence --- harness/test/protocol-conformance.test.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/harness/test/protocol-conformance.test.ts b/harness/test/protocol-conformance.test.ts index 8f03cada9..41651fca9 100644 --- a/harness/test/protocol-conformance.test.ts +++ b/harness/test/protocol-conformance.test.ts @@ -8,7 +8,7 @@ import { describe, expect, it } from "vitest"; import { caseRunsOnAdapter, collectProtocolCases } from "../src/protocol/vectors"; -import { runCase } from "../src/protocol/driver"; +import { runCase, type ProtocolAdapter } from "../src/protocol/driver"; import { parseLanguageAllowlist } from "../src/conformance/select"; import { typescriptProtocolAdapter } from "../src/protocol/runners/typescript"; import { @@ -187,6 +187,7 @@ const KNOWN_RUNNER_DIVERGENCES: Record> = { ["challenge.format :: basic_challenge", kotlinUnsupported("challenge.format", "format_error")], ["credential.parse :: basic_credential", kotlinUnsupported("credential.parse", "parse_error")], ["receipt.parse :: success_receipt", kotlinUnsupported("receipt.parse", "parse_error")], + ["receipt.format :: success_receipt", kotlinUnsupported("receipt.format", "format_error")], ["challenge.parse :: full_challenge", kotlinWithoutDescription("full_challenge")], [ "challenge.parse :: escaped_quotes_in_description", @@ -205,16 +206,29 @@ const KNOWN_RUNNER_DIVERGENCES: Record> = { ]), }; +// Format cases whose paired parse op the runner lacks: the TS reference re-parses its wire. +const REFERENCE_REPARSED_CASES: Record> = { + kotlin: new Set(["credential.format :: basic_credential"]), +}; +const withReferenceReparse = (adapter: ProtocolAdapter, op: string): ProtocolAdapter => ({ + name: adapter.name, + runProtocolRequest: (request) => + (request.op === op ? adapter : typescriptProtocolAdapter).runProtocolRequest(request), +}); + const allowlist = parseLanguageAllowlist(process.env.MPP_CONFORMANCE_LANGUAGES); const runners = discoverProtocolRunners().filter( (runner) => !allowlist || allowlist.has(runner.language), ); for (const runner of runners) { const known = KNOWN_RUNNER_DIVERGENCES[runner.language] ?? new Map(); + const reparsed = REFERENCE_REPARSED_CASES[runner.language] ?? new Set(); const keyOf = (testCase: (typeof cases)[number]) => `${testCase.op} :: ${testCase.scenario}`; describe(`mpp-protocol conformance (spawned ${runner.language} runner)`, () => { const adapter = spawnedProtocolAdapter(runner); - for (const testCase of cases.filter((c) => smokeCases.includes(c) || known.has(keyOf(c)))) { + for (const testCase of cases.filter( + (c) => smokeCases.includes(c) || known.has(keyOf(c)) || reparsed.has(keyOf(c)), + )) { if (!caseRunsOnAdapter(testCase, runner.language)) continue; const key = keyOf(testCase); if (known.has(key)) { @@ -227,7 +241,10 @@ for (const runner of runners) { continue; } it(key, async () => { - const result = await runCase(adapter, testCase); + const result = await runCase( + reparsed.has(key) ? withReferenceReparse(adapter, testCase.op) : adapter, + testCase, + ); expect(result.ok, result.detail).toBe(true); }); } From 00614e3b3c5a111939624a71dd5f283769080e01 Mon Sep 17 00:00:00 2001 From: Michael Moffett Date: Mon, 5 Oct 2026 10:48:32 -0600 Subject: [PATCH 6/6] test(harness): CI builds and tests the Kotlin protocol runner (installDist test) and Main.kt gains its 3-line header comment --- .github/workflows/harness.yml | 4 ++-- .../src/main/kotlin/com/solana/paykit/protocolrunner/Main.kt | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/harness.yml b/.github/workflows/harness.yml index f5dfee41a..5ab7faeb5 100644 --- a/.github/workflows/harness.yml +++ b/.github/workflows/harness.yml @@ -341,9 +341,9 @@ jobs: env: MPP_CONFORMANCE_LANGUAGES: kotlin run: pnpm exec vitest run test/conformance.test.ts - - name: Build the Kotlin protocol runner + - name: Build and test the Kotlin protocol runner working-directory: harness/kotlin-protocol-runner - run: gradle installDist --no-daemon + run: gradle installDist test --no-daemon - name: Run Kotlin mpp-protocol conformance vectors working-directory: harness env: diff --git a/harness/kotlin-protocol-runner/src/main/kotlin/com/solana/paykit/protocolrunner/Main.kt b/harness/kotlin-protocol-runner/src/main/kotlin/com/solana/paykit/protocolrunner/Main.kt index 58a415d3b..ea32c2abc 100644 --- a/harness/kotlin-protocol-runner/src/main/kotlin/com/solana/paykit/protocolrunner/Main.kt +++ b/harness/kotlin-protocol-runner/src/main/kotlin/com/solana/paykit/protocolrunner/Main.kt @@ -1,3 +1,6 @@ +// Kotlin mpp-protocol conformance runner. +// Reads one adapter-ABI request on stdin and writes one response line on stdout, +// per the contract in harness/src/protocol/runners/spawn.ts. package com.solana.paykit.protocolrunner import com.solana.paykit.protocols.mpp.core.MppHeaders