mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(approvals): the list leaves a dialog too new to judge
`GET /api/approvals` re-reads the pane of every pending dialog and resolves one whose frame shows no dialog, either because its options vanished or because the frame shows a turn running. `notePrompt` deliberately waits 3 s before making that same judgement, since the Notification hook can land before Ink paints the dialog and the working line is still on screen then. The list did not wait. That rarely mattered while it ran once per page load, but a client that polls it would resolve a live prompt as answered in the terminal, clearing the red alert on every device and the push Approve button with it. `sweepForList` skips a dialog younger than the delay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e9b2ecd552
commit
324aa75896
3 files changed
+36
-3
No files matched your search
@@ -7,7 +7,7 @@
|
||||
* The routes read the process-wide `approvalInbox` singleton, so every test
|
||||
* drains it in afterEach; a leaked pending item would bleed into the next test.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import Fastify, { type FastifyInstance } from 'fastify';
|
||||
import fastifyCookie from '@fastify/cookie';
|
||||
import { registerApprovalRoutes } from '../../src/web/routes/approval-routes.js';
|
||||
@@ -285,6 +285,10 @@ describe('approval routes', () => {
|
||||
});
|
||||
|
||||
describe('staleness sweep on GET /api/approvals', () => {
|
||||
/** Past the delay `notePrompt` waits before it judges a new dialog. */
|
||||
const pastTheDelay = () => vi.spyOn(Date, 'now').mockReturnValue(Date.now() + 3_100);
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('resolves an item whose dialog left the pane, and tells the other clients', async () => {
|
||||
const resolved: Array<Record<string, unknown>> = [];
|
||||
await postHook(harness, 'permission_prompt', { tool_name: 'Bash' });
|
||||
@@ -294,11 +298,24 @@ describe('approval routes', () => {
|
||||
// the pane knows. The dialog is gone from the frame the next capture sees.
|
||||
approvalInbox.onResolved = (info) => resolved.push({ ...info });
|
||||
session.terminalBuffer = 'claude> back at the composer';
|
||||
pastTheDelay();
|
||||
|
||||
expect(await listApprovals(harness)).toHaveLength(0);
|
||||
expect(resolved).toEqual([expect.objectContaining({ resolution: 'resolved_in_terminal' })]);
|
||||
});
|
||||
|
||||
it('leaves a dialog too new to judge, however often it is listed', async () => {
|
||||
// The hook beat Ink's paint: the frame still shows the turn, which alone
|
||||
// would count as proof that no dialog is up.
|
||||
session.terminalBuffer = '✻ Actualizing… (13s · esc to interrupt)';
|
||||
await postHook(harness, 'permission_prompt', { tool_name: 'Bash' });
|
||||
expect(await listApprovals(harness)).toHaveLength(1);
|
||||
expect(await listApprovals(harness)).toHaveLength(1);
|
||||
|
||||
pastTheDelay();
|
||||
expect(await listApprovals(harness)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('keeps an item whose dialog is still on screen', async () => {
|
||||
await postHook(harness, 'permission_prompt', { tool_name: 'Bash' });
|
||||
// Pane unchanged (PERMISSION_DIALOG): the human has not answered yet.
|
||||
|
||||
Reference in new issue
Block a user