chore(test-seams): move _test*/_force* seams from public to internal

Per audit-2026-05-04 P2-14: VoiceController._forceState,
_testFireSpeechEnd, _testCurrentSttSessionId were declared `public`
solely so XCTest could see them. Same for MCPClient._testHandle and
MCPServerHandle._testProcessIdentifier. These leaked test-only entry
points into each package's public API — anyone importing Voice could
call _forceState(.speaking) from production code.

Voice (4 seams): public → internal. Voice tests already use
@testable import Voice, so no test-side change required.

MCP (2 seams): public → @_spi(Testing) public. MCP tests opt in
via @testable @_spi(Testing) import JarvisMCP. The Harness package
(non-test) consumes _testHandle / _testProcessIdentifier for the
MCPCrashRunner SIGKILL recipe — opt in via @_spi(Testing) import.
@testable alone does NOT expose @_spi symbols — the SPI must be
explicitly imported, which is the desired discipline (each consumer
acknowledges they're reaching past the public ABI).

6 test seams total moved off the public ABI. Voice + MCP tests
pass (87 + 85). Harness builds clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
james.maesandClaude Opus 4.7 committed 2026-05-04 16:22:14 -05:00
1 parent 263705d39d
commit 57847f2f58
5 files changed
+14 -8

No files matched your search

@@ -1,6 +1,6 @@
import Foundation
import Darwin
import JarvisMCP
@_spi(Testing) import JarvisMCP
import MCP
/// MCPCrashRunner — Plan 08-03 Task 2.
+4 -1
View File
@@ -189,7 +189,10 @@ public actor MCPClient {
#if DEBUG
/// Test-only accessor into the registry. Used by restart-mutex tests that
/// need to SIGKILL the helper PID directly. NOT for production use.
public func _testHandle(named name: String) -> MCPServerHandle? {
/// Gated by `@_spi(Testing)` so callers must opt in via
/// `@_spi(Testing) import JarvisMCP` (used by tests via `@testable` and
/// by the eval `Harness` package). Plain `import JarvisMCP` cannot see it.
@_spi(Testing) public func _testHandle(named name: String) -> MCPServerHandle? {
registry[name]
}
#endif
@@ -338,7 +338,8 @@ public actor MCPServerHandle {
#if DEBUG
/// Returns the helper's PID for tests that need to SIGKILL the child directly.
public func _testProcessIdentifier() -> Int32 {
/// Gated by `@_spi(Testing)` — see `MCPClient._testHandle` for rationale.
@_spi(Testing) public func _testProcessIdentifier() -> Int32 {
process?.processIdentifier ?? -1
}
#endif
@@ -11,7 +11,7 @@
import XCTest
import Darwin
import MCP
@testable import JarvisMCP
@testable @_spi(Testing) import JarvisMCP
final class MCPRestartTests: XCTestCase {
private var helperBinary: URL!
@@ -229,12 +229,14 @@ public actor VoiceController {
// MARK: - Test seams
/// Force the controller into a specific state. Tests only.
public func _forceState(_ newState: VoiceState) async {
/// `internal` + `@testable import Voice` keeps this out of the public
/// ABI so production callers (App, Harness) cannot reach in.
internal func _forceState(_ newState: VoiceState) async {
state = newState
}
/// Simulate a VAD speech-end + STT finalization with given text. Tests only.
public func _testFireSpeechEnd(text: String = "") async {
internal func _testFireSpeechEnd(text: String = "") async {
await handleSTTFinalized(text: text)
}
@@ -242,13 +244,13 @@ public actor VoiceController {
/// finalize path with an explicit `sessionId` so tests can simulate a
/// finalize callback from a prior session arriving after the next
/// session has started.
public func _testFireSpeechEnd(text: String, sessionId: UInt64) async {
internal func _testFireSpeechEnd(text: String, sessionId: UInt64) async {
await handleSTTFinalized(text: text, sessionId: sessionId)
}
/// P1-3 stale-text seam (tests only). Returns the current session id so
/// tests can capture it before triggering a session boundary.
public func _testCurrentSttSessionId() -> UInt64 {
internal func _testCurrentSttSessionId() -> UInt64 {
sttSessionId
}