fix(time-context): close request-boundary gaps

This commit is contained in:
pku-xht
2026-08-07 21:55:18 +08:00
committed by Tianyi Cui
parent d8a85dcafd
commit 331b29d779
9 changed files with 78 additions and 5 deletions

View File

@@ -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')]),

View File

@@ -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 } }>>

View File

@@ -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', () => {