From 5a4632d12804aa4ee6c561a80b214204bb144169 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 01:08:51 -0700
Subject: [PATCH 01/39] test: cover Copilot PreToolUse feed gating
---
cmux.xcodeproj/project.pbxproj | 4 +
cmuxTests/CLICopilotHookFeedTests.swift | 140 +++++++++++++++++++
cmuxTests/FeedEventClassificationTests.swift | 10 ++
3 files changed, 154 insertions(+)
create mode 100644 cmuxTests/CLICopilotHookFeedTests.swift
diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj
index 4ae277c093a8..cf374a37f69b 100644
--- a/cmux.xcodeproj/project.pbxproj
+++ b/cmux.xcodeproj/project.pbxproj
@@ -160,6 +160,7 @@
A9F200000000000000000015 /* ClaudeStreamJSONAccumulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F100000000000000000015 /* ClaudeStreamJSONAccumulator.swift */; };
A5D4120DA1B2C3D4E5F60718 /* CLIAuthAliasTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D4120EA1B2C3D4E5F60718 /* CLIAuthAliasTests.swift */; };
C0D3F1F00000000000000103 /* CLICodexHookTimeoutRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F1F00000000000000104 /* CLICodexHookTimeoutRegressionTests.swift */; };
+ C6574C0A0000000000000001 /* CLICopilotHookFeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6574C0A0000000000000002 /* CLICopilotHookFeedTests.swift */; };
C46790000000000000000001 /* CLIForwardingLaunchArgumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46790000000000000000002 /* CLIForwardingLaunchArgumentTests.swift */; };
C46790000000000000000003 /* CLIForwardingLaunchRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46790000000000000000004 /* CLIForwardingLaunchRouter.swift */; };
A5D41207A1B2C3D4E5F60718 /* CLIGenericHookPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D41208A1B2C3D4E5F60718 /* CLIGenericHookPersistenceTests.swift */; };
@@ -1266,6 +1267,7 @@
A9F100000000000000000015 /* ClaudeStreamJSONAccumulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/ClaudeStreamJSONAccumulator.swift; sourceTree = ""; };
A5D4120EA1B2C3D4E5F60718 /* CLIAuthAliasTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIAuthAliasTests.swift; sourceTree = ""; };
C0D3F1F00000000000000104 /* CLICodexHookTimeoutRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexHookTimeoutRegressionTests.swift; sourceTree = ""; };
+ C6574C0A0000000000000002 /* CLICopilotHookFeedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICopilotHookFeedTests.swift; sourceTree = ""; };
C46790000000000000000002 /* CLIForwardingLaunchArgumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIForwardingLaunchArgumentTests.swift; sourceTree = ""; };
C46790000000000000000004 /* CLIForwardingLaunchRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/CLIForwardingLaunchRouter.swift; sourceTree = ""; };
A5D41208A1B2C3D4E5F60718 /* CLIGenericHookPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIGenericHookPersistenceTests.swift; sourceTree = ""; };
@@ -3220,6 +3222,7 @@
C12984000000000000000003 /* CLIStdioSIGPIPERegressionTests.swift */,
B05553B10000000000000002 /* CLITmuxCompatRemoteSplitTests.swift */,
C0D3F1F00000000000000104 /* CLICodexHookTimeoutRegressionTests.swift */,
+ C6574C0A0000000000000002 /* CLICopilotHookFeedTests.swift */,
A5D41208A1B2C3D4E5F60718 /* CLIGenericHookPersistenceTests.swift */,
A5D41212A1B2C3D4E5F60718 /* CLIHookNoResponseTests.swift */,
A5D4120AA1B2C3D4E5F60718 /* CLIRovoDevHookPersistenceTests.swift */,
@@ -4427,6 +4430,7 @@
A5D41220A1B2C3D4E5F60718 /* ClaudeHookSurfaceResolutionSwiftTests.swift in Sources */,
A5D4120DA1B2C3D4E5F60718 /* CLIAuthAliasTests.swift in Sources */,
C0D3F1F00000000000000103 /* CLICodexHookTimeoutRegressionTests.swift in Sources */,
+ C6574C0A0000000000000001 /* CLICopilotHookFeedTests.swift in Sources */,
C46790000000000000000001 /* CLIForwardingLaunchArgumentTests.swift in Sources */,
A5D41207A1B2C3D4E5F60718 /* CLIGenericHookPersistenceTests.swift in Sources */,
A5D41211A1B2C3D4E5F60718 /* CLIHookNoResponseTests.swift in Sources */,
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
new file mode 100644
index 000000000000..b486379e4484
--- /dev/null
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -0,0 +1,140 @@
+import Darwin
+import XCTest
+
+extension CLINotifyProcessIntegrationRegressionTests {
+ func testCopilotHookInstallWritesToHooksSubdirectory() throws {
+ let cliPath = try bundledCLIPath()
+ let root = FileManager.default.temporaryDirectory
+ .appendingPathComponent("cmux-copilot-hook-install-\(UUID().uuidString)", isDirectory: true)
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
+ defer { try? FileManager.default.removeItem(at: root) }
+
+ let result = runProcess(
+ executablePath: cliPath,
+ arguments: ["hooks", "copilot", "install", "--yes"],
+ environment: [
+ "HOME": root.path,
+ "PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
+ "CMUX_CLI_SENTRY_DISABLED": "1",
+ ],
+ timeout: 5
+ )
+
+ XCTAssertFalse(result.timedOut, result.stderr)
+ XCTAssertEqual(result.status, 0, result.stderr)
+
+ let hookURL = root
+ .appendingPathComponent(".copilot", isDirectory: true)
+ .appendingPathComponent("hooks", isDirectory: true)
+ .appendingPathComponent("cmux.json", isDirectory: false)
+ let json = try XCTUnwrap(
+ JSONSerialization.jsonObject(with: Data(contentsOf: hookURL)) as? [String: Any],
+ "Expected hook file at ~/.copilot/hooks/cmux.json"
+ )
+
+ XCTAssertEqual(json["version"] as? Int, 1)
+ let hooks = try XCTUnwrap(json["hooks"] as? [String: Any])
+
+ XCTAssertNotNil(hooks["SessionStart"], "Missing SessionStart hook")
+ XCTAssertNotNil(hooks["Stop"], "Missing Stop hook")
+ XCTAssertNotNil(hooks["Notification"], "Missing Notification hook")
+ XCTAssertNotNil(hooks["SessionEnd"], "Missing SessionEnd hook")
+ let preToolUse = try XCTUnwrap(hooks["PreToolUse"] as? [[String: Any]])
+ let preToolUseCommands = preToolUse
+ .compactMap { $0["hooks"] as? [[String: Any]] }
+ .flatMap { $0 }
+ XCTAssertTrue(
+ preToolUseCommands.contains {
+ ($0["command"] as? String)?.contains("hooks feed --source copilot --event PreToolUse") == true
+ && ($0["timeout"] as? Int) == 120_000
+ },
+ "Expected PreToolUse feed hook with 120 000 ms timeout, saw \(preToolUseCommands)"
+ )
+ }
+
+ func testCopilotFeedDecisionEmitsPreToolUsePermissionDecision() throws {
+ func runCopilotDecision(mode: String) throws -> (ProcessRunResult, [String: Any]) {
+ let cliPath = try bundledCLIPath()
+ let socketPath = makeSocketPath("copilot-feed-decision")
+ let listenerFD = try bindUnixSocket(at: socketPath)
+ let state = MockSocketServerState()
+ let root = FileManager.default.temporaryDirectory
+ .appendingPathComponent("cmux-copilot-feed-decision-\(UUID().uuidString)", isDirectory: true)
+ let workspaceId = "33333333-3333-3333-3333-333333333333"
+ let surfaceId = "44444444-4444-4444-4444-444444444444"
+
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
+ defer {
+ Darwin.close(listenerFD)
+ unlink(socketPath)
+ try? FileManager.default.removeItem(at: root)
+ }
+
+ let serverHandled = startMockServer(listenerFD: listenerFD, state: state) { line in
+ guard let payload = self.jsonObject(line) else {
+ return self.malformedRequestResponse(raw: line)
+ }
+ guard let id = payload["id"] as? String, let method = payload["method"] as? String else {
+ return self.malformedRequestResponse(id: payload["id"] as? String, raw: line)
+ }
+ XCTAssertEqual(method, "feed.push")
+ return self.v2Response(
+ id: id,
+ ok: true,
+ result: [
+ "status": "resolved",
+ "decision": ["kind": "permission", "mode": mode],
+ ]
+ )
+ }
+
+ let result = runProcess(
+ executablePath: cliPath,
+ arguments: ["hooks", "feed", "--source", "copilot", "--event", "PreToolUse"],
+ environment: [
+ "HOME": root.path,
+ "PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
+ "PWD": root.path,
+ "CMUX_SOCKET_PATH": socketPath,
+ "CMUX_WORKSPACE_ID": workspaceId,
+ "CMUX_SURFACE_ID": surfaceId,
+ "CMUX_COPILOT_PID": "525252",
+ "CMUX_CLI_SENTRY_DISABLED": "1",
+ ],
+ standardInput: #"{"hook_event_name":"PreToolUse","session_id":"copilot-session-123","cwd":"\#(root.path)","tool_name":"Bash","tool_input":{"command":"touch \#(root.appendingPathComponent("README.md").path)"}}"#,
+ timeout: 5
+ )
+ wait(for: [serverHandled], timeout: 5)
+
+ let feedEvents = state.commands.compactMap { command -> [String: Any]? in
+ guard let payload = self.jsonObject(command),
+ payload["method"] as? String == "feed.push",
+ let params = payload["params"] as? [String: Any],
+ let event = params["event"] as? [String: Any] else {
+ return nil
+ }
+ return event
+ }
+ XCTAssertEqual(feedEvents.count, 1, "Expected one Copilot Feed event, saw \(state.commands)")
+ return (result, try XCTUnwrap(feedEvents.first))
+ }
+
+ let (allow, allowEvent) = try runCopilotDecision(mode: "once")
+ XCTAssertFalse(allow.timedOut, allow.stderr)
+ XCTAssertEqual(allow.status, 0, allow.stderr)
+ XCTAssertEqual(allowEvent["hook_event_name"] as? String, "PermissionRequest")
+ XCTAssertEqual(allowEvent["_source"] as? String, "copilot")
+ XCTAssertEqual(allowEvent["_ppid"] as? Int, 525252)
+ let allowOutput = try XCTUnwrap(jsonObject(allow.stdout))
+ XCTAssertEqual(allowOutput["permissionDecision"] as? String, "allow")
+ XCTAssertNil(allowOutput["hookSpecificOutput"])
+
+ let (deny, _) = try runCopilotDecision(mode: "deny")
+ XCTAssertFalse(deny.timedOut, deny.stderr)
+ XCTAssertEqual(deny.status, 0, deny.stderr)
+ let denyOutput = try XCTUnwrap(jsonObject(deny.stdout))
+ XCTAssertEqual(denyOutput["permissionDecision"] as? String, "deny")
+ XCTAssertEqual(denyOutput["permissionDecisionReason"] as? String, "User denied permission via cmux Feed.")
+ XCTAssertNil(denyOutput["hookSpecificOutput"])
+ }
+}
diff --git a/cmuxTests/FeedEventClassificationTests.swift b/cmuxTests/FeedEventClassificationTests.swift
index fbfb10beef02..714823084ff9 100644
--- a/cmuxTests/FeedEventClassificationTests.swift
+++ b/cmuxTests/FeedEventClassificationTests.swift
@@ -93,6 +93,16 @@ struct FeedEventClassificationTests {
#expect(classify("gemini", "PreToolUse", tool: "Read").actionable == false)
}
+ /// Copilot's `PreToolUse` hook is the approval gate itself. It must
+ /// produce a blocking `PermissionRequest` even for tools that generic
+ /// telemetry-only agents would keep non-actionable.
+ @Test func copilotPreToolUseIsApprovalRequest() {
+ #expect(classify("copilot", "PreToolUse", tool: "Bash").name == "PermissionRequest")
+ #expect(classify("copilot", "PreToolUse", tool: "Bash").actionable == true)
+ #expect(classify("copilot", "PreToolUse", tool: "Read").name == "PermissionRequest")
+ #expect(classify("copilot", "PreToolUse", tool: "Read").actionable == true)
+ }
+
/// Even on the maybe-approval (generic pre-tool) path, the two dedicated
/// approval tool names route to their own wire kinds — they are never
/// collapsed into a generic `PermissionRequest`. Guards the shared
From 899cca3bda64a85187990d5b155ff5fc75db5c3e Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 01:08:56 -0700
Subject: [PATCH 02/39] fix: gate Copilot PreToolUse feed hooks
---
CLI/CMUXCLI+AgentHookDefinitions.swift | 2 +-
CLI/FeedEventClassifier.swift | 5 +++++
CLI/cmux.swift | 4 ++--
docs/agent-hooks.md | 2 +-
docs/feed.md | 2 +-
docs/notifications.md | 2 +-
web/app/[locale]/docs/notifications/page.tsx | 2 +-
7 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/CLI/CMUXCLI+AgentHookDefinitions.swift b/CLI/CMUXCLI+AgentHookDefinitions.swift
index 6e0e4ba7875c..1b59ef561c6c 100644
--- a/CLI/CMUXCLI+AgentHookDefinitions.swift
+++ b/CLI/CMUXCLI+AgentHookDefinitions.swift
@@ -311,7 +311,7 @@ extension CMUXCLI {
),
AgentHookDef(
name: "copilot", displayName: "Copilot", statusKey: "copilot",
- configDir: ".copilot", configFile: "config.json", configDirEnvOverride: "COPILOT_HOME",
+ configDir: ".copilot/hooks", configFile: "cmux.json", configDirEnvOverride: "COPILOT_HOME", configDirEnvOverrideSubpath: "hooks", createConfigDirIfMissing: true,
sessionStoreSuffix: "copilot", disableEnvVar: "CMUX_COPILOT_HOOKS_DISABLED",
hookMarker: "cmux hooks copilot", format: .nested(timeoutMs: 5000),
events: [
diff --git a/CLI/FeedEventClassifier.swift b/CLI/FeedEventClassifier.swift
index 7f6de0613f6d..c9e3e22c9174 100644
--- a/CLI/FeedEventClassifier.swift
+++ b/CLI/FeedEventClassifier.swift
@@ -216,6 +216,11 @@ struct FeedEventClassifier {
"agentSpawn": .sessionStart,
"stop": .response,
],
+ "copilot": [
+ // Copilot's PreToolUse hook is the permission gate itself:
+ // the hook output directly controls whether the tool runs.
+ "PreToolUse": .approvalRequest,
+ ],
]
/// Fallback table for agents without a dedicated entry in
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index 6217e60f27a4..c03edab641d1 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -28234,7 +28234,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
existing["hooks"] = hooks
- if case .flat = def.format { existing["version"] = 1 }
+ if case .flat = def.format { existing["version"] = 1 } else if def.name == "copilot" { existing["version"] = 1 }
if case .kiroAgentJSON = def.format {
if existing["name"] == nil {
existing["name"] = "cmux"
@@ -33210,7 +33210,6 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
return out
}
-
func hermesAgentBlock(_ message: String) -> String {
encode(["action": "block", "message": message])
}
@@ -33243,6 +33242,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
return encode(permissionRequestHookDecision(behavior: "allow"))
}
+ if source == "copilot" { return encode(mode == "deny" ? ["permissionDecision": "deny", "permissionDecisionReason": "User denied permission via cmux Feed."] : ["permissionDecision": "allow"]) }
if source == "hermes-agent" {
if mode == "deny" {
return hermesAgentBlock("User denied permission via cmux Feed.")
diff --git a/docs/agent-hooks.md b/docs/agent-hooks.md
index e918800425cb..2b4b530f2682 100644
--- a/docs/agent-hooks.md
+++ b/docs/agent-hooks.md
@@ -28,7 +28,7 @@ Supported agent names are `codex`, `grok`, `opencode`, `pi`, `omp`, `amp`, `curs
| Gemini | `gemini` | `~/.gemini/settings.json` | `gemini --resume ` | PreToolUse |
| Kiro CLI | `kiro-cli` | `~/.kiro/agents/cmux.json` or `$KIRO_HOME/agents/cmux.json` | `kiro-cli chat --resume-id ` | preToolUse, postToolUse |
| Rovo Dev | `acli` | `~/.rovodev/config.yml` | `acli rovodev run --restore ` | none |
-| Copilot | `copilot` | `~/.copilot/config.json` | `copilot --resume ` | PreToolUse |
+| Copilot | `copilot` | `~/.copilot/hooks/cmux.json` | `copilot --resume ` | PreToolUse |
| CodeBuddy | `codebuddy` | `~/.codebuddy/settings.json` | `codebuddy --resume ` | PreToolUse |
| Factory | `droid` | `~/.factory/settings.json` | `droid --resume ` | PreToolUse |
| Qoder | `qodercli` | `~/.qoder/settings.json` | `qodercli --resume ` | PreToolUse |
diff --git a/docs/feed.md b/docs/feed.md
index 43d77f1a87c2..9ea233b7098f 100644
--- a/docs/feed.md
+++ b/docs/feed.md
@@ -80,7 +80,7 @@ Installs supported agent hooks whose binaries are on `PATH`. See [Agent hook int
| OpenCode | `~/.config/opencode/plugins/cmux-feed.js` | plugin event bus |
| Cursor CLI | `~/.cursor/hooks.json` | beforeShellExecution |
| Gemini | `~/.gemini/settings.json` | PreToolUse |
-| Copilot | `~/.copilot/config.json` | PreToolUse |
+| Copilot | `~/.copilot/hooks/cmux.json` | PreToolUse |
| CodeBuddy | `~/.codebuddy/settings.json` | PreToolUse |
| Factory | `~/.factory/settings.json` | PreToolUse |
| Qoder | `~/.qoder/settings.json` | PreToolUse |
diff --git a/docs/notifications.md b/docs/notifications.md
index 8240448e2976..23f15fd55acc 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -117,7 +117,7 @@ See the [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-co
### GitHub Copilot CLI
-Copilot CLI supports [hooks](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks) that run shell commands at key lifecycle events. Add to `~/.copilot/config.json`:
+Copilot CLI supports [hooks](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks) that run shell commands at key lifecycle events. Add to `~/.copilot/hooks/cmux.json`:
```json
{
diff --git a/web/app/[locale]/docs/notifications/page.tsx b/web/app/[locale]/docs/notifications/page.tsx
index 0a90069c27ec..82dd6bb436ac 100644
--- a/web/app/[locale]/docs/notifications/page.tsx
+++ b/web/app/[locale]/docs/notifications/page.tsx
@@ -266,7 +266,7 @@ esac`}
),
})}
- {`{
+ {`{
"hooks": {
"userPromptSubmitted": [
{
From af195109b0ebebc1a212e0c5f153420f75dec4e7 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 01:17:51 -0700
Subject: [PATCH 03/39] fix: address Copilot hook review feedback
---
CLI/CMUXCLI+AgentHookDefinitions.swift | 8 ++++----
CLI/cmux.swift | 4 ++--
Resources/Localizable.xcstrings | 17 +++++++++++++++++
docs/notifications.md | 1 +
web/app/[locale]/docs/notifications/page.tsx | 1 +
5 files changed, 25 insertions(+), 6 deletions(-)
diff --git a/CLI/CMUXCLI+AgentHookDefinitions.swift b/CLI/CMUXCLI+AgentHookDefinitions.swift
index 1b59ef561c6c..dcc49bc732e2 100644
--- a/CLI/CMUXCLI+AgentHookDefinitions.swift
+++ b/CLI/CMUXCLI+AgentHookDefinitions.swift
@@ -20,7 +20,7 @@ extension CMUXCLI {
let binaryName: String
let format: HookFormat
let events: [HookEvent]
- let aliases: Set
+ let aliases: Set; let requiresVersionKey: Bool
let publishesStopNotification: Bool
/// Whether this agent's `SessionEnd`/`session-end` hook fires once per
/// conversation turn rather than at a true session teardown.
@@ -106,7 +106,7 @@ extension CMUXCLI {
binaryName: String? = nil,
sessionStoreSuffix: String, disableEnvVar: String, hookMarker: String,
format: HookFormat, events: [HookEvent],
- aliases: Set = [],
+ aliases: Set = [], requiresVersionKey: Bool = false,
publishesStopNotification: Bool = true,
sessionEndIsTurnBoundary: Bool = false,
feedHookEvents: [String] = [],
@@ -127,7 +127,7 @@ extension CMUXCLI {
let normalized = alias.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return normalized.isEmpty ? nil : normalized
})
- self.feedHookEvents = feedHookEvents
+ self.feedHookEvents = feedHookEvents; self.requiresVersionKey = requiresVersionKey
self.postInstallAction = postInstallAction
self.postInstallNote = postInstallNote
}
@@ -320,7 +320,7 @@ extension CMUXCLI {
.init(agentEvent: "Notification", cmuxSubcommand: "stop"),
.init(agentEvent: "SessionEnd", cmuxSubcommand: "session-end"),
],
- feedHookEvents: ["PreToolUse"]
+ requiresVersionKey: true, feedHookEvents: ["PreToolUse"]
),
AgentHookDef(
name: "codebuddy", displayName: "CodeBuddy", statusKey: "codebuddy",
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index c03edab641d1..1b9e14314479 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -28234,7 +28234,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
existing["hooks"] = hooks
- if case .flat = def.format { existing["version"] = 1 } else if def.name == "copilot" { existing["version"] = 1 }
+ if case .flat = def.format { existing["version"] = 1 }; if def.requiresVersionKey { existing["version"] = 1 }
if case .kiroAgentJSON = def.format {
if existing["name"] == nil {
existing["name"] = "cmux"
@@ -33242,7 +33242,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
return encode(permissionRequestHookDecision(behavior: "allow"))
}
- if source == "copilot" { return encode(mode == "deny" ? ["permissionDecision": "deny", "permissionDecisionReason": "User denied permission via cmux Feed."] : ["permissionDecision": "allow"]) }
+ if source == "copilot" { return encode(mode == "deny" ? ["permissionDecision": "deny", "permissionDecisionReason": String(localized: "cli.hooks.feed.permissionDeniedReason", defaultValue: "User denied permission via cmux Feed.")] : ["permissionDecision": "allow"]) }
if source == "hermes-agent" {
if mode == "deny" {
return hermesAgentBlock("User denied permission via cmux Feed.")
diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings
index 4859958b0be4..6b1671116f8a 100644
--- a/Resources/Localizable.xcstrings
+++ b/Resources/Localizable.xcstrings
@@ -672,6 +672,23 @@
}
}
},
+ "cli.hooks.feed.permissionDeniedReason": {
+ "extractionState": "manual",
+ "localizations": {
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "User denied permission via cmux Feed."
+ }
+ },
+ "ja": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "ユーザーが cmux Feed で権限を拒否しました。"
+ }
+ }
+ }
+ },
"cli.hooks.omp.aborted": {
"extractionState": "manual",
"localizations": {
diff --git a/docs/notifications.md b/docs/notifications.md
index 23f15fd55acc..ebd5bedd7638 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -121,6 +121,7 @@ Copilot CLI supports [hooks](https://docs.github.com/en/copilot/how-tos/use-copi
```json
{
+ "version": 1,
"hooks": {
"userPromptSubmitted": [
{
diff --git a/web/app/[locale]/docs/notifications/page.tsx b/web/app/[locale]/docs/notifications/page.tsx
index 82dd6bb436ac..e91b09be2f8a 100644
--- a/web/app/[locale]/docs/notifications/page.tsx
+++ b/web/app/[locale]/docs/notifications/page.tsx
@@ -267,6 +267,7 @@ esac`}
})}
{`{
+ "version": 1,
"hooks": {
"userPromptSubmitted": [
{
From 2e41944a3626605d8cf9fbf7630fb91fd2427e1d Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 01:29:35 -0700
Subject: [PATCH 04/39] fix: write Copilot hooks with direct schema
---
CLI/CMUXCLI+AgentHookDefinitions.swift | 12 +++++------
CLI/cmux.swift | 28 ++++++++++++-------------
cmuxTests/CLICopilotHookFeedTests.swift | 11 +++++-----
3 files changed, 25 insertions(+), 26 deletions(-)
diff --git a/CLI/CMUXCLI+AgentHookDefinitions.swift b/CLI/CMUXCLI+AgentHookDefinitions.swift
index dcc49bc732e2..ab29d5ab7f1c 100644
--- a/CLI/CMUXCLI+AgentHookDefinitions.swift
+++ b/CLI/CMUXCLI+AgentHookDefinitions.swift
@@ -20,7 +20,7 @@ extension CMUXCLI {
let binaryName: String
let format: HookFormat
let events: [HookEvent]
- let aliases: Set; let requiresVersionKey: Bool
+ let aliases: Set
let publishesStopNotification: Bool
/// Whether this agent's `SessionEnd`/`session-end` hook fires once per
/// conversation turn rather than at a true session teardown.
@@ -53,7 +53,7 @@ extension CMUXCLI {
enum HookFormat {
case flat // Cursor: {"hooks": {"event": [{"command": "..."}]}, "version": 1}
- case nested(timeoutMs: Int) // Nested type/command/timeout hooks; timeout unit is agent-specific.
+ case nested(timeoutMs: Int), copilotJSON(timeoutSeconds: Int) // Agent-specific command hook JSON.
case kiroAgentJSON(timeoutMs: Int) // ~/.kiro/agents/*.json flat command entries with timeout_ms
case antigravityJSON(timeoutSeconds: Int) // ~/.gemini/config/hooks.json named hook groups
case rovoDevYAML
@@ -106,7 +106,7 @@ extension CMUXCLI {
binaryName: String? = nil,
sessionStoreSuffix: String, disableEnvVar: String, hookMarker: String,
format: HookFormat, events: [HookEvent],
- aliases: Set = [], requiresVersionKey: Bool = false,
+ aliases: Set = [],
publishesStopNotification: Bool = true,
sessionEndIsTurnBoundary: Bool = false,
feedHookEvents: [String] = [],
@@ -127,7 +127,7 @@ extension CMUXCLI {
let normalized = alias.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return normalized.isEmpty ? nil : normalized
})
- self.feedHookEvents = feedHookEvents; self.requiresVersionKey = requiresVersionKey
+ self.feedHookEvents = feedHookEvents
self.postInstallAction = postInstallAction
self.postInstallNote = postInstallNote
}
@@ -313,14 +313,14 @@ extension CMUXCLI {
name: "copilot", displayName: "Copilot", statusKey: "copilot",
configDir: ".copilot/hooks", configFile: "cmux.json", configDirEnvOverride: "COPILOT_HOME", configDirEnvOverrideSubpath: "hooks", createConfigDirIfMissing: true,
sessionStoreSuffix: "copilot", disableEnvVar: "CMUX_COPILOT_HOOKS_DISABLED",
- hookMarker: "cmux hooks copilot", format: .nested(timeoutMs: 5000),
+ hookMarker: "cmux hooks copilot", format: .copilotJSON(timeoutSeconds: 5),
events: [
.init(agentEvent: "SessionStart", cmuxSubcommand: "session-start"),
.init(agentEvent: "Stop", cmuxSubcommand: "stop"),
.init(agentEvent: "Notification", cmuxSubcommand: "stop"),
.init(agentEvent: "SessionEnd", cmuxSubcommand: "session-end"),
],
- requiresVersionKey: true, feedHookEvents: ["PreToolUse"]
+ feedHookEvents: ["PreToolUse"]
),
AgentHookDef(
name: "codebuddy", displayName: "CodeBuddy", statusKey: "codebuddy",
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index 1b9e14314479..4a049f00824a 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -26965,10 +26965,7 @@ struct CMUXCLI {
Self.hookCommandString(for: def, event: event)
}
- /// Shell command the agent runs for a Feed bridge event. 120s timeout
- /// inside the shell is applied via the agent's `timeout` field in the
- /// nested hook config (see `buildHooksDict`); the shell command
- /// itself just dispatches.
+ /// Shell command the agent runs for a Feed bridge event.
func feedHookCommand(for def: AgentHookDef, agentEvent: String) -> String {
Self.feedHookCommandString(for: def, agentEvent: agentEvent)
}
@@ -26996,6 +26993,8 @@ struct CMUXCLI {
"hooks": [["type": "command", "command": cmd, "timeout": timeout] as [String: Any]]
] as [String: Any])
result[event.agentEvent] = groups
+ case .copilotJSON(let timeoutSeconds):
+ var entries = result[event.agentEvent] as? [[String: Any]] ?? []; entries.append(Self.copilotHookEntry(command: cmd, timeoutSeconds: timeoutSeconds)); result[event.agentEvent] = entries
case .antigravityJSON(let timeoutSeconds):
var entries = result[event.agentEvent] as? [[String: Any]] ?? []
entries.append(Self.antigravityHookEntry(
@@ -27008,10 +27007,7 @@ struct CMUXCLI {
break
}
}
- // Layer in Feed bridge entries. Blocking approval bridges get a long
- // timeout; Codex telemetry stays short so it never delays Codex's own
- // approval reviewer. Most nested agents use milliseconds. Codex, Grok,
- // and Antigravity hook schemas use seconds, so normalize before writing.
+ // Layer in Feed bridge entries; blocking approvals get a long timeout.
for agentEvent in def.feedHookEvents {
let feedCmd = feedHookCommand(for: def, agentEvent: agentEvent)
let feedTimeoutMs = feedHookTimeoutMs(for: def, agentEvent: agentEvent)
@@ -27034,6 +27030,8 @@ struct CMUXCLI {
"hooks": [["type": "command", "command": feedCmd, "timeout": timeout] as [String: Any]]
] as [String: Any])
result[agentEvent] = groups
+ case .copilotJSON:
+ var entries = result[agentEvent] as? [[String: Any]] ?? []; entries.append(Self.copilotHookEntry(command: feedCmd, timeoutSeconds: Self.timeoutSecondsFromMilliseconds(feedTimeoutMs))); result[agentEvent] = entries
case .antigravityJSON:
var entries = result[agentEvent] as? [[String: Any]] ?? []
entries.append(Self.antigravityHookEntry(
@@ -27071,6 +27069,8 @@ struct CMUXCLI {
return ((positiveTimeoutMs - 1) / 1000) + 1
}
+ private static func copilotHookEntry(command: String, timeoutSeconds: Int) -> [String: Any] { ["type": "command", "command": command, "timeoutSec": max(timeoutSeconds, 1)] }
+
private static func antigravityHookEntry(
command: String,
timeoutSeconds: Int,
@@ -28151,11 +28151,11 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
var cmuxInsertionIndexes: [String: [Int]] = [:]
for (event, value) in hooks {
switch def.format {
- case .flat, .kiroAgentJSON:
+ case .flat, .kiroAgentJSON, .copilotJSON:
guard let entries = value as? [[String: Any]] else { continue }
var rewrittenEntries: [[String: Any]] = []
for entry in entries {
- if isCmuxOwnedCommand(entry["command"] as? String ?? "") {
+ if Self.jsonHookValueContainsCmuxOwnedCommand(entry, for: def) {
Self.appendCmuxHookInsertionIndex(
rewrittenEntries.count,
for: event,
@@ -28208,7 +28208,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
// Add new cmux entries
for (event, value) in newHooks {
switch def.format {
- case .flat, .kiroAgentJSON:
+ case .flat, .kiroAgentJSON, .copilotJSON:
var entries = hooks[event] as? [[String: Any]] ?? []
if let newEntries = value as? [[String: Any]] {
if let insertionIndexes = cmuxInsertionIndexes[event], !insertionIndexes.isEmpty {
@@ -28234,7 +28234,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
existing["hooks"] = hooks
- if case .flat = def.format { existing["version"] = 1 }; if def.requiresVersionKey { existing["version"] = 1 }
+ if case .flat = def.format { existing["version"] = 1 }; if case .copilotJSON = def.format { existing["version"] = 1 }
if case .kiroAgentJSON = def.format {
if existing["name"] == nil {
existing["name"] = "cmux"
@@ -28488,10 +28488,10 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
for (event, value) in hooks {
switch def.format {
- case .flat, .kiroAgentJSON:
+ case .flat, .kiroAgentJSON, .copilotJSON:
guard var entries = value as? [[String: Any]] else { continue }
let before = entries.count
- entries.removeAll { isCmuxOwnedCommand($0["command"] as? String ?? "") }
+ entries.removeAll { Self.jsonHookValueContainsCmuxOwnedCommand($0, for: def) }
removed += before - entries.count
if entries.isEmpty {
hooks.removeValue(forKey: event)
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index b486379e4484..a9ccec95c32a 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -40,15 +40,14 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertNotNil(hooks["Notification"], "Missing Notification hook")
XCTAssertNotNil(hooks["SessionEnd"], "Missing SessionEnd hook")
let preToolUse = try XCTUnwrap(hooks["PreToolUse"] as? [[String: Any]])
- let preToolUseCommands = preToolUse
- .compactMap { $0["hooks"] as? [[String: Any]] }
- .flatMap { $0 }
XCTAssertTrue(
- preToolUseCommands.contains {
+ preToolUse.contains {
($0["command"] as? String)?.contains("hooks feed --source copilot --event PreToolUse") == true
- && ($0["timeout"] as? Int) == 120_000
+ && ($0["type"] as? String) == "command"
+ && ($0["timeoutSec"] as? Int) == 120
+ && $0["hooks"] == nil
},
- "Expected PreToolUse feed hook with 120 000 ms timeout, saw \(preToolUseCommands)"
+ "Expected direct PreToolUse command hook with 120s timeout, saw \(preToolUse)"
)
}
From b759c638fdb4d5718ae920cea270a8ea1c810276 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 01:31:57 -0700
Subject: [PATCH 05/39] fix: address Copilot decision review feedback
---
CLI/cmux.swift | 26 ++++----
Resources/Localizable.xcstrings | 108 ++++++++++++++++++++++++++++++++
2 files changed, 121 insertions(+), 13 deletions(-)
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index 4a049f00824a..3e7d4027054b 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -33106,12 +33106,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
private static func emitKiroDecisionIfHandled(decision: [String: Any]) -> Bool {
guard (decision["kind"] as? String) == "permission" else { return false }
let mode = (decision["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
- // Feed permission decisions carry a WorkstreamPermissionMode raw value:
- // `once` / `always` / `all` / `bypass` all allow the tool; `deny`
- // blocks. Fail closed on anything else — missing, empty, or an
- // unrecognized/typo mode — so a malformed decision blocks the tool
- // (exit 2 is Kiro's preToolUse deny signal) rather than silently
- // allowing work the user never approved.
+ // Fail closed on unknown Feed permission modes so malformed Kiro decisions never allow unapproved work.
let allowModes: Set = ["once", "always", "all", "bypass"]
if let mode, allowModes.contains(mode) {
print("{}")
@@ -33126,9 +33121,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
private static let skipInterviewAndPlanAnswer = "Skip interview and plan immediately"
-
- /// Encodes the user's decision in the agent's expected hook stdout
- /// shape so the agent honors it.
+ /// Encodes the user's decision in the agent's expected hook stdout shape.
private static func renderAgentDecision(
source: String,
hookEventName: String,
@@ -33172,9 +33165,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
]
}
- // PreToolUse output for non-Claude agents that still use a
- // PreToolUse-compatible permission bridge. Claude Code does not
- // use this path.
+ // PreToolUse-compatible permission bridge output for non-Claude agents.
func nonClaudePreToolDecision(
permission: String,
reason: String?,
@@ -33242,7 +33233,16 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
return encode(permissionRequestHookDecision(behavior: "allow"))
}
- if source == "copilot" { return encode(mode == "deny" ? ["permissionDecision": "deny", "permissionDecisionReason": String(localized: "cli.hooks.feed.permissionDeniedReason", defaultValue: "User denied permission via cmux Feed.")] : ["permissionDecision": "allow"]) }
+ if source == "copilot" {
+ if mode == "deny" {
+ let reason = String(
+ localized: "cli.hooks.feed.permissionDeniedReason",
+ defaultValue: "User denied permission via cmux Feed."
+ )
+ return encode(["permissionDecision": "deny", "permissionDecisionReason": reason])
+ }
+ return encode(["permissionDecision": "allow"])
+ }
if source == "hermes-agent" {
if mode == "deny" {
return hermesAgentBlock("User denied permission via cmux Feed.")
diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings
index 6b1671116f8a..72f46a758c73 100644
--- a/Resources/Localizable.xcstrings
+++ b/Resources/Localizable.xcstrings
@@ -675,17 +675,125 @@
"cli.hooks.feed.permissionDeniedReason": {
"extractionState": "manual",
"localizations": {
+ "ar": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "رفض المستخدم الإذن عبر cmux Feed."
+ }
+ },
+ "bs": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Korisnik je odbio dozvolu putem cmux Feeda."
+ }
+ },
+ "da": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Brugeren afviste tilladelsen via cmux Feed."
+ }
+ },
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Der Benutzer hat die Berechtigung über cmux Feed verweigert."
+ }
+ },
"en": {
"stringUnit": {
"state": "translated",
"value": "User denied permission via cmux Feed."
}
},
+ "es": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "El usuario denegó el permiso mediante cmux Feed."
+ }
+ },
+ "fr": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "L’utilisateur a refusé l’autorisation via cmux Feed."
+ }
+ },
+ "it": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "L’utente ha negato l’autorizzazione tramite cmux Feed."
+ }
+ },
"ja": {
"stringUnit": {
"state": "translated",
"value": "ユーザーが cmux Feed で権限を拒否しました。"
}
+ },
+ "km": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "អ្នកប្រើបានបដិសេធការអនុញ្ញាតតាមរយៈ cmux Feed។"
+ }
+ },
+ "ko": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "사용자가 cmux Feed에서 권한을 거부했습니다."
+ }
+ },
+ "nb": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Brukeren avslo tillatelsen via cmux Feed."
+ }
+ },
+ "pl": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Użytkownik odmówił uprawnienia przez cmux Feed."
+ }
+ },
+ "pt-BR": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "O usuário negou a permissão pelo cmux Feed."
+ }
+ },
+ "ru": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Пользователь отклонил разрешение через cmux Feed."
+ }
+ },
+ "th": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "ผู้ใช้ปฏิเสธสิทธิ์ผ่าน cmux Feed"
+ }
+ },
+ "tr": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Kullanıcı izni cmux Feed üzerinden reddetti."
+ }
+ },
+ "uk": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Користувач відхилив дозвіл через cmux Feed."
+ }
+ },
+ "zh-Hans": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "用户已通过 cmux Feed 拒绝权限。"
+ }
+ },
+ "zh-Hant": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "使用者已透過 cmux Feed 拒絕權限。"
+ }
}
}
},
From 07c39ad3f796cbd33035497a994467560cabe6c6 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 01:33:09 -0700
Subject: [PATCH 06/39] fix: clean up Copilot hook version logic
---
CLI/cmux.swift | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index 3e7d4027054b..3928a688c0b4 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -28234,7 +28234,10 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
existing["hooks"] = hooks
- if case .flat = def.format { existing["version"] = 1 }; if case .copilotJSON = def.format { existing["version"] = 1 }
+ switch def.format {
+ case .flat, .copilotJSON: existing["version"] = 1
+ default: break
+ }
if case .kiroAgentJSON = def.format {
if existing["name"] == nil {
existing["name"] = "cmux"
@@ -33235,10 +33238,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
if source == "copilot" {
if mode == "deny" {
- let reason = String(
- localized: "cli.hooks.feed.permissionDeniedReason",
- defaultValue: "User denied permission via cmux Feed."
- )
+ let reason = String(localized: "cli.hooks.feed.permissionDeniedReason", defaultValue: "User denied permission via cmux Feed.")
return encode(["permissionDecision": "deny", "permissionDecisionReason": reason])
}
return encode(["permissionDecision": "allow"])
From b28221d47ce83ef9e6c7ba41c57968a3ed5e75ad Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 01:41:09 -0700
Subject: [PATCH 07/39] fix: add Copilot hook timeout slack
---
CLI/cmux.swift | 2 +-
cmuxTests/CLICopilotHookFeedTests.swift | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index 3928a688c0b4..40f99fff3893 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -27031,7 +27031,7 @@ struct CMUXCLI {
] as [String: Any])
result[agentEvent] = groups
case .copilotJSON:
- var entries = result[agentEvent] as? [[String: Any]] ?? []; entries.append(Self.copilotHookEntry(command: feedCmd, timeoutSeconds: Self.timeoutSecondsFromMilliseconds(feedTimeoutMs))); result[agentEvent] = entries
+ var entries = result[agentEvent] as? [[String: Any]] ?? []; entries.append(Self.copilotHookEntry(command: feedCmd, timeoutSeconds: Self.timeoutSecondsFromMilliseconds(feedTimeoutMs) + 5)); result[agentEvent] = entries
case .antigravityJSON:
var entries = result[agentEvent] as? [[String: Any]] ?? []
entries.append(Self.antigravityHookEntry(
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index a9ccec95c32a..1aaf44bd6e3e 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -44,10 +44,10 @@ extension CLINotifyProcessIntegrationRegressionTests {
preToolUse.contains {
($0["command"] as? String)?.contains("hooks feed --source copilot --event PreToolUse") == true
&& ($0["type"] as? String) == "command"
- && ($0["timeoutSec"] as? Int) == 120
+ && ($0["timeoutSec"] as? Int) == 125
&& $0["hooks"] == nil
},
- "Expected direct PreToolUse command hook with 120s timeout, saw \(preToolUse)"
+ "Expected direct PreToolUse command hook with timeout slack, saw \(preToolUse)"
)
}
From bba9f387f191d4df5f7c15c5f5a0b06a40066230 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 01:55:22 -0700
Subject: [PATCH 08/39] fix: keep Copilot PreToolUse on permission schema
---
CLI/FeedEventClassifier.swift | 7 +++++--
cmuxTests/FeedEventClassificationTests.swift | 2 ++
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/CLI/FeedEventClassifier.swift b/CLI/FeedEventClassifier.swift
index c9e3e22c9174..25bb192cb721 100644
--- a/CLI/FeedEventClassifier.swift
+++ b/CLI/FeedEventClassifier.swift
@@ -47,8 +47,8 @@ struct FeedEventClassifier {
private enum FeedEventSemantic {
/// A real approval is pending; the user must approve/deny. Drives
/// the blocking Feed wait and the "needs approval" notification.
- /// Resolved against the tool name so Claude's `ExitPlanMode` /
- /// `AskUserQuestion` approvals route to their dedicated kinds.
+ /// Resolved against the tool name for agents whose response schema
+ /// supports dedicated `ExitPlanMode` / `AskUserQuestion` decisions.
case approvalRequest
/// A tool is about to run but no approval is pending. Telemetry
/// only. Used by agents that expose a *separate* approval event
@@ -109,6 +109,9 @@ struct FeedEventClassifier {
) -> (String, Bool) {
switch semantic {
case .approvalRequest:
+ if source == "copilot" {
+ return ("PermissionRequest", true)
+ }
return dedicatedApprovalEvent(for: toolName) ?? ("PermissionRequest", true)
case .toolStartMaybeApproval:
if let dedicated = dedicatedApprovalEvent(for: toolName) {
diff --git a/cmuxTests/FeedEventClassificationTests.swift b/cmuxTests/FeedEventClassificationTests.swift
index 714823084ff9..daeacfdab60f 100644
--- a/cmuxTests/FeedEventClassificationTests.swift
+++ b/cmuxTests/FeedEventClassificationTests.swift
@@ -101,6 +101,8 @@ struct FeedEventClassificationTests {
#expect(classify("copilot", "PreToolUse", tool: "Bash").actionable == true)
#expect(classify("copilot", "PreToolUse", tool: "Read").name == "PermissionRequest")
#expect(classify("copilot", "PreToolUse", tool: "Read").actionable == true)
+ #expect(classify("copilot", "PreToolUse", tool: "AskUserQuestion").name == "PermissionRequest")
+ #expect(classify("copilot", "PreToolUse", tool: "ExitPlanMode").name == "PermissionRequest")
}
/// Even on the maybe-approval (generic pre-tool) path, the two dedicated
From 2ec1d7b1b9276b639bfcd0ae04748d814406bcb7 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 02:04:54 -0700
Subject: [PATCH 09/39] fix: restrict Copilot feed permission modes
---
CLI/CodexTeamsApprovalBridge.swift | 4 +-
CLI/cmux.swift | 8 ++--
Sources/Feed/FeedPermissionActionPolicy.swift | 4 +-
cmuxTests/CLICopilotHookFeedTests.swift | 8 ++++
cmuxTests/FeedCoordinatorTests.swift | 42 +++++++++----------
5 files changed, 36 insertions(+), 30 deletions(-)
diff --git a/CLI/CodexTeamsApprovalBridge.swift b/CLI/CodexTeamsApprovalBridge.swift
index ce6e99c0f8b9..c60a5e903fbe 100644
--- a/CLI/CodexTeamsApprovalBridge.swift
+++ b/CLI/CodexTeamsApprovalBridge.swift
@@ -197,7 +197,7 @@ enum CodexTeamsApprovalBridge {
}
static func feedSourceSupportsPersistentPermissionModes(_ source: String) -> Bool {
- source != "hermes-agent"
+ source != "copilot" && source != "hermes-agent"
}
static func feedSourceSupportsOncePermissionMode(_ source: String, toolInputJSON: String?) -> Bool {
@@ -218,7 +218,7 @@ enum CodexTeamsApprovalBridge {
}
static func feedSourceSupportsBypassPermissions(_ source: String) -> Bool {
- source != "codex" && source != "claude" && source != "hermes-agent"
+ source != "codex" && source != "claude" && source != "copilot" && source != "hermes-agent"
}
static func requestIdString(_ requestId: Any) -> String {
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index 40f99fff3893..d950012fc5d3 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -33237,11 +33237,11 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
return encode(permissionRequestHookDecision(behavior: "allow"))
}
if source == "copilot" {
- if mode == "deny" {
- let reason = String(localized: "cli.hooks.feed.permissionDeniedReason", defaultValue: "User denied permission via cmux Feed.")
- return encode(["permissionDecision": "deny", "permissionDecisionReason": reason])
+ if mode == "once" {
+ return encode(["permissionDecision": "allow"])
}
- return encode(["permissionDecision": "allow"])
+ let reason = String(localized: "cli.hooks.feed.permissionDeniedReason", defaultValue: "User denied permission via cmux Feed.")
+ return encode(["permissionDecision": "deny", "permissionDecisionReason": reason])
}
if source == "hermes-agent" {
if mode == "deny" {
diff --git a/Sources/Feed/FeedPermissionActionPolicy.swift b/Sources/Feed/FeedPermissionActionPolicy.swift
index 8aaa62054b8a..a9f176d64728 100644
--- a/Sources/Feed/FeedPermissionActionPolicy.swift
+++ b/Sources/Feed/FeedPermissionActionPolicy.swift
@@ -5,7 +5,7 @@ enum FeedPermissionActionPolicy {
private typealias CodexPermissionCapabilities = (supportsOnce: Bool, supportsAlways: Bool, supportsAll: Bool)
static func supportsPersistentPermissionModes(source: WorkstreamSource) -> Bool {
- source != .hermesAgent
+ source != .copilot && source != .hermesAgent
}
static func supportsOncePermissionMode(source: WorkstreamSource, toolInputJSON: String?) -> Bool {
@@ -26,7 +26,7 @@ enum FeedPermissionActionPolicy {
}
static func supportsBypassPermissions(source: WorkstreamSource) -> Bool {
- source != .codex && source != .claude && source != .hermesAgent
+ source != .codex && source != .claude && source != .copilot && source != .hermesAgent
}
static func codexCapabilityToolInputJSON(source: WorkstreamSource, toolInputJSON: String) -> String? {
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index 1aaf44bd6e3e..1384d33afd84 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -135,5 +135,13 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertEqual(denyOutput["permissionDecision"] as? String, "deny")
XCTAssertEqual(denyOutput["permissionDecisionReason"] as? String, "User denied permission via cmux Feed.")
XCTAssertNil(denyOutput["hookSpecificOutput"])
+
+ let (unsupported, _) = try runCopilotDecision(mode: "always")
+ XCTAssertFalse(unsupported.timedOut, unsupported.stderr)
+ XCTAssertEqual(unsupported.status, 0, unsupported.stderr)
+ let unsupportedOutput = try XCTUnwrap(jsonObject(unsupported.stdout))
+ XCTAssertEqual(unsupportedOutput["permissionDecision"] as? String, "deny")
+ XCTAssertEqual(unsupportedOutput["permissionDecisionReason"] as? String, "User denied permission via cmux Feed.")
+ XCTAssertNil(unsupportedOutput["hookSpecificOutput"])
}
}
diff --git a/cmuxTests/FeedCoordinatorTests.swift b/cmuxTests/FeedCoordinatorTests.swift
index c18dd8744133..ed041eb76a2d 100644
--- a/cmuxTests/FeedCoordinatorTests.swift
+++ b/cmuxTests/FeedCoordinatorTests.swift
@@ -10,6 +10,15 @@ import CMUXAgentLaunch
@Suite("Feed coordinator", .serialized)
struct FeedCoordinatorTests {
+ private func expectPermissionCapabilities(source: WorkstreamSource, cliSource: String, toolInputJSON: String?, once: Bool, always: Bool, all: Bool) {
+ #expect(FeedPermissionActionPolicy.supportsOncePermissionMode(source: source, toolInputJSON: toolInputJSON) == once)
+ #expect(FeedPermissionActionPolicy.supportsAlwaysPermissionMode(source: source, toolInputJSON: toolInputJSON) == always)
+ #expect(FeedPermissionActionPolicy.supportsAllPermissionMode(source: source, toolInputJSON: toolInputJSON) == all)
+ #expect(CodexTeamsApprovalBridge.feedSourceSupportsOncePermissionMode(cliSource, toolInputJSON: toolInputJSON) == once)
+ #expect(CodexTeamsApprovalBridge.feedSourceSupportsAlwaysPermissionMode(cliSource, toolInputJSON: toolInputJSON) == always)
+ #expect(CodexTeamsApprovalBridge.feedSourceSupportsAllPermissionMode(cliSource, toolInputJSON: toolInputJSON) == all)
+ }
+
@Test func codexTeamsResolvesExplicitWorkingDirectoryFlags() {
let base = "/tmp/cmux-base"
@@ -70,45 +79,34 @@ struct FeedCoordinatorTests {
#expect(CodexTeamsApprovalBridge.feedSourceSupportsPersistentPermissionModes("codex"))
#expect(!CodexTeamsApprovalBridge.feedSourceSupportsBypassPermissions("codex"))
+ expectPermissionCapabilities(source: .copilot, cliSource: "copilot", toolInputJSON: nil, once: true, always: false, all: false)
+ #expect(!FeedPermissionActionPolicy.supportsPersistentPermissionModes(source: .copilot))
+ #expect(!FeedPermissionActionPolicy.supportsBypassPermissions(source: .copilot))
+ #expect(!CodexTeamsApprovalBridge.feedSourceSupportsPersistentPermissionModes("copilot"))
+ #expect(!CodexTeamsApprovalBridge.feedSourceSupportsBypassPermissions("copilot"))
+
let codexOneShotOnly = #"""
{"app_server_method":"item/commandExecution/requestApproval","available_decisions":["accept","decline"]}
"""#
- #expect(FeedPermissionActionPolicy.supportsOncePermissionMode(source: .codex, toolInputJSON: codexOneShotOnly))
- #expect(!FeedPermissionActionPolicy.supportsAlwaysPermissionMode(source: .codex, toolInputJSON: codexOneShotOnly))
- #expect(!FeedPermissionActionPolicy.supportsAllPermissionMode(source: .codex, toolInputJSON: codexOneShotOnly))
- #expect(CodexTeamsApprovalBridge.feedSourceSupportsOncePermissionMode("codex", toolInputJSON: codexOneShotOnly))
- #expect(!CodexTeamsApprovalBridge.feedSourceSupportsAlwaysPermissionMode("codex", toolInputJSON: codexOneShotOnly))
- #expect(!CodexTeamsApprovalBridge.feedSourceSupportsAllPermissionMode("codex", toolInputJSON: codexOneShotOnly))
+ expectPermissionCapabilities(source: .codex, cliSource: "codex", toolInputJSON: codexOneShotOnly, once: true, always: false, all: false)
let codexSession = #"""
{"app_server_method":"item/commandExecution/requestApproval","available_decisions":["accept","acceptForSession","decline"]}
"""#
- #expect(FeedPermissionActionPolicy.supportsAlwaysPermissionMode(source: .codex, toolInputJSON: codexSession))
- #expect(CodexTeamsApprovalBridge.feedSourceSupportsAlwaysPermissionMode("codex", toolInputJSON: codexSession))
+ expectPermissionCapabilities(source: .codex, cliSource: "codex", toolInputJSON: codexSession, once: true, always: true, all: false)
let truncatedCodexToolInput = #"{"app_server_method":"item/commandExecution/requestApproval","available_decisions":["accept"]"#
- #expect(!FeedPermissionActionPolicy.supportsOncePermissionMode(source: .codex, toolInputJSON: truncatedCodexToolInput))
- #expect(!FeedPermissionActionPolicy.supportsAlwaysPermissionMode(source: .codex, toolInputJSON: truncatedCodexToolInput))
- #expect(!FeedPermissionActionPolicy.supportsAllPermissionMode(source: .codex, toolInputJSON: truncatedCodexToolInput))
- #expect(!CodexTeamsApprovalBridge.feedSourceSupportsOncePermissionMode("codex", toolInputJSON: truncatedCodexToolInput))
- #expect(!CodexTeamsApprovalBridge.feedSourceSupportsAlwaysPermissionMode("codex", toolInputJSON: truncatedCodexToolInput))
- #expect(!CodexTeamsApprovalBridge.feedSourceSupportsAllPermissionMode("codex", toolInputJSON: truncatedCodexToolInput))
+ expectPermissionCapabilities(source: .codex, cliSource: "codex", toolInputJSON: truncatedCodexToolInput, once: false, always: false, all: false)
let codexFileChangeFallback = #"""
{"app_server_method":"item/fileChange/requestApproval"}
"""#
- #expect(FeedPermissionActionPolicy.supportsAlwaysPermissionMode(source: .codex, toolInputJSON: codexFileChangeFallback))
- #expect(CodexTeamsApprovalBridge.feedSourceSupportsAlwaysPermissionMode("codex", toolInputJSON: codexFileChangeFallback))
+ expectPermissionCapabilities(source: .codex, cliSource: "codex", toolInputJSON: codexFileChangeFallback, once: true, always: true, all: false)
let codexAmendment = #"""
{"app_server_method":"item/commandExecution/requestApproval","available_decisions":[{"acceptWithExecpolicyAmendment":{}}],"proposed_execpolicy_amendment":[{"kind":"prefix","value":"npm test"}]}
"""#
- #expect(!FeedPermissionActionPolicy.supportsOncePermissionMode(source: .codex, toolInputJSON: codexAmendment))
- #expect(!FeedPermissionActionPolicy.supportsAlwaysPermissionMode(source: .codex, toolInputJSON: codexAmendment))
- #expect(FeedPermissionActionPolicy.supportsAllPermissionMode(source: .codex, toolInputJSON: codexAmendment))
- #expect(!CodexTeamsApprovalBridge.feedSourceSupportsOncePermissionMode("codex", toolInputJSON: codexAmendment))
- #expect(!CodexTeamsApprovalBridge.feedSourceSupportsAlwaysPermissionMode("codex", toolInputJSON: codexAmendment))
- #expect(CodexTeamsApprovalBridge.feedSourceSupportsAllPermissionMode("codex", toolInputJSON: codexAmendment))
+ expectPermissionCapabilities(source: .codex, cliSource: "codex", toolInputJSON: codexAmendment, once: false, always: false, all: true)
#expect(FeedPermissionActionPolicy.supportsPersistentPermissionModes(source: .opencode))
#expect(FeedPermissionActionPolicy.supportsBypassPermissions(source: .opencode))
From bd85d15c9c29d7f912119bba6a6a3ad41459de00 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 02:14:27 -0700
Subject: [PATCH 10/39] test: convert Copilot hook feed coverage to Swift
Testing
---
cmuxTests/CLICopilotHookFeedTests.swift | 353 ++++++++++++++++++++----
1 file changed, 301 insertions(+), 52 deletions(-)
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index 1384d33afd84..58f9f38c3d80 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -1,15 +1,52 @@
import Darwin
-import XCTest
+import Foundation
+import Testing
-extension CLINotifyProcessIntegrationRegressionTests {
- func testCopilotHookInstallWritesToHooksSubdirectory() throws {
- let cliPath = try bundledCLIPath()
+@Suite("CLI Copilot hook feed")
+struct CLICopilotHookFeedTests {
+ final class BundleProbe {}
+
+ struct ProcessRunResult {
+ let status: Int32
+ let stdout: String
+ let stderr: String
+ let timedOut: Bool
+ }
+
+ final class MockSocketServerState: @unchecked Sendable {
+ private let lock = NSLock()
+ private var commands: [String] = []
+
+ func append(_ command: String) {
+ lock.lock()
+ commands.append(command)
+ lock.unlock()
+ }
+
+ func snapshot() -> [String] {
+ lock.lock()
+ let value = commands
+ lock.unlock()
+ return value
+ }
+ }
+
+ struct MockSocketServer {
+ let handled: DispatchSemaphore
+
+ func wait(timeout: TimeInterval) -> Bool {
+ handled.wait(timeout: .now() + timeout) == .success
+ }
+ }
+
+ @Test func copilotHookInstallWritesToHooksSubdirectory() throws {
+ let cliPath = try Self.bundledCLIPath()
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-copilot-hook-install-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
- let result = runProcess(
+ let result = Self.runProcess(
executablePath: cliPath,
arguments: ["hooks", "copilot", "install", "--yes"],
environment: [
@@ -20,27 +57,27 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ #expect(!result.timedOut, Comment(rawValue: result.stderr))
+ #expect(result.status == 0, Comment(rawValue: result.stderr))
let hookURL = root
.appendingPathComponent(".copilot", isDirectory: true)
.appendingPathComponent("hooks", isDirectory: true)
.appendingPathComponent("cmux.json", isDirectory: false)
- let json = try XCTUnwrap(
+ let json = try #require(
JSONSerialization.jsonObject(with: Data(contentsOf: hookURL)) as? [String: Any],
"Expected hook file at ~/.copilot/hooks/cmux.json"
)
- XCTAssertEqual(json["version"] as? Int, 1)
- let hooks = try XCTUnwrap(json["hooks"] as? [String: Any])
+ #expect(json["version"] as? Int == 1)
+ let hooks = try #require(json["hooks"] as? [String: Any])
- XCTAssertNotNil(hooks["SessionStart"], "Missing SessionStart hook")
- XCTAssertNotNil(hooks["Stop"], "Missing Stop hook")
- XCTAssertNotNil(hooks["Notification"], "Missing Notification hook")
- XCTAssertNotNil(hooks["SessionEnd"], "Missing SessionEnd hook")
- let preToolUse = try XCTUnwrap(hooks["PreToolUse"] as? [[String: Any]])
- XCTAssertTrue(
+ #expect(hooks["SessionStart"] != nil, "Missing SessionStart hook")
+ #expect(hooks["Stop"] != nil, "Missing Stop hook")
+ #expect(hooks["Notification"] != nil, "Missing Notification hook")
+ #expect(hooks["SessionEnd"] != nil, "Missing SessionEnd hook")
+ let preToolUse = try #require(hooks["PreToolUse"] as? [[String: Any]])
+ #expect(
preToolUse.contains {
($0["command"] as? String)?.contains("hooks feed --source copilot --event PreToolUse") == true
&& ($0["type"] as? String) == "command"
@@ -51,11 +88,11 @@ extension CLINotifyProcessIntegrationRegressionTests {
)
}
- func testCopilotFeedDecisionEmitsPreToolUsePermissionDecision() throws {
+ @Test func copilotFeedDecisionEmitsPreToolUsePermissionDecision() throws {
func runCopilotDecision(mode: String) throws -> (ProcessRunResult, [String: Any]) {
- let cliPath = try bundledCLIPath()
- let socketPath = makeSocketPath("copilot-feed-decision")
- let listenerFD = try bindUnixSocket(at: socketPath)
+ let cliPath = try Self.bundledCLIPath()
+ let socketPath = Self.makeSocketPath("copilot-feed-decision")
+ let listenerFD = try Self.bindUnixSocket(at: socketPath)
let state = MockSocketServerState()
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-copilot-feed-decision-\(UUID().uuidString)", isDirectory: true)
@@ -69,15 +106,15 @@ extension CLINotifyProcessIntegrationRegressionTests {
try? FileManager.default.removeItem(at: root)
}
- let serverHandled = startMockServer(listenerFD: listenerFD, state: state) { line in
- guard let payload = self.jsonObject(line) else {
- return self.malformedRequestResponse(raw: line)
+ let server = Self.startMockServer(listenerFD: listenerFD, state: state) { line in
+ guard let payload = Self.jsonObject(line) else {
+ return Self.malformedRequestResponse(raw: line)
}
guard let id = payload["id"] as? String, let method = payload["method"] as? String else {
- return self.malformedRequestResponse(id: payload["id"] as? String, raw: line)
+ return Self.malformedRequestResponse(id: payload["id"] as? String, raw: line)
}
- XCTAssertEqual(method, "feed.push")
- return self.v2Response(
+ #expect(method == "feed.push")
+ return Self.v2Response(
id: id,
ok: true,
result: [
@@ -87,7 +124,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
)
}
- let result = runProcess(
+ let result = Self.runProcess(
executablePath: cliPath,
arguments: ["hooks", "feed", "--source", "copilot", "--event", "PreToolUse"],
environment: [
@@ -103,10 +140,10 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"hook_event_name":"PreToolUse","session_id":"copilot-session-123","cwd":"\#(root.path)","tool_name":"Bash","tool_input":{"command":"touch \#(root.appendingPathComponent("README.md").path)"}}"#,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ #expect(server.wait(timeout: 5), "socket server did not observe feed.push")
- let feedEvents = state.commands.compactMap { command -> [String: Any]? in
- guard let payload = self.jsonObject(command),
+ let feedEvents = state.snapshot().compactMap { command -> [String: Any]? in
+ guard let payload = Self.jsonObject(command),
payload["method"] as? String == "feed.push",
let params = payload["params"] as? [String: Any],
let event = params["event"] as? [String: Any] else {
@@ -114,34 +151,246 @@ extension CLINotifyProcessIntegrationRegressionTests {
}
return event
}
- XCTAssertEqual(feedEvents.count, 1, "Expected one Copilot Feed event, saw \(state.commands)")
- return (result, try XCTUnwrap(feedEvents.first))
+ #expect(feedEvents.count == 1, "Expected one Copilot Feed event, saw \(state.snapshot())")
+ return (result, try #require(feedEvents.first))
}
let (allow, allowEvent) = try runCopilotDecision(mode: "once")
- XCTAssertFalse(allow.timedOut, allow.stderr)
- XCTAssertEqual(allow.status, 0, allow.stderr)
- XCTAssertEqual(allowEvent["hook_event_name"] as? String, "PermissionRequest")
- XCTAssertEqual(allowEvent["_source"] as? String, "copilot")
- XCTAssertEqual(allowEvent["_ppid"] as? Int, 525252)
- let allowOutput = try XCTUnwrap(jsonObject(allow.stdout))
- XCTAssertEqual(allowOutput["permissionDecision"] as? String, "allow")
- XCTAssertNil(allowOutput["hookSpecificOutput"])
+ #expect(!allow.timedOut, Comment(rawValue: allow.stderr))
+ #expect(allow.status == 0, Comment(rawValue: allow.stderr))
+ #expect(allowEvent["hook_event_name"] as? String == "PermissionRequest")
+ #expect(allowEvent["_source"] as? String == "copilot")
+ #expect(allowEvent["_ppid"] as? Int == 525252)
+ let allowOutput = try #require(Self.jsonObject(allow.stdout))
+ #expect(allowOutput["permissionDecision"] as? String == "allow")
+ #expect(allowOutput["hookSpecificOutput"] == nil)
let (deny, _) = try runCopilotDecision(mode: "deny")
- XCTAssertFalse(deny.timedOut, deny.stderr)
- XCTAssertEqual(deny.status, 0, deny.stderr)
- let denyOutput = try XCTUnwrap(jsonObject(deny.stdout))
- XCTAssertEqual(denyOutput["permissionDecision"] as? String, "deny")
- XCTAssertEqual(denyOutput["permissionDecisionReason"] as? String, "User denied permission via cmux Feed.")
- XCTAssertNil(denyOutput["hookSpecificOutput"])
+ #expect(!deny.timedOut, Comment(rawValue: deny.stderr))
+ #expect(deny.status == 0, Comment(rawValue: deny.stderr))
+ let denyOutput = try #require(Self.jsonObject(deny.stdout))
+ #expect(denyOutput["permissionDecision"] as? String == "deny")
+ #expect(denyOutput["permissionDecisionReason"] as? String == "User denied permission via cmux Feed.")
+ #expect(denyOutput["hookSpecificOutput"] == nil)
let (unsupported, _) = try runCopilotDecision(mode: "always")
- XCTAssertFalse(unsupported.timedOut, unsupported.stderr)
- XCTAssertEqual(unsupported.status, 0, unsupported.stderr)
- let unsupportedOutput = try XCTUnwrap(jsonObject(unsupported.stdout))
- XCTAssertEqual(unsupportedOutput["permissionDecision"] as? String, "deny")
- XCTAssertEqual(unsupportedOutput["permissionDecisionReason"] as? String, "User denied permission via cmux Feed.")
- XCTAssertNil(unsupportedOutput["hookSpecificOutput"])
+ #expect(!unsupported.timedOut, Comment(rawValue: unsupported.stderr))
+ #expect(unsupported.status == 0, Comment(rawValue: unsupported.stderr))
+ let unsupportedOutput = try #require(Self.jsonObject(unsupported.stdout))
+ #expect(unsupportedOutput["permissionDecision"] as? String == "deny")
+ #expect(unsupportedOutput["permissionDecisionReason"] as? String == "User denied permission via cmux Feed.")
+ #expect(unsupportedOutput["hookSpecificOutput"] == nil)
+ }
+
+ private static func bundledCLIPath() throws -> String {
+ try BundledCLITestSupport.bundledCLIPath(for: BundleProbe.self)
+ }
+
+ private static func makeSocketPath(_ name: String) -> String {
+ let shortID = UUID().uuidString.replacingOccurrences(of: "-", with: "").prefix(8)
+ return URL(fileURLWithPath: NSTemporaryDirectory())
+ .appendingPathComponent("cli-\(name.prefix(6))-\(shortID).sock")
+ .path
+ }
+
+ private static func bindUnixSocket(at path: String) throws -> Int32 {
+ unlink(path)
+ let fd = Darwin.socket(AF_UNIX, SOCK_STREAM, 0)
+ guard fd >= 0 else {
+ throw posixError("failed to create Unix socket")
+ }
+
+ var addr = sockaddr_un()
+ addr.sun_family = sa_family_t(AF_UNIX)
+ let maxPathLength = MemoryLayout.size(ofValue: addr.sun_path)
+ let utf8 = Array(path.utf8)
+ guard utf8.count < maxPathLength else {
+ Darwin.close(fd)
+ throw NSError(domain: "cmux.tests", code: 2, userInfo: [
+ NSLocalizedDescriptionKey: "socket path too long: \(path)",
+ ])
+ }
+ _ = withUnsafeMutablePointer(to: &addr.sun_path) { pointer in
+ pointer.withMemoryRebound(to: CChar.self, capacity: maxPathLength) { buffer in
+ for index in 0...size))
+ }
+ }
+ guard bindResult == 0 else {
+ Darwin.close(fd)
+ throw posixError("failed to bind Unix socket")
+ }
+ guard Darwin.listen(fd, 1) == 0 else {
+ Darwin.close(fd)
+ throw posixError("failed to listen on Unix socket")
+ }
+ return fd
+ }
+
+ private static func startMockServer(
+ listenerFD: Int32,
+ state: MockSocketServerState,
+ handler: @escaping @Sendable (String) -> String
+ ) -> MockSocketServer {
+ let handled = DispatchSemaphore(value: 0)
+ DispatchQueue.global(qos: .userInitiated).async {
+ var clientAddr = sockaddr_un()
+ var clientAddrLen = socklen_t(MemoryLayout.size)
+ let clientFD = withUnsafeMutablePointer(to: &clientAddr) { ptr in
+ ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPtr in
+ Darwin.accept(listenerFD, sockaddrPtr, &clientAddrLen)
+ }
+ }
+ guard clientFD >= 0 else {
+ handled.signal()
+ return
+ }
+ defer { Darwin.close(clientFD) }
+
+ readLines(from: clientFD) { line in
+ state.append(line)
+ writeLine(handler(line), to: clientFD)
+ handled.signal()
+ }
+ }
+ return MockSocketServer(handled: handled)
+ }
+
+ private static func readLines(from fd: Int32, handle: (String) -> Void) {
+ var pending = Data()
+ var buffer = [UInt8](repeating: 0, count: 4096)
+ while true {
+ let count = Darwin.read(fd, &buffer, buffer.count)
+ if count < 0 {
+ if errno == EINTR { continue }
+ return
+ }
+ if count == 0 { return }
+ pending.append(buffer, count: count)
+
+ while let newlineRange = pending.firstRange(of: Data([0x0A])) {
+ let lineData = pending.subdata(in: 0.. String {
+ var payload: [String: Any] = ["id": id, "ok": ok]
+ if let result { payload["result"] = result }
+ if let error { payload["error"] = error }
+ let data = try? JSONSerialization.data(withJSONObject: payload, options: [])
+ return String(data: data ?? Data("{}".utf8), encoding: .utf8) ?? "{}"
+ }
+
+ private static func malformedRequestResponse(id: String? = nil, raw: String) -> String {
+ v2Response(
+ id: id ?? "unknown",
+ ok: false,
+ error: ["code": "malformed_request", "message": "invalid or non-JSON payload", "raw": raw]
+ )
+ }
+
+ private static func jsonObject(_ line: String) -> [String: Any]? {
+ guard let data = line.data(using: .utf8) else { return nil }
+ return try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
+ }
+
+ private static func runProcess(
+ executablePath: String,
+ arguments: [String],
+ environment: [String: String],
+ standardInput: String? = nil,
+ timeout: TimeInterval
+ ) -> ProcessRunResult {
+ let process = Process()
+ process.executableURL = URL(fileURLWithPath: executablePath)
+ process.arguments = arguments
+ process.environment = environment
+
+ let stdout = Pipe()
+ let stderr = Pipe()
+ process.standardOutput = stdout
+ process.standardError = stderr
+
+ let stdinHandle: FileHandle?
+ let stdinURL: URL?
+ if let standardInput {
+ let url = FileManager.default.temporaryDirectory
+ .appendingPathComponent("cmux-test-stdin-\(UUID().uuidString).json")
+ do {
+ try Data(standardInput.utf8).write(to: url)
+ let handle = try FileHandle(forReadingFrom: url)
+ process.standardInput = handle
+ stdinHandle = handle
+ stdinURL = url
+ } catch {
+ try? FileManager.default.removeItem(at: url)
+ return ProcessRunResult(status: -1, stdout: "", stderr: "\(error)", timedOut: false)
+ }
+ } else {
+ stdinHandle = nil
+ stdinURL = nil
+ }
+ defer {
+ try? stdinHandle?.close()
+ if let stdinURL {
+ try? FileManager.default.removeItem(at: stdinURL)
+ }
+ }
+
+ let finished = DispatchSemaphore(value: 0)
+ process.terminationHandler = { _ in finished.signal() }
+
+ do {
+ try process.run()
+ } catch {
+ return ProcessRunResult(status: -1, stdout: "", stderr: "\(error)", timedOut: false)
+ }
+
+ let timedOut = finished.wait(timeout: .now() + timeout) != .success
+ if timedOut {
+ process.terminate()
+ _ = finished.wait(timeout: .now() + 1)
+ }
+
+ let stdoutData = stdout.fileHandleForReading.readDataToEndOfFile()
+ let stderrData = stderr.fileHandleForReading.readDataToEndOfFile()
+ return ProcessRunResult(
+ status: process.terminationStatus,
+ stdout: String(data: stdoutData, encoding: .utf8) ?? "",
+ stderr: String(data: stderrData, encoding: .utf8) ?? "",
+ timedOut: timedOut
+ )
+ }
+
+ private static func posixError(_ message: String) -> NSError {
+ NSError(
+ domain: NSPOSIXErrorDomain,
+ code: Int(errno),
+ userInfo: [NSLocalizedDescriptionKey: "\(message): \(String(cString: strerror(errno)))"]
+ )
}
}
From 95fc809ed9b809d559fcc7664e611ae3ba5e2836 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 02:16:21 -0700
Subject: [PATCH 11/39] test: address Copilot classification review notes
---
CLI/FeedEventClassifier.swift | 3 +++
cmuxTests/FeedEventClassificationTests.swift | 2 ++
2 files changed, 5 insertions(+)
diff --git a/CLI/FeedEventClassifier.swift b/CLI/FeedEventClassifier.swift
index 25bb192cb721..7ef367773ce5 100644
--- a/CLI/FeedEventClassifier.swift
+++ b/CLI/FeedEventClassifier.swift
@@ -109,6 +109,9 @@ struct FeedEventClassifier {
) -> (String, Bool) {
switch semantic {
case .approvalRequest:
+ // Copilot's hook response schema only understands top-level
+ // permissionDecision values, so keep every Copilot tool on the
+ // generic PermissionRequest wire kind.
if source == "copilot" {
return ("PermissionRequest", true)
}
diff --git a/cmuxTests/FeedEventClassificationTests.swift b/cmuxTests/FeedEventClassificationTests.swift
index daeacfdab60f..cc8efe1b363c 100644
--- a/cmuxTests/FeedEventClassificationTests.swift
+++ b/cmuxTests/FeedEventClassificationTests.swift
@@ -102,7 +102,9 @@ struct FeedEventClassificationTests {
#expect(classify("copilot", "PreToolUse", tool: "Read").name == "PermissionRequest")
#expect(classify("copilot", "PreToolUse", tool: "Read").actionable == true)
#expect(classify("copilot", "PreToolUse", tool: "AskUserQuestion").name == "PermissionRequest")
+ #expect(classify("copilot", "PreToolUse", tool: "AskUserQuestion").actionable == true)
#expect(classify("copilot", "PreToolUse", tool: "ExitPlanMode").name == "PermissionRequest")
+ #expect(classify("copilot", "PreToolUse", tool: "ExitPlanMode").actionable == true)
}
/// Even on the maybe-approval (generic pre-tool) path, the two dedicated
From 0fc56e31b2189b3863e2538e77f1312f5cc10250 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 03:08:00 -0700
Subject: [PATCH 12/39] fix: avoid blocking Copilot read-only tools
---
CLI/FeedEventClassifier.swift | 17 ++++++++++++++---
cmuxTests/FeedEventClassificationTests.swift | 13 ++++++++-----
2 files changed, 22 insertions(+), 8 deletions(-)
diff --git a/CLI/FeedEventClassifier.swift b/CLI/FeedEventClassifier.swift
index 7ef367773ce5..52d84d83685b 100644
--- a/CLI/FeedEventClassifier.swift
+++ b/CLI/FeedEventClassifier.swift
@@ -117,6 +117,16 @@ struct FeedEventClassifier {
}
return dedicatedApprovalEvent(for: toolName) ?? ("PermissionRequest", true)
case .toolStartMaybeApproval:
+ if source == "copilot" {
+ // Copilot's PreToolUse fires for every tool. Gate only tools
+ // that need a decision, but keep Copilot on PermissionRequest
+ // so renderAgentDecision emits top-level permissionDecision.
+ if toolName == "ExitPlanMode" || toolName == "AskUserQuestion" ||
+ Self.isSideEffectingTool(toolName, source: source) {
+ return ("PermissionRequest", true)
+ }
+ return ("PreToolUse", false)
+ }
if let dedicated = dedicatedApprovalEvent(for: toolName) {
return dedicated
}
@@ -223,9 +233,10 @@ struct FeedEventClassifier {
"stop": .response,
],
"copilot": [
- // Copilot's PreToolUse hook is the permission gate itself:
- // the hook output directly controls whether the tool runs.
- "PreToolUse": .approvalRequest,
+ // Copilot's PreToolUse hook is the permission gate itself, but it
+ // also fires before read-only tools. Resolve against the tool name
+ // so read-only tools fall through immediately with `{}`.
+ "PreToolUse": .toolStartMaybeApproval,
],
]
diff --git a/cmuxTests/FeedEventClassificationTests.swift b/cmuxTests/FeedEventClassificationTests.swift
index cc8efe1b363c..10dcb445d6f5 100644
--- a/cmuxTests/FeedEventClassificationTests.swift
+++ b/cmuxTests/FeedEventClassificationTests.swift
@@ -93,14 +93,17 @@ struct FeedEventClassificationTests {
#expect(classify("gemini", "PreToolUse", tool: "Read").actionable == false)
}
- /// Copilot's `PreToolUse` hook is the approval gate itself. It must
- /// produce a blocking `PermissionRequest` even for tools that generic
- /// telemetry-only agents would keep non-actionable.
+ /// Copilot's `PreToolUse` hook is the approval gate itself, but it also
+ /// fires before read-only tools. Mutating tools must block with Copilot's
+ /// top-level permission-decision path; read-only tools must fall through.
@Test func copilotPreToolUseIsApprovalRequest() {
#expect(classify("copilot", "PreToolUse", tool: "Bash").name == "PermissionRequest")
#expect(classify("copilot", "PreToolUse", tool: "Bash").actionable == true)
- #expect(classify("copilot", "PreToolUse", tool: "Read").name == "PermissionRequest")
- #expect(classify("copilot", "PreToolUse", tool: "Read").actionable == true)
+ #expect(classify("copilot", "PreToolUse", tool: "Read").name == "PreToolUse")
+ #expect(classify("copilot", "PreToolUse", tool: "Read").actionable == false)
+ #expect(classify("copilot", "PreToolUse", tool: "Grep").actionable == false)
+ #expect(classify("copilot", "PreToolUse", tool: "Glob").actionable == false)
+ #expect(classify("copilot", "PreToolUse", tool: "WebFetch").actionable == false)
#expect(classify("copilot", "PreToolUse", tool: "AskUserQuestion").name == "PermissionRequest")
#expect(classify("copilot", "PreToolUse", tool: "AskUserQuestion").actionable == true)
#expect(classify("copilot", "PreToolUse", tool: "ExitPlanMode").name == "PermissionRequest")
From 9f79ced49d83c1f40c7b113f5526e136afdd6227 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 03:22:40 -0700
Subject: [PATCH 13/39] fix: avoid Copilot notification stop hooks
---
CLI/CMUXCLI+AgentHookDefinitions.swift | 1 -
cmuxTests/CLICopilotHookFeedTests.swift | 2 +-
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/CLI/CMUXCLI+AgentHookDefinitions.swift b/CLI/CMUXCLI+AgentHookDefinitions.swift
index ab29d5ab7f1c..7046d444b7a7 100644
--- a/CLI/CMUXCLI+AgentHookDefinitions.swift
+++ b/CLI/CMUXCLI+AgentHookDefinitions.swift
@@ -317,7 +317,6 @@ extension CMUXCLI {
events: [
.init(agentEvent: "SessionStart", cmuxSubcommand: "session-start"),
.init(agentEvent: "Stop", cmuxSubcommand: "stop"),
- .init(agentEvent: "Notification", cmuxSubcommand: "stop"),
.init(agentEvent: "SessionEnd", cmuxSubcommand: "session-end"),
],
feedHookEvents: ["PreToolUse"]
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index 58f9f38c3d80..5c8148fa08e9 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -74,7 +74,7 @@ struct CLICopilotHookFeedTests {
#expect(hooks["SessionStart"] != nil, "Missing SessionStart hook")
#expect(hooks["Stop"] != nil, "Missing Stop hook")
- #expect(hooks["Notification"] != nil, "Missing Notification hook")
+ #expect(hooks["Notification"] == nil, "Copilot notifications must not be installed as stop hooks")
#expect(hooks["SessionEnd"] != nil, "Missing SessionEnd hook")
let preToolUse = try #require(hooks["PreToolUse"] as? [[String: Any]])
#expect(
From 35db201556d876ec5d57763a6579f5391df583be Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 03:39:12 -0700
Subject: [PATCH 14/39] docs: fix Copilot CLI hooks link
---
docs/notifications.md | 2 +-
web/app/[locale]/docs/notifications/page.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/notifications.md b/docs/notifications.md
index ebd5bedd7638..90c9742b1365 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -117,7 +117,7 @@ See the [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-co
### GitHub Copilot CLI
-Copilot CLI supports [hooks](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks) that run shell commands at key lifecycle events. Add to `~/.copilot/hooks/cmux.json`:
+Copilot CLI supports [hooks](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-hooks) that run shell commands at key lifecycle events. Add to `~/.copilot/hooks/cmux.json`:
```json
{
diff --git a/web/app/[locale]/docs/notifications/page.tsx b/web/app/[locale]/docs/notifications/page.tsx
index e91b09be2f8a..4d5621da89e4 100644
--- a/web/app/[locale]/docs/notifications/page.tsx
+++ b/web/app/[locale]/docs/notifications/page.tsx
@@ -262,7 +262,7 @@ esac`}
{t.rich("copilotCliHooksDesc", {
link: (chunks) => (
- {chunks}
+ {chunks}
),
})}
From 3bb1e4d630bc174334597ec5b67ea17ea908ef14 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 04:28:22 -0700
Subject: [PATCH 15/39] fix: use canonical Copilot hook schema
---
CLI/CMUXCLI+AgentHookDefinitions.swift | 10 ++++---
CLI/FeedEventClassifier.swift | 28 +++++++++++++++-----
CLI/cmux.swift | 21 ++++++++++++---
cmuxTests/CLICopilotHookFeedTests.swift | 23 ++++++++++------
cmuxTests/FeedEventClassificationTests.swift | 24 ++++++++++-------
docs/agent-hooks.md | 2 +-
docs/feed.md | 2 +-
7 files changed, 76 insertions(+), 34 deletions(-)
diff --git a/CLI/CMUXCLI+AgentHookDefinitions.swift b/CLI/CMUXCLI+AgentHookDefinitions.swift
index 7046d444b7a7..2eb2610f6a87 100644
--- a/CLI/CMUXCLI+AgentHookDefinitions.swift
+++ b/CLI/CMUXCLI+AgentHookDefinitions.swift
@@ -315,11 +315,13 @@ extension CMUXCLI {
sessionStoreSuffix: "copilot", disableEnvVar: "CMUX_COPILOT_HOOKS_DISABLED",
hookMarker: "cmux hooks copilot", format: .copilotJSON(timeoutSeconds: 5),
events: [
- .init(agentEvent: "SessionStart", cmuxSubcommand: "session-start"),
- .init(agentEvent: "Stop", cmuxSubcommand: "stop"),
- .init(agentEvent: "SessionEnd", cmuxSubcommand: "session-end"),
+ .init(agentEvent: "sessionStart", cmuxSubcommand: "session-start"),
+ .init(agentEvent: "userPromptSubmitted", cmuxSubcommand: "prompt-submit"),
+ .init(agentEvent: "agentStop", cmuxSubcommand: "stop"),
+ .init(agentEvent: "errorOccurred", cmuxSubcommand: "stop"),
+ .init(agentEvent: "sessionEnd", cmuxSubcommand: "session-end"),
],
- feedHookEvents: ["PreToolUse"]
+ feedHookEvents: ["preToolUse"]
),
AgentHookDef(
name: "codebuddy", displayName: "CodeBuddy", statusKey: "codebuddy",
diff --git a/CLI/FeedEventClassifier.swift b/CLI/FeedEventClassifier.swift
index 52d84d83685b..c1f22d3e50fb 100644
--- a/CLI/FeedEventClassifier.swift
+++ b/CLI/FeedEventClassifier.swift
@@ -118,7 +118,7 @@ struct FeedEventClassifier {
return dedicatedApprovalEvent(for: toolName) ?? ("PermissionRequest", true)
case .toolStartMaybeApproval:
if source == "copilot" {
- // Copilot's PreToolUse fires for every tool. Gate only tools
+ // Copilot's preToolUse fires for every tool. Gate only tools
// that need a decision, but keep Copilot on PermissionRequest
// so renderAgentDecision emits top-level permissionDecision.
if toolName == "ExitPlanMode" || toolName == "AskUserQuestion" ||
@@ -233,9 +233,10 @@ struct FeedEventClassifier {
"stop": .response,
],
"copilot": [
- // Copilot's PreToolUse hook is the permission gate itself, but it
- // also fires before read-only tools. Resolve against the tool name
- // so read-only tools fall through immediately with `{}`.
+ // Copilot's canonical hook config uses camelCase event names. Keep
+ // the PascalCase spelling for already-installed VS Code compatible
+ // hook files.
+ "preToolUse": .toolStartMaybeApproval,
"PreToolUse": .toolStartMaybeApproval,
],
]
@@ -313,9 +314,21 @@ struct FeedEventClassifier {
"generate_image",
]
+ /// Copilot's canonical hook payloads use lower-case runtime tool names.
+ /// Keep these source-scoped so another agent's lower-case read telemetry
+ /// is not accidentally broadened into approval traffic.
+ private static let copilotSideEffectingToolAliases: Set = [
+ "bash",
+ "powershell",
+ "create",
+ "edit",
+ "str_replace_editor",
+ "apply_patch",
+ ]
+
/// Whether a tool mutates state and deserves an approval prompt. Exact
- /// match against ``sideEffectingTools`` for every source; the `kiro`
- /// source additionally matches its case-insensitive internal aliases.
+ /// match against ``sideEffectingTools`` for every source; `copilot` and
+ /// `kiro` additionally match their case-insensitive internal aliases.
/// Kept source-scoped so another agent's lowercase tool name is not
/// escalated into an approval.
static func isSideEffectingTool(_ toolName: String, source: String) -> Bool {
@@ -323,6 +336,9 @@ struct FeedEventClassifier {
if sideEffectingTools.contains(toolName) {
return true
}
+ if source == "copilot" {
+ return copilotSideEffectingToolAliases.contains(toolName.lowercased())
+ }
if source == "kiro" {
return kiroSideEffectingToolAliases.contains(toolName.lowercased())
}
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index 56580c32b2ea..396126e7ddce 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -26947,7 +26947,9 @@ struct CMUXCLI {
] as [String: Any])
result[event.agentEvent] = groups
case .copilotJSON(let timeoutSeconds):
- var entries = result[event.agentEvent] as? [[String: Any]] ?? []; entries.append(Self.copilotHookEntry(command: cmd, timeoutSeconds: timeoutSeconds)); result[event.agentEvent] = entries
+ var entries = result[event.agentEvent] as? [[String: Any]] ?? []
+ entries.append(Self.copilotHookEntry(command: cmd, timeoutSeconds: timeoutSeconds))
+ result[event.agentEvent] = entries
case .antigravityJSON(let timeoutSeconds):
var entries = result[event.agentEvent] as? [[String: Any]] ?? []
entries.append(Self.antigravityHookEntry(
@@ -26984,7 +26986,12 @@ struct CMUXCLI {
] as [String: Any])
result[agentEvent] = groups
case .copilotJSON:
- var entries = result[agentEvent] as? [[String: Any]] ?? []; entries.append(Self.copilotHookEntry(command: feedCmd, timeoutSeconds: Self.timeoutSecondsFromMilliseconds(feedTimeoutMs) + 5)); result[agentEvent] = entries
+ var entries = result[agentEvent] as? [[String: Any]] ?? []
+ entries.append(Self.copilotHookEntry(
+ command: feedCmd,
+ timeoutSeconds: Self.timeoutSecondsFromMilliseconds(feedTimeoutMs) + 5
+ ))
+ result[agentEvent] = entries
case .antigravityJSON:
var entries = result[agentEvent] as? [[String: Any]] ?? []
entries.append(Self.antigravityHookEntry(
@@ -27022,7 +27029,13 @@ struct CMUXCLI {
return ((positiveTimeoutMs - 1) / 1000) + 1
}
- private static func copilotHookEntry(command: String, timeoutSeconds: Int) -> [String: Any] { ["type": "command", "command": command, "timeoutSec": max(timeoutSeconds, 1)] }
+ private static func copilotHookEntry(command: String, timeoutSeconds: Int) -> [String: Any] {
+ [
+ "type": "command",
+ "bash": command,
+ "timeoutSec": max(timeoutSeconds, 1),
+ ]
+ }
private static func antigravityHookEntry(
command: String,
@@ -32895,7 +32908,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
if let workspaceId = feedWorkspaceId(rawObject: stdinObj, fallback: env["CMUX_WORKSPACE_ID"]) {
eventDict["workspace_id"] = workspaceId
}
- let toolInput = stdinObj["tool_input"] ?? stdinObj["toolInput"] ?? toolCall?["args"]
+ let toolInput = stdinObj["tool_input"] ?? stdinObj["toolInput"] ?? stdinObj["toolArgs"] ?? toolCall?["args"]
if let cwd = firstString(in: stdinObj, keys: ["cwd", "working_directory", "workingDirectory"])
?? firstWorkspacePath(in: stdinObj)
?? (toolInput as? [String: Any]).flatMap({ firstString(in: $0, keys: ["Cwd", "cwd"]) }) {
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index 5c8148fa08e9..bec9697b683d 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -72,19 +72,26 @@ struct CLICopilotHookFeedTests {
#expect(json["version"] as? Int == 1)
let hooks = try #require(json["hooks"] as? [String: Any])
- #expect(hooks["SessionStart"] != nil, "Missing SessionStart hook")
- #expect(hooks["Stop"] != nil, "Missing Stop hook")
+ #expect(hooks["sessionStart"] != nil, "Missing sessionStart hook")
+ #expect(hooks["userPromptSubmitted"] != nil, "Missing userPromptSubmitted hook")
+ #expect(hooks["agentStop"] != nil, "Missing agentStop hook")
+ #expect(hooks["errorOccurred"] != nil, "Missing errorOccurred hook")
#expect(hooks["Notification"] == nil, "Copilot notifications must not be installed as stop hooks")
- #expect(hooks["SessionEnd"] != nil, "Missing SessionEnd hook")
- let preToolUse = try #require(hooks["PreToolUse"] as? [[String: Any]])
+ #expect(hooks["sessionEnd"] != nil, "Missing sessionEnd hook")
+ #expect(hooks["SessionStart"] == nil, "Copilot must use canonical camelCase hook names")
+ #expect(hooks["Stop"] == nil, "Copilot must use canonical agentStop/errorOccurred hook names")
+ #expect(hooks["SessionEnd"] == nil, "Copilot must use canonical camelCase hook names")
+ #expect(hooks["PreToolUse"] == nil, "Copilot must install canonical preToolUse hooks")
+ let preToolUse = try #require(hooks["preToolUse"] as? [[String: Any]])
#expect(
preToolUse.contains {
- ($0["command"] as? String)?.contains("hooks feed --source copilot --event PreToolUse") == true
+ ($0["bash"] as? String)?.contains("hooks feed --source copilot --event preToolUse") == true
&& ($0["type"] as? String) == "command"
&& ($0["timeoutSec"] as? Int) == 125
+ && $0["command"] == nil
&& $0["hooks"] == nil
},
- "Expected direct PreToolUse command hook with timeout slack, saw \(preToolUse)"
+ "Expected direct preToolUse bash hook with timeout slack, saw \(preToolUse)"
)
}
@@ -126,7 +133,7 @@ struct CLICopilotHookFeedTests {
let result = Self.runProcess(
executablePath: cliPath,
- arguments: ["hooks", "feed", "--source", "copilot", "--event", "PreToolUse"],
+ arguments: ["hooks", "feed", "--source", "copilot", "--event", "preToolUse"],
environment: [
"HOME": root.path,
"PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
@@ -137,7 +144,7 @@ struct CLICopilotHookFeedTests {
"CMUX_COPILOT_PID": "525252",
"CMUX_CLI_SENTRY_DISABLED": "1",
],
- standardInput: #"{"hook_event_name":"PreToolUse","session_id":"copilot-session-123","cwd":"\#(root.path)","tool_name":"Bash","tool_input":{"command":"touch \#(root.appendingPathComponent("README.md").path)"}}"#,
+ standardInput: #"{"sessionId":"copilot-session-123","cwd":"\#(root.path)","toolName":"bash","toolArgs":{"command":"touch \#(root.appendingPathComponent("README.md").path)"}}"#,
timeout: 5
)
#expect(server.wait(timeout: 5), "socket server did not observe feed.push")
diff --git a/cmuxTests/FeedEventClassificationTests.swift b/cmuxTests/FeedEventClassificationTests.swift
index 10dcb445d6f5..a4ff79711559 100644
--- a/cmuxTests/FeedEventClassificationTests.swift
+++ b/cmuxTests/FeedEventClassificationTests.swift
@@ -93,21 +93,25 @@ struct FeedEventClassificationTests {
#expect(classify("gemini", "PreToolUse", tool: "Read").actionable == false)
}
- /// Copilot's `PreToolUse` hook is the approval gate itself, but it also
+ /// Copilot's `preToolUse` hook is the approval gate itself, but it also
/// fires before read-only tools. Mutating tools must block with Copilot's
/// top-level permission-decision path; read-only tools must fall through.
@Test func copilotPreToolUseIsApprovalRequest() {
+ #expect(classify("copilot", "preToolUse", tool: "bash").name == "PermissionRequest")
+ #expect(classify("copilot", "preToolUse", tool: "bash").actionable == true)
+ #expect(classify("copilot", "preToolUse", tool: "create").actionable == true)
+ #expect(classify("copilot", "preToolUse", tool: "edit").actionable == true)
+ #expect(classify("copilot", "preToolUse", tool: "view").name == "PreToolUse")
+ #expect(classify("copilot", "preToolUse", tool: "view").actionable == false)
+ #expect(classify("copilot", "preToolUse", tool: "grep").actionable == false)
+ #expect(classify("copilot", "preToolUse", tool: "glob").actionable == false)
+ #expect(classify("copilot", "preToolUse", tool: "web_fetch").actionable == false)
+ #expect(classify("copilot", "preToolUse", tool: "AskUserQuestion").name == "PermissionRequest")
+ #expect(classify("copilot", "preToolUse", tool: "AskUserQuestion").actionable == true)
+ #expect(classify("copilot", "preToolUse", tool: "ExitPlanMode").name == "PermissionRequest")
+ #expect(classify("copilot", "preToolUse", tool: "ExitPlanMode").actionable == true)
#expect(classify("copilot", "PreToolUse", tool: "Bash").name == "PermissionRequest")
#expect(classify("copilot", "PreToolUse", tool: "Bash").actionable == true)
- #expect(classify("copilot", "PreToolUse", tool: "Read").name == "PreToolUse")
- #expect(classify("copilot", "PreToolUse", tool: "Read").actionable == false)
- #expect(classify("copilot", "PreToolUse", tool: "Grep").actionable == false)
- #expect(classify("copilot", "PreToolUse", tool: "Glob").actionable == false)
- #expect(classify("copilot", "PreToolUse", tool: "WebFetch").actionable == false)
- #expect(classify("copilot", "PreToolUse", tool: "AskUserQuestion").name == "PermissionRequest")
- #expect(classify("copilot", "PreToolUse", tool: "AskUserQuestion").actionable == true)
- #expect(classify("copilot", "PreToolUse", tool: "ExitPlanMode").name == "PermissionRequest")
- #expect(classify("copilot", "PreToolUse", tool: "ExitPlanMode").actionable == true)
}
/// Even on the maybe-approval (generic pre-tool) path, the two dedicated
diff --git a/docs/agent-hooks.md b/docs/agent-hooks.md
index 2b4b530f2682..b644092e6fff 100644
--- a/docs/agent-hooks.md
+++ b/docs/agent-hooks.md
@@ -28,7 +28,7 @@ Supported agent names are `codex`, `grok`, `opencode`, `pi`, `omp`, `amp`, `curs
| Gemini | `gemini` | `~/.gemini/settings.json` | `gemini --resume ` | PreToolUse |
| Kiro CLI | `kiro-cli` | `~/.kiro/agents/cmux.json` or `$KIRO_HOME/agents/cmux.json` | `kiro-cli chat --resume-id ` | preToolUse, postToolUse |
| Rovo Dev | `acli` | `~/.rovodev/config.yml` | `acli rovodev run --restore ` | none |
-| Copilot | `copilot` | `~/.copilot/hooks/cmux.json` | `copilot --resume ` | PreToolUse |
+| Copilot | `copilot` | `~/.copilot/hooks/cmux.json` | `copilot --resume ` | preToolUse |
| CodeBuddy | `codebuddy` | `~/.codebuddy/settings.json` | `codebuddy --resume ` | PreToolUse |
| Factory | `droid` | `~/.factory/settings.json` | `droid --resume ` | PreToolUse |
| Qoder | `qodercli` | `~/.qoder/settings.json` | `qodercli --resume ` | PreToolUse |
diff --git a/docs/feed.md b/docs/feed.md
index 9ea233b7098f..f3e2a9f602b7 100644
--- a/docs/feed.md
+++ b/docs/feed.md
@@ -80,7 +80,7 @@ Installs supported agent hooks whose binaries are on `PATH`. See [Agent hook int
| OpenCode | `~/.config/opencode/plugins/cmux-feed.js` | plugin event bus |
| Cursor CLI | `~/.cursor/hooks.json` | beforeShellExecution |
| Gemini | `~/.gemini/settings.json` | PreToolUse |
-| Copilot | `~/.copilot/hooks/cmux.json` | PreToolUse |
+| Copilot | `~/.copilot/hooks/cmux.json` | preToolUse |
| CodeBuddy | `~/.codebuddy/settings.json` | PreToolUse |
| Factory | `~/.factory/settings.json` | PreToolUse |
| Qoder | `~/.qoder/settings.json` | PreToolUse |
From d50ee57e1a0f1a775297d91332877d0d5abf95f8 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 04:32:53 -0700
Subject: [PATCH 16/39] chore: refresh Swift file length budget
---
.github/swift-file-length-budget.tsv | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index 4bd3eb1331bb..70752f918165 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -1,7 +1,7 @@
# cmux-owned Swift file length budget.
# Format: max_linesrelative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
-34289 CLI/cmux.swift
+34302 CLI/cmux.swift
17719 Sources/AppDelegate.swift
16112 Sources/ContentView.swift
13952 Sources/TerminalController.swift
@@ -129,7 +129,7 @@
716 Sources/TaskManagerSnapshot.swift
715 Sources/AppleScriptSupport.swift
710 Sources/TerminalSSHSessionDetector.swift
-707 CLI/CMUXCLI+AgentHookDefinitions.swift
+708 CLI/CMUXCLI+AgentHookDefinitions.swift
706 CLI/CMUXCLI+Config.swift
705 Sources/Panels/BrowserPopupWindowController.swift
699 cmuxTests/TerminalNotificationClearAllTests.swift
@@ -143,8 +143,8 @@
681 Sources/Panels/AgentSessionProcessStore.swift
680 Sources/FileExplorerSearchController.swift
677 Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+Bootstrap.swift
-668 cmuxTests/FeedCoordinatorTests.swift
668 cmuxTests/SettingsWindowPresenterTests.swift
+666 cmuxTests/FeedCoordinatorTests.swift
664 Sources/CmuxTopSnapshot.swift
663 Sources/PortScanner.swift
663 cmuxTests/SessionIndexViewTests.swift
From e302406e787970b608518963543df5434092c4f8 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 04:40:26 -0700
Subject: [PATCH 17/39] fix: gate Copilot ask_user hooks
---
CLI/FeedEventClassifier.swift | 2 +-
cmuxTests/FeedEventClassificationTests.swift | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/CLI/FeedEventClassifier.swift b/CLI/FeedEventClassifier.swift
index c1f22d3e50fb..6eaf2968f07b 100644
--- a/CLI/FeedEventClassifier.swift
+++ b/CLI/FeedEventClassifier.swift
@@ -121,7 +121,7 @@ struct FeedEventClassifier {
// Copilot's preToolUse fires for every tool. Gate only tools
// that need a decision, but keep Copilot on PermissionRequest
// so renderAgentDecision emits top-level permissionDecision.
- if toolName == "ExitPlanMode" || toolName == "AskUserQuestion" ||
+ if toolName == "ExitPlanMode" || toolName == "AskUserQuestion" || toolName == "ask_user" ||
Self.isSideEffectingTool(toolName, source: source) {
return ("PermissionRequest", true)
}
diff --git a/cmuxTests/FeedEventClassificationTests.swift b/cmuxTests/FeedEventClassificationTests.swift
index a4ff79711559..ca0c72307af9 100644
--- a/cmuxTests/FeedEventClassificationTests.swift
+++ b/cmuxTests/FeedEventClassificationTests.swift
@@ -106,6 +106,8 @@ struct FeedEventClassificationTests {
#expect(classify("copilot", "preToolUse", tool: "grep").actionable == false)
#expect(classify("copilot", "preToolUse", tool: "glob").actionable == false)
#expect(classify("copilot", "preToolUse", tool: "web_fetch").actionable == false)
+ #expect(classify("copilot", "preToolUse", tool: "ask_user").name == "PermissionRequest")
+ #expect(classify("copilot", "preToolUse", tool: "ask_user").actionable == true)
#expect(classify("copilot", "preToolUse", tool: "AskUserQuestion").name == "PermissionRequest")
#expect(classify("copilot", "preToolUse", tool: "AskUserQuestion").actionable == true)
#expect(classify("copilot", "preToolUse", tool: "ExitPlanMode").name == "PermissionRequest")
From 87130266cbc45cc77fd35ab561205ead008bbee1 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 04:49:36 -0700
Subject: [PATCH 18/39] fix: route Copilot errors as notifications
---
CLI/CMUXCLI+AgentHookDefinitions.swift | 2 +-
cmuxTests/CLICopilotHookFeedTests.swift | 9 +++++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/CLI/CMUXCLI+AgentHookDefinitions.swift b/CLI/CMUXCLI+AgentHookDefinitions.swift
index 2eb2610f6a87..00fdeb814623 100644
--- a/CLI/CMUXCLI+AgentHookDefinitions.swift
+++ b/CLI/CMUXCLI+AgentHookDefinitions.swift
@@ -318,7 +318,7 @@ extension CMUXCLI {
.init(agentEvent: "sessionStart", cmuxSubcommand: "session-start"),
.init(agentEvent: "userPromptSubmitted", cmuxSubcommand: "prompt-submit"),
.init(agentEvent: "agentStop", cmuxSubcommand: "stop"),
- .init(agentEvent: "errorOccurred", cmuxSubcommand: "stop"),
+ .init(agentEvent: "errorOccurred", cmuxSubcommand: "notification"),
.init(agentEvent: "sessionEnd", cmuxSubcommand: "session-end"),
],
feedHookEvents: ["preToolUse"]
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index bec9697b683d..eb73bf4947f1 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -82,6 +82,15 @@ struct CLICopilotHookFeedTests {
#expect(hooks["Stop"] == nil, "Copilot must use canonical agentStop/errorOccurred hook names")
#expect(hooks["SessionEnd"] == nil, "Copilot must use canonical camelCase hook names")
#expect(hooks["PreToolUse"] == nil, "Copilot must install canonical preToolUse hooks")
+ let errorOccurred = try #require(hooks["errorOccurred"] as? [[String: Any]])
+ #expect(
+ errorOccurred.contains {
+ ($0["bash"] as? String)?.contains("hooks copilot notification") == true
+ && ($0["type"] as? String) == "command"
+ && $0["command"] == nil
+ },
+ "Expected errorOccurred to route through notification handling, saw \(errorOccurred)"
+ )
let preToolUse = try #require(hooks["preToolUse"] as? [[String: Any]])
#expect(
preToolUse.contains {
From 3fdc675cf4c563192b24cee51afbe4e8aab5222c Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 05:01:13 -0700
Subject: [PATCH 19/39] docs: update Copilot error hook payload sample
---
docs/notifications.md | 2 +-
web/app/[locale]/docs/notifications/page.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/notifications.md b/docs/notifications.md
index 90c9742b1365..95f482a6c692 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -140,7 +140,7 @@ Copilot CLI supports [hooks](https://docs.github.com/en/copilot/how-tos/copilot-
"errorOccurred": [
{
"type": "command",
- "bash": "if command -v cmux &>/dev/null; then cmux notify --title 'Copilot CLI' --subtitle 'Error' --body \"$(cat | jq -r '.errorMessage // \"An error occurred\"' 2>/dev/null | head -c 100)\"; cmux set-status copilot_cli Error; else osascript -e 'display notification \"An error occurred\" with title \"Copilot CLI\"'; fi",
+ "bash": "if command -v cmux &>/dev/null; then cmux notify --title 'Copilot CLI' --subtitle 'Error' --body \"$(cat | jq -r '.error.message // .error.name // \"An error occurred\"' 2>/dev/null | head -c 100)\"; cmux set-status copilot_cli Error; else osascript -e 'display notification \"An error occurred\" with title \"Copilot CLI\"'; fi",
"timeoutSec": 5
}
],
diff --git a/web/app/[locale]/docs/notifications/page.tsx b/web/app/[locale]/docs/notifications/page.tsx
index 4d5621da89e4..090aa9c5df36 100644
--- a/web/app/[locale]/docs/notifications/page.tsx
+++ b/web/app/[locale]/docs/notifications/page.tsx
@@ -286,7 +286,7 @@ esac`}
"errorOccurred": [
{
"type": "command",
- "bash": "if command -v cmux &>/dev/null; then cmux notify --title 'Copilot CLI' --subtitle 'Error' --body 'An error occurred'; cmux set-status copilot_cli Error; fi",
+ "bash": "if command -v cmux &>/dev/null; then cmux notify --title 'Copilot CLI' --subtitle 'Error' --body \\\"$(cat | jq -r '.error.message // .error.name // \\\"An error occurred\\\"' 2>/dev/null | head -c 100)\\\"; cmux set-status copilot_cli Error; fi",
"timeoutSec": 5
}
],
From 5afbfeed14f13cd9c33d66889bdab5a432caabf1 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 05:38:44 -0700
Subject: [PATCH 20/39] fix: add Copilot permissionRequest feed hook
---
.github/swift-file-length-budget.tsv | 6 ++--
CLI/CMUXCLI+AgentHookDefinitions.swift | 2 +-
CLI/FeedEventClassifier.swift | 2 ++
CLI/cmux.swift | 13 +++++++-
cmuxTests/CLICopilotHookFeedTests.swift | 34 ++++++++++++++++++--
cmuxTests/FeedEventClassificationTests.swift | 8 +++--
docs/agent-hooks.md | 2 +-
docs/feed.md | 2 +-
8 files changed, 57 insertions(+), 12 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index 70752f918165..f1ebc56eaac3 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -1,7 +1,7 @@
# cmux-owned Swift file length budget.
# Format: max_linesrelative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
-34302 CLI/cmux.swift
+34313 CLI/cmux.swift
17719 Sources/AppDelegate.swift
16112 Sources/ContentView.swift
13952 Sources/TerminalController.swift
@@ -24,7 +24,7 @@
4487 Sources/Panels/FilePreviewPanel.swift
4478 Sources/cmuxApp.swift
4401 cmuxTests/BrowserPanelTests.swift
-4225 Sources/BrowserWindowPortal.swift
+4187 Sources/BrowserWindowPortal.swift
3934 Sources/Feed/FeedPanelView.swift
3926 cmuxTests/TabManagerUnitTests.swift
3896 cmuxTests/WindowAndDragTests.swift
@@ -42,7 +42,7 @@
2395 Sources/Mobile/MobileHostService.swift
2328 cmuxTests/CJKIMEInputTests.swift
2257 Sources/TerminalNotificationStore.swift
-2257 Sources/TerminalWindowPortal.swift
+2233 Sources/TerminalWindowPortal.swift
2126 cmuxTests/CmuxConfigTests.swift
2103 Sources/SessionPersistence.swift
2091 cmuxTests/ShortcutAndCommandPaletteTests.swift
diff --git a/CLI/CMUXCLI+AgentHookDefinitions.swift b/CLI/CMUXCLI+AgentHookDefinitions.swift
index 00fdeb814623..5f1510e91064 100644
--- a/CLI/CMUXCLI+AgentHookDefinitions.swift
+++ b/CLI/CMUXCLI+AgentHookDefinitions.swift
@@ -321,7 +321,7 @@ extension CMUXCLI {
.init(agentEvent: "errorOccurred", cmuxSubcommand: "notification"),
.init(agentEvent: "sessionEnd", cmuxSubcommand: "session-end"),
],
- feedHookEvents: ["preToolUse"]
+ feedHookEvents: ["permissionRequest", "preToolUse"]
),
AgentHookDef(
name: "codebuddy", displayName: "CodeBuddy", statusKey: "codebuddy",
diff --git a/CLI/FeedEventClassifier.swift b/CLI/FeedEventClassifier.swift
index 6eaf2968f07b..888947d8226b 100644
--- a/CLI/FeedEventClassifier.swift
+++ b/CLI/FeedEventClassifier.swift
@@ -236,6 +236,8 @@ struct FeedEventClassifier {
// Copilot's canonical hook config uses camelCase event names. Keep
// the PascalCase spelling for already-installed VS Code compatible
// hook files.
+ "permissionRequest": .approvalRequest,
+ "PermissionRequest": .approvalRequest,
"preToolUse": .toolStartMaybeApproval,
"PreToolUse": .toolStartMaybeApproval,
],
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index 396126e7ddce..c6d334d8f0d9 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -33032,6 +33032,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
}
let out = Self.renderAgentDecision(
source: source,
+ rawEventName: rawEvent,
hookEventName: hookEventName,
toolName: toolName,
toolInput: eventDict["tool_input"],
@@ -33093,6 +33094,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
/// Encodes the user's decision in the agent's expected hook stdout shape.
private static func renderAgentDecision(
source: String,
+ rawEventName: String,
hookEventName: String,
toolName: String,
toolInput: Any?,
@@ -33203,10 +33205,19 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
return encode(permissionRequestHookDecision(behavior: "allow"))
}
if source == "copilot" {
+ let reason = String(
+ localized: "cli.hooks.feed.permissionDeniedReason",
+ defaultValue: "User denied permission via cmux Feed."
+ )
+ if rawEventName == "permissionRequest" || rawEventName == "PermissionRequest" {
+ if mode == "once" {
+ return encode(["behavior": "allow"])
+ }
+ return encode(["behavior": "deny", "message": reason])
+ }
if mode == "once" {
return encode(["permissionDecision": "allow"])
}
- let reason = String(localized: "cli.hooks.feed.permissionDeniedReason", defaultValue: "User denied permission via cmux Feed.")
return encode(["permissionDecision": "deny", "permissionDecisionReason": reason])
}
if source == "hermes-agent" {
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index eb73bf4947f1..2826ea6d2409 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -82,6 +82,7 @@ struct CLICopilotHookFeedTests {
#expect(hooks["Stop"] == nil, "Copilot must use canonical agentStop/errorOccurred hook names")
#expect(hooks["SessionEnd"] == nil, "Copilot must use canonical camelCase hook names")
#expect(hooks["PreToolUse"] == nil, "Copilot must install canonical preToolUse hooks")
+ #expect(hooks["PermissionRequest"] == nil, "Copilot must install canonical permissionRequest hooks")
let errorOccurred = try #require(hooks["errorOccurred"] as? [[String: Any]])
#expect(
errorOccurred.contains {
@@ -102,10 +103,21 @@ struct CLICopilotHookFeedTests {
},
"Expected direct preToolUse bash hook with timeout slack, saw \(preToolUse)"
)
+ let permissionRequest = try #require(hooks["permissionRequest"] as? [[String: Any]])
+ #expect(
+ permissionRequest.contains {
+ ($0["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest") == true
+ && ($0["type"] as? String) == "command"
+ && ($0["timeoutSec"] as? Int) == 125
+ && $0["command"] == nil
+ && $0["hooks"] == nil
+ },
+ "Expected direct permissionRequest bash hook with timeout slack, saw \(permissionRequest)"
+ )
}
@Test func copilotFeedDecisionEmitsPreToolUsePermissionDecision() throws {
- func runCopilotDecision(mode: String) throws -> (ProcessRunResult, [String: Any]) {
+ func runCopilotDecision(mode: String, event: String = "preToolUse") throws -> (ProcessRunResult, [String: Any]) {
let cliPath = try Self.bundledCLIPath()
let socketPath = Self.makeSocketPath("copilot-feed-decision")
let listenerFD = try Self.bindUnixSocket(at: socketPath)
@@ -142,7 +154,7 @@ struct CLICopilotHookFeedTests {
let result = Self.runProcess(
executablePath: cliPath,
- arguments: ["hooks", "feed", "--source", "copilot", "--event", "preToolUse"],
+ arguments: ["hooks", "feed", "--source", "copilot", "--event", event],
environment: [
"HOME": root.path,
"PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
@@ -196,6 +208,24 @@ struct CLICopilotHookFeedTests {
#expect(unsupportedOutput["permissionDecision"] as? String == "deny")
#expect(unsupportedOutput["permissionDecisionReason"] as? String == "User denied permission via cmux Feed.")
#expect(unsupportedOutput["hookSpecificOutput"] == nil)
+
+ let (permissionAllow, permissionAllowEvent) = try runCopilotDecision(mode: "once", event: "permissionRequest")
+ #expect(!permissionAllow.timedOut, Comment(rawValue: permissionAllow.stderr))
+ #expect(permissionAllow.status == 0, Comment(rawValue: permissionAllow.stderr))
+ #expect(permissionAllowEvent["hook_event_name"] as? String == "PermissionRequest")
+ let permissionAllowOutput = try #require(Self.jsonObject(permissionAllow.stdout))
+ #expect(permissionAllowOutput["behavior"] as? String == "allow")
+ #expect(permissionAllowOutput["permissionDecision"] == nil)
+ #expect(permissionAllowOutput["hookSpecificOutput"] == nil)
+
+ let (permissionDeny, _) = try runCopilotDecision(mode: "deny", event: "permissionRequest")
+ #expect(!permissionDeny.timedOut, Comment(rawValue: permissionDeny.stderr))
+ #expect(permissionDeny.status == 0, Comment(rawValue: permissionDeny.stderr))
+ let permissionDenyOutput = try #require(Self.jsonObject(permissionDeny.stdout))
+ #expect(permissionDenyOutput["behavior"] as? String == "deny")
+ #expect(permissionDenyOutput["message"] as? String == "User denied permission via cmux Feed.")
+ #expect(permissionDenyOutput["permissionDecision"] == nil)
+ #expect(permissionDenyOutput["hookSpecificOutput"] == nil)
}
private static func bundledCLIPath() throws -> String {
diff --git a/cmuxTests/FeedEventClassificationTests.swift b/cmuxTests/FeedEventClassificationTests.swift
index ca0c72307af9..98360204af0a 100644
--- a/cmuxTests/FeedEventClassificationTests.swift
+++ b/cmuxTests/FeedEventClassificationTests.swift
@@ -93,10 +93,12 @@ struct FeedEventClassificationTests {
#expect(classify("gemini", "PreToolUse", tool: "Read").actionable == false)
}
- /// Copilot's `preToolUse` hook is the approval gate itself, but it also
- /// fires before read-only tools. Mutating tools must block with Copilot's
- /// top-level permission-decision path; read-only tools must fall through.
+ /// Copilot's `permissionRequest` hook blocks before the native permission
+ /// service, and `preToolUse` still gates side-effecting tools before they
+ /// execute. Read-only pre-tool events must fall through as telemetry.
@Test func copilotPreToolUseIsApprovalRequest() {
+ #expect(classify("copilot", "permissionRequest", tool: "bash").name == "PermissionRequest")
+ #expect(classify("copilot", "permissionRequest", tool: "bash").actionable == true)
#expect(classify("copilot", "preToolUse", tool: "bash").name == "PermissionRequest")
#expect(classify("copilot", "preToolUse", tool: "bash").actionable == true)
#expect(classify("copilot", "preToolUse", tool: "create").actionable == true)
diff --git a/docs/agent-hooks.md b/docs/agent-hooks.md
index b644092e6fff..70395bb5fda6 100644
--- a/docs/agent-hooks.md
+++ b/docs/agent-hooks.md
@@ -28,7 +28,7 @@ Supported agent names are `codex`, `grok`, `opencode`, `pi`, `omp`, `amp`, `curs
| Gemini | `gemini` | `~/.gemini/settings.json` | `gemini --resume ` | PreToolUse |
| Kiro CLI | `kiro-cli` | `~/.kiro/agents/cmux.json` or `$KIRO_HOME/agents/cmux.json` | `kiro-cli chat --resume-id ` | preToolUse, postToolUse |
| Rovo Dev | `acli` | `~/.rovodev/config.yml` | `acli rovodev run --restore ` | none |
-| Copilot | `copilot` | `~/.copilot/hooks/cmux.json` | `copilot --resume ` | preToolUse |
+| Copilot | `copilot` | `~/.copilot/hooks/cmux.json` | `copilot --resume ` | permissionRequest, preToolUse |
| CodeBuddy | `codebuddy` | `~/.codebuddy/settings.json` | `codebuddy --resume ` | PreToolUse |
| Factory | `droid` | `~/.factory/settings.json` | `droid --resume ` | PreToolUse |
| Qoder | `qodercli` | `~/.qoder/settings.json` | `qodercli --resume ` | PreToolUse |
diff --git a/docs/feed.md b/docs/feed.md
index f3e2a9f602b7..0083ff25a367 100644
--- a/docs/feed.md
+++ b/docs/feed.md
@@ -80,7 +80,7 @@ Installs supported agent hooks whose binaries are on `PATH`. See [Agent hook int
| OpenCode | `~/.config/opencode/plugins/cmux-feed.js` | plugin event bus |
| Cursor CLI | `~/.cursor/hooks.json` | beforeShellExecution |
| Gemini | `~/.gemini/settings.json` | PreToolUse |
-| Copilot | `~/.copilot/hooks/cmux.json` | preToolUse |
+| Copilot | `~/.copilot/hooks/cmux.json` | permissionRequest, preToolUse |
| CodeBuddy | `~/.codebuddy/settings.json` | PreToolUse |
| Factory | `~/.factory/settings.json` | PreToolUse |
| Qoder | `~/.qoder/settings.json` | PreToolUse |
From 70ec159572311be28dea18f4442d25922598bc11 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 05:48:16 -0700
Subject: [PATCH 21/39] fix: avoid duplicate Copilot feed approvals
---
.github/swift-file-length-budget.tsv | 4 ++--
CLI/CMUXCLI+AgentHookDefinitions.swift | 8 ++++++--
CLI/cmux.swift | 6 +++++-
cmuxTests/CLICopilotHookFeedTests.swift | 1 +
4 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index f1ebc56eaac3..2b3659043e54 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -1,7 +1,7 @@
# cmux-owned Swift file length budget.
# Format: max_linesrelative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
-34313 CLI/cmux.swift
+34317 CLI/cmux.swift
17719 Sources/AppDelegate.swift
16112 Sources/ContentView.swift
13952 Sources/TerminalController.swift
@@ -128,8 +128,8 @@
718 Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
716 Sources/TaskManagerSnapshot.swift
715 Sources/AppleScriptSupport.swift
+712 CLI/CMUXCLI+AgentHookDefinitions.swift
710 Sources/TerminalSSHSessionDetector.swift
-708 CLI/CMUXCLI+AgentHookDefinitions.swift
706 CLI/CMUXCLI+Config.swift
705 Sources/Panels/BrowserPopupWindowController.swift
699 cmuxTests/TerminalNotificationClearAllTests.swift
diff --git a/CLI/CMUXCLI+AgentHookDefinitions.swift b/CLI/CMUXCLI+AgentHookDefinitions.swift
index 5f1510e91064..2bc3423ab726 100644
--- a/CLI/CMUXCLI+AgentHookDefinitions.swift
+++ b/CLI/CMUXCLI+AgentHookDefinitions.swift
@@ -381,14 +381,18 @@ extension CMUXCLI {
}
static func feedHookCommandString(for def: AgentHookDef, agentEvent: String) -> String {
+ let telemetryOnlyFlag = def.name == "copilot" && agentEvent == "preToolUse"
+ ? " --telemetry-only"
+ : ""
+ let command = "cmux hooks feed --source \(def.name) --event \(agentEvent)\(telemetryOnlyFlag)"
switch def.format {
case .kiroAgentJSON:
return exitTwoPropagatingAgentHookShellCommand(
- "cmux hooks feed --source \(def.name) --event \(agentEvent)",
+ command,
for: def
)
default:
- return agentHookShellCommand("cmux hooks feed --source \(def.name) --event \(agentEvent)", for: def)
+ return agentHookShellCommand(command, for: def)
}
}
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index c6d334d8f0d9..a2be5387aa71 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -32871,11 +32871,15 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
// Decide whether this event is Feed-actionable. Non-actionable
// events are forwarded as telemetry (non-blocking) and exit `{}`
// so the agent proceeds without a decision.
- let (hookEventName, isActionable) = FeedEventClassifier.classify(
+ var (hookEventName, isActionable) = FeedEventClassifier.classify(
source: source,
event: rawEvent,
toolName: toolName
)
+ if commandArgs.contains("--telemetry-only") {
+ hookEventName = rawEvent.isEmpty ? hookEventName : rawEvent
+ isActionable = false
+ }
let env = ProcessInfo.processInfo.environment
if Self.shouldSuppressKiroFeedEvent(
source: source,
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index 2826ea6d2409..0ae68026a0e1 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -96,6 +96,7 @@ struct CLICopilotHookFeedTests {
#expect(
preToolUse.contains {
($0["bash"] as? String)?.contains("hooks feed --source copilot --event preToolUse") == true
+ && ($0["bash"] as? String)?.contains("--telemetry-only") == true
&& ($0["type"] as? String) == "command"
&& ($0["timeoutSec"] as? Int) == 125
&& $0["command"] == nil
From 5c9950f3a3e892b2af2bb69c9517a35f3b3eb816 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 06:15:42 -0700
Subject: [PATCH 22/39] fix: preserve Copilot preToolUse wire name
---
.github/swift-file-length-budget.tsv | 2 +-
CLI/cmux.swift | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index 2b3659043e54..5c611e05e6f9 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -1,7 +1,7 @@
# cmux-owned Swift file length budget.
# Format: max_linesrelative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
-34317 CLI/cmux.swift
+34319 CLI/cmux.swift
17719 Sources/AppDelegate.swift
16112 Sources/ContentView.swift
13952 Sources/TerminalController.swift
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index a2be5387aa71..6017de2eecc1 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -32877,7 +32877,9 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
toolName: toolName
)
if commandArgs.contains("--telemetry-only") {
- hookEventName = rawEvent.isEmpty ? hookEventName : rawEvent
+ if rawEvent == "preToolUse" || rawEvent == "PreToolUse" {
+ hookEventName = "PreToolUse"
+ }
isActionable = false
}
let env = ProcessInfo.processInfo.environment
From a27730cf232737df966d19dd36d4f7dc0539ac8c Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 06:26:15 -0700
Subject: [PATCH 23/39] fix: make Copilot preToolUse gate actionable
---
CLI/CMUXCLI+AgentHookDefinitions.swift | 7 ++-----
cmuxTests/CLICopilotHookFeedTests.swift | 14 ++------------
docs/agent-hooks.md | 2 +-
docs/feed.md | 2 +-
4 files changed, 6 insertions(+), 19 deletions(-)
diff --git a/CLI/CMUXCLI+AgentHookDefinitions.swift b/CLI/CMUXCLI+AgentHookDefinitions.swift
index 2bc3423ab726..06bcd716c192 100644
--- a/CLI/CMUXCLI+AgentHookDefinitions.swift
+++ b/CLI/CMUXCLI+AgentHookDefinitions.swift
@@ -321,7 +321,7 @@ extension CMUXCLI {
.init(agentEvent: "errorOccurred", cmuxSubcommand: "notification"),
.init(agentEvent: "sessionEnd", cmuxSubcommand: "session-end"),
],
- feedHookEvents: ["permissionRequest", "preToolUse"]
+ feedHookEvents: ["preToolUse"]
),
AgentHookDef(
name: "codebuddy", displayName: "CodeBuddy", statusKey: "codebuddy",
@@ -381,10 +381,7 @@ extension CMUXCLI {
}
static func feedHookCommandString(for def: AgentHookDef, agentEvent: String) -> String {
- let telemetryOnlyFlag = def.name == "copilot" && agentEvent == "preToolUse"
- ? " --telemetry-only"
- : ""
- let command = "cmux hooks feed --source \(def.name) --event \(agentEvent)\(telemetryOnlyFlag)"
+ let command = "cmux hooks feed --source \(def.name) --event \(agentEvent)"
switch def.format {
case .kiroAgentJSON:
return exitTwoPropagatingAgentHookShellCommand(
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index 0ae68026a0e1..fd4f89878e48 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -83,6 +83,7 @@ struct CLICopilotHookFeedTests {
#expect(hooks["SessionEnd"] == nil, "Copilot must use canonical camelCase hook names")
#expect(hooks["PreToolUse"] == nil, "Copilot must install canonical preToolUse hooks")
#expect(hooks["PermissionRequest"] == nil, "Copilot must install canonical permissionRequest hooks")
+ #expect(hooks["permissionRequest"] == nil, "Copilot uses preToolUse for the installed approval gate")
let errorOccurred = try #require(hooks["errorOccurred"] as? [[String: Any]])
#expect(
errorOccurred.contains {
@@ -96,7 +97,7 @@ struct CLICopilotHookFeedTests {
#expect(
preToolUse.contains {
($0["bash"] as? String)?.contains("hooks feed --source copilot --event preToolUse") == true
- && ($0["bash"] as? String)?.contains("--telemetry-only") == true
+ && ($0["bash"] as? String)?.contains("--telemetry-only") == false
&& ($0["type"] as? String) == "command"
&& ($0["timeoutSec"] as? Int) == 125
&& $0["command"] == nil
@@ -104,17 +105,6 @@ struct CLICopilotHookFeedTests {
},
"Expected direct preToolUse bash hook with timeout slack, saw \(preToolUse)"
)
- let permissionRequest = try #require(hooks["permissionRequest"] as? [[String: Any]])
- #expect(
- permissionRequest.contains {
- ($0["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest") == true
- && ($0["type"] as? String) == "command"
- && ($0["timeoutSec"] as? Int) == 125
- && $0["command"] == nil
- && $0["hooks"] == nil
- },
- "Expected direct permissionRequest bash hook with timeout slack, saw \(permissionRequest)"
- )
}
@Test func copilotFeedDecisionEmitsPreToolUsePermissionDecision() throws {
diff --git a/docs/agent-hooks.md b/docs/agent-hooks.md
index 70395bb5fda6..b644092e6fff 100644
--- a/docs/agent-hooks.md
+++ b/docs/agent-hooks.md
@@ -28,7 +28,7 @@ Supported agent names are `codex`, `grok`, `opencode`, `pi`, `omp`, `amp`, `curs
| Gemini | `gemini` | `~/.gemini/settings.json` | `gemini --resume ` | PreToolUse |
| Kiro CLI | `kiro-cli` | `~/.kiro/agents/cmux.json` or `$KIRO_HOME/agents/cmux.json` | `kiro-cli chat --resume-id ` | preToolUse, postToolUse |
| Rovo Dev | `acli` | `~/.rovodev/config.yml` | `acli rovodev run --restore ` | none |
-| Copilot | `copilot` | `~/.copilot/hooks/cmux.json` | `copilot --resume ` | permissionRequest, preToolUse |
+| Copilot | `copilot` | `~/.copilot/hooks/cmux.json` | `copilot --resume ` | preToolUse |
| CodeBuddy | `codebuddy` | `~/.codebuddy/settings.json` | `codebuddy --resume ` | PreToolUse |
| Factory | `droid` | `~/.factory/settings.json` | `droid --resume ` | PreToolUse |
| Qoder | `qodercli` | `~/.qoder/settings.json` | `qodercli --resume ` | PreToolUse |
diff --git a/docs/feed.md b/docs/feed.md
index 0083ff25a367..f3e2a9f602b7 100644
--- a/docs/feed.md
+++ b/docs/feed.md
@@ -80,7 +80,7 @@ Installs supported agent hooks whose binaries are on `PATH`. See [Agent hook int
| OpenCode | `~/.config/opencode/plugins/cmux-feed.js` | plugin event bus |
| Cursor CLI | `~/.cursor/hooks.json` | beforeShellExecution |
| Gemini | `~/.gemini/settings.json` | PreToolUse |
-| Copilot | `~/.copilot/hooks/cmux.json` | permissionRequest, preToolUse |
+| Copilot | `~/.copilot/hooks/cmux.json` | preToolUse |
| CodeBuddy | `~/.codebuddy/settings.json` | PreToolUse |
| Factory | `~/.factory/settings.json` | PreToolUse |
| Qoder | `~/.qoder/settings.json` | PreToolUse |
From 64340ca7b8d5852d208d41fab7fe52fce635b77c Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 06:33:56 -0700
Subject: [PATCH 24/39] fix: install Copilot permissionRequest gate
---
CLI/CMUXCLI+AgentHookDefinitions.swift | 6 ++++--
cmuxTests/CLICopilotHookFeedTests.swift | 19 ++++++++++++++++---
cmuxTests/FeedEventClassificationTests.swift | 7 ++++---
docs/agent-hooks.md | 2 +-
docs/feed.md | 2 +-
5 files changed, 26 insertions(+), 10 deletions(-)
diff --git a/CLI/CMUXCLI+AgentHookDefinitions.swift b/CLI/CMUXCLI+AgentHookDefinitions.swift
index 06bcd716c192..8b819a3c525e 100644
--- a/CLI/CMUXCLI+AgentHookDefinitions.swift
+++ b/CLI/CMUXCLI+AgentHookDefinitions.swift
@@ -321,7 +321,7 @@ extension CMUXCLI {
.init(agentEvent: "errorOccurred", cmuxSubcommand: "notification"),
.init(agentEvent: "sessionEnd", cmuxSubcommand: "session-end"),
],
- feedHookEvents: ["preToolUse"]
+ feedHookEvents: ["permissionRequest", "preToolUse"]
),
AgentHookDef(
name: "codebuddy", displayName: "CodeBuddy", statusKey: "codebuddy",
@@ -381,7 +381,9 @@ extension CMUXCLI {
}
static func feedHookCommandString(for def: AgentHookDef, agentEvent: String) -> String {
- let command = "cmux hooks feed --source \(def.name) --event \(agentEvent)"
+ // Copilot approval uses permissionRequest; preToolUse is telemetry to avoid duplicate prompts.
+ let telemetryOnlyFlag = def.name == "copilot" && agentEvent == "preToolUse" ? " --telemetry-only" : ""
+ let command = "cmux hooks feed --source \(def.name) --event \(agentEvent)\(telemetryOnlyFlag)"
switch def.format {
case .kiroAgentJSON:
return exitTwoPropagatingAgentHookShellCommand(
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index fd4f89878e48..7ddc1857cdbd 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -83,7 +83,6 @@ struct CLICopilotHookFeedTests {
#expect(hooks["SessionEnd"] == nil, "Copilot must use canonical camelCase hook names")
#expect(hooks["PreToolUse"] == nil, "Copilot must install canonical preToolUse hooks")
#expect(hooks["PermissionRequest"] == nil, "Copilot must install canonical permissionRequest hooks")
- #expect(hooks["permissionRequest"] == nil, "Copilot uses preToolUse for the installed approval gate")
let errorOccurred = try #require(hooks["errorOccurred"] as? [[String: Any]])
#expect(
errorOccurred.contains {
@@ -97,7 +96,7 @@ struct CLICopilotHookFeedTests {
#expect(
preToolUse.contains {
($0["bash"] as? String)?.contains("hooks feed --source copilot --event preToolUse") == true
- && ($0["bash"] as? String)?.contains("--telemetry-only") == false
+ && ($0["bash"] as? String)?.contains("--telemetry-only") == true
&& ($0["type"] as? String) == "command"
&& ($0["timeoutSec"] as? Int) == 125
&& $0["command"] == nil
@@ -105,9 +104,20 @@ struct CLICopilotHookFeedTests {
},
"Expected direct preToolUse bash hook with timeout slack, saw \(preToolUse)"
)
+ let permissionRequest = try #require(hooks["permissionRequest"] as? [[String: Any]])
+ #expect(
+ permissionRequest.contains {
+ ($0["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest") == true
+ && ($0["type"] as? String) == "command"
+ && ($0["timeoutSec"] as? Int) == 125
+ && $0["command"] == nil
+ && $0["hooks"] == nil
+ },
+ "Expected direct permissionRequest bash hook with timeout slack, saw \(permissionRequest)"
+ )
}
- @Test func copilotFeedDecisionEmitsPreToolUsePermissionDecision() throws {
+ @Test func copilotFeedDecisionEmitsCopilotPermissionDecisions() throws {
func runCopilotDecision(mode: String, event: String = "preToolUse") throws -> (ProcessRunResult, [String: Any]) {
let cliPath = try Self.bundledCLIPath()
let socketPath = Self.makeSocketPath("copilot-feed-decision")
@@ -174,6 +184,9 @@ struct CLICopilotHookFeedTests {
return (result, try #require(feedEvents.first))
}
+ // Fresh installs answer Copilot's permission service through
+ // permissionRequest. Keep unmarked preToolUse decision support for
+ // already-installed hook files and manual invocations.
let (allow, allowEvent) = try runCopilotDecision(mode: "once")
#expect(!allow.timedOut, Comment(rawValue: allow.stderr))
#expect(allow.status == 0, Comment(rawValue: allow.stderr))
diff --git a/cmuxTests/FeedEventClassificationTests.swift b/cmuxTests/FeedEventClassificationTests.swift
index 98360204af0a..02b64a209c69 100644
--- a/cmuxTests/FeedEventClassificationTests.swift
+++ b/cmuxTests/FeedEventClassificationTests.swift
@@ -93,9 +93,10 @@ struct FeedEventClassificationTests {
#expect(classify("gemini", "PreToolUse", tool: "Read").actionable == false)
}
- /// Copilot's `permissionRequest` hook blocks before the native permission
- /// service, and `preToolUse` still gates side-effecting tools before they
- /// execute. Read-only pre-tool events must fall through as telemetry.
+ /// Copilot's installed `permissionRequest` hook blocks before the native
+ /// permission service. Unmarked `preToolUse` still gates side-effecting
+ /// tools for legacy hook files and manual invocations; read-only pre-tool
+ /// events must fall through as telemetry.
@Test func copilotPreToolUseIsApprovalRequest() {
#expect(classify("copilot", "permissionRequest", tool: "bash").name == "PermissionRequest")
#expect(classify("copilot", "permissionRequest", tool: "bash").actionable == true)
diff --git a/docs/agent-hooks.md b/docs/agent-hooks.md
index b644092e6fff..cc67652fe8db 100644
--- a/docs/agent-hooks.md
+++ b/docs/agent-hooks.md
@@ -28,7 +28,7 @@ Supported agent names are `codex`, `grok`, `opencode`, `pi`, `omp`, `amp`, `curs
| Gemini | `gemini` | `~/.gemini/settings.json` | `gemini --resume ` | PreToolUse |
| Kiro CLI | `kiro-cli` | `~/.kiro/agents/cmux.json` or `$KIRO_HOME/agents/cmux.json` | `kiro-cli chat --resume-id ` | preToolUse, postToolUse |
| Rovo Dev | `acli` | `~/.rovodev/config.yml` | `acli rovodev run --restore ` | none |
-| Copilot | `copilot` | `~/.copilot/hooks/cmux.json` | `copilot --resume ` | preToolUse |
+| Copilot | `copilot` | `~/.copilot/hooks/cmux.json` | `copilot --resume ` | permissionRequest, preToolUse telemetry |
| CodeBuddy | `codebuddy` | `~/.codebuddy/settings.json` | `codebuddy --resume ` | PreToolUse |
| Factory | `droid` | `~/.factory/settings.json` | `droid --resume ` | PreToolUse |
| Qoder | `qodercli` | `~/.qoder/settings.json` | `qodercli --resume ` | PreToolUse |
diff --git a/docs/feed.md b/docs/feed.md
index f3e2a9f602b7..22b93def88d5 100644
--- a/docs/feed.md
+++ b/docs/feed.md
@@ -80,7 +80,7 @@ Installs supported agent hooks whose binaries are on `PATH`. See [Agent hook int
| OpenCode | `~/.config/opencode/plugins/cmux-feed.js` | plugin event bus |
| Cursor CLI | `~/.cursor/hooks.json` | beforeShellExecution |
| Gemini | `~/.gemini/settings.json` | PreToolUse |
-| Copilot | `~/.copilot/hooks/cmux.json` | preToolUse |
+| Copilot | `~/.copilot/hooks/cmux.json` | permissionRequest, preToolUse telemetry |
| CodeBuddy | `~/.codebuddy/settings.json` | PreToolUse |
| Factory | `~/.factory/settings.json` | PreToolUse |
| Qoder | `~/.qoder/settings.json` | PreToolUse |
From ff852935eb16b0debd681d32fa0843a6e4526c31 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 06:44:42 -0700
Subject: [PATCH 25/39] fix: tighten Copilot telemetry hooks
---
.github/swift-file-length-budget.tsv | 6 ++---
CLI/cmux.swift | 33 +++++++++++++++++++++++--
cmuxTests/CLICopilotHookFeedTests.swift | 4 +--
3 files changed, 36 insertions(+), 7 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index 5c611e05e6f9..0a32819280cb 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -1,7 +1,7 @@
# cmux-owned Swift file length budget.
# Format: max_linesrelative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
-34319 CLI/cmux.swift
+34348 CLI/cmux.swift
17719 Sources/AppDelegate.swift
16112 Sources/ContentView.swift
13952 Sources/TerminalController.swift
@@ -44,8 +44,8 @@
2257 Sources/TerminalNotificationStore.swift
2233 Sources/TerminalWindowPortal.swift
2126 cmuxTests/CmuxConfigTests.swift
-2103 Sources/SessionPersistence.swift
2091 cmuxTests/ShortcutAndCommandPaletteTests.swift
+2078 Sources/SessionPersistence.swift
1952 Sources/KeyboardShortcutSettingsFileStore.swift
1949 Sources/Panels/BrowserWebAuthnSupport.swift
1945 Sources/RestorableAgentSession.swift
@@ -128,7 +128,7 @@
718 Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
716 Sources/TaskManagerSnapshot.swift
715 Sources/AppleScriptSupport.swift
-712 CLI/CMUXCLI+AgentHookDefinitions.swift
+711 CLI/CMUXCLI+AgentHookDefinitions.swift
710 Sources/TerminalSSHSessionDetector.swift
706 CLI/CMUXCLI+Config.swift
705 Sources/Panels/BrowserPopupWindowController.swift
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index 6017de2eecc1..ce6366d0f984 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -25728,6 +25728,31 @@ struct CMUXCLI {
String(localized: "agent.generic.notification.body.sentNotification", defaultValue: "%@ sent a notification"),
def.displayName
)
+ if def.name == "copilot", let error = object["error"] as? [String: Any] {
+ let label = firstString(in: error, keys: ["name", "code", "status", "type"])
+ let details = firstString(in: error, keys: ["message", "description"])
+ ?? firstString(in: object, keys: ["message", "description"])
+ let body: String
+ switch (label.map(normalizedSingleLine), details.map(normalizedSingleLine)) {
+ case let (label?, details?) where !label.isEmpty && !details.isEmpty && details != label:
+ body = "\(label): \(details)"
+ case let (_, details?) where !details.isEmpty:
+ body = details
+ case let (label?, _) where !label.isEmpty:
+ body = label
+ default:
+ body = String.localizedStringWithFormat(
+ String(localized: "agent.generic.notification.body.reportedError", defaultValue: "%@ reported an error"),
+ def.displayName
+ )
+ }
+ return AgentHookNotificationSummary(
+ subtitle: String(localized: "agent.generic.notification.subtitle.error", defaultValue: "Error"),
+ body: truncate(body, maxLength: 180),
+ status: .error,
+ isFallback: false
+ )
+ }
let message = messageCandidates.compactMap { $0 }.first ?? fallbackBody
let signal = signalParts.compactMap { $0 }.joined(separator: " ")
let normalizedMessage = normalizedSingleLine(message)
@@ -26987,9 +27012,10 @@ struct CMUXCLI {
result[agentEvent] = groups
case .copilotJSON:
var entries = result[agentEvent] as? [[String: Any]] ?? []
+ let timeoutSeconds = Self.timeoutSecondsFromMilliseconds(feedTimeoutMs)
entries.append(Self.copilotHookEntry(
command: feedCmd,
- timeoutSeconds: Self.timeoutSecondsFromMilliseconds(feedTimeoutMs) + 5
+ timeoutSeconds: timeoutSeconds + (feedTimeoutMs >= 120_000 ? 5 : 0)
))
result[agentEvent] = entries
case .antigravityJSON:
@@ -27017,10 +27043,13 @@ struct CMUXCLI {
return Self.timeoutSecondsFromMilliseconds(timeoutMs)
}
- private func feedHookTimeoutMs(for def: AgentHookDef, agentEvent _: String) -> Int {
+ private func feedHookTimeoutMs(for def: AgentHookDef, agentEvent: String) -> Int {
if def.name == "codex" {
return 5_000
}
+ if def.name == "copilot", agentEvent == "preToolUse" {
+ return 5_000
+ }
return 120_000
}
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index 7ddc1857cdbd..5accfab27b39 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -98,11 +98,11 @@ struct CLICopilotHookFeedTests {
($0["bash"] as? String)?.contains("hooks feed --source copilot --event preToolUse") == true
&& ($0["bash"] as? String)?.contains("--telemetry-only") == true
&& ($0["type"] as? String) == "command"
- && ($0["timeoutSec"] as? Int) == 125
+ && ($0["timeoutSec"] as? Int) == 5
&& $0["command"] == nil
&& $0["hooks"] == nil
},
- "Expected direct preToolUse bash hook with timeout slack, saw \(preToolUse)"
+ "Expected telemetry preToolUse bash hook with short timeout, saw \(preToolUse)"
)
let permissionRequest = try #require(hooks["permissionRequest"] as? [[String: Any]])
#expect(
From 7d5c019862ff0e24f4bf95f48902cfc59b25428b Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 06:55:17 -0700
Subject: [PATCH 26/39] fix: remove Copilot preToolUse telemetry hook
---
.github/swift-file-length-budget.tsv | 5 +-
CLI/CMUXCLI+AgentHookDefinitions.swift | 7 +-
CLI/cmux.swift | 19 +-
Resources/feed-tui/index.ts | 10 +-
cmuxTests/CLICopilotHookFeedTests.swift | 196 +++++++++++++++++--
docs/agent-hooks.md | 2 +-
docs/feed.md | 2 +-
docs/notifications.md | 7 +
web/app/[locale]/docs/notifications/page.tsx | 7 +
9 files changed, 225 insertions(+), 30 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index 0a32819280cb..80f9e4039f8c 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -1,7 +1,7 @@
# cmux-owned Swift file length budget.
# Format: max_linesrelative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
-34348 CLI/cmux.swift
+34355 CLI/cmux.swift
17719 Sources/AppDelegate.swift
16112 Sources/ContentView.swift
13952 Sources/TerminalController.swift
@@ -128,7 +128,7 @@
718 Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
716 Sources/TaskManagerSnapshot.swift
715 Sources/AppleScriptSupport.swift
-711 CLI/CMUXCLI+AgentHookDefinitions.swift
+710 CLI/CMUXCLI+AgentHookDefinitions.swift
710 Sources/TerminalSSHSessionDetector.swift
706 CLI/CMUXCLI+Config.swift
705 Sources/Panels/BrowserPopupWindowController.swift
@@ -159,6 +159,7 @@
621 cmuxUITests/RightSidebarChromeHeightUITests.swift
620 cmuxTests/FinderFileDropRegressionTests.swift
620 cmuxTests/TerminalNotificationQueueTests.swift
+616 cmuxTests/CLICopilotHookFeedTests.swift
615 cmuxTests/RemoteTmuxControlParserTests.swift
612 cmuxUITests/FeedSidebarUITests.swift
608 Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift
diff --git a/CLI/CMUXCLI+AgentHookDefinitions.swift b/CLI/CMUXCLI+AgentHookDefinitions.swift
index 8b819a3c525e..34ee50bacb8e 100644
--- a/CLI/CMUXCLI+AgentHookDefinitions.swift
+++ b/CLI/CMUXCLI+AgentHookDefinitions.swift
@@ -319,9 +319,10 @@ extension CMUXCLI {
.init(agentEvent: "userPromptSubmitted", cmuxSubcommand: "prompt-submit"),
.init(agentEvent: "agentStop", cmuxSubcommand: "stop"),
.init(agentEvent: "errorOccurred", cmuxSubcommand: "notification"),
+ .init(agentEvent: "notification", cmuxSubcommand: "notification"),
.init(agentEvent: "sessionEnd", cmuxSubcommand: "session-end"),
],
- feedHookEvents: ["permissionRequest", "preToolUse"]
+ feedHookEvents: ["permissionRequest"]
),
AgentHookDef(
name: "codebuddy", displayName: "CodeBuddy", statusKey: "codebuddy",
@@ -381,9 +382,7 @@ extension CMUXCLI {
}
static func feedHookCommandString(for def: AgentHookDef, agentEvent: String) -> String {
- // Copilot approval uses permissionRequest; preToolUse is telemetry to avoid duplicate prompts.
- let telemetryOnlyFlag = def.name == "copilot" && agentEvent == "preToolUse" ? " --telemetry-only" : ""
- let command = "cmux hooks feed --source \(def.name) --event \(agentEvent)\(telemetryOnlyFlag)"
+ let command = "cmux hooks feed --source \(def.name) --event \(agentEvent)"
switch def.format {
case .kiroAgentJSON:
return exitTwoPropagatingAgentHookShellCommand(
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index ce6366d0f984..62e956e79f32 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -25728,10 +25728,11 @@ struct CMUXCLI {
String(localized: "agent.generic.notification.body.sentNotification", defaultValue: "%@ sent a notification"),
def.displayName
)
- if def.name == "copilot", let error = object["error"] as? [String: Any] {
+ let rawObject = parsedInput.rawObject ?? object
+ if def.name == "copilot", let error = rawObject["error"] as? [String: Any] {
let label = firstString(in: error, keys: ["name", "code", "status", "type"])
let details = firstString(in: error, keys: ["message", "description"])
- ?? firstString(in: object, keys: ["message", "description"])
+ ?? firstString(in: rawObject, keys: ["message", "description"])
let body: String
switch (label.map(normalizedSingleLine), details.map(normalizedSingleLine)) {
case let (label?, details?) where !label.isEmpty && !details.isEmpty && details != label:
@@ -27047,9 +27048,6 @@ struct CMUXCLI {
if def.name == "codex" {
return 5_000
}
- if def.name == "copilot", agentEvent == "preToolUse" {
- return 5_000
- }
return 120_000
}
@@ -28059,6 +28057,13 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
return false
}
+ private static func shouldPrependCmuxHookEntries(def: AgentHookDef, event: String) -> Bool {
+ // Copilot merges permissionRequest outputs with later hooks overriding
+ // earlier hooks. Keep existing policy hooks after cmux so their deny
+ // output cannot be overwritten by a Feed approval.
+ def.name == "copilot" && event == "permissionRequest"
+ }
+
private func installAgentHooks(_ def: AgentHookDef) throws {
if def.name == "opencode" {
try installOpenCodePluginHooks(def)
@@ -28206,7 +28211,9 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
case .flat, .kiroAgentJSON, .copilotJSON:
var entries = hooks[event] as? [[String: Any]] ?? []
if let newEntries = value as? [[String: Any]] {
- if let insertionIndexes = cmuxInsertionIndexes[event], !insertionIndexes.isEmpty {
+ if Self.shouldPrependCmuxHookEntries(def: def, event: event) {
+ entries.insert(contentsOf: newEntries, at: 0)
+ } else if let insertionIndexes = cmuxInsertionIndexes[event], !insertionIndexes.isEmpty {
Self.insertCmuxHookValues(newEntries, into: &entries, atOriginalIndexes: insertionIndexes)
} else {
entries.append(contentsOf: newEntries)
diff --git a/Resources/feed-tui/index.ts b/Resources/feed-tui/index.ts
index 7ced21bfde26..aa7f63d14bed 100644
--- a/Resources/feed-tui/index.ts
+++ b/Resources/feed-tui/index.ts
@@ -1123,7 +1123,7 @@ function sourceSupportsOncePermissionMode(source: string, toolInputCapabilities?
}
function sourceSupportsAlwaysPermissionMode(source: string, toolInputCapabilities?: string): boolean {
- if (source === "hermes-agent") {
+ if (!sourceSupportsPersistentPermissionModes(source)) {
return false;
}
if (source !== "codex") {
@@ -1133,7 +1133,7 @@ function sourceSupportsAlwaysPermissionMode(source: string, toolInputCapabilitie
}
function sourceSupportsAllPermissionMode(source: string, toolInputCapabilities?: string): boolean {
- if (source === "hermes-agent") {
+ if (!sourceSupportsPersistentPermissionModes(source)) {
return false;
}
if (source !== "codex") {
@@ -1223,8 +1223,12 @@ function codexDecisionAvailableOrUnspecified(decision: string, decisions: Set String {
try BundledCLITestSupport.bundledCLIPath(for: BundleProbe.self)
}
+ private static func runCopilotNotificationHook(
+ socketName: String,
+ payloadFields: [String: Any]
+ ) throws -> (result: ProcessRunResult, commands: [String], workspaceId: String, surfaceId: String) {
+ let cliPath = try Self.bundledCLIPath()
+ let socketPath = Self.makeSocketPath(socketName)
+ let listenerFD = try Self.bindUnixSocket(at: socketPath)
+ let state = MockSocketServerState()
+ let root = FileManager.default.temporaryDirectory
+ .appendingPathComponent("cmux-\(socketName)-\(UUID().uuidString)", isDirectory: true)
+ let workspaceId = "55555555-5555-5555-5555-555555555555"
+ let surfaceId = "66666666-6666-6666-6666-666666666666"
+
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
+ defer {
+ Darwin.close(listenerFD)
+ unlink(socketPath)
+ try? FileManager.default.removeItem(at: root)
+ }
+
+ let server = Self.startMockServer(listenerFD: listenerFD, state: state) { line in
+ guard let payload = Self.jsonObject(line) else {
+ return "OK"
+ }
+ guard let id = payload["id"] as? String, let method = payload["method"] as? String else {
+ return Self.malformedRequestResponse(id: payload["id"] as? String, raw: line)
+ }
+ switch method {
+ case "surface.list":
+ return Self.v2Response(
+ id: id,
+ ok: true,
+ result: [
+ "surfaces": [
+ ["id": surfaceId, "ref": "surface:1", "focused": true],
+ ],
+ ]
+ )
+ case "feed.push":
+ return Self.v2Response(id: id, ok: true, result: [:])
+ default:
+ return Self.v2Response(id: id, ok: true, result: [:])
+ }
+ }
+
+ var inputPayload = payloadFields
+ inputPayload["sessionId"] = inputPayload["sessionId"] ?? "copilot-notification-session"
+ inputPayload["cwd"] = inputPayload["cwd"] ?? root.path
+ let inputData = try JSONSerialization.data(withJSONObject: inputPayload, options: [.sortedKeys])
+ let input = try #require(String(data: inputData, encoding: .utf8))
+
+ let result = Self.runProcess(
+ executablePath: cliPath,
+ arguments: ["hooks", "copilot", "notification"],
+ environment: [
+ "HOME": root.path,
+ "PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
+ "PWD": root.path,
+ "CMUX_SOCKET_PATH": socketPath,
+ "CMUX_WORKSPACE_ID": workspaceId,
+ "CMUX_SURFACE_ID": surfaceId,
+ "CMUX_AGENT_HOOK_STATE_DIR": root.path,
+ "CMUX_CLI_SENTRY_DISABLED": "1",
+ ],
+ standardInput: input,
+ timeout: 5
+ )
+ #expect(server.wait(timeout: 5), "socket server did not observe notification commands")
+
+ return (result, state.snapshot(), workspaceId, surfaceId)
+ }
+
private static func makeSocketPath(_ name: String) -> String {
let shortID = UUID().uuidString.replacingOccurrences(of: "-", with: "").prefix(8)
return URL(fileURLWithPath: NSTemporaryDirectory())
diff --git a/docs/agent-hooks.md b/docs/agent-hooks.md
index cc67652fe8db..cc63fd97be53 100644
--- a/docs/agent-hooks.md
+++ b/docs/agent-hooks.md
@@ -28,7 +28,7 @@ Supported agent names are `codex`, `grok`, `opencode`, `pi`, `omp`, `amp`, `curs
| Gemini | `gemini` | `~/.gemini/settings.json` | `gemini --resume ` | PreToolUse |
| Kiro CLI | `kiro-cli` | `~/.kiro/agents/cmux.json` or `$KIRO_HOME/agents/cmux.json` | `kiro-cli chat --resume-id ` | preToolUse, postToolUse |
| Rovo Dev | `acli` | `~/.rovodev/config.yml` | `acli rovodev run --restore ` | none |
-| Copilot | `copilot` | `~/.copilot/hooks/cmux.json` | `copilot --resume ` | permissionRequest, preToolUse telemetry |
+| Copilot | `copilot` | `~/.copilot/hooks/cmux.json` | `copilot --resume ` | permissionRequest |
| CodeBuddy | `codebuddy` | `~/.codebuddy/settings.json` | `codebuddy --resume ` | PreToolUse |
| Factory | `droid` | `~/.factory/settings.json` | `droid --resume ` | PreToolUse |
| Qoder | `qodercli` | `~/.qoder/settings.json` | `qodercli --resume ` | PreToolUse |
diff --git a/docs/feed.md b/docs/feed.md
index 22b93def88d5..133c6c168413 100644
--- a/docs/feed.md
+++ b/docs/feed.md
@@ -80,7 +80,7 @@ Installs supported agent hooks whose binaries are on `PATH`. See [Agent hook int
| OpenCode | `~/.config/opencode/plugins/cmux-feed.js` | plugin event bus |
| Cursor CLI | `~/.cursor/hooks.json` | beforeShellExecution |
| Gemini | `~/.gemini/settings.json` | PreToolUse |
-| Copilot | `~/.copilot/hooks/cmux.json` | permissionRequest, preToolUse telemetry |
+| Copilot | `~/.copilot/hooks/cmux.json` | permissionRequest |
| CodeBuddy | `~/.codebuddy/settings.json` | PreToolUse |
| Factory | `~/.factory/settings.json` | PreToolUse |
| Qoder | `~/.qoder/settings.json` | PreToolUse |
diff --git a/docs/notifications.md b/docs/notifications.md
index 95f482a6c692..ec4faf9da740 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -144,6 +144,13 @@ Copilot CLI supports [hooks](https://docs.github.com/en/copilot/how-tos/copilot-
"timeoutSec": 5
}
],
+ "notification": [
+ {
+ "type": "command",
+ "bash": "if command -v cmux &>/dev/null; then cmux hooks copilot notification; fi",
+ "timeoutSec": 5
+ }
+ ],
"sessionEnd": [
{
"type": "command",
diff --git a/web/app/[locale]/docs/notifications/page.tsx b/web/app/[locale]/docs/notifications/page.tsx
index 090aa9c5df36..e6ee44373a6c 100644
--- a/web/app/[locale]/docs/notifications/page.tsx
+++ b/web/app/[locale]/docs/notifications/page.tsx
@@ -290,6 +290,13 @@ esac`}
"timeoutSec": 5
}
],
+ "notification": [
+ {
+ "type": "command",
+ "bash": "if command -v cmux &>/dev/null; then cmux hooks copilot notification; fi",
+ "timeoutSec": 5
+ }
+ ],
"sessionEnd": [
{
"type": "command",
From f4086b02d16e456c6c42dfac06ffd60ef8629770 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 07:20:46 -0700
Subject: [PATCH 27/39] fix: preserve Copilot permission policies
---
.github/swift-file-length-budget.tsv | 6 +++---
CLI/cmux.swift | 17 ++++-------------
cmuxTests/CLICopilotHookFeedTests.swift | 12 +++++-------
3 files changed, 12 insertions(+), 23 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index e7a7343b9585..2a48ab22166d 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -1,12 +1,12 @@
# cmux-owned Swift file length budget.
# Format: max_linesrelative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
-34355 CLI/cmux.swift
+34346 CLI/cmux.swift
17719 Sources/AppDelegate.swift
16112 Sources/ContentView.swift
13952 Sources/TerminalController.swift
12772 Sources/Workspace.swift
-12257 Sources/GhosttyTerminalView.swift
+12238 Sources/GhosttyTerminalView.swift
12144 cmuxTests/AppDelegateShortcutRoutingTests.swift
11924 Sources/Panels/BrowserPanel.swift
9335 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
@@ -159,8 +159,8 @@
621 cmuxUITests/RightSidebarChromeHeightUITests.swift
620 cmuxTests/FinderFileDropRegressionTests.swift
620 cmuxTests/TerminalNotificationQueueTests.swift
-616 cmuxTests/CLICopilotHookFeedTests.swift
615 cmuxTests/RemoteTmuxControlParserTests.swift
+614 cmuxTests/CLICopilotHookFeedTests.swift
612 cmuxUITests/FeedSidebarUITests.swift
608 Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift
606 Sources/SettingsNavigation.swift
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index 62e956e79f32..b25a1ac875b6 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -28057,13 +28057,6 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
return false
}
- private static func shouldPrependCmuxHookEntries(def: AgentHookDef, event: String) -> Bool {
- // Copilot merges permissionRequest outputs with later hooks overriding
- // earlier hooks. Keep existing policy hooks after cmux so their deny
- // output cannot be overwritten by a Feed approval.
- def.name == "copilot" && event == "permissionRequest"
- }
-
private func installAgentHooks(_ def: AgentHookDef) throws {
if def.name == "opencode" {
try installOpenCodePluginHooks(def)
@@ -28211,9 +28204,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
case .flat, .kiroAgentJSON, .copilotJSON:
var entries = hooks[event] as? [[String: Any]] ?? []
if let newEntries = value as? [[String: Any]] {
- if Self.shouldPrependCmuxHookEntries(def: def, event: event) {
- entries.insert(contentsOf: newEntries, at: 0)
- } else if let insertionIndexes = cmuxInsertionIndexes[event], !insertionIndexes.isEmpty {
+ if let insertionIndexes = cmuxInsertionIndexes[event], !insertionIndexes.isEmpty {
Self.insertCmuxHookValues(newEntries, into: &entries, atOriginalIndexes: insertionIndexes)
} else {
entries.append(contentsOf: newEntries)
@@ -33252,10 +33243,10 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
defaultValue: "User denied permission via cmux Feed."
)
if rawEventName == "permissionRequest" || rawEventName == "PermissionRequest" {
- if mode == "once" {
- return encode(["behavior": "allow"])
+ if mode == "deny" {
+ return encode(["behavior": "deny", "message": reason])
}
- return encode(["behavior": "deny", "message": reason])
+ return "{}"
}
if mode == "once" {
return encode(["permissionDecision": "allow"])
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index 300d5ef93d2d..c23469927100 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -140,14 +140,12 @@ struct CLICopilotHookFeedTests {
)
let permissionRequest = try #require(hooks["permissionRequest"] as? [[String: Any]])
#expect(
- (permissionRequest.first?["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest") == true,
- "Expected cmux permissionRequest hook to run before existing policy hooks, saw \(permissionRequest)"
+ (permissionRequest.first?["bash"] as? String)?.contains("\"behavior\":\"deny\"") == true,
+ "Expected existing permissionRequest policy hook to stay before cmux, saw \(permissionRequest)"
)
#expect(
- permissionRequest.dropFirst().contains {
- ($0["bash"] as? String)?.contains("\"behavior\":\"deny\"") == true
- },
- "Expected existing permissionRequest policy hook to be preserved after cmux, saw \(permissionRequest)"
+ (permissionRequest.last?["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest") == true,
+ "Expected cmux permissionRequest hook to be final within the user hooks file, saw \(permissionRequest)"
)
#expect(
permissionRequest.contains {
@@ -262,7 +260,7 @@ struct CLICopilotHookFeedTests {
#expect(permissionAllow.status == 0, Comment(rawValue: permissionAllow.stderr))
#expect(permissionAllowEvent["hook_event_name"] as? String == "PermissionRequest")
let permissionAllowOutput = try #require(Self.jsonObject(permissionAllow.stdout))
- #expect(permissionAllowOutput["behavior"] as? String == "allow")
+ #expect(permissionAllowOutput.isEmpty)
#expect(permissionAllowOutput["permissionDecision"] == nil)
#expect(permissionAllowOutput["hookSpecificOutput"] == nil)
From 5c3013600fcb72dead9de4411cf22df2838a32ba Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 07:25:59 -0700
Subject: [PATCH 28/39] fix: approve Copilot permission requests
---
CLI/cmux.swift | 32 +++++++++++++++++++------
cmuxTests/CLICopilotHookFeedTests.swift | 19 +++++++++++----
2 files changed, 39 insertions(+), 12 deletions(-)
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index b25a1ac875b6..b64e41a17f3b 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -28149,11 +28149,13 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
var rewrittenEntries: [[String: Any]] = []
for entry in entries {
if Self.jsonHookValueContainsCmuxOwnedCommand(entry, for: def) {
- Self.appendCmuxHookInsertionIndex(
- rewrittenEntries.count,
- for: event,
- to: &cmuxInsertionIndexes
- )
+ if Self.shouldPreserveCmuxHookInsertionIndex(def: def, event: event) {
+ Self.appendCmuxHookInsertionIndex(
+ rewrittenEntries.count,
+ for: event,
+ to: &cmuxInsertionIndexes
+ )
+ }
continue
}
rewrittenEntries.append(entry)
@@ -28173,7 +28175,8 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
rewrittenGroups.append(group)
continue
}
- if hookList.contains(where: { isCmuxOwnedCommand($0["command"] as? String ?? "") }) {
+ if Self.shouldPreserveCmuxHookInsertionIndex(def: def, event: event),
+ hookList.contains(where: { isCmuxOwnedCommand($0["command"] as? String ?? "") }) {
Self.appendCmuxHookInsertionIndex(
rewrittenGroups.count,
for: event,
@@ -28206,6 +28209,8 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
if let newEntries = value as? [[String: Any]] {
if let insertionIndexes = cmuxInsertionIndexes[event], !insertionIndexes.isEmpty {
Self.insertCmuxHookValues(newEntries, into: &entries, atOriginalIndexes: insertionIndexes)
+ } else if Self.shouldPrependNewCmuxHookValues(def: def, event: event) {
+ entries.insert(contentsOf: newEntries, at: 0)
} else {
entries.append(contentsOf: newEntries)
}
@@ -28216,6 +28221,8 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
if let newGroups = value as? [[String: Any]] {
if let insertionIndexes = cmuxInsertionIndexes[event], !insertionIndexes.isEmpty {
Self.insertCmuxHookValues(newGroups, into: &groups, atOriginalIndexes: insertionIndexes)
+ } else if Self.shouldPrependNewCmuxHookValues(def: def, event: event) {
+ groups.insert(contentsOf: newGroups, at: 0)
} else {
groups.append(contentsOf: newGroups)
}
@@ -28559,6 +28566,14 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
indexes[event, default: []].append(index)
}
+ private static func shouldPreserveCmuxHookInsertionIndex(def: AgentHookDef, event: String) -> Bool {
+ !(def.name == "copilot" && event == "permissionRequest")
+ }
+
+ private static func shouldPrependNewCmuxHookValues(def: AgentHookDef, event: String) -> Bool {
+ def.name == "copilot" && event == "permissionRequest"
+ }
+
private static func insertCmuxHookValues(_ values: [T], into target: inout [T], atOriginalIndexes indexes: [Int]) {
var insertedCount = 0
for originalIndex in indexes {
@@ -33246,7 +33261,10 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
if mode == "deny" {
return encode(["behavior": "deny", "message": reason])
}
- return "{}"
+ if mode == "once" {
+ return encode(["behavior": "allow"])
+ }
+ return encode(["behavior": "deny", "message": reason])
}
if mode == "once" {
return encode(["permissionDecision": "allow"])
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index c23469927100..3b8105398440 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -140,12 +140,12 @@ struct CLICopilotHookFeedTests {
)
let permissionRequest = try #require(hooks["permissionRequest"] as? [[String: Any]])
#expect(
- (permissionRequest.first?["bash"] as? String)?.contains("\"behavior\":\"deny\"") == true,
- "Expected existing permissionRequest policy hook to stay before cmux, saw \(permissionRequest)"
+ (permissionRequest.first?["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest") == true,
+ "Expected cmux permissionRequest hook to run before preserved policy hooks, saw \(permissionRequest)"
)
#expect(
- (permissionRequest.last?["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest") == true,
- "Expected cmux permissionRequest hook to be final within the user hooks file, saw \(permissionRequest)"
+ (permissionRequest.last?["bash"] as? String)?.contains("\"behavior\":\"deny\"") == true,
+ "Expected existing permissionRequest policy hook to stay after cmux so Copilot can merge it later, saw \(permissionRequest)"
)
#expect(
permissionRequest.contains {
@@ -260,7 +260,7 @@ struct CLICopilotHookFeedTests {
#expect(permissionAllow.status == 0, Comment(rawValue: permissionAllow.stderr))
#expect(permissionAllowEvent["hook_event_name"] as? String == "PermissionRequest")
let permissionAllowOutput = try #require(Self.jsonObject(permissionAllow.stdout))
- #expect(permissionAllowOutput.isEmpty)
+ #expect(permissionAllowOutput["behavior"] as? String == "allow")
#expect(permissionAllowOutput["permissionDecision"] == nil)
#expect(permissionAllowOutput["hookSpecificOutput"] == nil)
@@ -272,6 +272,15 @@ struct CLICopilotHookFeedTests {
#expect(permissionDenyOutput["message"] as? String == "User denied permission via cmux Feed.")
#expect(permissionDenyOutput["permissionDecision"] == nil)
#expect(permissionDenyOutput["hookSpecificOutput"] == nil)
+
+ let (permissionUnsupported, _) = try runCopilotDecision(mode: "always", event: "permissionRequest")
+ #expect(!permissionUnsupported.timedOut, Comment(rawValue: permissionUnsupported.stderr))
+ #expect(permissionUnsupported.status == 0, Comment(rawValue: permissionUnsupported.stderr))
+ let permissionUnsupportedOutput = try #require(Self.jsonObject(permissionUnsupported.stdout))
+ #expect(permissionUnsupportedOutput["behavior"] as? String == "deny")
+ #expect(permissionUnsupportedOutput["message"] as? String == "User denied permission via cmux Feed.")
+ #expect(permissionUnsupportedOutput["permissionDecision"] == nil)
+ #expect(permissionUnsupportedOutput["hookSpecificOutput"] == nil)
}
@Test func copilotErrorNotificationUsesStructuredErrorPayload() throws {
From 8acf4223eff3a49033190c0691d8a2c2907e8c4e Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 07:31:58 -0700
Subject: [PATCH 29/39] chore: refresh Swift file length budget
---
.github/swift-file-length-budget.tsv | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index 2a48ab22166d..7f0eacec8825 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -1,7 +1,7 @@
# cmux-owned Swift file length budget.
# Format: max_linesrelative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
-34346 CLI/cmux.swift
+34364 CLI/cmux.swift
17719 Sources/AppDelegate.swift
16112 Sources/ContentView.swift
13952 Sources/TerminalController.swift
@@ -156,11 +156,11 @@
636 Sources/TerminalController+ControlPaneContext.swift
630 Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutWhenClause.swift
629 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift
+623 cmuxTests/CLICopilotHookFeedTests.swift
621 cmuxUITests/RightSidebarChromeHeightUITests.swift
620 cmuxTests/FinderFileDropRegressionTests.swift
620 cmuxTests/TerminalNotificationQueueTests.swift
615 cmuxTests/RemoteTmuxControlParserTests.swift
-614 cmuxTests/CLICopilotHookFeedTests.swift
612 cmuxUITests/FeedSidebarUITests.swift
608 Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift
606 Sources/SettingsNavigation.swift
From db9f6168e7d58a834084d5ec631e7e355c494314 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 07:43:56 -0700
Subject: [PATCH 30/39] fix: preserve Copilot denial authority
---
.github/swift-file-length-budget.tsv | 4 +-
CLI/cmux.swift | 178 +++++++++++++++++++++++-
cmuxTests/CLICopilotHookFeedTests.swift | 122 +++++++++++++++-
3 files changed, 289 insertions(+), 15 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index 7f0eacec8825..48a9064c4307 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -1,7 +1,7 @@
# cmux-owned Swift file length budget.
# Format: max_linesrelative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
-34364 CLI/cmux.swift
+34530 CLI/cmux.swift
17719 Sources/AppDelegate.swift
16112 Sources/ContentView.swift
13952 Sources/TerminalController.swift
@@ -123,6 +123,7 @@
754 Sources/TerminalController+ControlWorkspaceContext.swift
752 cmuxUITests/CloseWorkspaceCmdDUITests.swift
738 Packages/macOS/CMUXProjectModel/Sources/CMUXProjectModel/XcodeProjectAdapter.swift
+731 cmuxTests/CLICopilotHookFeedTests.swift
726 cmuxTests/CLICodexHookTimeoutRegressionTests.swift
722 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift
718 Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
@@ -156,7 +157,6 @@
636 Sources/TerminalController+ControlPaneContext.swift
630 Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutWhenClause.swift
629 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift
-623 cmuxTests/CLICopilotHookFeedTests.swift
621 cmuxUITests/RightSidebarChromeHeightUITests.swift
620 cmuxTests/FinderFileDropRegressionTests.swift
620 cmuxTests/TerminalNotificationQueueTests.swift
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index b64e41a17f3b..4d2ca04b0a24 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -27018,6 +27018,12 @@ struct CMUXCLI {
command: feedCmd,
timeoutSeconds: timeoutSeconds + (feedTimeoutMs >= 120_000 ? 5 : 0)
))
+ if def.name == "copilot", agentEvent == "permissionRequest" {
+ entries.append(Self.copilotHookEntry(
+ command: "\(feedCmd) --copilot-deny-guard",
+ timeoutSeconds: Self.timeoutSecondsFromMilliseconds(5_000)
+ ))
+ }
result[agentEvent] = entries
case .antigravityJSON:
var entries = result[agentEvent] as? [[String: Any]] ?? []
@@ -28207,7 +28213,10 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
case .flat, .kiroAgentJSON, .copilotJSON:
var entries = hooks[event] as? [[String: Any]] ?? []
if let newEntries = value as? [[String: Any]] {
- if let insertionIndexes = cmuxInsertionIndexes[event], !insertionIndexes.isEmpty {
+ if Self.shouldBracketCmuxHookValues(def: def, event: event), !newEntries.isEmpty {
+ entries.insert(newEntries[0], at: 0)
+ entries.append(contentsOf: newEntries.dropFirst())
+ } else if let insertionIndexes = cmuxInsertionIndexes[event], !insertionIndexes.isEmpty {
Self.insertCmuxHookValues(newEntries, into: &entries, atOriginalIndexes: insertionIndexes)
} else if Self.shouldPrependNewCmuxHookValues(def: def, event: event) {
entries.insert(contentsOf: newEntries, at: 0)
@@ -28574,6 +28583,10 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
def.name == "copilot" && event == "permissionRequest"
}
+ private static func shouldBracketCmuxHookValues(def: AgentHookDef, event: String) -> Bool {
+ def.name == "copilot" && event == "permissionRequest"
+ }
+
private static func insertCmuxHookValues(_ values: [T], into target: inout [T], atOriginalIndexes indexes: [Int]) {
var insertedCount = 0
for originalIndex in indexes {
@@ -31127,6 +31140,120 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
return "fallback-\(String(digest.prefix(16)))"
}
+ private func fallbackFeedRequestId(
+ source: String,
+ sessionId: String,
+ rawEvent: String,
+ toolName: String,
+ rawObject: [String: Any]
+ ) -> String {
+ guard source == "copilot" else {
+ return "\(source)-\(sessionId)-\(rawEvent)-\(toolName)-\(Int(Date().timeIntervalSince1970 * 1000))"
+ }
+ var stableObject = rawObject
+ stableObject.removeValue(forKey: "timestamp")
+ let seedData = (try? JSONSerialization.data(
+ withJSONObject: stableObject,
+ options: [.sortedKeys]
+ )) ?? Data(String(describing: stableObject).utf8)
+ var seed = Data()
+ seed.append(Data(source.utf8))
+ seed.append(0)
+ seed.append(Data(sessionId.utf8))
+ seed.append(0)
+ seed.append(Data(rawEvent.utf8))
+ seed.append(0)
+ seed.append(Data(toolName.utf8))
+ seed.append(0)
+ seed.append(seedData)
+ let digest = SHA256.hash(data: seed)
+ .map { String(format: "%02x", $0) }
+ .joined()
+ return "\(source)-permission-\(String(digest.prefix(24)))"
+ }
+
+ private func copilotPermissionDecisionFileURL(
+ requestId: String,
+ env: [String: String]
+ ) -> URL {
+ let baseDirectory: URL
+ if let overrideDirectory = normalizedHookValue(env["CMUX_AGENT_HOOK_STATE_DIR"]) {
+ baseDirectory = URL(
+ fileURLWithPath: NSString(string: overrideDirectory).expandingTildeInPath,
+ isDirectory: true
+ )
+ } else {
+ baseDirectory = FileManager.default.homeDirectoryForCurrentUser
+ .appendingPathComponent(".cmuxterm", isDirectory: true)
+ }
+ let digest = SHA256.hash(data: Data(requestId.utf8))
+ .map { String(format: "%02x", $0) }
+ .joined()
+ return baseDirectory
+ .appendingPathComponent("copilot-permission-decisions", isDirectory: true)
+ .appendingPathComponent("\(digest).json", isDirectory: false)
+ }
+
+ private func recordCopilotPermissionDecisionIfNeeded(
+ source: String,
+ rawEventName: String,
+ requestId: String,
+ decision: [String: Any],
+ env: [String: String]
+ ) {
+ guard Self.isCopilotPermissionRequest(source: source, rawEventName: rawEventName) else {
+ return
+ }
+ let fileURL = copilotPermissionDecisionFileURL(requestId: requestId, env: env)
+ let mode = ((decision["mode"] as? String) ?? "deny")
+ .trimmingCharacters(in: .whitespacesAndNewlines)
+ .lowercased()
+ if mode == "once" {
+ try? FileManager.default.removeItem(at: fileURL)
+ return
+ }
+ do {
+ try FileManager.default.createDirectory(
+ at: fileURL.deletingLastPathComponent(),
+ withIntermediateDirectories: true
+ )
+ let payload: [String: Any] = [
+ "mode": "deny",
+ "updatedAt": Date().timeIntervalSince1970,
+ ]
+ let data = try JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys])
+ try data.write(to: fileURL, options: .atomic)
+ } catch {
+ try? FileManager.default.removeItem(at: fileURL)
+ }
+ }
+
+ private func copilotPermissionDenyGuardOutput(
+ source: String,
+ rawEventName: String,
+ requestId: String,
+ env: [String: String]
+ ) -> String {
+ guard Self.isCopilotPermissionRequest(source: source, rawEventName: rawEventName) else {
+ return "{}"
+ }
+ let fileURL = copilotPermissionDecisionFileURL(requestId: requestId, env: env)
+ defer { try? FileManager.default.removeItem(at: fileURL) }
+ guard let data = try? Data(contentsOf: fileURL),
+ let payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+ (payload["mode"] as? String) == "deny" else {
+ return "{}"
+ }
+ let updatedAt = payload["updatedAt"] as? TimeInterval ?? 0
+ guard Date().timeIntervalSince1970 - updatedAt <= 600 else {
+ return "{}"
+ }
+ return Self.encodeHookOutput([
+ "behavior": "deny",
+ "message": Self.copilotPermissionDeniedReason(),
+ ])
+ }
+
private func enrichUserPromptSubmitFeedEvent(
_ event: inout [String: Any],
hookEventName: String,
@@ -32984,9 +33111,25 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
)
let requestId = stdinObj["_opencode_request_id"] as? String
?? firstString(in: stdinObj, keys: ["request_id", "tool_use_id", "toolUseID"])
- ?? "\(source)-\(sessionId)-\(rawEvent)-\(toolName)-\(Int(Date().timeIntervalSince1970 * 1000))"
+ ?? fallbackFeedRequestId(
+ source: source,
+ sessionId: sessionId,
+ rawEvent: rawEvent,
+ toolName: toolName,
+ rawObject: stdinObj
+ )
eventDict["_opencode_request_id"] = requestId
+ if commandArgs.contains("--copilot-deny-guard") {
+ print(copilotPermissionDenyGuardOutput(
+ source: source,
+ rawEventName: rawEvent,
+ requestId: requestId,
+ env: env
+ ))
+ return
+ }
+
// Sync. For actionable events we block up to 120s waiting
// for the user's Feed click; the hook's stdout is then a
// proper hookSpecificOutput that Claude honors directly
@@ -33078,6 +33221,13 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
if source == "kiro", Self.emitKiroDecisionIfHandled(decision: decision) {
return
}
+ recordCopilotPermissionDecisionIfNeeded(
+ source: source,
+ rawEventName: rawEvent,
+ requestId: requestId,
+ decision: decision,
+ env: env
+ )
let out = Self.renderAgentDecision(
source: source,
rawEventName: rawEvent,
@@ -33138,6 +33288,25 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
exit(2)
}
+ private static func isCopilotPermissionRequest(source: String, rawEventName: String) -> Bool {
+ source == "copilot" && (rawEventName == "permissionRequest" || rawEventName == "PermissionRequest")
+ }
+
+ private static func copilotPermissionDeniedReason() -> String {
+ String(
+ localized: "cli.hooks.feed.permissionDeniedReason",
+ defaultValue: "User denied permission via cmux Feed."
+ )
+ }
+
+ private static func encodeHookOutput(_ obj: [String: Any]) -> String {
+ guard let data = try? JSONSerialization.data(withJSONObject: obj, options: [.sortedKeys]),
+ let string = String(data: data, encoding: .utf8) else {
+ return "{}"
+ }
+ return string
+ }
+
private static let skipInterviewAndPlanAnswer = "Skip interview and plan immediately"
/// Encodes the user's decision in the agent's expected hook stdout shape.
private static func renderAgentDecision(
@@ -33253,10 +33422,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
return encode(permissionRequestHookDecision(behavior: "allow"))
}
if source == "copilot" {
- let reason = String(
- localized: "cli.hooks.feed.permissionDeniedReason",
- defaultValue: "User denied permission via cmux Feed."
- )
+ let reason = Self.copilotPermissionDeniedReason()
if rawEventName == "permissionRequest" || rawEventName == "PermissionRequest" {
if mode == "deny" {
return encode(["behavior": "deny", "message": reason])
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index 3b8105398440..b6220959b7f4 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -140,16 +140,22 @@ struct CLICopilotHookFeedTests {
)
let permissionRequest = try #require(hooks["permissionRequest"] as? [[String: Any]])
#expect(
- (permissionRequest.first?["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest") == true,
- "Expected cmux permissionRequest hook to run before preserved policy hooks, saw \(permissionRequest)"
+ (permissionRequest.first?["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest") == true
+ && (permissionRequest.first?["bash"] as? String)?.contains("--copilot-deny-guard") == false,
+ "Expected cmux permissionRequest Feed hook to run before preserved policy hooks, saw \(permissionRequest)"
)
#expect(
- (permissionRequest.last?["bash"] as? String)?.contains("\"behavior\":\"deny\"") == true,
- "Expected existing permissionRequest policy hook to stay after cmux so Copilot can merge it later, saw \(permissionRequest)"
+ permissionRequest.contains { ($0["bash"] as? String)?.contains("\"behavior\":\"deny\"") == true },
+ "Expected existing permissionRequest policy hook to be preserved between cmux hooks, saw \(permissionRequest)"
+ )
+ #expect(
+ (permissionRequest.last?["bash"] as? String)?.contains("--copilot-deny-guard") == true,
+ "Expected cmux permissionRequest denial guard to run after preserved policy hooks, saw \(permissionRequest)"
)
#expect(
permissionRequest.contains {
($0["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest") == true
+ && ($0["bash"] as? String)?.contains("--copilot-deny-guard") == false
&& ($0["type"] as? String) == "command"
&& ($0["timeoutSec"] as? Int) == 125
&& $0["command"] == nil
@@ -157,6 +163,16 @@ struct CLICopilotHookFeedTests {
},
"Expected direct permissionRequest bash hook with timeout slack, saw \(permissionRequest)"
)
+ #expect(
+ permissionRequest.contains {
+ ($0["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest --copilot-deny-guard") == true
+ && ($0["type"] as? String) == "command"
+ && ($0["timeoutSec"] as? Int) == 5
+ && $0["command"] == nil
+ && $0["hooks"] == nil
+ },
+ "Expected direct permissionRequest denial guard hook with short timeout, saw \(permissionRequest)"
+ )
}
@Test func copilotFeedDecisionEmitsCopilotPermissionDecisions() throws {
@@ -206,6 +222,7 @@ struct CLICopilotHookFeedTests {
"CMUX_WORKSPACE_ID": workspaceId,
"CMUX_SURFACE_ID": surfaceId,
"CMUX_COPILOT_PID": "525252",
+ "CMUX_AGENT_HOOK_STATE_DIR": root.path,
"CMUX_CLI_SENTRY_DISABLED": "1",
],
standardInput: #"{"sessionId":"copilot-session-123","cwd":"\#(root.path)","toolName":"bash","toolArgs":{"command":"touch \#(root.appendingPathComponent("README.md").path)"}}"#,
@@ -226,6 +243,83 @@ struct CLICopilotHookFeedTests {
return (result, try #require(feedEvents.first))
}
+ func runCopilotPermissionRequestWithGuard(mode: String) throws -> (ProcessRunResult, ProcessRunResult, [String: Any]) {
+ let cliPath = try Self.bundledCLIPath()
+ let socketPath = Self.makeSocketPath("copilot-feed-guard")
+ let listenerFD = try Self.bindUnixSocket(at: socketPath)
+ let state = MockSocketServerState()
+ let root = FileManager.default.temporaryDirectory
+ .appendingPathComponent("cmux-copilot-feed-guard-\(UUID().uuidString)", isDirectory: true)
+ let workspaceId = "33333333-3333-3333-3333-333333333333"
+ let surfaceId = "44444444-4444-4444-4444-444444444444"
+
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
+ defer {
+ Darwin.close(listenerFD)
+ unlink(socketPath)
+ try? FileManager.default.removeItem(at: root)
+ }
+
+ let server = Self.startMockServer(listenerFD: listenerFD, state: state) { line in
+ guard let payload = Self.jsonObject(line) else {
+ return Self.malformedRequestResponse(raw: line)
+ }
+ guard let id = payload["id"] as? String, let method = payload["method"] as? String else {
+ return Self.malformedRequestResponse(id: payload["id"] as? String, raw: line)
+ }
+ #expect(method == "feed.push")
+ return Self.v2Response(
+ id: id,
+ ok: true,
+ result: [
+ "status": "resolved",
+ "decision": ["kind": "permission", "mode": mode],
+ ]
+ )
+ }
+
+ let input = #"{"sessionId":"copilot-session-123","cwd":"\#(root.path)","toolName":"bash","toolArgs":{"command":"touch \#(root.appendingPathComponent("README.md").path)"}}"#
+ let environment = [
+ "HOME": root.path,
+ "PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
+ "PWD": root.path,
+ "CMUX_SOCKET_PATH": socketPath,
+ "CMUX_WORKSPACE_ID": workspaceId,
+ "CMUX_SURFACE_ID": surfaceId,
+ "CMUX_COPILOT_PID": "525252",
+ "CMUX_AGENT_HOOK_STATE_DIR": root.path,
+ "CMUX_CLI_SENTRY_DISABLED": "1",
+ ]
+ let first = Self.runProcess(
+ executablePath: cliPath,
+ arguments: ["hooks", "feed", "--source", "copilot", "--event", "permissionRequest"],
+ environment: environment,
+ standardInput: input,
+ timeout: 5
+ )
+ #expect(server.wait(timeout: 5), "socket server did not observe feed.push")
+
+ let feedEvents = state.snapshot().compactMap { command -> [String: Any]? in
+ guard let payload = Self.jsonObject(command),
+ payload["method"] as? String == "feed.push",
+ let params = payload["params"] as? [String: Any],
+ let event = params["event"] as? [String: Any] else {
+ return nil
+ }
+ return event
+ }
+ let firstEvent = try #require(feedEvents.first)
+
+ let guardResult = Self.runProcess(
+ executablePath: cliPath,
+ arguments: ["hooks", "feed", "--source", "copilot", "--event", "permissionRequest", "--copilot-deny-guard"],
+ environment: environment,
+ standardInput: input,
+ timeout: 5
+ )
+ return (first, guardResult, firstEvent)
+ }
+
// Fresh installs answer Copilot's permission service through
// permissionRequest. Keep unmarked preToolUse decision support for
// already-installed hook files and manual invocations.
@@ -255,7 +349,7 @@ struct CLICopilotHookFeedTests {
#expect(unsupportedOutput["permissionDecisionReason"] as? String == "User denied permission via cmux Feed.")
#expect(unsupportedOutput["hookSpecificOutput"] == nil)
- let (permissionAllow, permissionAllowEvent) = try runCopilotDecision(mode: "once", event: "permissionRequest")
+ let (permissionAllow, permissionAllowGuard, permissionAllowEvent) = try runCopilotPermissionRequestWithGuard(mode: "once")
#expect(!permissionAllow.timedOut, Comment(rawValue: permissionAllow.stderr))
#expect(permissionAllow.status == 0, Comment(rawValue: permissionAllow.stderr))
#expect(permissionAllowEvent["hook_event_name"] as? String == "PermissionRequest")
@@ -263,8 +357,12 @@ struct CLICopilotHookFeedTests {
#expect(permissionAllowOutput["behavior"] as? String == "allow")
#expect(permissionAllowOutput["permissionDecision"] == nil)
#expect(permissionAllowOutput["hookSpecificOutput"] == nil)
+ #expect(!permissionAllowGuard.timedOut, Comment(rawValue: permissionAllowGuard.stderr))
+ #expect(permissionAllowGuard.status == 0, Comment(rawValue: permissionAllowGuard.stderr))
+ let permissionAllowGuardOutput = try #require(Self.jsonObject(permissionAllowGuard.stdout))
+ #expect(permissionAllowGuardOutput.isEmpty)
- let (permissionDeny, _) = try runCopilotDecision(mode: "deny", event: "permissionRequest")
+ let (permissionDeny, permissionDenyGuard, _) = try runCopilotPermissionRequestWithGuard(mode: "deny")
#expect(!permissionDeny.timedOut, Comment(rawValue: permissionDeny.stderr))
#expect(permissionDeny.status == 0, Comment(rawValue: permissionDeny.stderr))
let permissionDenyOutput = try #require(Self.jsonObject(permissionDeny.stdout))
@@ -272,8 +370,13 @@ struct CLICopilotHookFeedTests {
#expect(permissionDenyOutput["message"] as? String == "User denied permission via cmux Feed.")
#expect(permissionDenyOutput["permissionDecision"] == nil)
#expect(permissionDenyOutput["hookSpecificOutput"] == nil)
+ #expect(!permissionDenyGuard.timedOut, Comment(rawValue: permissionDenyGuard.stderr))
+ #expect(permissionDenyGuard.status == 0, Comment(rawValue: permissionDenyGuard.stderr))
+ let permissionDenyGuardOutput = try #require(Self.jsonObject(permissionDenyGuard.stdout))
+ #expect(permissionDenyGuardOutput["behavior"] as? String == "deny")
+ #expect(permissionDenyGuardOutput["message"] as? String == "User denied permission via cmux Feed.")
- let (permissionUnsupported, _) = try runCopilotDecision(mode: "always", event: "permissionRequest")
+ let (permissionUnsupported, permissionUnsupportedGuard, _) = try runCopilotPermissionRequestWithGuard(mode: "always")
#expect(!permissionUnsupported.timedOut, Comment(rawValue: permissionUnsupported.stderr))
#expect(permissionUnsupported.status == 0, Comment(rawValue: permissionUnsupported.stderr))
let permissionUnsupportedOutput = try #require(Self.jsonObject(permissionUnsupported.stdout))
@@ -281,6 +384,11 @@ struct CLICopilotHookFeedTests {
#expect(permissionUnsupportedOutput["message"] as? String == "User denied permission via cmux Feed.")
#expect(permissionUnsupportedOutput["permissionDecision"] == nil)
#expect(permissionUnsupportedOutput["hookSpecificOutput"] == nil)
+ #expect(!permissionUnsupportedGuard.timedOut, Comment(rawValue: permissionUnsupportedGuard.stderr))
+ #expect(permissionUnsupportedGuard.status == 0, Comment(rawValue: permissionUnsupportedGuard.stderr))
+ let permissionUnsupportedGuardOutput = try #require(Self.jsonObject(permissionUnsupportedGuard.stdout))
+ #expect(permissionUnsupportedGuardOutput["behavior"] as? String == "deny")
+ #expect(permissionUnsupportedGuardOutput["message"] as? String == "User denied permission via cmux Feed.")
}
@Test func copilotErrorNotificationUsesStructuredErrorPayload() throws {
From a5ef39447d0ebf648cf07f4c6c69e356755f23ca Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 07:56:44 -0700
Subject: [PATCH 31/39] fix: gate Copilot after native permissions
---
.github/swift-file-length-budget.tsv | 4 +-
CLI/CMUXCLI+AgentHookDefinitions.swift | 2 +-
CLI/cmux.swift | 188 +++----------------
cmuxTests/CLICopilotHookFeedTests.swift | 178 ++++--------------
cmuxTests/FeedEventClassificationTests.swift | 8 +-
docs/agent-hooks.md | 2 +-
docs/feed.md | 2 +-
7 files changed, 70 insertions(+), 314 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index 48a9064c4307..a48ff663add4 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -1,7 +1,7 @@
# cmux-owned Swift file length budget.
# Format: max_linesrelative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
-34530 CLI/cmux.swift
+34392 CLI/cmux.swift
17719 Sources/AppDelegate.swift
16112 Sources/ContentView.swift
13952 Sources/TerminalController.swift
@@ -123,7 +123,6 @@
754 Sources/TerminalController+ControlWorkspaceContext.swift
752 cmuxUITests/CloseWorkspaceCmdDUITests.swift
738 Packages/macOS/CMUXProjectModel/Sources/CMUXProjectModel/XcodeProjectAdapter.swift
-731 cmuxTests/CLICopilotHookFeedTests.swift
726 cmuxTests/CLICodexHookTimeoutRegressionTests.swift
722 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift
718 Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
@@ -157,6 +156,7 @@
636 Sources/TerminalController+ControlPaneContext.swift
630 Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutWhenClause.swift
629 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift
+625 cmuxTests/CLICopilotHookFeedTests.swift
621 cmuxUITests/RightSidebarChromeHeightUITests.swift
620 cmuxTests/FinderFileDropRegressionTests.swift
620 cmuxTests/TerminalNotificationQueueTests.swift
diff --git a/CLI/CMUXCLI+AgentHookDefinitions.swift b/CLI/CMUXCLI+AgentHookDefinitions.swift
index 34ee50bacb8e..8489099595fc 100644
--- a/CLI/CMUXCLI+AgentHookDefinitions.swift
+++ b/CLI/CMUXCLI+AgentHookDefinitions.swift
@@ -322,7 +322,7 @@ extension CMUXCLI {
.init(agentEvent: "notification", cmuxSubcommand: "notification"),
.init(agentEvent: "sessionEnd", cmuxSubcommand: "session-end"),
],
- feedHookEvents: ["permissionRequest"]
+ feedHookEvents: ["preToolUse"]
),
AgentHookDef(
name: "codebuddy", displayName: "CodeBuddy", statusKey: "codebuddy",
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index 4d2ca04b0a24..94f5dee7558e 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -27018,12 +27018,6 @@ struct CMUXCLI {
command: feedCmd,
timeoutSeconds: timeoutSeconds + (feedTimeoutMs >= 120_000 ? 5 : 0)
))
- if def.name == "copilot", agentEvent == "permissionRequest" {
- entries.append(Self.copilotHookEntry(
- command: "\(feedCmd) --copilot-deny-guard",
- timeoutSeconds: Self.timeoutSecondsFromMilliseconds(5_000)
- ))
- }
result[agentEvent] = entries
case .antigravityJSON:
var entries = result[agentEvent] as? [[String: Any]] ?? []
@@ -28213,10 +28207,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
case .flat, .kiroAgentJSON, .copilotJSON:
var entries = hooks[event] as? [[String: Any]] ?? []
if let newEntries = value as? [[String: Any]] {
- if Self.shouldBracketCmuxHookValues(def: def, event: event), !newEntries.isEmpty {
- entries.insert(newEntries[0], at: 0)
- entries.append(contentsOf: newEntries.dropFirst())
- } else if let insertionIndexes = cmuxInsertionIndexes[event], !insertionIndexes.isEmpty {
+ if let insertionIndexes = cmuxInsertionIndexes[event], !insertionIndexes.isEmpty {
Self.insertCmuxHookValues(newEntries, into: &entries, atOriginalIndexes: insertionIndexes)
} else if Self.shouldPrependNewCmuxHookValues(def: def, event: event) {
entries.insert(contentsOf: newEntries, at: 0)
@@ -28583,10 +28574,6 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
def.name == "copilot" && event == "permissionRequest"
}
- private static func shouldBracketCmuxHookValues(def: AgentHookDef, event: String) -> Bool {
- def.name == "copilot" && event == "permissionRequest"
- }
-
private static func insertCmuxHookValues(_ values: [T], into target: inout [T], atOriginalIndexes indexes: [Int]) {
var insertedCount = 0
for originalIndex in indexes {
@@ -31140,120 +31127,6 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
return "fallback-\(String(digest.prefix(16)))"
}
- private func fallbackFeedRequestId(
- source: String,
- sessionId: String,
- rawEvent: String,
- toolName: String,
- rawObject: [String: Any]
- ) -> String {
- guard source == "copilot" else {
- return "\(source)-\(sessionId)-\(rawEvent)-\(toolName)-\(Int(Date().timeIntervalSince1970 * 1000))"
- }
- var stableObject = rawObject
- stableObject.removeValue(forKey: "timestamp")
- let seedData = (try? JSONSerialization.data(
- withJSONObject: stableObject,
- options: [.sortedKeys]
- )) ?? Data(String(describing: stableObject).utf8)
- var seed = Data()
- seed.append(Data(source.utf8))
- seed.append(0)
- seed.append(Data(sessionId.utf8))
- seed.append(0)
- seed.append(Data(rawEvent.utf8))
- seed.append(0)
- seed.append(Data(toolName.utf8))
- seed.append(0)
- seed.append(seedData)
- let digest = SHA256.hash(data: seed)
- .map { String(format: "%02x", $0) }
- .joined()
- return "\(source)-permission-\(String(digest.prefix(24)))"
- }
-
- private func copilotPermissionDecisionFileURL(
- requestId: String,
- env: [String: String]
- ) -> URL {
- let baseDirectory: URL
- if let overrideDirectory = normalizedHookValue(env["CMUX_AGENT_HOOK_STATE_DIR"]) {
- baseDirectory = URL(
- fileURLWithPath: NSString(string: overrideDirectory).expandingTildeInPath,
- isDirectory: true
- )
- } else {
- baseDirectory = FileManager.default.homeDirectoryForCurrentUser
- .appendingPathComponent(".cmuxterm", isDirectory: true)
- }
- let digest = SHA256.hash(data: Data(requestId.utf8))
- .map { String(format: "%02x", $0) }
- .joined()
- return baseDirectory
- .appendingPathComponent("copilot-permission-decisions", isDirectory: true)
- .appendingPathComponent("\(digest).json", isDirectory: false)
- }
-
- private func recordCopilotPermissionDecisionIfNeeded(
- source: String,
- rawEventName: String,
- requestId: String,
- decision: [String: Any],
- env: [String: String]
- ) {
- guard Self.isCopilotPermissionRequest(source: source, rawEventName: rawEventName) else {
- return
- }
- let fileURL = copilotPermissionDecisionFileURL(requestId: requestId, env: env)
- let mode = ((decision["mode"] as? String) ?? "deny")
- .trimmingCharacters(in: .whitespacesAndNewlines)
- .lowercased()
- if mode == "once" {
- try? FileManager.default.removeItem(at: fileURL)
- return
- }
- do {
- try FileManager.default.createDirectory(
- at: fileURL.deletingLastPathComponent(),
- withIntermediateDirectories: true
- )
- let payload: [String: Any] = [
- "mode": "deny",
- "updatedAt": Date().timeIntervalSince1970,
- ]
- let data = try JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys])
- try data.write(to: fileURL, options: .atomic)
- } catch {
- try? FileManager.default.removeItem(at: fileURL)
- }
- }
-
- private func copilotPermissionDenyGuardOutput(
- source: String,
- rawEventName: String,
- requestId: String,
- env: [String: String]
- ) -> String {
- guard Self.isCopilotPermissionRequest(source: source, rawEventName: rawEventName) else {
- return "{}"
- }
- let fileURL = copilotPermissionDecisionFileURL(requestId: requestId, env: env)
- defer { try? FileManager.default.removeItem(at: fileURL) }
- guard let data = try? Data(contentsOf: fileURL),
- let payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
- (payload["mode"] as? String) == "deny" else {
- return "{}"
- }
- let updatedAt = payload["updatedAt"] as? TimeInterval ?? 0
- guard Date().timeIntervalSince1970 - updatedAt <= 600 else {
- return "{}"
- }
- return Self.encodeHookOutput([
- "behavior": "deny",
- "message": Self.copilotPermissionDeniedReason(),
- ])
- }
-
private func enrichUserPromptSubmitFeedEvent(
_ event: inout [String: Any],
hookEventName: String,
@@ -33111,23 +32984,25 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
)
let requestId = stdinObj["_opencode_request_id"] as? String
?? firstString(in: stdinObj, keys: ["request_id", "tool_use_id", "toolUseID"])
- ?? fallbackFeedRequestId(
- source: source,
- sessionId: sessionId,
- rawEvent: rawEvent,
- toolName: toolName,
- rawObject: stdinObj
- )
+ ?? "\(source)-\(sessionId)-\(rawEvent)-\(toolName)-\(Int(Date().timeIntervalSince1970 * 1000))"
eventDict["_opencode_request_id"] = requestId
- if commandArgs.contains("--copilot-deny-guard") {
- print(copilotPermissionDenyGuardOutput(
+ func emitCopilotPreToolUseDenyIfNeeded() -> Bool {
+ guard source == "copilot",
+ isActionable,
+ rawEvent == "preToolUse" || rawEvent == "PreToolUse" else {
+ return false
+ }
+ print(Self.renderAgentDecision(
source: source,
rawEventName: rawEvent,
- requestId: requestId,
- env: env
+ hookEventName: hookEventName,
+ toolName: toolName,
+ toolInput: eventDict["tool_input"],
+ rawObject: stdinObj,
+ decision: ["kind": "permission", "mode": "deny"]
))
- return
+ return true
}
// Sync. For actionable events we block up to 120s waiting
@@ -33203,6 +33078,9 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
responseTimeout: waitTimeout + 5
)
} catch {
+ if emitCopilotPreToolUseDenyIfNeeded() {
+ return
+ }
print("{}")
return
}
@@ -33212,6 +33090,9 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
let ok = respObj["ok"] as? Bool, ok,
let result = respObj["result"] as? [String: Any]
else {
+ if emitCopilotPreToolUseDenyIfNeeded() {
+ return
+ }
print("{}")
return
}
@@ -33221,13 +33102,6 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
if source == "kiro", Self.emitKiroDecisionIfHandled(decision: decision) {
return
}
- recordCopilotPermissionDecisionIfNeeded(
- source: source,
- rawEventName: rawEvent,
- requestId: requestId,
- decision: decision,
- env: env
- )
let out = Self.renderAgentDecision(
source: source,
rawEventName: rawEvent,
@@ -33240,6 +33114,9 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
print(out)
return
}
+ if emitCopilotPreToolUseDenyIfNeeded() {
+ return
+ }
print("{}")
}
@@ -33288,10 +33165,6 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
exit(2)
}
- private static func isCopilotPermissionRequest(source: String, rawEventName: String) -> Bool {
- source == "copilot" && (rawEventName == "permissionRequest" || rawEventName == "PermissionRequest")
- }
-
private static func copilotPermissionDeniedReason() -> String {
String(
localized: "cli.hooks.feed.permissionDeniedReason",
@@ -33299,14 +33172,6 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
)
}
- private static func encodeHookOutput(_ obj: [String: Any]) -> String {
- guard let data = try? JSONSerialization.data(withJSONObject: obj, options: [.sortedKeys]),
- let string = String(data: data, encoding: .utf8) else {
- return "{}"
- }
- return string
- }
-
private static let skipInterviewAndPlanAnswer = "Skip interview and plan immediately"
/// Encodes the user's decision in the agent's expected hook stdout shape.
private static func renderAgentDecision(
@@ -33427,10 +33292,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
if mode == "deny" {
return encode(["behavior": "deny", "message": reason])
}
- if mode == "once" {
- return encode(["behavior": "allow"])
- }
- return encode(["behavior": "deny", "message": reason])
+ return "{}"
}
if mode == "once" {
return encode(["permissionDecision": "allow"])
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index b6220959b7f4..f7d26256d0e4 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -117,8 +117,19 @@ struct CLICopilotHookFeedTests {
"Expected user preToolUse hook to be preserved, saw \(preservedPreToolUse)"
)
#expect(
- !preservedPreToolUse.contains { ($0["bash"] as? String)?.contains("cmux hooks feed --source copilot --event preToolUse") == true },
- "Copilot command preToolUse hooks fail closed, so cmux must remove stale telemetry hooks, saw \(preservedPreToolUse)"
+ !preservedPreToolUse.contains { ($0["bash"] as? String)?.contains("--telemetry-only") == true },
+ "Expected stale cmux telemetry-only preToolUse hook to be removed, saw \(preservedPreToolUse)"
+ )
+ #expect(
+ preservedPreToolUse.contains {
+ ($0["bash"] as? String)?.contains("hooks feed --source copilot --event preToolUse") == true
+ && ($0["bash"] as? String)?.contains("--telemetry-only") == false
+ && ($0["type"] as? String) == "command"
+ && ($0["timeoutSec"] as? Int) == 125
+ && $0["command"] == nil
+ && $0["hooks"] == nil
+ },
+ "Expected direct preToolUse Feed hook with timeout slack, saw \(preservedPreToolUse)"
)
let errorOccurred = try #require(hooks["errorOccurred"] as? [[String: Any]])
#expect(
@@ -140,43 +151,18 @@ struct CLICopilotHookFeedTests {
)
let permissionRequest = try #require(hooks["permissionRequest"] as? [[String: Any]])
#expect(
- (permissionRequest.first?["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest") == true
- && (permissionRequest.first?["bash"] as? String)?.contains("--copilot-deny-guard") == false,
- "Expected cmux permissionRequest Feed hook to run before preserved policy hooks, saw \(permissionRequest)"
- )
- #expect(
- permissionRequest.contains { ($0["bash"] as? String)?.contains("\"behavior\":\"deny\"") == true },
- "Expected existing permissionRequest policy hook to be preserved between cmux hooks, saw \(permissionRequest)"
- )
- #expect(
- (permissionRequest.last?["bash"] as? String)?.contains("--copilot-deny-guard") == true,
- "Expected cmux permissionRequest denial guard to run after preserved policy hooks, saw \(permissionRequest)"
- )
- #expect(
- permissionRequest.contains {
- ($0["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest") == true
- && ($0["bash"] as? String)?.contains("--copilot-deny-guard") == false
- && ($0["type"] as? String) == "command"
- && ($0["timeoutSec"] as? Int) == 125
- && $0["command"] == nil
- && $0["hooks"] == nil
- },
- "Expected direct permissionRequest bash hook with timeout slack, saw \(permissionRequest)"
- )
- #expect(
- permissionRequest.contains {
- ($0["bash"] as? String)?.contains("hooks feed --source copilot --event permissionRequest --copilot-deny-guard") == true
- && ($0["type"] as? String) == "command"
- && ($0["timeoutSec"] as? Int) == 5
- && $0["command"] == nil
- && $0["hooks"] == nil
- },
- "Expected direct permissionRequest denial guard hook with short timeout, saw \(permissionRequest)"
+ permissionRequest.count == 1
+ && (permissionRequest.first?["bash"] as? String)?.contains("\"behavior\":\"deny\"") == true,
+ "Expected existing permissionRequest policy hook to be preserved without cmux permissionRequest hooks, saw \(permissionRequest)"
)
}
@Test func copilotFeedDecisionEmitsCopilotPermissionDecisions() throws {
- func runCopilotDecision(mode: String, event: String = "preToolUse") throws -> (ProcessRunResult, [String: Any]) {
+ func runCopilotDecision(
+ mode: String,
+ event: String = "preToolUse",
+ status: String = "resolved"
+ ) throws -> (ProcessRunResult, [String: Any]) {
let cliPath = try Self.bundledCLIPath()
let socketPath = Self.makeSocketPath("copilot-feed-decision")
let listenerFD = try Self.bindUnixSocket(at: socketPath)
@@ -205,7 +191,7 @@ struct CLICopilotHookFeedTests {
id: id,
ok: true,
result: [
- "status": "resolved",
+ "status": status,
"decision": ["kind": "permission", "mode": mode],
]
)
@@ -243,86 +229,9 @@ struct CLICopilotHookFeedTests {
return (result, try #require(feedEvents.first))
}
- func runCopilotPermissionRequestWithGuard(mode: String) throws -> (ProcessRunResult, ProcessRunResult, [String: Any]) {
- let cliPath = try Self.bundledCLIPath()
- let socketPath = Self.makeSocketPath("copilot-feed-guard")
- let listenerFD = try Self.bindUnixSocket(at: socketPath)
- let state = MockSocketServerState()
- let root = FileManager.default.temporaryDirectory
- .appendingPathComponent("cmux-copilot-feed-guard-\(UUID().uuidString)", isDirectory: true)
- let workspaceId = "33333333-3333-3333-3333-333333333333"
- let surfaceId = "44444444-4444-4444-4444-444444444444"
-
- try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
- defer {
- Darwin.close(listenerFD)
- unlink(socketPath)
- try? FileManager.default.removeItem(at: root)
- }
-
- let server = Self.startMockServer(listenerFD: listenerFD, state: state) { line in
- guard let payload = Self.jsonObject(line) else {
- return Self.malformedRequestResponse(raw: line)
- }
- guard let id = payload["id"] as? String, let method = payload["method"] as? String else {
- return Self.malformedRequestResponse(id: payload["id"] as? String, raw: line)
- }
- #expect(method == "feed.push")
- return Self.v2Response(
- id: id,
- ok: true,
- result: [
- "status": "resolved",
- "decision": ["kind": "permission", "mode": mode],
- ]
- )
- }
-
- let input = #"{"sessionId":"copilot-session-123","cwd":"\#(root.path)","toolName":"bash","toolArgs":{"command":"touch \#(root.appendingPathComponent("README.md").path)"}}"#
- let environment = [
- "HOME": root.path,
- "PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
- "PWD": root.path,
- "CMUX_SOCKET_PATH": socketPath,
- "CMUX_WORKSPACE_ID": workspaceId,
- "CMUX_SURFACE_ID": surfaceId,
- "CMUX_COPILOT_PID": "525252",
- "CMUX_AGENT_HOOK_STATE_DIR": root.path,
- "CMUX_CLI_SENTRY_DISABLED": "1",
- ]
- let first = Self.runProcess(
- executablePath: cliPath,
- arguments: ["hooks", "feed", "--source", "copilot", "--event", "permissionRequest"],
- environment: environment,
- standardInput: input,
- timeout: 5
- )
- #expect(server.wait(timeout: 5), "socket server did not observe feed.push")
-
- let feedEvents = state.snapshot().compactMap { command -> [String: Any]? in
- guard let payload = Self.jsonObject(command),
- payload["method"] as? String == "feed.push",
- let params = payload["params"] as? [String: Any],
- let event = params["event"] as? [String: Any] else {
- return nil
- }
- return event
- }
- let firstEvent = try #require(feedEvents.first)
-
- let guardResult = Self.runProcess(
- executablePath: cliPath,
- arguments: ["hooks", "feed", "--source", "copilot", "--event", "permissionRequest", "--copilot-deny-guard"],
- environment: environment,
- standardInput: input,
- timeout: 5
- )
- return (first, guardResult, firstEvent)
- }
-
- // Fresh installs answer Copilot's permission service through
- // permissionRequest. Keep unmarked preToolUse decision support for
- // already-installed hook files and manual invocations.
+ // Fresh installs gate after Copilot's native permission service through
+ // preToolUse. Keep permissionRequest denial support for already-installed
+ // hook files and manual invocations, but approvals fall through there.
let (allow, allowEvent) = try runCopilotDecision(mode: "once")
#expect(!allow.timedOut, Comment(rawValue: allow.stderr))
#expect(allow.status == 0, Comment(rawValue: allow.stderr))
@@ -349,20 +258,24 @@ struct CLICopilotHookFeedTests {
#expect(unsupportedOutput["permissionDecisionReason"] as? String == "User denied permission via cmux Feed.")
#expect(unsupportedOutput["hookSpecificOutput"] == nil)
- let (permissionAllow, permissionAllowGuard, permissionAllowEvent) = try runCopilotPermissionRequestWithGuard(mode: "once")
+ let (timeout, _) = try runCopilotDecision(mode: "once", status: "timeout")
+ #expect(!timeout.timedOut, Comment(rawValue: timeout.stderr))
+ #expect(timeout.status == 0, Comment(rawValue: timeout.stderr))
+ let timeoutOutput = try #require(Self.jsonObject(timeout.stdout))
+ #expect(timeoutOutput["permissionDecision"] as? String == "deny")
+ #expect(timeoutOutput["permissionDecisionReason"] as? String == "User denied permission via cmux Feed.")
+ #expect(timeoutOutput["hookSpecificOutput"] == nil)
+
+ let (permissionAllow, permissionAllowEvent) = try runCopilotDecision(mode: "once", event: "permissionRequest")
#expect(!permissionAllow.timedOut, Comment(rawValue: permissionAllow.stderr))
#expect(permissionAllow.status == 0, Comment(rawValue: permissionAllow.stderr))
#expect(permissionAllowEvent["hook_event_name"] as? String == "PermissionRequest")
let permissionAllowOutput = try #require(Self.jsonObject(permissionAllow.stdout))
- #expect(permissionAllowOutput["behavior"] as? String == "allow")
+ #expect(permissionAllowOutput.isEmpty)
#expect(permissionAllowOutput["permissionDecision"] == nil)
#expect(permissionAllowOutput["hookSpecificOutput"] == nil)
- #expect(!permissionAllowGuard.timedOut, Comment(rawValue: permissionAllowGuard.stderr))
- #expect(permissionAllowGuard.status == 0, Comment(rawValue: permissionAllowGuard.stderr))
- let permissionAllowGuardOutput = try #require(Self.jsonObject(permissionAllowGuard.stdout))
- #expect(permissionAllowGuardOutput.isEmpty)
- let (permissionDeny, permissionDenyGuard, _) = try runCopilotPermissionRequestWithGuard(mode: "deny")
+ let (permissionDeny, _) = try runCopilotDecision(mode: "deny", event: "permissionRequest")
#expect(!permissionDeny.timedOut, Comment(rawValue: permissionDeny.stderr))
#expect(permissionDeny.status == 0, Comment(rawValue: permissionDeny.stderr))
let permissionDenyOutput = try #require(Self.jsonObject(permissionDeny.stdout))
@@ -370,25 +283,6 @@ struct CLICopilotHookFeedTests {
#expect(permissionDenyOutput["message"] as? String == "User denied permission via cmux Feed.")
#expect(permissionDenyOutput["permissionDecision"] == nil)
#expect(permissionDenyOutput["hookSpecificOutput"] == nil)
- #expect(!permissionDenyGuard.timedOut, Comment(rawValue: permissionDenyGuard.stderr))
- #expect(permissionDenyGuard.status == 0, Comment(rawValue: permissionDenyGuard.stderr))
- let permissionDenyGuardOutput = try #require(Self.jsonObject(permissionDenyGuard.stdout))
- #expect(permissionDenyGuardOutput["behavior"] as? String == "deny")
- #expect(permissionDenyGuardOutput["message"] as? String == "User denied permission via cmux Feed.")
-
- let (permissionUnsupported, permissionUnsupportedGuard, _) = try runCopilotPermissionRequestWithGuard(mode: "always")
- #expect(!permissionUnsupported.timedOut, Comment(rawValue: permissionUnsupported.stderr))
- #expect(permissionUnsupported.status == 0, Comment(rawValue: permissionUnsupported.stderr))
- let permissionUnsupportedOutput = try #require(Self.jsonObject(permissionUnsupported.stdout))
- #expect(permissionUnsupportedOutput["behavior"] as? String == "deny")
- #expect(permissionUnsupportedOutput["message"] as? String == "User denied permission via cmux Feed.")
- #expect(permissionUnsupportedOutput["permissionDecision"] == nil)
- #expect(permissionUnsupportedOutput["hookSpecificOutput"] == nil)
- #expect(!permissionUnsupportedGuard.timedOut, Comment(rawValue: permissionUnsupportedGuard.stderr))
- #expect(permissionUnsupportedGuard.status == 0, Comment(rawValue: permissionUnsupportedGuard.stderr))
- let permissionUnsupportedGuardOutput = try #require(Self.jsonObject(permissionUnsupportedGuard.stdout))
- #expect(permissionUnsupportedGuardOutput["behavior"] as? String == "deny")
- #expect(permissionUnsupportedGuardOutput["message"] as? String == "User denied permission via cmux Feed.")
}
@Test func copilotErrorNotificationUsesStructuredErrorPayload() throws {
diff --git a/cmuxTests/FeedEventClassificationTests.swift b/cmuxTests/FeedEventClassificationTests.swift
index 02b64a209c69..e0a153ae8b14 100644
--- a/cmuxTests/FeedEventClassificationTests.swift
+++ b/cmuxTests/FeedEventClassificationTests.swift
@@ -93,10 +93,10 @@ struct FeedEventClassificationTests {
#expect(classify("gemini", "PreToolUse", tool: "Read").actionable == false)
}
- /// Copilot's installed `permissionRequest` hook blocks before the native
- /// permission service. Unmarked `preToolUse` still gates side-effecting
- /// tools for legacy hook files and manual invocations; read-only pre-tool
- /// events must fall through as telemetry.
+ /// Copilot's installed `preToolUse` hook gates side-effecting tools after
+ /// the native permission service; read-only pre-tool events must fall
+ /// through as telemetry. `permissionRequest` remains classifiable for
+ /// already-installed hook files and manual invocations.
@Test func copilotPreToolUseIsApprovalRequest() {
#expect(classify("copilot", "permissionRequest", tool: "bash").name == "PermissionRequest")
#expect(classify("copilot", "permissionRequest", tool: "bash").actionable == true)
diff --git a/docs/agent-hooks.md b/docs/agent-hooks.md
index cc63fd97be53..b644092e6fff 100644
--- a/docs/agent-hooks.md
+++ b/docs/agent-hooks.md
@@ -28,7 +28,7 @@ Supported agent names are `codex`, `grok`, `opencode`, `pi`, `omp`, `amp`, `curs
| Gemini | `gemini` | `~/.gemini/settings.json` | `gemini --resume ` | PreToolUse |
| Kiro CLI | `kiro-cli` | `~/.kiro/agents/cmux.json` or `$KIRO_HOME/agents/cmux.json` | `kiro-cli chat --resume-id ` | preToolUse, postToolUse |
| Rovo Dev | `acli` | `~/.rovodev/config.yml` | `acli rovodev run --restore ` | none |
-| Copilot | `copilot` | `~/.copilot/hooks/cmux.json` | `copilot --resume ` | permissionRequest |
+| Copilot | `copilot` | `~/.copilot/hooks/cmux.json` | `copilot --resume ` | preToolUse |
| CodeBuddy | `codebuddy` | `~/.codebuddy/settings.json` | `codebuddy --resume ` | PreToolUse |
| Factory | `droid` | `~/.factory/settings.json` | `droid --resume ` | PreToolUse |
| Qoder | `qodercli` | `~/.qoder/settings.json` | `qodercli --resume ` | PreToolUse |
diff --git a/docs/feed.md b/docs/feed.md
index 133c6c168413..f3e2a9f602b7 100644
--- a/docs/feed.md
+++ b/docs/feed.md
@@ -80,7 +80,7 @@ Installs supported agent hooks whose binaries are on `PATH`. See [Agent hook int
| OpenCode | `~/.config/opencode/plugins/cmux-feed.js` | plugin event bus |
| Cursor CLI | `~/.cursor/hooks.json` | beforeShellExecution |
| Gemini | `~/.gemini/settings.json` | PreToolUse |
-| Copilot | `~/.copilot/hooks/cmux.json` | permissionRequest |
+| Copilot | `~/.copilot/hooks/cmux.json` | preToolUse |
| CodeBuddy | `~/.codebuddy/settings.json` | PreToolUse |
| Factory | `~/.factory/settings.json` | PreToolUse |
| Qoder | `~/.qoder/settings.json` | PreToolUse |
From 4e89e4ca355b24a1f657c369ad586ce65051c033 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 08:05:26 -0700
Subject: [PATCH 32/39] fix: fail closed for Copilot hook wrapper
---
.github/swift-file-length-budget.tsv | 4 ++--
CLI/CMUXCLI+AgentHookDefinitions.swift | 25 +++++++++++++++++++++++++
cmuxTests/CLICopilotHookFeedTests.swift | 24 ++++++++++++++++++++++--
3 files changed, 49 insertions(+), 4 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index a48ff663add4..d588b288b726 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -123,12 +123,12 @@
754 Sources/TerminalController+ControlWorkspaceContext.swift
752 cmuxUITests/CloseWorkspaceCmdDUITests.swift
738 Packages/macOS/CMUXProjectModel/Sources/CMUXProjectModel/XcodeProjectAdapter.swift
+735 CLI/CMUXCLI+AgentHookDefinitions.swift
726 cmuxTests/CLICodexHookTimeoutRegressionTests.swift
722 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift
718 Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
716 Sources/TaskManagerSnapshot.swift
715 Sources/AppleScriptSupport.swift
-710 CLI/CMUXCLI+AgentHookDefinitions.swift
710 Sources/TerminalSSHSessionDetector.swift
706 CLI/CMUXCLI+Config.swift
705 Sources/Panels/BrowserPopupWindowController.swift
@@ -152,11 +152,11 @@
655 Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+RuntimeLifecycle.swift
653 Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Import/Detection/BrowserInstalledBrowserDetector.swift
650 Sources/Panels/MarkdownRemoteImageLoader.swift
+645 cmuxTests/CLICopilotHookFeedTests.swift
641 cmuxTests/CommandPaletteNucleoFFITests.swift
636 Sources/TerminalController+ControlPaneContext.swift
630 Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutWhenClause.swift
629 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift
-625 cmuxTests/CLICopilotHookFeedTests.swift
621 cmuxUITests/RightSidebarChromeHeightUITests.swift
620 cmuxTests/FinderFileDropRegressionTests.swift
620 cmuxTests/TerminalNotificationQueueTests.swift
diff --git a/CLI/CMUXCLI+AgentHookDefinitions.swift b/CLI/CMUXCLI+AgentHookDefinitions.swift
index 8489099595fc..79e9e19dc356 100644
--- a/CLI/CMUXCLI+AgentHookDefinitions.swift
+++ b/CLI/CMUXCLI+AgentHookDefinitions.swift
@@ -383,6 +383,9 @@ extension CMUXCLI {
static func feedHookCommandString(for def: AgentHookDef, agentEvent: String) -> String {
let command = "cmux hooks feed --source \(def.name) --event \(agentEvent)"
+ if def.name == "copilot", agentEvent == "preToolUse" {
+ return copilotPreToolUseAgentHookShellCommand(command, for: def)
+ }
switch def.format {
case .kiroAgentJSON:
return exitTwoPropagatingAgentHookShellCommand(
@@ -403,6 +406,28 @@ extension CMUXCLI {
return "cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"; if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi; if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$\(def.disableEnvVar)\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then { if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments); else \"$cmux_cli\" \(routedArguments); fi; } || echo '{}'; else echo '{}'; fi"
}
+ private static func copilotPreToolUseAgentHookShellCommand(_ command: String, for def: AgentHookDef) -> String {
+ let routedArguments = command.hasPrefix("cmux ") ? String(command.dropFirst("cmux ".count)) : command
+ let denyOutput = shellSingleQuote(copilotPreToolUseDenyHookOutput())
+ return "cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"; if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi; if [ -z \"$CMUX_SURFACE_ID\" ] || [ \"$\(def.disableEnvVar)\" = \"1\" ]; then echo '{}'; elif [ -z \"$cmux_cli\" ]; then echo \(denyOutput); else { if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments); else \"$cmux_cli\" \(routedArguments); fi; } || echo \(denyOutput); fi"
+ }
+
+ private static func copilotPreToolUseDenyHookOutput() -> String {
+ let reason = String(
+ localized: "cli.hooks.feed.permissionDeniedReason",
+ defaultValue: "User denied permission via cmux Feed."
+ )
+ let payload = [
+ "permissionDecision": "deny",
+ "permissionDecisionReason": reason,
+ ]
+ guard let data = try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]),
+ let output = String(data: data, encoding: .utf8) else {
+ return #"{"permissionDecision":"deny","permissionDecisionReason":"User denied permission via cmux Feed."}"#
+ }
+ return output
+ }
+
private static func exitTwoPropagatingAgentHookShellCommand(_ command: String, for def: AgentHookDef) -> String {
let routedArguments = command.hasPrefix("cmux ") ? String(command.dropFirst("cmux ".count)) : command
return "cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"; if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi; if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$\(def.disableEnvVar)\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments); else \"$cmux_cli\" \(routedArguments); fi; status=$?; if [ \"$status\" -eq 2 ]; then exit 2; fi; if [ \"$status\" -ne 0 ]; then echo '{}'; fi; else echo '{}'; fi"
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index f7d26256d0e4..354e6cbd425b 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -120,8 +120,8 @@ struct CLICopilotHookFeedTests {
!preservedPreToolUse.contains { ($0["bash"] as? String)?.contains("--telemetry-only") == true },
"Expected stale cmux telemetry-only preToolUse hook to be removed, saw \(preservedPreToolUse)"
)
- #expect(
- preservedPreToolUse.contains {
+ let cmuxPreToolUseEntry = try #require(
+ preservedPreToolUse.first {
($0["bash"] as? String)?.contains("hooks feed --source copilot --event preToolUse") == true
&& ($0["bash"] as? String)?.contains("--telemetry-only") == false
&& ($0["type"] as? String) == "command"
@@ -131,6 +131,26 @@ struct CLICopilotHookFeedTests {
},
"Expected direct preToolUse Feed hook with timeout slack, saw \(preservedPreToolUse)"
)
+ let cmuxPreToolUseCommand = try #require(cmuxPreToolUseEntry["bash"] as? String)
+ let unavailableCmuxRun = Self.runProcess(
+ executablePath: "/bin/sh",
+ arguments: ["-c", cmuxPreToolUseCommand],
+ environment: [
+ "HOME": root.path,
+ "PATH": root.path,
+ "CMUX_BUNDLED_CLI_PATH": root.appendingPathComponent("missing-cmux").path,
+ "CMUX_SURFACE_ID": "surface:copilot-feed",
+ "CMUX_COPILOT_HOOKS_DISABLED": "0",
+ "CMUX_CLI_SENTRY_DISABLED": "1",
+ ],
+ standardInput: #"{"sessionId":"copilot-session-123","toolName":"bash","toolArgs":{"command":"touch README.md"}}"#,
+ timeout: 5
+ )
+ #expect(!unavailableCmuxRun.timedOut, Comment(rawValue: unavailableCmuxRun.stderr))
+ #expect(unavailableCmuxRun.status == 0, Comment(rawValue: unavailableCmuxRun.stderr))
+ let unavailableCmuxOutput = try #require(Self.jsonObject(unavailableCmuxRun.stdout))
+ #expect(unavailableCmuxOutput["permissionDecision"] as? String == "deny")
+ #expect(unavailableCmuxOutput["permissionDecisionReason"] as? String == "User denied permission via cmux Feed.")
let errorOccurred = try #require(hooks["errorOccurred"] as? [[String: Any]])
#expect(
errorOccurred.contains {
From 1c12804507ea22a9c1904d7c511d4f494cc00586 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 08:14:04 -0700
Subject: [PATCH 33/39] fix: deny Copilot when Feed socket is unavailable
---
.github/swift-file-length-budget.tsv | 4 +--
CLI/cmux.swift | 6 ++++
cmuxTests/CLICopilotHookFeedTests.swift | 37 +++++++++++++++++++++++++
3 files changed, 45 insertions(+), 2 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index ed9646385c24..bd0cf2980a74 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -1,7 +1,7 @@
# cmux-owned Swift file length budget.
# Format: max_linesrelative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
-34392 CLI/cmux.swift
+34398 CLI/cmux.swift
17827 Sources/AppDelegate.swift
16112 Sources/ContentView.swift
13952 Sources/TerminalController.swift
@@ -140,6 +140,7 @@
688 cmuxTests/KeyboardShortcutContextTests.swift
683 Packages/macOS/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift
683 Sources/Panels/CodexAppServerSession.swift
+682 cmuxTests/CLICopilotHookFeedTests.swift
681 Sources/Panels/AgentSessionProcessStore.swift
680 Sources/FileExplorerSearchController.swift
677 Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+Bootstrap.swift
@@ -152,7 +153,6 @@
655 Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+RuntimeLifecycle.swift
653 Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Import/Detection/BrowserInstalledBrowserDetector.swift
650 Sources/Panels/MarkdownRemoteImageLoader.swift
-645 cmuxTests/CLICopilotHookFeedTests.swift
641 cmuxTests/CommandPaletteNucleoFFITests.swift
636 Sources/TerminalController+ControlPaneContext.swift
630 Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutWhenClause.swift
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index 94f5dee7558e..84b92561ee9a 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -33061,12 +33061,18 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
)
} catch {
feedClient.close()
+ if emitCopilotPreToolUseDenyIfNeeded() {
+ return
+ }
print("{}")
return
}
ownedClient = feedClient
activeClient = feedClient
} else {
+ if emitCopilotPreToolUseDenyIfNeeded() {
+ return
+ }
print("{}")
return
}
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index 354e6cbd425b..0b357700a735 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -249,6 +249,35 @@ struct CLICopilotHookFeedTests {
return (result, try #require(feedEvents.first))
}
+ func runCopilotDecisionWithUnavailableSocket() throws -> ProcessRunResult {
+ let cliPath = try Self.bundledCLIPath()
+ let root = FileManager.default.temporaryDirectory
+ .appendingPathComponent("cmux-copilot-feed-unavailable-\(UUID().uuidString)", isDirectory: true)
+ let workspaceId = "33333333-3333-3333-3333-333333333333"
+ let surfaceId = "44444444-4444-4444-4444-444444444444"
+
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
+ defer { try? FileManager.default.removeItem(at: root) }
+
+ return Self.runProcess(
+ executablePath: cliPath,
+ arguments: ["hooks", "feed", "--source", "copilot", "--event", "preToolUse"],
+ environment: [
+ "HOME": root.path,
+ "PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
+ "PWD": root.path,
+ "CMUX_SOCKET_PATH": root.appendingPathComponent("missing.sock").path,
+ "CMUX_WORKSPACE_ID": workspaceId,
+ "CMUX_SURFACE_ID": surfaceId,
+ "CMUX_COPILOT_PID": "525252",
+ "CMUX_AGENT_HOOK_STATE_DIR": root.path,
+ "CMUX_CLI_SENTRY_DISABLED": "1",
+ ],
+ standardInput: #"{"sessionId":"copilot-session-123","cwd":"\#(root.path)","toolName":"bash","toolArgs":{"command":"touch \#(root.appendingPathComponent("README.md").path)"}}"#,
+ timeout: 5
+ )
+ }
+
// Fresh installs gate after Copilot's native permission service through
// preToolUse. Keep permissionRequest denial support for already-installed
// hook files and manual invocations, but approvals fall through there.
@@ -286,6 +315,14 @@ struct CLICopilotHookFeedTests {
#expect(timeoutOutput["permissionDecisionReason"] as? String == "User denied permission via cmux Feed.")
#expect(timeoutOutput["hookSpecificOutput"] == nil)
+ let unavailable = try runCopilotDecisionWithUnavailableSocket()
+ #expect(!unavailable.timedOut, Comment(rawValue: unavailable.stderr))
+ #expect(unavailable.status == 0, Comment(rawValue: unavailable.stderr))
+ let unavailableOutput = try #require(Self.jsonObject(unavailable.stdout))
+ #expect(unavailableOutput["permissionDecision"] as? String == "deny")
+ #expect(unavailableOutput["permissionDecisionReason"] as? String == "User denied permission via cmux Feed.")
+ #expect(unavailableOutput["hookSpecificOutput"] == nil)
+
let (permissionAllow, permissionAllowEvent) = try runCopilotDecision(mode: "once", event: "permissionRequest")
#expect(!permissionAllow.timedOut, Comment(rawValue: permissionAllow.stderr))
#expect(permissionAllow.status == 0, Comment(rawValue: permissionAllow.stderr))
From 663f1b6775609964e5e9435b2fc91cae04440e8d Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 09:07:16 -0700
Subject: [PATCH 34/39] test: stabilize SSH CLI output capture
---
...otifyProcessIntegrationRegressionTests.swift | 10 ++++------
cmuxTests/WorkspaceRemoteConnectionTests.swift | 17 ++++++++---------
2 files changed, 12 insertions(+), 15 deletions(-)
diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
index 9651e0f5be0e..d97c8f05941f 100644
--- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
+++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
@@ -3518,12 +3518,10 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
initialScript.contains("required terminal context missing for SSH PTY attach"),
initialScript
)
- XCTAssertTrue(
- initialScript.contains("ssh-$cmux_ssh_pty_workspace_id-$cmux_ssh_pty_surface_id"),
- initialScript
- )
+ XCTAssertTrue(initialScript.contains("ssh-$cmux_ssh_pty_workspace_id-$cmux_ssh_pty_surface_id"), initialScript)
+ let attachIDFlag = "--attachment-id \"$cmux_ssh_pty_surface_id\""
XCTAssertTrue(initialScript.contains("254|255"), initialScript)
- XCTAssertFalse(initialScript.contains("-surface"), initialScript)
+ XCTAssertTrue(initialScript.split(separator: "\n").first { $0.contains("ssh-pty-attach") }.map { $0.contains(attachIDFlag) && !$0.contains("--surface") } == true, initialScript)
XCTAssertTrue(
initialScript.contains("--workspace \"$cmux_ssh_pty_workspace_id\""),
initialScript
@@ -3550,7 +3548,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
terminalStartupScript
)
XCTAssertTrue(terminalStartupScript.contains("254|255"), terminalStartupScript)
- XCTAssertFalse(terminalStartupScript.contains("-surface"), terminalStartupScript)
+ XCTAssertTrue(terminalStartupScript.split(separator: "\n").first { $0.contains("ssh-pty-attach") }.map { $0.contains(attachIDFlag) && !$0.contains("--surface") } == true, terminalStartupScript)
XCTAssertTrue(
terminalStartupScript.contains("--workspace \"$cmux_ssh_pty_workspace_id\""),
terminalStartupScript
diff --git a/cmuxTests/WorkspaceRemoteConnectionTests.swift b/cmuxTests/WorkspaceRemoteConnectionTests.swift
index 5ca9fa61f08f..a0c3cc1d5322 100644
--- a/cmuxTests/WorkspaceRemoteConnectionTests.swift
+++ b/cmuxTests/WorkspaceRemoteConnectionTests.swift
@@ -3885,25 +3885,23 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
timeout: TimeInterval
) -> ProcessRunResult {
let process = Process()
- let stdoutPipe = Pipe()
+ let stdoutURL = FileManager.default.temporaryDirectory.appendingPathComponent("cmux-cli-stdout-\(UUID().uuidString)")
+ FileManager.default.createFile(atPath: stdoutURL.path, contents: nil)
+ guard let stdoutHandle = try? FileHandle(forWritingTo: stdoutURL) else { return ProcessRunResult(status: -1, stdout: "", stderr: "failed to create process output capture file", timedOut: false) }
+ defer { try? stdoutHandle.close(); try? FileManager.default.removeItem(at: stdoutURL) }
let stderrPipe = Pipe()
let stdinPipe = standardInput == nil ? nil : Pipe()
process.executableURL = URL(fileURLWithPath: executablePath)
process.arguments = arguments
process.environment = environment
process.standardInput = stdinPipe ?? FileHandle.nullDevice
- process.standardOutput = stdoutPipe
+ process.standardOutput = stdoutHandle
process.standardError = stderrPipe
do {
try process.run()
} catch {
- return ProcessRunResult(
- status: -1,
- stdout: "",
- stderr: String(describing: error),
- timedOut: false
- )
+ return ProcessRunResult(status: -1, stdout: "", stderr: String(describing: error), timedOut: false)
}
if let standardInput, let stdinPipe {
stdinPipe.fileHandleForWriting.write(Data(standardInput.utf8))
@@ -3921,8 +3919,9 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
process.terminate()
_ = exitSignal.wait(timeout: .now() + 1)
}
+ try? stdoutHandle.close()
- let stdout = String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
+ let stdout = (try? String(contentsOf: stdoutURL, encoding: .utf8)) ?? ""
let stderr = String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
return ProcessRunResult(
status: process.terminationStatus,
From fb3e64b1fd9dff607d67a303860b3f1e36745eb3 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 09:10:07 -0700
Subject: [PATCH 35/39] fix: scope Copilot preToolUse hook matcher
---
CLI/FeedEventClassifier.swift | 3 +++
CLI/cmux.swift | 13 ++++++-------
cmuxTests/CLICopilotHookFeedTests.swift | 2 +-
3 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/CLI/FeedEventClassifier.swift b/CLI/FeedEventClassifier.swift
index 888947d8226b..f0fc666faff2 100644
--- a/CLI/FeedEventClassifier.swift
+++ b/CLI/FeedEventClassifier.swift
@@ -328,6 +328,9 @@ struct FeedEventClassifier {
"apply_patch",
]
+ static let copilotPreToolUseApprovalToolMatcher =
+ copilotSideEffectingToolAliases.union(["ask_user"]).sorted().joined(separator: "|")
+
/// Whether a tool mutates state and deserves an approval prompt. Exact
/// match against ``sideEffectingTools`` for every source; `copilot` and
/// `kiro` additionally match their case-insensitive internal aliases.
diff --git a/CLI/cmux.swift b/CLI/cmux.swift
index 84b92561ee9a..52678d80a917 100644
--- a/CLI/cmux.swift
+++ b/CLI/cmux.swift
@@ -27016,7 +27016,8 @@ struct CMUXCLI {
let timeoutSeconds = Self.timeoutSecondsFromMilliseconds(feedTimeoutMs)
entries.append(Self.copilotHookEntry(
command: feedCmd,
- timeoutSeconds: timeoutSeconds + (feedTimeoutMs >= 120_000 ? 5 : 0)
+ timeoutSeconds: timeoutSeconds + (feedTimeoutMs >= 120_000 ? 5 : 0),
+ matcher: agentEvent == "preToolUse" ? FeedEventClassifier.copilotPreToolUseApprovalToolMatcher : nil
))
result[agentEvent] = entries
case .antigravityJSON:
@@ -27056,12 +27057,10 @@ struct CMUXCLI {
return ((positiveTimeoutMs - 1) / 1000) + 1
}
- private static func copilotHookEntry(command: String, timeoutSeconds: Int) -> [String: Any] {
- [
- "type": "command",
- "bash": command,
- "timeoutSec": max(timeoutSeconds, 1),
- ]
+ private static func copilotHookEntry(command: String, timeoutSeconds: Int, matcher: String? = nil) -> [String: Any] {
+ var entry: [String: Any] = ["type": "command", "bash": command, "timeoutSec": max(timeoutSeconds, 1)]
+ if let matcher { entry["matcher"] = matcher }
+ return entry
}
private static func antigravityHookEntry(
diff --git a/cmuxTests/CLICopilotHookFeedTests.swift b/cmuxTests/CLICopilotHookFeedTests.swift
index 0b357700a735..49c93eab5439 100644
--- a/cmuxTests/CLICopilotHookFeedTests.swift
+++ b/cmuxTests/CLICopilotHookFeedTests.swift
@@ -125,7 +125,7 @@ struct CLICopilotHookFeedTests {
($0["bash"] as? String)?.contains("hooks feed --source copilot --event preToolUse") == true
&& ($0["bash"] as? String)?.contains("--telemetry-only") == false
&& ($0["type"] as? String) == "command"
- && ($0["timeoutSec"] as? Int) == 125
+ && ($0["timeoutSec"] as? Int) == 125 && ($0["matcher"] as? String) == FeedEventClassifier.copilotPreToolUseApprovalToolMatcher
&& $0["command"] == nil
&& $0["hooks"] == nil
},
From b612558cb244307bd9ed5e432bc8e0dfb093dce5 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 09:24:32 -0700
Subject: [PATCH 36/39] test: migrate touched legacy suites to Swift Testing
---
.github/swift-file-length-budget.tsv | 4 +-
...ifyProcessIntegrationRegressionTests.swift | 255 +++++++++++++++-
.../WorkspaceRemoteConnectionTests.swift | 274 +++++++++++++++++-
3 files changed, 526 insertions(+), 7 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index d3006603ee16..24db48c94d9e 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -9,11 +9,11 @@
12240 Sources/GhosttyTerminalView.swift
12144 cmuxTests/AppDelegateShortcutRoutingTests.swift
11929 Sources/Panels/BrowserPanel.swift
-9333 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
+9584 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
8017 CLI/cmux_open.swift
7986 Sources/Panels/BrowserPanelView.swift
+7485 cmuxTests/WorkspaceRemoteConnectionTests.swift
7354 cmuxTests/WorkspaceUnitTests.swift
-7217 cmuxTests/WorkspaceRemoteConnectionTests.swift
6317 cmuxTests/SessionPersistenceTests.swift
6217 cmuxTests/GhosttyConfigTests.swift
6178 Sources/TabManager.swift
diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
index d97c8f05941f..d9a8b28d5ab2 100644
--- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
+++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
@@ -1,12 +1,131 @@
-import XCTest
import Darwin
+import Foundation
+import Testing
#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif
-final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
+private typealias XCTestExpectation = DispatchSemaphore
+
+private extension DispatchSemaphore {
+ func fulfill() {
+ signal()
+ }
+}
+
+private func legacyTimeout(_ timeout: TimeInterval) -> DispatchTime {
+ .now() + .milliseconds(Int((timeout * 1_000).rounded(.up)))
+}
+
+private func legacyFailure(_ message: String) {
+ Issue.record(Comment(rawValue: message.isEmpty ? "Assertion failed" : message))
+}
+
+private func expectation(description _: String) -> XCTestExpectation {
+ DispatchSemaphore(value: 0)
+}
+
+private func wait(for expectations: [XCTestExpectation], timeout: TimeInterval) {
+ for expectation in expectations {
+ if expectation.wait(timeout: legacyTimeout(timeout)) == .timedOut {
+ legacyFailure("Timed out waiting for expectation")
+ }
+ }
+}
+
+private func XCTFail(_ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ legacyFailure(message())
+}
+
+private func XCTAssertTrue(_ expression: @autoclosure () throws -> Bool, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ if try !expression() {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertFalse(_ expression: @autoclosure () throws -> Bool, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ if try expression() {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertEqual(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs != rhs {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertNotEqual(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs == rhs {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertNil(_ expression: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ if try expression() != nil {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertNotNil(_ expression: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ if try expression() == nil {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTUnwrap(_ expression: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) throws -> T {
+ guard let value = try expression() else {
+ legacyFailure(message())
+ throw NSError(domain: "CLINotifyProcessIntegrationRegressionTests", code: 1)
+ }
+ return value
+}
+
+@Suite(.serialized)
+final class CLINotifyProcessIntegrationRegressionTests {
+ private var teardownBlocks: [() -> Void] = []
+
+ deinit {
+ for block in teardownBlocks.reversed() {
+ block()
+ }
+ }
+
+ private func addTeardownBlock(_ block: @escaping () -> Void) {
+ teardownBlocks.append(block)
+ }
+
+ @Test
func testClaudeClearSessionStartMarksWorkspaceRunning() throws {
let context = try makeClaudeHookContext(name: "claude-clear-running")
defer { context.cleanup() }
@@ -33,6 +152,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudeSessionStartRecordIsNotRestorableUntilPrompt() throws {
let context = try makeClaudeHookContext(name: "claude-session-restorable")
defer { context.cleanup() }
@@ -73,6 +193,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudePreToolUseFeedContextReadsOnlyRecentTranscriptTail() throws {
let context = try makeClaudeHookContext(name: "claude-pretool-tail")
defer { context.cleanup() }
@@ -112,6 +233,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertFalse(String(describing: feedContext).contains("ancient"), "\(feedContext)")
}
+ @Test
func testClaudePreToolUseFeedContextKeepsOversizedFinalTranscriptLine() throws {
let context = try makeClaudeHookContext(name: "claude-pretool-oversized-final")
defer { context.cleanup() }
@@ -144,6 +266,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(assistantPreamble.hasPrefix("recent assistant response"), "\(feedContext)")
}
+ @Test
func testCodexStopReadsOversizedFinalTranscriptLine() throws {
let context = try makeClaudeHookContext(name: "codex-oversized-final-transcript")
defer { context.cleanup() }
@@ -178,6 +301,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexPromptSubmitDoesNotRefreshTerminalLastTurnDiffBaseline() throws {
let context = try makeClaudeHookContext(name: "codex-prompt-baseline")
defer { context.cleanup() }
@@ -324,6 +448,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(refreshedBaseCommit, promptCommit)
}
+ @Test
func testClaudeStopFromPreviousSessionDoesNotClobberClearRunningStatus() throws {
let context = try makeClaudeHookContext(name: "claude-clear-stale-stop")
defer { context.cleanup() }
@@ -385,6 +510,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(resumeBindingRequests.first?["auto_resume"] as? Bool, true)
}
+ @Test
func testClaudePromptSubmitFromNewSessionCanReplaceStoppedSession() throws {
let context = try makeClaudeHookContext(name: "claude-new-session-after-stop")
defer { context.cleanup() }
@@ -682,6 +808,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
return result
}
+ @Test
func testClaudeForkSessionStartKeepsParentSessionBoundToOriginalSurface() throws {
let context = try makeClaudeHookContext(name: "claude-fork-session-start")
defer { context.cleanup() }
@@ -714,6 +841,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudeForkSessionStartWithoutSurfaceIdentityDoesNotRegisterPIDOnFallbackPane() throws {
let context = try makeClaudeHookContext(name: "claude-fork-no-surface")
defer { context.cleanup() }
@@ -751,6 +879,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudeForkSessionStartRecognizesEqualsFlagForm() throws {
let context = try makeClaudeHookContext(name: "claude-fork-equals-form")
defer { context.cleanup() }
@@ -788,6 +917,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudeLegacyStoreBackfillsPaneBoundaryFromWorkspaceActiveSlot() throws {
let context = try makeClaudeHookContext(name: "claude-legacy-backfill")
defer { context.cleanup() }
@@ -869,6 +999,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudeForkedSessionPromptSubmitRecordsWhileParentTurnActive() throws {
let context = try makeClaudeHookContext(name: "claude-fork-prompt-submit")
defer { context.cleanup() }
@@ -921,6 +1052,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(request["surface_id"] as? String, context.surfaceId)
}
+ @Test
func testClaudeForkSessionEndBeforeFirstPromptDoesNotConsumeParentSession() throws {
let context = try makeClaudeHookContext(name: "claude-fork-session-end")
defer { context.cleanup() }
@@ -972,6 +1104,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudeForkedSessionPromptSubmitRecordsWithSurfaceRefForm() throws {
let context = try makeClaudeHookContext(name: "claude-fork-surface-ref")
defer { context.cleanup() }
@@ -1008,6 +1141,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudeStaleStopFromClosedPaneStaysStaleWhenSurfaceResolutionFallsBack() throws {
let context = try makeClaudeHookContext(name: "claude-stale-stop-fallback")
defer { context.cleanup() }
@@ -1079,6 +1213,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudeStaleStopStaysStaleAfterAnotherPaneBecomesWorkspaceActive() throws {
let context = try makeClaudeHookContext(name: "claude-stale-stop-multi-pane")
defer { context.cleanup() }
@@ -1143,6 +1278,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudeNewSessionReplacesStoppedSessionInPaneAfterAnotherPaneBecameActive() throws {
let context = try makeClaudeHookContext(name: "claude-replace-multi-pane")
defer { context.cleanup() }
@@ -1202,6 +1338,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudeStaleTurnSessionEndDoesNotConsumeSessionAfterAnotherPaneBecameActive() throws {
let context = try makeClaudeHookContext(name: "claude-stale-end-multi-pane")
defer { context.cleanup() }
@@ -1255,6 +1392,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudeParentPaneStopAppliesAfterForkedSessionPromoted() throws {
let context = try makeClaudeHookContext(name: "claude-fork-parent-stop")
defer { context.cleanup() }
@@ -1290,6 +1428,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudePromptSubmitResumeBindingPersistsSafeAuthSelectionValues() throws {
let context = try makeClaudeHookContext(name: "claude-resume-env-redaction")
defer { context.cleanup() }
@@ -1360,6 +1499,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudeSessionEndChecksConsumedWorkspaceBeforeClearingVisibleState() throws {
let context = try makeClaudeHookContext(name: "claude-stale-session-end-workspace")
defer { context.cleanup() }
@@ -1429,6 +1569,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClaudeSessionEndDoesNotConsumeSameSessionStaleTurn() throws {
let context = try makeClaudeHookContext(name: "claude-stale-session-end-turn")
defer { context.cleanup() }
@@ -1487,6 +1628,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(active["turnId"] as? String, "turn-2")
}
+ @Test
func testClaudeSessionEndClearsMatchingSurfaceResumeBinding() throws {
let context = try makeClaudeHookContext(name: "claude-session-end-resume-clear")
defer { context.cleanup() }
@@ -1532,6 +1674,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(request["source"] as? String, "agent-hook")
}
+ @Test
func testNestedCodexPromptAndStopDoNotReplaceParentResumeBinding() throws {
let context = try makeClaudeHookContext(name: "codex-nested-resume-guard")
defer { context.cleanup() }
@@ -1619,6 +1762,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testGenericAgentNotificationUpdatesLifecycleForNeedsInput() throws {
let context = try makeClaudeHookContext(name: "codex-notification-lifecycle")
defer { context.cleanup() }
@@ -1676,6 +1820,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(record["agentLifecycle"] as? String, "needsInput")
}
+ @Test
func testGenericAgentStaleIdleStopDoesNotOverwriteNewerRunningLifecycle() throws {
let context = try makeClaudeHookContext(name: "codex-stale-idle-stop-lifecycle")
defer { context.cleanup() }
@@ -1736,6 +1881,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(newRecord["agentLifecycle"] as? String, "running")
}
+ @Test
func testGenericAgentStaleIdleNotificationDoesNotOverwriteNewerRunningLifecycle() throws {
let context = try makeClaudeHookContext(name: "codex-stale-idle-notification-lifecycle")
defer { context.cleanup() }
@@ -1796,6 +1942,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(newRecord["agentLifecycle"] as? String, "running")
}
+ @Test
func testCodexLegacyStopWithoutTurnIdPopsStoredTurnStack() throws {
let context = try makeClaudeHookContext(name: "codex-legacy-stop-turn-stack")
defer { context.cleanup() }
@@ -1857,6 +2004,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testGenericAgentTurnIdsStayNestedAcrossTurnChanges() throws {
let context = try makeClaudeHookContext(name: "generic-turn-stack")
defer { context.cleanup() }
@@ -1932,6 +2080,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexTurnStackPreservesAnonymousDepthBetweenKnownTurns() throws {
let context = try makeClaudeHookContext(name: "codex-mixed-anonymous-depth")
defer { context.cleanup() }
@@ -2037,6 +2186,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexStopWithNewTurnIdPopsAnonymousDepthUnderKnownParent() throws {
let context = try makeClaudeHookContext(name: "codex-anonymous-depth-turn-stop")
defer { context.cleanup() }
@@ -2098,6 +2248,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexStopAfterInterruptedPriorTurnStillNotifies() throws {
let context = try makeClaudeHookContext(name: "codex-interrupted-turn-depth")
defer { context.cleanup() }
@@ -2151,6 +2302,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexStopPrunesLateTerminalPriorTurnBeforeCurrentStop() throws {
let context = try makeClaudeHookContext(name: "codex-late-terminal-prior-turn")
defer { context.cleanup() }
@@ -2217,6 +2369,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexTerminalNestedChildDoesNotClearParentTurnStack() throws {
let context = try makeClaudeHookContext(name: "codex-terminal-child-stack")
defer { context.cleanup() }
@@ -2314,6 +2467,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexTerminalInterruptedStackClearsBeforeCurrentPrompt() throws {
let context = try makeClaudeHookContext(name: "codex-terminal-stack-reset")
defer { context.cleanup() }
@@ -2378,6 +2532,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexDepthOnlyLegacyNestingRemainsNestedWhenTurnIdsAppear() throws {
let context = try makeClaudeHookContext(name: "codex-depth-only-nested")
defer { context.cleanup() }
@@ -2441,6 +2596,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexDepthOnlyHistoricalTerminalDoesNotResetActiveParent() throws {
let context = try makeClaudeHookContext(name: "codex-depth-only-history")
defer { context.cleanup() }
@@ -2512,6 +2668,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexDepthOnlyTurnContextOnlyParentDoesNotResetActiveParent() throws {
let context = try makeClaudeHookContext(name: "codex-depth-only-context-parent")
defer { context.cleanup() }
@@ -2582,6 +2739,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexDepthOnlyParentStopNotifiesAfterChildTurnStops() throws {
let context = try makeClaudeHookContext(name: "codex-depth-only-parent-stop")
defer { context.cleanup() }
@@ -2654,6 +2812,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexDepthOnlySiblingStaysNestedAfterChildTerminalTranscript() throws {
let context = try makeClaudeHookContext(name: "codex-depth-only-child-terminal")
defer { context.cleanup() }
@@ -2741,6 +2900,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexDepthOnlyTerminalHistoryDoesNotResetUnknownActiveDepth() throws {
let context = try makeClaudeHookContext(name: "codex-depth-only-terminal-history")
defer { context.cleanup() }
@@ -2812,6 +2972,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexStopFromStaleOlderTurnDoesNotNotifyWhileNewerTurnIsActive() throws {
let context = try makeClaudeHookContext(name: "codex-stale-turn-stop")
defer { context.cleanup() }
@@ -2877,6 +3038,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexLateStopFromOlderTurnDoesNotNotifyAfterNewerTurnCompleted() throws {
let context = try makeClaudeHookContext(name: "codex-late-stale-turn-stop")
defer { context.cleanup() }
@@ -2941,6 +3103,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexStopWithMissedPromptSubmitClearsTerminalStaleTurn() throws {
let context = try makeClaudeHookContext(name: "codex-missed-prompt-stale-turn")
defer { context.cleanup() }
@@ -2985,6 +3148,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexStopWithMissedPromptSubmitClearsFullyTerminalStack() throws {
let context = try makeClaudeHookContext(name: "codex-missed-prompt-terminal-stack")
defer { context.cleanup() }
@@ -3048,6 +3212,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexStopWithUnseenTurnIdNotSuppressedAtIdleDepth() throws {
let context = try makeClaudeHookContext(name: "codex-unseen-turn-stop")
defer { context.cleanup() }
@@ -3095,6 +3260,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testManagedCodexSubagentStopDoesNotReplaceResumeBinding() throws {
let context = try makeClaudeHookContext(name: "codex-managed-resume-guard")
defer { context.cleanup() }
@@ -3130,6 +3296,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexStopIgnoresStaleSubagentRelayFromCompletedTurnWithoutTurnId() throws {
let context = try makeClaudeHookContext(name: "codex-stale-relay")
defer { context.cleanup() }
@@ -3171,6 +3338,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testManagedCodexSubagentSessionEndDoesNotClearParentResumeBinding() throws {
let context = try makeClaudeHookContext(name: "codex-managed-end-resume-guard")
defer { context.cleanup() }
@@ -3223,6 +3391,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertNotNil(savedSessions[sessionId], "Suppressed SessionEnd should leave the stored parent session intact")
}
+ @Test
func testRightSidebarCLIForwardsV1SocketCommandsQuietly() throws {
let cliPath = try bundledCLIPath()
let cases: [(name: String, arguments: [String], expectedCommand: String, response: String, stdout: String)] = [
@@ -3276,6 +3445,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
}
}
+ @Test
func testRightSidebarInvalidCommandValidatesBeforeTargetResolution() throws {
let cliPath = try bundledCLIPath()
let missingSocketPath = "/tmp/cmux-test-missing-\(UUID().uuidString).sock"
@@ -3297,6 +3467,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertFalse(result.stderr.contains("Socket"), result.stderr)
}
+ @Test
func testRightSidebarInvalidSetModeValidatesBeforeTargetResolution() throws {
let cliPath = try bundledCLIPath()
let missingSocketPath = "/tmp/cmux-test-missing-\(UUID().uuidString).sock"
@@ -3318,16 +3489,19 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertFalse(result.stderr.contains("Socket"), result.stderr)
}
+ @Test
func testSSHPersistentPTYUsesReusableForegroundAuthControlConnection() throws {
let run = try runMockedSSH(arguments: [])
try assertSSHPersistentPTYUsesReusableForegroundAuthControlConnection(run: run)
}
+ @Test
func testSSHPersistentPTYTreatsControlPersistZeroAsReusable() throws {
let run = try runMockedSSH(arguments: ["--ssh-option", "ControlPersist=0"])
try assertSSHPersistentPTYUsesReusableForegroundAuthControlConnection(run: run)
}
+ @Test
func testSSHPersistentPTYJSONReportsResolvedSessionID() throws {
let run = try runMockedSSH(arguments: [], jsonOutput: true)
let payload = try jsonPayload(from: run.stdout)
@@ -3341,6 +3515,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertNotNil(UUID(uuidString: String(persistentDaemonSlot.dropFirst(4))))
}
+ @Test
func testSSHPersistentPTYJSONResolvesSessionIDWhenWorkspaceCreateOmitsSurfaceID() throws {
let run = try runMockedSSH(arguments: [], jsonOutput: true, omitWorkspaceCreateSurfaceID: true)
let payload = try jsonPayload(from: run.stdout)
@@ -3352,6 +3527,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertNotNil(UUID(uuidString: String(persistentDaemonSlot.dropFirst(4))))
}
+ @Test
func testSSHForwardAgentFlagPropagatesCallerAgentSocket() throws {
let agentSocketPath = try makeExistingAgentSocketPath()
let run = try runMockedSSH(
@@ -3368,6 +3544,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
}
+ @Test
func testSSHForwardAgentOptionPropagatesCallerAgentSocket() throws {
let agentSocketPath = try makeExistingAgentSocketPath()
let run = try runMockedSSH(
@@ -3384,6 +3561,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
}
+ @Test
func testSSHForwardAgentRepeatedOptionUsesLastValue() throws {
let run = try runMockedSSH(
arguments: [
@@ -3406,6 +3584,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertNil(configureParams["ssh_auth_sock"])
}
+ @Test
func testSSHPreservesCallerAgentSocketForOpenSSHConfigResolution() throws {
let agentSocketPath = try makeExistingAgentSocketPath()
let run = try runMockedSSH(
@@ -3423,6 +3602,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
}
+ @Test
func testSSHForwardAgentLiteralSocketPathPropagatesSocketPath() throws {
let agentSocketPath = try makeExistingAgentSocketPath()
let run = try runMockedSSH(
@@ -3438,6 +3618,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
}
+ @Test
func testSSHForwardAgentTildeSocketPathExpandsSocketPath() throws {
let homeURL = try makeTemporaryDirectory(prefix: "cmux-ssh-home")
let tildeSocketPath = "~/.ssh/cmux-test-agent.sock"
@@ -3459,6 +3640,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(configureParams["ssh_auth_sock"] as? String, expandedSocketURL.path)
}
+ @Test
func testSSHForwardAgentAskDoesNotPropagateInvalidSocketPath() throws {
let run = try runMockedSSH(
arguments: ["--ssh-option", "ForwardAgent=ask"],
@@ -3475,6 +3657,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertNil(configureParams["ssh_auth_sock"])
}
+ @Test
func testSSHNoForwardAgentFlagOverridesConfig() throws {
let agentSocketPath = try makeExistingAgentSocketPath()
let run = try runMockedSSH(
@@ -3566,6 +3749,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertNotNil(UUID(uuidString: String(persistentDaemonSlot.dropFirst(4))))
}
+ @Test
func testSSHPersistentPTYFallsBackWhenForegroundAuthCannotBeReused() throws {
let cases: [(name: String, arguments: [String])] = [
("control-master-no", ["--ssh-option", "ControlMaster=no"]),
@@ -3601,6 +3785,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
}
}
+ @Test
func testSSHPTYAttachBridgeErrorClearsLocalStateBeforeReady() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshpty")
@@ -3693,6 +3878,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_attach_end"])
}
+ @Test
func testSSHPTYAttachBridgeEOFWhileSessionRunsExitsWithoutSSHRetryStatus() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshptyeof")
@@ -3802,6 +3988,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions", "workspace.remote.pty_detach"])
}
+ @Test
func testSSHPTYAttachBridgeEOFWhenSessionGoneClearsLocalState() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshptygone")
@@ -3901,6 +4088,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions", "workspace.remote.pty_attach_end"])
}
+ @Test
func testSSHPTYAttachWithoutSurfaceDoesNotSendLocalAttachEnd() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshptynosurface")
@@ -3990,6 +4178,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions"])
}
+ @Test
func testSSHPTYAttachBridgeResetWhenSessionGoneClearsLocalState() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshptyrst")
@@ -4089,6 +4278,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions", "workspace.remote.pty_attach_end"])
}
+ @Test
func testSSHPTYAttachWaitUsesCurrentTerminalSizeForBridgeHandshake() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshptysize")
@@ -4270,6 +4460,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(capturedHandshake?["rows"] as? Int, 43)
}
+ @Test
func testSSHPTYAttachSendsResizeWithoutBlockingEOFLocalCleanup() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshptyresize")
@@ -4438,6 +4629,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(methods.filter { $0 == "workspace.remote.pty_attach_end" }.count, 1)
}
+ @Test
func testSSHSessionAttachCreatesSurfaceWithPersistedPTYSessionID() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshattach")
@@ -4508,6 +4700,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(state.snapshot().count, 1)
}
+ @Test
func testSSHPTYAttachRequireExistingPassesBridgeFlag() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshreq")
@@ -4599,6 +4792,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_attach_end"])
}
+ @Test
func testSSHPTYAttachRequireExistingSessionNotFoundFailsWithoutWaitRetry() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshreqmissing")
@@ -4683,6 +4877,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_attach_end"])
}
+ @Test
func testSSHSessionListAllWorkspacesReportsQueryErrors() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshlist")
@@ -4743,6 +4938,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(result.stderr.contains("remote connection is not active"), result.stderr)
}
+ @Test
func testSSHSessionCleanupAllReportsPartialFailures() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshclean")
@@ -4834,6 +5030,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertFalse(result.stderr.contains("close failed"), result.stderr)
}
+ @Test
func testSSHSessionCleanupAllWorkspacesReportsListErrors() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshcleanall")
@@ -4913,6 +5110,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(result.stderr.contains("remote connection is not active"), result.stderr)
}
+ @Test
func testSSHSessionCleanupAllWorkspacesAllRejectsMissingWorkspaceID() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshcleanallmissing")
@@ -4987,6 +5185,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(result.stderr.contains("missing workspace_id in SSH PTY session list response"), result.stderr)
}
+ @Test
func testSSHSessionCleanupAllWorkspacesSessionIDRejectsMissingWorkspaceID() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshcleansessionmissing")
@@ -5061,6 +5260,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(result.stderr.contains("missing workspace_id in SSH PTY session list response"), result.stderr)
}
+ @Test
func testSSHSessionCleanupAllWorkspacesSessionIDReportsNotFound() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshcleansessiongone")
@@ -5127,6 +5327,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(result.stderr.contains("persistent SSH PTY session is no longer running"), result.stderr)
}
+ @Test
func testSSHSessionCleanupAllWorkspacesSessionIDCountsDuplicateIDsPerWorkspace() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshcleandup")
@@ -5213,6 +5414,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(Set(closedWorkspaces), Set([workspaceA, workspaceB]))
}
+ @Test
func testRightSidebarCLIResolvesWindowAndWorkspaceHandlesBeforeForwarding() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("rs-target")
@@ -5291,6 +5493,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(state.commands.last, "right_sidebar set find --tab=\(workspaceId) --window=\(windowId)")
}
+ @Test
func testRightSidebarCLIRejectsUnresolvedWorkspaceHandleBeforeForwarding() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("rs-miss")
@@ -5347,6 +5550,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
}
@MainActor
+ @Test
func testNotifyWithUUIDSurfaceDoesNotRequireCallerWorkspaceOrWindow() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("notify-uuid-surface")
@@ -5413,6 +5617,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testNotificationCLIActionsUseSocketAPIAndParseExtendedFields() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("notif-actions")
@@ -5618,6 +5823,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testListNotificationsKeepsOldServerPipeBodiesAsBody() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("notif-old-pipe")
@@ -5662,6 +5868,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(row["tab_title"] is NSNull)
}
+ @Test
func testCodexPromptSubmitRebindsRestoredSessionToCurrentCallerSurface() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex-rebind")
@@ -5771,6 +5978,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testNewPaneWindowFlagScopesWorkspaceIndex() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("pane-window")
@@ -5850,6 +6058,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testFocusPaneWindowFlagRejectsPaneFromOtherWindow() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("pane-other-window")
@@ -5932,6 +6141,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testReorderSurfaceWindowFlagRejectsSurfaceFromOtherWindow() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("surface-other-window")
@@ -6014,6 +6224,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testSendWindowFlagRejectsUnknownWindowRefBeforeMutation() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("send-window-ref")
@@ -6075,6 +6286,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testVMNewWindowFlagValidatesBeforeCreate() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("vm-window-validate")
@@ -6122,6 +6334,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testVMNewWindowFlagAcceptsCaseInsensitiveUUID() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("vm-window-case")
@@ -6194,6 +6407,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testPipePaneWindowFlagDoesNotBecomePositionalCommandText() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("pipe-window")
@@ -6275,6 +6489,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testPipePaneWindowWorkspaceOmittedSurfaceDoesNotUseSelectedWorkspaceSurface() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("pipe-window-workspace")
@@ -6371,6 +6586,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testRespawnPaneWindowFlagDoesNotBecomePositionalCommandText() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("respawn-window")
@@ -6452,6 +6668,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testMoveSurfaceWindowFlagKeepsIndexedSourceInCallerContext() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("move-surface-window")
@@ -6531,6 +6748,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testMoveSurfaceWindowFlagAllowsSourceSurfaceRefFromOtherWindow() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("move-surface-cross-window")
@@ -6589,6 +6807,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testSidebarMetadataWindowFlagTargetsSelectedWorkspaceInWindow() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("status-window")
@@ -6639,6 +6858,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(result.stderr.isEmpty, result.stderr)
}
+ @Test
func testSidebarMetadataWindowFlagAfterSeparatorStaysMessageText() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("log-separator")
@@ -6687,6 +6907,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(result.stderr.isEmpty, result.stderr)
}
+ @Test
func testSidebarMetadataWindowFlagFailsWhenWindowHasNoCurrentWorkspace() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("status-window-empty")
@@ -6735,6 +6956,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testNotifyWindowFlagResolvesCurrentWorkspaceInWindow() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("notify-window")
@@ -6798,6 +7020,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testNotifyWindowSurfaceRefResolvesAcrossTargetWindowWorkspaces() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("notify-window-surface")
@@ -6919,6 +7142,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(methods, ["window.list", "workspace.list", "surface.list", "surface.list"])
}
+ @Test
func testNotifyWindowSurfaceIndexUsesCurrentWorkspaceInTargetWindow() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("notify-window-surface-index")
@@ -7002,6 +7226,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(methods, ["window.list", "workspace.current", "surface.list"])
}
+ @Test
func testWorkspaceActionWindowFlagResolvesCurrentWorkspaceInWindow() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("action-window")
@@ -7064,6 +7289,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testClearNotificationsWindowFlagFailsWhenWindowHasNoCurrentWorkspace() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("clear-window-empty")
@@ -7112,6 +7338,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testTreeCommandForwardsWindowFlag() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("tree-window")
@@ -7166,6 +7393,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(result.stderr.isEmpty, result.stderr)
}
+ @Test
func testTreeCommandWindowFlagSurvivesLegacyFallback() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("tree-legacy-window")
@@ -7296,6 +7524,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertFalse(result.stdout.contains(otherWindowId), result.stdout)
}
+ @Test
func testCodexPromptSubmitWithForeignCmuxEnvDoesNotFallbackToSelectedWorkspace() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex-foreign-env")
@@ -7359,6 +7588,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexPromptSubmitWithForeignCmuxEnvIgnoresStaleMappedSession() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex-foreign-mapped")
@@ -7445,6 +7675,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexPromptSubmitWithInvalidSurfaceDoesNotFallbackToFocusedSurface() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex-invalid-surface")
@@ -7512,6 +7743,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexPromptSubmitWithInvalidMappedWorkspaceDoesNotFallbackToSelectedWorkspace() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex-invalid-mapped")
@@ -7598,6 +7830,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testCodexTeamsForkPromptPublishesResumeBinding() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex-team-resume")
@@ -7739,6 +7972,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testAgentPromptClearsSurfaceResumeBindingWhenResumeCommandUnavailable() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("agent-resume-unavailable")
@@ -7845,6 +8079,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(request["checkpoint_id"] as? String, sessionId)
}
+ @Test
func testGenericAgentSessionEndClearsMatchingSurfaceResumeBinding() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("agent-resume-clear")
@@ -7928,6 +8163,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(request["source"] as? String, "agent-hook")
}
+ @Test
func testSurfaceResumeClearCLIForwardsCheckpointAndSourceGuards() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("resume-clear-guards")
@@ -7987,6 +8223,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(request["source"] as? String, "agent-hook")
}
+ @Test
func testSurfaceResumeSetCLIPreservesQuotedShellCommand() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("resume-set-shell")
@@ -8046,6 +8283,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(request["command"] as? String, "tmux attach -t work")
}
+ @Test
func testSurfaceResumeSetCLIStopsParsingOptionsAfterTerminator() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("resume-set-terminator")
@@ -8112,6 +8350,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testSurfaceResumeSetCLIDoesNotScopeExplicitSurfaceToEnvWorkspace() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("resume-set-surface")
@@ -8167,6 +8406,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(request["surface_id"] as? String, movedSurfaceId)
}
+ @Test
func testSurfaceResumeSetCLIRejectsTrailingShellTokens() throws {
let cliPath = try bundledCLIPath()
let missingSocketPath = "/tmp/cmux-test-missing-\(UUID().uuidString).sock"
@@ -8196,6 +8436,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertFalse(result.stderr.contains("Socket"), result.stderr)
}
+ @Test
func testSurfaceResumeSetCLIRejectsPreTerminatorCommandTokens() throws {
let cliPath = try bundledCLIPath()
let missingSocketPath = "/tmp/cmux-test-missing-\(UUID().uuidString).sock"
@@ -8223,6 +8464,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertFalse(result.stderr.contains("Socket"), result.stderr)
}
+ @Test
func testSurfaceResumeSetCLIRejectsDanglingValueOptionsBeforeSocketRequest() throws {
let cliPath = try bundledCLIPath()
let workspaceId = "11111111-1111-1111-1111-111111111111"
@@ -8277,6 +8519,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
}
}
+ @Test
func testSurfaceResumeClearCLIRejectsMalformedGuardsBeforeClearing() throws {
let cliPath = try bundledCLIPath()
let missingSocketPath = "/tmp/cmux-test-missing-\(UUID().uuidString).sock"
@@ -8303,6 +8546,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertFalse(result.stderr.contains("Socket"), result.stderr)
}
+ @Test
func testSurfaceResumeClearCLINormalizesWindowIndex() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("resume-clear-window")
@@ -8381,6 +8625,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(request["surface_id"] as? String, surfaceId)
}
+ @Test
func testSurfaceResumeClearCLIParsesLocalWindowOption() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("resume-clear-local-window")
@@ -8705,6 +8950,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
}
}
+ @Test
func testBrowserImportDefaultsNonInteractiveInCodingAgent() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("browser-import-agent")
@@ -8788,6 +9034,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testBrowserImportUsesInteractiveDialogOutsideCodingAgent() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("browser-import-human")
@@ -8854,6 +9101,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testBrowserImportInteractiveFlagForcesDialogInCodingAgent() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("browser-import-agent-interactive")
@@ -8908,6 +9156,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testBrowserProfilesListRoutesToSocketMethod() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("browser-profile-list")
@@ -8964,6 +9213,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
)
}
+ @Test
func testBrowserProfilesCreateClearAndDeleteRouteToSocketMethods() throws {
let cliPath = try bundledCLIPath()
let cases: [(name: String, arguments: [String], expectedMethod: String, expectedParams: [String], responseResult: [String: Any])] = [
@@ -9262,6 +9512,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
// inherited env), not the operator's focused pane returned by system.identify. Without the fix it
// stamped the focused surface (A), desyncing CMUX_SURFACE_ID from CMUX_PANEL_ID and jumbling codex
// into the wrong surface on reload.
+ @Test
func testTmuxCompatEnvStampsLaunchSurfaceNotFocusedPane() throws {
let cliPath = try bundledCLIPath()
let tmpDir = FileManager.default.temporaryDirectory
diff --git a/cmuxTests/WorkspaceRemoteConnectionTests.swift b/cmuxTests/WorkspaceRemoteConnectionTests.swift
index a0c3cc1d5322..36461aac85da 100644
--- a/cmuxTests/WorkspaceRemoteConnectionTests.swift
+++ b/cmuxTests/WorkspaceRemoteConnectionTests.swift
@@ -1,6 +1,7 @@
import Darwin
import Combine
-import XCTest
+import Foundation
+import Testing
import CmuxCore
import CmuxRemoteDaemon
import CmuxRemoteSession
@@ -13,6 +14,151 @@ import CmuxTerminal
@testable import cmux
#endif
+private typealias XCTestExpectation = DispatchSemaphore
+
+private extension DispatchSemaphore {
+ func fulfill() {
+ signal()
+ }
+}
+
+private func legacyTimeout(_ timeout: TimeInterval) -> DispatchTime {
+ .now() + .milliseconds(Int((timeout * 1_000).rounded(.up)))
+}
+
+private func legacyFailure(_ message: String) {
+ Issue.record(Comment(rawValue: message.isEmpty ? "Assertion failed" : message))
+}
+
+private func expectation(description _: String) -> XCTestExpectation {
+ DispatchSemaphore(value: 0)
+}
+
+private func wait(for expectations: [XCTestExpectation], timeout: TimeInterval) {
+ for expectation in expectations {
+ if expectation.wait(timeout: legacyTimeout(timeout)) == .timedOut {
+ legacyFailure("Timed out waiting for expectation")
+ }
+ }
+}
+
+private func XCTFail(_ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ legacyFailure(message())
+}
+
+private func XCTAssertTrue(_ expression: @autoclosure () throws -> Bool, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ if try !expression() {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertFalse(_ expression: @autoclosure () throws -> Bool, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ if try expression() {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertEqual(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs != rhs {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertNotEqual(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs == rhs {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertGreaterThan(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs <= rhs {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertGreaterThanOrEqual(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs < rhs {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertNil(_ expression: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ if try expression() != nil {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertNotNil(_ expression: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ if try expression() == nil {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertNoThrow(_ expression: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
+ do {
+ _ = try expression()
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+}
+
+private func XCTAssertThrowsError(_ expression: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line, _ errorHandler: (Error) -> Void = { _ in }) {
+ do {
+ _ = try expression()
+ legacyFailure(message())
+ } catch {
+ errorHandler(error)
+ }
+}
+
+private func XCTUnwrap(_ expression: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) throws -> T {
+ guard let value = try expression() else {
+ legacyFailure(message())
+ throw NSError(domain: "WorkspaceRemoteConnectionTests", code: 1)
+ }
+ return value
+}
+
/// Closure shape shared by the scripted process-runner tests; mirrors the
/// legacy `runProcessOverrideForTesting` static signature so the scripted
/// bodies stay byte-identical.
@@ -30,7 +176,8 @@ private struct ScriptedRemoteProcessRunner: RemoteSessionProcessRunning, @unchec
}
}
-final class WorkspaceRemoteConnectionTests: XCTestCase {
+@Suite(.serialized)
+final class WorkspaceRemoteConnectionTests {
private struct ProcessRunResult {
let status: Int32, stdout: String, stderr: String
let timedOut: Bool
@@ -199,6 +346,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
.filter { !$0.isEmpty }
}
+ @Test
func testGeneratedBashBootstrapSourcesLoginFilesInBashPrecedenceOrder() throws {
XCTAssertEqual(
try runGeneratedBashBootstrapMarkers(startupFiles: [
@@ -226,6 +374,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
)
}
+ @Test
func testGeneratedFallbackShellBootstrapPrependsCmuxBinOnce() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
@@ -275,6 +424,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(cmuxBinEntries.count, 1, path)
}
+ @Test
func testRemoteRelayMetadataCleanupScriptRemovesMatchingSocketAddr() {
let fileManager = FileManager.default
let home = fileManager.temporaryDirectory.appendingPathComponent("cmux-relay-cleanup-\(UUID().uuidString)")
@@ -313,6 +463,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertFalse(fileManager.fileExists(atPath: ttyURL.path))
}
+ @Test
func testRemoteRelayMetadataCleanupScriptPreservesDifferentSocketAddr() {
let fileManager = FileManager.default
let home = fileManager.temporaryDirectory.appendingPathComponent("cmux-relay-cleanup-preserve-\(UUID().uuidString)")
@@ -351,6 +502,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertFalse(fileManager.fileExists(atPath: ttyURL.path))
}
+ @Test
func testRemoteStaleRelayListenerCleanupScriptKillsMatchingPersistentRelayListener() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory.appendingPathComponent("cmux-stale-relay-cleanup-\(UUID().uuidString)")
@@ -414,6 +566,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertTrue(killOutput.contains("-KILL 34057"), killOutput)
}
+ @Test
func testRemoteStaleRelayListenerCleanupScriptPreservesDifferentPersistentSlot() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory.appendingPathComponent("cmux-stale-relay-preserve-\(UUID().uuidString)")
@@ -472,6 +625,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(try String(contentsOf: killLog, encoding: .utf8), "")
}
+ @Test
func testRemoteStaleRelayListenerCleanupScriptMatchesPersistentSlotExactly() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory.appendingPathComponent("cmux-stale-relay-slot-prefix-\(UUID().uuidString)")
@@ -530,6 +684,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(try String(contentsOf: killLog, encoding: .utf8), "")
}
+ @Test
func testRemoteStaleRelayListenerCleanupScriptKillsMetadataMatchedListenerWithoutChild() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory.appendingPathComponent("cmux-stale-relay-metadata-\(UUID().uuidString)")
@@ -606,6 +761,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertTrue(killOutput.contains("-KILL 33681"), killOutput)
}
+ @Test
func testRemoteStaleRelayListenerCleanupScriptPreservesMetadataMatchedDifferentPersistentSlot() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory.appendingPathComponent("cmux-stale-relay-metadata-preserve-\(UUID().uuidString)")
@@ -676,6 +832,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(try String(contentsOf: killLog, encoding: .utf8), "")
}
+ @Test
func testRelayZshBootstrapUsesRealHomeHistoryByDefault() throws {
let histfile = try runRelayZshHistfile { home in
try ":\n".write(to: home.appendingPathComponent(".zshenv"), atomically: true, encoding: .utf8)
@@ -686,6 +843,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertTrue(histfile.hasSuffix("/.zsh_history"))
}
+ @Test
func testRelayZshBootstrapUsesUserUpdatedZdotdirHistory() throws {
let histfile = try runRelayZshHistfile { home in
let altZdotdir = home.appendingPathComponent("dotfiles")
@@ -702,6 +860,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertTrue(histfile.contains("/dotfiles/.zsh_history"))
}
+ @Test
func testRemoteUTF8LocaleSetupLinesSeedUTF8LocaleWhenMissing() {
let script = (RemoteShellEnvironment.utf8LocaleSetupLines() + [
#"printf '%s' "${LANG}|${LC_CTYPE}|${LC_ALL}""#,
@@ -726,6 +885,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(result.stdout.trimmingCharacters(in: .whitespacesAndNewlines), "C.UTF-8|C.UTF-8|C.UTF-8")
}
+ @Test
func testRemoteUTF8LocaleSetupLinesPreserveExistingUTF8Locale() {
let script = (RemoteShellEnvironment.utf8LocaleSetupLines() + [
#"printf '%s' "${LANG}|${LC_CTYPE}|${LC_ALL}""#,
@@ -750,6 +910,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(result.stdout.trimmingCharacters(in: .whitespacesAndNewlines), "ja_JP.UTF-8||")
}
+ @Test
func testDaemonSocketForwardArgumentsTargetBakedVMSocket() {
let configuration = WorkspaceRemoteConfiguration(
destination: "cmux-macmini",
@@ -782,6 +943,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(arguments.last, "cmux-macmini")
}
+ @Test
func testProxyBrokerTransportKeySeparatesVMBakedSSHFromStandardSSH() {
let standard = WorkspaceRemoteConfiguration(
destination: "cmux-macmini",
@@ -871,6 +1033,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testWebSocketVMWithoutDaemonEndpointSkipsProxyStartup() {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -895,6 +1058,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testSkipBootstrapPersistentPTYDoesNotFailBakedCapabilityPreflight() {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -925,6 +1089,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertFalse((daemon?["detail"] as? String)?.contains("pty.session") == true)
}
+ @Test
func testRemoteDaemonCapabilityErrorsUseUserFacingMessage() {
let message = RemoteDaemonStrings.appLocalized.missingRequiredCapabilitiesMessage([
"pty.session",
@@ -956,6 +1121,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testWebSocketVMWithDaemonEndpointStartsProxyCapableConnection() {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -986,6 +1152,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
workspace.disconnectRemoteConnection(clearConfiguration: true)
}
+ @Test
func testReverseRelayStartupFailureDetailCapturesImmediateForwardingFailure() throws {
let process = Process()
let stderrPipe = Pipe()
@@ -1006,6 +1173,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(detail, "remote port forwarding failed for listen port 64009")
}
+ @Test
func testExecutableSearchPathsIncludesHomebrewAndHomeFallbacks() {
let paths = RemoteSessionCoordinator.executableSearchPaths(
environment: [
@@ -1033,6 +1201,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
)
}
+ @Test
func testParsePathHelperPathsExtractsPathEntries() {
XCTAssertEqual(
RemoteSessionCoordinator.parsePathHelperPaths(
@@ -1046,6 +1215,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
)
}
+ @Test
func testParsePathHelperPathsIgnoresMANPATHAssignments() {
XCTAssertEqual(
RemoteSessionCoordinator.parsePathHelperPaths(
@@ -1063,6 +1233,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testRemoteTerminalSurfaceLookupTracksOnlyActiveSSHSurfaces() throws {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1088,6 +1259,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testWebSocketRemoteTerminalEndLeavesConnectedStateWithinBoundedTime() throws {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1135,6 +1307,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testWebSocketRemoteTerminalEndWithoutStartupCommandStillDisconnects() throws {
let workspace = Workspace()
let initialConfig = WorkspaceRemoteConfiguration(
@@ -1198,6 +1371,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testRemoteTerminalSessionEndPublishesDisconnectedDetailWithoutTransientNil() throws {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1238,6 +1412,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testRemoteTerminalSessionEndIgnoresDuplicateRelayCallbackAfterSurfaceProcessed() throws {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1267,6 +1442,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testForegroundSSHAuthReadyBeforeRemoteConfigureStartsDeferredConnect() {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1291,6 +1467,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testForegroundSSHAuthReadyReconnectsConfiguredDisconnectedRemoteWorkspace() {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1317,6 +1494,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testForegroundSSHAuthReadyBufferedTokenDoesNotReconnectDifferentConfiguration() {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1340,6 +1518,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testRemoteReconnectingStateIsExposedInStatusPayload() {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1367,6 +1546,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testForegroundSSHAuthReadyIgnoresMismatchedConfiguredToken() {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1390,6 +1570,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testRemoteTerminalSessionEndRequestsControlMasterCleanupAndLeavesWorkspaceDisconnected() throws {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1445,6 +1626,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testRemoteTerminalSessionEndWithoutCallbackRelayPortStillCleansControlMaster() throws {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1495,6 +1677,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testRemoteTerminalSessionEndPreservesPersistentPTYWorkspace() throws {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1549,6 +1732,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testTeardownRemoteConnectionRequestsControlMasterCleanupWhileStillConnecting() {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1601,6 +1785,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testTeardownRemoteConnectionRequestsControlMasterCleanupWithoutExplicitControlPath() {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1648,6 +1833,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testClosingRemoteWorkspaceRequestsControlMasterCleanup() throws {
let manager = TabManager()
let remainingWorkspace = try XCTUnwrap(manager.selectedWorkspace)
@@ -1704,6 +1890,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testDetachLastRemoteSurfacePreservesRemoteSessionWithoutCleanup() throws {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -1751,6 +1938,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testClosingSourceWorkspaceAfterDetachingRemoteSurfaceSkipsControlMasterCleanup() throws {
let manager = TabManager()
let sourceWorkspace = try XCTUnwrap(manager.selectedWorkspace)
@@ -1804,6 +1992,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testClosingMixedSourceWorkspaceAfterDetachingLastRemoteSurfaceSkipsControlMasterCleanup() throws {
let manager = TabManager()
let sourceWorkspace = try XCTUnwrap(manager.selectedWorkspace)
@@ -1859,6 +2048,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testTransferredRemoteSurfaceCleansUpControlMasterWhenSessionEndsInLocalWorkspace() throws {
let manager = TabManager()
let sourceWorkspace = try XCTUnwrap(manager.selectedWorkspace)
@@ -1914,6 +2104,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testRemoteTerminalSessionEndDisconnectsWorkspaceWhenBrowserPanelsRemain() throws {
let workspace = Workspace()
let paneID = try XCTUnwrap(workspace.bonsplitController.allPaneIds.first)
@@ -1955,6 +2146,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testClosingInitialRemoteTerminalPaneKeepsSiblingRemotePaneAlive() throws {
let workspace = Workspace()
let initialTerminalID = try XCTUnwrap(workspace.focusedTerminalPanel?.id)
@@ -2005,6 +2197,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertTrue(cleanupArguments.isEmpty)
}
+ @Test
func testRemoteDropPathUsesLowercasedExtensionAndProvidedUUID() throws {
let fileURL = URL(fileURLWithPath: "/Users/test/Screen Shot.PNG")
let uuid = try XCTUnwrap(UUID(uuidString: "12345678-1234-1234-1234-1234567890AB"))
@@ -2015,6 +2208,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testDaemonBootstrapUploadUsesAbsoluteHomePathForScpDestination() throws {
let fileManager = FileManager.default
let directoryURL = fileManager.temporaryDirectory.appendingPathComponent(
@@ -2114,6 +2308,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testPersistentPTYBootstrapReinstallsOldDaemonMissingPTYCapability() throws {
let previousAllowLocalBuild = getenv("CMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD").map { String(cString: $0) }
let previousDaemonBinary = getenv("CMUX_REMOTE_DAEMON_BINARY").map { String(cString: $0) }
@@ -2219,6 +2414,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testPersistentReverseRelayCancelsStaleControlMasterForwardBeforeReusingRelayPort() throws {
let forwardInvoked = DispatchSemaphore(value: 0)
let lock = NSLock()
@@ -2309,6 +2505,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testPersistentReverseRelayCleansStaleRemoteListenerAndRetriesControlMasterForward() throws {
let retryForwardInvoked = DispatchSemaphore(value: 0)
let lock = NSLock()
@@ -2431,6 +2628,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testDetachAttachPreservesRemoteTerminalSurfaceTracking() throws {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -2472,6 +2670,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testDetachAttachPreservesPersistentPTYSessionIDAcrossWorkspaces() throws {
let source = Workspace()
let destination = Workspace()
@@ -2521,6 +2720,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testDetachAttachDoesNotAdoptPersistentPTYSessionIDAcrossNilRelayWorkspaces() throws {
let source = Workspace()
let destination = Workspace()
@@ -2590,6 +2790,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testExplicitRemotePTYSessionSurfaceTracksRemoteTerminalWithoutDefaultStartup() throws {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -2634,6 +2835,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testRemoteDisconnectClearsExplicitRemotePTYSessionIDBeforeReseed() throws {
let workspace = Workspace()
let explicitSessionConfig = WorkspaceRemoteConfiguration(
@@ -2690,6 +2892,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testRemoteReconfigureClearsExplicitRemotePTYSessionIDForTrackedSurface() throws {
let workspace = Workspace()
let originalConfig = WorkspaceRemoteConfiguration(
@@ -2738,6 +2941,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testExplicitRemotePTYSessionSplitTracksRemoteTerminalWithoutDefaultStartup() throws {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -2776,6 +2980,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testPersistentRemoteTerminalSeedsDefaultPTYSessionIDForSnapshot() throws {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -2806,6 +3011,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testDetachAttachPreservesSurfaceTTYMetadata() throws {
let source = Workspace()
let destination = Workspace()
@@ -2829,6 +3035,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(source.bonsplitController.tabs(inPane: sourcePaneID).count, 0)
}
+ @Test
func testDetectedSSHUploadFailureCleansUpEarlierRemoteUploads() throws {
let fileManager = FileManager.default
let directoryURL = fileManager.temporaryDirectory.appendingPathComponent(
@@ -2895,6 +3102,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertTrue(cleanupCommand.contains(String(uploadedRemotePath)))
}
+ @Test
func testDetectsForegroundSSHSessionForTTY() {
let session = TerminalSSHSessionDetector.detectForTesting(
ttyName: "/dev/ttys004",
@@ -2934,6 +3142,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
)
}
+ @Test
func testDetectsForegroundSSHSessionWithShortControlPathFlag() {
let session = TerminalSSHSessionDetector.detectForTesting(
ttyName: "/dev/ttys004",
@@ -2959,6 +3168,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertFalse(scpArgs.contains("-S"))
}
+ @Test
func testDetectsForegroundEternalTerminalSessionForTTY() {
let session = TerminalSSHSessionDetector.detectForTesting(
ttyName: "/dev/ttys004",
@@ -2991,6 +3201,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
)
}
+ @Test
func testDetectsEternalTerminalSessionWithoutTreatingETPortAsSSHPort() {
let session = TerminalSSHSessionDetector.detectForTesting(
ttyName: "/dev/ttys004",
@@ -3019,6 +3230,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(scpArgs.last, "lawrence@example.com:/tmp/cmux-drop-123.png")
}
+ @Test
func testDetectsEternalTerminalSessionWithBracketedIPv6ServerPortForSCP() {
let session = TerminalSSHSessionDetector.detectForTesting(
ttyName: "/dev/ttys004",
@@ -3045,6 +3257,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(scpArgs.last, "lawrence@[2001:db8::1]:/tmp/cmux-drop-123.png")
}
+ @Test
func testDetectsEternalTerminalSessionWithFullIPv6ServerPortForSCP() {
let session = TerminalSSHSessionDetector.detectForTesting(
ttyName: "/dev/ttys004",
@@ -3071,6 +3284,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(scpArgs.last, "lawrence@[2001:db8:0:0:0:0:0:1]:/tmp/cmux-drop-123.png")
}
+ @Test
func testDetectsEternalTerminalSessionPreservesAmbiguousCompressedIPv6LiteralForSCP() {
let session = TerminalSSHSessionDetector.detectForTesting(
ttyName: "/dev/ttys004",
@@ -3097,6 +3311,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(scpArgs.last, "lawrence@[2001:db8::1:2022]:/tmp/cmux-drop-123.png")
}
+ @Test
func testDetectsEternalTerminalSessionIgnoresOptionsAfterDestination() {
let session = TerminalSSHSessionDetector.detectForTesting(
ttyName: "/dev/ttys004",
@@ -3123,6 +3338,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(scpArgs.last, "lawrence@example.com:/tmp/cmux-drop-123.png")
}
+ @Test
func testDetectsEternalTerminalSessionStripsNativeJumpHostServerPortForSCP() {
let session = TerminalSSHSessionDetector.detectForTesting(
ttyName: "/dev/ttys004",
@@ -3152,6 +3368,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(scpArgs.last, "lawrence@example.com:/tmp/cmux-drop-123.png")
}
+ @Test
func testDetectsEternalTerminalSessionSSHOptionsForSCP() {
let session = TerminalSSHSessionDetector.detectForTesting(
ttyName: "/dev/ttys004",
@@ -3193,6 +3410,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
)
}
+ @Test
func testDaemonTransportArgumentsReuseConfiguredControlPath() {
let configuration = WorkspaceRemoteConfiguration(
destination: "cmux-macmini",
@@ -3223,6 +3441,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertTrue(arguments.last?.contains("/remote/cmuxd-remote") ?? false)
}
+ @Test
func testDaemonTransportArgumentsReuseWhitespaceConfiguredControlPath() {
let configuration = WorkspaceRemoteConfiguration(
destination: "cmux-macmini",
@@ -3251,6 +3470,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertTrue(arguments.contains(where: { $0 == "ControlPath /tmp/cmux-ssh-%C" || $0 == "ControlPath=/tmp/cmux-ssh-%C" }))
}
+ @Test
func testReverseRelayControlMasterArgumentsReuseConfiguredControlSocket() throws {
let configuration = WorkspaceRemoteConfiguration(
destination: "cmux-macmini",
@@ -3287,6 +3507,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertTrue(arguments.contains("cmux-macmini"))
}
+ @Test
func testReverseRelayControlMasterCancelArgumentsUseRemoteListenPortOnly() throws {
let configuration = WorkspaceRemoteConfiguration(
destination: "cmux-macmini",
@@ -3323,6 +3544,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertTrue(arguments.contains("cmux-macmini"))
}
+ @Test
func testReverseRelayControlMasterArgumentsReuseWhitespaceConfiguredControlSocket() throws {
let configuration = WorkspaceRemoteConfiguration(
destination: "cmux-macmini",
@@ -3356,6 +3578,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertTrue(arguments.contains("forward"))
}
+ @Test
func testDetectedSSHSessionBracketsIPv6LiteralSCPDestination() {
let session = DetectedSSHSession(
destination: "lawrence@2001:db8::1",
@@ -3379,6 +3602,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(scpArgs.last, "lawrence@[2001:db8::1]:/tmp/cmux-drop-123.png")
}
+ @Test
func testDetectsForegroundSSHSessionWithLowercaseAgentFlag() {
let session = TerminalSSHSessionDetector.detectForTesting(
ttyName: "/dev/ttys004",
@@ -3398,6 +3622,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertFalse(session?.forwardAgent ?? true)
}
+ @Test
func testDetectsForegroundSSHSessionIgnoringBindInterfaceValue() {
let session = TerminalSSHSessionDetector.detectForTesting(
ttyName: "/dev/ttys004",
@@ -3416,6 +3641,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
XCTAssertEqual(session?.destination, "lawrence@example.com")
}
+ @Test
func testIgnoresBackgroundSSHProcessForTTY() {
let session = TerminalSSHSessionDetector.detectForTesting(
ttyName: "ttys004",
@@ -3431,6 +3657,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testProxyOnlyErrorsKeepSSHWorkspaceConnectedAndLoggedInSidebar() {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -3488,6 +3715,7 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
@MainActor
+ @Test
func testWebSocketDaemonTransportErrorClearsConnectedSidebarState() {
let workspace = Workspace()
let config = WorkspaceRemoteConfiguration(
@@ -3532,7 +3760,8 @@ final class WorkspaceRemoteConnectionTests: XCTestCase {
}
}
-final class CLINotifyProcessIntegrationTests: XCTestCase {
+@Suite(.serialized)
+final class CLINotifyProcessIntegrationTests {
private struct ProcessRunResult {
let status: Int32
let stdout: String
@@ -3931,6 +4160,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testAgentHookLaunchEnvironmentDoesNotPersistPathOrShell() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("hook")
@@ -4006,6 +4236,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
XCTAssertEqual(persistedEnvironment, ["CODEX_HOME": "/tmp/codex home"])
}
+ @Test
func testCodexHookStopSetsRateLimitStatusFromTranscript() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4084,6 +4315,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookStopTreatsTypedCodexErrorEventAsFailure() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4158,6 +4390,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookStopFallsBackToDiscoveredTranscriptWhenProvidedPathUnavailable() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4236,6 +4469,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexPromptSubmitRetiresPreviousMonitorLeaseForSameSession() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4314,6 +4548,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookStopTreatsCodexErrorInfoPayloadAsFailure() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4375,6 +4610,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookStopTreatsStructuredCodexErrorInfoPayloadAsFailure() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4436,6 +4672,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookStopTreatsCamelCaseCodexErrorInfoPayloadAsFailure() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4497,6 +4734,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookStopTreatsTypedHookPayloadAsFailure() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4558,6 +4796,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookStopTreatsExplicitErrorFieldAsFailureSignal() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4625,6 +4864,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookStopDoesNotKeepOldTranscriptErrorAfterSuccessfulTurn() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4703,6 +4943,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookStopPrefersExplicitErrorPayloadOverHealthyTranscript() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4777,6 +5018,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookStopTreatsCompletedTurnWithoutAssistantAsFailure() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4851,6 +5093,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookStopDoesNotSynthesizeNoFinalResponseAfterScopedAssistantMessage() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4923,6 +5166,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookStopIgnoresUnscopedTranscriptErrorWithoutTurnEvidence() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -4993,6 +5237,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookMonitorSetsErrorStatusFromCompletedTranscriptWithoutAssistant() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -5073,6 +5318,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookMonitorReportsExplicitErrorBeforeTerminalCompletion() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -5153,6 +5399,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookMonitorNotifiesOnRequestUserInput() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -5253,6 +5500,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
XCTAssertTrue(process.isRunning, "Monitor should keep watching the turn after publishing input notification")
}
+ @Test
func testCodexHookMonitorNotifiesOnResponseItemRequestUserInput() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex-response-item")
@@ -5353,6 +5601,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
XCTAssertTrue(process.isRunning, "Monitor should keep watching the turn after publishing input notification")
}
+ @Test
func testCodexHookMonitorReResolvesUnavailableTranscriptPath() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -5437,6 +5686,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testCodexHookMonitorIgnoresUnscopedTerminalForTurnScopedMonitor() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex")
@@ -5554,6 +5804,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
)
}
+ @Test
func testPTYBridgeFlushesReadyBeforeImmediateExit() throws {
let rpcClient = ImmediateExitPTYBridgeRPC()
let stopped = DispatchSemaphore(value: 0)
@@ -5601,6 +5852,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
XCTAssertEqual(rpcClient.attachCalls.first?.requireExisting, true)
}
+ @Test
func testPTYBridgeBuffersOutputUntilReadyFrame() throws {
let rpcClient = ImmediateOutputThenExitPTYBridgeRPC()
let stopped = DispatchSemaphore(value: 0)
@@ -5643,6 +5895,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
XCTAssertTrue(responseText.contains("early-output"), responseText)
}
+ @Test
func testPTYBridgeForwardsInputWithoutWaitingForWriteCompletion() throws {
let rpcClient = DeferredWriteCompletionPTYBridgeRPC()
let server = RemotePTYBridgeServer(
@@ -5685,6 +5938,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
rpcClient.completeWrites()
}
+ @Test
func testPTYBridgeStopRetainsServerUntilCleanupRuns() throws {
let rpcClient = ImmediateExitPTYBridgeRPC()
let stopped = DispatchSemaphore(value: 0)
@@ -5709,6 +5963,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
XCTAssertEqual(stopped.wait(timeout: .now() + 2), .success)
}
+ @Test
func testPTYBridgeKeepsOutputOpenAfterClientHalfClose() throws {
let rpcClient = DelayedOutputPTYBridgeRPC()
let stopped = DispatchSemaphore(value: 0)
@@ -5748,6 +6003,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
XCTAssertEqual(rpcClient.detachSemaphore.wait(timeout: .now() + 0.1), .timedOut)
}
+ @Test
func testPTYBridgeKeepsOutputOpenAfterClientHalfCloseWithoutPID() throws {
let rpcClient = DelayedOutputPTYBridgeRPC()
let stopped = DispatchSemaphore(value: 0)
@@ -5787,6 +6043,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
XCTAssertEqual(rpcClient.detachSemaphore.wait(timeout: .now() + 0.1), .timedOut)
}
+ @Test
func testPTYBridgeDefersHalfCloseUntilAttachCompletes() throws {
let attachStarted = DispatchSemaphore(value: 0)
let attachGate = DispatchSemaphore(value: 0)
@@ -5832,6 +6089,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
XCTAssertEqual(rpcClient.detachSemaphore.wait(timeout: .now() + 0.1), .timedOut)
}
+ @Test
func testPTYBridgeDetachesWhenClientSocketClosesAfterAttach() throws {
let rpcClient = DelayedOutputPTYBridgeRPC()
let stopped = DispatchSemaphore(value: 0)
@@ -5870,6 +6128,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
XCTAssertEqual(stopped.wait(timeout: .now() + 2), .success)
}
+ @Test
func testPTYBridgeClosesBackpressuredOutput() throws {
let rpcClient = FloodPTYBridgeRPC()
let stopped = DispatchSemaphore(value: 0)
@@ -6273,6 +6532,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
}
@MainActor
+ @Test
func testNotifyWithWorkspaceHandleKeepsCallerSurfaceFallback() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("notify")
@@ -6341,6 +6601,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
}
@MainActor
+ @Test
func testNotifyWithWorkspaceHandlePreservesSyncTargetValidation() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("notify-handle")
@@ -6415,6 +6676,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
}
@MainActor
+ @Test
func testTriggerFlashFallsBackFromStaleCallerWorkspaceAndSurfaceIDs() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("flash")
@@ -6528,6 +6790,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
}
@MainActor
+ @Test
func testSSHCommandCreatesConfiguresAndSelectsRemoteWorkspaceViaCLI() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("ssh")
@@ -6668,6 +6931,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
}
@MainActor
+ @Test
func testSSHCommandDoesNotDeferReconnectWhenWhitespaceControlMasterDisablesMultiplexing() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("ssh-controlmaster-no")
@@ -6768,6 +7032,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
}
@MainActor
+ @Test
func testNotifyPrefersCallerTTYOverFocusedSurfaceWhenCallerIDsAreStale() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("notify-tty")
@@ -6843,6 +7108,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
}
@MainActor
+ @Test
func testNotifyInTmuxPrefersCallerTTYOverStaleValidSurfaceID() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("notify-tmux-tty")
@@ -6917,6 +7183,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
}
@MainActor
+ @Test
func testTriggerFlashPrefersCallerTTYOverFocusedSurfaceWhenCallerIDsAreStale() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("flash-tty")
@@ -7073,6 +7340,7 @@ final class CLINotifyProcessIntegrationTests: XCTestCase {
}
@MainActor
+ @Test
func testTriggerFlashInTmuxPrefersCallerTTYOverStaleValidSurfaceID() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("flash-tmux-tty")
From 9ef971e675ca29da63f384d09d787c83491f9692 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 09:31:01 -0700
Subject: [PATCH 37/39] test: avoid blocking main actor in migrated waits
---
.github/swift-file-length-budget.tsv | 4 ++--
...INotifyProcessIntegrationRegressionTests.swift | 15 ++++++++++-----
cmuxTests/WorkspaceRemoteConnectionTests.swift | 15 ++++++++++-----
3 files changed, 22 insertions(+), 12 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index 24db48c94d9e..201702c81c28 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -9,10 +9,10 @@
12240 Sources/GhosttyTerminalView.swift
12144 cmuxTests/AppDelegateShortcutRoutingTests.swift
11929 Sources/Panels/BrowserPanel.swift
-9584 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
+9589 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
8017 CLI/cmux_open.swift
7986 Sources/Panels/BrowserPanelView.swift
-7485 cmuxTests/WorkspaceRemoteConnectionTests.swift
+7490 cmuxTests/WorkspaceRemoteConnectionTests.swift
7354 cmuxTests/WorkspaceUnitTests.swift
6317 cmuxTests/SessionPersistenceTests.swift
6217 cmuxTests/GhosttyConfigTests.swift
diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
index d9a8b28d5ab2..156a913529ad 100644
--- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
+++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
@@ -15,10 +15,6 @@ private extension DispatchSemaphore {
}
}
-private func legacyTimeout(_ timeout: TimeInterval) -> DispatchTime {
- .now() + .milliseconds(Int((timeout * 1_000).rounded(.up)))
-}
-
private func legacyFailure(_ message: String) {
Issue.record(Comment(rawValue: message.isEmpty ? "Assertion failed" : message))
}
@@ -29,7 +25,16 @@ private func expectation(description _: String) -> XCTestExpectation {
private func wait(for expectations: [XCTestExpectation], timeout: TimeInterval) {
for expectation in expectations {
- if expectation.wait(timeout: legacyTimeout(timeout)) == .timedOut {
+ let deadline = Date().addingTimeInterval(timeout)
+ var fulfilled = false
+ while Date() < deadline {
+ if expectation.wait(timeout: .now()) == .success {
+ fulfilled = true
+ break
+ }
+ _ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01))
+ }
+ if !fulfilled, expectation.wait(timeout: .now()) == .timedOut {
legacyFailure("Timed out waiting for expectation")
}
}
diff --git a/cmuxTests/WorkspaceRemoteConnectionTests.swift b/cmuxTests/WorkspaceRemoteConnectionTests.swift
index 36461aac85da..b950dd0a64ba 100644
--- a/cmuxTests/WorkspaceRemoteConnectionTests.swift
+++ b/cmuxTests/WorkspaceRemoteConnectionTests.swift
@@ -22,10 +22,6 @@ private extension DispatchSemaphore {
}
}
-private func legacyTimeout(_ timeout: TimeInterval) -> DispatchTime {
- .now() + .milliseconds(Int((timeout * 1_000).rounded(.up)))
-}
-
private func legacyFailure(_ message: String) {
Issue.record(Comment(rawValue: message.isEmpty ? "Assertion failed" : message))
}
@@ -36,7 +32,16 @@ private func expectation(description _: String) -> XCTestExpectation {
private func wait(for expectations: [XCTestExpectation], timeout: TimeInterval) {
for expectation in expectations {
- if expectation.wait(timeout: legacyTimeout(timeout)) == .timedOut {
+ let deadline = Date().addingTimeInterval(timeout)
+ var fulfilled = false
+ while Date() < deadline {
+ if expectation.wait(timeout: .now()) == .success {
+ fulfilled = true
+ break
+ }
+ _ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01))
+ }
+ if !fulfilled, expectation.wait(timeout: .now()) == .timedOut {
legacyFailure("Timed out waiting for expectation")
}
}
From 9097e2cb77191d0602b2831b8479e2d65cc45771 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 09:49:58 -0700
Subject: [PATCH 38/39] test: finish Swift Testing migration for hook suite
extensions
---
.github/swift-file-length-budget.tsv | 8 +-
cmuxTests/CLIAuthAliasTests.swift | 6 +-
.../CLIGenericHookPersistenceTests.swift | 57 ++--
cmuxTests/CLILegacyHookAliasTests.swift | 6 +-
...ifyProcessIntegrationRegressionTests.swift | 109 -------
cmuxTests/CLINotifyProcessTestSupport.swift | 283 +++++++++++++++++-
.../CLIRovoDevHookPersistenceTests.swift | 18 +-
...SHPTYAttachProbeReplyRegressionTests.swift | 1 +
.../SSHStartupSignalLifecycleTests.swift | 22 +-
cmuxTests/VMSSHCommandTests.swift | 8 +-
10 files changed, 349 insertions(+), 169 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index 201702c81c28..21ba04837eb7 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -9,7 +9,7 @@
12240 Sources/GhosttyTerminalView.swift
12144 cmuxTests/AppDelegateShortcutRoutingTests.swift
11929 Sources/Panels/BrowserPanel.swift
-9589 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
+9480 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
8017 CLI/cmux_open.swift
7986 Sources/Panels/BrowserPanelView.swift
7490 cmuxTests/WorkspaceRemoteConnectionTests.swift
@@ -29,7 +29,7 @@
3926 cmuxTests/TabManagerUnitTests.swift
3896 cmuxTests/WindowAndDragTests.swift
3734 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
-3699 cmuxTests/CLIGenericHookPersistenceTests.swift
+3706 cmuxTests/CLIGenericHookPersistenceTests.swift
3397 Sources/CmuxConfig.swift
3364 cmuxTests/TabManagerSessionSnapshotTests.swift
3058 Sources/Update/UpdateTitlebarAccessory.swift
@@ -106,12 +106,13 @@
865 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
859 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Workspace/ControlCommandCoordinator+Workspace.swift
847 cmuxTests/AgentSessionAutoResumeSettingsTests.swift
-845 cmuxTests/SSHStartupSignalLifecycleTests.swift
+847 cmuxTests/SSHStartupSignalLifecycleTests.swift
830 Sources/TaskManagerTypes.swift
825 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift
824 Sources/MainWindowFocusController.swift
810 Packages/macOS/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/SwiftViewInterpreterTests.swift
802 Sources/WorkspaceContentView.swift
+799 cmuxTests/CLINotifyProcessTestSupport.swift
797 Sources/ClosedItemHistory.swift
779 cmuxUITests/BrowserOmnibarSuggestionsUITests.swift
769 Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+Input.swift
@@ -201,7 +202,6 @@
531 Sources/App/WorkspaceRuntimeSettings.swift
530 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttyRuntime.swift
529 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Scene/SettingsWindowScene.swift
-528 cmuxTests/CLINotifyProcessTestSupport.swift
528 cmuxUITests/AutomationSocketUITests.swift
527 CLI/CLISocketPathResolver.swift
526 Packages/macOS/CmuxSettings/Sources/CmuxSettings/SocketControl/SocketControlSettings.swift
diff --git a/cmuxTests/CLIAuthAliasTests.swift b/cmuxTests/CLIAuthAliasTests.swift
index 547da5518337..8dc1c0d3b3e2 100644
--- a/cmuxTests/CLIAuthAliasTests.swift
+++ b/cmuxTests/CLIAuthAliasTests.swift
@@ -1,7 +1,9 @@
-import XCTest
import Darwin
+import Foundation
+import Testing
extension CLINotifyProcessIntegrationRegressionTests {
+ @Test
func testTopLevelLoginAliasesAuthLogin() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("auth-login")
@@ -61,7 +63,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Expected login alias to call auth.begin_sign_in, saw \(state.commands)"
)
}
-
+ @Test
func testTopLevelLogoutAliasesAuthLogout() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("auth-logout")
diff --git a/cmuxTests/CLIGenericHookPersistenceTests.swift b/cmuxTests/CLIGenericHookPersistenceTests.swift
index e02145f1b932..110df2cf784d 100644
--- a/cmuxTests/CLIGenericHookPersistenceTests.swift
+++ b/cmuxTests/CLIGenericHookPersistenceTests.swift
@@ -1,5 +1,6 @@
-import XCTest
import Darwin
+import Foundation
+import Testing
extension CLINotifyProcessIntegrationRegressionTests {
struct GenericHookPersistenceScenario {
@@ -12,7 +13,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
let expectedArguments: [String]
let expectedEnvironment: [String: String]?
}
-
+ @Test
func testGenericHookAgentsPersistSanitizedLaunchCommandsForSessionRestore() throws {
let scenarios: [GenericHookPersistenceScenario] = [
GenericHookPersistenceScenario(
@@ -279,12 +280,12 @@ extension CLINotifyProcessIntegrationRegressionTests {
]
for scenario in scenarios {
- try XCTContext.runActivity(named: scenario.agent) { _ in
+ try runLegacyActivity(named: scenario.agent) {
try runGenericHookPersistenceScenario(scenario)
}
}
}
-
+ @Test
func testAntigravityStopAndNotificationsUseGenericNotificationPath() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("antigravity-notification")
@@ -552,7 +553,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Expected Antigravity error notifications to mark error status, saw \(errorCommands)"
)
}
-
+ @Test
func testHermesAgentNotificationsUseShellHookExtraPayload() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("hermes-notification")
@@ -707,7 +708,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertNil(responseSession["lastNotificationStatus"])
XCTAssertEqual(responseSession["runtimeStatus"] as? String, "running")
}
-
+ @Test
func testHermesAgentSessionEndIsTurnBoundaryButFinalizeTearsDown() throws {
// Hermes fires the `on_session_end` plugin hook once per conversation turn
// (end of every run_conversation()), not at the true session boundary, and a
@@ -862,7 +863,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Hermes on_session_finalize is a true teardown and must consume the restore record"
)
}
-
+ @Test
func testAntigravityHookInstallUsesNativeHooksJSONShape() throws {
let cliPath = try bundledCLIPath()
let root = FileManager.default.temporaryDirectory
@@ -947,7 +948,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertNotNil(cmuxGroup["Notification"])
XCTAssertNotNil(cmuxGroup["PostToolUse"])
}
-
+ @Test
func testKiroHookInstallUsesAgentConfigShapeAndPreservesDenyExit() throws {
let cliPath = try bundledCLIPath()
let root = FileManager.default.temporaryDirectory
@@ -1002,7 +1003,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertNotNil(hooks["postToolUse"])
XCTAssertNotNil(hooks["stop"])
}
-
+ @Test
func testKiroFeedDenyUsesPreToolUseExitCodeTwo() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("kiro-feed-deny")
@@ -1083,6 +1084,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
/// / `bypass`, the WorkstreamPermissionMode raw values) must exit 0 so
/// Kiro proceeds; an unrecognized/malformed mode must fail closed with
/// exit 2 rather than silently allowing the tool.
+ @Test
func testKiroFeedAllowModesProceedAndUnknownModeDenies() throws {
func runKiroDecision(mode: String) throws -> ProcessRunResult {
let cliPath = try bundledCLIPath()
@@ -1149,6 +1151,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
/// tools (`fs_write`) still emit. Guards that suppression keys off the
/// classified wire name (`PostToolUse`) rather than the raw camelCase hook
/// event — i.e. the suppression actually triggers for real Kiro events.
+ @Test
func testKiroStandardLevelSuppressesReadOnlyToolFeedEvents() throws {
func feedPushCount(forTool tool: String) throws -> Int {
let cliPath = try bundledCLIPath()
@@ -1193,7 +1196,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
// server to record it (generous timeout to avoid flaking on the
// socket/process round-trip under CI load). A suppressed event
// sends nothing, so this wait simply times out silently.
- _ = XCTWaiter().wait(for: [serverHandled], timeout: 5)
+ _ = waitForLegacyExpectations([serverHandled], timeout: 5, recordFailure: false)
return state.commands.filter { $0.contains("feed.push") }.count
}
@@ -1202,7 +1205,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertGreaterThan(try feedPushCount(forTool: "fs_write"), 0,
"mutating kiro tool at standard level must still emit telemetry")
}
-
+ @Test
func testLowercaseGenericFeedToolsStayTelemetryOutsideKiro() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("generic-lowercase-feed-tool")
@@ -1270,7 +1273,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertEqual(event["_ppid"] as? Int, 626262)
XCTAssertEqual(waitTimeout.doubleValue, 0)
}
-
+ @Test
func testAntigravityFeedHookMissingSessionIdUsesStableFallback() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("antigravity-feed-stable-session")
@@ -1357,7 +1360,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
)
XCTAssertEqual(events.compactMap { $0["_ppid"] as? Int }, [424242, 424242, 424242])
}
-
+ @Test
func testGrokNotificationHookUsesPayloadMessageAndStopDoesNotSendGenericNotification() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("grok-notification")
@@ -1935,7 +1938,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Fallback notifications should preserve the saved needs-input status, saw \(neutralFallbackCommands)"
)
}
-
+ @Test
func testGrokStopFallbackCompletionsFireForTwoConcurrentThreads() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("grok-two-threads")
@@ -2114,7 +2117,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
)
}
}
-
+ @Test
func testGrokStopNotificationFallsBackWhenTranscriptCwdIsUnavailable() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("grok-stop-without-cwd")
@@ -2215,7 +2218,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Generic Grok completion after Stop fallback must not double-notify, saw \(duplicateCompletionCommands)"
)
}
-
+ @Test
func testGrokNotificationStillFiresOnRepeatedPromptWhenFeedTelemetryDoesNotReply() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("grok-repeat")
@@ -2313,7 +2316,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
)
}
}
-
+ @Test
func testGrokSessionEndDoesNotDropRoutingForLaterChatMessages() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("grok-turns")
@@ -2491,7 +2494,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Expected Grok route to remain available after multiple chat-message SessionEnd events"
)
}
-
+ @Test
func testGrokCompletionDoesNotResetStatusWhileSiblingSessionRuns() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("grok-sibling-status")
@@ -2630,7 +2633,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Completing Grok session must not reset the shared Grok status while a sibling session is running, saw \(completionCommands)"
)
}
-
+ @Test
func testGrokCompletionResetsStatusWhenSiblingRunningRecordHasDeadPID() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("grok-stale-sibling-status")
@@ -2774,7 +2777,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
}
return encoded
}
-
+ @Test
func testGrokHookInstallRoutesNotificationEventToNotificationSubcommand() throws {
let cliPath = try bundledCLIPath()
let root = FileManager.default.temporaryDirectory
@@ -2880,7 +2883,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Expected setup to remove legacy cmux-owned Grok hook file"
)
}
-
+ @Test
func testGrokHookInstallPinsInstallingCLIAndSocketWithoutCMUXInterpolation() throws {
let cliPath = try bundledCLIPath()
let root = FileManager.default.temporaryDirectory
@@ -2940,7 +2943,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Grok hook commands must not depend on CMUX environment interpolation, saw \(allCommands)"
)
}
-
+ @Test
func testGrokHookInstallPreservesUserWrappedLegacyCommands() throws {
let cliPath = try bundledCLIPath()
let root = FileManager.default.temporaryDirectory
@@ -3006,7 +3009,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Expected setup to remove only exact cmux-owned legacy commands, saw \(commands)"
)
}
-
+ @Test
func testGrokHookInstallPreservesLegacyFileMetadataWhenPruningOwnedHooks() throws {
let cliPath = try bundledCLIPath()
let root = FileManager.default.temporaryDirectory
@@ -3057,7 +3060,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertEqual(legacyJSON["version"] as? Int, 1)
XCTAssertNil(legacyJSON["hooks"])
}
-
+ @Test
func testCodexHookInstallPrefersLaunchingAppBundledCLI() throws {
let cliPath = try bundledCLIPath()
let root = FileManager.default.temporaryDirectory
@@ -3140,7 +3143,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Codex setup should collapse duplicate cmux-owned prompt hooks to one entry, saw \(allCommands)"
)
}
-
+ @Test
func testGrokHookInstallRejectsFileAtHooksDirectory() throws {
let cliPath = try bundledCLIPath()
let root = FileManager.default.temporaryDirectory
@@ -3294,6 +3297,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
/// drop CODEX_HOME, so the resume/fork binding fell back to a bare `codex resume ` against the
/// default home and failed with "No saved session found". The hook must still carry the captured
/// CODEX_HOME into the resume binding's environment.
+ @Test
func testCodexHookPreservesCodexHomeWhenLaunchCommandUnavailable() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex-home")
@@ -3427,6 +3431,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
/// When the agent process's controlling TTY is bound to a different, accessible surface in the same
/// workspace, that TTY is ground truth and must override the leaked env surface — otherwise the
/// session routes to the wrong pane and the no-pid-gate resume binding persists it across reload.
+ @Test
func testCodexHookOverridesLeakedEnvSurfaceWithProcessTTYBinding() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex-surface")
@@ -3522,6 +3527,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
/// longer resolves to an accessible surface. That must NOT abort hook routing — the agent's own
/// TTY-bound pane is valid, so the hook recovers and still publishes the resume binding there
/// instead of no-op'ing (which would silently lose the session across reload).
+ @Test
func testCodexHookRecoversFromStaleEnvSurfaceViaProcessTTYBinding() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex-stale")
@@ -3612,6 +3618,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
/// argv so they never get a resume/fork binding. The CODEX_HOME env-only fallback must NOT bypass
/// that — a captured-but-rejected argv keeps returning nil even when CODEX_HOME is present, so no
/// env-only record is persisted for the one-shot command.
+ @Test
func testCodexHookDoesNotPersistEnvOnlyRecordForNonRestorableExec() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("codex-exec")
diff --git a/cmuxTests/CLILegacyHookAliasTests.swift b/cmuxTests/CLILegacyHookAliasTests.swift
index 1b8ecc7463c6..9a10b0174d29 100644
--- a/cmuxTests/CLILegacyHookAliasTests.swift
+++ b/cmuxTests/CLILegacyHookAliasTests.swift
@@ -1,7 +1,9 @@
-import XCTest
import Darwin
+import Foundation
+import Testing
extension CLINotifyProcessIntegrationRegressionTests {
+ @Test
func testLegacyCodexHookAliasReturnsJSONWithoutHelpAndPersistsSession() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("legacy-codex")
@@ -66,7 +68,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertEqual(session["surfaceId"] as? String, surfaceId)
XCTAssertNotNil(session["launchCommand"] as? [String: Any])
}
-
+ @Test
func testLegacyFeedHookAliasReturnsJSONWithoutHelpOutsideCmuxTerminal() throws {
let cliPath = try bundledCLIPath()
var environment = ProcessInfo.processInfo.environment
diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
index 156a913529ad..de93c706755b 100644
--- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
+++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
@@ -7,115 +7,6 @@ import Testing
@testable import cmux
#endif
-private typealias XCTestExpectation = DispatchSemaphore
-
-private extension DispatchSemaphore {
- func fulfill() {
- signal()
- }
-}
-
-private func legacyFailure(_ message: String) {
- Issue.record(Comment(rawValue: message.isEmpty ? "Assertion failed" : message))
-}
-
-private func expectation(description _: String) -> XCTestExpectation {
- DispatchSemaphore(value: 0)
-}
-
-private func wait(for expectations: [XCTestExpectation], timeout: TimeInterval) {
- for expectation in expectations {
- let deadline = Date().addingTimeInterval(timeout)
- var fulfilled = false
- while Date() < deadline {
- if expectation.wait(timeout: .now()) == .success {
- fulfilled = true
- break
- }
- _ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01))
- }
- if !fulfilled, expectation.wait(timeout: .now()) == .timedOut {
- legacyFailure("Timed out waiting for expectation")
- }
- }
-}
-
-private func XCTFail(_ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
- legacyFailure(message())
-}
-
-private func XCTAssertTrue(_ expression: @autoclosure () throws -> Bool, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
- do {
- if try !expression() {
- legacyFailure(message())
- }
- } catch {
- legacyFailure("\(message()) \(error)")
- }
-}
-
-private func XCTAssertFalse(_ expression: @autoclosure () throws -> Bool, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
- do {
- if try expression() {
- legacyFailure(message())
- }
- } catch {
- legacyFailure("\(message()) \(error)")
- }
-}
-
-private func XCTAssertEqual(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
- do {
- let lhs = try expression1()
- let rhs = try expression2()
- if lhs != rhs {
- legacyFailure(message())
- }
- } catch {
- legacyFailure("\(message()) \(error)")
- }
-}
-
-private func XCTAssertNotEqual(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
- do {
- let lhs = try expression1()
- let rhs = try expression2()
- if lhs == rhs {
- legacyFailure(message())
- }
- } catch {
- legacyFailure("\(message()) \(error)")
- }
-}
-
-private func XCTAssertNil(_ expression: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
- do {
- if try expression() != nil {
- legacyFailure(message())
- }
- } catch {
- legacyFailure("\(message()) \(error)")
- }
-}
-
-private func XCTAssertNotNil(_ expression: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) {
- do {
- if try expression() == nil {
- legacyFailure(message())
- }
- } catch {
- legacyFailure("\(message()) \(error)")
- }
-}
-
-private func XCTUnwrap(_ expression: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = "", file _: StaticString = #filePath, line _: UInt = #line) throws -> T {
- guard let value = try expression() else {
- legacyFailure(message())
- throw NSError(domain: "CLINotifyProcessIntegrationRegressionTests", code: 1)
- }
- return value
-}
-
@Suite(.serialized)
final class CLINotifyProcessIntegrationRegressionTests {
private var teardownBlocks: [() -> Void] = []
diff --git a/cmuxTests/CLINotifyProcessTestSupport.swift b/cmuxTests/CLINotifyProcessTestSupport.swift
index 018ef47d2775..e7d583e91503 100644
--- a/cmuxTests/CLINotifyProcessTestSupport.swift
+++ b/cmuxTests/CLINotifyProcessTestSupport.swift
@@ -1,7 +1,278 @@
-import XCTest
import Darwin
+import Foundation
+import Testing
extension CLINotifyProcessIntegrationRegressionTests {
+ final class LegacyExpectation: @unchecked Sendable {
+ private let lock = NSLock()
+ private let semaphore = DispatchSemaphore(value: 0)
+ private var fulfilled = false
+ private let description: String
+
+ init(description: String) {
+ self.description = description
+ }
+
+ func fulfill() {
+ lock.lock()
+ let shouldSignal = !fulfilled
+ fulfilled = true
+ lock.unlock()
+
+ if shouldSignal {
+ semaphore.signal()
+ }
+ }
+
+ func pollFulfilled() -> Bool {
+ lock.lock()
+ if fulfilled {
+ lock.unlock()
+ return true
+ }
+ lock.unlock()
+
+ guard semaphore.wait(timeout: .now()) == .success else {
+ return false
+ }
+
+ lock.lock()
+ fulfilled = true
+ lock.unlock()
+ return true
+ }
+
+ var timeoutMessage: String {
+ "Timed out waiting for expectation: \(description)"
+ }
+ }
+
+ func expectation(description: String) -> LegacyExpectation {
+ LegacyExpectation(description: description)
+ }
+
+ func wait(
+ for expectations: [LegacyExpectation],
+ timeout: TimeInterval,
+ file: StaticString = #filePath,
+ line: UInt = #line
+ ) {
+ _ = waitForLegacyExpectations(
+ expectations,
+ timeout: timeout,
+ recordFailure: true,
+ file: file,
+ line: line
+ )
+ }
+
+ @discardableResult
+ func waitForLegacyExpectations(
+ _ expectations: [LegacyExpectation],
+ timeout: TimeInterval,
+ recordFailure: Bool,
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+ ) -> Bool {
+ var allFulfilled = true
+ for expectation in expectations {
+ let deadline = Date().addingTimeInterval(timeout)
+ while Date() < deadline {
+ if expectation.pollFulfilled() {
+ break
+ }
+ _ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01))
+ }
+
+ if !expectation.pollFulfilled() {
+ allFulfilled = false
+ if recordFailure {
+ legacyFailure(expectation.timeoutMessage)
+ }
+ }
+ }
+ return allFulfilled
+ }
+
+ func runLegacyActivity(named _: String, _ body: () throws -> T) rethrows -> T {
+ try body()
+ }
+
+ func legacyFailure(_ message: String) {
+ Issue.record(Comment(rawValue: message.isEmpty ? "Assertion failed" : message))
+ }
+
+ func XCTFail(
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+ ) {
+ legacyFailure(message())
+ }
+
+ func XCTAssertTrue(
+ _ expression: @autoclosure () throws -> Bool,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+ ) {
+ do {
+ if try !expression() {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+ }
+
+ func XCTAssertFalse(
+ _ expression: @autoclosure () throws -> Bool,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+ ) {
+ do {
+ if try expression() {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+ }
+
+ func XCTAssertEqual(
+ _ expression1: @autoclosure () throws -> T,
+ _ expression2: @autoclosure () throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+ ) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs != rhs {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+ }
+
+ func XCTAssertNotEqual(
+ _ expression1: @autoclosure () throws -> T,
+ _ expression2: @autoclosure () throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+ ) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs == rhs {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+ }
+
+ func XCTAssertGreaterThan(
+ _ expression1: @autoclosure () throws -> T,
+ _ expression2: @autoclosure () throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+ ) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs <= rhs {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+ }
+
+ func XCTAssertGreaterThanOrEqual(
+ _ expression1: @autoclosure () throws -> T,
+ _ expression2: @autoclosure () throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+ ) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs < rhs {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+ }
+
+ func XCTAssertLessThan(
+ _ expression1: @autoclosure () throws -> T,
+ _ expression2: @autoclosure () throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+ ) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs >= rhs {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+ }
+
+ func XCTAssertNil(
+ _ expression: @autoclosure () throws -> T?,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+ ) {
+ do {
+ if try expression() != nil {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+ }
+
+ func XCTAssertNotNil(
+ _ expression: @autoclosure () throws -> T?,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+ ) {
+ do {
+ if try expression() == nil {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
+ }
+ }
+
+ func XCTUnwrap(
+ _ expression: @autoclosure () throws -> T?,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+ ) throws -> T {
+ guard let value = try expression() else {
+ legacyFailure(message())
+ throw NSError(domain: "CLINotifyProcessIntegrationRegressionTests", code: 1)
+ }
+ return value
+ }
+
struct ProcessRunResult {
let status: Int32
let stdout: String
@@ -135,7 +406,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
return FileManager.default.fileExists(atPath: path)
}
- func startBridgeErrorServer(listenerFD: Int32, message: String) -> XCTestExpectation {
+ func startBridgeErrorServer(listenerFD: Int32, message: String) -> LegacyExpectation {
let handled = expectation(description: "pty bridge error server handled")
DispatchQueue.global(qos: .userInitiated).async {
defer { handled.fulfill() }
@@ -185,7 +456,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
return handled
}
- func startBridgeReadyThenCloseServer(listenerFD: Int32) -> XCTestExpectation {
+ func startBridgeReadyThenCloseServer(listenerFD: Int32) -> LegacyExpectation {
let handled = expectation(description: "pty bridge ready close server handled")
DispatchQueue.global(qos: .userInitiated).async {
defer { handled.fulfill() }
@@ -235,7 +506,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
return handled
}
- func startBridgeReadyThenResetAfterClientEOFServer(listenerFD: Int32) -> XCTestExpectation {
+ func startBridgeReadyThenResetAfterClientEOFServer(listenerFD: Int32) -> LegacyExpectation {
let handled = expectation(description: "pty bridge ready reset server handled")
DispatchQueue.global(qos: .userInitiated).async {
defer { handled.fulfill() }
@@ -313,7 +584,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
state: MockSocketServerState,
fulfillWhen: (@Sendable (String) -> Bool)? = nil,
handler: @escaping @Sendable (String) -> String
- ) -> XCTestExpectation {
+ ) -> LegacyExpectation {
startMockServerAllowingNoResponse(
listenerFD: listenerFD,
state: state,
@@ -328,7 +599,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
state: MockSocketServerState,
fulfillWhen: (@Sendable (String) -> Bool)? = nil,
handler: @escaping @Sendable (String) -> String?
- ) -> XCTestExpectation {
+ ) -> LegacyExpectation {
let handled = expectation(description: "cli mock socket handled")
DispatchQueue.global(qos: .userInitiated).async {
var didFulfill = false
diff --git a/cmuxTests/CLIRovoDevHookPersistenceTests.swift b/cmuxTests/CLIRovoDevHookPersistenceTests.swift
index 83d3912a493b..343f420a8c6f 100644
--- a/cmuxTests/CLIRovoDevHookPersistenceTests.swift
+++ b/cmuxTests/CLIRovoDevHookPersistenceTests.swift
@@ -1,7 +1,9 @@
-import XCTest
import Darwin
+import Foundation
+import Testing
extension CLINotifyProcessIntegrationRegressionTests {
+ @Test
func testRovoDevPromptSubmitInfersSessionIdFromWorkspaceMetadataAndPersistsLaunchCommand() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("rovo-infer")
@@ -94,7 +96,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Expected Rovo Dev prompt status to target current workspace, saw \(state.commands)"
)
}
-
+ @Test
func testRovoDevPromptSubmitInfersNewestMatchingWorkspaceMetadata() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("rovo-newest")
@@ -181,7 +183,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertNil(sessions["rovo-older-session"] as? [String: Any])
XCTAssertNotNil(sessions["rovo-newer-session"] as? [String: Any])
}
-
+ @Test
func testRovoDevPromptSubmitReadsConfiguredPersistenceDirWithCommentsHashAndApostrophePath() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("rovo-config")
@@ -278,7 +280,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
let sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
XCTAssertNotNil(sessions[sessionId] as? [String: Any])
}
-
+ @Test
func testRovoDevPromptSubmitWithoutCwdDoesNotInferUnrelatedSession() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("rovo-nocwd")
@@ -350,7 +352,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertNil(sessions[unrelatedSessionId] as? [String: Any])
}
}
-
+ @Test
func testRovoDevInstallDoesNotReplaceUnreadableConfigPath() throws {
let cliPath = try bundledCLIPath()
let root = FileManager.default.temporaryDirectory
@@ -380,7 +382,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertTrue(result.stderr.contains("could not be read"), result.stderr)
XCTAssertTrue(FileManager.default.fileExists(atPath: sentinelURL.path))
}
-
+ @Test
func testRovoAliasCreatesConfigDirAndInstallsRovoDevHooksFromSetup() throws {
let cliPath = try bundledCLIPath()
let root = FileManager.default.temporaryDirectory
@@ -417,7 +419,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertTrue(config.contains(#"hooks rovodev prompt-submit"#), config)
XCTAssertTrue(config.contains(#"CMUX_BUNDLED_CLI_PATH"#), config)
}
-
+ @Test
func testSetupHooksRejectsConflictingAgentFilters() throws {
let cliPath = try bundledCLIPath()
let root = FileManager.default.temporaryDirectory
@@ -440,7 +442,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertNotEqual(result.status, 0)
XCTAssertTrue(result.stderr.contains("Conflicting hooks target"), result.stderr)
}
-
+ @Test
func testSetupHooksRejectsMultiplePositionalTargets() throws {
let cliPath = try bundledCLIPath()
let root = FileManager.default.temporaryDirectory
diff --git a/cmuxTests/CLISSHPTYAttachProbeReplyRegressionTests.swift b/cmuxTests/CLISSHPTYAttachProbeReplyRegressionTests.swift
index 38a784c2c07a..fde55a613c91 100644
--- a/cmuxTests/CLISSHPTYAttachProbeReplyRegressionTests.swift
+++ b/cmuxTests/CLISSHPTYAttachProbeReplyRegressionTests.swift
@@ -3,6 +3,7 @@ import Foundation
import Testing
extension CLINotifyProcessIntegrationRegressionTests {
+ @Test
func testSSHPTYAttachPreservesPipedProbeLikeInputBeforeForwardingInput() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("sshptyprobe")
diff --git a/cmuxTests/SSHStartupSignalLifecycleTests.swift b/cmuxTests/SSHStartupSignalLifecycleTests.swift
index 6c825ecd62c3..d9ce94c1bf02 100644
--- a/cmuxTests/SSHStartupSignalLifecycleTests.swift
+++ b/cmuxTests/SSHStartupSignalLifecycleTests.swift
@@ -1,7 +1,9 @@
-import XCTest
import Darwin
+import Foundation
+import Testing
extension CLINotifyProcessIntegrationRegressionTests {
+ @Test
func testSSHPaneCloseSignalDoesNotReportSessionEndToSharedTransport() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
@@ -64,7 +66,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
)
}
}
-
+ @Test
func testSSHPaneCloseSignalDoesNotTerminateWrappedSSHChild() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
@@ -149,7 +151,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertTrue(sessionEndCalls.isEmpty, recordedCalls)
}
}
-
+ @Test
func testSSHStartupRetriesTransientSSHExitBeforeReportingSessionEnd() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
@@ -209,7 +211,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
.filter { $0.contains("ssh-session-end") }
XCTAssertEqual(sessionEndCalls.count, 1, recordedCalls)
}
-
+ @Test
func testSSHStartupRemovesStaleCmuxControlSocketBeforeLaunchingPaneSSH() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
@@ -290,7 +292,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertTrue(sshLog.contains("-G"), sshLog)
XCTAssertTrue(sshLog.contains("-O check"), sshLog)
}
-
+ @Test
func testSSHStartupStopsAtConfiguredReconnectLimit() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
@@ -346,7 +348,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
.filter { $0.contains("ssh-session-end") }
XCTAssertEqual(sessionEndCalls.count, 1, recordedCalls)
}
-
+ @Test
func testSSHStartupDoesNotRetryNonTransientSSHExit() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
@@ -401,7 +403,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
.filter { $0.contains("ssh-session-end") }
XCTAssertEqual(sessionEndCalls.count, 1, recordedCalls)
}
-
+ @Test
func testSSHSignalDerivedChildExitReportsSessionEnd() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
@@ -449,7 +451,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
.filter { $0.contains("ssh-session-end") }
XCTAssertEqual(sessionEndCalls.count, 1, recordedCalls)
}
-
+ @Test
func testSSHSignalDuringReconnectDelayDoesNotStartAnotherSSH() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
@@ -509,7 +511,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
.filter { $0.contains("ssh-session-end") }
XCTAssertEqual(sessionEndCalls.count, 1, recordedCalls)
}
-
+ @Test
func testSSHStartupPrintsFinalErrorBannerWhenStderrIsCaptured() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
@@ -554,7 +556,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertTrue(result.stderr.contains("[cmux] ssh exited with status 1."), result.stderr)
XCTAssertTrue(result.stderr.contains("[cmux] press Enter to close this pane."), result.stderr)
}
-
+ @Test
func testSSHStartupForwardsStdinToBackgroundedSSH() throws {
// Regression test for cmux ssh sessions where output flowed back from
// the remote (prompt rendered) but typed keystrokes never reached the
diff --git a/cmuxTests/VMSSHCommandTests.swift b/cmuxTests/VMSSHCommandTests.swift
index b4991cf87844..ef7ed90816f6 100644
--- a/cmuxTests/VMSSHCommandTests.swift
+++ b/cmuxTests/VMSSHCommandTests.swift
@@ -1,7 +1,9 @@
-import XCTest
import Darwin
+import Foundation
+import Testing
extension CLINotifyProcessIntegrationRegressionTests {
+ @Test
func testVMSSHOpensManagedWorkspaceThroughSharedSSHPath() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("vm-ssh")
@@ -120,7 +122,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertNotNil(configureParams["terminal_startup_command"] as? String)
XCTAssertNotNil(configureParams["relay_port"] as? Int)
}
-
+ @Test
func testSSHCommandGlobalWindowOverridesCallerEnvironment() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("ssh-global-window")
@@ -218,7 +220,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
["window.focus", "workspace.create", "workspace.remote.configure"]
)
}
-
+ @Test
func testVMSSHInfoRemainsPrintOnly() throws {
let cliPath = try bundledCLIPath()
let socketPath = makeSocketPath("vm-info")
From cb1ea2d611f3e2707eedf850bce08300c2727466 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 22 Jun 2026 10:01:35 -0700
Subject: [PATCH 39/39] test: avoid suite captures in migrated hook assertions
---
.github/swift-file-length-budget.tsv | 2 +-
cmuxTests/CLIAuthAliasTests.swift | 20 +-
.../CLIGenericHookPersistenceTests.swift | 1026 +++----
cmuxTests/CLILegacyHookAliasTests.swift | 34 +-
...ifyProcessIntegrationRegressionTests.swift | 2420 ++++++++---------
cmuxTests/CLINotifyProcessTestSupport.swift | 460 ++--
.../CLIRovoDevHookPersistenceTests.swift | 104 +-
...SHPTYAttachProbeReplyRegressionTests.swift | 2 +-
.../SSHStartupSignalLifecycleTests.swift | 126 +-
cmuxTests/VMSSHCommandTests.swift | 76 +-
10 files changed, 2136 insertions(+), 2134 deletions(-)
diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv
index 21ba04837eb7..d33ce84990e8 100644
--- a/.github/swift-file-length-budget.tsv
+++ b/.github/swift-file-length-budget.tsv
@@ -112,7 +112,7 @@
824 Sources/MainWindowFocusController.swift
810 Packages/macOS/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/SwiftViewInterpreterTests.swift
802 Sources/WorkspaceContentView.swift
-799 cmuxTests/CLINotifyProcessTestSupport.swift
+801 cmuxTests/CLINotifyProcessTestSupport.swift
797 Sources/ClosedItemHistory.swift
779 cmuxUITests/BrowserOmnibarSuggestionsUITests.swift
769 Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+Input.swift
diff --git a/cmuxTests/CLIAuthAliasTests.swift b/cmuxTests/CLIAuthAliasTests.swift
index 8dc1c0d3b3e2..8b5d7001a2a8 100644
--- a/cmuxTests/CLIAuthAliasTests.swift
+++ b/cmuxTests/CLIAuthAliasTests.swift
@@ -54,11 +54,11 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "Opening sign-in popup on the cmux web app.\nSigned in as dev@example.com.\n")
- XCTAssertTrue(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "Opening sign-in popup on the cmux web app.\nSigned in as dev@example.com.\n")
+ legacyAssertTrue(
state.commands.contains { $0.contains(#""method":"auth.begin_sign_in""#) },
"Expected login alias to call auth.begin_sign_in, saw \(state.commands)"
)
@@ -114,11 +114,11 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "Signed out.\n")
- XCTAssertTrue(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "Signed out.\n")
+ legacyAssertTrue(
state.commands.contains { $0.contains(#""method":"auth.sign_out""#) },
"Expected logout alias to call auth.sign_out, saw \(state.commands)"
)
diff --git a/cmuxTests/CLIGenericHookPersistenceTests.swift b/cmuxTests/CLIGenericHookPersistenceTests.swift
index 110df2cf784d..67611443d81c 100644
--- a/cmuxTests/CLIGenericHookPersistenceTests.swift
+++ b/cmuxTests/CLIGenericHookPersistenceTests.swift
@@ -339,7 +339,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: input,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
@@ -347,9 +347,9 @@ extension CLINotifyProcessIntegrationRegressionTests {
"session-start",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"SessionStart"}"#
)
- XCTAssertFalse(start.timedOut, start.stderr)
- XCTAssertEqual(start.status, 0, start.stderr)
- XCTAssertEqual(start.stdout, "{}\n")
+ legacyAssertFalse(start.timedOut, start.stderr)
+ legacyAssertEqual(start.status, 0, start.stderr)
+ legacyAssertEqual(start.stdout, "{}\n")
let backgroundMessage = "Antigravity is waiting on background work"
let backgroundStopCommandStart = state.commands.count
@@ -357,20 +357,20 @@ extension CLINotifyProcessIntegrationRegressionTests {
"stop",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"Stop","last_assistant_message":"\#(backgroundMessage)","fullyIdle":false}"#
)
- XCTAssertFalse(backgroundStop.timedOut, backgroundStop.stderr)
- XCTAssertEqual(backgroundStop.status, 0, backgroundStop.stderr)
- XCTAssertEqual(backgroundStop.stdout, "{}\n")
+ legacyAssertFalse(backgroundStop.timedOut, backgroundStop.stderr)
+ legacyAssertEqual(backgroundStop.status, 0, backgroundStop.stderr)
+ legacyAssertEqual(backgroundStop.stdout, "{}\n")
let backgroundStopCommands = Array(state.commands.dropFirst(backgroundStopCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
backgroundStopCommands.contains { $0.hasPrefix("notify_target_async ") },
"Antigravity Stop with active background work must not publish idle notifications, saw \(backgroundStopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
backgroundStopCommands.contains { $0.contains("set_status antigravity Running") },
"Antigravity Stop with active background work should keep the session running, saw \(backgroundStopCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
backgroundStopCommands.contains { $0.contains("set_status antigravity Idle") },
"Antigravity Stop with active background work must not mark idle, saw \(backgroundStopCommands)"
)
@@ -380,16 +380,16 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"Notification","message":"Turn complete in 1.0s.","fullyIdle":false}"#
)
- XCTAssertFalse(backgroundDuplicate.timedOut, backgroundDuplicate.stderr)
- XCTAssertEqual(backgroundDuplicate.status, 0, backgroundDuplicate.stderr)
- XCTAssertEqual(backgroundDuplicate.stdout, "{}\n")
+ legacyAssertFalse(backgroundDuplicate.timedOut, backgroundDuplicate.stderr)
+ legacyAssertEqual(backgroundDuplicate.status, 0, backgroundDuplicate.stderr)
+ legacyAssertEqual(backgroundDuplicate.stdout, "{}\n")
let backgroundDuplicateCommands = Array(state.commands.dropFirst(backgroundDuplicateCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
backgroundDuplicateCommands.contains { $0.hasPrefix("notify_target_async ") },
"Idle-classified Antigravity notifications must not double-notify while background work is active, saw \(backgroundDuplicateCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
backgroundDuplicateCommands.contains { $0.contains("set_status antigravity Idle") },
"Idle-classified Antigravity notifications must not override the running status while background work is active, saw \(backgroundDuplicateCommands)"
)
@@ -399,33 +399,33 @@ extension CLINotifyProcessIntegrationRegressionTests {
"session-start",
input: #"{"session_id":"\#(missingFullyIdleSessionId)","cwd":"\#(root.path)","hook_event_name":"SessionStart"}"#
)
- XCTAssertFalse(missingFullyIdleStart.timedOut, missingFullyIdleStart.stderr)
- XCTAssertEqual(missingFullyIdleStart.status, 0, missingFullyIdleStart.stderr)
- XCTAssertEqual(missingFullyIdleStart.stdout, "{}\n")
+ legacyAssertFalse(missingFullyIdleStart.timedOut, missingFullyIdleStart.stderr)
+ legacyAssertEqual(missingFullyIdleStart.status, 0, missingFullyIdleStart.stderr)
+ legacyAssertEqual(missingFullyIdleStart.stdout, "{}\n")
let missingFullyIdleBackgroundStop = runAntigravityHook(
"stop",
input: #"{"session_id":"\#(missingFullyIdleSessionId)","cwd":"\#(root.path)","hook_event_name":"Stop","last_assistant_message":"Background work still running","fullyIdle":false}"#
)
- XCTAssertFalse(missingFullyIdleBackgroundStop.timedOut, missingFullyIdleBackgroundStop.stderr)
- XCTAssertEqual(missingFullyIdleBackgroundStop.status, 0, missingFullyIdleBackgroundStop.stderr)
- XCTAssertEqual(missingFullyIdleBackgroundStop.stdout, "{}\n")
+ legacyAssertFalse(missingFullyIdleBackgroundStop.timedOut, missingFullyIdleBackgroundStop.stderr)
+ legacyAssertEqual(missingFullyIdleBackgroundStop.status, 0, missingFullyIdleBackgroundStop.stderr)
+ legacyAssertEqual(missingFullyIdleBackgroundStop.stdout, "{}\n")
let missingFullyIdleNotificationCommandStart = state.commands.count
let missingFullyIdleNotification = runAntigravityHook(
"notification",
input: #"{"session_id":"\#(missingFullyIdleSessionId)","cwd":"\#(root.path)","hook_event_name":"Notification","message":"Turn complete in 2.0s."}"#
)
- XCTAssertFalse(missingFullyIdleNotification.timedOut, missingFullyIdleNotification.stderr)
- XCTAssertEqual(missingFullyIdleNotification.status, 0, missingFullyIdleNotification.stderr)
- XCTAssertEqual(missingFullyIdleNotification.stdout, "{}\n")
+ legacyAssertFalse(missingFullyIdleNotification.timedOut, missingFullyIdleNotification.stderr)
+ legacyAssertEqual(missingFullyIdleNotification.status, 0, missingFullyIdleNotification.stderr)
+ legacyAssertEqual(missingFullyIdleNotification.stdout, "{}\n")
let missingFullyIdleNotificationCommands = Array(state.commands.dropFirst(missingFullyIdleNotificationCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
missingFullyIdleNotificationCommands.contains { $0.hasPrefix("notify_target_async ") },
"Antigravity idle notifications without fullyIdle must publish instead of staying suppressed, saw \(missingFullyIdleNotificationCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
missingFullyIdleNotificationCommands.contains { $0.contains("set_status antigravity Idle") },
"Antigravity idle notifications must not reset the shared status while another background session is running, saw \(missingFullyIdleNotificationCommands)"
)
@@ -436,19 +436,19 @@ extension CLINotifyProcessIntegrationRegressionTests {
"stop",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"AfterAgent","last_assistant_message":"\#(stopMessage)"}"#
)
- XCTAssertFalse(stop.timedOut, stop.stderr)
- XCTAssertEqual(stop.status, 0, stop.stderr)
- XCTAssertEqual(stop.stdout, "{}\n")
+ legacyAssertFalse(stop.timedOut, stop.stderr)
+ legacyAssertEqual(stop.status, 0, stop.stderr)
+ legacyAssertEqual(stop.stdout, "{}\n")
let stopCommands = Array(state.commands.dropFirst(stopCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
stopCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Antigravity|Completed in ")
&& $0.contains(stopMessage)
},
"Expected Antigravity stop to publish a turn-completion notification, saw \(stopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
stopCommands.contains { $0.contains("set_status antigravity Idle") },
"Expected Antigravity stop to leave the session idle, saw \(stopCommands)"
)
@@ -458,16 +458,16 @@ extension CLINotifyProcessIntegrationRegressionTests {
"session-end",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"SessionEnd"}"#
)
- XCTAssertFalse(sessionEnd.timedOut, sessionEnd.stderr)
- XCTAssertEqual(sessionEnd.status, 0, sessionEnd.stderr)
- XCTAssertEqual(sessionEnd.stdout, "{}\n")
+ legacyAssertFalse(sessionEnd.timedOut, sessionEnd.stderr)
+ legacyAssertEqual(sessionEnd.status, 0, sessionEnd.stderr)
+ legacyAssertEqual(sessionEnd.stdout, "{}\n")
let sessionEndCommands = Array(state.commands.dropFirst(sessionEndCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
sessionEndCommands.contains { $0.contains("feed.push") },
"Expected Antigravity SessionEnd to emit feed telemetry, saw \(sessionEndCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
sessionEndCommands.contains { $0.hasPrefix("clear_agent_pid antigravity.") },
"Antigravity SessionEnd is a turn boundary and must not clear saved routing, saw \(sessionEndCommands)"
)
@@ -477,12 +477,12 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"Notification","message":"Turn complete in 2.0s."}"#
)
- XCTAssertFalse(duplicateCompletion.timedOut, duplicateCompletion.stderr)
- XCTAssertEqual(duplicateCompletion.status, 0, duplicateCompletion.stderr)
- XCTAssertEqual(duplicateCompletion.stdout, "{}\n")
+ legacyAssertFalse(duplicateCompletion.timedOut, duplicateCompletion.stderr)
+ legacyAssertEqual(duplicateCompletion.status, 0, duplicateCompletion.stderr)
+ legacyAssertEqual(duplicateCompletion.stdout, "{}\n")
let duplicateCompletionCommands = Array(state.commands.dropFirst(duplicateCompletionCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
duplicateCompletionCommands.contains { $0.hasPrefix("notify_target_async ") },
"Antigravity turn-completion notification must not double-notify after stop already did, saw \(duplicateCompletionCommands)"
)
@@ -493,18 +493,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"Notification","reason":"permission_prompt","message":"\#(permissionMessage)"}"#
)
- XCTAssertFalse(permission.timedOut, permission.stderr)
- XCTAssertEqual(permission.status, 0, permission.stderr)
- XCTAssertEqual(permission.stdout, "{}\n")
+ legacyAssertFalse(permission.timedOut, permission.stderr)
+ legacyAssertEqual(permission.status, 0, permission.stderr)
+ legacyAssertEqual(permission.stdout, "{}\n")
let permissionCommands = Array(state.commands.dropFirst(permissionCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
permissionCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Antigravity|Permission|\(permissionMessage)")
},
"Expected Antigravity permission notifications to publish through cmux, saw \(permissionCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
permissionCommands.contains { $0.contains("set_status antigravity Antigravity needs input") },
"Expected Antigravity permission notifications to mark needs-input, saw \(permissionCommands)"
)
@@ -515,18 +515,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
"stop",
input: #"{"conversationId":"\#(sessionId)","workspacePaths":["\#(root.path)"],"hook_event_name":"Stop","terminationReason":"error","error":"\#(stopErrorMessage)","fullyIdle":true}"#
)
- XCTAssertFalse(stopError.timedOut, stopError.stderr)
- XCTAssertEqual(stopError.status, 0, stopError.stderr)
- XCTAssertEqual(stopError.stdout, "{}\n")
+ legacyAssertFalse(stopError.timedOut, stopError.stderr)
+ legacyAssertEqual(stopError.status, 0, stopError.stderr)
+ legacyAssertEqual(stopError.stdout, "{}\n")
let stopErrorCommands = Array(state.commands.dropFirst(stopErrorCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
stopErrorCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Antigravity|Error|\(stopErrorMessage)")
},
"Expected Antigravity Stop errors to publish through cmux, saw \(stopErrorCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
stopErrorCommands.contains { $0.contains("set_status antigravity Antigravity error") },
"Expected Antigravity Stop errors to mark error status, saw \(stopErrorCommands)"
)
@@ -537,18 +537,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"Notification","message":"\#(errorMessage)"}"#
)
- XCTAssertFalse(error.timedOut, error.stderr)
- XCTAssertEqual(error.status, 0, error.stderr)
- XCTAssertEqual(error.stdout, "{}\n")
+ legacyAssertFalse(error.timedOut, error.stderr)
+ legacyAssertEqual(error.status, 0, error.stderr)
+ legacyAssertEqual(error.stdout, "{}\n")
let errorCommands = Array(state.commands.dropFirst(errorCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
errorCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Antigravity|Error|\(errorMessage)")
},
"Expected Antigravity error notifications to publish through cmux, saw \(errorCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
errorCommands.contains { $0.contains("set_status antigravity Antigravity error") },
"Expected Antigravity error notifications to mark error status, saw \(errorCommands)"
)
@@ -607,24 +607,24 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: input,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
func storedHermesSession() throws -> [String: Any] {
let storeURL = root.appendingPathComponent("hermes-agent-hook-sessions.json", isDirectory: false)
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- let sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
- return try XCTUnwrap(sessions[sessionId] as? [String: Any])
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ let sessions = try legacyUnwrap(json["sessions"] as? [String: Any])
+ return try legacyUnwrap(sessions[sessionId] as? [String: Any])
}
let start = runHermesHook(
"session-start",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"on_session_start"}"#
)
- XCTAssertFalse(start.timedOut, start.stderr)
- XCTAssertEqual(start.status, 0, start.stderr)
- XCTAssertEqual(start.stdout, "{}\n")
+ legacyAssertFalse(start.timedOut, start.stderr)
+ legacyAssertEqual(start.status, 0, start.stderr)
+ legacyAssertEqual(start.stdout, "{}\n")
let assistantResponse = "Updated README.md and added usage notes."
let stopCommandStart = state.commands.count
@@ -632,19 +632,19 @@ extension CLINotifyProcessIntegrationRegressionTests {
"agent-response",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"post_llm_call","extra":{"user_message":"make the docs clearer","assistant_response":"\#(assistantResponse)","model":"gpt-4","platform":"cli"}}"#
)
- XCTAssertFalse(stop.timedOut, stop.stderr)
- XCTAssertEqual(stop.status, 0, stop.stderr)
- XCTAssertEqual(stop.stdout, "{}\n")
+ legacyAssertFalse(stop.timedOut, stop.stderr)
+ legacyAssertEqual(stop.status, 0, stop.stderr)
+ legacyAssertEqual(stop.stdout, "{}\n")
let stopCommands = Array(state.commands.dropFirst(stopCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
stopCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Hermes Agent|Completed in ")
&& $0.contains("|\(assistantResponse)")
},
"Expected Hermes completion notification to use extra.assistant_response, saw \(stopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
stopCommands.contains { $0.contains("set_status hermes-agent Idle") },
"Expected Hermes completion to leave status idle, saw \(stopCommands)"
)
@@ -654,59 +654,59 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"pre_approval_request","extra":{"command":"rm -rf build","description":"recursive delete","pattern_key":"recursive delete","surface":"cli"}}"#
)
- XCTAssertFalse(approval.timedOut, approval.stderr)
- XCTAssertEqual(approval.status, 0, approval.stderr)
- XCTAssertEqual(approval.stdout, "{}\n")
+ legacyAssertFalse(approval.timedOut, approval.stderr)
+ legacyAssertEqual(approval.status, 0, approval.stderr)
+ legacyAssertEqual(approval.stdout, "{}\n")
let approvalCommands = Array(state.commands.dropFirst(approvalCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
approvalCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Hermes Agent|Permission|recursive delete: rm -rf build")
},
"Expected Hermes approval notification to include description and command, saw \(approvalCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
approvalCommands.contains { $0.contains("set_status hermes-agent Hermes Agent needs input") },
"Expected Hermes approval notification to mark needs input, saw \(approvalCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
approvalCommands.contains { $0.contains(#""method":"feed.push""#) },
"Hermes approval notifications are also installed as feed hooks, so the generic notification handler must not push duplicate feed events. Saw \(approvalCommands)"
)
let session = try storedHermesSession()
- XCTAssertEqual(session["lastSubtitle"] as? String, "Permission")
- XCTAssertEqual(session["lastBody"] as? String, "recursive delete: rm -rf build")
- XCTAssertEqual(session["lastNotificationStatus"] as? String, "needsInput")
+ legacyAssertEqual(session["lastSubtitle"] as? String, "Permission")
+ legacyAssertEqual(session["lastBody"] as? String, "recursive delete: rm -rf build")
+ legacyAssertEqual(session["lastNotificationStatus"] as? String, "needsInput")
let responseCommandStart = state.commands.count
let response = runHermesHook(
"approval-response",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"post_approval_response","extra":{"approved":true}}"#
)
- XCTAssertFalse(response.timedOut, response.stderr)
- XCTAssertEqual(response.status, 0, response.stderr)
- XCTAssertEqual(response.stdout, "{}\n")
+ legacyAssertFalse(response.timedOut, response.stderr)
+ legacyAssertEqual(response.status, 0, response.stderr)
+ legacyAssertEqual(response.stdout, "{}\n")
let responseCommands = Array(state.commands.dropFirst(responseCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
responseCommands.contains { $0.contains("clear_notifications --tab=\(workspaceId) --panel=\(surfaceId)") },
"Expected Hermes approval response to clear the approval notification, saw \(responseCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
responseCommands.contains { $0.contains("set_status hermes-agent Running") },
"Expected Hermes approval response to restore running status, saw \(responseCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
responseCommands.contains { $0.contains(#""method":"feed.push""#) },
"Hermes approval responses are also installed as feed hooks, so the generic approval handler must not push duplicate feed events. Saw \(responseCommands)"
)
let responseSession = try storedHermesSession()
- XCTAssertNil(responseSession["lastSubtitle"])
- XCTAssertNil(responseSession["lastBody"])
- XCTAssertNil(responseSession["lastNotificationStatus"])
- XCTAssertEqual(responseSession["runtimeStatus"] as? String, "running")
+ legacyAssertNil(responseSession["lastSubtitle"])
+ legacyAssertNil(responseSession["lastBody"])
+ legacyAssertNil(responseSession["lastNotificationStatus"])
+ legacyAssertEqual(responseSession["runtimeStatus"] as? String, "running")
}
@Test
func testHermesAgentSessionEndIsTurnBoundaryButFinalizeTearsDown() throws {
@@ -772,7 +772,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: input,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
@@ -791,18 +791,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
"session-start",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"on_session_start"}"#
)
- XCTAssertFalse(start.timedOut, start.stderr)
- XCTAssertEqual(start.status, 0, start.stderr)
+ legacyAssertFalse(start.timedOut, start.stderr)
+ legacyAssertEqual(start.status, 0, start.stderr)
// Finish a turn so a restorable record exists for the session.
let stop = runHermesHook(
"agent-response",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"post_llm_call","extra":{"user_message":"do the thing","assistant_response":"done","model":"gpt-4","platform":"cli"}}"#
)
- XCTAssertFalse(stop.timedOut, stop.stderr)
- XCTAssertEqual(stop.status, 0, stop.stderr)
+ legacyAssertFalse(stop.timedOut, stop.stderr)
+ legacyAssertEqual(stop.status, 0, stop.stderr)
- XCTAssertNotNil(
+ legacyAssertNotNil(
try storedHermesSessionIfPresent(),
"Expected a Hermes session record to exist before the per-turn session-end hook fires"
)
@@ -814,24 +814,24 @@ extension CLINotifyProcessIntegrationRegressionTests {
"session-end",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"on_session_end"}"#
)
- XCTAssertFalse(sessionEnd.timedOut, sessionEnd.stderr)
- XCTAssertEqual(sessionEnd.status, 0, sessionEnd.stderr)
- XCTAssertEqual(sessionEnd.stdout, "{}\n")
+ legacyAssertFalse(sessionEnd.timedOut, sessionEnd.stderr)
+ legacyAssertEqual(sessionEnd.status, 0, sessionEnd.stderr)
+ legacyAssertEqual(sessionEnd.stdout, "{}\n")
let sessionEndCommands = Array(state.commands.dropFirst(sessionEndCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
sessionEndCommands.contains { $0.contains("feed.push") },
"Expected Hermes session-end to emit feed telemetry, saw \(sessionEndCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
sessionEndCommands.contains { $0.hasPrefix("clear_agent_pid hermes-agent.") },
"Hermes on_session_end fires per turn and must not clear saved routing, saw \(sessionEndCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
sessionEndCommands.contains { $0.contains("surface.resume.clear") },
"Hermes on_session_end fires per turn and must not clear the surface resume binding, saw \(sessionEndCommands)"
)
- XCTAssertNotNil(
+ legacyAssertNotNil(
try storedHermesSessionIfPresent(),
"Hermes on_session_end fires per turn and must not consume the restore record, saw it removed from the store"
)
@@ -845,20 +845,20 @@ extension CLINotifyProcessIntegrationRegressionTests {
"session-finalize",
input: #"{"session_id":"\#(sessionId)","cwd":"\#(root.path)","hook_event_name":"on_session_finalize"}"#
)
- XCTAssertFalse(finalize.timedOut, finalize.stderr)
- XCTAssertEqual(finalize.status, 0, finalize.stderr)
- XCTAssertEqual(finalize.stdout, "{}\n")
+ legacyAssertFalse(finalize.timedOut, finalize.stderr)
+ legacyAssertEqual(finalize.status, 0, finalize.stderr)
+ legacyAssertEqual(finalize.stdout, "{}\n")
let finalizeCommands = Array(state.commands.dropFirst(finalizeCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
finalizeCommands.contains { $0.hasPrefix("clear_agent_pid hermes-agent.") },
"Hermes on_session_finalize is a true teardown and must clear agent PID routing, saw \(finalizeCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
finalizeCommands.contains { $0.contains("surface.resume.clear") },
"Hermes on_session_finalize is a true teardown and must clear the surface resume binding, saw \(finalizeCommands)"
)
- XCTAssertNil(
+ legacyAssertNil(
try storedHermesSessionIfPresent(),
"Hermes on_session_finalize is a true teardown and must consume the restore record"
)
@@ -883,17 +883,17 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let hookURL = root
.appendingPathComponent(".gemini", isDirectory: true)
.appendingPathComponent("config", isDirectory: true)
.appendingPathComponent("hooks.json", isDirectory: false)
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: hookURL)) as? [String: Any])
- XCTAssertNil(json["hooks"])
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: hookURL)) as? [String: Any])
+ legacyAssertNil(json["hooks"])
- let cmuxGroup = try XCTUnwrap(json["cmux"] as? [String: Any])
+ let cmuxGroup = try legacyUnwrap(json["cmux"] as? [String: Any])
let allCommands = cmuxGroup.values
.compactMap { $0 as? [[String: Any]] }
.flatMap { entries in
@@ -908,25 +908,25 @@ extension CLINotifyProcessIntegrationRegressionTests {
return commands
}
}
- XCTAssertFalse(allCommands.isEmpty)
- XCTAssertTrue(
+ legacyAssertFalse(allCommands.isEmpty)
+ legacyAssertTrue(
allCommands.allSatisfy { $0.contains("cmux-antigravity-hook-v2") },
"Expected Antigravity hooks to use the pinned dispatch path, saw \(allCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
allCommands.contains { $0.contains("'\(root.path)'") || $0.contains("\"\(root.path)\"") },
"Directory-valued CMUX_BUNDLED_CLI_PATH must not be embedded as a hook executable, saw \(allCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
allCommands.contains { $0.contains(#"[ -n "$CMUX_SURFACE_ID" ]"#) },
"Antigravity hooks must still dispatch when agy does not preserve CMUX_SURFACE_ID, saw \(allCommands)"
)
- let preToolUse = try XCTUnwrap(cmuxGroup["PreToolUse"] as? [[String: Any]])
+ let preToolUse = try legacyUnwrap(cmuxGroup["PreToolUse"] as? [[String: Any]])
let preToolCommands = preToolUse
.compactMap { $0["hooks"] as? [[String: Any]] }
.flatMap { $0 }
- XCTAssertTrue(
+ legacyAssertTrue(
preToolCommands.contains {
($0["command"] as? String)?.contains("hooks feed --source antigravity --event PreToolUse") == true
&& ($0["timeout"] as? Int) == 120
@@ -934,19 +934,19 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Expected Antigravity PreToolUse feed hook with second-based timeout, saw \(preToolCommands)"
)
- let stop = try XCTUnwrap(cmuxGroup["Stop"] as? [[String: Any]])
- XCTAssertTrue(
+ let stop = try legacyUnwrap(cmuxGroup["Stop"] as? [[String: Any]])
+ legacyAssertTrue(
stop.contains {
($0["command"] as? String)?.contains("hooks antigravity stop") == true
&& ($0["timeout"] as? Int) == 10
},
"Expected Antigravity Stop hook to be a direct command handler, saw \(stop)"
)
- XCTAssertNotNil(cmuxGroup["SessionStart"])
- XCTAssertNotNil(cmuxGroup["SessionEnd"])
- XCTAssertNotNil(cmuxGroup["turn-completion"])
- XCTAssertNotNil(cmuxGroup["Notification"])
- XCTAssertNotNil(cmuxGroup["PostToolUse"])
+ legacyAssertNotNil(cmuxGroup["SessionStart"])
+ legacyAssertNotNil(cmuxGroup["SessionEnd"])
+ legacyAssertNotNil(cmuxGroup["turn-completion"])
+ legacyAssertNotNil(cmuxGroup["Notification"])
+ legacyAssertNotNil(cmuxGroup["PostToolUse"])
}
@Test
func testKiroHookInstallUsesAgentConfigShapeAndPreservesDenyExit() throws {
@@ -968,9 +968,9 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(
result.stdout.contains("kiro-cli chat --agent cmux"),
"Expected Kiro install to print the --agent cmux activation hint, saw: \(result.stdout)"
)
@@ -978,17 +978,17 @@ extension CLINotifyProcessIntegrationRegressionTests {
let hookURL = root
.appendingPathComponent("agents", isDirectory: true)
.appendingPathComponent("cmux.json", isDirectory: false)
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: hookURL)) as? [String: Any])
- XCTAssertEqual(json["name"] as? String, "cmux")
- XCTAssertNil(json["version"], "Kiro agent configs should not receive Cursor's hooks version field")
- XCTAssertEqual(
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: hookURL)) as? [String: Any])
+ legacyAssertEqual(json["name"] as? String, "cmux")
+ legacyAssertNil(json["version"], "Kiro agent configs should not receive Cursor's hooks version field")
+ legacyAssertEqual(
json["tools"] as? [String], ["*"],
"Kiro cmux agent must grant the full tool set so `--agent cmux` can run tools and fire preToolUse hooks"
)
- let hooks = try XCTUnwrap(json["hooks"] as? [String: Any])
- let preToolUse = try XCTUnwrap(hooks["preToolUse"] as? [[String: Any]])
- XCTAssertTrue(
+ let hooks = try legacyUnwrap(json["hooks"] as? [String: Any])
+ let preToolUse = try legacyUnwrap(hooks["preToolUse"] as? [[String: Any]])
+ legacyAssertTrue(
preToolUse.contains {
($0["command"] as? String)?.contains("hooks feed --source kiro --event preToolUse") == true
&& ($0["timeout_ms"] as? Int) == 120_000
@@ -998,10 +998,10 @@ extension CLINotifyProcessIntegrationRegressionTests {
},
"Expected Kiro preToolUse feed hook to preserve cmux's exit status for deny decisions, saw \(preToolUse)"
)
- XCTAssertNotNil(hooks["agentSpawn"])
- XCTAssertNotNil(hooks["userPromptSubmit"])
- XCTAssertNotNil(hooks["postToolUse"])
- XCTAssertNotNil(hooks["stop"])
+ legacyAssertNotNil(hooks["agentSpawn"])
+ legacyAssertNotNil(hooks["userPromptSubmit"])
+ legacyAssertNotNil(hooks["postToolUse"])
+ legacyAssertNotNil(hooks["stop"])
}
@Test
func testKiroFeedDenyUsesPreToolUseExitCodeTwo() throws {
@@ -1028,7 +1028,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
guard let id = payload["id"] as? String, let method = payload["method"] as? String else {
return self.malformedRequestResponse(id: payload["id"] as? String, raw: line)
}
- XCTAssertEqual(method, "feed.push")
+ legacyAssertEqual(method, "feed.push")
return self.v2Response(
id: id,
ok: true,
@@ -1059,11 +1059,11 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"hook_event_name":"preToolUse","session_id":"kiro-session-123","cwd":"\#(root.path)","tool_name":"fs_write","tool_input":{"operations":[{"mode":"Line","path":"\#(root.appendingPathComponent("README.md").path)"}]}}"#,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 2, result.stderr)
- XCTAssertTrue(result.stderr.contains("User denied permission via cmux Feed."), result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 2, result.stderr)
+ legacyAssertTrue(result.stderr.contains("User denied permission via cmux Feed."), result.stderr)
let feedEvents = state.commands.compactMap { command -> [String: Any]? in
guard let payload = self.jsonObject(command),
@@ -1074,10 +1074,10 @@ extension CLINotifyProcessIntegrationRegressionTests {
}
return event
}
- XCTAssertEqual(feedEvents.count, 1, "Expected one Kiro Feed event, saw \(state.commands)")
- XCTAssertEqual(feedEvents.first?["hook_event_name"] as? String, "PermissionRequest")
- XCTAssertEqual(feedEvents.first?["_source"] as? String, "kiro")
- XCTAssertEqual(feedEvents.first?["_ppid"] as? Int, 525252)
+ legacyAssertEqual(feedEvents.count, 1, "Expected one Kiro Feed event, saw \(state.commands)")
+ legacyAssertEqual(feedEvents.first?["hook_event_name"] as? String, "PermissionRequest")
+ legacyAssertEqual(feedEvents.first?["_source"] as? String, "kiro")
+ legacyAssertEqual(feedEvents.first?["_ppid"] as? Int, 525252)
}
/// The Feed permission modes that allow a tool (`once` / `always` / `all`
@@ -1129,21 +1129,21 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"hook_event_name":"preToolUse","session_id":"kiro-session-mode","cwd":"\#(root.path)","tool_name":"fs_write","tool_input":{"operations":[{"mode":"Line","path":"\#(root.appendingPathComponent("README.md").path)"}]}}"#,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
for mode in ["once", "always", "all", "bypass"] {
let result = try runKiroDecision(mode: mode)
- XCTAssertFalse(result.timedOut, "\(mode): \(result.stderr)")
- XCTAssertEqual(result.status, 0, "mode \(mode) should allow (exit 0): \(result.stderr)")
- XCTAssertEqual(result.stdout, "{}\n", "mode \(mode) should print {}")
+ legacyAssertFalse(result.timedOut, "\(mode): \(result.stderr)")
+ legacyAssertEqual(result.status, 0, "mode \(mode) should allow (exit 0): \(result.stderr)")
+ legacyAssertEqual(result.stdout, "{}\n", "mode \(mode) should print {}")
}
let unknown = try runKiroDecision(mode: "totally-bogus-mode")
- XCTAssertFalse(unknown.timedOut, unknown.stderr)
- XCTAssertEqual(unknown.status, 2, "unrecognized mode must fail closed (exit 2): \(unknown.stderr)")
- XCTAssertTrue(unknown.stderr.contains("unrecognized"), unknown.stderr)
+ legacyAssertFalse(unknown.timedOut, unknown.stderr)
+ legacyAssertEqual(unknown.status, 2, "unrecognized mode must fail closed (exit 2): \(unknown.stderr)")
+ legacyAssertTrue(unknown.stderr.contains("unrecognized"), unknown.stderr)
}
/// At the default `standard` notification level, Kiro read-only tool
@@ -1189,20 +1189,20 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"hook_event_name":"postToolUse","session_id":"kiro-suppress","cwd":"\#(root.path)","tool_name":"\#(tool)"}"#,
timeout: 5
)
- XCTAssertFalse(result.timedOut, "\(tool): \(result.stderr)")
- XCTAssertEqual(result.status, 0, "\(tool): \(result.stderr)")
- XCTAssertEqual(result.stdout, "{}\n", "\(tool) stdout")
+ legacyAssertFalse(result.timedOut, "\(tool): \(result.stderr)")
+ legacyAssertEqual(result.status, 0, "\(tool): \(result.stderr)")
+ legacyAssertEqual(result.stdout, "{}\n", "\(tool) stdout")
// A non-suppressed event sends one feed.push, so wait for the
// server to record it (generous timeout to avoid flaking on the
// socket/process round-trip under CI load). A suppressed event
// sends nothing, so this wait simply times out silently.
- _ = waitForLegacyExpectations([serverHandled], timeout: 5, recordFailure: false)
+ _ = legacyWaitForExpectations([serverHandled], timeout: 5, recordFailure: false)
return state.commands.filter { $0.contains("feed.push") }.count
}
- XCTAssertEqual(try feedPushCount(forTool: "fs_read"), 0,
+ legacyAssertEqual(try feedPushCount(forTool: "fs_read"), 0,
"read-only kiro tool at standard level must be suppressed")
- XCTAssertGreaterThan(try feedPushCount(forTool: "fs_write"), 0,
+ legacyAssertGreaterThan(try feedPushCount(forTool: "fs_write"), 0,
"mutating kiro tool at standard level must still emit telemetry")
}
@Test
@@ -1230,7 +1230,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
guard let id = payload["id"] as? String, let method = payload["method"] as? String else {
return self.malformedRequestResponse(id: payload["id"] as? String, raw: line)
}
- XCTAssertEqual(method, "feed.push")
+ legacyAssertEqual(method, "feed.push")
return self.v2Response(id: id, ok: true, result: ["status": "acknowledged"])
}
@@ -1250,11 +1250,11 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"hook_event_name":"PreToolUse","session_id":"gemini-session-123","cwd":"\#(root.path)","tool_name":"write","tool_input":{"path":"\#(root.appendingPathComponent("README.md").path)"}}"#,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
let feedPushes = state.commands.compactMap { command -> [String: Any]? in
guard let payload = self.jsonObject(command),
@@ -1264,14 +1264,14 @@ extension CLINotifyProcessIntegrationRegressionTests {
}
return params
}
- XCTAssertEqual(feedPushes.count, 1, "Expected one generic Feed event, saw \(state.commands)")
- let event = try XCTUnwrap(feedPushes.first?["event"] as? [String: Any])
- let waitTimeout = try XCTUnwrap(feedPushes.first?["wait_timeout_seconds"] as? NSNumber)
- XCTAssertEqual(event["hook_event_name"] as? String, "PreToolUse")
- XCTAssertEqual(event["_source"] as? String, "gemini")
- XCTAssertEqual(event["tool_name"] as? String, "write")
- XCTAssertEqual(event["_ppid"] as? Int, 626262)
- XCTAssertEqual(waitTimeout.doubleValue, 0)
+ legacyAssertEqual(feedPushes.count, 1, "Expected one generic Feed event, saw \(state.commands)")
+ let event = try legacyUnwrap(feedPushes.first?["event"] as? [String: Any])
+ let waitTimeout = try legacyUnwrap(feedPushes.first?["wait_timeout_seconds"] as? NSNumber)
+ legacyAssertEqual(event["hook_event_name"] as? String, "PreToolUse")
+ legacyAssertEqual(event["_source"] as? String, "gemini")
+ legacyAssertEqual(event["tool_name"] as? String, "write")
+ legacyAssertEqual(event["_ppid"] as? Int, 626262)
+ legacyAssertEqual(waitTimeout.doubleValue, 0)
}
@Test
func testAntigravityFeedHookMissingSessionIdUsesStableFallback() throws {
@@ -1310,7 +1310,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
guard let id = payload["id"] as? String, let method = payload["method"] as? String else {
return self.malformedRequestResponse(id: payload["id"] as? String, raw: line)
}
- XCTAssertEqual(method, "feed.push")
+ legacyAssertEqual(method, "feed.push")
return self.v2Response(id: id, ok: true, result: ["status": "acknowledged"])
}
let result = runProcess(
@@ -1320,26 +1320,26 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: input,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
let input = #"{"hook_event_name":"PreToolUse","workspacePaths":["\#(root.path)"],"notification":{"transcript_path":"\#(root.appendingPathComponent("transcript-a.jsonl").path)"},"toolCall":{"name":"read_file","args":{"path":"README.md"}}}"#
let first = runFeedHook(input: input)
- XCTAssertFalse(first.timedOut, first.stderr)
- XCTAssertEqual(first.status, 0, first.stderr)
- XCTAssertEqual(first.stdout, "{}\n")
+ legacyAssertFalse(first.timedOut, first.stderr)
+ legacyAssertEqual(first.status, 0, first.stderr)
+ legacyAssertEqual(first.stdout, "{}\n")
let second = runFeedHook(input: input)
- XCTAssertFalse(second.timedOut, second.stderr)
- XCTAssertEqual(second.status, 0, second.stderr)
- XCTAssertEqual(second.stdout, "{}\n")
+ legacyAssertFalse(second.timedOut, second.stderr)
+ legacyAssertEqual(second.status, 0, second.stderr)
+ legacyAssertEqual(second.stdout, "{}\n")
let differentTranscriptInput = #"{"hook_event_name":"PreToolUse","workspacePaths":["\#(root.path)"],"notification":{"transcript_path":"\#(root.appendingPathComponent("transcript-b.jsonl").path)"},"toolCall":{"name":"read_file","args":{"path":"README.md"}}}"#
let third = runFeedHook(input: differentTranscriptInput)
- XCTAssertFalse(third.timedOut, third.stderr)
- XCTAssertEqual(third.status, 0, third.stderr)
- XCTAssertEqual(third.stdout, "{}\n")
+ legacyAssertFalse(third.timedOut, third.stderr)
+ legacyAssertEqual(third.status, 0, third.stderr)
+ legacyAssertEqual(third.stdout, "{}\n")
let events = state.commands.compactMap { command -> [String: Any]? in
guard let payload = self.jsonObject(command),
@@ -1351,14 +1351,14 @@ extension CLINotifyProcessIntegrationRegressionTests {
return event
}
let sessionIds = events.compactMap { $0["session_id"] as? String }
- XCTAssertEqual(sessionIds.count, 3, "Expected three feed events, saw \(state.commands)")
- XCTAssertEqual(sessionIds[0], sessionIds[1])
- XCTAssertNotEqual(sessionIds[1], sessionIds[2])
- XCTAssertTrue(
+ legacyAssertEqual(sessionIds.count, 3, "Expected three feed events, saw \(state.commands)")
+ legacyAssertEqual(sessionIds[0], sessionIds[1])
+ legacyAssertNotEqual(sessionIds[1], sessionIds[2])
+ legacyAssertTrue(
sessionIds[0].hasPrefix("antigravity-fallback-"),
"Expected deterministic Antigravity fallback session id, saw \(sessionIds[0])"
)
- XCTAssertEqual(events.compactMap { $0["_ppid"] as? Int }, [424242, 424242, 424242])
+ legacyAssertEqual(events.compactMap { $0["_ppid"] as? Int }, [424242, 424242, 424242])
}
@Test
func testGrokNotificationHookUsesPayloadMessageAndStopDoesNotSendGenericNotification() throws {
@@ -1416,7 +1416,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: input,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
@@ -1424,10 +1424,10 @@ extension CLINotifyProcessIntegrationRegressionTests {
"session-start",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"SessionStart"}"#
)
- XCTAssertFalse(start.timedOut, start.stderr)
- XCTAssertEqual(start.status, 0, start.stderr)
- XCTAssertEqual(start.stdout, "{}\n")
- XCTAssertFalse(
+ legacyAssertFalse(start.timedOut, start.stderr)
+ legacyAssertEqual(start.status, 0, start.stderr)
+ legacyAssertEqual(start.stdout, "{}\n")
+ legacyAssertFalse(
state.commands.contains { $0.contains("set_status grok") || $0.hasPrefix("notify_target_async ") },
"Grok SessionStart should only establish routing state, saw \(state.commands)"
)
@@ -1437,16 +1437,16 @@ extension CLINotifyProcessIntegrationRegressionTests {
"stop",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Stop"}"#
)
- XCTAssertFalse(stop.timedOut, stop.stderr)
- XCTAssertEqual(stop.status, 0, stop.stderr)
- XCTAssertEqual(stop.stdout, "{}\n")
+ legacyAssertFalse(stop.timedOut, stop.stderr)
+ legacyAssertEqual(stop.status, 0, stop.stderr)
+ legacyAssertEqual(stop.stdout, "{}\n")
let stopCommands = Array(state.commands.dropFirst(stopCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
stopCommands.contains { $0.hasPrefix("notify_target_async ") },
"Grok Stop should not publish a generic completion notification; Notification carries the real message. Saw \(stopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
stopCommands.contains { $0.contains("set_status grok Idle") },
"Expected Grok Stop to keep task-manager status idle, saw \(stopCommands)"
)
@@ -1456,41 +1456,41 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"Grok finished updating docs"}"#
)
- XCTAssertFalse(notification.timedOut, notification.stderr)
- XCTAssertEqual(notification.status, 0, notification.stderr)
- XCTAssertEqual(notification.stdout, "{}\n")
+ legacyAssertFalse(notification.timedOut, notification.stderr)
+ legacyAssertEqual(notification.status, 0, notification.stderr)
+ legacyAssertEqual(notification.stdout, "{}\n")
let notificationCommands = Array(state.commands.dropFirst(notificationCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
notificationCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Completed|Grok finished updating docs")
},
"Expected Grok Notification to forward the payload message, saw \(notificationCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
notificationCommands.contains { $0.contains("set_status grok Idle") },
"Expected completion notification to leave Grok idle, saw \(notificationCommands)"
)
let storeURL = root.appendingPathComponent("grok-hook-sessions.json", isDirectory: false)
- var json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- var sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
- var session = try XCTUnwrap(sessions[sessionId] as? [String: Any])
- XCTAssertEqual(session["lastSubtitle"] as? String, "Completed")
- XCTAssertEqual(session["lastBody"] as? String, "Grok finished updating docs")
- XCTAssertEqual(session["lastNotificationStatus"] as? String, "idle")
+ var json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ var sessions = try legacyUnwrap(json["sessions"] as? [String: Any])
+ var session = try legacyUnwrap(sessions[sessionId] as? [String: Any])
+ legacyAssertEqual(session["lastSubtitle"] as? String, "Completed")
+ legacyAssertEqual(session["lastBody"] as? String, "Grok finished updating docs")
+ legacyAssertEqual(session["lastNotificationStatus"] as? String, "idle")
let preAssistantInternalCommandStart = state.commands.count
let preAssistantInternal = runGrokHook(
"notification",
input: #"{"sessionId":"grok-before-assistant","cwd":"\#(root.path)","hookEventName":"Notification","message":"SessionNotification { update: HookExecution { event_name: session_start } }"}"#
)
- XCTAssertFalse(preAssistantInternal.timedOut, preAssistantInternal.stderr)
- XCTAssertEqual(preAssistantInternal.status, 0, preAssistantInternal.stderr)
- XCTAssertEqual(preAssistantInternal.stdout, "{}\n")
+ legacyAssertFalse(preAssistantInternal.timedOut, preAssistantInternal.stderr)
+ legacyAssertEqual(preAssistantInternal.status, 0, preAssistantInternal.stderr)
+ legacyAssertEqual(preAssistantInternal.stdout, "{}\n")
let preAssistantInternalCommands = Array(state.commands.dropFirst(preAssistantInternalCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
preAssistantInternalCommands.contains { $0.hasPrefix("notify_target_async ") },
"Grok internal session notifications should not notify before there is an assistant response, saw \(preAssistantInternalCommands)"
)
@@ -1500,36 +1500,36 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"grok-generic-before-assistant","cwd":"\#(root.path)","hookEventName":"Notification","message":"Turn complete in 3.8s."}"#
)
- XCTAssertFalse(preAssistantGeneric.timedOut, preAssistantGeneric.stderr)
- XCTAssertEqual(preAssistantGeneric.status, 0, preAssistantGeneric.stderr)
- XCTAssertEqual(preAssistantGeneric.stdout, "{}\n")
+ legacyAssertFalse(preAssistantGeneric.timedOut, preAssistantGeneric.stderr)
+ legacyAssertEqual(preAssistantGeneric.status, 0, preAssistantGeneric.stderr)
+ legacyAssertEqual(preAssistantGeneric.stdout, "{}\n")
let preAssistantGenericCommands = Array(state.commands.dropFirst(preAssistantGenericCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
preAssistantGenericCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Completed|Task completed")
},
"Grok generic completion notifications should still fire before there is an assistant response, saw \(preAssistantGenericCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
preAssistantGenericCommands.contains { $0.contains("set_status grok Idle") },
"Expected generic completion without an assistant response to leave Grok idle, saw \(preAssistantGenericCommands)"
)
- json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
- let preAssistantGenericSession = try XCTUnwrap(sessions["grok-generic-before-assistant"] as? [String: Any])
- XCTAssertEqual(preAssistantGenericSession["lastSubtitle"] as? String, "Completed")
- XCTAssertEqual(preAssistantGenericSession["lastBody"] as? String, "Task completed")
- XCTAssertEqual(preAssistantGenericSession["lastNotificationStatus"] as? String, "idle")
+ json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ sessions = try legacyUnwrap(json["sessions"] as? [String: Any])
+ let preAssistantGenericSession = try legacyUnwrap(sessions["grok-generic-before-assistant"] as? [String: Any])
+ legacyAssertEqual(preAssistantGenericSession["lastSubtitle"] as? String, "Completed")
+ legacyAssertEqual(preAssistantGenericSession["lastBody"] as? String, "Task completed")
+ legacyAssertEqual(preAssistantGenericSession["lastNotificationStatus"] as? String, "idle")
let assistantMessage = "**42.** That's the answer, according to Deep Thought."
let nextTurnPrompt = runGrokHook(
"prompt-submit",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"UserPromptSubmit","prompt":"next turn"}"#
)
- XCTAssertFalse(nextTurnPrompt.timedOut, nextTurnPrompt.stderr)
- XCTAssertEqual(nextTurnPrompt.status, 0, nextTurnPrompt.stderr)
- XCTAssertEqual(nextTurnPrompt.stdout, "{}\n")
+ legacyAssertFalse(nextTurnPrompt.timedOut, nextTurnPrompt.stderr)
+ legacyAssertEqual(nextTurnPrompt.status, 0, nextTurnPrompt.stderr)
+ legacyAssertEqual(nextTurnPrompt.stdout, "{}\n")
try writeGrokAssistantTranscript(
grokHome: grokHome,
@@ -1542,19 +1542,19 @@ extension CLINotifyProcessIntegrationRegressionTests {
"stop",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Stop"}"#
)
- XCTAssertFalse(enrichedStop.timedOut, enrichedStop.stderr)
- XCTAssertEqual(enrichedStop.status, 0, enrichedStop.stderr)
- XCTAssertEqual(enrichedStop.stdout, "{}\n")
+ legacyAssertFalse(enrichedStop.timedOut, enrichedStop.stderr)
+ legacyAssertEqual(enrichedStop.status, 0, enrichedStop.stderr)
+ legacyAssertEqual(enrichedStop.stdout, "{}\n")
let enrichedStopCommands = Array(state.commands.dropFirst(enrichedStopCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
enrichedStopCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Completed in ")
&& $0.contains(assistantMessage)
},
"Expected Grok Stop fallback to publish the cwd-scoped assistant response when Grok only emits internal Notification events, saw \(enrichedStopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
enrichedStopCommands.contains { $0.contains("set_status grok Idle") },
"Expected enriched Grok Stop to leave Grok idle, saw \(enrichedStopCommands)"
)
@@ -1565,15 +1565,15 @@ extension CLINotifyProcessIntegrationRegressionTests {
"session-start",
input: #"{"sessionId":"\#(oversizedSessionId)","cwd":"\#(root.path)","hookEventName":"SessionStart"}"#
)
- XCTAssertFalse(oversizedStart.timedOut, oversizedStart.stderr)
- XCTAssertEqual(oversizedStart.status, 0, oversizedStart.stderr)
+ legacyAssertFalse(oversizedStart.timedOut, oversizedStart.stderr)
+ legacyAssertEqual(oversizedStart.status, 0, oversizedStart.stderr)
let oversizedPrompt = runGrokHook(
"prompt-submit",
input: #"{"sessionId":"\#(oversizedSessionId)","cwd":"\#(root.path)","hookEventName":"UserPromptSubmit","prompt":"oversized turn"}"#
)
- XCTAssertFalse(oversizedPrompt.timedOut, oversizedPrompt.stderr)
- XCTAssertEqual(oversizedPrompt.status, 0, oversizedPrompt.stderr)
+ legacyAssertFalse(oversizedPrompt.timedOut, oversizedPrompt.stderr)
+ legacyAssertEqual(oversizedPrompt.status, 0, oversizedPrompt.stderr)
let oversizedSessionURL = grokHome
.appendingPathComponent("sessions", isDirectory: true)
@@ -1589,11 +1589,11 @@ extension CLINotifyProcessIntegrationRegressionTests {
"stop",
input: #"{"sessionId":"\#(oversizedSessionId)","cwd":"\#(root.path)","hookEventName":"Stop"}"#
)
- XCTAssertFalse(oversizedStop.timedOut, oversizedStop.stderr)
- XCTAssertEqual(oversizedStop.status, 0, oversizedStop.stderr)
+ legacyAssertFalse(oversizedStop.timedOut, oversizedStop.stderr)
+ legacyAssertEqual(oversizedStop.status, 0, oversizedStop.stderr)
let oversizedStopCommands = Array(state.commands.dropFirst(oversizedStopCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
oversizedStopCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Completed in ")
&& $0.contains("Oversized Grok assistant response")
@@ -1606,15 +1606,15 @@ extension CLINotifyProcessIntegrationRegressionTests {
"session-start",
input: #"{"sessionId":"\#(multibyteSessionId)","cwd":"\#(root.path)","hookEventName":"SessionStart"}"#
)
- XCTAssertFalse(multibyteStart.timedOut, multibyteStart.stderr)
- XCTAssertEqual(multibyteStart.status, 0, multibyteStart.stderr)
+ legacyAssertFalse(multibyteStart.timedOut, multibyteStart.stderr)
+ legacyAssertEqual(multibyteStart.status, 0, multibyteStart.stderr)
let multibytePrompt = runGrokHook(
"prompt-submit",
input: #"{"sessionId":"\#(multibyteSessionId)","cwd":"\#(root.path)","hookEventName":"UserPromptSubmit","prompt":"multibyte boundary"}"#
)
- XCTAssertFalse(multibytePrompt.timedOut, multibytePrompt.stderr)
- XCTAssertEqual(multibytePrompt.status, 0, multibytePrompt.stderr)
+ legacyAssertFalse(multibytePrompt.timedOut, multibytePrompt.stderr)
+ legacyAssertEqual(multibytePrompt.status, 0, multibytePrompt.stderr)
let multibyteSessionURL = grokHome
.appendingPathComponent("sessions", isDirectory: true)
@@ -1641,7 +1641,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
break
}
}
- let historyData = try XCTUnwrap(multibyteHistoryData)
+ let historyData = try legacyUnwrap(multibyteHistoryData)
try historyData.write(to: multibyteSessionURL.appendingPathComponent("chat_history.jsonl", isDirectory: false))
let multibyteStopCommandStart = state.commands.count
@@ -1649,11 +1649,11 @@ extension CLINotifyProcessIntegrationRegressionTests {
"stop",
input: #"{"sessionId":"\#(multibyteSessionId)","cwd":"\#(root.path)","hookEventName":"Stop"}"#
)
- XCTAssertFalse(multibyteStop.timedOut, multibyteStop.stderr)
- XCTAssertEqual(multibyteStop.status, 0, multibyteStop.stderr)
+ legacyAssertFalse(multibyteStop.timedOut, multibyteStop.stderr)
+ legacyAssertEqual(multibyteStop.status, 0, multibyteStop.stderr)
let multibyteStopCommands = Array(state.commands.dropFirst(multibyteStopCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
multibyteStopCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Completed in ")
&& $0.contains(multibyteAssistantMessage)
@@ -1666,16 +1666,16 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"Turn complete in 3.8s."}"#
)
- XCTAssertFalse(genericCompletion.timedOut, genericCompletion.stderr)
- XCTAssertEqual(genericCompletion.status, 0, genericCompletion.stderr)
- XCTAssertEqual(genericCompletion.stdout, "{}\n")
+ legacyAssertFalse(genericCompletion.timedOut, genericCompletion.stderr)
+ legacyAssertEqual(genericCompletion.status, 0, genericCompletion.stderr)
+ legacyAssertEqual(genericCompletion.stdout, "{}\n")
let genericCompletionCommands = Array(state.commands.dropFirst(genericCompletionCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
genericCompletionCommands.contains { $0.hasPrefix("notify_target_async ") },
"Grok completion Notification must not double-notify after Stop fallback already published the completion, saw \(genericCompletionCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
genericCompletionCommands.contains { $0.contains("set_status grok Idle") },
"Expected enriched completion notification to leave Grok idle, saw \(genericCompletionCommands)"
)
@@ -1686,18 +1686,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(sameCwdMissingSessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"Turn complete in 4.0s."}"#
)
- XCTAssertFalse(sameCwdMissing.timedOut, sameCwdMissing.stderr)
- XCTAssertEqual(sameCwdMissing.status, 0, sameCwdMissing.stderr)
- XCTAssertEqual(sameCwdMissing.stdout, "{}\n")
+ legacyAssertFalse(sameCwdMissing.timedOut, sameCwdMissing.stderr)
+ legacyAssertEqual(sameCwdMissing.status, 0, sameCwdMissing.stderr)
+ legacyAssertEqual(sameCwdMissing.stdout, "{}\n")
let sameCwdMissingCommands = Array(state.commands.dropFirst(sameCwdMissingCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
sameCwdMissingCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Completed|Task completed")
},
"Grok completion without a matching session transcript should still fire a generic completion notification, saw \(sameCwdMissingCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
sameCwdMissingCommands.contains { $0.contains(assistantMessage) },
"Grok completion notifications must not read another session from the same cwd, saw \(sameCwdMissingCommands)"
)
@@ -1730,18 +1730,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"cwd":"\#(root.path)","hookEventName":"Notification","message":"Turn complete in 4.2s."}"#
)
- XCTAssertFalse(envResolved.timedOut, envResolved.stderr)
- XCTAssertEqual(envResolved.status, 0, envResolved.stderr)
- XCTAssertEqual(envResolved.stdout, "{}\n")
+ legacyAssertFalse(envResolved.timedOut, envResolved.stderr)
+ legacyAssertEqual(envResolved.status, 0, envResolved.stderr)
+ legacyAssertEqual(envResolved.stdout, "{}\n")
let envResolvedCommands = Array(state.commands.dropFirst(envResolvedCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
envResolvedCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Completed|\(envResolvedMessage)")
},
"Grok completion without a payload session id should use the resolved hook session id, saw \(envResolvedCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
envResolvedCommands.contains { $0.contains(unrelatedLatestMessage) },
"Grok completion without a payload session id must not fall back to the latest unrelated cwd session, saw \(envResolvedCommands)"
)
@@ -1751,12 +1751,12 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"SessionNotification { update: MemoryFlushCompleted { result: written } }"}"#
)
- XCTAssertFalse(hookExecution.timedOut, hookExecution.stderr)
- XCTAssertEqual(hookExecution.status, 0, hookExecution.stderr)
- XCTAssertEqual(hookExecution.stdout, "{}\n")
+ legacyAssertFalse(hookExecution.timedOut, hookExecution.stderr)
+ legacyAssertEqual(hookExecution.status, 0, hookExecution.stderr)
+ legacyAssertEqual(hookExecution.stdout, "{}\n")
let hookExecutionCommands = Array(state.commands.dropFirst(hookExecutionCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
hookExecutionCommands.contains { $0.hasPrefix("notify_target_async ") },
"Grok internal session notifications must not replay the last assistant response as a fresh notification, saw \(hookExecutionCommands)"
)
@@ -1777,22 +1777,22 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(scopedMissSessionId)","cwd":"\#(missingCwd.path)","hookEventName":"Notification","message":"Turn complete in 4.0s."}"#
)
- XCTAssertFalse(scopedMiss.timedOut, scopedMiss.stderr)
- XCTAssertEqual(scopedMiss.status, 0, scopedMiss.stderr)
- XCTAssertEqual(scopedMiss.stdout, "{}\n")
+ legacyAssertFalse(scopedMiss.timedOut, scopedMiss.stderr)
+ legacyAssertEqual(scopedMiss.status, 0, scopedMiss.stderr)
+ legacyAssertEqual(scopedMiss.stdout, "{}\n")
let scopedMissCommands = Array(state.commands.dropFirst(scopedMissCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
scopedMissCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Completed|Task completed")
},
"Grok completion without a cwd-scoped transcript should still fire a generic completion notification, saw \(scopedMissCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
scopedMissCommands.contains { $0.contains(otherProjectMessage) },
"Grok completion notifications must not read another cwd's latest session, saw \(scopedMissCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
scopedMissCommands.contains { $0.contains("set_status grok Idle") },
"Expected scoped completion without transcript to leave Grok idle, saw \(scopedMissCommands)"
)
@@ -1803,18 +1803,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Notification","reason":"idle_prompt","message":"\#(waitingMessage)"}"#
)
- XCTAssertFalse(waiting.timedOut, waiting.stderr)
- XCTAssertEqual(waiting.status, 0, waiting.stderr)
- XCTAssertEqual(waiting.stdout, "{}\n")
+ legacyAssertFalse(waiting.timedOut, waiting.stderr)
+ legacyAssertEqual(waiting.status, 0, waiting.stderr)
+ legacyAssertEqual(waiting.stdout, "{}\n")
let waitingCommands = Array(state.commands.dropFirst(waitingCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
waitingCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Waiting|\(waitingMessage)")
},
"Expected waiting notification to forward the payload message, saw \(waitingCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
waitingCommands.contains { $0.contains("set_status grok Grok needs input") },
"Expected waiting notification to mark Grok as needing input, saw \(waitingCommands)"
)
@@ -1824,28 +1824,28 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Notification"}"#
)
- XCTAssertFalse(fallback.timedOut, fallback.stderr)
- XCTAssertEqual(fallback.status, 0, fallback.stderr)
- XCTAssertEqual(fallback.stdout, "{}\n")
+ legacyAssertFalse(fallback.timedOut, fallback.stderr)
+ legacyAssertEqual(fallback.status, 0, fallback.stderr)
+ legacyAssertEqual(fallback.stdout, "{}\n")
let fallbackCommands = Array(state.commands.dropFirst(fallbackCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
fallbackCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Waiting|\(waitingMessage)")
},
"Expected empty Grok Notification payload to reuse the saved message, saw \(fallbackCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
fallbackCommands.contains { $0.contains("set_status grok Grok needs input") },
"Expected fallback notification to preserve the saved needs-input status, saw \(fallbackCommands)"
)
- json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
- session = try XCTUnwrap(sessions[sessionId] as? [String: Any])
- XCTAssertEqual(session["lastSubtitle"] as? String, "Waiting")
- XCTAssertEqual(session["lastBody"] as? String, waitingMessage)
- XCTAssertEqual(session["lastNotificationStatus"] as? String, "needsInput")
+ json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ sessions = try legacyUnwrap(json["sessions"] as? [String: Any])
+ session = try legacyUnwrap(sessions[sessionId] as? [String: Any])
+ legacyAssertEqual(session["lastSubtitle"] as? String, "Waiting")
+ legacyAssertEqual(session["lastBody"] as? String, waitingMessage)
+ legacyAssertEqual(session["lastNotificationStatus"] as? String, "needsInput")
for neutralMessage in ["Invalid input format", "Question mark rendered"] {
let neutralCommandStart = state.commands.count
@@ -1853,16 +1853,16 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"\#(neutralMessage)"}"#
)
- XCTAssertFalse(neutral.timedOut, neutral.stderr)
- XCTAssertEqual(neutral.status, 0, neutral.stderr)
- XCTAssertEqual(neutral.stdout, "{}\n")
+ legacyAssertFalse(neutral.timedOut, neutral.stderr)
+ legacyAssertEqual(neutral.status, 0, neutral.stderr)
+ legacyAssertEqual(neutral.stdout, "{}\n")
let neutralCommands = Array(state.commands.dropFirst(neutralCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
neutralCommands.contains { $0.hasPrefix("notify_target_async ") },
"Neutral classifier text should not alert as needs-input, saw \(neutralCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
neutralCommands.contains { $0.contains("set_status grok ") },
"Neutral classifier text should not replace the saved status, saw \(neutralCommands)"
)
@@ -1874,18 +1874,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"\#(incompleteWaitingMessage)"}"#
)
- XCTAssertFalse(incompleteWaiting.timedOut, incompleteWaiting.stderr)
- XCTAssertEqual(incompleteWaiting.status, 0, incompleteWaiting.stderr)
- XCTAssertEqual(incompleteWaiting.stdout, "{}\n")
+ legacyAssertFalse(incompleteWaiting.timedOut, incompleteWaiting.stderr)
+ legacyAssertEqual(incompleteWaiting.status, 0, incompleteWaiting.stderr)
+ legacyAssertEqual(incompleteWaiting.stdout, "{}\n")
let incompleteWaitingCommands = Array(state.commands.dropFirst(incompleteWaitingCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
incompleteWaitingCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Waiting|\(incompleteWaitingMessage)")
},
"Incomplete/undone waiting text should not be classified as a completion, saw \(incompleteWaitingCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
incompleteWaitingCommands.contains { $0.contains("Grok|Completed|") },
"Incomplete/undone waiting text must not emit a completed notification, saw \(incompleteWaitingCommands)"
)
@@ -1896,44 +1896,44 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"\#(progressMessage)"}"#
)
- XCTAssertFalse(progress.timedOut, progress.stderr)
- XCTAssertEqual(progress.status, 0, progress.stderr)
- XCTAssertEqual(progress.stdout, "{}\n")
+ legacyAssertFalse(progress.timedOut, progress.stderr)
+ legacyAssertEqual(progress.status, 0, progress.stderr)
+ legacyAssertEqual(progress.stdout, "{}\n")
let progressCommands = Array(state.commands.dropFirst(progressCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
progressCommands.contains { $0.hasPrefix("notify_target_async ") },
"Unclassified Grok notifications are progress/bookkeeping and should not alert, saw \(progressCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
progressCommands.contains { $0.contains("set_status grok ") },
"Unclassified Grok notifications should not clear or replace active status, saw \(progressCommands)"
)
- json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
- session = try XCTUnwrap(sessions[sessionId] as? [String: Any])
- XCTAssertEqual(session["lastSubtitle"] as? String, "Waiting")
- XCTAssertEqual(session["lastBody"] as? String, incompleteWaitingMessage)
- XCTAssertEqual(session["lastNotificationStatus"] as? String, "needsInput")
+ json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ sessions = try legacyUnwrap(json["sessions"] as? [String: Any])
+ session = try legacyUnwrap(sessions[sessionId] as? [String: Any])
+ legacyAssertEqual(session["lastSubtitle"] as? String, "Waiting")
+ legacyAssertEqual(session["lastBody"] as? String, incompleteWaitingMessage)
+ legacyAssertEqual(session["lastNotificationStatus"] as? String, "needsInput")
let neutralFallbackCommandStart = state.commands.count
let neutralFallback = runGrokHook(
"notification",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Notification"}"#
)
- XCTAssertFalse(neutralFallback.timedOut, neutralFallback.stderr)
- XCTAssertEqual(neutralFallback.status, 0, neutralFallback.stderr)
- XCTAssertEqual(neutralFallback.stdout, "{}\n")
+ legacyAssertFalse(neutralFallback.timedOut, neutralFallback.stderr)
+ legacyAssertEqual(neutralFallback.status, 0, neutralFallback.stderr)
+ legacyAssertEqual(neutralFallback.stdout, "{}\n")
let neutralFallbackCommands = Array(state.commands.dropFirst(neutralFallbackCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
neutralFallbackCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Waiting|\(incompleteWaitingMessage)")
},
"Expected empty payload to reuse the last terminal saved notification, saw \(neutralFallbackCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
neutralFallbackCommands.contains { $0.contains("set_status grok Grok needs input") },
"Fallback notifications should preserve the saved needs-input status, saw \(neutralFallbackCommands)"
)
@@ -2009,7 +2009,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: input,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
@@ -2028,18 +2028,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
input: #"{"sessionId":"\#(thread.sessionId)","cwd":"\#(root.path)","hookEventName":"SessionStart"}"#,
surfaceId: thread.surfaceId
)
- XCTAssertFalse(start.timedOut, start.stderr)
- XCTAssertEqual(start.status, 0, start.stderr)
- XCTAssertEqual(start.stdout, "{}\n")
+ legacyAssertFalse(start.timedOut, start.stderr)
+ legacyAssertEqual(start.status, 0, start.stderr)
+ legacyAssertEqual(start.stdout, "{}\n")
let prompt = runGrokHook(
"prompt-submit",
input: #"{"sessionId":"\#(thread.sessionId)","cwd":"\#(root.path)","hookEventName":"UserPromptSubmit","prompt":"thread \#(thread.index) prompt"}"#,
surfaceId: thread.surfaceId
)
- XCTAssertFalse(prompt.timedOut, prompt.stderr)
- XCTAssertEqual(prompt.status, 0, prompt.stderr)
- XCTAssertEqual(prompt.stdout, "{}\n")
+ legacyAssertFalse(prompt.timedOut, prompt.stderr)
+ legacyAssertEqual(prompt.status, 0, prompt.stderr)
+ legacyAssertEqual(prompt.stdout, "{}\n")
let internalCommandStart = state.commands.count
let internalNotification = runGrokHook(
@@ -2047,12 +2047,12 @@ extension CLINotifyProcessIntegrationRegressionTests {
input: #"{"sessionId":"\#(thread.sessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"SessionNotification { update: HookExecution { event_name: user_prompt_submit } }"}"#,
surfaceId: thread.surfaceId
)
- XCTAssertFalse(internalNotification.timedOut, internalNotification.stderr)
- XCTAssertEqual(internalNotification.status, 0, internalNotification.stderr)
- XCTAssertEqual(internalNotification.stdout, "{}\n")
+ legacyAssertFalse(internalNotification.timedOut, internalNotification.stderr)
+ legacyAssertEqual(internalNotification.status, 0, internalNotification.stderr)
+ legacyAssertEqual(internalNotification.stdout, "{}\n")
let internalCommands = Array(state.commands.dropFirst(internalCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
internalCommands.contains { $0.hasPrefix("notify_target_async ") },
"Prompt-submit bookkeeping for Grok thread \(thread.index) must not notify, saw \(internalCommands)"
)
@@ -2074,12 +2074,12 @@ extension CLINotifyProcessIntegrationRegressionTests {
input: #"{"sessionId":"\#(thread.sessionId)","cwd":"\#(root.path)","hookEventName":"Stop"}"#,
surfaceId: thread.surfaceId
)
- XCTAssertFalse(stop.timedOut, stop.stderr)
- XCTAssertEqual(stop.status, 0, stop.stderr)
- XCTAssertEqual(stop.stdout, "{}\n")
+ legacyAssertFalse(stop.timedOut, stop.stderr)
+ legacyAssertEqual(stop.status, 0, stop.stderr)
+ legacyAssertEqual(stop.stdout, "{}\n")
let stopCommands = Array(state.commands.dropFirst(stopCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
stopCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(thread.surfaceId) Grok|Completed in ")
&& $0.contains(thread.assistantMessage)
@@ -2087,12 +2087,12 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Expected Grok Stop fallback to notify for thread \(thread.index), saw \(stopCommands)"
)
if thread.index == 1 {
- XCTAssertFalse(
+ legacyAssertFalse(
stopCommands.contains { $0.contains("set_status grok Idle") },
"First Grok thread must not reset shared status while thread 2 is still running, saw \(stopCommands)"
)
} else {
- XCTAssertTrue(
+ legacyAssertTrue(
stopCommands.contains { $0.contains("set_status grok Idle") },
"Expected final Grok Stop to leave Grok idle, saw \(stopCommands)"
)
@@ -2106,12 +2106,12 @@ extension CLINotifyProcessIntegrationRegressionTests {
input: #"{"sessionId":"\#(thread.sessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"SessionNotification { update: HookExecution { event_name: stop } }"}"#,
surfaceId: thread.surfaceId
)
- XCTAssertFalse(notification.timedOut, notification.stderr)
- XCTAssertEqual(notification.status, 0, notification.stderr)
- XCTAssertEqual(notification.stdout, "{}\n")
+ legacyAssertFalse(notification.timedOut, notification.stderr)
+ legacyAssertEqual(notification.status, 0, notification.stderr)
+ legacyAssertEqual(notification.stdout, "{}\n")
let notificationCommands = Array(state.commands.dropFirst(notificationCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
notificationCommands.contains { $0.hasPrefix("notify_target_async ") },
"Internal Grok Notification after Stop fallback must not double-notify thread \(thread.index), saw \(notificationCommands)"
)
@@ -2170,7 +2170,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: input,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
@@ -2178,27 +2178,27 @@ extension CLINotifyProcessIntegrationRegressionTests {
"session-start",
input: #"{"sessionId":"\#(sessionId)","hookEventName":"SessionStart"}"#
)
- XCTAssertFalse(start.timedOut, start.stderr)
- XCTAssertEqual(start.status, 0, start.stderr)
- XCTAssertEqual(start.stdout, "{}\n")
+ legacyAssertFalse(start.timedOut, start.stderr)
+ legacyAssertEqual(start.status, 0, start.stderr)
+ legacyAssertEqual(start.stdout, "{}\n")
let stopCommandStart = state.commands.count
let stop = runGrokHook(
"stop",
input: #"{"sessionId":"\#(sessionId)","hookEventName":"Stop"}"#
)
- XCTAssertFalse(stop.timedOut, stop.stderr)
- XCTAssertEqual(stop.status, 0, stop.stderr)
- XCTAssertEqual(stop.stdout, "{}\n")
+ legacyAssertFalse(stop.timedOut, stop.stderr)
+ legacyAssertEqual(stop.status, 0, stop.stderr)
+ legacyAssertEqual(stop.stdout, "{}\n")
let stopCommands = Array(state.commands.dropFirst(stopCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
stopCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Completed|Grok session completed")
},
"Expected Grok Stop without cwd to notify with a generic completion body, saw \(stopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
stopCommands.contains { $0.contains("set_status grok Idle") },
"Expected Grok Stop without cwd to leave Grok idle, saw \(stopCommands)"
)
@@ -2208,12 +2208,12 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(sessionId)","hookEventName":"Notification","message":"Turn complete in 1.0s."}"#
)
- XCTAssertFalse(duplicateCompletion.timedOut, duplicateCompletion.stderr)
- XCTAssertEqual(duplicateCompletion.status, 0, duplicateCompletion.stderr)
- XCTAssertEqual(duplicateCompletion.stdout, "{}\n")
+ legacyAssertFalse(duplicateCompletion.timedOut, duplicateCompletion.stderr)
+ legacyAssertEqual(duplicateCompletion.status, 0, duplicateCompletion.stderr)
+ legacyAssertEqual(duplicateCompletion.stdout, "{}\n")
let duplicateCompletionCommands = Array(state.commands.dropFirst(duplicateCompletionCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
duplicateCompletionCommands.contains { $0.hasPrefix("notify_target_async ") },
"Generic Grok completion after Stop fallback must not double-notify, saw \(duplicateCompletionCommands)"
)
@@ -2272,7 +2272,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: input,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
@@ -2280,16 +2280,16 @@ extension CLINotifyProcessIntegrationRegressionTests {
"session-start",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"SessionStart"}"#
)
- XCTAssertFalse(start.timedOut, start.stderr)
- XCTAssertEqual(start.status, 0, start.stderr)
+ legacyAssertFalse(start.timedOut, start.stderr)
+ legacyAssertEqual(start.status, 0, start.stderr)
for index in 1...2 {
let prompt = runGrokHook(
"prompt-submit",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"UserPromptSubmit","prompt":"prompt \#(index)"}"#
)
- XCTAssertFalse(prompt.timedOut, prompt.stderr)
- XCTAssertEqual(prompt.status, 0, prompt.stderr)
+ legacyAssertFalse(prompt.timedOut, prompt.stderr)
+ legacyAssertEqual(prompt.status, 0, prompt.stderr)
let message = "Turn complete in \(index).0s."
let commandStart = state.commands.count
@@ -2299,18 +2299,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
stallFeedTelemetry: index == 2
)
- XCTAssertFalse(notification.timedOut, notification.stderr)
- XCTAssertEqual(notification.status, 0, notification.stderr)
- XCTAssertEqual(notification.stdout, "{}\n")
+ legacyAssertFalse(notification.timedOut, notification.stderr)
+ legacyAssertEqual(notification.status, 0, notification.stderr)
+ legacyAssertEqual(notification.stdout, "{}\n")
let notificationCommands = Array(state.commands.dropFirst(commandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
notificationCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Completed|Task completed")
},
"Expected Grok completion notification for prompt \(index), saw \(notificationCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
notificationCommands.contains { $0.contains("set_status grok Idle") },
"Expected Grok completion for prompt \(index) to leave Grok idle, saw \(notificationCommands)"
)
@@ -2376,7 +2376,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: input,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
@@ -2385,9 +2385,9 @@ extension CLINotifyProcessIntegrationRegressionTests {
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"SessionStart"}"#,
environment: initialEnvironment
)
- XCTAssertFalse(start.timedOut, start.stderr)
- XCTAssertEqual(start.status, 0, start.stderr)
- XCTAssertEqual(start.stdout, "{}\n")
+ legacyAssertFalse(start.timedOut, start.stderr)
+ legacyAssertEqual(start.status, 0, start.stderr)
+ legacyAssertEqual(start.stdout, "{}\n")
for index in 1...2 {
let promptCommandStart = state.commands.count
@@ -2395,20 +2395,20 @@ extension CLINotifyProcessIntegrationRegressionTests {
"prompt-submit",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"UserPromptSubmit","prompt":"message \#(index)"}"#
)
- XCTAssertFalse(prompt.timedOut, prompt.stderr)
- XCTAssertEqual(prompt.status, 0, prompt.stderr)
- XCTAssertEqual(prompt.stdout, "{}\n")
+ legacyAssertFalse(prompt.timedOut, prompt.stderr)
+ legacyAssertEqual(prompt.status, 0, prompt.stderr)
+ legacyAssertEqual(prompt.stdout, "{}\n")
let promptCommands = Array(state.commands.dropFirst(promptCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
promptCommands.contains { $0.contains("set_status grok Running") },
"Expected Grok prompt \(index) to reuse the saved target without CMUX env, saw \(promptCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
promptCommands.contains { $0 == "clear_notifications --tab=\(workspaceId) --panel=\(surfaceId)" },
"Expected Grok prompt \(index) to clear only its own surface notifications, saw \(promptCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
promptCommands.contains { $0 == "clear_notifications --tab=\(workspaceId)" },
"Grok prompt \(index) must not clear sibling surface notifications, saw \(promptCommands)"
)
@@ -2418,12 +2418,12 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"SessionNotification { update: HookExecution { event_name: user_prompt_submit } }"}"#
)
- XCTAssertFalse(internalNotification.timedOut, internalNotification.stderr)
- XCTAssertEqual(internalNotification.status, 0, internalNotification.stderr)
- XCTAssertEqual(internalNotification.stdout, "{}\n")
+ legacyAssertFalse(internalNotification.timedOut, internalNotification.stderr)
+ legacyAssertEqual(internalNotification.status, 0, internalNotification.stderr)
+ legacyAssertEqual(internalNotification.stdout, "{}\n")
let internalCommands = Array(state.commands.dropFirst(internalCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
internalCommands.contains { $0.hasPrefix("notify_target_async ") },
"Grok internal prompt bookkeeping for chat message \(index) must not notify, saw \(internalCommands)"
)
@@ -2433,12 +2433,12 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"HookExecution { event_name: user_prompt_submit }"}"#
)
- XCTAssertFalse(bareInternalNotification.timedOut, bareInternalNotification.stderr)
- XCTAssertEqual(bareInternalNotification.status, 0, bareInternalNotification.stderr)
- XCTAssertEqual(bareInternalNotification.stdout, "{}\n")
+ legacyAssertFalse(bareInternalNotification.timedOut, bareInternalNotification.stderr)
+ legacyAssertEqual(bareInternalNotification.status, 0, bareInternalNotification.stderr)
+ legacyAssertEqual(bareInternalNotification.stdout, "{}\n")
let bareInternalCommands = Array(state.commands.dropFirst(bareInternalCommandStart))
- XCTAssertFalse(
+ legacyAssertFalse(
bareInternalCommands.contains { $0.hasPrefix("notify_target_async ") },
"Grok bare hook execution bookkeeping for chat message \(index) must not notify, saw \(bareInternalCommands)"
)
@@ -2448,18 +2448,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"Turn complete in \#(index).0s."}"#
)
- XCTAssertFalse(notification.timedOut, notification.stderr)
- XCTAssertEqual(notification.status, 0, notification.stderr)
- XCTAssertEqual(notification.stdout, "{}\n")
+ legacyAssertFalse(notification.timedOut, notification.stderr)
+ legacyAssertEqual(notification.status, 0, notification.stderr)
+ legacyAssertEqual(notification.stdout, "{}\n")
let notificationCommands = Array(state.commands.dropFirst(notificationCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
notificationCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(surfaceId) Grok|Completed|Task completed")
},
"Expected Grok completion notification for chat message \(index), saw \(notificationCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
notificationCommands.contains { $0.contains("set_status grok Idle") },
"Expected Grok completion for chat message \(index) to leave Grok idle, saw \(notificationCommands)"
)
@@ -2469,27 +2469,27 @@ extension CLINotifyProcessIntegrationRegressionTests {
"session-end",
input: #"{"sessionId":"\#(sessionId)","cwd":"\#(root.path)","hookEventName":"SessionEnd"}"#
)
- XCTAssertFalse(sessionEnd.timedOut, sessionEnd.stderr)
- XCTAssertEqual(sessionEnd.status, 0, sessionEnd.stderr)
- XCTAssertEqual(sessionEnd.stdout, "{}\n")
+ legacyAssertFalse(sessionEnd.timedOut, sessionEnd.stderr)
+ legacyAssertEqual(sessionEnd.status, 0, sessionEnd.stderr)
+ legacyAssertEqual(sessionEnd.stdout, "{}\n")
let sessionEndCommands = Array(state.commands.dropFirst(sessionEndCommandStart))
let sessionEndMethods = sessionEndCommands.compactMap { self.jsonObject($0)?["method"] as? String }
- XCTAssertEqual(
+ legacyAssertEqual(
sessionEndMethods,
["feed.push"],
"Grok SessionEnd should only emit feed telemetry from the saved route, saw \(sessionEndCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
sessionEndCommands.contains { $0.hasPrefix("clear_agent_pid grok.") },
"Grok SessionEnd is a chat-turn boundary and must not clear the saved route, saw \(sessionEndCommands)"
)
}
let storeURL = root.appendingPathComponent("grok-hook-sessions.json", isDirectory: false)
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- let sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
- XCTAssertNotNil(
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ let sessions = try legacyUnwrap(json["sessions"] as? [String: Any])
+ legacyAssertNotNil(
sessions[sessionId],
"Expected Grok route to remain available after multiple chat-message SessionEnd events"
)
@@ -2567,7 +2567,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: input,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
@@ -2576,38 +2576,38 @@ extension CLINotifyProcessIntegrationRegressionTests {
input: #"{"sessionId":"\#(runningSessionId)","cwd":"\#(root.path)","hookEventName":"SessionStart"}"#,
environment: environment(surfaceId: runningSurfaceId)
)
- XCTAssertFalse(runningStart.timedOut, runningStart.stderr)
- XCTAssertEqual(runningStart.status, 0, runningStart.stderr)
+ legacyAssertFalse(runningStart.timedOut, runningStart.stderr)
+ legacyAssertEqual(runningStart.status, 0, runningStart.stderr)
let completingStart = runGrokHook(
"session-start",
input: #"{"sessionId":"\#(completingSessionId)","cwd":"\#(root.path)","hookEventName":"SessionStart"}"#,
environment: environment(surfaceId: completingSurfaceId)
)
- XCTAssertFalse(completingStart.timedOut, completingStart.stderr)
- XCTAssertEqual(completingStart.status, 0, completingStart.stderr)
+ legacyAssertFalse(completingStart.timedOut, completingStart.stderr)
+ legacyAssertEqual(completingStart.status, 0, completingStart.stderr)
let runningStop = runGrokHook(
"stop",
input: #"{"sessionId":"\#(runningSessionId)","cwd":"\#(root.path)","hookEventName":"Stop"}"#
)
- XCTAssertFalse(runningStop.timedOut, runningStop.stderr)
- XCTAssertEqual(runningStop.status, 0, runningStop.stderr)
+ legacyAssertFalse(runningStop.timedOut, runningStop.stderr)
+ legacyAssertEqual(runningStop.status, 0, runningStop.stderr)
let promptCommandStart = state.commands.count
let runningPrompt = runGrokHook(
"prompt-submit",
input: #"{"sessionId":"\#(runningSessionId)","cwd":"\#(root.path)","hookEventName":"UserPromptSubmit","prompt":"keep running"}"#
)
- XCTAssertFalse(runningPrompt.timedOut, runningPrompt.stderr)
- XCTAssertEqual(runningPrompt.status, 0, runningPrompt.stderr)
+ legacyAssertFalse(runningPrompt.timedOut, runningPrompt.stderr)
+ legacyAssertEqual(runningPrompt.status, 0, runningPrompt.stderr)
let promptCommands = Array(state.commands.dropFirst(promptCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
promptCommands.contains { $0 == "clear_notifications --tab=\(workspaceId) --panel=\(runningSurfaceId)" },
"Expected running Grok prompt to clear only its own surface notifications, saw \(promptCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
promptCommands.contains { $0.contains("set_status grok Running") },
"Expected running Grok prompt to mark Grok running, saw \(promptCommands)"
)
@@ -2617,18 +2617,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
"notification",
input: #"{"sessionId":"\#(completingSessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"Turn complete in 1.0s."}"#
)
- XCTAssertFalse(completingNotification.timedOut, completingNotification.stderr)
- XCTAssertEqual(completingNotification.status, 0, completingNotification.stderr)
- XCTAssertEqual(completingNotification.stdout, "{}\n")
+ legacyAssertFalse(completingNotification.timedOut, completingNotification.stderr)
+ legacyAssertEqual(completingNotification.status, 0, completingNotification.stderr)
+ legacyAssertEqual(completingNotification.stdout, "{}\n")
let completionCommands = Array(state.commands.dropFirst(completionCommandStart))
- XCTAssertTrue(
+ legacyAssertTrue(
completionCommands.contains {
$0.contains("notify_target_async \(workspaceId) \(completingSurfaceId) Grok|Completed|Task completed")
},
"Expected completing Grok session to notify its own surface, saw \(completionCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
completionCommands.contains { $0.contains("set_status grok Idle") },
"Completing Grok session must not reset the shared Grok status while a sibling session is running, saw \(completionCommands)"
)
@@ -2718,21 +2718,21 @@ extension CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"sessionId":"\#(completingSessionId)","cwd":"\#(root.path)","hookEventName":"Notification","message":"Turn complete in 1.0s."}"#,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(completion.timedOut, completion.stderr)
- XCTAssertEqual(completion.status, 0, completion.stderr)
- XCTAssertEqual(completion.stdout, "{}\n")
+ legacyAssertFalse(completion.timedOut, completion.stderr)
+ legacyAssertEqual(completion.status, 0, completion.stderr)
+ legacyAssertEqual(completion.stdout, "{}\n")
- XCTAssertTrue(
+ legacyAssertTrue(
state.commands.contains { $0.contains("set_status grok Idle") },
"Dead PID running records must not keep the shared Grok status running, saw \(state.commands)"
)
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- let sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
- let staleSession = try XCTUnwrap(sessions[staleSessionId] as? [String: Any])
- XCTAssertNil(
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ let sessions = try legacyUnwrap(json["sessions"] as? [String: Any])
+ let staleSession = try legacyUnwrap(sessions[staleSessionId] as? [String: Any])
+ legacyAssertNil(
staleSession["runtimeStatus"],
"Dead PID running records should be cleared when they are ignored"
)
@@ -2830,16 +2830,16 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let hookURL = root
.appendingPathComponent(".grok", isDirectory: true)
.appendingPathComponent("hooks", isDirectory: true)
.appendingPathComponent("cmux-session.json", isDirectory: false)
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: hookURL)) as? [String: Any])
- let hooks = try XCTUnwrap(json["hooks"] as? [String: Any])
- let notificationGroups = try XCTUnwrap(hooks["Notification"] as? [[String: Any]])
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: hookURL)) as? [String: Any])
+ let hooks = try legacyUnwrap(json["hooks"] as? [String: Any])
+ let notificationGroups = try legacyUnwrap(hooks["Notification"] as? [[String: Any]])
let notificationCommands = notificationGroups
.compactMap { $0["hooks"] as? [[String: Any]] }
.flatMap { $0 }
@@ -2848,7 +2848,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
.compactMap { $0["hooks"] as? [[String: Any]] }
.flatMap { $0 }
.compactMap { $0["timeout"] as? Int }
- let preToolUseGroups = try XCTUnwrap(hooks["PreToolUse"] as? [[String: Any]])
+ let preToolUseGroups = try legacyUnwrap(hooks["PreToolUse"] as? [[String: Any]])
let preToolUseTimeouts = preToolUseGroups
.compactMap { $0["hooks"] as? [[String: Any]] }
.flatMap { $0 }
@@ -2860,25 +2860,25 @@ extension CLINotifyProcessIntegrationRegressionTests {
.flatMap { $0 }
.compactMap { $0["command"] as? String }
- XCTAssertTrue(
+ legacyAssertTrue(
notificationCommands.contains { $0.contains("cmux hooks grok notification") },
"Expected Grok Notification to dispatch to the notification handler, saw \(notificationCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
notificationCommands.contains { $0.contains("cmux hooks grok stop") },
"Grok Notification should not use the generic stop handler, saw \(notificationCommands)"
)
- XCTAssertEqual(notificationTimeouts, [5])
- XCTAssertEqual(preToolUseTimeouts, [120])
- XCTAssertFalse(
+ legacyAssertEqual(notificationTimeouts, [5])
+ legacyAssertEqual(preToolUseTimeouts, [120])
+ legacyAssertFalse(
allCommands.contains { $0.contains("[ -n \"$CMUX_SURFACE_ID\" ]") },
"Grok strips CMUX_* from hook subprocesses, so installed commands must not gate on CMUX_SURFACE_ID. Saw \(allCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
allCommands.contains { $0.contains("$CMUX_") },
"Grok treats $VAR references as required hook environment, so installed commands must avoid CMUX variable interpolation. Saw \(allCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
FileManager.default.fileExists(atPath: legacyHookURL.path),
"Expected setup to remove legacy cmux-owned Grok hook file"
)
@@ -2909,15 +2909,15 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let hookURL = root
.appendingPathComponent(".grok", isDirectory: true)
.appendingPathComponent("hooks", isDirectory: true)
.appendingPathComponent("cmux-session.json", isDirectory: false)
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: hookURL)) as? [String: Any])
- let hooks = try XCTUnwrap(json["hooks"] as? [String: Any])
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: hookURL)) as? [String: Any])
+ let hooks = try legacyUnwrap(json["hooks"] as? [String: Any])
let allCommands = hooks.values
.compactMap { $0 as? [[String: Any]] }
.flatMap { $0 }
@@ -2925,20 +2925,20 @@ extension CLINotifyProcessIntegrationRegressionTests {
.flatMap { $0 }
.compactMap { $0["command"] as? String }
- XCTAssertFalse(allCommands.isEmpty)
- XCTAssertTrue(
+ legacyAssertFalse(allCommands.isEmpty)
+ legacyAssertTrue(
allCommands.allSatisfy { $0.contains("cmux-grok-hook-v2") },
"Expected installed Grok hooks to carry the owned-hook marker, saw \(allCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
allCommands.allSatisfy { $0.contains("'\(pinnedCLI.path)'") },
"Expected installed Grok hooks to pin the installing CLI path, saw \(allCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
allCommands.allSatisfy { $0.contains("--socket '\(socketPath)'") },
"Expected installed Grok hooks to pin the installing socket path, saw \(allCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
allCommands.contains { $0.contains("$CMUX_") },
"Grok hook commands must not depend on CMUX environment interpolation, saw \(allCommands)"
)
@@ -2992,19 +2992,19 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
- let legacyJSON = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: legacyHookURL)) as? [String: Any])
- let hooks = try XCTUnwrap(legacyJSON["hooks"] as? [String: Any])
- let notificationGroups = try XCTUnwrap(hooks["Notification"] as? [[String: Any]])
+ let legacyJSON = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: legacyHookURL)) as? [String: Any])
+ let hooks = try legacyUnwrap(legacyJSON["hooks"] as? [String: Any])
+ let notificationGroups = try legacyUnwrap(hooks["Notification"] as? [[String: Any]])
let commands = notificationGroups
.compactMap { $0["hooks"] as? [[String: Any]] }
.flatMap { $0 }
.compactMap { $0["command"] as? String }
- XCTAssertEqual(commands, [preservedCommand])
- XCTAssertFalse(
+ legacyAssertEqual(commands, [preservedCommand])
+ legacyAssertFalse(
commands.contains { $0.hasPrefix("[ \"$CMUX_GROK_HOOKS_DISABLED\"") },
"Expected setup to remove only exact cmux-owned legacy commands, saw \(commands)"
)
@@ -3053,12 +3053,12 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
- let legacyJSON = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: legacyHookURL)) as? [String: Any])
- XCTAssertEqual(legacyJSON["version"] as? Int, 1)
- XCTAssertNil(legacyJSON["hooks"])
+ let legacyJSON = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: legacyHookURL)) as? [String: Any])
+ legacyAssertEqual(legacyJSON["version"] as? Int, 1)
+ legacyAssertNil(legacyJSON["hooks"])
}
@Test
func testCodexHookInstallPrefersLaunchingAppBundledCLI() throws {
@@ -3109,12 +3109,12 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let hookURL = codexHome.appendingPathComponent("hooks.json", isDirectory: false)
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: hookURL)) as? [String: Any])
- let hooks = try XCTUnwrap(json["hooks"] as? [String: Any])
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: hookURL)) as? [String: Any])
+ let hooks = try legacyUnwrap(json["hooks"] as? [String: Any])
let allCommands = hooks.values
.compactMap { $0 as? [[String: Any]] }
.flatMap { $0 }
@@ -3122,22 +3122,22 @@ extension CLINotifyProcessIntegrationRegressionTests {
.flatMap { $0 }
.compactMap { $0["command"] as? String }
- XCTAssertTrue(
+ legacyAssertTrue(
allCommands.contains {
$0.contains("CMUX_BUNDLED_CLI_PATH")
&& $0.contains("\"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" hooks codex prompt-submit")
},
"Codex hooks should route through the launching app's bundled CLI, saw \(allCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
allCommands.contains { $0.contains("command -v cmux >/dev/null 2>&1 && cmux hooks codex") },
"Codex hooks must not use the reload-global cmux shim directly, saw \(allCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
allCommands.contains { $0 == previousBundledHookCommand },
"Codex setup should replace bundled-CLI hooks that did not pin CMUX_SOCKET_PATH, saw \(allCommands)"
)
- XCTAssertEqual(
+ legacyAssertEqual(
allCommands.filter { $0.contains("hooks codex prompt-submit") }.count,
1,
"Codex setup should collapse duplicate cmux-owned prompt hooks to one entry, saw \(allCommands)"
@@ -3166,19 +3166,19 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertNotEqual(result.status, 0, result.stdout)
- XCTAssertTrue(
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertNotEqual(result.status, 0, result.stdout)
+ legacyAssertTrue(
result.stderr.contains("cmux could not create the hooks directory: a file exists at \(hooksPath.path); remove or rename the conflicting file and re-run `cmux hooks setup`"),
result.stderr
)
- XCTAssertFalse(
+ legacyAssertFalse(
result.stdout.contains("Required agent configuration is missing."),
result.stdout
)
var isDirectory: ObjCBool = true
- XCTAssertTrue(FileManager.default.fileExists(atPath: hooksPath.path, isDirectory: &isDirectory))
- XCTAssertFalse(isDirectory.boolValue)
+ legacyAssertTrue(FileManager.default.fileExists(atPath: hooksPath.path, isDirectory: &isDirectory))
+ legacyAssertFalse(isDirectory.boolValue)
}
func runGenericHookPersistenceScenario(_ scenario: GenericHookPersistenceScenario) throws {
@@ -3244,25 +3244,25 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
let storeURL = root.appendingPathComponent("\(scenario.agent)-hook-sessions.json", isDirectory: false)
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- let sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
- let session = try XCTUnwrap(sessions[scenario.sessionId] as? [String: Any])
- XCTAssertEqual(session["workspaceId"] as? String, workspaceId)
- XCTAssertEqual(session["surfaceId"] as? String, surfaceId)
- XCTAssertEqual(session["cwd"] as? String, workspace.path)
-
- let launchCommand = try XCTUnwrap(session["launchCommand"] as? [String: Any])
- XCTAssertEqual(launchCommand["launcher"] as? String, scenario.agent)
- XCTAssertEqual(launchCommand["executablePath"] as? String, scenario.executable)
- XCTAssertEqual(launchCommand["arguments"] as? [String], scenario.expectedArguments)
- XCTAssertEqual(launchCommand["workingDirectory"] as? String, workspace.path)
- XCTAssertEqual(launchCommand["environment"] as? [String: String], scenario.expectedEnvironment)
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ let sessions = try legacyUnwrap(json["sessions"] as? [String: Any])
+ let session = try legacyUnwrap(sessions[scenario.sessionId] as? [String: Any])
+ legacyAssertEqual(session["workspaceId"] as? String, workspaceId)
+ legacyAssertEqual(session["surfaceId"] as? String, surfaceId)
+ legacyAssertEqual(session["cwd"] as? String, workspace.path)
+
+ let launchCommand = try legacyUnwrap(session["launchCommand"] as? [String: Any])
+ legacyAssertEqual(launchCommand["launcher"] as? String, scenario.agent)
+ legacyAssertEqual(launchCommand["executablePath"] as? String, scenario.executable)
+ legacyAssertEqual(launchCommand["arguments"] as? [String], scenario.expectedArguments)
+ legacyAssertEqual(launchCommand["workingDirectory"] as? String, workspace.path)
+ legacyAssertEqual(launchCommand["environment"] as? [String: String], scenario.expectedEnvironment)
if scenario.agent == "kiro" {
let resumeSetRequests = state.commands.compactMap { command -> [String: Any]? in
@@ -3272,17 +3272,17 @@ extension CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- XCTAssertEqual(resumeSetRequests.count, 1, state.commands.joined(separator: "\n"))
- let params = try XCTUnwrap(resumeSetRequests.first)
- XCTAssertEqual(params["kind"] as? String, "kiro")
- XCTAssertEqual(params["checkpoint_id"] as? String, scenario.sessionId)
- XCTAssertEqual(params["auto_resume"] as? Bool, true)
- XCTAssertEqual(
+ legacyAssertEqual(resumeSetRequests.count, 1, state.commands.joined(separator: "\n"))
+ let params = try legacyUnwrap(resumeSetRequests.first)
+ legacyAssertEqual(params["kind"] as? String, "kiro")
+ legacyAssertEqual(params["checkpoint_id"] as? String, scenario.sessionId)
+ legacyAssertEqual(params["auto_resume"] as? Bool, true)
+ legacyAssertEqual(
params["command"] as? String,
"{ cd -- '\(workspace.path)' 2>/dev/null || [ ! -d '\(workspace.path)' ]; } && '\(scenario.executable)' 'chat' '--resume-id' '\(scenario.sessionId)' '--agent' 'cmux' '--trust-tools' 'fs_read,fs_write'"
)
- XCTAssertEqual(params["environment"] as? [String: String], scenario.expectedEnvironment)
- XCTAssertFalse(
+ legacyAssertEqual(params["environment"] as? [String: String], scenario.expectedEnvironment)
+ legacyAssertFalse(
state.commands.contains { command in
self.jsonObject(command)?["method"] as? String == "surface.resume.clear"
},
@@ -3390,37 +3390,37 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let resumeRequests = state.snapshot().compactMap { command -> [String: Any]? in
guard let payload = self.jsonObject(command),
payload["method"] as? String == "surface.resume.set" else { return nil }
return payload["params"] as? [String: Any]
}
- let params = try XCTUnwrap(resumeRequests.last, "expected a surface.resume.set; saw \(state.snapshot())")
+ let params = try legacyUnwrap(resumeRequests.last, "expected a surface.resume.set; saw \(state.snapshot())")
let boundEnvironment = params["environment"] as? [String: String]
- XCTAssertEqual(
+ legacyAssertEqual(
boundEnvironment?["CODEX_HOME"], codexHome,
"resume binding must carry the captured CODEX_HOME; params=\(params)"
)
- let command = try XCTUnwrap(params["command"] as? String)
- XCTAssertTrue(command.contains("'resume' '\(sessionId)'"), command)
+ let command = try legacyUnwrap(params["command"] as? String)
+ legacyAssertTrue(command.contains("'resume' '\(sessionId)'"), command)
// The env-only record must also be PERSISTED to the hook session store (its arguments are
// empty, so the store's "only assign launchCommand when arguments is non-empty" gate would
// otherwise drop it) — a later fork/resume that reads the store rather than re-deriving from a
// live hook env still needs CODEX_HOME.
let storeURL = root.appendingPathComponent("codex-hook-sessions.json")
- let storeJSON = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- let sessions = try XCTUnwrap(storeJSON["sessions"] as? [String: Any])
- let persisted = try XCTUnwrap(sessions[sessionId] as? [String: Any])
- let persistedLaunch = try XCTUnwrap(
+ let storeJSON = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ let sessions = try legacyUnwrap(storeJSON["sessions"] as? [String: Any])
+ let persisted = try legacyUnwrap(sessions[sessionId] as? [String: Any])
+ let persistedLaunch = try legacyUnwrap(
persisted["launchCommand"] as? [String: Any],
"env-only launchCommand must be persisted for the fork path"
)
- XCTAssertEqual(
+ legacyAssertEqual(
(persistedLaunch["environment"] as? [String: String])?["CODEX_HOME"], codexHome,
"persisted launchCommand must carry CODEX_HOME"
)
@@ -3506,17 +3506,17 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let resumeRequests = state.snapshot().compactMap { command -> [String: Any]? in
guard let payload = self.jsonObject(command),
payload["method"] as? String == "surface.resume.set" else { return nil }
return payload["params"] as? [String: Any]
}
- let params = try XCTUnwrap(resumeRequests.last, "expected a surface.resume.set; saw \(state.snapshot())")
- XCTAssertEqual(
+ let params = try legacyUnwrap(resumeRequests.last, "expected a surface.resume.set; saw \(state.snapshot())")
+ legacyAssertEqual(
params["surface_id"] as? String, ttySurfaceId,
"PID/TTY ground truth must override the leaked env CMUX_SURFACE_ID; params=\(params)"
)
@@ -3595,20 +3595,20 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let resumeRequests = state.snapshot().compactMap { command -> [String: Any]? in
guard let payload = self.jsonObject(command),
payload["method"] as? String == "surface.resume.set" else { return nil }
return payload["params"] as? [String: Any]
}
- let params = try XCTUnwrap(
+ let params = try legacyUnwrap(
resumeRequests.last,
"stale ambient surface must not drop the hook; expected a surface.resume.set, saw \(state.snapshot())"
)
- XCTAssertEqual(
+ legacyAssertEqual(
params["surface_id"] as? String, ttySurfaceId,
"a stale ambient CMUX_SURFACE_ID must fall through to the TTY pane; params=\(params)"
)
@@ -3686,9 +3686,9 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
// No env-only CODEX_HOME record may be persisted for the rejected non-restorable argv.
let storeURL = root.appendingPathComponent("codex-hook-sessions.json")
@@ -3697,7 +3697,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
let sessions = storeJSON["sessions"] as? [String: Any],
let persisted = sessions[sessionId] as? [String: Any] {
let env = (persisted["launchCommand"] as? [String: Any])?["environment"] as? [String: String]
- XCTAssertNil(
+ legacyAssertNil(
env?["CODEX_HOME"],
"non-restorable codex exec must not persist an env-only CODEX_HOME record; launchCommand=\(persisted["launchCommand"] ?? "nil")"
)
diff --git a/cmuxTests/CLILegacyHookAliasTests.swift b/cmuxTests/CLILegacyHookAliasTests.swift
index 9a10b0174d29..7e2df12d0e3b 100644
--- a/cmuxTests/CLILegacyHookAliasTests.swift
+++ b/cmuxTests/CLILegacyHookAliasTests.swift
@@ -53,20 +53,20 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
- XCTAssertFalse(result.stdout.contains("Usage:"), result.stdout)
- XCTAssertFalse(result.stderr.contains("Usage:"), result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
+ legacyAssertFalse(result.stdout.contains("Usage:"), result.stdout)
+ legacyAssertFalse(result.stderr.contains("Usage:"), result.stderr)
let storeURL = root.appendingPathComponent("codex-hook-sessions.json", isDirectory: false)
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- let sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
- let session = try XCTUnwrap(sessions[surfaceId] as? [String: Any])
- XCTAssertEqual(session["workspaceId"] as? String, workspaceId)
- XCTAssertEqual(session["surfaceId"] as? String, surfaceId)
- XCTAssertNotNil(session["launchCommand"] as? [String: Any])
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ let sessions = try legacyUnwrap(json["sessions"] as? [String: Any])
+ let session = try legacyUnwrap(sessions[surfaceId] as? [String: Any])
+ legacyAssertEqual(session["workspaceId"] as? String, workspaceId)
+ legacyAssertEqual(session["surfaceId"] as? String, surfaceId)
+ legacyAssertNotNil(session["launchCommand"] as? [String: Any])
}
@Test
func testLegacyFeedHookAliasReturnsJSONWithoutHelpOutsideCmuxTerminal() throws {
@@ -85,10 +85,10 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
- XCTAssertFalse(result.stdout.contains("Usage:"), result.stdout)
- XCTAssertFalse(result.stderr.contains("Usage:"), result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
+ legacyAssertFalse(result.stdout.contains("Usage:"), result.stdout)
+ legacyAssertFalse(result.stderr.contains("Usage:"), result.stderr)
}
}
diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
index de93c706755b..1b5055b30d6a 100644
--- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
+++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
@@ -32,14 +32,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"clear-session","source":"clear","cwd":"\#(context.root.path)","hook_event_name":"SessionStart"}"#
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
- XCTAssertTrue(
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
+ legacyAssertTrue(
context.state.commands.contains { $0 == "clear_notifications --tab=\(context.workspaceId)" },
"Expected clear SessionStart to clear stale notifications, saw \(context.state.commands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
context.state.commands.contains {
$0.hasPrefix("set_status claude_code Running --icon=bolt.fill --color=#4C8DFF --tab=\(context.workspaceId)")
&& $0.contains("--panel=\(context.surfaceId)")
@@ -59,16 +59,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
arguments: ["hooks", "claude", "session-start"],
standardInput: #"{"session_id":"\#(sessionId)","source":"startup","cwd":"\#(context.root.path)","transcript_path":"\#(context.root.path)/projects/startup-only-session.jsonl","hook_event_name":"SessionStart"}"#
)
- XCTAssertFalse(start.timedOut, start.stderr)
- XCTAssertEqual(start.status, 0, start.stderr)
+ legacyAssertFalse(start.timedOut, start.stderr)
+ legacyAssertEqual(start.status, 0, start.stderr)
var record = try readClaudeHookSession(sessionId, context: context)
- XCTAssertEqual(
+ legacyAssertEqual(
record["isRestorable"] as? Bool,
false,
"Startup SessionStart records are only routing state until Claude creates a conversation."
)
- XCTAssertEqual(
+ legacyAssertEqual(
record["transcriptPath"] as? String,
"\(context.root.path)/projects/startup-only-session.jsonl"
)
@@ -78,11 +78,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
arguments: ["hooks", "claude", "prompt-submit"],
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"turn-1","cwd":"\#(context.root.path)","transcript_path":"\#(context.root.path)/projects/startup-only-session.jsonl","hook_event_name":"UserPromptSubmit"}"#
)
- XCTAssertFalse(prompt.timedOut, prompt.stderr)
- XCTAssertEqual(prompt.status, 0, prompt.stderr)
+ legacyAssertFalse(prompt.timedOut, prompt.stderr)
+ legacyAssertEqual(prompt.status, 0, prompt.stderr)
record = try readClaudeHookSession(sessionId, context: context)
- XCTAssertEqual(
+ legacyAssertEqual(
record["isRestorable"] as? Bool,
true,
"UserPromptSubmit marks the session eligible for resume."
@@ -118,15 +118,15 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"tail-session","turn_id":"turn-1","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"echo recent"}}"#
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- let preToolEvent = try XCTUnwrap(
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ let preToolEvent = try legacyUnwrap(
feedPushEvents(in: context).last { $0["hook_event_name"] as? String == "PreToolUse" }
)
- let feedContext = try XCTUnwrap(preToolEvent["context"] as? [String: Any])
- XCTAssertEqual(feedContext["lastUserMessage"] as? String, "recent user message")
- XCTAssertEqual(feedContext["assistantPreamble"] as? String, "recent assistant response")
- XCTAssertFalse(String(describing: feedContext).contains("ancient"), "\(feedContext)")
+ let feedContext = try legacyUnwrap(preToolEvent["context"] as? [String: Any])
+ legacyAssertEqual(feedContext["lastUserMessage"] as? String, "recent user message")
+ legacyAssertEqual(feedContext["assistantPreamble"] as? String, "recent assistant response")
+ legacyAssertFalse(String(describing: feedContext).contains("ancient"), "\(feedContext)")
}
@Test
@@ -152,14 +152,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"oversized-final-session","turn_id":"turn-1","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"echo huge"}}"#
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- let preToolEvent = try XCTUnwrap(
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ let preToolEvent = try legacyUnwrap(
feedPushEvents(in: context).last { $0["hook_event_name"] as? String == "PreToolUse" }
)
- let feedContext = try XCTUnwrap(preToolEvent["context"] as? [String: Any])
- let assistantPreamble = try XCTUnwrap(feedContext["assistantPreamble"] as? String)
- XCTAssertTrue(assistantPreamble.hasPrefix("recent assistant response"), "\(feedContext)")
+ let feedContext = try legacyUnwrap(preToolEvent["context"] as? [String: Any])
+ let assistantPreamble = try legacyUnwrap(feedContext["assistantPreamble"] as? String)
+ legacyAssertTrue(assistantPreamble.hasPrefix("recent assistant response"), "\(feedContext)")
}
@Test
@@ -187,9 +187,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"codex-oversized-final-session","turn_id":"\#(turnId)","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":null}"#
)
- XCTAssertFalse(stop.timedOut, stop.stderr)
- XCTAssertEqual(stop.status, 0, stop.stderr)
- XCTAssertTrue(
+ legacyAssertFalse(stop.timedOut, stop.stderr)
+ legacyAssertEqual(stop.status, 0, stop.stderr)
+ legacyAssertTrue(
context.state.commands.contains { command in
command.contains("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|Error|Codex ended before sending a final response")
},
@@ -210,8 +210,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
environment: ["PATH": "/usr/bin:/bin:/usr/sbin:/sbin"],
timeout: 10
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
guard result.status == 0 else {
throw NSError(domain: "CLINotifyProcessIntegrationRegressionTests.git", code: Int(result.status))
}
@@ -219,8 +219,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
func baselineRecords() throws -> [[String: Any]] {
let storeURL = context.root.appendingPathComponent("agent-turn-diff-baselines.json")
- let store = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- return try XCTUnwrap(store["records"] as? [[String: Any]])
+ let store = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ return try legacyUnwrap(store["records"] as? [[String: Any]])
}
_ = try runGit(["init"])
@@ -240,8 +240,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"turn-0","cwd":"\#(context.root.path)","hook_event_name":"SessionStart"}"#,
extraEnvironment: codexLaunchEnvironment(context: context, sessionId: sessionId)
)
- XCTAssertFalse(sessionStart.timedOut, sessionStart.stderr)
- XCTAssertEqual(sessionStart.status, 0, sessionStart.stderr)
+ legacyAssertFalse(sessionStart.timedOut, sessionStart.stderr)
+ legacyAssertEqual(sessionStart.status, 0, sessionStart.stderr)
try "one\ntwo\n".write(to: storyURL, atomically: true, encoding: .utf8)
_ = try runGit(["add", "story.txt"])
@@ -254,16 +254,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit"}"#,
extraEnvironment: codexLaunchEnvironment(context: context, sessionId: sessionId)
)
- XCTAssertFalse(promptSubmit.timedOut, promptSubmit.stderr)
- XCTAssertEqual(promptSubmit.status, 0, promptSubmit.stderr)
+ legacyAssertFalse(promptSubmit.timedOut, promptSubmit.stderr)
+ legacyAssertEqual(promptSubmit.status, 0, promptSubmit.stderr)
let records = try baselineRecords()
- let startRecord = try XCTUnwrap(records.first { $0["turnId"] as? String == "turn-0" })
- let promptRecord = try XCTUnwrap(records.first { $0["turnId"] as? String == "turn-1" })
- XCTAssertEqual(startRecord["baseCommit"] as? String, initialCommit)
- XCTAssertEqual(promptRecord["baseCommit"] as? String, promptCommit)
- XCTAssertEqual(promptRecord["workspaceId"] as? String, context.workspaceId)
- XCTAssertEqual(promptRecord["surfaceId"] as? String, context.surfaceId)
+ let startRecord = try legacyUnwrap(records.first { $0["turnId"] as? String == "turn-0" })
+ let promptRecord = try legacyUnwrap(records.first { $0["turnId"] as? String == "turn-1" })
+ legacyAssertEqual(startRecord["baseCommit"] as? String, initialCommit)
+ legacyAssertEqual(promptRecord["baseCommit"] as? String, promptCommit)
+ legacyAssertEqual(promptRecord["workspaceId"] as? String, context.workspaceId)
+ legacyAssertEqual(promptRecord["surfaceId"] as? String, context.surfaceId)
try "one\ntwo\nnested\n".write(to: storyURL, atomically: true, encoding: .utf8)
_ = try runGit(["add", "story.txt"])
@@ -274,16 +274,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit"}"#,
extraEnvironment: codexLaunchEnvironment(context: context, sessionId: sessionId)
)
- XCTAssertFalse(childPrompt.timedOut, childPrompt.stderr)
- XCTAssertEqual(childPrompt.status, 0, childPrompt.stderr)
+ legacyAssertFalse(childPrompt.timedOut, childPrompt.stderr)
+ legacyAssertEqual(childPrompt.status, 0, childPrompt.stderr)
let childRecords = try baselineRecords()
- XCTAssertNil(
+ legacyAssertNil(
childRecords.first { $0["turnId"] as? String == "child-turn" },
"Nested Codex prompts should not create a last-turn diff baseline."
)
- let parentRecordAfterChild = try XCTUnwrap(childRecords.first { $0["turnId"] as? String == "turn-1" })
- XCTAssertEqual(parentRecordAfterChild["baseCommit"] as? String, promptCommit)
+ let parentRecordAfterChild = try legacyUnwrap(childRecords.first { $0["turnId"] as? String == "turn-1" })
+ legacyAssertEqual(parentRecordAfterChild["baseCommit"] as? String, promptCommit)
let childStop = runCodexHook(
context: context,
@@ -291,8 +291,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"child done"}"#,
extraEnvironment: codexLaunchEnvironment(context: context, sessionId: sessionId)
)
- XCTAssertFalse(childStop.timedOut, childStop.stderr)
- XCTAssertEqual(childStop.status, 0, childStop.stderr)
+ legacyAssertFalse(childStop.timedOut, childStop.stderr)
+ legacyAssertEqual(childStop.status, 0, childStop.stderr)
let parentStop = runCodexHook(
context: context,
@@ -300,8 +300,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"parent done"}"#,
extraEnvironment: codexLaunchEnvironment(context: context, sessionId: sessionId)
)
- XCTAssertFalse(parentStop.timedOut, parentStop.stderr)
- XCTAssertEqual(parentStop.status, 0, parentStop.stderr)
+ legacyAssertFalse(parentStop.timedOut, parentStop.stderr)
+ legacyAssertEqual(parentStop.status, 0, parentStop.stderr)
try "one\ntwo\nthree\n".write(to: storyURL, atomically: true, encoding: .utf8)
let dirtyPromptSubmit = runCodexHook(
@@ -310,13 +310,13 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit"}"#,
extraEnvironment: codexLaunchEnvironment(context: context, sessionId: sessionId)
)
- XCTAssertFalse(dirtyPromptSubmit.timedOut, dirtyPromptSubmit.stderr)
- XCTAssertEqual(dirtyPromptSubmit.status, 0, dirtyPromptSubmit.stderr)
+ legacyAssertFalse(dirtyPromptSubmit.timedOut, dirtyPromptSubmit.stderr)
+ legacyAssertEqual(dirtyPromptSubmit.status, 0, dirtyPromptSubmit.stderr)
let dirtyRecords = try baselineRecords()
- let dirtyRecord = try XCTUnwrap(dirtyRecords.first { $0["turnId"] as? String == "turn-1" })
- let dirtyBaseCommit = try XCTUnwrap(dirtyRecord["baseCommit"] as? String)
- XCTAssertEqual(dirtyBaseCommit, promptCommit)
+ let dirtyRecord = try legacyUnwrap(dirtyRecords.first { $0["turnId"] as? String == "turn-1" })
+ let dirtyBaseCommit = try legacyUnwrap(dirtyRecord["baseCommit"] as? String)
+ legacyAssertEqual(dirtyBaseCommit, promptCommit)
let dirtyStop = runCodexHook(
context: context,
@@ -324,8 +324,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"dirty done"}"#,
extraEnvironment: codexLaunchEnvironment(context: context, sessionId: sessionId)
)
- XCTAssertFalse(dirtyStop.timedOut, dirtyStop.stderr)
- XCTAssertEqual(dirtyStop.status, 0, dirtyStop.stderr)
+ legacyAssertFalse(dirtyStop.timedOut, dirtyStop.stderr)
+ legacyAssertEqual(dirtyStop.status, 0, dirtyStop.stderr)
try "one\ntwo\nthree\nfour\n".write(to: storyURL, atomically: true, encoding: .utf8)
let refreshedDirtyPromptSubmit = runCodexHook(
@@ -334,14 +334,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit"}"#,
extraEnvironment: codexLaunchEnvironment(context: context, sessionId: sessionId)
)
- XCTAssertFalse(refreshedDirtyPromptSubmit.timedOut, refreshedDirtyPromptSubmit.stderr)
- XCTAssertEqual(refreshedDirtyPromptSubmit.status, 0, refreshedDirtyPromptSubmit.stderr)
+ legacyAssertFalse(refreshedDirtyPromptSubmit.timedOut, refreshedDirtyPromptSubmit.stderr)
+ legacyAssertEqual(refreshedDirtyPromptSubmit.status, 0, refreshedDirtyPromptSubmit.stderr)
let refreshedRecords = try baselineRecords()
- let refreshedRecord = try XCTUnwrap(refreshedRecords.first { $0["turnId"] as? String == "turn-1" })
- let refreshedBaseCommit = try XCTUnwrap(refreshedRecord["baseCommit"] as? String)
- XCTAssertEqual(refreshedBaseCommit, dirtyBaseCommit)
- XCTAssertEqual(refreshedBaseCommit, promptCommit)
+ let refreshedRecord = try legacyUnwrap(refreshedRecords.first { $0["turnId"] as? String == "turn-1" })
+ let refreshedBaseCommit = try legacyUnwrap(refreshedRecord["baseCommit"] as? String)
+ legacyAssertEqual(refreshedBaseCommit, dirtyBaseCommit)
+ legacyAssertEqual(refreshedBaseCommit, promptCommit)
}
@Test
@@ -354,41 +354,41 @@ final class CLINotifyProcessIntegrationRegressionTests {
arguments: ["hooks", "claude", "session-start"],
standardInput: #"{"session_id":"old-session","cwd":"\#(context.root.path)","hook_event_name":"SessionStart"}"#
)
- XCTAssertFalse(oldStart.timedOut, oldStart.stderr)
- XCTAssertEqual(oldStart.status, 0, oldStart.stderr)
+ legacyAssertFalse(oldStart.timedOut, oldStart.stderr)
+ legacyAssertEqual(oldStart.status, 0, oldStart.stderr)
let clearStart = runClaudeHook(
context: context,
arguments: ["hooks", "claude", "session-start"],
standardInput: #"{"session_id":"clear-session","source":"clear","cwd":"\#(context.root.path)","hook_event_name":"SessionStart"}"#
)
- XCTAssertFalse(clearStart.timedOut, clearStart.stderr)
- XCTAssertEqual(clearStart.status, 0, clearStart.stderr)
+ legacyAssertFalse(clearStart.timedOut, clearStart.stderr)
+ legacyAssertEqual(clearStart.status, 0, clearStart.stderr)
let lateOldStart = runClaudeHook(
context: context,
arguments: ["hooks", "claude", "session-start"],
standardInput: #"{"session_id":"old-session","source":"startup","cwd":"\#(context.root.path)","hook_event_name":"SessionStart"}"#
)
- XCTAssertFalse(lateOldStart.timedOut, lateOldStart.stderr)
- XCTAssertEqual(lateOldStart.status, 0, lateOldStart.stderr)
+ legacyAssertFalse(lateOldStart.timedOut, lateOldStart.stderr)
+ legacyAssertEqual(lateOldStart.status, 0, lateOldStart.stderr)
let staleStop = runClaudeHook(
context: context,
arguments: ["hooks", "claude", "stop"],
standardInput: #"{"session_id":"old-session","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"old turn finished late"}"#
)
- XCTAssertFalse(staleStop.timedOut, staleStop.stderr)
- XCTAssertEqual(staleStop.status, 0, staleStop.stderr)
+ legacyAssertFalse(staleStop.timedOut, staleStop.stderr)
+ legacyAssertEqual(staleStop.status, 0, staleStop.stderr)
- XCTAssertTrue(
+ legacyAssertTrue(
context.state.commands.contains {
$0.hasPrefix("set_status claude_code Running --icon=bolt.fill --color=#4C8DFF --tab=\(context.workspaceId)")
&& $0.contains("--panel=\(context.surfaceId)")
},
"Expected clear SessionStart to mark Claude running, saw \(context.state.commands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
context.state.commands.contains {
$0.hasPrefix("set_status claude_code Idle ") && $0.contains("--tab=\(context.workspaceId)")
},
@@ -401,9 +401,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- XCTAssertEqual(resumeBindingRequests.count, 1, context.state.commands.joined(separator: "\n"))
- XCTAssertEqual(resumeBindingRequests.first?["checkpoint_id"] as? String, "clear-session")
- XCTAssertEqual(resumeBindingRequests.first?["auto_resume"] as? Bool, true)
+ legacyAssertEqual(resumeBindingRequests.count, 1, context.state.commands.joined(separator: "\n"))
+ legacyAssertEqual(resumeBindingRequests.first?["checkpoint_id"] as? String, "clear-session")
+ legacyAssertEqual(resumeBindingRequests.first?["auto_resume"] as? Bool, true)
}
@Test
@@ -416,32 +416,32 @@ final class CLINotifyProcessIntegrationRegressionTests {
arguments: ["hooks", "claude", "session-start"],
standardInput: #"{"session_id":"old-session","cwd":"\#(context.root.path)","hook_event_name":"SessionStart"}"#
)
- XCTAssertFalse(oldStart.timedOut, oldStart.stderr)
- XCTAssertEqual(oldStart.status, 0, oldStart.stderr)
+ legacyAssertFalse(oldStart.timedOut, oldStart.stderr)
+ legacyAssertEqual(oldStart.status, 0, oldStart.stderr)
let oldPrompt = runClaudeHook(
context: context,
arguments: ["hooks", "claude", "prompt-submit"],
standardInput: #"{"session_id":"old-session","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"PromptSubmit"}"#
)
- XCTAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
- XCTAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
+ legacyAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
+ legacyAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
let oldStop = runClaudeHook(
context: context,
arguments: ["hooks", "claude", "stop"],
standardInput: #"{"session_id":"old-session","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"old turn finished"}"#
)
- XCTAssertFalse(oldStop.timedOut, oldStop.stderr)
- XCTAssertEqual(oldStop.status, 0, oldStop.stderr)
+ legacyAssertFalse(oldStop.timedOut, oldStop.stderr)
+ legacyAssertEqual(oldStop.status, 0, oldStop.stderr)
let newStart = runClaudeHook(
context: context,
arguments: ["hooks", "claude", "session-start"],
standardInput: #"{"session_id":"new-session","source":"startup","cwd":"\#(context.root.path)","hook_event_name":"SessionStart"}"#
)
- XCTAssertFalse(newStart.timedOut, newStart.stderr)
- XCTAssertEqual(newStart.status, 0, newStart.stderr)
+ legacyAssertFalse(newStart.timedOut, newStart.stderr)
+ legacyAssertEqual(newStart.status, 0, newStart.stderr)
let newPromptStart = context.state.commands.count
let newPrompt = runClaudeHook(
@@ -449,11 +449,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
arguments: ["hooks", "claude", "prompt-submit"],
standardInput: #"{"session_id":"new-session","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"PromptSubmit"}"#
)
- XCTAssertFalse(newPrompt.timedOut, newPrompt.stderr)
- XCTAssertEqual(newPrompt.status, 0, newPrompt.stderr)
+ legacyAssertFalse(newPrompt.timedOut, newPrompt.stderr)
+ legacyAssertEqual(newPrompt.status, 0, newPrompt.stderr)
let newPromptCommands = Array(context.state.commands.dropFirst(newPromptStart))
- XCTAssertTrue(
+ legacyAssertTrue(
newPromptCommands.contains {
$0.hasPrefix("set_status claude_code Running --icon=bolt.fill --color=#4C8DFF --tab=\(context.workspaceId)")
},
@@ -700,7 +700,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: standardInput,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
@@ -726,11 +726,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(parentSessionId)","source":"resume","cwd":"\#(context.root.path)","hook_event_name":"SessionStart"}"#,
extraEnvironment: claudeForkLaunchEnvironment(context: context, parentSessionId: parentSessionId)
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let parentRecord = try readClaudeHookSession(parentSessionId, context: context)
- XCTAssertEqual(
+ legacyAssertEqual(
parentRecord["surfaceId"] as? String,
parentSurfaceId,
"Fork-session SessionStart reports the parent session id and must not steal the parent record's surface binding for the fork pane"
@@ -766,10 +766,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(parentSessionId)","source":"resume","cwd":"\#(context.root.path)","hook_event_name":"SessionStart"}"#,
extraEnvironment: environment
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
- XCTAssertFalse(
+ legacyAssertFalse(
context.state.commands.contains { $0.hasPrefix("set_agent_pid claude_code ") },
"A fork SessionStart without an authoritative surface must not register its PID on a borrowed fallback pane, saw \(context.state.commands)"
)
@@ -802,11 +802,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
arguments: ["/usr/local/bin/claude", "--resume", parentSessionId, "--fork-session=true"]
)
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let parentRecord = try readClaudeHookSession(parentSessionId, context: context)
- XCTAssertEqual(
+ legacyAssertEqual(
parentRecord["surfaceId"] as? String,
parentSurfaceId,
"Fork detection must recognize the --fork-session=true flag form the launch sanitizer already accepts"
@@ -872,8 +872,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"session-3","turn_id":"turn-3","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"three"}"#,
extraEnvironment: ["CMUX_SURFACE_ID": paneB]
)
- XCTAssertFalse(paneBPrompt.timedOut, paneBPrompt.stderr)
- XCTAssertEqual(paneBPrompt.status, 0, paneBPrompt.stderr)
+ legacyAssertFalse(paneBPrompt.timedOut, paneBPrompt.stderr)
+ legacyAssertEqual(paneBPrompt.status, 0, paneBPrompt.stderr)
// …then a late Stop from stale session-1 in pane A must stay stale:
// the pane boundary (session-2 owns pane A) has to survive the upgrade
@@ -884,11 +884,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"session-1","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"late"}"#,
extraEnvironment: ["CMUX_SURFACE_ID": paneA]
)
- XCTAssertFalse(lateStop.timedOut, lateStop.stderr)
- XCTAssertEqual(lateStop.status, 0, lateStop.stderr)
+ legacyAssertFalse(lateStop.timedOut, lateStop.stderr)
+ legacyAssertEqual(lateStop.status, 0, lateStop.stderr)
let staleRecord = try readClaudeHookSession("session-1", context: context)
- XCTAssertEqual(
+ legacyAssertEqual(
staleRecord["agentLifecycle"] as? String,
"running",
"A legacy store must backfill the pane boundary so pre-upgrade stale sessions stay stale after another pane promotes"
@@ -919,16 +919,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(forkedSessionId)","turn_id":"fork-turn-1","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"diverge here"}"#,
extraEnvironment: claudeForkLaunchEnvironment(context: context, parentSessionId: parentSessionId)
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let forkedRecord = try readClaudeHookSession(forkedSessionId, context: context)
- XCTAssertEqual(
+ legacyAssertEqual(
forkedRecord["surfaceId"] as? String,
context.surfaceId,
"The forked session's first prompt-submit must bind the forked session to the fork pane even while the parent session owns the workspace's active turn"
)
- XCTAssertEqual(
+ legacyAssertEqual(
forkedRecord["isRestorable"] as? Bool,
true,
"The forked session must become restorable so a cmux restart resumes the fork, not the parent"
@@ -942,10 +942,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- XCTAssertEqual(resumeBindingRequests.count, 1, promptCommands.joined(separator: "\n"))
- let request = try XCTUnwrap(resumeBindingRequests.first)
- XCTAssertEqual(request["checkpoint_id"] as? String, forkedSessionId)
- XCTAssertEqual(request["surface_id"] as? String, context.surfaceId)
+ legacyAssertEqual(resumeBindingRequests.count, 1, promptCommands.joined(separator: "\n"))
+ let request = try legacyUnwrap(resumeBindingRequests.first)
+ legacyAssertEqual(request["checkpoint_id"] as? String, forkedSessionId)
+ legacyAssertEqual(request["surface_id"] as? String, context.surfaceId)
}
@Test
@@ -972,11 +972,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(parentSessionId)","cwd":"\#(context.root.path)","hook_event_name":"SessionEnd"}"#,
extraEnvironment: claudeForkLaunchEnvironment(context: context, parentSessionId: parentSessionId)
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let parentRecord = try readClaudeHookSession(parentSessionId, context: context)
- XCTAssertEqual(
+ legacyAssertEqual(
parentRecord["surfaceId"] as? String,
parentSurfaceId,
"A pre-prompt fork exit must not consume the parent session record the original pane still owns"
@@ -988,11 +988,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- XCTAssertTrue(
+ legacyAssertTrue(
resumeClearRequests.isEmpty,
"A pre-prompt fork exit must not clear the parent pane's resume binding, saw \(resumeClearRequests)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
context.state.commands.contains {
$0.hasPrefix("clear_agent_pid claude_code ") && $0.contains("--panel=\(context.surfaceId)")
},
@@ -1026,11 +1026,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(forkedSessionId)","turn_id":"fork-turn-1","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"diverge here"}"#,
extraEnvironment: ["CMUX_SURFACE_ID": "surface:2"]
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let forkedRecord = try readClaudeHookSession(forkedSessionId, context: context)
- XCTAssertEqual(
+ legacyAssertEqual(
forkedRecord["surfaceId"] as? String,
context.surfaceId,
"A forked session's first prompt-submit must record via the resolved surface when the hook supplies the surface as a ref"
@@ -1093,16 +1093,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(staleSessionId)","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"late stop"}"#,
extraEnvironment: ["CMUX_SURFACE_ID": closedSurfaceId]
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let staleRecord = try readClaudeHookSession(staleSessionId, context: context)
- XCTAssertEqual(
+ legacyAssertEqual(
staleRecord["surfaceId"] as? String,
closedSurfaceId,
"A stale hook resolved to a fallback surface must not retarget the session record to a pane it never owned"
)
- XCTAssertEqual(
+ legacyAssertEqual(
staleRecord["agentLifecycle"] as? String,
"running",
"A late Stop from a closed pane must stay stale when surface resolution fell back to another pane"
@@ -1139,8 +1139,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
("prompt-submit", #"{"session_id":"session-2","turn_id":"turn-2","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"two"}"#),
] {
let result = runHook(subcommand, stdin: stdin, surface: paneA)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
}
// Pane B: a different session (e.g. a forked conversation) takes the
@@ -1150,8 +1150,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
stdin: #"{"session_id":"session-3","turn_id":"turn-3","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"three"}"#,
surface: paneB
)
- XCTAssertFalse(paneBPrompt.timedOut, paneBPrompt.stderr)
- XCTAssertEqual(paneBPrompt.status, 0, paneBPrompt.stderr)
+ legacyAssertFalse(paneBPrompt.timedOut, paneBPrompt.stderr)
+ legacyAssertEqual(paneBPrompt.status, 0, paneBPrompt.stderr)
// A late Stop from the superseded session-1 in pane A must stay stale
// even though the workspace-active session now lives in pane B.
@@ -1160,14 +1160,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
stdin: #"{"session_id":"session-1","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"late"}"#,
surface: paneA
)
- XCTAssertFalse(lateStop.timedOut, lateStop.stderr)
- XCTAssertEqual(lateStop.status, 0, lateStop.stderr)
+ legacyAssertFalse(lateStop.timedOut, lateStop.stderr)
+ legacyAssertEqual(lateStop.status, 0, lateStop.stderr)
let stateURL = context.root.appendingPathComponent("claude-hook-sessions.json")
- let savedState = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
- let activeSessions = try XCTUnwrap(savedState["activeSessionsByWorkspace"] as? [String: Any])
- let active = try XCTUnwrap(activeSessions[context.workspaceId] as? [String: Any])
- XCTAssertEqual(
+ let savedState = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
+ let activeSessions = try legacyUnwrap(savedState["activeSessionsByWorkspace"] as? [String: Any])
+ let active = try legacyUnwrap(activeSessions[context.workspaceId] as? [String: Any])
+ legacyAssertEqual(
active["sessionId"] as? String,
"session-3",
"A late Stop from a session replaced in its own pane must not re-promote it after another pane became the workspace-active session"
@@ -1205,8 +1205,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
("prompt-submit", #"{"session_id":"session-3","turn_id":"turn-3","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"three"}"#, paneB),
] {
let result = runHook(subcommand, stdin: stdin, surface: surface)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
}
// Pane A: the user starts a fresh Claude session. It must replace the
@@ -1217,17 +1217,17 @@ final class CLINotifyProcessIntegrationRegressionTests {
("prompt-submit", #"{"session_id":"session-4","turn_id":"turn-4","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"four"}"#),
] {
let result = runHook(subcommand, stdin: stdin, surface: paneA)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
}
let newRecord = try readClaudeHookSession("session-4", context: context)
- XCTAssertEqual(
+ legacyAssertEqual(
newRecord["surfaceId"] as? String,
paneA,
"A fresh session in an idle pane must record against its own pane"
)
- XCTAssertEqual(
+ legacyAssertEqual(
newRecord["isRestorable"] as? Bool,
true,
"A fresh session replacing a stopped session in its own pane must not be dropped as stale after another pane became workspace-active"
@@ -1265,8 +1265,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
("prompt-submit", #"{"session_id":"session-3","turn_id":"turn-3","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"three"}"#, paneB),
] {
let result = runHook(subcommand, stdin: stdin, surface: surface)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
}
// A stale SessionEnd for session-1's finished turn-1 must not consume
@@ -1277,11 +1277,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
stdin: #"{"session_id":"session-1","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"SessionEnd"}"#,
surface: paneA
)
- XCTAssertFalse(staleEnd.timedOut, staleEnd.stderr)
- XCTAssertEqual(staleEnd.status, 0, staleEnd.stderr)
+ legacyAssertFalse(staleEnd.timedOut, staleEnd.stderr)
+ legacyAssertEqual(staleEnd.status, 0, staleEnd.stderr)
let record = try readClaudeHookSession("session-1", context: context)
- XCTAssertEqual(
+ legacyAssertEqual(
record["surfaceId"] as? String,
paneA,
"A stale turn-mismatched SessionEnd must not consume a session that is still active in its own pane after another pane became workspace-active"
@@ -1313,11 +1313,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(parentSessionId)","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"parent turn finished"}"#,
extraEnvironment: ["CMUX_SURFACE_ID": parentSurfaceId]
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let parentRecord = try readClaudeHookSession(parentSessionId, context: context)
- XCTAssertEqual(
+ legacyAssertEqual(
parentRecord["agentLifecycle"] as? String,
"idle",
"The parent pane's Stop must keep applying after the forked session became the workspace's active session in another pane"
@@ -1356,8 +1356,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","source":"startup","cwd":"\#(context.root.path)","hook_event_name":"SessionStart"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(start.timedOut, start.stderr)
- XCTAssertEqual(start.status, 0, start.stderr)
+ legacyAssertFalse(start.timedOut, start.stderr)
+ legacyAssertEqual(start.status, 0, start.stderr)
let commandStart = context.state.commands.count
let prompt = runClaudeHookWithoutServer(
@@ -1366,8 +1366,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(prompt.timedOut, prompt.stderr)
- XCTAssertEqual(prompt.status, 0, prompt.stderr)
+ legacyAssertFalse(prompt.timedOut, prompt.stderr)
+ legacyAssertEqual(prompt.status, 0, prompt.stderr)
let promptCommands = Array(context.state.commands.dropFirst(commandStart))
let resumeBindingRequests = promptCommands.compactMap { command -> [String: Any]? in
@@ -1377,19 +1377,19 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- XCTAssertEqual(resumeBindingRequests.count, 1, promptCommands.joined(separator: "\n"))
- let request = try XCTUnwrap(resumeBindingRequests.first)
- XCTAssertEqual(request["auto_resume"] as? Bool, true)
- let environment = try XCTUnwrap(request["environment"] as? [String: Any])
- XCTAssertEqual(environment["CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV"] as? String, "1")
- XCTAssertEqual(
+ legacyAssertEqual(resumeBindingRequests.count, 1, promptCommands.joined(separator: "\n"))
+ let request = try legacyUnwrap(resumeBindingRequests.first)
+ legacyAssertEqual(request["auto_resume"] as? Bool, true)
+ let environment = try legacyUnwrap(request["environment"] as? [String: Any])
+ legacyAssertEqual(environment["CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV"] as? String, "1")
+ legacyAssertEqual(
environment["CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV_KEYS"] as? String,
"ANTHROPIC_BASE_URL,ANTHROPIC_MODEL,CLAUDE_CONFIG_DIR"
)
- XCTAssertNil(environment["ANTHROPIC_API_KEY"])
- XCTAssertEqual(environment["ANTHROPIC_BASE_URL"] as? String, "https://api.example.test")
- XCTAssertEqual(environment["ANTHROPIC_MODEL"] as? String, "claude-sonnet-test")
- XCTAssertEqual(
+ legacyAssertNil(environment["ANTHROPIC_API_KEY"])
+ legacyAssertEqual(environment["ANTHROPIC_BASE_URL"] as? String, "https://api.example.test")
+ legacyAssertEqual(environment["ANTHROPIC_MODEL"] as? String, "claude-sonnet-test")
+ legacyAssertEqual(
environment["CLAUDE_CONFIG_DIR"] as? String,
context.root.appendingPathComponent("claude-config", isDirectory: true).path
)
@@ -1442,24 +1442,24 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"cwd":"\#(context.root.path)","hook_event_name":"SessionEnd"}"#
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
- let savedState = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
- let savedSessions = try XCTUnwrap(savedState["sessions"] as? [String: Any])
- XCTAssertNil(
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
+ let savedState = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
+ let savedSessions = try legacyUnwrap(savedState["sessions"] as? [String: Any])
+ legacyAssertNil(
savedSessions[staleSessionId],
"Expected fallback session-end handling to consume the seeded stale session"
)
- XCTAssertFalse(
+ legacyAssertFalse(
context.state.commands.contains { $0.hasPrefix("clear_status claude_code ") && $0.contains("--tab=\(staleWorkspaceId)") },
"Expected stale SessionEnd not to clear the consumed workspace, saw \(context.state.commands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
context.state.commands.contains { $0.hasPrefix("clear_agent_pid claude_code ") && $0.contains("--tab=\(staleWorkspaceId)") },
"Expected stale SessionEnd not to clear the consumed workspace PID, saw \(context.state.commands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
context.state.commands.contains { $0 == "clear_notifications --tab=\(staleWorkspaceId)" },
"Expected stale SessionEnd not to clear the consumed workspace notifications, saw \(context.state.commands)"
)
@@ -1502,26 +1502,26 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"SessionEnd"}"#
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertFalse(
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(
context.state.commands.contains { $0.hasPrefix("clear_agent_pid claude_code ") && $0.contains("--tab=\(context.workspaceId)") },
"Expected stale same-session turn not to clear current PID, saw \(context.state.commands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
context.state.commands.contains { $0 == "clear_notifications --tab=\(context.workspaceId)" },
"Expected stale same-session turn not to clear current notifications, saw \(context.state.commands)"
)
- let savedState = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
- let savedSessions = try XCTUnwrap(savedState["sessions"] as? [String: Any])
- XCTAssertNotNil(
+ let savedState = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
+ let savedSessions = try legacyUnwrap(savedState["sessions"] as? [String: Any])
+ legacyAssertNotNil(
savedSessions[sessionId],
"Expected stale same-session SessionEnd not to consume the active session"
)
- let activeSessions = try XCTUnwrap(savedState["activeSessionsByWorkspace"] as? [String: Any])
- let active = try XCTUnwrap(activeSessions[context.workspaceId] as? [String: Any])
- XCTAssertEqual(active["turnId"] as? String, "turn-2")
+ let activeSessions = try legacyUnwrap(savedState["activeSessionsByWorkspace"] as? [String: Any])
+ let active = try legacyUnwrap(activeSessions[context.workspaceId] as? [String: Any])
+ legacyAssertEqual(active["turnId"] as? String, "turn-2")
}
@Test
@@ -1554,8 +1554,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","cwd":"\#(context.root.path)","hook_event_name":"SessionEnd"}"#
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let clearRequests = context.state.commands.compactMap { command -> [String: Any]? in
guard let payload = jsonObject(command),
payload["method"] as? String == "surface.resume.clear" else {
@@ -1563,11 +1563,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- let request = try XCTUnwrap(clearRequests.first)
- XCTAssertNil(request["workspace_id"])
- XCTAssertEqual(request["surface_id"] as? String, context.surfaceId)
- XCTAssertEqual(request["checkpoint_id"] as? String, sessionId)
- XCTAssertEqual(request["source"] as? String, "agent-hook")
+ let request = try legacyUnwrap(clearRequests.first)
+ legacyAssertNil(request["workspace_id"])
+ legacyAssertEqual(request["surface_id"] as? String, context.surfaceId)
+ legacyAssertEqual(request["checkpoint_id"] as? String, sessionId)
+ legacyAssertEqual(request["source"] as? String, "agent-hook")
}
@Test
@@ -1585,9 +1585,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"spawn subagent"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
- XCTAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
- XCTAssertTrue(
+ legacyAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
+ legacyAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
+ legacyAssertTrue(
context.state.commands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.set" },
"Parent Codex prompt should publish a resume binding, saw \(context.state.commands)"
)
@@ -1599,14 +1599,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"return 1+1"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childPrompt.timedOut, childPrompt.stderr)
- XCTAssertEqual(childPrompt.status, 0, childPrompt.stderr)
+ legacyAssertFalse(childPrompt.timedOut, childPrompt.stderr)
+ legacyAssertEqual(childPrompt.status, 0, childPrompt.stderr)
let childPromptCommands = Array(context.state.commands.dropFirst(childPromptStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childPromptCommands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.set" },
"Nested Codex prompt should not replace the parent resume binding, saw \(childPromptCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
childPromptCommands.contains { $0.hasPrefix("set_status codex Running ") },
"Nested Codex prompt should not rewrite parent Running status, saw \(childPromptCommands)"
)
@@ -1618,18 +1618,18 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"2"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childStop.timedOut, childStop.stderr)
- XCTAssertEqual(childStop.status, 0, childStop.stderr)
+ legacyAssertFalse(childStop.timedOut, childStop.stderr)
+ legacyAssertEqual(childStop.status, 0, childStop.stderr)
let childStopCommands = Array(context.state.commands.dropFirst(childStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
childStopCommands.contains { $0.contains(#""method":"feed.push""#) && $0.contains(#""hook_event_name":"Stop""#) },
"Nested Codex Stop should remain Feed telemetry, saw \(childStopCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
childStopCommands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.set" },
"Nested Codex Stop should not replace the parent resume binding, saw \(childStopCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
childStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"Nested Codex Stop should not notify or mark the parent idle, saw \(childStopCommands)"
)
@@ -1641,18 +1641,18 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"parent done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentStop.timedOut, parentStop.stderr)
- XCTAssertEqual(parentStop.status, 0, parentStop.stderr)
+ legacyAssertFalse(parentStop.timedOut, parentStop.stderr)
+ legacyAssertEqual(parentStop.status, 0, parentStop.stderr)
let parentStopCommands = Array(context.state.commands.dropFirst(parentStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.set" },
"Parent Codex Stop should still refresh the resume binding, saw \(parentStopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"Parent Codex Stop should still notify, saw \(parentStopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { $0.hasPrefix("set_status codex ") && $0.contains(" Idle ") },
"Parent Codex Stop should mark Codex idle, saw \(parentStopCommands)"
)
@@ -1673,8 +1673,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"continue"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(prompt.timedOut, prompt.stderr)
- XCTAssertEqual(prompt.status, 0, prompt.stderr)
+ legacyAssertFalse(prompt.timedOut, prompt.stderr)
+ legacyAssertEqual(prompt.status, 0, prompt.stderr)
let stop = runCodexHook(
context: context,
@@ -1682,14 +1682,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"turn-1","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(stop.timedOut, stop.stderr)
- XCTAssertEqual(stop.status, 0, stop.stderr)
+ legacyAssertFalse(stop.timedOut, stop.stderr)
+ legacyAssertEqual(stop.status, 0, stop.stderr)
let stateURL = context.root.appendingPathComponent("codex-hook-sessions.json")
- var state = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
- var sessions = try XCTUnwrap(state["sessions"] as? [String: Any])
- var record = try XCTUnwrap(sessions[sessionId] as? [String: Any])
- XCTAssertEqual(record["agentLifecycle"] as? String, "idle")
+ var state = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
+ var sessions = try legacyUnwrap(state["sessions"] as? [String: Any])
+ var record = try legacyUnwrap(sessions[sessionId] as? [String: Any])
+ legacyAssertEqual(record["agentLifecycle"] as? String, "idle")
let notificationStart = context.state.commands.count
let notification = runCodexHook(
@@ -1698,11 +1698,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","cwd":"\#(context.root.path)","hook_event_name":"Notification","message":"permission approval required"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(notification.timedOut, notification.stderr)
- XCTAssertEqual(notification.status, 0, notification.stderr)
+ legacyAssertFalse(notification.timedOut, notification.stderr)
+ legacyAssertEqual(notification.status, 0, notification.stderr)
let notificationCommands = Array(context.state.commands.dropFirst(notificationStart))
- XCTAssertTrue(
+ legacyAssertTrue(
notificationCommands.contains {
$0.hasPrefix("set_agent_lifecycle codex needsInput --tab=\(context.workspaceId)")
&& $0.contains("--panel=\(context.surfaceId)")
@@ -1710,10 +1710,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
"Notification requiring user input must correct the visible lifecycle, saw \(notificationCommands)"
)
- state = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
- sessions = try XCTUnwrap(state["sessions"] as? [String: Any])
- record = try XCTUnwrap(sessions[sessionId] as? [String: Any])
- XCTAssertEqual(record["agentLifecycle"] as? String, "needsInput")
+ state = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
+ sessions = try legacyUnwrap(state["sessions"] as? [String: Any])
+ record = try legacyUnwrap(sessions[sessionId] as? [String: Any])
+ legacyAssertEqual(record["agentLifecycle"] as? String, "needsInput")
}
@Test
@@ -1733,8 +1733,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(oldSessionId)","turn_id":"old-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"old"}"#,
extraEnvironment: oldEnvironment
)
- XCTAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
- XCTAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
+ legacyAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
+ legacyAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
let newPrompt = runCodexHook(
context: context,
@@ -1742,8 +1742,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(newSessionId)","turn_id":"new-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"new"}"#,
extraEnvironment: newEnvironment
)
- XCTAssertFalse(newPrompt.timedOut, newPrompt.stderr)
- XCTAssertEqual(newPrompt.status, 0, newPrompt.stderr)
+ legacyAssertFalse(newPrompt.timedOut, newPrompt.stderr)
+ legacyAssertEqual(newPrompt.status, 0, newPrompt.stderr)
let staleStopStart = context.state.commands.count
let staleStop = runCodexHook(
@@ -1752,29 +1752,29 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(oldSessionId)","turn_id":"old-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"old done"}"#,
extraEnvironment: oldEnvironment
)
- XCTAssertFalse(staleStop.timedOut, staleStop.stderr)
- XCTAssertEqual(staleStop.status, 0, staleStop.stderr)
+ legacyAssertFalse(staleStop.timedOut, staleStop.stderr)
+ legacyAssertEqual(staleStop.status, 0, staleStop.stderr)
let staleStopCommands = Array(context.state.commands.dropFirst(staleStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
staleStopCommands.contains { $0.hasPrefix("set_agent_lifecycle codex idle ") },
"A stale Stop from an older session must not mark the surface idle, saw \(staleStopCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
staleStopCommands.contains { $0.hasPrefix("set_status codex ") && $0.contains(" Idle ") },
"A stale Stop from an older session must not replace the newer Running status, saw \(staleStopCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
staleStopCommands.contains { $0.hasPrefix("notify_target") },
"A stale Stop from an older session must not publish a completion notification, saw \(staleStopCommands)"
)
let stateURL = context.root.appendingPathComponent("codex-hook-sessions.json")
- let state = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
- let sessions = try XCTUnwrap(state["sessions"] as? [String: Any])
- let newRecord = try XCTUnwrap(sessions[newSessionId] as? [String: Any])
- XCTAssertEqual(newRecord["runtimeStatus"] as? String, "running")
- XCTAssertEqual(newRecord["agentLifecycle"] as? String, "running")
+ let state = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
+ let sessions = try legacyUnwrap(state["sessions"] as? [String: Any])
+ let newRecord = try legacyUnwrap(sessions[newSessionId] as? [String: Any])
+ legacyAssertEqual(newRecord["runtimeStatus"] as? String, "running")
+ legacyAssertEqual(newRecord["agentLifecycle"] as? String, "running")
}
@Test
@@ -1794,8 +1794,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(oldSessionId)","turn_id":"old-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"old"}"#,
extraEnvironment: oldEnvironment
)
- XCTAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
- XCTAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
+ legacyAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
+ legacyAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
let newPrompt = runCodexHook(
context: context,
@@ -1803,8 +1803,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(newSessionId)","turn_id":"new-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"new"}"#,
extraEnvironment: newEnvironment
)
- XCTAssertFalse(newPrompt.timedOut, newPrompt.stderr)
- XCTAssertEqual(newPrompt.status, 0, newPrompt.stderr)
+ legacyAssertFalse(newPrompt.timedOut, newPrompt.stderr)
+ legacyAssertEqual(newPrompt.status, 0, newPrompt.stderr)
let staleNotificationStart = context.state.commands.count
let staleNotification = runCodexHook(
@@ -1813,29 +1813,29 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(oldSessionId)","cwd":"\#(context.root.path)","hook_event_name":"Notification","message":"done"}"#,
extraEnvironment: oldEnvironment
)
- XCTAssertFalse(staleNotification.timedOut, staleNotification.stderr)
- XCTAssertEqual(staleNotification.status, 0, staleNotification.stderr)
+ legacyAssertFalse(staleNotification.timedOut, staleNotification.stderr)
+ legacyAssertEqual(staleNotification.status, 0, staleNotification.stderr)
let staleNotificationCommands = Array(context.state.commands.dropFirst(staleNotificationStart))
- XCTAssertFalse(
+ legacyAssertFalse(
staleNotificationCommands.contains { $0.hasPrefix("set_agent_lifecycle codex idle ") },
"A stale idle notification must not mark the newer session idle, saw \(staleNotificationCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
staleNotificationCommands.contains { $0.hasPrefix("set_status codex ") && $0.contains(" Idle ") },
"A stale idle notification must not replace the newer Running status, saw \(staleNotificationCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
staleNotificationCommands.contains { $0.hasPrefix("notify_target") },
"A stale idle notification must not publish a completion notification, saw \(staleNotificationCommands)"
)
let stateURL = context.root.appendingPathComponent("codex-hook-sessions.json")
- let state = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
- let sessions = try XCTUnwrap(state["sessions"] as? [String: Any])
- let newRecord = try XCTUnwrap(sessions[newSessionId] as? [String: Any])
- XCTAssertEqual(newRecord["runtimeStatus"] as? String, "running")
- XCTAssertEqual(newRecord["agentLifecycle"] as? String, "running")
+ let state = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
+ let sessions = try legacyUnwrap(state["sessions"] as? [String: Any])
+ let newRecord = try legacyUnwrap(sessions[newSessionId] as? [String: Any])
+ legacyAssertEqual(newRecord["runtimeStatus"] as? String, "running")
+ legacyAssertEqual(newRecord["agentLifecycle"] as? String, "running")
}
@Test
@@ -1853,8 +1853,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"parent"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
- XCTAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
+ legacyAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
+ legacyAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
let childPrompt = runCodexHook(
context: context,
@@ -1862,8 +1862,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"child"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childPrompt.timedOut, childPrompt.stderr)
- XCTAssertEqual(childPrompt.status, 0, childPrompt.stderr)
+ legacyAssertFalse(childPrompt.timedOut, childPrompt.stderr)
+ legacyAssertEqual(childPrompt.status, 0, childPrompt.stderr)
let legacyChildStopStart = context.state.commands.count
let legacyChildStop = runCodexHook(
@@ -1872,10 +1872,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"child done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(legacyChildStop.timedOut, legacyChildStop.stderr)
- XCTAssertEqual(legacyChildStop.status, 0, legacyChildStop.stderr)
+ legacyAssertFalse(legacyChildStop.timedOut, legacyChildStop.stderr)
+ legacyAssertEqual(legacyChildStop.status, 0, legacyChildStop.stderr)
let legacyChildStopCommands = Array(context.state.commands.dropFirst(legacyChildStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
legacyChildStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"A legacy child Stop without a turn_id must stay nested, saw \(legacyChildStopCommands)"
)
@@ -1887,14 +1887,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"parent done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentStop.timedOut, parentStop.stderr)
- XCTAssertEqual(parentStop.status, 0, parentStop.stderr)
+ legacyAssertFalse(parentStop.timedOut, parentStop.stderr)
+ legacyAssertEqual(parentStop.status, 0, parentStop.stderr)
let parentStopCommands = Array(context.state.commands.dropFirst(parentStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"The parent Stop must still notify after a legacy child Stop without a turn_id, saw \(parentStopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { $0.hasPrefix("set_status codex ") && $0.contains(" Idle ") },
"The parent Stop must still mark Codex idle after a legacy child Stop without a turn_id, saw \(parentStopCommands)"
)
@@ -1916,8 +1916,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"BeforeAgent","prompt":"parent"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
- XCTAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
+ legacyAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
+ legacyAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
let childPromptStart = context.state.commands.count
let childPrompt = runAgentHook(
@@ -1927,14 +1927,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","hook_event_name":"BeforeAgent","prompt":"child"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childPrompt.timedOut, childPrompt.stderr)
- XCTAssertEqual(childPrompt.status, 0, childPrompt.stderr)
+ legacyAssertFalse(childPrompt.timedOut, childPrompt.stderr)
+ legacyAssertEqual(childPrompt.status, 0, childPrompt.stderr)
let childPromptCommands = Array(context.state.commands.dropFirst(childPromptStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childPromptCommands.contains { (self.jsonObject($0)?["method"] as? String)?.hasPrefix("surface.resume.") == true },
"A generic nested turn_id prompt must not replace the parent resume binding, saw \(childPromptCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
childPromptCommands.contains { $0.hasPrefix("set_status gemini Running ") },
"A generic nested turn_id prompt must not rewrite parent Running status, saw \(childPromptCommands)"
)
@@ -1947,10 +1947,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","hook_event_name":"AfterAgent","last_assistant_message":"child done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childStop.timedOut, childStop.stderr)
- XCTAssertEqual(childStop.status, 0, childStop.stderr)
+ legacyAssertFalse(childStop.timedOut, childStop.stderr)
+ legacyAssertEqual(childStop.status, 0, childStop.stderr)
let childStopCommands = Array(context.state.commands.dropFirst(childStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status gemini ") },
"A generic nested turn_id Stop must not notify or mark the parent idle, saw \(childStopCommands)"
)
@@ -1963,14 +1963,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"AfterAgent","last_assistant_message":"parent done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentStop.timedOut, parentStop.stderr)
- XCTAssertEqual(parentStop.status, 0, parentStop.stderr)
+ legacyAssertFalse(parentStop.timedOut, parentStop.stderr)
+ legacyAssertEqual(parentStop.status, 0, parentStop.stderr)
let parentStopCommands = Array(context.state.commands.dropFirst(parentStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Gemini|") },
"The generic parent Stop must still notify after its nested child, saw \(parentStopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { $0.hasPrefix("set_status gemini ") && $0.contains(" Idle ") },
"The generic parent Stop must still mark Gemini idle, saw \(parentStopCommands)"
)
@@ -1991,8 +1991,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"parent"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
- XCTAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
+ legacyAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
+ legacyAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
let anonymousChildPromptStart = context.state.commands.count
let anonymousChildPrompt = runCodexHook(
@@ -2001,14 +2001,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"anonymous child"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(anonymousChildPrompt.timedOut, anonymousChildPrompt.stderr)
- XCTAssertEqual(anonymousChildPrompt.status, 0, anonymousChildPrompt.stderr)
+ legacyAssertFalse(anonymousChildPrompt.timedOut, anonymousChildPrompt.stderr)
+ legacyAssertEqual(anonymousChildPrompt.status, 0, anonymousChildPrompt.stderr)
let anonymousChildPromptCommands = Array(context.state.commands.dropFirst(anonymousChildPromptStart))
- XCTAssertFalse(
+ legacyAssertFalse(
anonymousChildPromptCommands.contains { (self.jsonObject($0)?["method"] as? String)?.hasPrefix("surface.resume.") == true },
"An anonymous child under a known parent must not replace the parent resume binding, saw \(anonymousChildPromptCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
anonymousChildPromptCommands.contains { $0.hasPrefix("set_status codex Running ") },
"An anonymous child under a known parent must not rewrite parent Running status, saw \(anonymousChildPromptCommands)"
)
@@ -2020,14 +2020,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"grandchild-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"grandchild"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(grandchildPrompt.timedOut, grandchildPrompt.stderr)
- XCTAssertEqual(grandchildPrompt.status, 0, grandchildPrompt.stderr)
+ legacyAssertFalse(grandchildPrompt.timedOut, grandchildPrompt.stderr)
+ legacyAssertEqual(grandchildPrompt.status, 0, grandchildPrompt.stderr)
let grandchildPromptCommands = Array(context.state.commands.dropFirst(grandchildPromptStart))
- XCTAssertFalse(
+ legacyAssertFalse(
grandchildPromptCommands.contains { (self.jsonObject($0)?["method"] as? String)?.hasPrefix("surface.resume.") == true },
"A known grandchild after anonymous depth must stay nested, saw \(grandchildPromptCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
grandchildPromptCommands.contains { $0.hasPrefix("set_status codex Running ") },
"A known grandchild after anonymous depth must not rewrite parent Running status, saw \(grandchildPromptCommands)"
)
@@ -2039,10 +2039,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"grandchild-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"grandchild done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(grandchildStop.timedOut, grandchildStop.stderr)
- XCTAssertEqual(grandchildStop.status, 0, grandchildStop.stderr)
+ legacyAssertFalse(grandchildStop.timedOut, grandchildStop.stderr)
+ legacyAssertEqual(grandchildStop.status, 0, grandchildStop.stderr)
let grandchildStopCommands = Array(context.state.commands.dropFirst(grandchildStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
grandchildStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"Grandchild Stop must not collapse anonymous child depth and notify, saw \(grandchildStopCommands)"
)
@@ -2054,10 +2054,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"anonymous child done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(anonymousChildStop.timedOut, anonymousChildStop.stderr)
- XCTAssertEqual(anonymousChildStop.status, 0, anonymousChildStop.stderr)
+ legacyAssertFalse(anonymousChildStop.timedOut, anonymousChildStop.stderr)
+ legacyAssertEqual(anonymousChildStop.status, 0, anonymousChildStop.stderr)
let anonymousChildStopCommands = Array(context.state.commands.dropFirst(anonymousChildStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
anonymousChildStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"Anonymous child Stop must stay nested after a known grandchild Stop, saw \(anonymousChildStopCommands)"
)
@@ -2069,14 +2069,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"parent done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentStop.timedOut, parentStop.stderr)
- XCTAssertEqual(parentStop.status, 0, parentStop.stderr)
+ legacyAssertFalse(parentStop.timedOut, parentStop.stderr)
+ legacyAssertEqual(parentStop.status, 0, parentStop.stderr)
let parentStopCommands = Array(context.state.commands.dropFirst(parentStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"The parent Stop must still notify after mixed anonymous depth, saw \(parentStopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { $0.hasPrefix("set_status codex ") && $0.contains(" Idle ") },
"The parent Stop must still mark Codex idle after mixed anonymous depth, saw \(parentStopCommands)"
)
@@ -2097,8 +2097,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"parent"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
- XCTAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
+ legacyAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
+ legacyAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
let anonymousChildPrompt = runCodexHook(
context: context,
@@ -2106,8 +2106,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"anonymous child"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(anonymousChildPrompt.timedOut, anonymousChildPrompt.stderr)
- XCTAssertEqual(anonymousChildPrompt.status, 0, anonymousChildPrompt.stderr)
+ legacyAssertFalse(anonymousChildPrompt.timedOut, anonymousChildPrompt.stderr)
+ legacyAssertEqual(anonymousChildPrompt.status, 0, anonymousChildPrompt.stderr)
let childStopStart = context.state.commands.count
let childStop = runCodexHook(
@@ -2116,10 +2116,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"child done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childStop.timedOut, childStop.stderr)
- XCTAssertEqual(childStop.status, 0, childStop.stderr)
+ legacyAssertFalse(childStop.timedOut, childStop.stderr)
+ legacyAssertEqual(childStop.status, 0, childStop.stderr)
let childStopCommands = Array(context.state.commands.dropFirst(childStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"The child Stop should pop anonymous depth without notifying, saw \(childStopCommands)"
)
@@ -2131,14 +2131,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"parent done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentStop.timedOut, parentStop.stderr)
- XCTAssertEqual(parentStop.status, 0, parentStop.stderr)
+ legacyAssertFalse(parentStop.timedOut, parentStop.stderr)
+ legacyAssertEqual(parentStop.status, 0, parentStop.stderr)
let parentStopCommands = Array(context.state.commands.dropFirst(parentStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"The parent Stop must still notify after a child Stop supplies a new turn_id, saw \(parentStopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { $0.hasPrefix("set_status codex ") && $0.contains(" Idle ") },
"The parent Stop must still mark Codex idle after a child Stop supplies a new turn_id, saw \(parentStopCommands)"
)
@@ -2165,8 +2165,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"old-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"interrupted"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(interruptedPrompt.timedOut, interruptedPrompt.stderr)
- XCTAssertEqual(interruptedPrompt.status, 0, interruptedPrompt.stderr)
+ legacyAssertFalse(interruptedPrompt.timedOut, interruptedPrompt.stderr)
+ legacyAssertEqual(interruptedPrompt.status, 0, interruptedPrompt.stderr)
let currentPrompt = runCodexHook(
context: context,
@@ -2174,8 +2174,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"current-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"finish now"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(currentPrompt.timedOut, currentPrompt.stderr)
- XCTAssertEqual(currentPrompt.status, 0, currentPrompt.stderr)
+ legacyAssertFalse(currentPrompt.timedOut, currentPrompt.stderr)
+ legacyAssertEqual(currentPrompt.status, 0, currentPrompt.stderr)
let stopStart = context.state.commands.count
let currentStop = runCodexHook(
@@ -2184,15 +2184,15 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"current-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"current done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(currentStop.timedOut, currentStop.stderr)
- XCTAssertEqual(currentStop.status, 0, currentStop.stderr)
+ legacyAssertFalse(currentStop.timedOut, currentStop.stderr)
+ legacyAssertEqual(currentStop.status, 0, currentStop.stderr)
let stopCommands = Array(context.state.commands.dropFirst(stopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
stopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"A stale prompt depth from an interrupted prior turn must not suppress the current top-level completion notification, saw \(stopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
stopCommands.contains { $0.hasPrefix("set_status codex ") && $0.contains(" Idle ") },
"A stale prompt depth from an interrupted prior turn must not leave Codex marked running, saw \(stopCommands)"
)
@@ -2218,8 +2218,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"old-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"old"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
- XCTAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
+ legacyAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
+ legacyAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
let currentPromptStart = context.state.commands.count
let currentPrompt = runCodexHook(
@@ -2228,10 +2228,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"current-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"current"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(currentPrompt.timedOut, currentPrompt.stderr)
- XCTAssertEqual(currentPrompt.status, 0, currentPrompt.stderr)
+ legacyAssertFalse(currentPrompt.timedOut, currentPrompt.stderr)
+ legacyAssertEqual(currentPrompt.status, 0, currentPrompt.stderr)
let currentPromptCommands = Array(context.state.commands.dropFirst(currentPromptStart))
- XCTAssertFalse(
+ legacyAssertFalse(
currentPromptCommands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.set" },
"Before the late terminal transcript update, the current prompt should still look nested, saw \(currentPromptCommands)"
)
@@ -2251,15 +2251,15 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"current-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"current done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(currentStop.timedOut, currentStop.stderr)
- XCTAssertEqual(currentStop.status, 0, currentStop.stderr)
+ legacyAssertFalse(currentStop.timedOut, currentStop.stderr)
+ legacyAssertEqual(currentStop.status, 0, currentStop.stderr)
let currentStopCommands = Array(context.state.commands.dropFirst(currentStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
currentStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"A late terminal prior turn must not suppress the current top-level completion notification, saw \(currentStopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
currentStopCommands.contains { $0.hasPrefix("set_status codex ") && $0.contains(" Idle ") },
"A late terminal prior turn must not leave Codex marked running after the current Stop, saw \(currentStopCommands)"
)
@@ -2286,8 +2286,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"spawn child"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
- XCTAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
+ legacyAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
+ legacyAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
let childPrompt = runCodexHook(
context: context,
@@ -2295,8 +2295,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","transcript_path":"\#(terminalChildTranscript.path)","hook_event_name":"UserPromptSubmit","prompt":"first child"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childPrompt.timedOut, childPrompt.stderr)
- XCTAssertEqual(childPrompt.status, 0, childPrompt.stderr)
+ legacyAssertFalse(childPrompt.timedOut, childPrompt.stderr)
+ legacyAssertEqual(childPrompt.status, 0, childPrompt.stderr)
let siblingPromptStart = context.state.commands.count
let siblingPrompt = runCodexHook(
@@ -2305,14 +2305,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"sibling-turn","cwd":"\#(context.root.path)","transcript_path":"\#(terminalChildTranscript.path)","hook_event_name":"UserPromptSubmit","prompt":"second child"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(siblingPrompt.timedOut, siblingPrompt.stderr)
- XCTAssertEqual(siblingPrompt.status, 0, siblingPrompt.stderr)
+ legacyAssertFalse(siblingPrompt.timedOut, siblingPrompt.stderr)
+ legacyAssertEqual(siblingPrompt.status, 0, siblingPrompt.stderr)
let siblingPromptCommands = Array(context.state.commands.dropFirst(siblingPromptStart))
- XCTAssertFalse(
+ legacyAssertFalse(
siblingPromptCommands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.set" },
"A sibling child prompt after a terminal child transcript must stay nested, saw \(siblingPromptCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
siblingPromptCommands.contains { $0.hasPrefix("set_status codex Running ") },
"A sibling child prompt after a terminal child transcript must not rewrite parent Running status, saw \(siblingPromptCommands)"
)
@@ -2324,10 +2324,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","transcript_path":"\#(terminalChildTranscript.path)","hook_event_name":"Stop","last_assistant_message":"first child done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childStop.timedOut, childStop.stderr)
- XCTAssertEqual(childStop.status, 0, childStop.stderr)
+ legacyAssertFalse(childStop.timedOut, childStop.stderr)
+ legacyAssertEqual(childStop.status, 0, childStop.stderr)
let childStopCommands = Array(context.state.commands.dropFirst(childStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"A late terminal child Stop must remain nested after a sibling child prompt, saw \(childStopCommands)"
)
@@ -2339,10 +2339,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"sibling-turn","cwd":"\#(context.root.path)","transcript_path":"\#(terminalChildTranscript.path)","hook_event_name":"Stop","last_assistant_message":"second child done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(siblingStop.timedOut, siblingStop.stderr)
- XCTAssertEqual(siblingStop.status, 0, siblingStop.stderr)
+ legacyAssertFalse(siblingStop.timedOut, siblingStop.stderr)
+ legacyAssertEqual(siblingStop.status, 0, siblingStop.stderr)
let siblingStopCommands = Array(context.state.commands.dropFirst(siblingStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
siblingStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"The sibling child Stop must not notify while the parent is active, saw \(siblingStopCommands)"
)
@@ -2354,10 +2354,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"parent done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentStop.timedOut, parentStop.stderr)
- XCTAssertEqual(parentStop.status, 0, parentStop.stderr)
+ legacyAssertFalse(parentStop.timedOut, parentStop.stderr)
+ legacyAssertEqual(parentStop.status, 0, parentStop.stderr)
let parentStopCommands = Array(context.state.commands.dropFirst(parentStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"The parent Stop should still notify after terminal nested children, saw \(parentStopCommands)"
)
@@ -2385,8 +2385,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"parent"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
- XCTAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
+ legacyAssertFalse(parentPrompt.timedOut, parentPrompt.stderr)
+ legacyAssertEqual(parentPrompt.status, 0, parentPrompt.stderr)
let childPrompt = runCodexHook(
context: context,
@@ -2394,8 +2394,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"child"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childPrompt.timedOut, childPrompt.stderr)
- XCTAssertEqual(childPrompt.status, 0, childPrompt.stderr)
+ legacyAssertFalse(childPrompt.timedOut, childPrompt.stderr)
+ legacyAssertEqual(childPrompt.status, 0, childPrompt.stderr)
let currentPromptStart = context.state.commands.count
let currentPrompt = runCodexHook(
@@ -2404,10 +2404,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"current-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"current"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(currentPrompt.timedOut, currentPrompt.stderr)
- XCTAssertEqual(currentPrompt.status, 0, currentPrompt.stderr)
+ legacyAssertFalse(currentPrompt.timedOut, currentPrompt.stderr)
+ legacyAssertEqual(currentPrompt.status, 0, currentPrompt.stderr)
let currentPromptCommands = Array(context.state.commands.dropFirst(currentPromptStart))
- XCTAssertTrue(
+ legacyAssertTrue(
currentPromptCommands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.set" },
"A current prompt after a fully terminal interrupted stack must become top-level, saw \(currentPromptCommands)"
)
@@ -2419,10 +2419,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"current-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"current done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(currentStop.timedOut, currentStop.stderr)
- XCTAssertEqual(currentStop.status, 0, currentStop.stderr)
+ legacyAssertFalse(currentStop.timedOut, currentStop.stderr)
+ legacyAssertEqual(currentStop.status, 0, currentStop.stderr)
let currentStopCommands = Array(context.state.commands.dropFirst(currentStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
currentStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"A current Stop after a fully terminal interrupted stack must notify, saw \(currentStopCommands)"
)
@@ -2464,14 +2464,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"child"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childPrompt.timedOut, childPrompt.stderr)
- XCTAssertEqual(childPrompt.status, 0, childPrompt.stderr)
+ legacyAssertFalse(childPrompt.timedOut, childPrompt.stderr)
+ legacyAssertEqual(childPrompt.status, 0, childPrompt.stderr)
let childPromptCommands = Array(context.state.commands.dropFirst(childPromptStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childPromptCommands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.set" },
"A legacy depth-only nested prompt that first gains a turn_id must remain nested, saw \(childPromptCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
childPromptCommands.contains { $0.hasPrefix("set_status codex Running ") },
"A legacy depth-only nested prompt that first gains a turn_id must not rewrite parent Running status, saw \(childPromptCommands)"
)
@@ -2483,10 +2483,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"child done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childStop.timedOut, childStop.stderr)
- XCTAssertEqual(childStop.status, 0, childStop.stderr)
+ legacyAssertFalse(childStop.timedOut, childStop.stderr)
+ legacyAssertEqual(childStop.status, 0, childStop.stderr)
let childStopCommands = Array(context.state.commands.dropFirst(childStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"A legacy depth-only nested Stop that first gains a turn_id must remain nested, saw \(childStopCommands)"
)
@@ -2536,14 +2536,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"child"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childPrompt.timedOut, childPrompt.stderr)
- XCTAssertEqual(childPrompt.status, 0, childPrompt.stderr)
+ legacyAssertFalse(childPrompt.timedOut, childPrompt.stderr)
+ legacyAssertEqual(childPrompt.status, 0, childPrompt.stderr)
let childPromptCommands = Array(context.state.commands.dropFirst(childPromptStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childPromptCommands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.set" },
"A historical terminal turn must not make an active depth-only parent look finished, saw \(childPromptCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
childPromptCommands.contains { $0.hasPrefix("set_status codex Running ") },
"A historical terminal turn must not let the child rewrite parent Running status, saw \(childPromptCommands)"
)
@@ -2555,10 +2555,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"child done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childStop.timedOut, childStop.stderr)
- XCTAssertEqual(childStop.status, 0, childStop.stderr)
+ legacyAssertFalse(childStop.timedOut, childStop.stderr)
+ legacyAssertEqual(childStop.status, 0, childStop.stderr)
let childStopCommands = Array(context.state.commands.dropFirst(childStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"A historical terminal turn must not let a child Stop notify while the parent is active, saw \(childStopCommands)"
)
@@ -2607,14 +2607,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"child"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childPrompt.timedOut, childPrompt.stderr)
- XCTAssertEqual(childPrompt.status, 0, childPrompt.stderr)
+ legacyAssertFalse(childPrompt.timedOut, childPrompt.stderr)
+ legacyAssertEqual(childPrompt.status, 0, childPrompt.stderr)
let childPromptCommands = Array(context.state.commands.dropFirst(childPromptStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childPromptCommands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.set" },
"A non-terminal parent turn_context must not make a depth-only parent look finished, saw \(childPromptCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
childPromptCommands.contains { $0.hasPrefix("set_status codex Running ") },
"A non-terminal parent turn_context must not let the child rewrite parent Running status, saw \(childPromptCommands)"
)
@@ -2626,10 +2626,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"child done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childStop.timedOut, childStop.stderr)
- XCTAssertEqual(childStop.status, 0, childStop.stderr)
+ legacyAssertFalse(childStop.timedOut, childStop.stderr)
+ legacyAssertEqual(childStop.status, 0, childStop.stderr)
let childStopCommands = Array(context.state.commands.dropFirst(childStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"A non-terminal parent turn_context must not let a child Stop notify while the parent is active, saw \(childStopCommands)"
)
@@ -2670,8 +2670,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"child"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childPrompt.timedOut, childPrompt.stderr)
- XCTAssertEqual(childPrompt.status, 0, childPrompt.stderr)
+ legacyAssertFalse(childPrompt.timedOut, childPrompt.stderr)
+ legacyAssertEqual(childPrompt.status, 0, childPrompt.stderr)
let childStopStart = context.state.commands.count
let childStop = runCodexHook(
@@ -2680,10 +2680,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"child done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childStop.timedOut, childStop.stderr)
- XCTAssertEqual(childStop.status, 0, childStop.stderr)
+ legacyAssertFalse(childStop.timedOut, childStop.stderr)
+ legacyAssertEqual(childStop.status, 0, childStop.stderr)
let childStopCommands = Array(context.state.commands.dropFirst(childStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"The child Stop should close only the child depth, saw \(childStopCommands)"
)
@@ -2695,14 +2695,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"parent-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"parent done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(parentStop.timedOut, parentStop.stderr)
- XCTAssertEqual(parentStop.status, 0, parentStop.stderr)
+ legacyAssertFalse(parentStop.timedOut, parentStop.stderr)
+ legacyAssertEqual(parentStop.status, 0, parentStop.stderr)
let parentStopCommands = Array(context.state.commands.dropFirst(parentStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"The depth-only parent Stop must notify after its child turn stops, saw \(parentStopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
parentStopCommands.contains { $0.hasPrefix("set_status codex ") && $0.contains(" Idle ") },
"The depth-only parent Stop must mark Codex idle, saw \(parentStopCommands)"
)
@@ -2751,12 +2751,12 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"child done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childStop.timedOut, childStop.stderr)
- XCTAssertEqual(childStop.status, 0, childStop.stderr)
- XCTAssertEqual(childStop.stdout.trimmingCharacters(in: .whitespacesAndNewlines), "{}")
- XCTAssertEqual(childStop.stderr, "")
+ legacyAssertFalse(childStop.timedOut, childStop.stderr)
+ legacyAssertEqual(childStop.status, 0, childStop.stderr)
+ legacyAssertEqual(childStop.stdout.trimmingCharacters(in: .whitespacesAndNewlines), "{}")
+ legacyAssertEqual(childStop.stderr, "")
let childStopCommands = Array(context.state.commands.dropFirst(childStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"A late terminal child Stop must stay nested while the depth-only parent remains active, saw \(childStopCommands)"
)
@@ -2768,14 +2768,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"sibling-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"sibling"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(siblingPrompt.timedOut, siblingPrompt.stderr)
- XCTAssertEqual(siblingPrompt.status, 0, siblingPrompt.stderr)
+ legacyAssertFalse(siblingPrompt.timedOut, siblingPrompt.stderr)
+ legacyAssertEqual(siblingPrompt.status, 0, siblingPrompt.stderr)
let siblingPromptCommands = Array(context.state.commands.dropFirst(siblingPromptStart))
- XCTAssertFalse(
+ legacyAssertFalse(
siblingPromptCommands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.set" },
"A sibling prompt must stay nested while the depth-only parent remains active, saw \(siblingPromptCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
siblingPromptCommands.contains { $0.hasPrefix("set_status codex Running ") },
"A sibling prompt must not rewrite parent Running status while the depth-only parent remains active, saw \(siblingPromptCommands)"
)
@@ -2787,10 +2787,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"sibling-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"sibling done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(siblingStop.timedOut, siblingStop.stderr)
- XCTAssertEqual(siblingStop.status, 0, siblingStop.stderr)
+ legacyAssertFalse(siblingStop.timedOut, siblingStop.stderr)
+ legacyAssertEqual(siblingStop.status, 0, siblingStop.stderr)
let siblingStopCommands = Array(context.state.commands.dropFirst(siblingStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
siblingStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"A sibling Stop must stay nested while the depth-only parent remains active, saw \(siblingStopCommands)"
)
@@ -2840,14 +2840,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"child"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childPrompt.timedOut, childPrompt.stderr)
- XCTAssertEqual(childPrompt.status, 0, childPrompt.stderr)
+ legacyAssertFalse(childPrompt.timedOut, childPrompt.stderr)
+ legacyAssertEqual(childPrompt.status, 0, childPrompt.stderr)
let childPromptCommands = Array(context.state.commands.dropFirst(childPromptStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childPromptCommands.contains { (self.jsonObject($0)?["method"] as? String)?.hasPrefix("surface.resume.") == true },
"Terminal history alone must not make unknown depth-only active prompts look finished, saw \(childPromptCommands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
childPromptCommands.contains { $0.hasPrefix("set_status codex Running ") },
"Terminal history alone must not let the child rewrite parent Running status, saw \(childPromptCommands)"
)
@@ -2859,10 +2859,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"child-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"child done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(childStop.timedOut, childStop.stderr)
- XCTAssertEqual(childStop.status, 0, childStop.stderr)
+ legacyAssertFalse(childStop.timedOut, childStop.stderr)
+ legacyAssertEqual(childStop.status, 0, childStop.stderr)
let childStopCommands = Array(context.state.commands.dropFirst(childStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
childStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"Terminal history alone must not let a child Stop notify while unknown depth remains, saw \(childStopCommands)"
)
@@ -2889,8 +2889,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"old-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"old"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
- XCTAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
+ legacyAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
+ legacyAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
let currentPrompt = runCodexHook(
context: context,
@@ -2898,8 +2898,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"current-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"current"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(currentPrompt.timedOut, currentPrompt.stderr)
- XCTAssertEqual(currentPrompt.status, 0, currentPrompt.stderr)
+ legacyAssertFalse(currentPrompt.timedOut, currentPrompt.stderr)
+ legacyAssertEqual(currentPrompt.status, 0, currentPrompt.stderr)
let staleStopStart = context.state.commands.count
let staleStop = runCodexHook(
@@ -2908,11 +2908,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"old-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"old done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(staleStop.timedOut, staleStop.stderr)
- XCTAssertEqual(staleStop.status, 0, staleStop.stderr)
+ legacyAssertFalse(staleStop.timedOut, staleStop.stderr)
+ legacyAssertEqual(staleStop.status, 0, staleStop.stderr)
let staleStopCommands = Array(context.state.commands.dropFirst(staleStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
staleStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"A stale Stop from an older turn must not notify or mark the newer active turn idle, saw \(staleStopCommands)"
)
@@ -2924,11 +2924,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"current-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"current done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(currentStop.timedOut, currentStop.stderr)
- XCTAssertEqual(currentStop.status, 0, currentStop.stderr)
+ legacyAssertFalse(currentStop.timedOut, currentStop.stderr)
+ legacyAssertEqual(currentStop.status, 0, currentStop.stderr)
let currentStopCommands = Array(context.state.commands.dropFirst(currentStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
currentStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"The current turn should still notify after a stale older Stop, saw \(currentStopCommands)"
)
@@ -2955,8 +2955,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"old-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"old"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
- XCTAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
+ legacyAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
+ legacyAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
let currentPrompt = runCodexHook(
context: context,
@@ -2964,8 +2964,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"current-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"current"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(currentPrompt.timedOut, currentPrompt.stderr)
- XCTAssertEqual(currentPrompt.status, 0, currentPrompt.stderr)
+ legacyAssertFalse(currentPrompt.timedOut, currentPrompt.stderr)
+ legacyAssertEqual(currentPrompt.status, 0, currentPrompt.stderr)
let currentStopStart = context.state.commands.count
let currentStop = runCodexHook(
@@ -2974,10 +2974,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"current-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"current done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(currentStop.timedOut, currentStop.stderr)
- XCTAssertEqual(currentStop.status, 0, currentStop.stderr)
+ legacyAssertFalse(currentStop.timedOut, currentStop.stderr)
+ legacyAssertEqual(currentStop.status, 0, currentStop.stderr)
let currentStopCommands = Array(context.state.commands.dropFirst(currentStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
currentStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"The current turn should notify before a late stale Stop arrives, saw \(currentStopCommands)"
)
@@ -2989,11 +2989,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"old-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"old done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(lateStop.timedOut, lateStop.stderr)
- XCTAssertEqual(lateStop.status, 0, lateStop.stderr)
+ legacyAssertFalse(lateStop.timedOut, lateStop.stderr)
+ legacyAssertEqual(lateStop.status, 0, lateStop.stderr)
let lateStopCommands = Array(context.state.commands.dropFirst(lateStopStart))
- XCTAssertFalse(
+ legacyAssertFalse(
lateStopCommands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"A late stale Stop from an older turn must not duplicate the newer turn completion, saw \(lateStopCommands)"
)
@@ -3020,8 +3020,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"old-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"old"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
- XCTAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
+ legacyAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
+ legacyAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
let currentStopStart = context.state.commands.count
let currentStop = runCodexHook(
@@ -3030,15 +3030,15 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"current-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"current done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(currentStop.timedOut, currentStop.stderr)
- XCTAssertEqual(currentStop.status, 0, currentStop.stderr)
+ legacyAssertFalse(currentStop.timedOut, currentStop.stderr)
+ legacyAssertEqual(currentStop.status, 0, currentStop.stderr)
let currentStopCommands = Array(context.state.commands.dropFirst(currentStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
currentStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"A Stop after a missed prompt-submit must clear terminal stale turns and notify, saw \(currentStopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
currentStopCommands.contains { $0.hasPrefix("set_status codex ") && $0.contains(" Idle ") },
"A Stop after a missed prompt-submit must mark Codex idle, saw \(currentStopCommands)"
)
@@ -3094,15 +3094,15 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"current-turn","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"current done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(currentStop.timedOut, currentStop.stderr)
- XCTAssertEqual(currentStop.status, 0, currentStop.stderr)
+ legacyAssertFalse(currentStop.timedOut, currentStop.stderr)
+ legacyAssertEqual(currentStop.status, 0, currentStop.stderr)
let currentStopCommands = Array(context.state.commands.dropFirst(currentStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
currentStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"A missed prompt-submit Stop must clear a fully terminal stored stack and notify, saw \(currentStopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
currentStopCommands.contains { $0.hasPrefix("set_status codex ") && $0.contains(" Idle ") },
"A missed prompt-submit Stop must clear a fully terminal stored stack and mark Codex idle, saw \(currentStopCommands)"
)
@@ -3123,8 +3123,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"old-turn","cwd":"\#(context.root.path)","hook_event_name":"UserPromptSubmit","prompt":"old"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
- XCTAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
+ legacyAssertFalse(oldPrompt.timedOut, oldPrompt.stderr)
+ legacyAssertEqual(oldPrompt.status, 0, oldPrompt.stderr)
let oldStop = runCodexHook(
context: context,
@@ -3132,8 +3132,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"old-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"old done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(oldStop.timedOut, oldStop.stderr)
- XCTAssertEqual(oldStop.status, 0, oldStop.stderr)
+ legacyAssertFalse(oldStop.timedOut, oldStop.stderr)
+ legacyAssertEqual(oldStop.status, 0, oldStop.stderr)
let unseenStopStart = context.state.commands.count
let unseenStop = runCodexHook(
@@ -3142,15 +3142,15 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","turn_id":"new-turn","cwd":"\#(context.root.path)","hook_event_name":"Stop","last_assistant_message":"new done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(unseenStop.timedOut, unseenStop.stderr)
- XCTAssertEqual(unseenStop.status, 0, unseenStop.stderr)
+ legacyAssertFalse(unseenStop.timedOut, unseenStop.stderr)
+ legacyAssertEqual(unseenStop.status, 0, unseenStop.stderr)
let unseenStopCommands = Array(context.state.commands.dropFirst(unseenStopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
unseenStopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"A Stop with a missed prompt-submit must still notify at idle depth, saw \(unseenStopCommands)"
)
- XCTAssertTrue(
+ legacyAssertTrue(
unseenStopCommands.contains { $0.hasPrefix("set_status codex ") },
"A Stop with a missed prompt-submit must still update Codex status, saw \(unseenStopCommands)"
)
@@ -3175,18 +3175,18 @@ final class CLINotifyProcessIntegrationRegressionTests {
], uniquingKeysWith: { _, new in new })
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
- XCTAssertTrue(
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
+ legacyAssertTrue(
context.state.commands.contains { $0.contains(#""method":"feed.push""#) && $0.contains(#""hook_event_name":"Stop""#) },
"Managed subagent Stop should remain Feed telemetry, saw \(context.state.commands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
context.state.commands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.set" },
"Managed subagent Stop should not publish a child resume binding, saw \(context.state.commands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
context.state.commands.contains { $0.hasPrefix("notify_target") || $0.hasPrefix("set_status codex ") },
"Managed subagent Stop should not notify or clobber visible status, saw \(context.state.commands)"
)
@@ -3214,8 +3214,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"top-level"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(prompt.timedOut, prompt.stderr)
- XCTAssertEqual(prompt.status, 0, prompt.stderr)
+ legacyAssertFalse(prompt.timedOut, prompt.stderr)
+ legacyAssertEqual(prompt.status, 0, prompt.stderr)
let stopStart = context.state.commands.count
let stop = runCodexHook(
@@ -3224,11 +3224,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: #"{"session_id":"\#(sessionId)","cwd":"\#(context.root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"Stop","last_assistant_message":"parent done"}"#,
extraEnvironment: launchEnvironment
)
- XCTAssertFalse(stop.timedOut, stop.stderr)
- XCTAssertEqual(stop.status, 0, stop.stderr)
+ legacyAssertFalse(stop.timedOut, stop.stderr)
+ legacyAssertEqual(stop.status, 0, stop.stderr)
let stopCommands = Array(context.state.commands.dropFirst(stopStart))
- XCTAssertTrue(
+ legacyAssertTrue(
stopCommands.contains { $0.hasPrefix("notify_target_async \(context.workspaceId) \(context.surfaceId) Codex|") },
"Stale completed-turn subagent relay should not suppress the parent completion notification, saw \(stopCommands)"
)
@@ -3272,19 +3272,19 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertFalse(
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(
context.state.commands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.clear" },
"Managed subagent SessionEnd should not clear the parent resume binding, saw \(context.state.commands)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
context.state.commands.contains { $0.hasPrefix("clear_agent_pid codex.") },
"Managed subagent SessionEnd should not clear the visible parent PID, saw \(context.state.commands)"
)
- let savedState = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
- let savedSessions = try XCTUnwrap(savedState["sessions"] as? [String: Any])
- XCTAssertNotNil(savedSessions[sessionId], "Suppressed SessionEnd should leave the stored parent session intact")
+ let savedState = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
+ let savedSessions = try legacyUnwrap(savedState["sessions"] as? [String: Any])
+ legacyAssertNotNil(savedSessions[sessionId], "Suppressed SessionEnd should leave the stored parent session intact")
}
@Test
@@ -3317,7 +3317,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
let serverHandled = startMockServer(listenerFD: listenerFD, state: state) { line in
- XCTAssertEqual(line, item.expectedCommand)
+ legacyAssertEqual(line, item.expectedCommand)
return item.response
}
@@ -3332,12 +3332,12 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, "\(item.name): \(result.stderr)")
- XCTAssertEqual(result.status, 0, "\(item.name): \(result.stderr)")
- XCTAssertEqual(result.stdout, item.stdout, item.name)
- XCTAssertTrue(result.stderr.isEmpty, "\(item.name): \(result.stderr)")
- XCTAssertEqual(state.commands, [item.expectedCommand], item.name)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, "\(item.name): \(result.stderr)")
+ legacyAssertEqual(result.status, 0, "\(item.name): \(result.stderr)")
+ legacyAssertEqual(result.stdout, item.stdout, item.name)
+ legacyAssertTrue(result.stderr.isEmpty, "\(item.name): \(result.stderr)")
+ legacyAssertEqual(state.commands, [item.expectedCommand], item.name)
}
}
@@ -3356,11 +3356,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stdout.isEmpty, result.stdout)
- XCTAssertTrue(result.stderr.contains("Unknown right-sidebar command 'unknown'"), result.stderr)
- XCTAssertFalse(result.stderr.contains("Socket"), result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stdout.isEmpty, result.stdout)
+ legacyAssertTrue(result.stderr.contains("Unknown right-sidebar command 'unknown'"), result.stderr)
+ legacyAssertFalse(result.stderr.contains("Socket"), result.stderr)
}
@Test
@@ -3378,11 +3378,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stdout.isEmpty, result.stdout)
- XCTAssertTrue(result.stderr.contains("Unknown right-sidebar mode 'unknown'"), result.stderr)
- XCTAssertFalse(result.stderr.contains("Socket"), result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stdout.isEmpty, result.stdout)
+ legacyAssertTrue(result.stderr.contains("Unknown right-sidebar mode 'unknown'"), result.stderr)
+ legacyAssertFalse(result.stderr.contains("Socket"), result.stderr)
}
@Test
@@ -3401,26 +3401,26 @@ final class CLINotifyProcessIntegrationRegressionTests {
func testSSHPersistentPTYJSONReportsResolvedSessionID() throws {
let run = try runMockedSSH(arguments: [], jsonOutput: true)
let payload = try jsonPayload(from: run.stdout)
- let sessionID = try XCTUnwrap(payload["ssh_pty_session_id"] as? String)
- let persistentDaemonSlot = try XCTUnwrap(payload["persistent_daemon_slot"] as? String)
+ let sessionID = try legacyUnwrap(payload["ssh_pty_session_id"] as? String)
+ let persistentDaemonSlot = try legacyUnwrap(payload["persistent_daemon_slot"] as? String)
- XCTAssertEqual(sessionID, "ssh-\(run.workspaceId)-\(run.surfaceId)")
- XCTAssertFalse(sessionID.contains("$"), sessionID)
- XCTAssertFalse(sessionID.contains("{"), sessionID)
- XCTAssertTrue(persistentDaemonSlot.hasPrefix("ssh-"), persistentDaemonSlot)
- XCTAssertNotNil(UUID(uuidString: String(persistentDaemonSlot.dropFirst(4))))
+ legacyAssertEqual(sessionID, "ssh-\(run.workspaceId)-\(run.surfaceId)")
+ legacyAssertFalse(sessionID.contains("$"), sessionID)
+ legacyAssertFalse(sessionID.contains("{"), sessionID)
+ legacyAssertTrue(persistentDaemonSlot.hasPrefix("ssh-"), persistentDaemonSlot)
+ legacyAssertNotNil(UUID(uuidString: String(persistentDaemonSlot.dropFirst(4))))
}
@Test
func testSSHPersistentPTYJSONResolvesSessionIDWhenWorkspaceCreateOmitsSurfaceID() throws {
let run = try runMockedSSH(arguments: [], jsonOutput: true, omitWorkspaceCreateSurfaceID: true)
let payload = try jsonPayload(from: run.stdout)
- let sessionID = try XCTUnwrap(payload["ssh_pty_session_id"] as? String)
- let persistentDaemonSlot = try XCTUnwrap(payload["persistent_daemon_slot"] as? String)
+ let sessionID = try legacyUnwrap(payload["ssh_pty_session_id"] as? String)
+ let persistentDaemonSlot = try legacyUnwrap(payload["persistent_daemon_slot"] as? String)
- XCTAssertEqual(sessionID, "ssh-\(run.workspaceId)-\(run.surfaceId)")
- XCTAssertTrue(persistentDaemonSlot.hasPrefix("ssh-"), persistentDaemonSlot)
- XCTAssertNotNil(UUID(uuidString: String(persistentDaemonSlot.dropFirst(4))))
+ legacyAssertEqual(sessionID, "ssh-\(run.workspaceId)-\(run.surfaceId)")
+ legacyAssertTrue(persistentDaemonSlot.hasPrefix("ssh-"), persistentDaemonSlot)
+ legacyAssertNotNil(UUID(uuidString: String(persistentDaemonSlot.dropFirst(4))))
}
@Test
@@ -3430,14 +3430,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
arguments: ["--forward-agent"],
environmentOverrides: ["SSH_AUTH_SOCK": agentSocketPath]
)
- let createParams = try XCTUnwrap(params(for: "workspace.create", in: run.requests))
- let configureParams = try XCTUnwrap(params(for: "workspace.remote.configure", in: run.requests))
- let sshOptions = try XCTUnwrap(configureParams["ssh_options"] as? [String])
- let initialEnv = try XCTUnwrap(createParams["initial_env"] as? [String: String])
+ let createParams = try legacyUnwrap(params(for: "workspace.create", in: run.requests))
+ let configureParams = try legacyUnwrap(params(for: "workspace.remote.configure", in: run.requests))
+ let sshOptions = try legacyUnwrap(configureParams["ssh_options"] as? [String])
+ let initialEnv = try legacyUnwrap(createParams["initial_env"] as? [String: String])
- XCTAssertTrue(sshOptions.contains("ForwardAgent=yes"), "ssh_options: \(sshOptions)")
- XCTAssertEqual(initialEnv["SSH_AUTH_SOCK"], agentSocketPath)
- XCTAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
+ legacyAssertTrue(sshOptions.contains("ForwardAgent=yes"), "ssh_options: \(sshOptions)")
+ legacyAssertEqual(initialEnv["SSH_AUTH_SOCK"], agentSocketPath)
+ legacyAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
}
@Test
@@ -3447,14 +3447,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
arguments: ["--ssh-option", "ForwardAgent=yes"],
environmentOverrides: ["SSH_AUTH_SOCK": agentSocketPath]
)
- let createParams = try XCTUnwrap(params(for: "workspace.create", in: run.requests))
- let configureParams = try XCTUnwrap(params(for: "workspace.remote.configure", in: run.requests))
- let sshOptions = try XCTUnwrap(configureParams["ssh_options"] as? [String])
- let initialEnv = try XCTUnwrap(createParams["initial_env"] as? [String: String])
+ let createParams = try legacyUnwrap(params(for: "workspace.create", in: run.requests))
+ let configureParams = try legacyUnwrap(params(for: "workspace.remote.configure", in: run.requests))
+ let sshOptions = try legacyUnwrap(configureParams["ssh_options"] as? [String])
+ let initialEnv = try legacyUnwrap(createParams["initial_env"] as? [String: String])
- XCTAssertTrue(sshOptions.contains("ForwardAgent=yes"), "ssh_options: \(sshOptions)")
- XCTAssertEqual(initialEnv["SSH_AUTH_SOCK"], agentSocketPath)
- XCTAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
+ legacyAssertTrue(sshOptions.contains("ForwardAgent=yes"), "ssh_options: \(sshOptions)")
+ legacyAssertEqual(initialEnv["SSH_AUTH_SOCK"], agentSocketPath)
+ legacyAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
}
@Test
@@ -3468,16 +3468,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
"SSH_AUTH_SOCK": "/tmp/cmux-test-agent-\(UUID().uuidString).sock",
]
)
- let createParams = try XCTUnwrap(params(for: "workspace.create", in: run.requests))
- let configureParams = try XCTUnwrap(params(for: "workspace.remote.configure", in: run.requests))
- let sshOptions = try XCTUnwrap(configureParams["ssh_options"] as? [String])
+ let createParams = try legacyUnwrap(params(for: "workspace.create", in: run.requests))
+ let configureParams = try legacyUnwrap(params(for: "workspace.remote.configure", in: run.requests))
+ let sshOptions = try legacyUnwrap(configureParams["ssh_options"] as? [String])
- XCTAssertEqual(sshOptions.filter { $0.hasPrefix("ForwardAgent=") }, [
+ legacyAssertEqual(sshOptions.filter { $0.hasPrefix("ForwardAgent=") }, [
"ForwardAgent=yes",
"ForwardAgent=no",
])
- XCTAssertNil(createParams["initial_env"])
- XCTAssertNil(configureParams["ssh_auth_sock"])
+ legacyAssertNil(createParams["initial_env"])
+ legacyAssertNil(configureParams["ssh_auth_sock"])
}
@Test
@@ -3489,13 +3489,13 @@ final class CLINotifyProcessIntegrationRegressionTests {
"SSH_AUTH_SOCK": agentSocketPath,
]
)
- let createParams = try XCTUnwrap(params(for: "workspace.create", in: run.requests))
- let configureParams = try XCTUnwrap(params(for: "workspace.remote.configure", in: run.requests))
- let initialEnv = try XCTUnwrap(createParams["initial_env"] as? [String: String])
+ let createParams = try legacyUnwrap(params(for: "workspace.create", in: run.requests))
+ let configureParams = try legacyUnwrap(params(for: "workspace.remote.configure", in: run.requests))
+ let initialEnv = try legacyUnwrap(createParams["initial_env"] as? [String: String])
- XCTAssertNil(configureParams["ssh_options"])
- XCTAssertEqual(initialEnv["SSH_AUTH_SOCK"], agentSocketPath)
- XCTAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
+ legacyAssertNil(configureParams["ssh_options"])
+ legacyAssertEqual(initialEnv["SSH_AUTH_SOCK"], agentSocketPath)
+ legacyAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
}
@Test
@@ -3504,14 +3504,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
let run = try runMockedSSH(
arguments: ["--ssh-option", "ForwardAgent=\(agentSocketPath)"]
)
- let createParams = try XCTUnwrap(params(for: "workspace.create", in: run.requests))
- let configureParams = try XCTUnwrap(params(for: "workspace.remote.configure", in: run.requests))
- let sshOptions = try XCTUnwrap(configureParams["ssh_options"] as? [String])
- let initialEnv = try XCTUnwrap(createParams["initial_env"] as? [String: String])
+ let createParams = try legacyUnwrap(params(for: "workspace.create", in: run.requests))
+ let configureParams = try legacyUnwrap(params(for: "workspace.remote.configure", in: run.requests))
+ let sshOptions = try legacyUnwrap(configureParams["ssh_options"] as? [String])
+ let initialEnv = try legacyUnwrap(createParams["initial_env"] as? [String: String])
- XCTAssertTrue(sshOptions.contains("ForwardAgent=\(agentSocketPath)"), "ssh_options: \(sshOptions)")
- XCTAssertEqual(initialEnv["SSH_AUTH_SOCK"], agentSocketPath)
- XCTAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
+ legacyAssertTrue(sshOptions.contains("ForwardAgent=\(agentSocketPath)"), "ssh_options: \(sshOptions)")
+ legacyAssertEqual(initialEnv["SSH_AUTH_SOCK"], agentSocketPath)
+ legacyAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
}
@Test
@@ -3526,14 +3526,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
"HOME": homeURL.path,
]
)
- let createParams = try XCTUnwrap(params(for: "workspace.create", in: run.requests))
- let configureParams = try XCTUnwrap(params(for: "workspace.remote.configure", in: run.requests))
- let sshOptions = try XCTUnwrap(configureParams["ssh_options"] as? [String])
- let initialEnv = try XCTUnwrap(createParams["initial_env"] as? [String: String])
+ let createParams = try legacyUnwrap(params(for: "workspace.create", in: run.requests))
+ let configureParams = try legacyUnwrap(params(for: "workspace.remote.configure", in: run.requests))
+ let sshOptions = try legacyUnwrap(configureParams["ssh_options"] as? [String])
+ let initialEnv = try legacyUnwrap(createParams["initial_env"] as? [String: String])
- XCTAssertTrue(sshOptions.contains("ForwardAgent=\(tildeSocketPath)"), "ssh_options: \(sshOptions)")
- XCTAssertEqual(initialEnv["SSH_AUTH_SOCK"], expandedSocketURL.path)
- XCTAssertEqual(configureParams["ssh_auth_sock"] as? String, expandedSocketURL.path)
+ legacyAssertTrue(sshOptions.contains("ForwardAgent=\(tildeSocketPath)"), "ssh_options: \(sshOptions)")
+ legacyAssertEqual(initialEnv["SSH_AUTH_SOCK"], expandedSocketURL.path)
+ legacyAssertEqual(configureParams["ssh_auth_sock"] as? String, expandedSocketURL.path)
}
@Test
@@ -3544,13 +3544,13 @@ final class CLINotifyProcessIntegrationRegressionTests {
"SSH_AUTH_SOCK": "/tmp/cmux-test-agent-\(UUID().uuidString).sock",
]
)
- let createParams = try XCTUnwrap(params(for: "workspace.create", in: run.requests))
- let configureParams = try XCTUnwrap(params(for: "workspace.remote.configure", in: run.requests))
- let sshOptions = try XCTUnwrap(configureParams["ssh_options"] as? [String])
+ let createParams = try legacyUnwrap(params(for: "workspace.create", in: run.requests))
+ let configureParams = try legacyUnwrap(params(for: "workspace.remote.configure", in: run.requests))
+ let sshOptions = try legacyUnwrap(configureParams["ssh_options"] as? [String])
- XCTAssertTrue(sshOptions.contains("ForwardAgent=ask"), "ssh_options: \(sshOptions)")
- XCTAssertNil(createParams["initial_env"])
- XCTAssertNil(configureParams["ssh_auth_sock"])
+ legacyAssertTrue(sshOptions.contains("ForwardAgent=ask"), "ssh_options: \(sshOptions)")
+ legacyAssertNil(createParams["initial_env"])
+ legacyAssertNil(configureParams["ssh_auth_sock"])
}
@Test
@@ -3562,14 +3562,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
"SSH_AUTH_SOCK": agentSocketPath,
]
)
- let createParams = try XCTUnwrap(params(for: "workspace.create", in: run.requests))
- let configureParams = try XCTUnwrap(params(for: "workspace.remote.configure", in: run.requests))
- let sshOptions = try XCTUnwrap(configureParams["ssh_options"] as? [String])
- let initialEnv = try XCTUnwrap(createParams["initial_env"] as? [String: String])
+ let createParams = try legacyUnwrap(params(for: "workspace.create", in: run.requests))
+ let configureParams = try legacyUnwrap(params(for: "workspace.remote.configure", in: run.requests))
+ let sshOptions = try legacyUnwrap(configureParams["ssh_options"] as? [String])
+ let initialEnv = try legacyUnwrap(createParams["initial_env"] as? [String: String])
- XCTAssertTrue(sshOptions.contains("ForwardAgent=no"), "ssh_options: \(sshOptions)")
- XCTAssertEqual(initialEnv["SSH_AUTH_SOCK"], agentSocketPath)
- XCTAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
+ legacyAssertTrue(sshOptions.contains("ForwardAgent=no"), "ssh_options: \(sshOptions)")
+ legacyAssertEqual(initialEnv["SSH_AUTH_SOCK"], agentSocketPath)
+ legacyAssertEqual(configureParams["ssh_auth_sock"] as? String, agentSocketPath)
}
private func assertSSHPersistentPTYUsesReusableForegroundAuthControlConnection(
@@ -3577,72 +3577,72 @@ final class CLINotifyProcessIntegrationRegressionTests {
file: StaticString = #filePath,
line: UInt = #line
) throws {
- let createParams = try XCTUnwrap(params(for: "workspace.create", in: run.requests))
- let configureParams = try XCTUnwrap(params(for: "workspace.remote.configure", in: run.requests))
- let initialCommand = try XCTUnwrap(createParams["initial_command"] as? String)
- let terminalStartupCommand = try XCTUnwrap(configureParams["terminal_startup_command"] as? String)
- let initialScript = try XCTUnwrap(decodedReusableStartupScript(from: initialCommand))
- let terminalStartupScript = try XCTUnwrap(decodedReusableStartupScript(from: terminalStartupCommand))
-
- XCTAssertTrue(initialScript.contains("ssh-pty-attach"), initialScript)
- XCTAssertTrue(initialScript.contains("--wait"), initialScript)
- XCTAssertTrue(initialScript.contains("ssh-session-end"), initialScript)
- XCTAssertTrue(initialScript.contains("CMUX_WORKSPACE_ID"), initialScript)
- XCTAssertTrue(initialScript.contains("CMUX_SURFACE_ID"), initialScript)
- XCTAssertTrue(
+ let createParams = try legacyUnwrap(params(for: "workspace.create", in: run.requests))
+ let configureParams = try legacyUnwrap(params(for: "workspace.remote.configure", in: run.requests))
+ let initialCommand = try legacyUnwrap(createParams["initial_command"] as? String)
+ let terminalStartupCommand = try legacyUnwrap(configureParams["terminal_startup_command"] as? String)
+ let initialScript = try legacyUnwrap(decodedReusableStartupScript(from: initialCommand))
+ let terminalStartupScript = try legacyUnwrap(decodedReusableStartupScript(from: terminalStartupCommand))
+
+ legacyAssertTrue(initialScript.contains("ssh-pty-attach"), initialScript)
+ legacyAssertTrue(initialScript.contains("--wait"), initialScript)
+ legacyAssertTrue(initialScript.contains("ssh-session-end"), initialScript)
+ legacyAssertTrue(initialScript.contains("CMUX_WORKSPACE_ID"), initialScript)
+ legacyAssertTrue(initialScript.contains("CMUX_SURFACE_ID"), initialScript)
+ legacyAssertTrue(
initialScript.contains("required workspace context missing for SSH PTY attach"),
initialScript
)
- XCTAssertTrue(
+ legacyAssertTrue(
initialScript.contains("required terminal context missing for SSH PTY attach"),
initialScript
)
- XCTAssertTrue(initialScript.contains("ssh-$cmux_ssh_pty_workspace_id-$cmux_ssh_pty_surface_id"), initialScript)
+ legacyAssertTrue(initialScript.contains("ssh-$cmux_ssh_pty_workspace_id-$cmux_ssh_pty_surface_id"), initialScript)
let attachIDFlag = "--attachment-id \"$cmux_ssh_pty_surface_id\""
- XCTAssertTrue(initialScript.contains("254|255"), initialScript)
- XCTAssertTrue(initialScript.split(separator: "\n").first { $0.contains("ssh-pty-attach") }.map { $0.contains(attachIDFlag) && !$0.contains("--surface") } == true, initialScript)
- XCTAssertTrue(
+ legacyAssertTrue(initialScript.contains("254|255"), initialScript)
+ legacyAssertTrue(initialScript.split(separator: "\n").first { $0.contains("ssh-pty-attach") }.map { $0.contains(attachIDFlag) && !$0.contains("--surface") } == true, initialScript)
+ legacyAssertTrue(
initialScript.contains("--workspace \"$cmux_ssh_pty_workspace_id\""),
initialScript
)
- XCTAssertEqual(
+ legacyAssertEqual(
initialScript.components(separatedBy: "workspace.remote.foreground_auth_ready").count - 1,
1,
initialScript
)
- XCTAssertTrue(terminalStartupScript.contains("ssh-pty-attach"), terminalStartupScript)
- XCTAssertTrue(terminalStartupScript.contains("ssh-session-end"), terminalStartupScript)
- XCTAssertTrue(terminalStartupScript.contains("CMUX_WORKSPACE_ID"), terminalStartupScript)
- XCTAssertTrue(terminalStartupScript.contains("CMUX_SURFACE_ID"), terminalStartupScript)
- XCTAssertTrue(
+ legacyAssertTrue(terminalStartupScript.contains("ssh-pty-attach"), terminalStartupScript)
+ legacyAssertTrue(terminalStartupScript.contains("ssh-session-end"), terminalStartupScript)
+ legacyAssertTrue(terminalStartupScript.contains("CMUX_WORKSPACE_ID"), terminalStartupScript)
+ legacyAssertTrue(terminalStartupScript.contains("CMUX_SURFACE_ID"), terminalStartupScript)
+ legacyAssertTrue(
terminalStartupScript.contains("required workspace context missing for SSH PTY attach"),
terminalStartupScript
)
- XCTAssertTrue(
+ legacyAssertTrue(
terminalStartupScript.contains("required terminal context missing for SSH PTY attach"),
terminalStartupScript
)
- XCTAssertTrue(
+ legacyAssertTrue(
terminalStartupScript.contains("ssh-$cmux_ssh_pty_workspace_id-$cmux_ssh_pty_surface_id"),
terminalStartupScript
)
- XCTAssertTrue(terminalStartupScript.contains("254|255"), terminalStartupScript)
- XCTAssertTrue(terminalStartupScript.split(separator: "\n").first { $0.contains("ssh-pty-attach") }.map { $0.contains(attachIDFlag) && !$0.contains("--surface") } == true, terminalStartupScript)
- XCTAssertTrue(
+ legacyAssertTrue(terminalStartupScript.contains("254|255"), terminalStartupScript)
+ legacyAssertTrue(terminalStartupScript.split(separator: "\n").first { $0.contains("ssh-pty-attach") }.map { $0.contains(attachIDFlag) && !$0.contains("--surface") } == true, terminalStartupScript)
+ legacyAssertTrue(
terminalStartupScript.contains("--workspace \"$cmux_ssh_pty_workspace_id\""),
terminalStartupScript
)
- XCTAssertEqual(
+ legacyAssertEqual(
terminalStartupScript.components(separatedBy: "workspace.remote.foreground_auth_ready").count - 1,
1,
terminalStartupScript
)
- XCTAssertEqual(configureParams["auto_connect"] as? Bool, false)
- XCTAssertNotNil(configureParams["foreground_auth_token"] as? String)
- XCTAssertEqual(configureParams["preserve_after_terminal_exit"] as? Bool, true)
- let persistentDaemonSlot = try XCTUnwrap(configureParams["persistent_daemon_slot"] as? String)
- XCTAssertTrue(persistentDaemonSlot.hasPrefix("ssh-"), persistentDaemonSlot)
- XCTAssertNotNil(UUID(uuidString: String(persistentDaemonSlot.dropFirst(4))))
+ legacyAssertEqual(configureParams["auto_connect"] as? Bool, false)
+ legacyAssertNotNil(configureParams["foreground_auth_token"] as? String)
+ legacyAssertEqual(configureParams["preserve_after_terminal_exit"] as? Bool, true)
+ let persistentDaemonSlot = try legacyUnwrap(configureParams["persistent_daemon_slot"] as? String)
+ legacyAssertTrue(persistentDaemonSlot.hasPrefix("ssh-"), persistentDaemonSlot)
+ legacyAssertNotNil(UUID(uuidString: String(persistentDaemonSlot.dropFirst(4))))
}
@Test
@@ -3656,28 +3656,28 @@ final class CLINotifyProcessIntegrationRegressionTests {
for testCase in cases {
let run = try runMockedSSH(arguments: testCase.arguments)
- let createParams = try XCTUnwrap(
+ let createParams = try legacyUnwrap(
params(for: "workspace.create", in: run.requests),
testCase.name
)
- let configureParams = try XCTUnwrap(
+ let configureParams = try legacyUnwrap(
params(for: "workspace.remote.configure", in: run.requests),
testCase.name
)
- let initialCommand = try XCTUnwrap(createParams["initial_command"] as? String, testCase.name)
- let terminalStartupCommand = try XCTUnwrap(
+ let initialCommand = try legacyUnwrap(createParams["initial_command"] as? String, testCase.name)
+ let terminalStartupCommand = try legacyUnwrap(
configureParams["terminal_startup_command"] as? String,
testCase.name
)
let initialScript = decodedReusableStartupScript(from: initialCommand) ?? initialCommand
let terminalStartupScript = decodedReusableStartupScript(from: terminalStartupCommand) ?? terminalStartupCommand
- XCTAssertFalse(initialScript.contains("ssh-pty-attach"), testCase.name)
- XCTAssertFalse(terminalStartupScript.contains("ssh-pty-attach"), testCase.name)
- XCTAssertEqual(configureParams["auto_connect"] as? Bool, true, testCase.name)
- XCTAssertNil(configureParams["foreground_auth_token"], testCase.name)
- XCTAssertNil(configureParams["preserve_after_terminal_exit"], testCase.name)
- XCTAssertNil(configureParams["persistent_daemon_slot"], testCase.name)
+ legacyAssertFalse(initialScript.contains("ssh-pty-attach"), testCase.name)
+ legacyAssertFalse(terminalStartupScript.contains("ssh-pty-attach"), testCase.name)
+ legacyAssertEqual(configureParams["auto_connect"] as? Bool, true, testCase.name)
+ legacyAssertNil(configureParams["foreground_auth_token"], testCase.name)
+ legacyAssertNil(configureParams["preserve_after_terminal_exit"], testCase.name)
+ legacyAssertNil(configureParams["persistent_daemon_slot"], testCase.name)
}
}
@@ -3708,9 +3708,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
let params = payload["params"] as? [String: Any] ?? [:]
switch method {
case "workspace.remote.pty_bridge":
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["session_id"] as? String, sessionId)
- XCTAssertEqual(params["attachment_id"] as? String, surfaceId)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["session_id"] as? String, sessionId)
+ legacyAssertEqual(params["attachment_id"] as? String, surfaceId)
return self.v2Response(
id: id,
ok: true,
@@ -3723,9 +3723,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "workspace.remote.pty_attach_end":
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["surface_id"] as? String, surfaceId)
- XCTAssertEqual(params["session_id"] as? String, sessionId)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(params["session_id"] as? String, sessionId)
return self.v2Response(
id: id,
ok: true,
@@ -3765,13 +3765,13 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [socketHandled, bridgeHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stdout.isEmpty, result.stdout)
- XCTAssertTrue(result.stderr.contains("ssh-pty-attach: remote PTY start failed"), result.stderr)
+ legacyWait(for: [socketHandled, bridgeHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stdout.isEmpty, result.stdout)
+ legacyAssertTrue(result.stderr.contains("ssh-pty-attach: remote PTY start failed"), result.stderr)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
- XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_attach_end"])
+ legacyAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_attach_end"])
}
@Test
@@ -3801,7 +3801,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
let params = payload["params"] as? [String: Any] ?? [:]
switch method {
case "workspace.remote.pty_bridge":
- XCTAssertEqual(params["require_existing"] as? Bool, true)
+ legacyAssertEqual(params["require_existing"] as? Bool, true)
return self.v2Response(
id: id,
ok: true,
@@ -3814,8 +3814,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "workspace.remote.pty_resize":
- XCTAssertEqual(params["attachment_token"] as? String, "attach-token")
- XCTAssertEqual(params["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(params["attachment_token"] as? String, "attach-token")
+ legacyAssertEqual(params["surface_id"] as? String, surfaceId)
return self.v2Response(id: id, ok: true, result: ["resized": true])
case "workspace.remote.pty_sessions":
return self.v2Response(
@@ -3831,11 +3831,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "workspace.remote.pty_detach":
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["surface_id"] as? String, surfaceId)
- XCTAssertEqual(params["session_id"] as? String, sessionId)
- XCTAssertEqual(params["attachment_id"] as? String, surfaceId)
- XCTAssertEqual(params["attachment_token"] as? String, "attach-token")
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(params["session_id"] as? String, sessionId)
+ legacyAssertEqual(params["attachment_id"] as? String, surfaceId)
+ legacyAssertEqual(params["attachment_token"] as? String, "attach-token")
return self.v2Response(
id: id,
ok: true,
@@ -3873,15 +3873,15 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [socketHandled, bridgeHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 254, result.stderr)
- XCTAssertTrue(
+ legacyWait(for: [socketHandled, bridgeHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 254, result.stderr)
+ legacyAssertTrue(
result.stderr.contains("ssh-pty-attach: bridge closed while remote PTY session is still running"),
result.stderr
)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
- XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions", "workspace.remote.pty_detach"])
+ legacyAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions", "workspace.remote.pty_detach"])
}
@Test
@@ -3923,8 +3923,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
)
case "workspace.remote.pty_resize":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["attachment_token"] as? String, "attach-token")
- XCTAssertEqual(params["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(params["attachment_token"] as? String, "attach-token")
+ legacyAssertEqual(params["surface_id"] as? String, surfaceId)
return self.v2Response(id: id, ok: true, result: ["resized": true])
case "workspace.remote.pty_sessions":
return self.v2Response(
@@ -3936,9 +3936,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
)
case "workspace.remote.pty_attach_end":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["surface_id"] as? String, surfaceId)
- XCTAssertEqual(params["session_id"] as? String, sessionId)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(params["session_id"] as? String, sessionId)
return self.v2Response(
id: id,
ok: true,
@@ -3975,13 +3975,13 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [socketHandled, bridgeHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stdout.isEmpty, result.stdout)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyWait(for: [socketHandled, bridgeHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stdout.isEmpty, result.stdout)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
- XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions", "workspace.remote.pty_attach_end"])
+ legacyAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions", "workspace.remote.pty_attach_end"])
}
@Test
@@ -4010,11 +4010,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
let params = payload["params"] as? [String: Any] ?? [:]
switch method {
case "workspace.remote.pty_bridge":
- XCTAssertNil(params["surface_id"])
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["session_id"] as? String, sessionId)
+ legacyAssertNil(params["surface_id"])
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["session_id"] as? String, sessionId)
let attachmentID = params["attachment_id"] as? String
- XCTAssertNotNil(attachmentID.flatMap { UUID(uuidString: $0) })
+ legacyAssertNotNil(attachmentID.flatMap { UUID(uuidString: $0) })
return self.v2Response(
id: id,
ok: true,
@@ -4027,11 +4027,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "workspace.remote.pty_resize":
- XCTAssertEqual(params["attachment_token"] as? String, "attach-token")
- XCTAssertNil(params["surface_id"])
+ legacyAssertEqual(params["attachment_token"] as? String, "attach-token")
+ legacyAssertNil(params["surface_id"])
return self.v2Response(id: id, ok: true, result: ["resized": true])
case "workspace.remote.pty_sessions":
- XCTAssertNil(params["surface_id"])
+ legacyAssertNil(params["surface_id"])
return self.v2Response(
id: id,
ok: true,
@@ -4065,13 +4065,13 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [socketHandled, bridgeHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stdout.isEmpty, result.stdout)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyWait(for: [socketHandled, bridgeHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stdout.isEmpty, result.stdout)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
- XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions"])
+ legacyAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions"])
}
@Test
@@ -4113,8 +4113,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
)
case "workspace.remote.pty_resize":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["attachment_token"] as? String, "attach-token")
- XCTAssertEqual(params["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(params["attachment_token"] as? String, "attach-token")
+ legacyAssertEqual(params["surface_id"] as? String, surfaceId)
return self.v2Response(id: id, ok: true, result: ["resized": true])
case "workspace.remote.pty_sessions":
return self.v2Response(
@@ -4126,9 +4126,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
)
case "workspace.remote.pty_attach_end":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["surface_id"] as? String, surfaceId)
- XCTAssertEqual(params["session_id"] as? String, sessionId)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(params["session_id"] as? String, sessionId)
return self.v2Response(
id: id,
ok: true,
@@ -4165,13 +4165,13 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [socketHandled, bridgeHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stdout.isEmpty, result.stdout)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyWait(for: [socketHandled, bridgeHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stdout.isEmpty, result.stdout)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
- XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions", "workspace.remote.pty_attach_end"])
+ legacyAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions", "workspace.remote.pty_attach_end"])
}
@Test
@@ -4246,9 +4246,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
)
case "workspace.remote.pty_resize":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["attachment_token"] as? String, "attach-token")
- XCTAssertEqual(params["cols"] as? Int, 132)
- XCTAssertEqual(params["rows"] as? Int, 43)
+ legacyAssertEqual(params["attachment_token"] as? String, "attach-token")
+ legacyAssertEqual(params["cols"] as? Int, 132)
+ legacyAssertEqual(params["rows"] as? Int, 43)
return self.v2Response(id: id, ok: true, result: ["resized": true])
case "workspace.remote.pty_sessions":
return self.v2Response(id: id, ok: true, result: ["sessions": []])
@@ -4272,7 +4272,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
}
- let bridgeHandled = expectation(description: "bridge handshake captured")
+ let bridgeHandled = legacyExpectation(description: "bridge handshake captured")
DispatchQueue.global(qos: .userInitiated).async {
defer { bridgeHandled.fulfill() }
var clientAddr = sockaddr_in()
@@ -4333,27 +4333,27 @@ final class CLINotifyProcessIntegrationRegressionTests {
try process.run()
slaveHandle.closeFile()
- XCTAssertEqual(bridgeRequestReceived.wait(timeout: .now() + 5), .success)
+ legacyAssertEqual(bridgeRequestReceived.wait(timeout: .now() + 5), .success)
try setPTYSize(cols: 132, rows: 43)
allowBridgeResponse.signal()
- XCTAssertEqual(handshakeReceived.wait(timeout: .now() + 5), .success)
+ legacyAssertEqual(handshakeReceived.wait(timeout: .now() + 5), .success)
let exited = DispatchSemaphore(value: 0)
DispatchQueue.global(qos: .userInitiated).async {
process.waitUntilExit()
exited.signal()
}
- XCTAssertEqual(exited.wait(timeout: .now() + 5), .success)
- wait(for: [socketHandled, bridgeHandled], timeout: 5)
+ legacyAssertEqual(exited.wait(timeout: .now() + 5), .success)
+ legacyWait(for: [socketHandled, bridgeHandled], timeout: 5)
let stderr = String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
- XCTAssertEqual(process.terminationStatus, 0, stderr)
+ legacyAssertEqual(process.terminationStatus, 0, stderr)
handshakeLock.lock()
let capturedHandshake = handshakePayload
handshakeLock.unlock()
- XCTAssertEqual(capturedHandshake?["token"] as? String, token)
- XCTAssertEqual(capturedHandshake?["cols"] as? Int, 132)
- XCTAssertEqual(capturedHandshake?["rows"] as? Int, 43)
+ legacyAssertEqual(capturedHandshake?["token"] as? String, token)
+ legacyAssertEqual(capturedHandshake?["cols"] as? Int, 132)
+ legacyAssertEqual(capturedHandshake?["rows"] as? Int, 43)
}
@Test
@@ -4436,7 +4436,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
let socketHandled = (0..<2).map { _ in startMockServer(listenerFD: listenerFD, state: state, handler: socketHandler) }
- let bridgeHandled = expectation(description: "controlled bridge handled")
+ let bridgeHandled = legacyExpectation(description: "controlled bridge handled")
DispatchQueue.global(qos: .userInitiated).async {
defer { bridgeHandled.fulfill() }
var clientAddr = sockaddr_in()
@@ -4493,16 +4493,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
process.terminate()
}
}
- XCTAssertEqual(bridgeReady.wait(timeout: .now() + 5), .success)
+ legacyAssertEqual(bridgeReady.wait(timeout: .now() + 5), .success)
- XCTAssertEqual(
+ legacyAssertEqual(
resizeRequestReceived.wait(timeout: .now() + 5),
.success,
"Expected ssh-pty-attach to issue its initial resize RPC after bridge ready"
)
closeBridge.signal()
- wait(for: [bridgeHandled], timeout: 5)
+ legacyWait(for: [bridgeHandled], timeout: 5)
allowResizeResponse.signal()
let exited = DispatchSemaphore(value: 0)
@@ -4510,19 +4510,19 @@ final class CLINotifyProcessIntegrationRegressionTests {
process.waitUntilExit()
exited.signal()
}
- XCTAssertEqual(exited.wait(timeout: .now() + 5), .success)
+ legacyAssertEqual(exited.wait(timeout: .now() + 5), .success)
- wait(for: socketHandled, timeout: 5)
+ legacyWait(for: socketHandled, timeout: 5)
let stdout = String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let stderr = String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
- XCTAssertEqual(process.terminationStatus, 0, stderr)
- XCTAssertEqual(stdout, "")
- XCTAssertEqual(stderr, "")
+ legacyAssertEqual(process.terminationStatus, 0, stderr)
+ legacyAssertEqual(stdout, "")
+ legacyAssertEqual(stderr, "")
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
- XCTAssertEqual(methods.filter { $0 == "workspace.remote.pty_bridge" }.count, 1)
- XCTAssertEqual(methods.filter { $0 == "workspace.remote.pty_resize" }.count, 1)
- XCTAssertEqual(methods.filter { $0 == "workspace.remote.pty_sessions" }.count, 1)
- XCTAssertEqual(methods.filter { $0 == "workspace.remote.pty_attach_end" }.count, 1)
+ legacyAssertEqual(methods.filter { $0 == "workspace.remote.pty_bridge" }.count, 1)
+ legacyAssertEqual(methods.filter { $0 == "workspace.remote.pty_resize" }.count, 1)
+ legacyAssertEqual(methods.filter { $0 == "workspace.remote.pty_sessions" }.count, 1)
+ legacyAssertEqual(methods.filter { $0 == "workspace.remote.pty_attach_end" }.count, 1)
}
@Test
@@ -4546,19 +4546,19 @@ final class CLINotifyProcessIntegrationRegressionTests {
let method = payload["method"] as? String else {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "surface.create")
+ legacyAssertEqual(method, "surface.create")
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["remote_pty_session_id"] as? String, sessionId)
- XCTAssertEqual(params["focus"] as? Bool, true)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["remote_pty_session_id"] as? String, sessionId)
+ legacyAssertEqual(params["focus"] as? Bool, true)
let initialCommand = params["initial_command"] as? String ?? ""
- XCTAssertTrue(initialCommand.hasPrefix("/bin/sh -c "), initialCommand)
- XCTAssertTrue(initialCommand.contains("ssh-pty-attach"), initialCommand)
- XCTAssertTrue(initialCommand.contains("--require-existing"), initialCommand)
- XCTAssertTrue(initialCommand.contains(sessionId), initialCommand)
- XCTAssertTrue(initialCommand.contains("CMUX_WORKSPACE_ID"), initialCommand)
- XCTAssertTrue(initialCommand.contains("CMUX_SURFACE_ID"), initialCommand)
- XCTAssertTrue(initialCommand.contains("254|255"), initialCommand)
+ legacyAssertTrue(initialCommand.hasPrefix("/bin/sh -c "), initialCommand)
+ legacyAssertTrue(initialCommand.contains("ssh-pty-attach"), initialCommand)
+ legacyAssertTrue(initialCommand.contains("--require-existing"), initialCommand)
+ legacyAssertTrue(initialCommand.contains(sessionId), initialCommand)
+ legacyAssertTrue(initialCommand.contains("CMUX_WORKSPACE_ID"), initialCommand)
+ legacyAssertTrue(initialCommand.contains("CMUX_SURFACE_ID"), initialCommand)
+ legacyAssertTrue(initialCommand.contains("254|255"), initialCommand)
return self.v2Response(
id: id,
ok: true,
@@ -4589,11 +4589,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
- XCTAssertEqual(state.snapshot().count, 1)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyAssertEqual(state.snapshot().count, 1)
}
@Test
@@ -4623,10 +4623,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
let params = payload["params"] as? [String: Any] ?? [:]
switch method {
case "workspace.remote.pty_bridge":
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["session_id"] as? String, sessionId)
- XCTAssertEqual(params["attachment_id"] as? String, surfaceId)
- XCTAssertEqual(params["require_existing"] as? Bool, true)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["session_id"] as? String, sessionId)
+ legacyAssertEqual(params["attachment_id"] as? String, surfaceId)
+ legacyAssertEqual(params["require_existing"] as? Bool, true)
return self.v2Response(
id: id,
ok: true,
@@ -4639,9 +4639,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "workspace.remote.pty_attach_end":
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["surface_id"] as? String, surfaceId)
- XCTAssertEqual(params["session_id"] as? String, sessionId)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(params["session_id"] as? String, sessionId)
return self.v2Response(
id: id,
ok: true,
@@ -4680,12 +4680,12 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [socketHandled, bridgeHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stderr.contains("ssh-pty-attach: missing session"), result.stderr)
+ legacyWait(for: [socketHandled, bridgeHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stderr.contains("ssh-pty-attach: missing session"), result.stderr)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
- XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_attach_end"])
+ legacyAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_attach_end"])
}
@Test
@@ -4712,10 +4712,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
let params = payload["params"] as? [String: Any] ?? [:]
switch method {
case "workspace.remote.pty_bridge":
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["session_id"] as? String, sessionId)
- XCTAssertEqual(params["attachment_id"] as? String, surfaceId)
- XCTAssertEqual(params["require_existing"] as? Bool, true)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["session_id"] as? String, sessionId)
+ legacyAssertEqual(params["attachment_id"] as? String, surfaceId)
+ legacyAssertEqual(params["require_existing"] as? Bool, true)
return self.v2Response(
id: id,
ok: false,
@@ -4725,9 +4725,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "workspace.remote.pty_attach_end":
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["surface_id"] as? String, surfaceId)
- XCTAssertEqual(params["session_id"] as? String, sessionId)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(params["session_id"] as? String, sessionId)
return self.v2Response(
id: id,
ok: true,
@@ -4765,12 +4765,12 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 3
)
- wait(for: [socketHandled], timeout: 3)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stderr.contains("persistent SSH PTY session is no longer running"), result.stderr)
+ legacyWait(for: [socketHandled], timeout: 3)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stderr.contains("persistent SSH PTY session is no longer running"), result.stderr)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
- XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_attach_end"])
+ legacyAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_attach_end"])
}
@Test
@@ -4792,9 +4792,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
let method = payload["method"] as? String else {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "workspace.remote.pty_sessions")
+ legacyAssertEqual(method, "workspace.remote.pty_sessions")
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["all_workspaces"] as? Bool, true)
+ legacyAssertEqual(params["all_workspaces"] as? Bool, true)
return self.v2Response(
id: id,
ok: true,
@@ -4825,13 +4825,13 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertFalse(result.stdout.contains("No persisted SSH PTY sessions"), result.stdout)
- XCTAssertTrue(result.stderr.contains("ssh-session-list failed for 1 remote workspace"), result.stderr)
- XCTAssertTrue(result.stderr.contains("workspace:4"), result.stderr)
- XCTAssertTrue(result.stderr.contains("remote connection is not active"), result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertFalse(result.stdout.contains("No persisted SSH PTY sessions"), result.stdout)
+ legacyAssertTrue(result.stderr.contains("ssh-session-list failed for 1 remote workspace"), result.stderr)
+ legacyAssertTrue(result.stderr.contains("workspace:4"), result.stderr)
+ legacyAssertTrue(result.stderr.contains("remote connection is not active"), result.stderr)
}
@Test
@@ -4858,7 +4858,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
let params = payload["params"] as? [String: Any] ?? [:]
switch method {
case "workspace.remote.pty_sessions":
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
return self.v2Response(
id: id,
ok: true,
@@ -4880,12 +4880,12 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "workspace.remote.pty_close":
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
let sessionId = params["session_id"] as? String
if sessionId == closedSessionId {
return self.v2Response(id: id, ok: true, result: ["closed": true])
}
- XCTAssertEqual(sessionId, failedSessionId)
+ legacyAssertEqual(sessionId, failedSessionId)
return self.v2Response(
id: id,
ok: false,
@@ -4915,15 +4915,15 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stdout.contains("Closed 1 persisted SSH PTY session"), result.stdout)
- XCTAssertTrue(result.stderr.contains("ssh-session-cleanup failed for 2 persisted SSH PTY sessions"), result.stderr)
- XCTAssertTrue(result.stderr.contains(failedSessionId), result.stderr)
- XCTAssertTrue(result.stderr.contains("missing session_id in SSH PTY session list response"), result.stderr)
- XCTAssertTrue(result.stderr.contains("remote PTY operation failed"), result.stderr)
- XCTAssertFalse(result.stderr.contains("close failed"), result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stdout.contains("Closed 1 persisted SSH PTY session"), result.stdout)
+ legacyAssertTrue(result.stderr.contains("ssh-session-cleanup failed for 2 persisted SSH PTY sessions"), result.stderr)
+ legacyAssertTrue(result.stderr.contains(failedSessionId), result.stderr)
+ legacyAssertTrue(result.stderr.contains("missing session_id in SSH PTY session list response"), result.stderr)
+ legacyAssertTrue(result.stderr.contains("remote PTY operation failed"), result.stderr)
+ legacyAssertFalse(result.stderr.contains("close failed"), result.stderr)
}
@Test
@@ -4949,7 +4949,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
let params = payload["params"] as? [String: Any] ?? [:]
switch method {
case "workspace.remote.pty_sessions":
- XCTAssertEqual(params["all_workspaces"] as? Bool, true)
+ legacyAssertEqual(params["all_workspaces"] as? Bool, true)
return self.v2Response(
id: id,
ok: true,
@@ -4969,8 +4969,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "workspace.remote.pty_close":
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["session_id"] as? String, closedSessionId)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["session_id"] as? String, closedSessionId)
return self.v2Response(id: id, ok: true, result: ["closed": true])
default:
return self.v2Response(
@@ -4996,14 +4996,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stdout.contains("Closed 1 persisted SSH PTY session"), result.stdout)
- XCTAssertTrue(result.stderr.contains("ssh-session-cleanup failed for 1 persisted SSH PTY session"), result.stderr)
- XCTAssertTrue(result.stderr.contains("workspace-query"), result.stderr)
- XCTAssertTrue(result.stderr.contains("workspace:4"), result.stderr)
- XCTAssertTrue(result.stderr.contains("remote connection is not active"), result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stdout.contains("Closed 1 persisted SSH PTY session"), result.stdout)
+ legacyAssertTrue(result.stderr.contains("ssh-session-cleanup failed for 1 persisted SSH PTY session"), result.stderr)
+ legacyAssertTrue(result.stderr.contains("workspace-query"), result.stderr)
+ legacyAssertTrue(result.stderr.contains("workspace:4"), result.stderr)
+ legacyAssertTrue(result.stderr.contains("remote connection is not active"), result.stderr)
}
@Test
@@ -5028,7 +5028,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
let params = payload["params"] as? [String: Any] ?? [:]
switch method {
case "workspace.remote.pty_sessions":
- XCTAssertEqual(params["all_workspaces"] as? Bool, true)
+ legacyAssertEqual(params["all_workspaces"] as? Bool, true)
return self.v2Response(
id: id,
ok: true,
@@ -5071,14 +5071,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertFalse(state.snapshot().contains { $0.contains("workspace.remote.pty_close") })
- XCTAssertTrue(result.stderr.contains("ssh-session-cleanup failed for 1 persisted SSH PTY session"), result.stderr)
- XCTAssertTrue(result.stderr.contains(sessionId), result.stderr)
- XCTAssertTrue(result.stderr.contains("workspace:missing"), result.stderr)
- XCTAssertTrue(result.stderr.contains("missing workspace_id in SSH PTY session list response"), result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertFalse(state.snapshot().contains { $0.contains("workspace.remote.pty_close") })
+ legacyAssertTrue(result.stderr.contains("ssh-session-cleanup failed for 1 persisted SSH PTY session"), result.stderr)
+ legacyAssertTrue(result.stderr.contains(sessionId), result.stderr)
+ legacyAssertTrue(result.stderr.contains("workspace:missing"), result.stderr)
+ legacyAssertTrue(result.stderr.contains("missing workspace_id in SSH PTY session list response"), result.stderr)
}
@Test
@@ -5103,7 +5103,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
let params = payload["params"] as? [String: Any] ?? [:]
switch method {
case "workspace.remote.pty_sessions":
- XCTAssertEqual(params["all_workspaces"] as? Bool, true)
+ legacyAssertEqual(params["all_workspaces"] as? Bool, true)
return self.v2Response(
id: id,
ok: true,
@@ -5146,14 +5146,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertFalse(state.snapshot().contains { $0.contains("workspace.remote.pty_close") })
- XCTAssertTrue(result.stderr.contains("ssh-session-cleanup failed for 1 persisted SSH PTY session"), result.stderr)
- XCTAssertTrue(result.stderr.contains(sessionId), result.stderr)
- XCTAssertTrue(result.stderr.contains("workspace:missing"), result.stderr)
- XCTAssertTrue(result.stderr.contains("missing workspace_id in SSH PTY session list response"), result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertFalse(state.snapshot().contains { $0.contains("workspace.remote.pty_close") })
+ legacyAssertTrue(result.stderr.contains("ssh-session-cleanup failed for 1 persisted SSH PTY session"), result.stderr)
+ legacyAssertTrue(result.stderr.contains(sessionId), result.stderr)
+ legacyAssertTrue(result.stderr.contains("workspace:missing"), result.stderr)
+ legacyAssertTrue(result.stderr.contains("missing workspace_id in SSH PTY session list response"), result.stderr)
}
@Test
@@ -5214,13 +5214,13 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertFalse(state.snapshot().contains { $0.contains("workspace.remote.pty_close") })
- XCTAssertTrue(result.stderr.contains("ssh-session-cleanup failed for 1 persisted SSH PTY session"), result.stderr)
- XCTAssertTrue(result.stderr.contains(sessionId), result.stderr)
- XCTAssertTrue(result.stderr.contains("persistent SSH PTY session is no longer running"), result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertFalse(state.snapshot().contains { $0.contains("workspace.remote.pty_close") })
+ legacyAssertTrue(result.stderr.contains("ssh-session-cleanup failed for 1 persisted SSH PTY session"), result.stderr)
+ legacyAssertTrue(result.stderr.contains(sessionId), result.stderr)
+ legacyAssertTrue(result.stderr.contains("persistent SSH PTY session is no longer running"), result.stderr)
}
@Test
@@ -5247,7 +5247,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
let params = payload["params"] as? [String: Any] ?? [:]
switch method {
case "workspace.remote.pty_sessions":
- XCTAssertEqual(params["all_workspaces"] as? Bool, true)
+ legacyAssertEqual(params["all_workspaces"] as? Bool, true)
return self.v2Response(
id: id,
ok: true,
@@ -5265,8 +5265,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "workspace.remote.pty_close":
- XCTAssertEqual(params["session_id"] as? String, sessionId)
- XCTAssertTrue([workspaceA, workspaceB].contains(params["workspace_id"] as? String))
+ legacyAssertEqual(params["session_id"] as? String, sessionId)
+ legacyAssertTrue([workspaceA, workspaceB].contains(params["workspace_id"] as? String))
return self.v2Response(id: id, ok: true, result: ["closed": true])
default:
return self.v2Response(
@@ -5292,10 +5292,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stdout.contains("Closed 2 persisted SSH PTY sessions"), result.stdout)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stdout.contains("Closed 2 persisted SSH PTY sessions"), result.stdout)
let closedWorkspaces = state.snapshot().compactMap { line -> String? in
guard let payload = self.jsonObject(line),
@@ -5306,8 +5306,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return params["workspace_id"] as? String
}
- XCTAssertEqual(closedWorkspaces.count, 2)
- XCTAssertEqual(Set(closedWorkspaces), Set([workspaceA, workspaceB]))
+ legacyAssertEqual(closedWorkspaces.count, 2)
+ legacyAssertEqual(Set(closedWorkspaces), Set([workspaceA, workspaceB]))
}
@Test
@@ -5342,7 +5342,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
)
case "workspace.list":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(
id: id,
ok: true,
@@ -5362,7 +5362,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
}
- XCTAssertEqual(line, "right_sidebar set find --tab=\(workspaceId) --window=\(windowId)")
+ legacyAssertEqual(line, "right_sidebar set find --tab=\(workspaceId) --window=\(windowId)")
return "OK"
}
@@ -5377,16 +5377,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stdout.isEmpty, result.stdout)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stdout.isEmpty, result.stdout)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["window.list", "workspace.list"]
)
- XCTAssertEqual(state.commands.last, "right_sidebar set find --tab=\(workspaceId) --window=\(windowId)")
+ legacyAssertEqual(state.commands.last, "right_sidebar set find --tab=\(workspaceId) --window=\(windowId)")
}
@Test
@@ -5407,7 +5407,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
let method = payload["method"] as? String else {
return "ERROR: Unexpected command \(line)"
}
- XCTAssertEqual(method, "workspace.list")
+ legacyAssertEqual(method, "workspace.list")
return self.v2Response(
id: id,
ok: true,
@@ -5430,16 +5430,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stdout.isEmpty, result.stdout)
- XCTAssertTrue(result.stderr.contains("Workspace ref not found"), result.stderr)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stdout.isEmpty, result.stdout)
+ legacyAssertTrue(result.stderr.contains("Workspace ref not found"), result.stderr)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["workspace.list"]
)
- XCTAssertFalse(
+ legacyAssertFalse(
state.commands.contains { $0.hasPrefix("right_sidebar ") },
"Expected no right_sidebar command after target resolution failed, saw \(state.commands)"
)
@@ -5474,10 +5474,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertNil(params["workspace_id"], "surface UUIDs should not be constrained to the caller workspace")
- XCTAssertNil(params["window_id"], "surface UUIDs should not require an explicit window")
- XCTAssertEqual(params["surface_id"] as? String, callerSurface)
- XCTAssertEqual(params["body"] as? String, "Body")
+ legacyAssertNil(params["workspace_id"], "surface UUIDs should not be constrained to the caller workspace")
+ legacyAssertNil(params["window_id"], "surface UUIDs should not require an explicit window")
+ legacyAssertEqual(params["surface_id"] as? String, callerSurface)
+ legacyAssertEqual(params["body"] as? String, "Body")
return self.v2Response(
id: id,
ok: true,
@@ -5502,12 +5502,12 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
- XCTAssertTrue(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyAssertTrue(
state.commands.contains { $0.contains("\"method\":\"notification.create\"") },
"Expected notify to use single-call UUID notification path, saw \(state.commands)"
)
@@ -5548,7 +5548,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
environment: environment,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
@@ -5558,14 +5558,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return "0:\(notificationId)|\(workspaceId)|\(surfaceId)|unread|List Fields|cli-test|body|2026-01-01T00:00:00Z|pct:CLI%7CNotification Workspace"
}
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
var rows = try notificationRows(from: result.stdout)
- var row = try XCTUnwrap(rows.first(where: { $0["id"] as? String == notificationId }))
- XCTAssertEqual(row["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(row["surface_id"] as? String, surfaceId)
- XCTAssertEqual(row["created_at"] as? String, "2026-01-01T00:00:00Z")
- XCTAssertEqual(row["tab_title"] as? String, "CLI|Notification Workspace")
+ var row = try legacyUnwrap(rows.first(where: { $0["id"] as? String == notificationId }))
+ legacyAssertEqual(row["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(row["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(row["created_at"] as? String, "2026-01-01T00:00:00Z")
+ legacyAssertEqual(row["tab_title"] as? String, "CLI|Notification Workspace")
result = run(["--json", "list-notifications", "--id-format", "uuids"]) { line in
guard line == "list_notifications" else {
@@ -5573,11 +5573,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return "0:\(notificationId)|\(workspaceId)|\(surfaceId)|unread|List Fields|cli-test|body|2026-01-01T00:00:00Z|pct:CLI%7CNotification Workspace"
}
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
rows = try notificationRows(from: result.stdout)
- row = try XCTUnwrap(rows.first(where: { $0["id"] as? String == notificationId }))
- XCTAssertEqual(row["created_at"] as? String, "2026-01-01T00:00:00Z")
+ row = try legacyUnwrap(rows.first(where: { $0["id"] as? String == notificationId }))
+ legacyAssertEqual(row["created_at"] as? String, "2026-01-01T00:00:00Z")
result = run(["mark-notification-read", "--id", notificationId, "--json", "--id-format", "uuids"]) { line in
guard let payload = self.jsonObject(line),
@@ -5585,16 +5585,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
let method = payload["method"] as? String else {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "notification.mark_read")
+ legacyAssertEqual(method, "notification.mark_read")
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["id"] as? String, notificationId)
+ legacyAssertEqual(params["id"] as? String, notificationId)
return self.v2Response(id: id, ok: true, result: ["marked_read": 1, "id": notificationId])
}
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let markByIdPayload = try jsonPayload(from: result.stdout)
- XCTAssertEqual(markByIdPayload["marked_read"] as? Int, 1)
- XCTAssertEqual(markByIdPayload["id"] as? String, notificationId)
+ legacyAssertEqual(markByIdPayload["marked_read"] as? Int, 1)
+ legacyAssertEqual(markByIdPayload["id"] as? String, notificationId)
result = run(["dismiss-notification", "--all-read", "--json", "--id-format", "uuids"]) { line in
guard let payload = self.jsonObject(line),
@@ -5602,16 +5602,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
let method = payload["method"] as? String else {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "notification.dismiss")
+ legacyAssertEqual(method, "notification.dismiss")
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["all_read"] as? Bool, true)
+ legacyAssertEqual(params["all_read"] as? Bool, true)
return self.v2Response(id: id, ok: true, result: ["dismissed": 1, "all_read": true])
}
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let dismissPayload = try jsonPayload(from: result.stdout)
- XCTAssertEqual(dismissPayload["dismissed"] as? Int, 1)
- XCTAssertEqual(dismissPayload["all_read"] as? Bool, true)
+ legacyAssertEqual(dismissPayload["dismissed"] as? Int, 1)
+ legacyAssertEqual(dismissPayload["all_read"] as? Bool, true)
result = run([
"mark-notification-read",
@@ -5626,21 +5626,21 @@ final class CLINotifyProcessIntegrationRegressionTests {
let method = payload["method"] as? String else {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "notification.mark_read")
+ legacyAssertEqual(method, "notification.mark_read")
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["tab_id"] as? String, workspaceId)
- XCTAssertEqual(params["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(params["tab_id"] as? String, workspaceId)
+ legacyAssertEqual(params["surface_id"] as? String, surfaceId)
return self.v2Response(
id: id,
ok: true,
result: ["marked_read": 1, "workspace_id": workspaceId, "surface_id": surfaceId]
)
}
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let markScopedPayload = try jsonPayload(from: result.stdout)
- XCTAssertEqual(markScopedPayload["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(markScopedPayload["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(markScopedPayload["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(markScopedPayload["surface_id"] as? String, surfaceId)
result = run(["open-notification", "--id", openNotificationId, "--json", "--id-format", "uuids"]) { line in
guard let payload = self.jsonObject(line),
@@ -5648,9 +5648,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
let method = payload["method"] as? String else {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "notification.open")
+ legacyAssertEqual(method, "notification.open")
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["id"] as? String, openNotificationId)
+ legacyAssertEqual(params["id"] as? String, openNotificationId)
return self.v2Response(
id: id,
ok: true,
@@ -5663,12 +5663,12 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
}
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let openPayload = try jsonPayload(from: result.stdout)
- XCTAssertEqual(openPayload["workspace_id"] as? String, openWorkspaceId)
- XCTAssertEqual(openPayload["surface_id"] as? String, openSurfaceId)
- XCTAssertEqual(openPayload["is_read"] as? Bool, true)
+ legacyAssertEqual(openPayload["workspace_id"] as? String, openWorkspaceId)
+ legacyAssertEqual(openPayload["surface_id"] as? String, openSurfaceId)
+ legacyAssertEqual(openPayload["is_read"] as? Bool, true)
result = run(["jump-to-unread", "--json", "--id-format", "uuids"]) { line in
guard let payload = self.jsonObject(line),
@@ -5676,9 +5676,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
let method = payload["method"] as? String else {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "notification.jump_to_unread")
+ legacyAssertEqual(method, "notification.jump_to_unread")
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertTrue(params.isEmpty, "jump-to-unread should not send selector params")
+ legacyAssertTrue(params.isEmpty, "jump-to-unread should not send selector params")
return self.v2Response(
id: id,
ok: true,
@@ -5691,13 +5691,13 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
}
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let jumpPayload = try jsonPayload(from: result.stdout)
- XCTAssertEqual(jumpPayload["id"] as? String, jumpNotificationId)
- XCTAssertEqual(jumpPayload["workspace_id"] as? String, openWorkspaceId)
- XCTAssertEqual(jumpPayload["surface_id"] as? String, openSurfaceId)
- XCTAssertEqual(jumpPayload["is_read"] as? Bool, true)
+ legacyAssertEqual(jumpPayload["id"] as? String, jumpNotificationId)
+ legacyAssertEqual(jumpPayload["workspace_id"] as? String, openWorkspaceId)
+ legacyAssertEqual(jumpPayload["surface_id"] as? String, openSurfaceId)
+ legacyAssertEqual(jumpPayload["is_read"] as? Bool, true)
let methods = state.snapshot().map { command -> String in
if command == "list_notifications" {
@@ -5705,7 +5705,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return self.jsonObject(command)?["method"] as? String ?? "invalid"
}
- XCTAssertEqual(
+ legacyAssertEqual(
methods,
[
"list_notifications",
@@ -5752,16 +5752,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let rows = try notificationRows(from: result.stdout)
- let row = try XCTUnwrap(rows.first)
- XCTAssertEqual(row["id"] as? String, notificationId)
- XCTAssertEqual(row["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(row["body"] as? String, "alpha|beta|gamma")
- XCTAssertTrue(row["created_at"] is NSNull)
- XCTAssertTrue(row["tab_title"] is NSNull)
+ let row = try legacyUnwrap(rows.first)
+ legacyAssertEqual(row["id"] as? String, notificationId)
+ legacyAssertEqual(row["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(row["body"] as? String, "alpha|beta|gamma")
+ legacyAssertTrue(row["created_at"] is NSNull)
+ legacyAssertTrue(row["tab_title"] is NSNull)
}
@Test
@@ -5858,17 +5858,17 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- let sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
- let session = try XCTUnwrap(sessions[sessionId] as? [String: Any])
- XCTAssertEqual(session["workspaceId"] as? String, currentWorkspaceId)
- XCTAssertEqual(session["surfaceId"] as? String, currentSurfaceId)
- XCTAssertTrue(
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ let sessions = try legacyUnwrap(json["sessions"] as? [String: Any])
+ let session = try legacyUnwrap(sessions[sessionId] as? [String: Any])
+ legacyAssertEqual(session["workspaceId"] as? String, currentWorkspaceId)
+ legacyAssertEqual(session["surfaceId"] as? String, currentSurfaceId)
+ legacyAssertTrue(
state.commands.contains { $0.contains("set_status codex Running") && $0.contains("--tab=\(currentWorkspaceId)") },
"Expected Codex prompt status to target current workspace, saw \(state.commands)"
)
@@ -5899,7 +5899,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
switch method {
case "workspace.list":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(
id: id,
ok: true,
@@ -5915,9 +5915,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
)
case "pane.create":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["direction"] as? String, "right")
+ legacyAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["direction"] as? String, "right")
return self.v2Response(
id: id,
ok: true,
@@ -5944,11 +5944,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["workspace.list", "pane.create"]
)
@@ -5980,7 +5980,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
let params = payload["params"] as? [String: Any] ?? [:]
switch method {
case "workspace.list":
- XCTAssertEqual(params["window_id"] as? String, targetWindowId)
+ legacyAssertEqual(params["window_id"] as? String, targetWindowId)
return self.v2Response(
id: id,
ok: true,
@@ -5995,8 +5995,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "pane.list":
- XCTAssertEqual(params["window_id"] as? String, targetWindowId)
- XCTAssertEqual(params["workspace_id"] as? String, targetWorkspaceId)
+ legacyAssertEqual(params["window_id"] as? String, targetWindowId)
+ legacyAssertEqual(params["workspace_id"] as? String, targetWorkspaceId)
return self.v2Response(
id: id,
ok: true,
@@ -6027,11 +6027,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertNotEqual(result.status, 0)
- XCTAssertTrue(result.stderr.contains("Pane not found in window"), result.stderr)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertNotEqual(result.status, 0)
+ legacyAssertTrue(result.stderr.contains("Pane not found in window"), result.stderr)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["workspace.list", "pane.list"]
)
@@ -6063,7 +6063,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
let params = payload["params"] as? [String: Any] ?? [:]
switch method {
case "workspace.list":
- XCTAssertEqual(params["window_id"] as? String, targetWindowId)
+ legacyAssertEqual(params["window_id"] as? String, targetWindowId)
return self.v2Response(
id: id,
ok: true,
@@ -6078,8 +6078,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "surface.list":
- XCTAssertEqual(params["window_id"] as? String, targetWindowId)
- XCTAssertEqual(params["workspace_id"] as? String, targetWorkspaceId)
+ legacyAssertEqual(params["window_id"] as? String, targetWindowId)
+ legacyAssertEqual(params["workspace_id"] as? String, targetWorkspaceId)
return self.v2Response(
id: id,
ok: true,
@@ -6110,11 +6110,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertNotEqual(result.status, 0)
- XCTAssertTrue(result.stderr.contains("Surface not found in window"), result.stderr)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertNotEqual(result.status, 0)
+ legacyAssertTrue(result.stderr.contains("Surface not found in window"), result.stderr)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["workspace.list", "surface.list"]
)
@@ -6172,11 +6172,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertNotEqual(result.status, 0)
- XCTAssertTrue(result.stderr.contains("Window not found: window:2"), result.stderr)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertNotEqual(result.status, 0)
+ legacyAssertTrue(result.stderr.contains("Window not found: window:2"), result.stderr)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["window.list"]
)
@@ -6220,11 +6220,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stderr.contains("Window not found: \(missingWindowId)"), result.stderr)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stderr.contains("Window not found: \(missingWindowId)"), result.stderr)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["window.list"]
)
@@ -6293,11 +6293,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stdout.contains("OK vm-test-case-window"), result.stdout)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stdout.contains("OK vm-test-case-window"), result.stdout)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["window.list", "vm.create"]
)
@@ -6342,7 +6342,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "system.identify":
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(
id: id,
ok: true,
@@ -6355,8 +6355,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "surface.read_text":
- XCTAssertEqual(params["window_id"] as? String, windowId)
- XCTAssertEqual(params["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["surface_id"] as? String, surfaceId)
return self.v2Response(id: id, ok: true, result: ["text": "hello\n"])
default:
return self.v2Response(id: id, ok: false, error: ["code": "unexpected", "message": "unexpected method: \(method)"])
@@ -6375,11 +6375,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "hello\nOK\n")
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "hello\nOK\n")
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["window.list", "system.identify", "surface.read_text"]
)
@@ -6424,7 +6424,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "workspace.list":
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(
id: id,
ok: true,
@@ -6451,9 +6451,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "surface.read_text":
- XCTAssertEqual(params["window_id"] as? String, windowId)
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertNil(params["surface_id"], line)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertNil(params["surface_id"], line)
return self.v2Response(id: id, ok: true, result: ["text": "workspace text\n"])
default:
return self.v2Response(id: id, ok: false, error: ["code": "unexpected", "message": "unexpected method: \(method)"])
@@ -6472,11 +6472,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "workspace text\nOK\n")
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "workspace text\nOK\n")
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["window.list", "workspace.list", "surface.read_text"]
)
@@ -6521,7 +6521,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "system.identify":
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(
id: id,
ok: true,
@@ -6534,9 +6534,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "surface.send_text":
- XCTAssertEqual(params["window_id"] as? String, windowId)
- XCTAssertEqual(params["surface_id"] as? String, surfaceId)
- XCTAssertEqual(params["text"] as? String, "echo fresh\n")
+ legacyAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(params["text"] as? String, "echo fresh\n")
return self.v2Response(id: id, ok: true, result: ["surface_id": surfaceId])
default:
return self.v2Response(id: id, ok: false, error: ["code": "unexpected", "message": "unexpected method: \(method)"])
@@ -6555,10 +6555,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["window.list", "system.identify", "surface.send_text"]
)
@@ -6589,8 +6589,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
switch method {
case "surface.list":
- XCTAssertNil(params["window_id"])
- XCTAssertNil(params["workspace_id"])
+ legacyAssertNil(params["window_id"])
+ legacyAssertNil(params["workspace_id"])
return self.v2Response(
id: id,
ok: true,
@@ -6605,9 +6605,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "surface.move":
- XCTAssertEqual(params["surface_id"] as? String, sourceSurfaceId)
- XCTAssertEqual(params["window_id"] as? String, targetWindowId)
- XCTAssertEqual(params["workspace_id"] as? String, targetWorkspaceId)
+ legacyAssertEqual(params["surface_id"] as? String, sourceSurfaceId)
+ legacyAssertEqual(params["window_id"] as? String, targetWindowId)
+ legacyAssertEqual(params["workspace_id"] as? String, targetWorkspaceId)
return self.v2Response(
id: id,
ok: true,
@@ -6634,11 +6634,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["surface.list", "surface.move"]
)
@@ -6669,8 +6669,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
guard method == "surface.move" else {
return self.v2Response(id: id, ok: false, error: ["code": "unexpected", "message": "unexpected method: \(method)"])
}
- XCTAssertEqual(params["surface_id"] as? String, sourceSurfaceRef)
- XCTAssertEqual(params["window_id"] as? String, targetWindowId)
+ legacyAssertEqual(params["surface_id"] as? String, sourceSurfaceRef)
+ legacyAssertEqual(params["window_id"] as? String, targetWindowId)
return self.v2Response(
id: id,
ok: true,
@@ -6693,11 +6693,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["surface.move"]
)
@@ -6725,13 +6725,13 @@ final class CLINotifyProcessIntegrationRegressionTests {
return self.v2Response(id: id, ok: false, error: ["code": "unexpected", "message": "unexpected method: \(method)"])
}
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(id: id, ok: true, result: ["workspace_id": workspaceId])
}
- XCTAssertTrue(line.hasPrefix("set_status build running"), line)
- XCTAssertTrue(line.contains("--tab=\(workspaceId)"), line)
- XCTAssertFalse(line.contains("--window"), line)
+ legacyAssertTrue(line.hasPrefix("set_status build running"), line)
+ legacyAssertTrue(line.contains("--tab=\(workspaceId)"), line)
+ legacyAssertFalse(line.contains("--window"), line)
return "OK"
}
@@ -6747,11 +6747,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
}
@Test
@@ -6776,11 +6776,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
return self.v2Response(id: id, ok: false, error: ["code": "unexpected", "message": "unexpected method: \(method)"])
}
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(id: id, ok: true, result: ["workspace_id": workspaceId])
}
- XCTAssertEqual(line, "log --tab=\(workspaceId) -- --window target")
+ legacyAssertEqual(line, "log --tab=\(workspaceId) -- --window target")
return "OK"
}
@@ -6796,11 +6796,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
}
@Test
@@ -6826,7 +6826,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
return self.v2Response(id: id, ok: false, error: ["code": "unexpected", "message": "unexpected method: \(method)"])
}
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(id: id, ok: true, result: [:])
}
@@ -6842,11 +6842,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stderr.contains("set-status: targeted window has no current workspace"), result.stderr)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stderr.contains("set-status: targeted window has no current workspace"), result.stderr)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["workspace.current"]
)
@@ -6877,13 +6877,13 @@ final class CLINotifyProcessIntegrationRegressionTests {
switch method {
case "workspace.current":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(id: id, ok: true, result: ["workspace_id": workspaceId])
case "notification.create":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["title"] as? String, "Window Notify")
+ legacyAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["title"] as? String, "Window Notify")
return self.v2Response(
id: id,
ok: true,
@@ -6906,11 +6906,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["workspace.current", "notification.create"]
)
@@ -6937,8 +6937,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
guard let payload = self.jsonObject(line),
let id = payload["id"] as? String,
let method = payload["method"] as? String else {
- XCTAssertTrue(line.hasPrefix("notify_target \(targetWorkspaceId) \(targetSurfaceId) "), line)
- XCTAssertTrue(line.contains("Window Surface Notify"), line)
+ legacyAssertTrue(line.hasPrefix("notify_target \(targetWorkspaceId) \(targetSurfaceId) "), line)
+ legacyAssertTrue(line.contains("Window Surface Notify"), line)
return "OK"
}
let params = payload["params"] as? [String: Any] ?? [:]
@@ -6959,7 +6959,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "workspace.list":
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(
id: id,
ok: true,
@@ -6979,7 +6979,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "surface.list":
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
switch params["workspace_id"] as? String {
case selectedWorkspaceId:
return self.v2Response(
@@ -7010,7 +7010,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
default:
- XCTFail("Unexpected surface.list params: \(params)")
+ legacyFail("Unexpected surface.list params: \(params)")
return self.v2Response(id: id, ok: false, error: ["code": "unexpected", "message": "unexpected workspace"])
}
default:
@@ -7030,12 +7030,12 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
let methods = state.commands.compactMap { self.jsonObject($0)?["method"] as? String }
- XCTAssertEqual(methods, ["window.list", "workspace.list", "surface.list", "surface.list"])
+ legacyAssertEqual(methods, ["window.list", "workspace.list", "surface.list", "surface.list"])
}
@Test
@@ -7057,8 +7057,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
guard let payload = self.jsonObject(line),
let id = payload["id"] as? String,
let method = payload["method"] as? String else {
- XCTAssertTrue(line.hasPrefix("notify_target \(selectedWorkspaceId) \(selectedSurfaceId) "), line)
- XCTAssertTrue(line.contains("Window Indexed Notify"), line)
+ legacyAssertTrue(line.hasPrefix("notify_target \(selectedWorkspaceId) \(selectedSurfaceId) "), line)
+ legacyAssertTrue(line.contains("Window Indexed Notify"), line)
return "OK"
}
let params = payload["params"] as? [String: Any] ?? [:]
@@ -7079,11 +7079,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "workspace.current":
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(id: id, ok: true, result: ["workspace_id": selectedWorkspaceId])
case "surface.list":
- XCTAssertEqual(params["window_id"] as? String, windowId)
- XCTAssertEqual(params["workspace_id"] as? String, selectedWorkspaceId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["workspace_id"] as? String, selectedWorkspaceId)
return self.v2Response(
id: id,
ok: true,
@@ -7114,12 +7114,12 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
let methods = state.commands.compactMap { self.jsonObject($0)?["method"] as? String }
- XCTAssertEqual(methods, ["window.list", "workspace.current", "surface.list"])
+ legacyAssertEqual(methods, ["window.list", "workspace.current", "surface.list"])
}
@Test
@@ -7146,13 +7146,13 @@ final class CLINotifyProcessIntegrationRegressionTests {
switch method {
case "workspace.current":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(id: id, ok: true, result: ["workspace_id": workspaceId])
case "workspace.action":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
- XCTAssertEqual(params["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(params["action"] as? String, "pin")
+ legacyAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(params["action"] as? String, "pin")
return self.v2Response(
id: id,
ok: true,
@@ -7175,11 +7175,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["workspace.current", "workspace.action"]
)
@@ -7208,7 +7208,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
return self.v2Response(id: id, ok: false, error: ["code": "unexpected", "message": "unexpected method: \(method)"])
}
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(id: id, ok: true, result: [:])
}
@@ -7224,11 +7224,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stderr.contains("clear-notifications: targeted window has no current workspace"), result.stderr)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stderr.contains("clear-notifications: targeted window has no current workspace"), result.stderr)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["workspace.current"]
)
@@ -7258,8 +7258,8 @@ final class CLINotifyProcessIntegrationRegressionTests {
return self.v2Response(id: id, ok: false, error: ["code": "unexpected", "message": "unexpected method: \(method)"])
}
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
- XCTAssertEqual(params["all_windows"] as? Bool, false)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["all_windows"] as? Bool, false)
return self.v2Response(
id: id,
ok: true,
@@ -7283,10 +7283,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
}
@Test
@@ -7316,14 +7316,14 @@ final class CLINotifyProcessIntegrationRegressionTests {
switch method {
case "system.tree":
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(
id: id,
ok: false,
error: ["code": "method_not_found", "message": "system.tree"]
)
case "system.identify":
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(
id: id,
ok: true,
@@ -7349,7 +7349,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "workspace.list":
- XCTAssertEqual(params["window_id"] as? String, "window:2")
+ legacyAssertEqual(params["window_id"] as? String, "window:2")
return self.v2Response(
id: id,
ok: true,
@@ -7362,7 +7362,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "pane.list":
- XCTAssertTrue([workspaceId, "workspace:1"].contains(params["workspace_id"] as? String))
+ legacyAssertTrue([workspaceId, "workspace:1"].contains(params["workspace_id"] as? String))
return self.v2Response(
id: id,
ok: true,
@@ -7373,7 +7373,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
]
)
case "surface.list":
- XCTAssertTrue([workspaceId, "workspace:1"].contains(params["workspace_id"] as? String))
+ legacyAssertTrue([workspaceId, "workspace:1"].contains(params["workspace_id"] as? String))
return self.v2Response(
id: id,
ok: true,
@@ -7408,16 +7408,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
let payload = try jsonPayload(from: result.stdout)
- let windows = try XCTUnwrap(payload["windows"] as? [[String: Any]])
- XCTAssertEqual(windows.count, 1, result.stdout)
- XCTAssertEqual(windows.first?["id"] as? String, windowId)
- XCTAssertFalse(result.stdout.contains(otherWindowId), result.stdout)
+ let windows = try legacyUnwrap(payload["windows"] as? [[String: Any]])
+ legacyAssertEqual(windows.count, 1, result.stdout)
+ legacyAssertEqual(windows.first?["id"] as? String, windowId)
+ legacyAssertFalse(result.stdout.contains(otherWindowId), result.stdout)
}
@Test
@@ -7474,11 +7474,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
- XCTAssertFalse(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
+ legacyAssertFalse(
state.commands.contains { $0.contains("set_status codex Running") || $0.contains("notify_target_async") },
"Foreign cmux env must not mutate the selected workspace, saw \(state.commands)"
)
@@ -7561,11 +7561,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
- XCTAssertFalse(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
+ legacyAssertFalse(
state.commands.contains { $0.contains("set_status codex Running") || $0.contains("notify_target_async") },
"Foreign cmux env must not reuse stale mapped sessions, saw \(state.commands)"
)
@@ -7629,11 +7629,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
- XCTAssertFalse(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
+ legacyAssertFalse(
state.commands.contains { $0.contains("set_status codex Running") || $0.contains("notify_target_async") },
"Invalid surface must not fall back to the focused surface, saw \(state.commands)"
)
@@ -7716,11 +7716,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
Darwin.close(listenerFD)
listenerClosed = true
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
- XCTAssertFalse(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
+ legacyAssertFalse(
state.commands.contains { $0.contains("set_status codex Running") || $0.contains("notify_target_async") },
"Invalid mapped workspace must not mutate the selected workspace, saw \(state.commands)"
)
@@ -7847,9 +7847,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let resumeBindingRequests = state.commands.compactMap { command -> [String: Any]? in
guard let payload = jsonObject(command),
@@ -7858,11 +7858,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- XCTAssertEqual(resumeBindingRequests.count, 1, state.commands.joined(separator: "\n"))
- let request = try XCTUnwrap(resumeBindingRequests.first)
- XCTAssertEqual(request["checkpoint_id"] as? String, sessionId)
- XCTAssertEqual(request["auto_resume"] as? Bool, true)
- XCTAssertEqual(
+ legacyAssertEqual(resumeBindingRequests.count, 1, state.commands.joined(separator: "\n"))
+ let request = try legacyUnwrap(resumeBindingRequests.first)
+ legacyAssertEqual(request["checkpoint_id"] as? String, sessionId)
+ legacyAssertEqual(request["auto_resume"] as? Bool, true)
+ legacyAssertEqual(
request["command"] as? String,
"{ cd -- '\(root.path)' 2>/dev/null || [ ! -d '\(root.path)' ]; } && '/usr/local/bin/cmux' 'codex-teams' 'resume' '\(sessionId)' '--model' 'gpt-5.4' '--sandbox' 'danger-full-access'"
)
@@ -7929,7 +7929,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
case "surface.resume.clear":
return self.v2Response(id: id, ok: true, result: ["cleared": true])
case "surface.resume.set":
- XCTFail("Non-resumable launcher should not publish a resume binding")
+ legacyFail("Non-resumable launcher should not publish a resume binding")
return self.v2Response(id: id, ok: true, result: ["ok": true])
case "feed.push":
return self.v2Response(id: id, ok: true, result: [:])
@@ -7957,9 +7957,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let clearRequests = state.commands.compactMap { command -> [String: Any]? in
guard let payload = jsonObject(command),
@@ -7968,11 +7968,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- let request = try XCTUnwrap(clearRequests.first)
- XCTAssertNil(request["workspace_id"])
- XCTAssertEqual(request["surface_id"] as? String, surfaceId)
- XCTAssertEqual(request["source"] as? String, "agent-hook")
- XCTAssertEqual(request["checkpoint_id"] as? String, sessionId)
+ let request = try legacyUnwrap(clearRequests.first)
+ legacyAssertNil(request["workspace_id"])
+ legacyAssertEqual(request["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(request["source"] as? String, "agent-hook")
+ legacyAssertEqual(request["checkpoint_id"] as? String, sessionId)
}
@Test
@@ -8042,9 +8042,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let clearRequests = state.commands.compactMap { command -> [String: Any]? in
guard let payload = jsonObject(command),
payload["method"] as? String == "surface.resume.clear" else {
@@ -8052,11 +8052,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- let request = try XCTUnwrap(clearRequests.first)
- XCTAssertNil(request["workspace_id"])
- XCTAssertEqual(request["surface_id"] as? String, surfaceId)
- XCTAssertEqual(request["checkpoint_id"] as? String, sessionId)
- XCTAssertEqual(request["source"] as? String, "agent-hook")
+ let request = try legacyUnwrap(clearRequests.first)
+ legacyAssertNil(request["workspace_id"])
+ legacyAssertEqual(request["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(request["checkpoint_id"] as? String, sessionId)
+ legacyAssertEqual(request["source"] as? String, "agent-hook")
}
@Test
@@ -8078,7 +8078,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
let method = payload["method"] as? String else {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "surface.resume.clear")
+ legacyAssertEqual(method, "surface.resume.clear")
return self.v2Response(id: id, ok: true, result: ["cleared": false])
}
@@ -8100,10 +8100,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
let clearRequests = state.commands.compactMap { command -> [String: Any]? in
guard let payload = jsonObject(command),
@@ -8112,11 +8112,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- let request = try XCTUnwrap(clearRequests.first)
- XCTAssertEqual(request["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(request["surface_id"] as? String, surfaceId)
- XCTAssertEqual(request["checkpoint_id"] as? String, "new-session")
- XCTAssertEqual(request["source"] as? String, "agent-hook")
+ let request = try legacyUnwrap(clearRequests.first)
+ legacyAssertEqual(request["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(request["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(request["checkpoint_id"] as? String, "new-session")
+ legacyAssertEqual(request["source"] as? String, "agent-hook")
}
@Test
@@ -8138,7 +8138,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
let method = payload["method"] as? String else {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "surface.resume.set")
+ legacyAssertEqual(method, "surface.resume.set")
return self.v2Response(id: id, ok: true, result: ["resume_binding": [:]])
}
@@ -8159,10 +8159,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
let setRequests = state.commands.compactMap { command -> [String: Any]? in
guard let payload = jsonObject(command),
@@ -8171,12 +8171,12 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- XCTAssertEqual(setRequests.count, 1)
- let request = try XCTUnwrap(setRequests.first)
- XCTAssertEqual(request["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(request["surface_id"] as? String, surfaceId)
- XCTAssertEqual(request["kind"] as? String, "tmux")
- XCTAssertEqual(request["command"] as? String, "tmux attach -t work")
+ legacyAssertEqual(setRequests.count, 1)
+ let request = try legacyUnwrap(setRequests.first)
+ legacyAssertEqual(request["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(request["surface_id"] as? String, surfaceId)
+ legacyAssertEqual(request["kind"] as? String, "tmux")
+ legacyAssertEqual(request["command"] as? String, "tmux attach -t work")
}
@Test
@@ -8198,7 +8198,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
let method = payload["method"] as? String else {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "surface.resume.set")
+ legacyAssertEqual(method, "surface.resume.set")
return self.v2Response(id: id, ok: true, result: ["resume_binding": [:]])
}
@@ -8223,10 +8223,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
let setRequests = state.commands.compactMap { command -> [String: Any]? in
guard let payload = jsonObject(command),
@@ -8235,12 +8235,12 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- let request = try XCTUnwrap(setRequests.first)
- XCTAssertEqual(request["workspace_id"] as? String, workspaceId)
- XCTAssertEqual(request["surface_id"] as? String, surfaceId)
- XCTAssertNil(request["name"])
- XCTAssertNil(request["kind"])
- XCTAssertEqual(
+ let request = try legacyUnwrap(setRequests.first)
+ legacyAssertEqual(request["workspace_id"] as? String, workspaceId)
+ legacyAssertEqual(request["surface_id"] as? String, surfaceId)
+ legacyAssertNil(request["name"])
+ legacyAssertNil(request["kind"])
+ legacyAssertEqual(
request["command"] as? String,
"'myapp' '--name' 'foo' '--kind' 'bar' '--cwd' '/tmp/ignored' '--surface' 'not-a-target'"
)
@@ -8265,7 +8265,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
let method = payload["method"] as? String else {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "surface.resume.set")
+ legacyAssertEqual(method, "surface.resume.set")
return self.v2Response(id: id, ok: true, result: ["resume_binding": [:]])
}
@@ -8286,9 +8286,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let setRequests = state.commands.compactMap { command -> [String: Any]? in
guard let payload = jsonObject(command),
@@ -8297,9 +8297,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- let request = try XCTUnwrap(setRequests.first)
- XCTAssertNil(request["workspace_id"])
- XCTAssertEqual(request["surface_id"] as? String, movedSurfaceId)
+ let request = try legacyUnwrap(setRequests.first)
+ legacyAssertNil(request["workspace_id"])
+ legacyAssertEqual(request["surface_id"] as? String, movedSurfaceId)
}
@Test
@@ -8326,10 +8326,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertNotEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stderr.contains("surface resume set: unexpected argument 'attach' after --shell"))
- XCTAssertFalse(result.stderr.contains("Socket"), result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertNotEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stderr.contains("surface resume set: unexpected argument 'attach' after --shell"))
+ legacyAssertFalse(result.stderr.contains("Socket"), result.stderr)
}
@Test
@@ -8354,10 +8354,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stderr.contains("surface resume set: unexpected argument 'myapp' before --"))
- XCTAssertFalse(result.stderr.contains("Socket"), result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stderr.contains("surface resume set: unexpected argument 'myapp' before --"))
+ legacyAssertFalse(result.stderr.contains("Socket"), result.stderr)
}
@Test
@@ -8407,11 +8407,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stdout.isEmpty, result.stdout)
- XCTAssertTrue(result.stderr.contains(item.expected), result.stderr)
- XCTAssertFalse(result.stderr.contains("Socket"), result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stdout.isEmpty, result.stdout)
+ legacyAssertTrue(result.stderr.contains(item.expected), result.stderr)
+ legacyAssertFalse(result.stderr.contains("Socket"), result.stderr)
}
}
@@ -8436,10 +8436,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertNotEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stderr.contains("surface resume clear: --checkpoint requires a value"))
- XCTAssertFalse(result.stderr.contains("Socket"), result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertNotEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stderr.contains("surface resume clear: --checkpoint requires a value"))
+ legacyAssertFalse(result.stderr.contains("Socket"), result.stderr)
}
@Test
@@ -8473,7 +8473,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
return self.v2Response(id: id, ok: true, result: ["window_id": windowId])
case "surface.list":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(
id: id,
ok: true,
@@ -8497,10 +8497,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
let clearRequests = state.commands.compactMap { command -> [String: Any]? in
guard let payload = jsonObject(command),
@@ -8509,16 +8509,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- XCTAssertFalse(
+ legacyAssertFalse(
state.commands.contains { command in
jsonObject(command)?["method"] as? String == "window.focus"
},
"surface resume metadata commands should route by window_id without focusing the window"
)
- let request = try XCTUnwrap(clearRequests.first)
- XCTAssertEqual(request["window_id"] as? String, windowId)
- XCTAssertNotEqual(request["window_id"] as? String, "0")
- XCTAssertEqual(request["surface_id"] as? String, surfaceId)
+ let request = try legacyUnwrap(clearRequests.first)
+ legacyAssertEqual(request["window_id"] as? String, windowId)
+ legacyAssertNotEqual(request["window_id"] as? String, "0")
+ legacyAssertEqual(request["surface_id"] as? String, surfaceId)
}
@Test
@@ -8549,7 +8549,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
)
case "surface.list":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowId)
+ legacyAssertEqual(params["window_id"] as? String, windowId)
return self.v2Response(
id: id,
ok: true,
@@ -8573,11 +8573,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
- XCTAssertFalse(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
+ legacyAssertFalse(
state.commands.contains { command in
jsonObject(command)?["method"] as? String == "window.focus"
},
@@ -8591,9 +8591,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
}
return payload["params"] as? [String: Any]
}
- let request = try XCTUnwrap(clearRequests.first)
- XCTAssertEqual(request["window_id"] as? String, windowId)
- XCTAssertEqual(request["surface_id"] as? String, surfaceId)
+ let request = try legacyUnwrap(clearRequests.first)
+ legacyAssertEqual(request["window_id"] as? String, windowId)
+ legacyAssertEqual(request["surface_id"] as? String, surfaceId)
}
private struct ClaudeHookContext {
@@ -8823,15 +8823,15 @@ final class CLINotifyProcessIntegrationRegressionTests {
standardInput: standardInput,
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
+ legacyWait(for: [serverHandled], timeout: 5)
return result
}
private func readClaudeHookSession(_ sessionId: String, context: ClaudeHookContext) throws -> [String: Any] {
let stateURL = context.root.appendingPathComponent("claude-hook-sessions.json")
- let state = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
- let sessions = try XCTUnwrap(state["sessions"] as? [String: Any])
- return try XCTUnwrap(sessions[sessionId] as? [String: Any])
+ let state = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any])
+ let sessions = try legacyUnwrap(state["sessions"] as? [String: Any])
+ return try legacyUnwrap(sessions[sessionId] as? [String: Any])
}
private func feedPushEvents(in context: ClaudeHookContext) -> [[String: Any]] {
@@ -8865,7 +8865,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "browser.import.cookies")
+ legacyAssertEqual(method, "browser.import.cookies")
guard method == "browser.import.cookies" else {
return self.v2Response(
id: id,
@@ -8874,11 +8874,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
)
}
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["scope"] as? String, "cookiesOnly")
- XCTAssertEqual(params["browser"] as? String, "Chrome")
- XCTAssertEqual(params["source_profiles"] as? [String], ["Default"])
- XCTAssertEqual(params["domain_filters"] as? [String], ["github.com"])
- XCTAssertEqual(params["destination_profile"] as? String, "Dev")
+ legacyAssertEqual(params["scope"] as? String, "cookiesOnly")
+ legacyAssertEqual(params["browser"] as? String, "Chrome")
+ legacyAssertEqual(params["source_profiles"] as? [String], ["Default"])
+ legacyAssertEqual(params["domain_filters"] as? [String], ["github.com"])
+ legacyAssertEqual(params["destination_profile"] as? String, "Dev")
return self.v2Response(
id: id,
ok: true,
@@ -8915,16 +8915,16 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
- let stdoutJSON = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(result.stdout.utf8)) as? [String: Any])
- XCTAssertEqual(stdoutJSON["browser"] as? String, "Chrome")
- XCTAssertEqual(stdoutJSON["imported_cookies"] as? Int, 3)
- XCTAssertEqual(stdoutJSON["skipped_cookies"] as? Int, 1)
- XCTAssertTrue(
+ let stdoutJSON = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(result.stdout.utf8)) as? [String: Any])
+ legacyAssertEqual(stdoutJSON["browser"] as? String, "Chrome")
+ legacyAssertEqual(stdoutJSON["imported_cookies"] as? Int, 3)
+ legacyAssertEqual(stdoutJSON["skipped_cookies"] as? Int, 1)
+ legacyAssertTrue(
state.commands.contains { $0.contains(#""method":"browser.import.cookies""#) },
"Expected coding-agent import to use non-interactive import, saw \(state.commands)"
)
@@ -8949,7 +8949,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "browser.import.dialog")
+ legacyAssertEqual(method, "browser.import.dialog")
guard method == "browser.import.dialog" else {
return self.v2Response(
id: id,
@@ -8958,7 +8958,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
)
}
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertNil(params["scope"])
+ legacyAssertNil(params["scope"])
return self.v2Response(id: id, ok: true, result: ["opened": true])
}
@@ -8986,12 +8986,12 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
- XCTAssertTrue(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyAssertTrue(
state.commands.contains { $0.contains(#""method":"browser.import.dialog""#) },
"Expected human import to open the interactive dialog, saw \(state.commands)"
)
@@ -9016,7 +9016,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "browser.import.dialog")
+ legacyAssertEqual(method, "browser.import.dialog")
guard method == "browser.import.dialog" else {
return self.v2Response(
id: id,
@@ -9025,7 +9025,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
)
}
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertNil(params["scope"])
+ legacyAssertNil(params["scope"])
return self.v2Response(id: id, ok: true, result: ["opened": true])
}
@@ -9041,12 +9041,12 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK\n")
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
- XCTAssertTrue(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK\n")
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyAssertTrue(
state.commands.contains { $0.contains(#""method":"browser.import.dialog""#) },
"Expected --interactive to force the dialog in coding-agent env, saw \(state.commands)"
)
@@ -9071,7 +9071,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, "browser.profiles.list")
+ legacyAssertEqual(method, "browser.profiles.list")
return self.v2Response(
id: id,
ok: true,
@@ -9099,11 +9099,11 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stdout.contains("default\tDefault\t52B43C05-4A1D-45D3-8FD5-9EF94952E445"), result.stdout)
- XCTAssertTrue(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stdout.contains("default\tDefault\t52B43C05-4A1D-45D3-8FD5-9EF94952E445"), result.stdout)
+ legacyAssertTrue(
state.commands.contains { $0.contains(#""method":"browser.profiles.list""#) },
"Expected browser profiles list to call browser.profiles.list, saw \(state.commands)"
)
@@ -9178,9 +9178,9 @@ final class CLINotifyProcessIntegrationRegressionTests {
return self.malformedRequestResponse(raw: line)
}
- XCTAssertEqual(method, testCase.expectedMethod)
+ legacyAssertEqual(method, testCase.expectedMethod)
for expectedParam in testCase.expectedParams {
- XCTAssertTrue(line.contains(expectedParam), line)
+ legacyAssertTrue(line.contains(expectedParam), line)
}
return self.v2Response(id: id, ok: true, result: testCase.responseResult)
}
@@ -9196,10 +9196,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(
state.commands.contains { $0.contains(#""method":"\#(testCase.expectedMethod)""#) },
"Expected \(testCase.expectedMethod), saw \(state.commands)"
)
@@ -9310,10 +9310,10 @@ final class CLINotifyProcessIntegrationRegressionTests {
let sawConfigureRequest = waitForMockSocketCommand(in: state) { line in
line.contains(#""method":"workspace.remote.configure""#)
}
- XCTAssertTrue(sawConfigureRequest, "Expected workspace.remote.configure, saw \(state.snapshot())", file: file, line: line)
- XCTAssertFalse(result.timedOut, result.stderr, file: file, line: line)
- XCTAssertEqual(result.status, 0, result.stderr, file: file, line: line)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr, file: file, line: line)
+ legacyAssertTrue(sawConfigureRequest, "Expected workspace.remote.configure, saw \(state.snapshot())", file: file, line: line)
+ legacyAssertFalse(result.timedOut, result.stderr, file: file, line: line)
+ legacyAssertEqual(result.status, 0, result.stderr, file: file, line: line)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr, file: file, line: line)
let requests = state.snapshot().compactMap { jsonObject($0) }
return MockedSSHRun(
@@ -9346,7 +9346,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true
)
- XCTAssertTrue(
+ legacyAssertTrue(
FileManager.default.createFile(atPath: url.path, contents: Data()),
"Expected to create \(url.path)"
)
@@ -9386,7 +9386,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
private func notificationRows(from stdout: String) throws -> [[String: Any]] {
let data = Data(stdout.utf8)
- return try XCTUnwrap(
+ return try legacyUnwrap(
JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]],
"Expected notification JSON array, got: \(stdout)"
)
@@ -9394,7 +9394,7 @@ final class CLINotifyProcessIntegrationRegressionTests {
private func jsonPayload(from stdout: String) throws -> [String: Any] {
let data = Data(stdout.utf8)
- return try XCTUnwrap(
+ return try legacyUnwrap(
JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
"Expected JSON object, got: \(stdout)"
)
@@ -9461,20 +9461,20 @@ extension CLINotifyProcessIntegrationRegressionTests {
],
timeout: 30
)
- wait(for: [handled], timeout: 30)
+ legacyWait(for: [handled], timeout: 30)
- XCTAssertTrue(
+ legacyAssertTrue(
result.stdout.contains("CMUX_SURFACE_ID=\(launchSurface)"),
"launcher must stamp the LAUNCH surface; stdout:\n\(result.stdout)\nstderr:\n\(result.stderr)"
)
- XCTAssertFalse(
+ legacyAssertFalse(
result.stdout.contains("CMUX_SURFACE_ID=\(focusedSurface)"),
"launcher must NOT stamp the focused surface; stdout:\n\(result.stdout)"
)
- XCTAssertTrue(result.stdout.contains("CMUX_WORKSPACE_ID=\(launchWorkspace)"), result.stdout)
+ legacyAssertTrue(result.stdout.contains("CMUX_WORKSPACE_ID=\(launchWorkspace)"), result.stdout)
// Matched-pair invariant: SURFACE == PANEL (the desync is exactly the bug). The surface-scoped
// tab id passes through untouched.
- XCTAssertTrue(result.stdout.contains("CMUX_PANEL_ID=\(launchSurface)"), result.stdout)
- XCTAssertTrue(result.stdout.contains("CMUX_TAB_ID=\(launchTab)"), result.stdout)
+ legacyAssertTrue(result.stdout.contains("CMUX_PANEL_ID=\(launchSurface)"), result.stdout)
+ legacyAssertTrue(result.stdout.contains("CMUX_TAB_ID=\(launchTab)"), result.stdout)
}
}
diff --git a/cmuxTests/CLINotifyProcessTestSupport.swift b/cmuxTests/CLINotifyProcessTestSupport.swift
index e7d583e91503..146689e83131 100644
--- a/cmuxTests/CLINotifyProcessTestSupport.swift
+++ b/cmuxTests/CLINotifyProcessTestSupport.swift
@@ -2,275 +2,277 @@ import Darwin
import Foundation
import Testing
-extension CLINotifyProcessIntegrationRegressionTests {
- final class LegacyExpectation: @unchecked Sendable {
- private let lock = NSLock()
- private let semaphore = DispatchSemaphore(value: 0)
- private var fulfilled = false
- private let description: String
+typealias LegacyExpectation = CLINotifyProcessIntegrationRegressionTests.LegacyExpectation
- init(description: String) {
- self.description = description
- }
+func legacyExpectation(description: String) -> LegacyExpectation {
+ LegacyExpectation(description: description)
+}
- func fulfill() {
- lock.lock()
- let shouldSignal = !fulfilled
- fulfilled = true
- lock.unlock()
+func legacyWait(
+ for expectations: [LegacyExpectation],
+ timeout: TimeInterval,
+ file: StaticString = #filePath,
+ line: UInt = #line
+) {
+ _ = legacyWaitForExpectations(
+ expectations,
+ timeout: timeout,
+ recordFailure: true,
+ file: file,
+ line: line
+ )
+}
- if shouldSignal {
- semaphore.signal()
+@discardableResult
+func legacyWaitForExpectations(
+ _ expectations: [LegacyExpectation],
+ timeout: TimeInterval,
+ recordFailure: Bool,
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+) -> Bool {
+ var allFulfilled = true
+ for expectation in expectations {
+ let deadline = Date().addingTimeInterval(timeout)
+ while Date() < deadline {
+ if expectation.pollFulfilled() {
+ break
}
+ _ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01))
}
- func pollFulfilled() -> Bool {
- lock.lock()
- if fulfilled {
- lock.unlock()
- return true
+ if !expectation.pollFulfilled() {
+ allFulfilled = false
+ if recordFailure {
+ legacyFailure(expectation.timeoutMessage)
}
- lock.unlock()
-
- guard semaphore.wait(timeout: .now()) == .success else {
- return false
- }
-
- lock.lock()
- fulfilled = true
- lock.unlock()
- return true
- }
-
- var timeoutMessage: String {
- "Timed out waiting for expectation: \(description)"
}
}
+ return allFulfilled
+}
- func expectation(description: String) -> LegacyExpectation {
- LegacyExpectation(description: description)
- }
+func runLegacyActivity(named _: String, _ body: () throws -> T) rethrows -> T {
+ try body()
+}
- func wait(
- for expectations: [LegacyExpectation],
- timeout: TimeInterval,
- file: StaticString = #filePath,
- line: UInt = #line
- ) {
- _ = waitForLegacyExpectations(
- expectations,
- timeout: timeout,
- recordFailure: true,
- file: file,
- line: line
- )
- }
+func legacyFailure(_ message: String) {
+ Issue.record(Comment(rawValue: message.isEmpty ? "Assertion failed" : message))
+}
- @discardableResult
- func waitForLegacyExpectations(
- _ expectations: [LegacyExpectation],
- timeout: TimeInterval,
- recordFailure: Bool,
- file _: StaticString = #filePath,
- line _: UInt = #line
- ) -> Bool {
- var allFulfilled = true
- for expectation in expectations {
- let deadline = Date().addingTimeInterval(timeout)
- while Date() < deadline {
- if expectation.pollFulfilled() {
- break
- }
- _ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01))
- }
+func legacyFail(
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+) {
+ legacyFailure(message())
+}
- if !expectation.pollFulfilled() {
- allFulfilled = false
- if recordFailure {
- legacyFailure(expectation.timeoutMessage)
- }
- }
+func legacyAssertTrue(
+ _ expression: @autoclosure () throws -> Bool,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+) {
+ do {
+ if try !expression() {
+ legacyFailure(message())
}
- return allFulfilled
+ } catch {
+ legacyFailure("\(message()) \(error)")
}
+}
- func runLegacyActivity(named _: String, _ body: () throws -> T) rethrows -> T {
- try body()
+func legacyAssertFalse(
+ _ expression: @autoclosure () throws -> Bool,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+) {
+ do {
+ if try expression() {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
}
+}
- func legacyFailure(_ message: String) {
- Issue.record(Comment(rawValue: message.isEmpty ? "Assertion failed" : message))
+func legacyAssertEqual(
+ _ expression1: @autoclosure () throws -> T,
+ _ expression2: @autoclosure () throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs != rhs {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
}
+}
- func XCTFail(
- _ message: @autoclosure () -> String = "",
- file _: StaticString = #filePath,
- line _: UInt = #line
- ) {
- legacyFailure(message())
+func legacyAssertNotEqual(
+ _ expression1: @autoclosure () throws -> T,
+ _ expression2: @autoclosure () throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs == rhs {
+ legacyFailure(message())
+ }
+ } catch {
+ legacyFailure("\(message()) \(error)")
}
+}
- func XCTAssertTrue(
- _ expression: @autoclosure () throws -> Bool,
- _ message: @autoclosure () -> String = "",
- file _: StaticString = #filePath,
- line _: UInt = #line
- ) {
- do {
- if try !expression() {
- legacyFailure(message())
- }
- } catch {
- legacyFailure("\(message()) \(error)")
+func legacyAssertGreaterThan(
+ _ expression1: @autoclosure () throws -> T,
+ _ expression2: @autoclosure () throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs <= rhs {
+ legacyFailure(message())
}
+ } catch {
+ legacyFailure("\(message()) \(error)")
}
+}
- func XCTAssertFalse(
- _ expression: @autoclosure () throws -> Bool,
- _ message: @autoclosure () -> String = "",
- file _: StaticString = #filePath,
- line _: UInt = #line
- ) {
- do {
- if try expression() {
- legacyFailure(message())
- }
- } catch {
- legacyFailure("\(message()) \(error)")
+func legacyAssertGreaterThanOrEqual(
+ _ expression1: @autoclosure () throws -> T,
+ _ expression2: @autoclosure () throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs < rhs {
+ legacyFailure(message())
}
+ } catch {
+ legacyFailure("\(message()) \(error)")
}
+}
- func XCTAssertEqual(
- _ expression1: @autoclosure () throws -> T,
- _ expression2: @autoclosure () throws -> T,
- _ message: @autoclosure () -> String = "",
- file _: StaticString = #filePath,
- line _: UInt = #line
- ) {
- do {
- let lhs = try expression1()
- let rhs = try expression2()
- if lhs != rhs {
- legacyFailure(message())
- }
- } catch {
- legacyFailure("\(message()) \(error)")
+func legacyAssertLessThan(
+ _ expression1: @autoclosure () throws -> T,
+ _ expression2: @autoclosure () throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+) {
+ do {
+ let lhs = try expression1()
+ let rhs = try expression2()
+ if lhs >= rhs {
+ legacyFailure(message())
}
+ } catch {
+ legacyFailure("\(message()) \(error)")
}
+}
- func XCTAssertNotEqual(
- _ expression1: @autoclosure () throws -> T,
- _ expression2: @autoclosure () throws -> T,
- _ message: @autoclosure () -> String = "",
- file _: StaticString = #filePath,
- line _: UInt = #line
- ) {
- do {
- let lhs = try expression1()
- let rhs = try expression2()
- if lhs == rhs {
- legacyFailure(message())
- }
- } catch {
- legacyFailure("\(message()) \(error)")
+func legacyAssertNil(
+ _ expression: @autoclosure () throws -> T?,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+) {
+ do {
+ if try expression() != nil {
+ legacyFailure(message())
}
+ } catch {
+ legacyFailure("\(message()) \(error)")
}
+}
- func XCTAssertGreaterThan(
- _ expression1: @autoclosure () throws -> T,
- _ expression2: @autoclosure () throws -> T,
- _ message: @autoclosure () -> String = "",
- file _: StaticString = #filePath,
- line _: UInt = #line
- ) {
- do {
- let lhs = try expression1()
- let rhs = try expression2()
- if lhs <= rhs {
- legacyFailure(message())
- }
- } catch {
- legacyFailure("\(message()) \(error)")
+func legacyAssertNotNil(
+ _ expression: @autoclosure () throws -> T?,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+) {
+ do {
+ if try expression() == nil {
+ legacyFailure(message())
}
+ } catch {
+ legacyFailure("\(message()) \(error)")
}
+}
- func XCTAssertGreaterThanOrEqual(
- _ expression1: @autoclosure () throws -> T,
- _ expression2: @autoclosure () throws -> T,
- _ message: @autoclosure () -> String = "",
- file _: StaticString = #filePath,
- line _: UInt = #line
- ) {
- do {
- let lhs = try expression1()
- let rhs = try expression2()
- if lhs < rhs {
- legacyFailure(message())
- }
- } catch {
- legacyFailure("\(message()) \(error)")
- }
+func legacyUnwrap(
+ _ expression: @autoclosure () throws -> T?,
+ _ message: @autoclosure () -> String = "",
+ file _: StaticString = #filePath,
+ line _: UInt = #line
+) throws -> T {
+ guard let value = try expression() else {
+ legacyFailure(message())
+ throw NSError(domain: "CLINotifyProcessIntegrationRegressionTests", code: 1)
}
+ return value
+}
- func XCTAssertLessThan(
- _ expression1: @autoclosure () throws -> T,
- _ expression2: @autoclosure () throws -> T,
- _ message: @autoclosure () -> String = "",
- file _: StaticString = #filePath,
- line _: UInt = #line
- ) {
- do {
- let lhs = try expression1()
- let rhs = try expression2()
- if lhs >= rhs {
- legacyFailure(message())
- }
- } catch {
- legacyFailure("\(message()) \(error)")
+extension CLINotifyProcessIntegrationRegressionTests {
+ final class LegacyExpectation: @unchecked Sendable {
+ private let lock = NSLock()
+ private let semaphore = DispatchSemaphore(value: 0)
+ private var fulfilled = false
+ private let description: String
+
+ init(description: String) {
+ self.description = description
}
- }
- func XCTAssertNil(
- _ expression: @autoclosure () throws -> T?,
- _ message: @autoclosure () -> String = "",
- file _: StaticString = #filePath,
- line _: UInt = #line
- ) {
- do {
- if try expression() != nil {
- legacyFailure(message())
+ func fulfill() {
+ lock.lock()
+ let shouldSignal = !fulfilled
+ fulfilled = true
+ lock.unlock()
+
+ if shouldSignal {
+ semaphore.signal()
}
- } catch {
- legacyFailure("\(message()) \(error)")
}
- }
- func XCTAssertNotNil(
- _ expression: @autoclosure () throws -> T?,
- _ message: @autoclosure () -> String = "",
- file _: StaticString = #filePath,
- line _: UInt = #line
- ) {
- do {
- if try expression() == nil {
- legacyFailure(message())
+ func pollFulfilled() -> Bool {
+ lock.lock()
+ if fulfilled {
+ lock.unlock()
+ return true
}
- } catch {
- legacyFailure("\(message()) \(error)")
+ lock.unlock()
+
+ guard semaphore.wait(timeout: .now()) == .success else {
+ return false
+ }
+
+ lock.lock()
+ fulfilled = true
+ lock.unlock()
+ return true
}
- }
- func XCTUnwrap(
- _ expression: @autoclosure () throws -> T?,
- _ message: @autoclosure () -> String = "",
- file _: StaticString = #filePath,
- line _: UInt = #line
- ) throws -> T {
- guard let value = try expression() else {
- legacyFailure(message())
- throw NSError(domain: "CLINotifyProcessIntegrationRegressionTests", code: 1)
+ var timeoutMessage: String {
+ "Timed out waiting for expectation: \(description)"
}
- return value
}
struct ProcessRunResult {
@@ -317,13 +319,13 @@ extension CLINotifyProcessIntegrationRegressionTests {
func bindUnixSocket(at path: String) throws -> Int32 {
unlink(path)
let fd = Darwin.socket(AF_UNIX, SOCK_STREAM, 0)
- XCTAssertGreaterThanOrEqual(fd, 0)
+ legacyAssertGreaterThanOrEqual(fd, 0)
var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
let maxPathLength = MemoryLayout.size(ofValue: addr.sun_path)
let utf8 = Array(path.utf8)
- XCTAssertLessThan(utf8.count, maxPathLength)
+ legacyAssertLessThan(utf8.count, maxPathLength)
_ = withUnsafeMutablePointer(to: &addr.sun_path) { pointer in
pointer.withMemoryRebound(to: CChar.self, capacity: maxPathLength) { buffer in
for index in 0...size))
}
}
- XCTAssertEqual(bindResult, 0)
- XCTAssertEqual(Darwin.listen(fd, 1), 0)
+ legacyAssertEqual(bindResult, 0)
+ legacyAssertEqual(Darwin.listen(fd, 1), 0)
return fd
}
@@ -407,7 +409,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
}
func startBridgeErrorServer(listenerFD: Int32, message: String) -> LegacyExpectation {
- let handled = expectation(description: "pty bridge error server handled")
+ let handled = legacyExpectation(description: "pty bridge error server handled")
DispatchQueue.global(qos: .userInitiated).async {
defer { handled.fulfill() }
@@ -457,7 +459,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
}
func startBridgeReadyThenCloseServer(listenerFD: Int32) -> LegacyExpectation {
- let handled = expectation(description: "pty bridge ready close server handled")
+ let handled = legacyExpectation(description: "pty bridge ready close server handled")
DispatchQueue.global(qos: .userInitiated).async {
defer { handled.fulfill() }
@@ -507,7 +509,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
}
func startBridgeReadyThenResetAfterClientEOFServer(listenerFD: Int32) -> LegacyExpectation {
- let handled = expectation(description: "pty bridge ready reset server handled")
+ let handled = legacyExpectation(description: "pty bridge ready reset server handled")
DispatchQueue.global(qos: .userInitiated).async {
defer { handled.fulfill() }
@@ -600,7 +602,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
fulfillWhen: (@Sendable (String) -> Bool)? = nil,
handler: @escaping @Sendable (String) -> String?
) -> LegacyExpectation {
- let handled = expectation(description: "cli mock socket handled")
+ let handled = legacyExpectation(description: "cli mock socket handled")
DispatchQueue.global(qos: .userInitiated).async {
var didFulfill = false
func fulfillOnce() {
diff --git a/cmuxTests/CLIRovoDevHookPersistenceTests.swift b/cmuxTests/CLIRovoDevHookPersistenceTests.swift
index 343f420a8c6f..17ded1ec1638 100644
--- a/cmuxTests/CLIRovoDevHookPersistenceTests.swift
+++ b/cmuxTests/CLIRovoDevHookPersistenceTests.swift
@@ -71,27 +71,27 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
let storeURL = root.appendingPathComponent("rovodev-hook-sessions.json", isDirectory: false)
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- let sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
- let session = try XCTUnwrap(sessions[sessionId] as? [String: Any])
- XCTAssertEqual(session["workspaceId"] as? String, workspaceId)
- XCTAssertEqual(session["surfaceId"] as? String, surfaceId)
- XCTAssertEqual(session["cwd"] as? String, workspace.path)
-
- let launchCommand = try XCTUnwrap(session["launchCommand"] as? [String: Any])
- XCTAssertEqual(launchCommand["launcher"] as? String, "rovodev")
- XCTAssertEqual(launchCommand["executablePath"] as? String, "/usr/local/bin/acli")
- XCTAssertEqual(
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ let sessions = try legacyUnwrap(json["sessions"] as? [String: Any])
+ let session = try legacyUnwrap(sessions[sessionId] as? [String: Any])
+ legacyAssertEqual(session["workspaceId"] as? String, workspaceId)
+ legacyAssertEqual(session["surfaceId"] as? String, surfaceId)
+ legacyAssertEqual(session["cwd"] as? String, workspace.path)
+
+ let launchCommand = try legacyUnwrap(session["launchCommand"] as? [String: Any])
+ legacyAssertEqual(launchCommand["launcher"] as? String, "rovodev")
+ legacyAssertEqual(launchCommand["executablePath"] as? String, "/usr/local/bin/acli")
+ legacyAssertEqual(
launchCommand["arguments"] as? [String],
["/usr/local/bin/acli", "rovodev", "run", "--yolo"]
)
- XCTAssertTrue(
+ legacyAssertTrue(
state.commands.contains { $0.contains("set_status rovodev Running") && $0.contains("--tab=\(workspaceId)") },
"Expected Rovo Dev prompt status to target current workspace, saw \(state.commands)"
)
@@ -172,16 +172,16 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
let storeURL = root.appendingPathComponent("rovodev-hook-sessions.json", isDirectory: false)
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- let sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
- XCTAssertNil(sessions["rovo-older-session"] as? [String: Any])
- XCTAssertNotNil(sessions["rovo-newer-session"] as? [String: Any])
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ let sessions = try legacyUnwrap(json["sessions"] as? [String: Any])
+ legacyAssertNil(sessions["rovo-older-session"] as? [String: Any])
+ legacyAssertNotNil(sessions["rovo-newer-session"] as? [String: Any])
}
@Test
func testRovoDevPromptSubmitReadsConfiguredPersistenceDirWithCommentsHashAndApostrophePath() throws {
@@ -270,15 +270,15 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
let storeURL = root.appendingPathComponent("rovodev-hook-sessions.json", isDirectory: false)
- let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
- let sessions = try XCTUnwrap(json["sessions"] as? [String: Any])
- XCTAssertNotNil(sessions[sessionId] as? [String: Any])
+ let json = try legacyUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: storeURL)) as? [String: Any])
+ let sessions = try legacyUnwrap(json["sessions"] as? [String: Any])
+ legacyAssertNotNil(sessions[sessionId] as? [String: Any])
}
@Test
func testRovoDevPromptSubmitWithoutCwdDoesNotInferUnrelatedSession() throws {
@@ -340,16 +340,16 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "{}\n")
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "{}\n")
let storeURL = root.appendingPathComponent("rovodev-hook-sessions.json", isDirectory: false)
if let data = try? Data(contentsOf: storeURL),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let sessions = json["sessions"] as? [String: Any] {
- XCTAssertNil(sessions[unrelatedSessionId] as? [String: Any])
+ legacyAssertNil(sessions[unrelatedSessionId] as? [String: Any])
}
}
@Test
@@ -377,10 +377,10 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertNotEqual(result.status, 0)
- XCTAssertTrue(result.stderr.contains("could not be read"), result.stderr)
- XCTAssertTrue(FileManager.default.fileExists(atPath: sentinelURL.path))
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertNotEqual(result.status, 0)
+ legacyAssertTrue(result.stderr.contains("could not be read"), result.stderr)
+ legacyAssertTrue(FileManager.default.fileExists(atPath: sentinelURL.path))
}
@Test
func testRovoAliasCreatesConfigDirAndInstallsRovoDevHooksFromSetup() throws {
@@ -407,17 +407,17 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stdout.contains("rovodev:"), result.stdout)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stdout.contains("rovodev:"), result.stdout)
let config = try String(
contentsOf: configDir.appendingPathComponent("config.yml", isDirectory: false),
encoding: .utf8
)
- XCTAssertTrue(config.contains("eventHooks:"), config)
- XCTAssertTrue(config.contains(#"hooks rovodev prompt-submit"#), config)
- XCTAssertTrue(config.contains(#"CMUX_BUNDLED_CLI_PATH"#), config)
+ legacyAssertTrue(config.contains("eventHooks:"), config)
+ legacyAssertTrue(config.contains(#"hooks rovodev prompt-submit"#), config)
+ legacyAssertTrue(config.contains(#"CMUX_BUNDLED_CLI_PATH"#), config)
}
@Test
func testSetupHooksRejectsConflictingAgentFilters() throws {
@@ -438,9 +438,9 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertNotEqual(result.status, 0)
- XCTAssertTrue(result.stderr.contains("Conflicting hooks target"), result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertNotEqual(result.status, 0)
+ legacyAssertTrue(result.stderr.contains("Conflicting hooks target"), result.stderr)
}
@Test
func testSetupHooksRejectsMultiplePositionalTargets() throws {
@@ -461,9 +461,9 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertNotEqual(result.status, 0)
- XCTAssertTrue(result.stderr.contains("Too many hooks targets"), result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertNotEqual(result.status, 0)
+ legacyAssertTrue(result.stderr.contains("Too many hooks targets"), result.stderr)
}
func writeRovoDevSessionMetadata(
diff --git a/cmuxTests/CLISSHPTYAttachProbeReplyRegressionTests.swift b/cmuxTests/CLISSHPTYAttachProbeReplyRegressionTests.swift
index fde55a613c91..9182f5ab3ce9 100644
--- a/cmuxTests/CLISSHPTYAttachProbeReplyRegressionTests.swift
+++ b/cmuxTests/CLISSHPTYAttachProbeReplyRegressionTests.swift
@@ -95,7 +95,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [socketHandled], timeout: 5)
+ legacyWait(for: [socketHandled], timeout: 5)
#expect(bridgeHandled.wait(timeout: .now() + 5) == .success)
#expect(!result.timedOut)
#expect(result.status == 0)
diff --git a/cmuxTests/SSHStartupSignalLifecycleTests.swift b/cmuxTests/SSHStartupSignalLifecycleTests.swift
index d9ce94c1bf02..0ec7edcdfca3 100644
--- a/cmuxTests/SSHStartupSignalLifecycleTests.swift
+++ b/cmuxTests/SSHStartupSignalLifecycleTests.swift
@@ -53,14 +53,14 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- let expectedStatus = try XCTUnwrap(expectedStatuses[signal])
- XCTAssertEqual(result.status, expectedStatus, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ let expectedStatus = try legacyUnwrap(expectedStatuses[signal])
+ legacyAssertEqual(result.status, expectedStatus, result.stderr)
let recordedCalls = (try? String(contentsOf: logFile, encoding: .utf8)) ?? ""
let sessionEndCalls = recordedCalls
.split(separator: "\n")
.filter { $0.contains("ssh-session-end") }
- XCTAssertTrue(
+ legacyAssertTrue(
sessionEndCalls.isEmpty,
"Pane-close \(signal) must not call ssh-session-end because that can tear down the shared SSH transport and kill sibling panes; recorded: \(recordedCalls)"
)
@@ -124,10 +124,10 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- let expectedStatus = try XCTUnwrap(expectedStatuses[signal])
- XCTAssertEqual(result.status, expectedStatus, result.stderr)
- XCTAssertTrue(
+ legacyAssertFalse(result.timedOut, result.stderr)
+ let expectedStatus = try legacyUnwrap(expectedStatuses[signal])
+ legacyAssertEqual(result.status, expectedStatus, result.stderr)
+ legacyAssertTrue(
waitForSSHSignalLifecycleLog(childSignalLog) { contents in
contents.contains("child-completed") ||
contents.contains("child-hup") ||
@@ -137,8 +137,8 @@ extension CLINotifyProcessIntegrationRegressionTests {
"Timed out waiting for fake SSH child to record completion or signal for \(signal)"
)
let childSignalLogContents = (try? String(contentsOf: childSignalLog, encoding: .utf8)) ?? ""
- XCTAssertTrue(childSignalLogContents.contains("child-started"), childSignalLogContents)
- XCTAssertFalse(
+ legacyAssertTrue(childSignalLogContents.contains("child-started"), childSignalLogContents)
+ legacyAssertFalse(
childSignalLogContents.contains("child-hup") ||
childSignalLogContents.contains("child-int") ||
childSignalLogContents.contains("child-term"),
@@ -148,7 +148,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
let sessionEndCalls = recordedCalls
.split(separator: "\n")
.filter { $0.contains("ssh-session-end") }
- XCTAssertTrue(sessionEndCalls.isEmpty, recordedCalls)
+ legacyAssertTrue(sessionEndCalls.isEmpty, recordedCalls)
}
}
@Test
@@ -198,18 +198,18 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
// The regular `cmux ssh` bootstrap path runs one SSH command to install
// the remote bootstrap and another to open the session. A transient
// install-channel failure therefore yields three raw SSH invocations:
// failed install, retried install, successful session.
- XCTAssertEqual((try? String(contentsOf: attemptFile, encoding: .utf8))?.trimmingCharacters(in: .whitespacesAndNewlines), "3")
+ legacyAssertEqual((try? String(contentsOf: attemptFile, encoding: .utf8))?.trimmingCharacters(in: .whitespacesAndNewlines), "3")
let recordedCalls = (try? String(contentsOf: logFile, encoding: .utf8)) ?? ""
let sessionEndCalls = recordedCalls
.split(separator: "\n")
.filter { $0.contains("ssh-session-end") }
- XCTAssertEqual(sessionEndCalls.count, 1, recordedCalls)
+ legacyAssertEqual(sessionEndCalls.count, 1, recordedCalls)
}
@Test
func testSSHStartupRemovesStaleCmuxControlSocketBeforeLaunchingPaneSSH() throws {
@@ -230,7 +230,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
let staleSocketFD = try bindUnixSocket(at: staleControlPath.path)
Darwin.close(staleSocketFD)
- XCTAssertTrue(fileManager.fileExists(atPath: staleControlPath.path))
+ legacyAssertTrue(fileManager.fileExists(atPath: staleControlPath.path))
try writeShellFile(at: fakeCLI, lines: [
"#!/bin/sh",
@@ -284,13 +284,13 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertFalse(fileManager.fileExists(atPath: staleControlPath.path))
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(fileManager.fileExists(atPath: staleControlPath.path))
let sshLog = (try? String(contentsOf: logFile, encoding: .utf8)) ?? ""
- XCTAssertTrue(sshLog.contains("-G"), sshLog)
- XCTAssertTrue(sshLog.contains("-O check"), sshLog)
+ legacyAssertTrue(sshLog.contains("-G"), sshLog)
+ legacyAssertTrue(sshLog.contains("-O check"), sshLog)
}
@Test
func testSSHStartupStopsAtConfiguredReconnectLimit() throws {
@@ -339,14 +339,14 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 255, result.stderr)
- XCTAssertEqual((try? String(contentsOf: attemptFile, encoding: .utf8))?.trimmingCharacters(in: .whitespacesAndNewlines), "3")
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 255, result.stderr)
+ legacyAssertEqual((try? String(contentsOf: attemptFile, encoding: .utf8))?.trimmingCharacters(in: .whitespacesAndNewlines), "3")
let recordedCalls = (try? String(contentsOf: logFile, encoding: .utf8)) ?? ""
let sessionEndCalls = recordedCalls
.split(separator: "\n")
.filter { $0.contains("ssh-session-end") }
- XCTAssertEqual(sessionEndCalls.count, 1, recordedCalls)
+ legacyAssertEqual(sessionEndCalls.count, 1, recordedCalls)
}
@Test
func testSSHStartupDoesNotRetryNonTransientSSHExit() throws {
@@ -394,14 +394,14 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertEqual((try? String(contentsOf: attemptFile, encoding: .utf8))?.trimmingCharacters(in: .whitespacesAndNewlines), "1")
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertEqual((try? String(contentsOf: attemptFile, encoding: .utf8))?.trimmingCharacters(in: .whitespacesAndNewlines), "1")
let recordedCalls = (try? String(contentsOf: logFile, encoding: .utf8)) ?? ""
let sessionEndCalls = recordedCalls
.split(separator: "\n")
.filter { $0.contains("ssh-session-end") }
- XCTAssertEqual(sessionEndCalls.count, 1, recordedCalls)
+ legacyAssertEqual(sessionEndCalls.count, 1, recordedCalls)
}
@Test
func testSSHSignalDerivedChildExitReportsSessionEnd() throws {
@@ -443,13 +443,13 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 130, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 130, result.stderr)
let recordedCalls = (try? String(contentsOf: logFile, encoding: .utf8)) ?? ""
let sessionEndCalls = recordedCalls
.split(separator: "\n")
.filter { $0.contains("ssh-session-end") }
- XCTAssertEqual(sessionEndCalls.count, 1, recordedCalls)
+ legacyAssertEqual(sessionEndCalls.count, 1, recordedCalls)
}
@Test
func testSSHSignalDuringReconnectDelayDoesNotStartAnotherSSH() throws {
@@ -502,14 +502,14 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 143, result.stderr)
- XCTAssertEqual((try? String(contentsOf: attemptFile, encoding: .utf8))?.trimmingCharacters(in: .whitespacesAndNewlines), "1")
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 143, result.stderr)
+ legacyAssertEqual((try? String(contentsOf: attemptFile, encoding: .utf8))?.trimmingCharacters(in: .whitespacesAndNewlines), "1")
let recordedCalls = (try? String(contentsOf: logFile, encoding: .utf8)) ?? ""
let sessionEndCalls = recordedCalls
.split(separator: "\n")
.filter { $0.contains("ssh-session-end") }
- XCTAssertEqual(sessionEndCalls.count, 1, recordedCalls)
+ legacyAssertEqual(sessionEndCalls.count, 1, recordedCalls)
}
@Test
func testSSHStartupPrintsFinalErrorBannerWhenStderrIsCaptured() throws {
@@ -551,10 +551,10 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 1, result.stderr)
- XCTAssertTrue(result.stderr.contains("[cmux] ssh exited with status 1."), result.stderr)
- XCTAssertTrue(result.stderr.contains("[cmux] press Enter to close this pane."), result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 1, result.stderr)
+ legacyAssertTrue(result.stderr.contains("[cmux] ssh exited with status 1."), result.stderr)
+ legacyAssertTrue(result.stderr.contains("[cmux] press Enter to close this pane."), result.stderr)
}
@Test
func testSSHStartupForwardsStdinToBackgroundedSSH() throws {
@@ -610,11 +610,11 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
let recorded = (try? String(contentsOf: stdinCapture, encoding: .utf8))?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
- XCTAssertEqual(
+ legacyAssertEqual(
recorded,
"FORWARDED_KEYSTROKE",
"Backgrounded ssh in the startup wrapper must inherit the wrapper's stdin so that keystrokes from the surface PTY reach the remote shell. Got: \(recorded.isEmpty ? "" : recorded)"
@@ -704,20 +704,20 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
let requests = try state.commands.map { line -> [String: Any] in
- let data = try XCTUnwrap(line.data(using: .utf8))
- return try XCTUnwrap(JSONSerialization.jsonObject(with: data, options: []) as? [String: Any])
+ let data = try legacyUnwrap(line.data(using: .utf8))
+ return try legacyUnwrap(JSONSerialization.jsonObject(with: data, options: []) as? [String: Any])
}
- let configureRequest = try XCTUnwrap(
+ let configureRequest = try legacyUnwrap(
requests.first { ($0["method"] as? String) == "workspace.remote.configure" }
)
- let configureParams = try XCTUnwrap(configureRequest["params"] as? [String: Any])
- return try XCTUnwrap(configureParams["terminal_startup_command"] as? String)
+ let configureParams = try legacyUnwrap(configureRequest["params"] as? [String: Any])
+ return try legacyUnwrap(configureParams["terminal_startup_command"] as? String)
}
private func generatedVMSSHInitialStartupCommand() throws -> String {
@@ -744,8 +744,8 @@ extension CLINotifyProcessIntegrationRegressionTests {
switch method {
case "vm.attach_info":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["id"] as? String, vmID)
- XCTAssertEqual(params["require_daemon"] as? Bool, true)
+ legacyAssertEqual(params["id"] as? String, vmID)
+ legacyAssertEqual(params["require_daemon"] as? Bool, true)
return self.v2Response(
id: id,
ok: true,
@@ -806,20 +806,20 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
let requests = try state.commands.map { line -> [String: Any] in
- let data = try XCTUnwrap(line.data(using: .utf8))
- return try XCTUnwrap(JSONSerialization.jsonObject(with: data, options: []) as? [String: Any])
+ let data = try legacyUnwrap(line.data(using: .utf8))
+ return try legacyUnwrap(JSONSerialization.jsonObject(with: data, options: []) as? [String: Any])
}
- let createRequest = try XCTUnwrap(
+ let createRequest = try legacyUnwrap(
requests.first { ($0["method"] as? String) == "workspace.create" }
)
- let createParams = try XCTUnwrap(createRequest["params"] as? [String: Any])
- return try XCTUnwrap(createParams["initial_command"] as? String)
+ let createParams = try legacyUnwrap(createRequest["params"] as? [String: Any])
+ return try legacyUnwrap(createParams["initial_command"] as? String)
}
private func writeShellFile(at url: URL, lines: [String]) throws {
diff --git a/cmuxTests/VMSSHCommandTests.swift b/cmuxTests/VMSSHCommandTests.swift
index ef7ed90816f6..a68cd80cc18d 100644
--- a/cmuxTests/VMSSHCommandTests.swift
+++ b/cmuxTests/VMSSHCommandTests.swift
@@ -29,8 +29,8 @@ extension CLINotifyProcessIntegrationRegressionTests {
switch method {
case "vm.attach_info":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["id"] as? String, vmID)
- XCTAssertEqual(params["require_daemon"] as? Bool, true)
+ legacyAssertEqual(params["id"] as? String, vmID)
+ legacyAssertEqual(params["require_daemon"] as? Bool, true)
return self.v2Response(
id: id,
ok: true,
@@ -47,7 +47,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
)
case "workspace.create":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowID)
+ legacyAssertEqual(params["window_id"] as? String, windowID)
return self.v2Response(
id: id,
ok: true,
@@ -94,33 +94,33 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK workspace=\(workspaceRef) target=cmux@gateway.freestyle.sh state=connecting\n")
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK workspace=\(workspaceRef) target=cmux@gateway.freestyle.sh state=connecting\n")
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
let requests = try state.commands.map { line -> [String: Any] in
- let data = try XCTUnwrap(line.data(using: .utf8))
- return try XCTUnwrap(JSONSerialization.jsonObject(with: data, options: []) as? [String: Any])
+ let data = try legacyUnwrap(line.data(using: .utf8))
+ return try legacyUnwrap(JSONSerialization.jsonObject(with: data, options: []) as? [String: Any])
}
- XCTAssertEqual(
+ legacyAssertEqual(
requests.compactMap { $0["method"] as? String },
["vm.attach_info", "workspace.create", "workspace.rename", "workspace.remote.configure", "workspace.select"]
)
- let configureRequest = try XCTUnwrap(
+ let configureRequest = try legacyUnwrap(
requests.first { $0["method"] as? String == "workspace.remote.configure" },
"Expected workspace.remote.configure RPC request"
)
- let configureParams = try XCTUnwrap(configureRequest["params"] as? [String: Any])
- XCTAssertEqual(configureParams["workspace_id"] as? String, workspaceID)
- XCTAssertEqual(configureParams["destination"] as? String, "cmux@gateway.freestyle.sh")
- XCTAssertEqual(configureParams["port"] as? Int, 2222)
- XCTAssertEqual(configureParams["local_socket_path"] as? String, socketPath)
- XCTAssertEqual(configureParams["skip_daemon_bootstrap"] as? Bool, true)
- XCTAssertNotNil(configureParams["terminal_startup_command"] as? String)
- XCTAssertNotNil(configureParams["relay_port"] as? Int)
+ let configureParams = try legacyUnwrap(configureRequest["params"] as? [String: Any])
+ legacyAssertEqual(configureParams["workspace_id"] as? String, workspaceID)
+ legacyAssertEqual(configureParams["destination"] as? String, "cmux@gateway.freestyle.sh")
+ legacyAssertEqual(configureParams["port"] as? Int, 2222)
+ legacyAssertEqual(configureParams["local_socket_path"] as? String, socketPath)
+ legacyAssertEqual(configureParams["skip_daemon_bootstrap"] as? Bool, true)
+ legacyAssertNotNil(configureParams["terminal_startup_command"] as? String)
+ legacyAssertNotNil(configureParams["relay_port"] as? Int)
}
@Test
func testSSHCommandGlobalWindowOverridesCallerEnvironment() throws {
@@ -149,13 +149,13 @@ extension CLINotifyProcessIntegrationRegressionTests {
switch method {
case "window.focus":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowID)
+ legacyAssertEqual(params["window_id"] as? String, windowID)
return self.v2Response(id: id, ok: true, result: ["window_id": windowID])
case "workspace.create":
let params = payload["params"] as? [String: Any] ?? [:]
- XCTAssertEqual(params["window_id"] as? String, windowID)
- XCTAssertNil(params["workspace_id"])
- XCTAssertNil(params["surface_id"])
+ legacyAssertEqual(params["window_id"] as? String, windowID)
+ legacyAssertNil(params["workspace_id"])
+ legacyAssertNil(params["surface_id"])
return self.v2Response(
id: id,
ok: true,
@@ -205,17 +205,17 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertEqual(result.stdout, "OK workspace=\(workspaceRef) target=cmux-macmini state=connecting\n")
- XCTAssertTrue(result.stderr.isEmpty, result.stderr)
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertEqual(result.stdout, "OK workspace=\(workspaceRef) target=cmux-macmini state=connecting\n")
+ legacyAssertTrue(result.stderr.isEmpty, result.stderr)
let requests = try state.commands.map { line -> [String: Any] in
- let data = try XCTUnwrap(line.data(using: .utf8))
- return try XCTUnwrap(JSONSerialization.jsonObject(with: data, options: []) as? [String: Any])
+ let data = try legacyUnwrap(line.data(using: .utf8))
+ return try legacyUnwrap(JSONSerialization.jsonObject(with: data, options: []) as? [String: Any])
}
- XCTAssertEqual(
+ legacyAssertEqual(
requests.compactMap { $0["method"] as? String },
["window.focus", "workspace.create", "workspace.remote.configure"]
)
@@ -269,12 +269,12 @@ extension CLINotifyProcessIntegrationRegressionTests {
timeout: 5
)
- wait(for: [serverHandled], timeout: 5)
- XCTAssertFalse(result.timedOut, result.stderr)
- XCTAssertEqual(result.status, 0, result.stderr)
- XCTAssertTrue(result.stdout.contains("ssh cmux@gateway.freestyle.sh -p 2222"), result.stdout)
- XCTAssertTrue(result.stdout.contains("password: lease-token"), result.stdout)
- XCTAssertEqual(
+ legacyWait(for: [serverHandled], timeout: 5)
+ legacyAssertFalse(result.timedOut, result.stderr)
+ legacyAssertEqual(result.status, 0, result.stderr)
+ legacyAssertTrue(result.stdout.contains("ssh cmux@gateway.freestyle.sh -p 2222"), result.stdout)
+ legacyAssertTrue(result.stdout.contains("password: lease-token"), result.stdout)
+ legacyAssertEqual(
state.commands.compactMap { self.jsonObject($0)?["method"] as? String },
["vm.ssh_info"]
)