fix(web): fence plan mode projection races
This commit is contained in:
@@ -10,7 +10,7 @@ The layering/protocol decisions are recorded in the [GUI layering and RPC protoc
|
||||
|
||||
The mux stream projects the latest log-backed title as a validated `session/title` control frame after each attached-session subscription baseline and immediately after the corresponding live raw title event. This projection does not add titles to `session.list`; cold sessions remain metadata-only there until opening or resuming attaches their logs.
|
||||
|
||||
Plan mode uses two unary methods instead of deriving current state from a history page: `session.planMode` returns the committed state plus any boundary-pending selection, and `session.setPlanMode` records a selection and returns the same authoritative shape. Both return `null` when the optional host service is absent; `null` is capability absence, while `{ active: false }` is a supported inactive session. Committed changes still arrive through the raw logged `plan/mode` session event.
|
||||
Plan mode uses two unary methods instead of deriving current state from a history page: `session.planMode` returns the committed state plus any boundary-pending selection, and `session.setPlanMode` records a selection and returns the same authoritative shape. A present `pending` target must differ from `active`; a net-zero service cleanup intent projects as `{ active }`, and the wire schema rejects equal values. Both methods return `null` when the optional host service is absent; `null` is capability absence, while `{ active: false }` is a supported inactive session. Committed changes still arrive through the raw logged `plan/mode` session event.
|
||||
|
||||
## Carrier layer (`/client` + root)
|
||||
|
||||
|
||||
@@ -113,7 +113,10 @@ export const sessionCancelValueSchema = z.object({
|
||||
export const planModeStateSchema = z.object({
|
||||
active: z.boolean(),
|
||||
pending: z.boolean().optional(),
|
||||
}) satisfies z.ZodType<Wire<PlanModeState>>
|
||||
}).refine(
|
||||
state => state.pending === undefined || state.pending !== state.active,
|
||||
{ message: 'pending must differ from active when present', path: ['pending'] },
|
||||
) satisfies z.ZodType<Wire<PlanModeState>>
|
||||
|
||||
/** session.planMode request payload. */
|
||||
export const sessionPlanModeRequestSchema = z.object({
|
||||
|
||||
@@ -47,7 +47,7 @@ export interface SessionSummary {
|
||||
/**
|
||||
* Plan collaboration state exposed to clients. `active` is the logged state
|
||||
* shaping the current request; `pending`, when present, is the user's
|
||||
* next-boundary selection.
|
||||
* next-boundary selection and differs from `active`.
|
||||
*/
|
||||
export interface PlanModeState {
|
||||
active: boolean
|
||||
|
||||
@@ -114,6 +114,8 @@ describe('sessions domain schemas', () => {
|
||||
expect(sessionSetPlanModeValueSchema.parse({ active: true })).toEqual({ active: true })
|
||||
expect(() => sessionSetPlanModeRequestSchema.parse({ sessionId: 's1', active: 'yes' })).toThrow()
|
||||
expect(() => sessionPlanModeValueSchema.parse({ active: 'yes' })).toThrow()
|
||||
expect(() => sessionPlanModeValueSchema.parse({ active: false, pending: false })).toThrow()
|
||||
expect(() => sessionSetPlanModeValueSchema.parse({ active: true, pending: true })).toThrow()
|
||||
expect(contentBlockSchema.parse({ type: 'text', text: 'x', extra: 1 })).toMatchObject({ extra: 1 })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user