KetraTerm Testkit
Repository test support for connector simulation, deterministic terminal replay, and independent conformance checks. Testkit is used by KetraTerm's own modules; it is not a published Maven library.
How to Use in Tests
Add project dependencies to the test source set that needs the helpers. Declare production APIs that your tests import directly; testkit does not re-export its implementation dependencies. The connector example below needs transport API:
// In a KetraTerm module's build.gradle.kts.
dependencies {
testImplementation(project(":ketraterm-testkit"))
testImplementation(project(":ketraterm-transport-api"))
}
MockConnector
MockConnector
implements the raw-byte connector contract. feedFromHost calls the installed
listener synchronously with the supplied array and slice; it does not copy or
parse incoming bytes. The listener must consume or copy that slice before the
callback returns. Outbound writes are copied, and writtenBytes returns a fresh
array containing all captured writes.
import io.github.ketraterm.testkit.MockConnector
import io.github.ketraterm.transport.TerminalConnectorListener
fun main() {
val connector = MockConnector()
var received = ""
var remoteExitCode: Int? = null
connector.start(object : TerminalConnectorListener {
override fun onBytes(bytes: ByteArray, offset: Int, length: Int) {
received = bytes.decodeToString(offset, offset + length)
}
override fun onClosed(exitCode: Int?) {
remoteExitCode = exitCode
}
override fun onError(error: Throwable) = throw error
})
connector.feedFromHost("hello".encodeToByteArray())
val reply = "reply".encodeToByteArray()
connector.write(reply, 0, reply.size)
check(received == "hello")
check(connector.writtenBytes.contentEquals(reply))
connector.simulateClosed(0)
check(remoteExitCode == 0)
connector.close()
check(connector.startCount == 1 && connector.closeCount == 1)
}
start accepts one listener and rejects restart or start after local closure.
close records each call and prevents subsequent writes and resizes; it does not
emit a remote exit callback. Trigger remote closure or failure explicitly with
simulateClosed or simulateCrash. These helpers notify the listener without
changing isClosed, which records local closure only. resizeCalls records
ordered (columns, rows) pairs. Serialize access to the fake; it does not add
concurrency control.
Headless conformance replay
TerminalConformanceHarness
wires the production parser, host adapter, core, response queue, and render reader
without a session, PTY, or UI. A harness retains state between apply, replay,
and snapshot calls. Create a fresh instance for each independent run.
A TerminalReplayTranscript
preserves input chunk boundaries, interleaved resizes, and explicit end-of-input
placement. Input events copy their source bytes. chunked and bytewise do not
append end-of-input automatically; TerminalReplayChunkings.exhaustive does so
by default.
import io.github.ketraterm.testkit.TerminalConformanceDiffer
import io.github.ketraterm.testkit.TerminalConformanceHarness
import io.github.ketraterm.testkit.TerminalReplayChunkings
fun assertChunkingInvariant(bytes: ByteArray) {
val variants = TerminalReplayChunkings.exhaustive(bytes)
val expected = TerminalConformanceHarness(80, 24).replay(variants.first().transcript)
for (variant in variants.drop(1)) {
val actual = TerminalConformanceHarness(80, 24).replay(variant.transcript)
val diff = TerminalConformanceDiffer.compare(expected, actual)
check(diff.isEmpty) { "${variant.name}: ${diff.format()}" }
}
}
Snapshots copy retained history and the live grid, including cell attributes,
clusters, hyperlinks, wraps, cursor, modes, host metadata, and cumulative response
bytes. They omit internal storage handles and generation counters.
TerminalConformanceDiffer returns bounded field-level differences.
exhaustive covers every two-way split plus selected fragmented partitions; its
quadratic byte copying is appropriate for small protocol fixtures.
Running Testkit Tests
The graphics sender corpus pins Yazi and Kitty serializer output independently of terminal implementation. Ordinary tests verify fixture integrity, decoded source pixels, and JDK PNG decoder assumptions. Regenerating the corpus is an explicit developer operation, not a CI download.
Run commands from the repository root with JDK 25; use gradlew.bat on Windows.
./gradlew :ketraterm-testkit:test
./gradlew :ketraterm-testkit:publishedConsumerTest
./gradlew :ketraterm-testkit:publicationVerificationTest
Ordinary test covers helper behavior, specification fixtures, model regressions,
and Java compilation against exported project API variants. It skips opt-in
oracle/generated campaigns and excludes the separate publication and retained
client suites. check also runs publishedConsumerTest; packaging verification
is separate and is included in the root publicationChecks aggregate. See the
published-consumer guide for prerequisites,
compiler/metadata combinations, and retained client baselines.
Generated campaigns
| Task | Default cases | Check |
|---|---|---|
xtermDifferentialTest |
2,000 generated cases plus fixed corpus | Independent headless xterm.js comparison. |
xtermDifferentialSmokeTest |
100 | Generated xterm.js comparison. |
xtermDifferentialNightlyTest |
100,000 | Generated xterm.js comparison. |
xtermDifferentialReleaseAudit |
500,000 | Generated xterm.js comparison. |
resizeReflowInvariantSmokeTest |
100 | Mixed-width text and repeated resize invariants. |
resizeReflowInvariantNightlyTest |
10,000 | Mixed-width text and repeated resize invariants. |
cursorWrapModelSmokeTest |
100 | Independent grid model comparison. |
cursorWrapModelNightlyTest |
25,000 | Independent grid model comparison. |
Invoke these tasks with the :ketraterm-testkit: prefix. xterm tasks require
Node.js and npm on PATH; they install locked dependencies with
npm ci --ignore-scripts and run the oracle's unit tests first. See the
oracle guide for process protocol and setup.
Resize/reflow and grid-model campaigns require no Node.js or native terminal.
Override case counts and deterministic shards with the matching Gradle properties:
| Campaign | Count | First index | Reports under module build/reports/ |
|---|---|---|---|
| xterm differential | xtermDifferentialCases |
xtermDifferentialStartIndex |
xterm-differential/ |
| Resize/reflow | resizeReflowCases |
resizeReflowStartIndex |
resize-reflow-invariant/ |
| Grid model | cursorWrapCases |
cursorWrapStartIndex |
cursor-wrap-model/ |
For example:
./gradlew :ketraterm-testkit:cursorWrapModelSmokeTest -PcursorWrapCases=250 -PcursorWrapStartIndex=500
Campaign reports retain deterministic replay metadata and manifests. Failures include reduced reproductions and structural differences. Differential tests compare the observable intersection with the oracle; intentional disagreements must name their rationale and exact mismatch paths. KetraTerm's resize/reflow policy is checked by its own invariants rather than treated as xterm parity.
See Module.md for helper structure and dependencies.