Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions cmuxTests/AgentHookDeliveryQueueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,156 @@ struct AgentHookDeliveryQueueTests {
)
}

@Test("Installed Codex lifecycle hooks persist after app-owned queue replay")
func installedCodexHooksPersistThroughDeliveryQueue() async throws {
let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self)
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-codex-installed-queue-persistence-\(UUID().uuidString)", isDirectory: true)
let codexHome = root.appendingPathComponent(".codex", isDirectory: true)
let socketPath = makeCodexHookSocketPath("codex-q")
let listenerFD = try bindCodexHookUnixSocket(at: socketPath)
let commands = CodexHookCapturedSocketCommands()
let workspaceID = "11111111-1111-1111-1111-111111111111"
let surfaceID = "22222222-2222-2222-2222-222222222222"
let sessionID = "issue-13489-session"
let processID = Int(getpid())
let stateURL = root.appendingPathComponent("codex-hook-sessions.json", isDirectory: false)
let transcriptURL = root.appendingPathComponent("rollout-\(sessionID).jsonl", isDirectory: false)

try FileManager.default.createDirectory(at: codexHome, withIntermediateDirectories: true)
try #"{"type":"session_meta","payload":{"id":"\#(sessionID)","source":"cli","originator":"codex-tui"}}"#
.write(to: transcriptURL, atomically: true, encoding: .utf8)
defer {
Darwin.close(listenerFD)
unlink(socketPath)
try? FileManager.default.removeItem(at: root)
}

startCodexHookMockSocketServerAccepting(
listenerFD: listenerFD,
commands: commands,
surfaceId: surfaceID,
connectionLimit: 32,
processBinding: CodexHookMockProcessBinding(
processID: processID,
workspaceID: workspaceID,
surfaceID: surfaceID
)
)

let install = runCodexHookProcess(
executablePath: cliPath,
arguments: ["hooks", "codex", "install", "--yes"],
environment: codexHookTestEnvironment(root: root, codexHome: codexHome),
timeout: 5
)
#expect(!install.timedOut, Comment(rawValue: install.stderr))
#expect(install.status == 0, Comment(rawValue: install.stderr))

let installedHooks = try codexHookEntries(in: codexHome)
let environment: [String: String] = [
"HOME": root.path,
"CODEX_HOME": codexHome.path,
"PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
"PWD": root.path,
"TMPDIR": 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",
"CMUX_BUNDLED_CLI_PATH": cliPath,
"CMUX_CODEX_HOOK_CMUX_BIN": cliPath,
"CMUX_CODEX_PID": String(processID),
]
let deliveryProcess = AgentHookDeliveryProcess(
executableURLProvider: { URL(fileURLWithPath: cliPath) },
processTimeout: .seconds(5),
deliveryTimeout: .seconds(6),
terminationGrace: .milliseconds(100)
)
let queue = AgentHookDeliveryQueue(process: deliveryProcess)

func replayInstalledHook(eventName: String, payload: String) async throws {
let command = try #require(
installedHooks.first { $0.eventName == eventName }?.command
)
let admissionCount = commands.snapshot()
.compactMap(codexHookJSONObject)
.filter { $0["method"] as? String == "agent.hook.enqueue" }
.count

let hook = runCodexHookProcess(
executablePath: "/bin/sh",
arguments: ["-c", command],
environment: environment,
standardInput: payload,
timeout: 3
)
#expect(!hook.timedOut, Comment(rawValue: hook.stderr))
#expect(hook.status == 0, Comment(rawValue: hook.stderr))
#expect(hook.stdout == "{}\n")

let admissions = commands.snapshot()
.compactMap(codexHookJSONObject)
.filter { $0["method"] as? String == "agent.hook.enqueue" }
#expect(admissions.count == admissionCount + 1)
let request = try #require(admissions.last)
let params = try #require(request["params"] as? [String: Any])
let admittedEnvironment = try #require(params["environment"] as? [String: String])
#expect(admittedEnvironment["CMUX_CODEX_PID"] == String(processID))
#expect(admittedEnvironment["CMUX_WORKSPACE_ID"] == workspaceID)
#expect(admittedEnvironment["CMUX_SURFACE_ID"] == surfaceID)
#expect(admittedEnvironment["CMUX_AGENT_HOOK_ROUTE_SNAPSHOT"] == "1")

let event = try #require(AgentHookDeliveryEvent(params: params))
#expect(queue.enqueue(event))
let drained = await Task.detached {
queue.waitForPriorDeliveries(
orderingKey: event.orderingKey,
timeout: 5
)
}.value
#expect(drained, "Queued Codex hook must finish delivery before the regression assertion")
}

func persistedSession() throws -> [String: Any] {
let rootObject = try #require(
JSONSerialization.jsonObject(with: Data(contentsOf: stateURL)) as? [String: Any]
)
let sessions = try #require(rootObject["sessions"] as? [String: Any])
if let record = sessions[sessionID] as? [String: Any] {
return record
}
return try #require(
sessions.values
.compactMap { $0 as? [String: Any] }
.first { $0["sessionId"] as? String == sessionID }
)
}

try await replayInstalledHook(
eventName: "SessionStart",
payload: #"{"session_id":"\#(sessionID)","cwd":"\#(root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"SessionStart"}"#
)
let started = try persistedSession()
#expect(started["workspaceId"] as? String == workspaceID)
#expect(started["surfaceId"] as? String == surfaceID)
#expect((started["pid"] as? NSNumber)?.intValue == processID)
#expect(started["runtimeStatus"] as? String == "running")

try await replayInstalledHook(
eventName: "UserPromptSubmit",
payload: #"{"session_id":"\#(sessionID)","turn_id":"turn-13489","cwd":"\#(root.path)","transcript_path":"\#(transcriptURL.path)","hook_event_name":"UserPromptSubmit","prompt":"verify persistence"}"#
)
let prompted = try persistedSession()
#expect(prompted["workspaceId"] as? String == workspaceID)
#expect(prompted["surfaceId"] as? String == surfaceID)
#expect((prompted["pid"] as? NSNumber)?.intValue == processID)
#expect(prompted["runtimeStatus"] as? String == "running")
#expect(prompted["activePromptTurnIds"] as? [String] == ["turn-13489"])
}

@MainActor
@Test("Relay TTY resolution is scoped to the owning remote workspace")
func relayTTYResolutionUsesOwningWorkspace() throws {
Expand Down
Loading