test(ci): cover the owned promotion paths

An agent-attributed timeout promotion fences the job to the calling
session in both shell tools, and the pwsh promoted job's cancel hook is
exercised through a registry kill.
This commit is contained in:
Yichen Jiang committed 2026-09-01 20:53:32 +08:00
1 parent da48f65160
commit 592dfd8854
2 files changed
+75

No files matched your search

@@ -217,6 +217,31 @@ describe('foreground timeout promotion', () => {
await until(() => ctx.jobs.get(job!.id).status === 'killed' ? true : undefined)
})
it('promotes under the calling agent so the job is fenced to its session', async () => {
const ctx = await setup()
const owner = {
id: SessionId('promote-owner'),
session: { id: SessionId('promote-owner'), header: { cwd: process.cwd() } },
status: 'idle',
ctx,
} as unknown as Agent
ctx.agents.register(owner)
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: ToolCallId('observe-promote-owned'),
name: 'bash',
arguments: { command: 'printf "held\\n"; sleep 30', description: 'test command', timeoutMs: 250 },
agent: owner,
})
expect(text(result)).toContain('moved to background job')
const job = ctx.jobs.list(owner)[0]
expect(job).toBeDefined()
expect(job!.ownerSession).toBe(owner.id)
expect(() => ctx.jobs.readRecord(job!.id, 0)).toThrow(/belongs to another session/)
expect(ctx.jobs.kill(job!.id, owner, { reason: 'test cleanup' })).toBe('requested')
await until(() => ctx.jobs.get(job!.id, owner).status === 'killed' ? true : undefined)
})
it('falls back to the timeout kill when the job admission refuses the promotion', async () => {
const ctx = await setup()
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
@@ -275,6 +275,56 @@ describe('foreground timeout promotion (pwsh)', () => {
await until(() => ctx.jobs.get(job!.id).status === 'completed' ? true : undefined)
})
it('promotes under the calling agent and stops the promoted job through the registry kill', async () => {
const { ctx, pwsh } = await setup()
const owner = {
id: SessionId('pwsh-promote-owner'),
session: { id: SessionId('pwsh-promote-owner'), header: { cwd: process.cwd() } },
status: 'idle',
ctx,
} as unknown as Agent
ctx.agents.register(owner)
let killed = false
let resolveDone: () => void = () => {}
const proc = {
status: 'running',
exitCode: null,
signal: null,
done: new Promise<void>((resolve) => { resolveDone = resolve }),
readOutput: () => ({ delta: '', lossy: false }),
kill: () => {
killed = true
proc.status = 'killed'
proc.signal = 'SIGTERM'
resolveDone()
return true
},
promotion: Promise.resolve({ accepted: false, accept() { this.accepted = true }, decline() {} }),
result: () => Promise.reject(new Error('result projection unused after promotion')),
} as unknown as ShellExecution & { status: string; signal: string | null }
pwsh.foregroundHandler = () => proc
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: ToolCallId('pwsh-observe-promote-owned'),
name: 'pwsh',
arguments: { command: 'Get-Slow', description: 'test command', timeoutMs: 250 },
agent: owner,
})
expect((result.content[0] as { text: string }).text).toContain('moved to background job')
const job = ctx.jobs.list(owner)[0]
expect(job).toBeDefined()
expect(job!.ownerSession).toBe(owner.id)
expect(ctx.jobs.kill(job!.id, owner, { reason: 'test cleanup' })).toBe('requested')
await until(() => killed ? true : undefined)
const settled = await until(() => {
const snapshot = ctx.jobs.get(job!.id, owner)
return snapshot.status === 'killed' ? snapshot : undefined
})
expect(settled.detail).toBe('signal: SIGTERM; test cleanup')
})
it('declines the offer and reports the timeout when no job controller serves the owner', async () => {
// The same composition minus dsh-tool-jobs: the registry exists, so the
// deadline still offers, but admission refuses and the tool falls back.