feat(v0.3): H2 server — Claude Code hooks → live status side-channel
- types: ClaudeStatus + ServerMessage 'status'; Session.claudeStatus; SessionManager.handleHookEvent - session: inject WEBTERM_SESSION + WEBTERM_HOOK_URL into the spawned shell env so hooks know which tab they belong to - http/hook.ts: pure event→status mapper (PreToolUse→working, PermissionRequest/ Notification:permission_prompt→waiting, Stop/idle_prompt→idle); 6 unit tests - server: POST /hook (loopback-only, express.json 64kb) → manager pushes 'status' - fix: closeIdleConnections() on shutdown so an idle hook keep-alive socket doesn't hang close()/SIGTERM - integration ⑦ (real PTY): attach → POST /hook → ws receives status. 205 tests green.
This commit is contained in:
@@ -545,4 +545,48 @@ describe('startServer — integration', () => {
|
||||
await waitForClose(ws2, 3_000).catch(() => undefined)
|
||||
},
|
||||
)
|
||||
|
||||
// ── ⑦ Hook side-channel → status push (H2, needs real PTY — sandbox-off) ───
|
||||
itPty(
|
||||
'⑦ [needs real PTY (sandbox-off)] POST /hook pushes a status frame to the attached ws (H2)',
|
||||
async () => {
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${port}${cfg.wsPath}`, {
|
||||
headers: { Origin: `http://127.0.0.1:${port}` },
|
||||
})
|
||||
await waitForOpen(ws, 3_000)
|
||||
ws.send(JSON.stringify({ type: 'attach', sessionId: null }))
|
||||
const attached = (await waitForMessage(
|
||||
ws,
|
||||
(m) =>
|
||||
typeof m === 'object' && m !== null && (m as Record<string, unknown>)['type'] === 'attached',
|
||||
5_000,
|
||||
)) as Record<string, unknown>
|
||||
const sessionId = attached['sessionId'] as string
|
||||
|
||||
// Arm the status listener BEFORE firing the hook — the server pushes the
|
||||
// status frame synchronously (before the POST returns 204), so attaching
|
||||
// the listener after the fetch would race past it.
|
||||
const statusPromise = waitForMessage(
|
||||
ws,
|
||||
(m) =>
|
||||
typeof m === 'object' && m !== null && (m as Record<string, unknown>)['type'] === 'status',
|
||||
5_000,
|
||||
)
|
||||
|
||||
// Simulate a Claude Code PermissionRequest hook firing for this session.
|
||||
const res = await fetch(`http://127.0.0.1:${port}/hook`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Webterm-Session': sessionId },
|
||||
body: JSON.stringify({ hook_event_name: 'PermissionRequest', tool_name: 'Bash' }),
|
||||
})
|
||||
expect(res.status).toBe(204)
|
||||
|
||||
const status = (await statusPromise) as Record<string, unknown>
|
||||
expect(status['status']).toBe('waiting')
|
||||
expect(status['detail']).toBe('Bash')
|
||||
|
||||
ws.close()
|
||||
await waitForClose(ws, 3_000).catch(() => undefined)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user