P4-B: notifications Web Push (VAPID) sur passage en waiting
- db: migration id=4 `push_subscriptions` (liées au token d'auth) ; clés VAPID en settings - PushService: bootstrap idempotent des clés VAPID, subscribe/unsubscribe/count, notify() best-effort (purge des abonnements 410/404 Gone) ; sender injectable pour les tests - routes/push.ts: GET vapid-public-key, POST subscribe/unsubscribe/test (toutes sous auth globale) - pty-manager: déclencheur push sur FRONT MONTANT vers waiting, débouncé 1500ms et annulable (faux positif ignoré) ; câblage app/index/config (--vapid-contact) - shared/api.ts: types VapidKeyResponse / PushSubscribeRequest / PushUnsubscribeRequest - deps: web-push (+ @types/web-push) côté serveur - tests: push-service (idempotence, UPSERT, 410-purge, payload) + trigger pty-manager (169 verts) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import { FLOW, REPLAY_TAIL_BYTES, type SessionSummary } from '@arboretum/shared';
|
||||
import { PtyManager, type ClientBinding } from '../src/core/pty-manager.js';
|
||||
import type { PushPayload, PushService } from '../src/core/push-service.js';
|
||||
import { openDb, type Db } from '../src/db/index.js';
|
||||
|
||||
// resolveClaudeBin() fait `which claude` : on le stub pour ne pas dépendre d'un claude réel en PATH.
|
||||
@@ -344,6 +345,45 @@ describe('PtyManager (pty mocké)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('push trigger (P4-B)', () => {
|
||||
it('une notif sur le front montant busy→waiting, après le debounce ; pas de notif en restant busy', async () => {
|
||||
const sessDir = mkdtempSync(join(tmpdir(), 'arb-push-'));
|
||||
const notifies: PushPayload[] = [];
|
||||
const fakePush = {
|
||||
notify: async (p: PushPayload) => {
|
||||
notifies.push(p);
|
||||
},
|
||||
} as unknown as PushService;
|
||||
const m = new PtyManager(db, sessDir, fakePush);
|
||||
try {
|
||||
const summary = m.spawn({ cwd, command: 'claude' });
|
||||
const p = lastPty();
|
||||
const regFile = join(sessDir, `${p.pid}.json`);
|
||||
const writeReg = (status: string, waitingFor?: string): void =>
|
||||
writeFileSync(regFile, JSON.stringify({ pid: p.pid, procStart: '1', sessionId: 'sid', cwd, status, ...(waitingFor ? { waitingFor } : {}) }));
|
||||
|
||||
// busy : front montant vers busy, pas de notif
|
||||
writeReg('busy');
|
||||
p.emitData('working…');
|
||||
await sleep(260);
|
||||
expect(notifies).toHaveLength(0);
|
||||
|
||||
// waiting + écran permission : front montant vers waiting → planifie la notif (debounce 1500ms)
|
||||
writeReg('waiting', 'permission prompt');
|
||||
p.emitData('\x1b[2J\x1b[HDo you want to create x.txt?\r\n❯ 1. Yes\r\n2. No\r\n');
|
||||
await sleep(260);
|
||||
expect(notifies).toHaveLength(0); // pas encore : debounce en cours
|
||||
|
||||
await sleep(1500); // dépasse le debounce
|
||||
expect(notifies).toHaveLength(1);
|
||||
expect(notifies[0]).toMatchObject({ sessionId: summary.id, kind: 'permission', url: `/sessions/${summary.id}` });
|
||||
} finally {
|
||||
m.shutdown();
|
||||
rmSync(sessDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('flow control', () => {
|
||||
it('pause() UNIQUEMENT quand tous les interactifs dépassent HIGH ; resume() quand le min repasse sous LOW', () => {
|
||||
const { summary, pty } = spawnBash();
|
||||
|
||||
Reference in New Issue
Block a user