fix(time-context): close request-boundary gaps
This commit is contained in:
@@ -95,7 +95,7 @@ export const sessionSearchValueSchema = z.object({
|
||||
hasMore: z.boolean(),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'session.search'>>>
|
||||
|
||||
/** session.create request payload (at most one of workspaceId / cwd). */
|
||||
/** session.create payload; timeZone stays schema-optional so Host omission returns `invalid-time-zone`. */
|
||||
export const sessionCreateRequestSchema = z.object({
|
||||
workspaceId: workspaceIdSchema.optional(),
|
||||
cwd: z.string().optional(),
|
||||
@@ -247,7 +247,7 @@ export const sessionSelectModelValueSchema = z.object({
|
||||
/** ContentBlock passthrough: core is merge-extensible — the type discriminant envelope is strict, the rest stays wide. */
|
||||
export const contentBlockSchema = z.looseObject({ type: z.string() })
|
||||
|
||||
/** session.prompt request payload. */
|
||||
/** session.prompt payload; clientTimeZone stays schema-optional so Host omission returns `invalid-time-zone`. */
|
||||
export const sessionPromptRequestSchema = z.object({
|
||||
sessionId: sessionIdSchema,
|
||||
mode: z.union([z.literal('queue'), z.literal('steer')]),
|
||||
|
||||
@@ -215,6 +215,7 @@ export interface SessionsApi {
|
||||
workspaceId?: WorkspaceId
|
||||
cwd?: string
|
||||
sessionId?: SessionId
|
||||
/** Required by the Host; optional here so omission returns the stable `invalid-time-zone` RPC error. */
|
||||
timeZone?: string
|
||||
}>):
|
||||
Promise<RpcResponse<{ sessionId: SessionId }>>
|
||||
@@ -300,6 +301,7 @@ export interface SessionsApi {
|
||||
sessionId: SessionId
|
||||
mode: 'queue' | 'steer'
|
||||
content: ContentBlock[]
|
||||
/** Required by the Host; optional here so omission returns the stable `invalid-time-zone` RPC error. */
|
||||
clientTimeZone?: string
|
||||
}>):
|
||||
Promise<RpcResponse<{ accepted: true; command?: { kind: 'success'; text?: string } }>>
|
||||
|
||||
@@ -549,6 +549,51 @@ describe('cold Session zone identity', () => {
|
||||
})
|
||||
expect(resume).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a missing zone', undefined, null],
|
||||
['an invalid zone', 'CST', 'CST'],
|
||||
] as const)('rejects %s before resuming a cold Session', async (_case, clientTimeZone, detailValue) => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
const sessionId = sid('session-cold-prompt-zone')
|
||||
const meta = header('session-cold-prompt-zone', 1000, { timeZone: 'UTC' })
|
||||
ctx.provide('sessionPersistence', {
|
||||
list: () => Promise.resolve([meta]),
|
||||
inspect: () => Promise.resolve({ meta, events: [] as SessionEvent[] }),
|
||||
locate: () => undefined,
|
||||
} as never)
|
||||
const resume = vi.spyOn(ctx.agents, 'resume')
|
||||
const api = createApiProxy(ctx, {
|
||||
defaultTarget: () => ({ provider: 'p', model: 'm' }),
|
||||
cwd: '/tmp',
|
||||
workspaceRoot: '/tmp',
|
||||
})
|
||||
|
||||
const promptRequest = request({
|
||||
sessionId,
|
||||
mode: 'queue' as const,
|
||||
content: [{ type: 'text' as const, text: 'rejected before resume' }],
|
||||
clientTimeZone: clientTimeZone ?? 'UTC',
|
||||
})
|
||||
if (clientTimeZone === undefined) {
|
||||
delete (promptRequest.payload as { clientTimeZone?: string }).clientTimeZone
|
||||
}
|
||||
const response = await api.sessions.prompt(promptRequest)
|
||||
|
||||
expect(response.result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'invalid-time-zone',
|
||||
details: { field: 'clientTimeZone', value: detailValue },
|
||||
},
|
||||
})
|
||||
expect(resume).not.toHaveBeenCalled()
|
||||
expect(ctx.agents.get(sessionId)).toBeUndefined()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
describe('sessions.prompt synchronous rejection', () => {
|
||||
|
||||
Reference in New Issue
Block a user