feat(workspace): Axis 2 Phase B — client→server workspace handshake
Wires the workspace handshake end to end so a session's workspace is bound
from the client's cwd at connect time and is event-sourced for replay.
- Go TUI sends Hello{workingDir=os.Getwd()} as the first WS frame on connect
(protocol.go encoder + client.go connect path; golden test pins the wire
format against the Kotlin discriminator).
- Server adds ClientMessage.Hello, stashes the per-connection workingDir, and
on session start resolves it through WorkspaceResolver's trust pipeline,
emits SessionWorkspaceBoundEvent (invariant #9), and threads the resolved
workspace into OrchestrationConfig for the live run. A Hello after the first
StartSession is ignored (warn); a rejected path binds the resolver fallback.
- Replay derives the workspace from the recorded event: SessionState gains
boundWorkspace, DefaultSessionReducer fills it from SessionWorkspaceBoundEvent,
and ReplayOrchestrator uses it (Path.of only, no filesystem re-query —
invariant #8) with graceful fallback to config for pre-Phase-B logs.
Absent Hello / null resolver degrades to the prior config-workspace behavior.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||
import com.correx.core.events.events.SessionWorkspaceBoundEvent
|
||||
import com.correx.core.events.events.StageCompletedEvent
|
||||
import com.correx.core.events.events.StageFailedEvent
|
||||
import com.correx.core.events.events.TransitionExecutedEvent
|
||||
@@ -9,6 +10,7 @@ import com.correx.core.events.events.WorkflowStartedEvent
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.events.types.TransitionId
|
||||
import com.correx.core.sessions.BoundWorkspace
|
||||
import com.correx.core.sessions.DefaultSessionReducer
|
||||
import com.correx.core.sessions.SessionState
|
||||
import com.correx.core.sessions.SessionStatus
|
||||
@@ -227,6 +229,55 @@ class DefaultSessionReducerTest {
|
||||
assertEquals(timestamp, result.updatedAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SessionWorkspaceBoundEvent sets boundWorkspace in state`() {
|
||||
val result = reducer.reduce(
|
||||
state = initialState(),
|
||||
event = stored(
|
||||
sessionId = sessionId,
|
||||
payload = SessionWorkspaceBoundEvent(
|
||||
sessionId = sessionId,
|
||||
workspaceRoot = "/home/user/project",
|
||||
allowedPaths = listOf("/home/user/project"),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
BoundWorkspace(
|
||||
workspaceRoot = "/home/user/project",
|
||||
allowedPaths = listOf("/home/user/project"),
|
||||
),
|
||||
result.boundWorkspace,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `boundWorkspace is preserved by subsequent unrelated events`() {
|
||||
val withWorkspace = initialState().copy(
|
||||
boundWorkspace = BoundWorkspace(
|
||||
workspaceRoot = "/home/user/project",
|
||||
allowedPaths = listOf("/home/user/project"),
|
||||
)
|
||||
)
|
||||
|
||||
val result = reducer.reduce(
|
||||
state = withWorkspace,
|
||||
event = stored(
|
||||
sessionId = sessionId,
|
||||
payload = WorkflowStartedEvent(sessionId, workflowId = "wf", startStageId = StageId("s1"))
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
BoundWorkspace(
|
||||
workspaceRoot = "/home/user/project",
|
||||
allowedPaths = listOf("/home/user/project"),
|
||||
),
|
||||
result.boundWorkspace,
|
||||
)
|
||||
}
|
||||
|
||||
private fun initialState() =
|
||||
SessionState(
|
||||
status = SessionStatus.CREATED
|
||||
|
||||
Reference in New Issue
Block a user