Merge remote-tracking branch 'upstream/master' into feat/produced-files-folder
This commit is contained in:
@@ -468,6 +468,80 @@ 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',
|
||||
})
|
||||
|
||||
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)', () => {
|
||||
|
||||
@@ -50,7 +50,10 @@ function bench(options: {
|
||||
_parent: unknown,
|
||||
_childId: SessionId,
|
||||
_content: unknown,
|
||||
_delivery: { source: { kind: string; rpcId: RpcId }; signal: AbortSignal },
|
||||
_delivery: {
|
||||
source: { kind: string; rpcId: RpcId; clientTimeZone?: string }
|
||||
signal: AbortSignal
|
||||
},
|
||||
) => options.followupError === undefined
|
||||
? Promise.resolve('message-1')
|
||||
: Promise.reject(options.followupError))
|
||||
@@ -270,6 +273,43 @@ describe('subagent gateway', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('canonicalizes browser-zone provenance before delivering a child prompt', async () => {
|
||||
const { api, parent, followup } = bench()
|
||||
const alias = 'US/Pacific'
|
||||
const canonical = new Intl.DateTimeFormat('en-US', { timeZone: alias })
|
||||
.resolvedOptions().timeZone
|
||||
const content = [{ type: 'text' as const, text: 'continue locally' }]
|
||||
const signal = new AbortController().signal
|
||||
await expect(api.subagents.prompt(request({
|
||||
parentSessionId: PARENT,
|
||||
childSessionId: CHILD,
|
||||
mode: 'continuable',
|
||||
content,
|
||||
clientTimeZone: alias,
|
||||
}), signal)).resolves.toMatchObject({ result: { ok: true } })
|
||||
expect(followup).toHaveBeenCalledWith(parent, CHILD, content, {
|
||||
source: { kind: 'user', rpcId: RpcId('subagent-rpc'), clientTimeZone: canonical },
|
||||
signal,
|
||||
})
|
||||
|
||||
const invalid = await api.subagents.prompt(request({
|
||||
parentSessionId: PARENT,
|
||||
childSessionId: CHILD,
|
||||
mode: 'continuable',
|
||||
content,
|
||||
clientTimeZone: 'Not/A_Real_Zone',
|
||||
}), signal)
|
||||
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: 'Not/A_Real_Zone' },
|
||||
},
|
||||
})
|
||||
expect(followup).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('fails before delivery when the parent is absent and maps continuation failures', async () => {
|
||||
const absent = bench({ parentLive: false })
|
||||
expect((await absent.api.subagents.prompt(request({
|
||||
|
||||
@@ -39,6 +39,7 @@ import { hostFrameSchema, muxFrameSchema, askUserQuestionItemSchema } from '../s
|
||||
import { approvalRequestIdSchema, approvalResponsePayloadSchema } from '../src/api/approvals.schema.ts'
|
||||
import { askUserQuestionAnswerSchema, questionResponsePayloadSchema } from '../src/api/questions.schema.ts'
|
||||
import { goalEditRequestSchema } from '../src/api/goals.schema.ts'
|
||||
import { subagentPromptRequestSchema } from '../src/api/subagents.schema.ts'
|
||||
|
||||
describe('RpcId', () => {
|
||||
it('brands a raw string at zero runtime cost', () => {
|
||||
@@ -63,6 +64,7 @@ describe('rpcErrorSchema', () => {
|
||||
expect(rpcErrorSchema.parse({ code: 'cancelled', message: 'm', details: {} }).code).toBe('cancelled')
|
||||
expect(rpcErrorSchema.parse({ code: 'session-not-found', message: 'm', details: { sessionId: 's' } }).code).toBe('session-not-found')
|
||||
expect(rpcErrorSchema.parse({ code: 'session-conflict', message: 'm', details: { sessionId: 's', requestedCwd: '/a', existingCwd: '/b' } }).code).toBe('session-conflict')
|
||||
expect(rpcErrorSchema.parse({ code: 'invalid-time-zone', message: 'm', details: { value: 'CST' } }).code).toBe('invalid-time-zone')
|
||||
expect(rpcErrorSchema.parse({ code: 'workspace-attach-failed', message: 'm', details: { sessionId: 's', workspaceId: 'w' } }).code).toBe('workspace-attach-failed')
|
||||
expect(rpcErrorSchema.parse({ code: 'workspace-not-found', message: 'm', details: { workspaceId: 'w' } }).code).toBe('workspace-not-found')
|
||||
expect(rpcErrorSchema.parse({ code: 'workspace-invalid-path', message: 'm', details: { path: '/x' } }).code).toBe('workspace-invalid-path')
|
||||
@@ -248,8 +250,17 @@ describe('sessions domain schemas', () => {
|
||||
}],
|
||||
failures: [],
|
||||
})).toThrow()
|
||||
const prompt = sessionPromptRequestSchema.parse({ sessionId: 's1', mode: 'queue', content: [{ type: 'text', text: 'hi' }] })
|
||||
const prompt = sessionPromptRequestSchema.parse({
|
||||
sessionId: 's1',
|
||||
mode: 'queue',
|
||||
content: [{ type: 'text', text: 'hi' }],
|
||||
clientTimeZone: 'Asia/Shanghai',
|
||||
})
|
||||
expect(prompt.mode).toBe('queue')
|
||||
expect(prompt.clientTimeZone).toBe('Asia/Shanghai')
|
||||
expect(sessionPromptRequestSchema.parse({
|
||||
sessionId: 's1', mode: 'queue', content: [],
|
||||
}).clientTimeZone).toBeUndefined()
|
||||
expect(() => sessionPromptRequestSchema.parse({ sessionId: 's1', mode: 'inject', content: [] })).toThrow()
|
||||
expect(sessionPromptValueSchema.parse({ accepted: true }).accepted).toBe(true)
|
||||
// The command slot appears only when the prompt dispatched a slash command.
|
||||
@@ -275,6 +286,24 @@ describe('sessions domain schemas', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('subagent domain schemas', () => {
|
||||
it('carries optional request-local browser-zone provenance on prompts', () => {
|
||||
expect(subagentPromptRequestSchema.parse({
|
||||
parentSessionId: 'parent',
|
||||
childSessionId: 'child',
|
||||
mode: 'continuable',
|
||||
content: [{ type: 'text', text: 'continue' }],
|
||||
clientTimeZone: 'Asia/Shanghai',
|
||||
}).clientTimeZone).toBe('Asia/Shanghai')
|
||||
expect(subagentPromptRequestSchema.parse({
|
||||
parentSessionId: 'parent',
|
||||
childSessionId: 'child',
|
||||
mode: 'continuable',
|
||||
content: [],
|
||||
}).clientTimeZone).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('host domain schemas', () => {
|
||||
it('validates describe request/value', () => {
|
||||
expect(hostDescribeRequestSchema.parse({})).toEqual({})
|
||||
|
||||
Reference in New Issue
Block a user