fix(tmux-context): contain executor rejection as a warning, correct suppression claim

Review round on #758.

bash.run() only promises to resolve for nonzero exits, timeouts, and aborts,
and bash.resolve() can reject on policy grounds, so either could escape the
serial agent/step listener and abort the model turn — contradicting the
plugin's documented failed-query no-op contract. Contain both and log a
warning instead; the location is optional context.

The Agent Note claimed an unchanged location suppresses the query. It does
not: only the interval floor is checked before the query, while change
suppression compares state the query returned. Corrected in both languages
and re-recorded the i18n pairs.
This commit is contained in:
Turtle
2026-07-29 11:11:17 +08:00
parent f15ff737cd
commit 44a657c149
9 changed files with 73 additions and 17 deletions

View File

@@ -51,8 +51,10 @@ class FakeBash extends BashExecutor {
commands: string[] = []
result: BashRunResult = runResult(`${tmuxLine()}\n`)
runError?: Error
resolveError?: Error
override resolve(request: BashExecRequest): BashExecSpec {
if (this.resolveError) throw this.resolveError
return {
command: request.command,
workdir: request.workdir ?? '/work',
@@ -325,6 +327,46 @@ describe('tmux-context no-op paths', () => {
expect(contextTexts(session)).toHaveLength(0)
})
it('warns and injects nothing when the executor rejects the run', async () => {
const { ctx, bash } = await mount({}, true)
bash.runError = new Error('bash executor unavailable')
const warn = vi.spyOn(ctx.logger, 'warn')
const session = new Session(SessionId('run-rejected'))
openMessageTurn(session, 1)
await fire(ctx, sessionAgent(session), 1, 1)
expect(contextTexts(session)).toHaveLength(0)
expect(warn).toHaveBeenCalledWith(expect.stringContaining('bash executor unavailable'))
})
it('warns and injects nothing when the executor rejects the command at resolve', async () => {
const { ctx, bash } = await mount({}, true)
bash.resolveError = new Error('command denied by policy')
const warn = vi.spyOn(ctx.logger, 'warn')
const session = new Session(SessionId('resolve-rejected'))
openMessageTurn(session, 1)
await fire(ctx, sessionAgent(session), 1, 1)
expect(contextTexts(session)).toHaveLength(0)
expect(warn).toHaveBeenCalledWith(expect.stringContaining('command denied by policy'))
})
it('reports a non-Error rejection in the warning', async () => {
const { ctx, bash } = await mount({}, true)
// Non-Error throw: the executor seam is typed, but a bad impl can reject with anything.
bash.runError = 'spawn refused' as unknown as Error
const warn = vi.spyOn(ctx.logger, 'warn')
const session = new Session(SessionId('non-error-rejection'))
openMessageTurn(session, 1)
await fire(ctx, sessionAgent(session), 1, 1)
expect(contextTexts(session)).toHaveLength(0)
expect(warn).toHaveBeenCalledWith(expect.stringContaining('spawn refused'))
})
it('skips an already-aborted step and runs before ordinary agent/step listeners', async () => {
const { ctx } = await mount({}, true)
const session = new Session(SessionId('ordering'))