refactor(schedule): make absolute times explicit

This commit is contained in:
Tianyi Cui
2026-08-09 16:30:11 +08:00
parent 3d6498e91b
commit b7ec8429a9
109 changed files with 1248 additions and 3219 deletions

View File

@@ -31,10 +31,7 @@ const sid = (id: string): SessionId => id as SessionId
let nextRpc = 1
function request<P>(payload: P): RpcRequest<P> {
return {
rpcId: RpcId(`cold-${String(nextRpc++)}`),
payload: { timeZone: 'UTC', clientTimeZone: 'UTC', ...payload },
}
return { rpcId: RpcId(`cold-${String(nextRpc++)}`), payload }
}
function header(id: string, createdAt: number, extra: Partial<SessionHeader> = {}): SessionHeader {
@@ -471,6 +468,81 @@ describe('subagent ownership fence', () => {
expect(response.result.ok).toBe(true)
expect(followup).toHaveBeenCalledOnce()
})
it('canonicalizes a supplied browser zone on the exact prompt and rejects invalid names', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
const session = ctx.sessions.create(sid('session-browser-zone'), { meta: { cwd: '/proj' } })
const followup = vi.fn()
const agent = { id: session.id, session, status: 'idle', ctx, followup } as unknown as Agent
ctx.agents.register(agent)
const api = createApiProxy(ctx, {
defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
cwd: '/tmp',
workspaceRoot: '/tmp',
})
const alias = 'US/Pacific'
const canonical = new Intl.DateTimeFormat('en-US', { timeZone: alias })
.resolvedOptions().timeZone
const zonedRequest = request({
sessionId: agent.id,
mode: 'queue' as const,
content: [{ type: 'text' as const, text: 'zoned work' }],
clientTimeZone: alias,
})
await expect(api.sessions.prompt(zonedRequest)).resolves.toMatchObject({
result: { ok: true },
})
expect(followup).toHaveBeenNthCalledWith(1, expect.objectContaining({
source: { kind: 'user', rpcId: zonedRequest.rpcId, clientTimeZone: canonical },
}))
const utcRequest = request({
sessionId: agent.id,
mode: 'queue' as const,
content: [{ type: 'text' as const, text: 'UTC work' }],
clientTimeZone: 'UTC',
})
await expect(api.sessions.prompt(utcRequest)).resolves.toMatchObject({
result: { ok: true },
})
expect(followup).toHaveBeenNthCalledWith(2, expect.objectContaining({
source: { kind: 'user', rpcId: utcRequest.rpcId, clientTimeZone: 'UTC' },
}))
const unzonedRequest = request({
sessionId: agent.id,
mode: 'queue' as const,
content: [{ type: 'text' as const, text: 'headless work' }],
})
await expect(api.sessions.prompt(unzonedRequest)).resolves.toMatchObject({
result: { ok: true },
})
expect(followup).toHaveBeenNthCalledWith(3, expect.objectContaining({
source: { kind: 'user', rpcId: unzonedRequest.rpcId },
}))
for (const clientTimeZone of ['', ' UTC', 'CST', 'Not/A_Real_Zone']) {
const invalid = await api.sessions.prompt(request({
sessionId: agent.id,
mode: 'queue' as const,
content: [{ type: 'text' as const, text: 'invalid zone' }],
clientTimeZone,
}))
expect(invalid.result).toEqual({
ok: false,
error: {
code: 'invalid-time-zone',
message: 'clientTimeZone must be UTC or a valid IANA Area/Location name',
details: { value: clientTimeZone },
},
})
}
expect(followup).toHaveBeenCalledTimes(3)
})
})
describe('degenerate composition (no persistence, no factory)', () => {
@@ -513,89 +585,6 @@ describe('degenerate composition (no persistence, no factory)', () => {
})
})
describe('cold Session zone identity', () => {
it('rejects a different requested zone before resuming a persisted identity', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
const sessionId = sid('session-cold-zone-conflict')
const meta = header('session-cold-zone-conflict', 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 response = await api.sessions.create(request({
sessionId,
cwd: '/proj',
timeZone: 'Asia/Shanghai',
}))
expect(response.result).toMatchObject({
ok: false,
error: {
code: 'session-conflict',
details: {
sessionId,
existingCwd: '/proj',
existingTimeZone: 'UTC',
requestedTimeZone: 'Asia/Shanghai',
},
},
})
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', () => {
it('maps a synchronous send throw (disposed/invalid input) to agent-busy with the reason attached', async () => {
const ctx = new Context()