refactor(schedule): make absolute times explicit
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user