mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
fix(agent-hooks): keep Antigravity status posts alive when an event carries no payload
The PowerShell post substituted `{}` for empty stdin, and the POSIX script does
the same before posting. curl forwards stdin verbatim, so a payload-less event
now arrives as `payload=""` and the shared ingest dropped it — losing the status
transition its `hook_event_name` still carried.
Accept an empty Antigravity payload as `{}` at the ingest boundary, scoped to
that source so every other agent keeps rejecting a body it cannot parse.
This commit is contained in:
@@ -96,7 +96,11 @@ async function startHookListener(): Promise<{
|
||||
|
||||
type HookRun = { exitCode: number | null; stdout: string; stderr: string; timedOut: boolean }
|
||||
|
||||
function runWrapper(wrapperPath: string, env: NodeJS.ProcessEnv): Promise<HookRun> {
|
||||
function runWrapper(
|
||||
wrapperPath: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
stdinPayload: string = PAYLOAD
|
||||
): Promise<HookRun> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Why: mirror how Antigravity spawns the hook — `cmd /c <bare .cmd path>`, the exact
|
||||
// chain in the bug report's process trace.
|
||||
@@ -127,7 +131,7 @@ function runWrapper(wrapperPath: string, env: NodeJS.ProcessEnv): Promise<HookRu
|
||||
// Why: curl exits once the POST is written; give the listener a beat to finish reading it.
|
||||
setTimeout(() => resolve({ exitCode, stdout, stderr, timedOut }), 250)
|
||||
})
|
||||
child.stdin.end(Buffer.from(PAYLOAD, 'utf8'))
|
||||
child.stdin.end(Buffer.from(stdinPayload, 'utf8'))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -227,6 +231,35 @@ describe.skipIf(process.platform !== 'win32')('Antigravity Windows hook payload
|
||||
// Why: five wrapper launches plus a real install can overrun the default under load.
|
||||
}, 60_000)
|
||||
|
||||
// Why (#15117): Antigravity fires some events with no stdin at all. PowerShell substituted
|
||||
// `{}` before posting; curl forwards the empty body, so prove the post still happens — the
|
||||
// listener's matching allowance is covered in agent-hook-listener-antigravity.test.ts.
|
||||
it('still posts a status event when the agent supplies no payload', async () => {
|
||||
home = mkdtempSync(join(tmpdir(), 'orca-antigravity-hook-'))
|
||||
homedirMock.mockReturnValue(home)
|
||||
expect(new AntigravityHookService().install().state).toBe('installed')
|
||||
|
||||
const listener = await startHookListener()
|
||||
server = listener.server
|
||||
const result = await runWrapper(
|
||||
join(home, '.orca', 'agent-hooks', 'antigravity-pre-invocation.cmd'),
|
||||
hookEnvironment({
|
||||
USERPROFILE: home,
|
||||
HOME: home,
|
||||
ORCA_AGENT_HOOK_PORT: String(listener.port),
|
||||
ORCA_AGENT_HOOK_TOKEN: HOOK_TOKEN,
|
||||
ORCA_PANE_KEY: PANE_KEY
|
||||
}),
|
||||
''
|
||||
)
|
||||
|
||||
expect(result.timedOut).toBe(false)
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(listener.posts).toHaveLength(1)
|
||||
expect(listener.posts[0].payload).toBe('')
|
||||
expect(listener.posts[0].hookEventName).toBe('PreInvocation')
|
||||
}, 30_000)
|
||||
|
||||
it('exits without reading stdin when the pane env is missing', async () => {
|
||||
home = mkdtempSync(join(tmpdir(), 'orca-antigravity-hook-'))
|
||||
homedirMock.mockReturnValue(home)
|
||||
|
||||
@@ -23,6 +23,57 @@ describe('shared agent-hook-listener', () => {
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
// Why (#15117): the Windows script posts stdin verbatim through curl, so an event that
|
||||
// fires without a payload arrives as `payload=""` rather than the `{}` the POSIX script
|
||||
// substitutes. Both transports must still produce the status transition.
|
||||
it('accepts an Antigravity event whose payload arrived empty', () => {
|
||||
const started = normalizeHookPayload(
|
||||
state,
|
||||
'antigravity',
|
||||
{
|
||||
paneKey: PANE_KEY,
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'wt',
|
||||
hook_event_name: 'PreInvocation',
|
||||
payload: ''
|
||||
},
|
||||
'production'
|
||||
)
|
||||
expect(started?.payload).toMatchObject({ state: 'working', agentType: 'antigravity' })
|
||||
})
|
||||
|
||||
it('still drops an Antigravity event whose payload is malformed rather than empty', () => {
|
||||
expect(
|
||||
normalizeHookPayload(
|
||||
state,
|
||||
'antigravity',
|
||||
{
|
||||
paneKey: PANE_KEY,
|
||||
hook_event_name: 'PreInvocation',
|
||||
payload: '{"prompt":'
|
||||
},
|
||||
'production'
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
// Why: the empty-payload allowance is Antigravity's documented policy, not a global one —
|
||||
// every other source must keep rejecting a body it cannot parse.
|
||||
it('does not extend the empty-payload allowance to other sources', () => {
|
||||
expect(
|
||||
normalizeHookPayload(
|
||||
state,
|
||||
'gemini',
|
||||
{
|
||||
paneKey: PANE_KEY,
|
||||
hook_event_name: 'BeforeAgent',
|
||||
payload: ''
|
||||
},
|
||||
'production'
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('normalizes Antigravity invocation and tool hooks', () => {
|
||||
const started = normalizeHookPayload(
|
||||
state,
|
||||
|
||||
@@ -4369,6 +4369,13 @@ export function normalizeHookPayload(
|
||||
const hookPayload =
|
||||
typeof rawPayload === 'string'
|
||||
? (() => {
|
||||
// Why (#15117): some Antigravity events fire with no stdin at all, yet still carry a
|
||||
// status transition in `hook_event_name`. The POSIX script maps empty input to `{}`
|
||||
// before posting; the Windows script pipes stdin straight into curl and cannot, so
|
||||
// accept the empty body here instead of dropping the event.
|
||||
if (source === 'antigravity' && rawPayload.trim() === '') {
|
||||
return {}
|
||||
}
|
||||
try {
|
||||
return parseAgentHookJson(rawPayload)
|
||||
} catch {
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('wsl.exe mode separator', () => {
|
||||
it('sees a `--` separator split across lines by the formatter', () => {
|
||||
// Why: the guard once matched line-by-line and was blind to this exact shape,
|
||||
// which is how every multi-element argv array in this repo is formatted.
|
||||
const formatted = ["args: [", " '-d',", " distro,", " '--',", " 'bash'", "]"].join('\n')
|
||||
const formatted = ['args: [', " '-d',", ' distro,', " '--',", " 'bash'", ']'].join('\n')
|
||||
|
||||
expect(ARGV_FORM.test(codeText(formatted))).toBe(true)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user