Skip to content

Commit 5ba0146

Browse files
authored
test: regression test for manaflow-ai#6618 shellState dedup race (#32) (#39)
* test: add failing regression test for manaflow-ai#6618 shellState dedup race (#32) The test references TerminalController.SocketFastPathState, which is currently private — so this commit does not compile. The access-widening fix follows in the next commit, demonstrating the test genuinely fails without it. * fix: widen SocketFastPathState to internal so the regression test compiles (#32)
1 parent a0c57b2 commit 5ba0146

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

Sources/TerminalController.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,9 @@ class TerminalController {
437437
let panelId: UUID
438438
}
439439

440-
private final class SocketFastPathState: @unchecked Sendable {
440+
// `internal` (not `private`) so the dedup logic can be exercised by
441+
// `TerminalControllerSocketSecurityTests` via `@testable import` (regression #6618).
442+
final class SocketFastPathState: @unchecked Sendable {
441443
private let queue = DispatchQueue(label: "com.cmux.socket-fast-path")
442444
private var lastReportedDirectories: [SocketSurfaceKey: String] = [:]
443445
private var lastReportedShellStates: [SocketSurfaceKey: Workspace.PanelShellActivityState] = [:]

cmuxTests/TerminalControllerSocketSecurityTests.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,39 @@ final class TerminalControllerSocketSecurityTests: XCTestCase {
2727
super.tearDown()
2828
}
2929

30+
/// Regression for #6618: `shouldPublishShellActivity` used to record the state
31+
/// it was queried with (write-on-read). When a report arrived before the panel
32+
/// existed, that premature write suppressed every later identical report, so the
33+
/// panel's shell state stayed `.unknown` forever. The read and the write are now
34+
/// split — `recordShellActivity` is the only writer and runs only after a
35+
/// confirmed main-thread apply.
36+
func testShellActivityDedupDoesNotSuppressWhenApplyWasNeverRecorded() {
37+
let fastPath = TerminalController.SocketFastPathState()
38+
let workspaceId = UUID()
39+
let panelId = UUID()
40+
let idle = Workspace.PanelShellActivityState.promptIdle
41+
42+
// First report for this surface must publish.
43+
XCTAssertTrue(fastPath.shouldPublishShellActivity(
44+
workspaceId: workspaceId, panelId: panelId, state: idle))
45+
46+
// The panel was absent, so the apply was never recorded. Querying again must
47+
// NOT have been suppressed by the previous read (the core of the #6618 bug).
48+
XCTAssertTrue(fastPath.shouldPublishShellActivity(
49+
workspaceId: workspaceId, panelId: panelId, state: idle))
50+
51+
// After a confirmed apply is recorded, the identical report is deduped.
52+
// recordShellActivity hops a serial queue; the subsequent sync read is FIFO-
53+
// ordered behind it, so this is deterministic without sleeping.
54+
fastPath.recordShellActivity(workspaceId: workspaceId, panelId: panelId, state: idle)
55+
XCTAssertFalse(fastPath.shouldPublishShellActivity(
56+
workspaceId: workspaceId, panelId: panelId, state: idle))
57+
58+
// A different state always publishes.
59+
XCTAssertTrue(fastPath.shouldPublishShellActivity(
60+
workspaceId: workspaceId, panelId: panelId, state: .commandRunning))
61+
}
62+
3063
func testSocketPermissionsFollowAccessMode() throws {
3164
let tabManager = TabManager()
3265

0 commit comments

Comments
 (0)