Skip to content
Merged
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -273,23 +273,32 @@ extension ControlCommandCoordinator {
let afterID = uuid(params, "after_workspace_id")
let dryRun = bool(params, "dry_run") ?? false

let targetCount = (index != nil ? 1 : 0) + (beforeID != nil ? 1 : 0) + (afterID != nil ? 1 : 0)
if targetCount != 1 {
// Count supplied selectors, not resolved identities. An unknown ref
// must neither look like a missing target nor hide a conflicting one.
let targetCount = ["index", "before_workspace_id", "after_workspace_id"]
.filter { hasNonNull(params, $0) }.count
if targetCount != 1 || (hasNonNull(params, "index") && index == nil) {
return .err(
code: "invalid_params",
message: "Specify exactly one target: index, before_workspace_id, or after_workspace_id",
data: nil
)
}

let resolution = context?.controlReorderWorkspace(
routing: routingSelectors(params),
workspaceID: workspaceID,
toIndex: index,
beforeWorkspaceID: beforeID,
afterWorkspaceID: afterID,
dryRun: dryRun
) ?? .notFound
let resolution: ControlWorkspaceReorderResolution
if (hasNonNull(params, "before_workspace_id") && beforeID == nil)
|| (hasNonNull(params, "after_workspace_id") && afterID == nil) {
resolution = .notFound
} else {
resolution = context?.controlReorderWorkspace(
routing: routingSelectors(params),
workspaceID: workspaceID,
toIndex: index,
beforeWorkspaceID: beforeID,
afterWorkspaceID: afterID,
dryRun: dryRun
) ?? .notFound
}
switch resolution {
case .notFound:
return .err(code: "not_found", message: "Workspace not found", data: .object([
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Foundation
import Testing
@testable import CmuxControlSocket

@MainActor
struct ControlWorkspaceReorderTargetTests {
@Test(arguments: ["before_workspace_id", "after_workspace_id"])
func unresolvedRelativeTargetReportsNotFound(key: String) {
let context = FakeWorkspaceControlCommandContext()
let coordinator = ControlCommandCoordinator(context: context)
let result = coordinator.handle(ControlRequest(id: .int(1), method: "workspace.reorder", params: [
"workspace_id": .string(UUID().uuidString),
key: .string("workspace:999999"),
"dry_run": .bool(true)
]))
guard case .err(let code, _, _) = result else {
Issue.record("An unknown relative target must fail")
return
}
#expect(code == "not_found")
#expect(context.reorderCall == nil)
}

@Test(arguments: ["before_workspace_id", "after_workspace_id"])
func unresolvedTargetStillConflictsWithIndex(key: String) {
let context = FakeWorkspaceControlCommandContext()
let coordinator = ControlCommandCoordinator(context: context)
let result = coordinator.handle(ControlRequest(id: .int(1), method: "workspace.reorder", params: [
"workspace_id": .string(UUID().uuidString),
key: .string("workspace:999999"),
"index": .int(0)
]))
guard case .err(let code, _, _) = result else {
Issue.record("Conflicting targets must fail")
return
}
#expect(code == "invalid_params")
#expect(context.reorderCall == nil)
}

@Test(arguments: ["before_workspace_id", "after_workspace_id"], [true, false])
func knownRelativeTargetReachesPlannerWithoutIndex(key: String, dryRun: Bool) throws {
let context = FakeWorkspaceControlCommandContext()
let coordinator = ControlCommandCoordinator(context: context)
let workspaceID = UUID()
let targetID = UUID()
let workspaceRef = coordinator.ensureRef(kind: .workspace, uuid: workspaceID)
let targetRef = coordinator.ensureRef(kind: .workspace, uuid: targetID)
context.reorderResolution = .resolved(
windowID: nil,
plan: ControlWorkspaceReorderPlanItem(workspaceID: workspaceID, fromIndex: 1, toIndex: 0)
)
let result = coordinator.handle(ControlRequest(id: .int(1), method: "workspace.reorder", params: [
"workspace_id": .string(workspaceRef), key: .string(targetRef), "dry_run": .bool(dryRun)
]))
guard case .ok = result else {
Issue.record("A known relative target must succeed")
return
}
let call = try #require(context.reorderCall)
#expect(call.workspaceID == workspaceID)
#expect(call.index == nil)
#expect(call.before == (key == "before_workspace_id" ? targetID : nil))
#expect(call.after == (key == "after_workspace_id" ? targetID : nil))
#expect(call.dryRun == dryRun)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ final class FakeWorkspaceControlCommandContext: ControlCommandContext {
var listResolution: ControlWorkspaceListResolution = .tabManagerUnavailable
var currentResolution: ControlWorkspaceCurrentResolution = .tabManagerUnavailable
var closeResolution: ControlWorkspaceCloseResolution = .tabManagerUnavailable
var reorderResolution: ControlWorkspaceReorderResolution = .notFound
var reorderCall: (workspaceID: UUID, index: Int?, before: UUID?, after: UUID?, dryRun: Bool)?
var addWorkspaceToGroupResolution: ControlWorkspaceGroupAddResolution = .tabManagerUnavailable
var addWorkspaceToGroupCall: (
groupID: UUID,
Expand Down Expand Up @@ -144,6 +146,18 @@ final class FakeWorkspaceControlCommandContext: ControlCommandContext {
return addWorkspaceToGroupResolution
}

func controlReorderWorkspace(
routing: ControlRoutingSelectors,
workspaceID: UUID,
toIndex: Int?,
beforeWorkspaceID: UUID?,
afterWorkspaceID: UUID?,
dryRun: Bool
) -> ControlWorkspaceReorderResolution {
reorderCall = (workspaceID, toIndex, beforeWorkspaceID, afterWorkspaceID, dryRun)
return reorderResolution
}

func controlWorkspaceRemoteTerminalSessionEnd(
workspaceID: UUID, surfaceID: UUID, relayPort: Int?,
terminalLifecycleID: UUID?, sessionID: String?,
Expand Down
Loading