fix(agent): bound push suppression to observed browser attention
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE "AgentPushPresence" ADD COLUMN "inactiveUntil" DATETIME;
|
||||
ALTER TABLE "AgentPushCoordinator" ADD COLUMN "unknownSince" DATETIME;
|
||||
ALTER TABLE "AgentPushDelivery" ADD COLUMN "lastSuppressionReason" TEXT;
|
||||
ALTER TABLE "AgentPushDelivery" ADD COLUMN "lastSuppressionClientKind" TEXT;
|
||||
ALTER TABLE "AgentPushDelivery" ADD COLUMN "lastSuppressedAt" DATETIME;
|
||||
@@ -249,6 +249,7 @@ model AgentPushPresence {
|
||||
visible Boolean
|
||||
focused Boolean
|
||||
lastInteractionAt DateTime?
|
||||
inactiveUntil DateTime?
|
||||
expiresAt DateTime
|
||||
@@index([ownerEpoch, expiresAt])
|
||||
}
|
||||
@@ -273,6 +274,7 @@ model AgentPushCoordinator {
|
||||
localPromptAt DateTime?
|
||||
nativeActive Boolean @default(false)
|
||||
nativeUnknown Boolean @default(true)
|
||||
unknownSince DateTime?
|
||||
uncertain Boolean @default(true)
|
||||
lockOwner String?
|
||||
lockUntil DateTime?
|
||||
@@ -296,6 +298,9 @@ model AgentPushDelivery {
|
||||
expiresAt DateTime
|
||||
updatedAt DateTime @updatedAt
|
||||
outcome String?
|
||||
lastSuppressionReason String?
|
||||
lastSuppressionClientKind String?
|
||||
lastSuppressedAt DateTime?
|
||||
@@index([status, dueAt])
|
||||
@@index([installationId, status])
|
||||
@@index([ownerEpoch, sourceId, cursor])
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createHash, createPublicKey, ECDH } from 'node:crypto';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const PUSH_POLICY = Object.freeze({ promptGraceMs: 5_000, debounceMs: 2_000, interactionGraceMs: 30_000,
|
||||
presenceHeartbeatMs: 15_000, presenceLeaseMs: 60_000 });
|
||||
export const PUSH_POLICY = Object.freeze({ promptGraceMs: 5_000, debounceMs: 2_000, interactionGraceMs: 5_000,
|
||||
inactiveGraceMs: 5_000, unknownGraceMs: 5_000, presenceHeartbeatMs: 15_000, presenceLeaseMs: 60_000 });
|
||||
export const MAX_AGE_MS = 5 * 60_000;
|
||||
export const MAX_ATTEMPTS = 4;
|
||||
export const ROOT_OWNER = 'root';
|
||||
|
||||
+69
-20
@@ -15,6 +15,7 @@ type Options = { db: PrismaClient; config: PushConfig; context: (after?: string,
|
||||
broadcast?: (state: ReadState) => void; warmupMs?: number };
|
||||
const date = (ms: number) => new Date(ms);
|
||||
const activeStatuses = ['pending', 'intent'];
|
||||
type Suppression = { reason: string; clientKind: 'web' | 'unknown' | 'runtime_web' | 'runtime_unknown' | 'system' };
|
||||
|
||||
/** Sole coordinator: persisted source checkpoint, installations, presence, claims and outcomes. */
|
||||
export class AgentPushCoordinator {
|
||||
@@ -23,7 +24,7 @@ export class AgentPushCoordinator {
|
||||
private readonly now: () => number;
|
||||
private readonly warmUntil: number;
|
||||
private started = false;
|
||||
private untrackedConnections = new Set<string>();
|
||||
private untrackedConnections = new Map<string, { at: number; kind: 'web' | 'unknown' }>();
|
||||
private running = false;
|
||||
private timer?: ReturnType<typeof setInterval>;
|
||||
private stopped = false;
|
||||
@@ -42,7 +43,7 @@ export class AgentPushCoordinator {
|
||||
await tx.agentPushInstallation.updateMany({ where: { enabled: true }, data: { enabled: false } });
|
||||
await tx.agentPushDelivery.updateMany({ where: { status: { in: activeStatuses } }, data: { status: 'cancelled', outcome: 'owner_changed' } });
|
||||
await tx.agentPushCoordinator.update({ where: { id: 'root' }, data: { ownerEpoch: this.epoch, contextObservedAt: null,
|
||||
nativeUnknown: true, uncertain: true, lockOwner: null, lockUntil: null } });
|
||||
nativeUnknown: true, unknownSince: null, uncertain: true, lockOwner: null, lockUntil: null } });
|
||||
}
|
||||
});
|
||||
this.started = true;
|
||||
@@ -102,22 +103,46 @@ export class AgentPushCoordinator {
|
||||
const old = await tx.agentPushPresence.findUnique({ where: { id } });
|
||||
if (old?.ownerEpoch === this.epoch && value.sequence <= old.sequence) return { expiresAt: old.expiresAt.toISOString() };
|
||||
if (!old && await tx.agentPushPresence.count({ where: { ownerEpoch: this.epoch, expiresAt: { gt: date(now) } } }) >= 512) throw fail(429, 'Too many presence sessions');
|
||||
const data = { ownerEpoch: this.epoch, sequence: value.sequence, visible: value.visibility === 'visible', focused: value.focused,
|
||||
lastInteractionAt: value.interacted ? date(now) : old?.ownerEpoch === this.epoch ? old.lastInteractionAt : null, expiresAt: date(now + PUSH_POLICY.presenceLeaseMs) };
|
||||
const previous = old?.ownerEpoch === this.epoch ? old : null;
|
||||
const visible = value.visibility === 'visible', focused = visible && value.focused;
|
||||
const data = { ownerEpoch: this.epoch, sequence: value.sequence, visible, focused,
|
||||
lastInteractionAt: value.interacted && visible ? date(now) : previous?.lastInteractionAt || null,
|
||||
// Only a transition out of proven focus starts grace. Inactive heartbeats cannot renew it.
|
||||
inactiveUntil: focused ? null : previous?.visible && previous.focused && previous.expiresAt.getTime() > now ? date(now + PUSH_POLICY.inactiveGraceMs) : previous?.inactiveUntil || null,
|
||||
expiresAt: date(now + PUSH_POLICY.presenceLeaseMs) };
|
||||
const row = await tx.agentPushPresence.upsert({ where: { id }, create: { id, ...data }, update: data });
|
||||
return { expiresAt: row.expiresAt.toISOString() };
|
||||
});
|
||||
}
|
||||
async retirePresence(id: string) {
|
||||
// Keep recent interaction's grace even after a best-effort pagehide release.
|
||||
await this.options.db.agentPushPresence.updateMany({ where: { id, ownerEpoch: this.epoch }, data: { visible: false, focused: false } });
|
||||
await this.options.db.$transaction(async tx => {
|
||||
const old = await tx.agentPushPresence.findUnique({ where: { id } });
|
||||
if (old?.ownerEpoch !== this.epoch) return;
|
||||
await tx.agentPushPresence.update({ where: { id }, data: { visible: false, focused: false,
|
||||
inactiveUntil: old.visible && old.focused && old.expiresAt.getTime() > this.now() ? date(this.now() + PUSH_POLICY.inactiveGraceMs) : old.inactiveUntil } });
|
||||
});
|
||||
}
|
||||
async connect(id: string, kind?: string, sessionId?: string) {
|
||||
this.untrackedConnections.add(id);
|
||||
await this.options.db.agentPushConnection.upsert({ where: { id }, create: { id, ownerEpoch: this.epoch, kind: kind || 'unknown', sessionId,
|
||||
expiresAt: date(this.now() + PUSH_POLICY.presenceLeaseMs) }, update: { expiresAt: date(this.now() + PUSH_POLICY.presenceLeaseMs) } });
|
||||
const now = this.now();
|
||||
if (kind !== 'native' && !this.untrackedConnections.has(id)) this.untrackedConnections.set(id, { at: now, kind: kind === 'web' ? 'web' : 'unknown' });
|
||||
await this.initialize();
|
||||
await this.options.db.$transaction(async tx => {
|
||||
const connection = await tx.agentPushConnection.upsert({ where: { id }, create: { id, ownerEpoch: this.epoch, kind: kind || 'unknown', sessionId,
|
||||
expiresAt: date(now + PUSH_POLICY.presenceLeaseMs) }, update: { expiresAt: date(now + PUSH_POLICY.presenceLeaseMs) } });
|
||||
const observed = connection.kind === 'web' && connection.sessionId
|
||||
? await tx.agentPushPresence.findFirst({ where: { id: connection.sessionId, ownerEpoch: this.epoch } }) : null;
|
||||
if (connection.kind !== 'native' && !observed) {
|
||||
// Raw clients have no stable identity. One persisted introduction window per owner
|
||||
// prevents reconnects (including invented UUIDs) from manufacturing attention.
|
||||
await tx.agentPushCoordinator.updateMany({ where: { id: 'root', ownerEpoch: this.epoch, unknownSince: null }, data: { unknownSince: date(now) } });
|
||||
}
|
||||
});
|
||||
this.untrackedConnections.delete(id);
|
||||
}
|
||||
async heartbeat(id: string) {
|
||||
// Transport liveness never creates a client, changes its attribution or proves attention.
|
||||
await this.options.db.agentPushConnection.updateMany({ where: { id, ownerEpoch: this.epoch }, data: { expiresAt: date(this.now() + PUSH_POLICY.presenceLeaseMs) } });
|
||||
}
|
||||
async disconnect(id: string) { this.untrackedConnections.delete(id); await this.options.db.agentPushConnection.deleteMany({ where: { id, ownerEpoch: this.epoch } }); }
|
||||
async notePrompt() {
|
||||
if (!this.options.config.adminToken) return;
|
||||
@@ -238,12 +263,20 @@ export class AgentPushCoordinator {
|
||||
if (read) this.options.broadcast?.(read);
|
||||
return context.hasMore;
|
||||
}
|
||||
private async browserActive(tx: Tx, now: number): Promise<boolean> {
|
||||
if (this.untrackedConnections.size) return true;
|
||||
const presences = await tx.agentPushPresence.findMany({ where: { ownerEpoch: this.epoch, expiresAt: { gt: date(now) } } });
|
||||
if (presences.some(v => (v.visible && v.focused) || (v.lastInteractionAt && v.lastInteractionAt.getTime() + PUSH_POLICY.interactionGraceMs > now))) return true;
|
||||
private async browserSuppression(tx: Tx, now: number): Promise<Suppression | undefined> {
|
||||
const observations = await tx.agentPushPresence.findMany({ where: { ownerEpoch: this.epoch } });
|
||||
const presences = observations.filter(v => v.expiresAt.getTime() > now);
|
||||
if (presences.some(v => v.visible && v.focused)) return { reason: 'foreground_focused', clientKind: 'web' };
|
||||
if (presences.some(v => v.lastInteractionAt && v.lastInteractionAt.getTime() + PUSH_POLICY.interactionGraceMs > now)) return { reason: 'recent_interaction', clientKind: 'web' };
|
||||
if (presences.some(v => v.inactiveUntil && v.inactiveUntil.getTime() > now)) return { reason: 'inactive_grace', clientKind: 'web' };
|
||||
const connections = await tx.agentPushConnection.findMany({ where: { ownerEpoch: this.epoch, expiresAt: { gt: date(now) } } });
|
||||
return connections.some(v => v.kind !== 'native' && (v.kind !== 'web' || !presences.some(p => p.id === v.sessionId)));
|
||||
const unobserved = connections.filter(v => v.kind !== 'native' && (v.kind !== 'web' || !observations.some(p => p.id === v.sessionId)));
|
||||
if (!unobserved.length && !this.untrackedConnections.size) return;
|
||||
const firstUntracked = [...this.untrackedConnections.values()].sort((a, b) => a.at - b.at)[0];
|
||||
await tx.agentPushCoordinator.updateMany({ where: { id: 'root', ownerEpoch: this.epoch, unknownSince: null }, data: { unknownSince: date(firstUntracked?.at ?? now) } });
|
||||
const state = await tx.agentPushCoordinator.findUniqueOrThrow({ where: { id: 'root' } });
|
||||
if (state.unknownSince!.getTime() + PUSH_POLICY.unknownGraceMs > now) return { reason: 'unknown_client_grace',
|
||||
clientKind: unobserved[0]?.kind === 'web' ? 'web' : firstUntracked?.kind || 'unknown' };
|
||||
}
|
||||
private async dispatch(id: string) {
|
||||
const claim = await this.options.db.$transaction(async tx => {
|
||||
@@ -251,6 +284,10 @@ export class AgentPushCoordinator {
|
||||
const state = await tx.agentPushCoordinator.findUniqueOrThrow({ where: { id: 'root' } });
|
||||
const row = await tx.agentPushDelivery.findUnique({ where: { id } });
|
||||
if (!row || row.status !== 'pending' || row.dueAt.getTime() > now) return;
|
||||
const suppress = async ({ reason, clientKind }: Suppression) => {
|
||||
await tx.agentPushDelivery.update({ where: { id }, data: { dueAt: date(now + PUSH_POLICY.debounceMs),
|
||||
lastSuppressionReason: reason, lastSuppressionClientKind: clientKind, lastSuppressedAt: date(now) } });
|
||||
};
|
||||
const installation = await tx.agentPushInstallation.findUnique({ where: { id: row.installationId } });
|
||||
const read = await this.readState(tx);
|
||||
if (!installation?.enabled || installation.ownerEpoch !== this.epoch || installation.version !== row.installationVersion || row.expiresAt.getTime() <= now
|
||||
@@ -258,11 +295,17 @@ export class AgentPushCoordinator {
|
||||
await tx.agentPushDelivery.update({ where: { id }, data: { status: 'cancelled', outcome: 'obsolete' } }); return;
|
||||
}
|
||||
if (!this.options.config.enabled || state.lockOwner !== this.instanceId || !state.lockUntil || state.lockUntil.getTime() <= now
|
||||
|| state.uncertain || !state.contextObservedAt || state.contextObservedAt.getTime() < now - 10_000 || now < this.warmUntil) return;
|
||||
if (row.kind === 'message' && (state.nativeActive || state.nativeUnknown || await this.browserActive(tx, now)
|
||||
|| (state.latestUserMessageAt && state.latestUserMessageAt.getTime() + PUSH_POLICY.promptGraceMs > now)
|
||||
|| (state.localPromptAt && state.localPromptAt.getTime() + PUSH_POLICY.promptGraceMs > now))) {
|
||||
await tx.agentPushDelivery.update({ where: { id }, data: { dueAt: date(now + PUSH_POLICY.debounceMs) } }); return;
|
||||
|| state.uncertain || !state.contextObservedAt || state.contextObservedAt.getTime() < now - 10_000 || now < this.warmUntil) {
|
||||
await suppress({ reason: now < this.warmUntil ? 'startup_warmup' : 'source_or_coordinator_unavailable', clientKind: 'system' }); return;
|
||||
}
|
||||
if (row.kind === 'message') {
|
||||
const attention: Suppression | undefined = state.nativeActive ? { reason: 'runtime_active', clientKind: 'runtime_web' }
|
||||
: state.nativeUnknown ? { reason: 'runtime_unknown', clientKind: 'runtime_unknown' } : await this.browserSuppression(tx, now);
|
||||
if (attention) { await suppress(attention); return; }
|
||||
if ((state.latestUserMessageAt && state.latestUserMessageAt.getTime() + PUSH_POLICY.promptGraceMs > now)
|
||||
|| (state.localPromptAt && state.localPromptAt.getTime() + PUSH_POLICY.promptGraceMs > now)) {
|
||||
await suppress({ reason: 'prompt_grace', clientKind: 'system' }); return;
|
||||
}
|
||||
}
|
||||
if (row.attempts >= MAX_ATTEMPTS) { await tx.agentPushDelivery.update({ where: { id }, data: { status: 'failed', outcome: 'attempt_limit' } }); return; }
|
||||
const registration = parseSafe(InstallationBody, JSON.parse(installation.payload));
|
||||
@@ -312,7 +355,13 @@ export class AgentPushCoordinator {
|
||||
await this.options.db.agentPushDelivery.deleteMany({ where: { status: { notIn: activeStatuses }, updatedAt: { lt: date(this.now() - 7 * 86_400_000) } } });
|
||||
} catch {
|
||||
// An unavailable source, DB, malformed contract or clock uncertainty blocks dispatch without leaking payloads.
|
||||
try { await this.options.db.agentPushCoordinator.updateMany({ where: { id: 'root', lockOwner: this.instanceId }, data: { uncertain: true } }); } catch { /* Retry on next loop. */ }
|
||||
try {
|
||||
await this.options.db.$transaction(async tx => {
|
||||
const held = await tx.agentPushCoordinator.updateMany({ where: { id: 'root', lockOwner: this.instanceId }, data: { uncertain: true } });
|
||||
if (held.count) await tx.agentPushDelivery.updateMany({ where: { ownerEpoch: this.epoch, status: 'pending' }, data: {
|
||||
lastSuppressionReason: 'source_or_coordinator_unavailable', lastSuppressionClientKind: 'system', lastSuppressedAt: date(this.now()) } });
|
||||
});
|
||||
} catch { /* Retry on next loop. */ }
|
||||
} finally {
|
||||
try { await this.options.db.agentPushCoordinator.updateMany({ where: { id: 'root', lockOwner: this.instanceId }, data: { lockOwner: null, lockUntil: null } }); } catch { /* Lease expiry recovers. */ }
|
||||
this.running = false;
|
||||
|
||||
+1
-1
@@ -138,7 +138,7 @@ function attachAgentSocket(app: FastifyInstance, baseUrl: string, apiKey: string
|
||||
};
|
||||
const authTimeout = setTimeout(() => browser.close(4401, 'Authentication required'), 5000);
|
||||
const heartbeat = setInterval(() => { if (!alive) browser.terminate(); else { alive = false; browser.ping(); } }, 25_000);
|
||||
browser.on('pong', () => { alive = true; if (authenticated && env.ADMIN_TOKEN) void push.connect(connectionId).catch(() => {}); });
|
||||
browser.on('pong', () => { alive = true; if (authenticated && env.ADMIN_TOKEN) void push.heartbeat(connectionId).catch(() => {}); });
|
||||
const dispose = () => {
|
||||
if (disposed) return;
|
||||
disposed = true; clearTimeout(authTimeout); clearInterval(heartbeat); cleanup.delete(dispose);
|
||||
|
||||
@@ -70,17 +70,88 @@ test('first source bootstrap never alerts history; later durable candidates debo
|
||||
const restarted = new AgentPushCoordinator({ ...f.manager.options, warmupMs: 0 }); await restarted.tick(); assert.equal(f.sent.length, 1);
|
||||
});
|
||||
|
||||
test('all tabs veto: visible focused idle, recent interaction and unknown connections; leases eventually expire', async () => {
|
||||
const f = await fixture(), tab1 = randomUUID(), tab2 = randomUUID(), unknown = randomUUID();
|
||||
test('all tabs veto while focused; hidden heartbeats and stale sequences cannot extend short transition grace', async () => {
|
||||
const f = await fixture(), tab1 = randomUUID(), tab2 = randomUUID();
|
||||
await f.manager.presence(tab1, { sequence: 1, visibility: 'hidden', focused: false, interacted: false });
|
||||
await f.manager.presence(tab2, { sequence: 1, visibility: 'visible', focused: true, interacted: false });
|
||||
f.candidate(); await f.manager.tick(); f.advance(2_000); await f.manager.tick(); assert.equal(f.sent.length, 0, 'focused but idle still vetoes');
|
||||
let receipt = await prisma.agentPushDelivery.findFirstOrThrow();
|
||||
assert.equal(receipt.lastSuppressionReason, 'foreground_focused'); assert.equal(receipt.lastSuppressionClientKind, 'web');
|
||||
await f.manager.presence(tab2, { sequence: 2, visibility: 'hidden', focused: false, interacted: true });
|
||||
f.advance(2_000); await f.manager.tick(); assert.equal(f.sent.length, 0, 'recent input vetoes blur');
|
||||
await f.manager.presence(tab2, { sequence: 1, visibility: 'visible', focused: true, interacted: false });
|
||||
assert.equal((await prisma.agentPushPresence.findUniqueOrThrow({ where: { id: tab2 } })).sequence, 2);
|
||||
await f.manager.connect(unknown); f.advance(31_000); await f.manager.tick(); assert.equal(f.sent.length, 0, 'unknown authenticated socket vetoes');
|
||||
f.advance(61_000); await f.manager.tick(); assert.equal(f.sent.length, 1, 'expired stale sessions retire');
|
||||
await f.manager.presence(tab2, { sequence: 3, visibility: 'hidden', focused: false, interacted: true });
|
||||
f.advance(4_000); await f.manager.tick(); assert.equal(f.sent.length, 1, 'hidden receipts do not renew attention');
|
||||
receipt = await prisma.agentPushDelivery.findFirstOrThrow();
|
||||
assert.equal(receipt.lastSuppressionReason, 'inactive_grace'); assert.ok(receipt.lastSuppressedAt);
|
||||
});
|
||||
|
||||
test('blur and visible recent interaction have short grace; fresh focus renews a live lease', async () => {
|
||||
const f = await fixture(), tab = randomUUID();
|
||||
await f.manager.presence(tab, { sequence: 1, visibility: 'visible', focused: true, interacted: false });
|
||||
f.candidate(); await f.manager.tick(); f.advance(2_000);
|
||||
await f.manager.presence(tab, { sequence: 2, visibility: 'visible', focused: false, interacted: true });
|
||||
await f.manager.tick(); assert.equal(f.sent.length, 0);
|
||||
assert.equal((await prisma.agentPushDelivery.findFirstOrThrow()).lastSuppressionReason, 'recent_interaction');
|
||||
f.advance(3_000); await f.manager.presence(tab, { sequence: 3, visibility: 'visible', focused: false, interacted: false });
|
||||
f.advance(3_000); await f.manager.tick(); assert.equal(f.sent.length, 1);
|
||||
await f.manager.presence(tab, { sequence: 4, visibility: 'visible', focused: true, interacted: false });
|
||||
f.candidate('12'); await f.manager.tick(); f.advance(45_000);
|
||||
await f.manager.presence(tab, { sequence: 5, visibility: 'visible', focused: true, interacted: false });
|
||||
f.advance(20_000); await f.manager.tick(); assert.equal(f.sent.length, 1, 'fresh foreground heartbeat extends attention');
|
||||
f.advance(41_000); await f.manager.tick(); assert.equal(f.sent.length, 2, 'stale focus expires');
|
||||
});
|
||||
|
||||
test('anonymous introduction grace survives heartbeat, new UUID reconnects and coordinator restart without renewing', async () => {
|
||||
const f = await fixture(), connection = randomUUID();
|
||||
await f.manager.connect(connection); f.candidate(); await f.manager.tick(); f.advance(2_000); await f.manager.tick();
|
||||
assert.equal(f.sent.length, 0);
|
||||
const first = await prisma.agentPushCoordinator.findUniqueOrThrow({ where: { id: 'root' } });
|
||||
assert.equal((await prisma.agentPushDelivery.findFirstOrThrow()).lastSuppressionReason, 'unknown_client_grace');
|
||||
await f.manager.heartbeat(connection); await f.manager.disconnect(connection);
|
||||
await f.manager.connect(randomUUID(), 'web', randomUUID()); f.advance(4_000); await f.manager.tick(); assert.equal(f.sent.length, 1);
|
||||
f.candidate('12'); await f.manager.tick(); f.advance(2_000);
|
||||
const restarted = new AgentPushCoordinator({ ...f.manager.options, warmupMs: 0 });
|
||||
await restarted.connect(randomUUID()); await restarted.tick(); assert.equal(f.sent.length, 2);
|
||||
assert.deepEqual((await prisma.agentPushCoordinator.findUniqueOrThrow({ where: { id: 'root' } })).unknownSince, first.unknownSince);
|
||||
// Spending the anonymous budget never disables an independent authenticated browser lease.
|
||||
await f.manager.presence(randomUUID(), { sequence: 1, visibility: 'visible', focused: true, interacted: false });
|
||||
f.candidate('13'); await f.manager.tick(); f.advance(2_000); await f.manager.tick(); assert.equal(f.sent.length, 2);
|
||||
});
|
||||
|
||||
test('expired observed web lease is inactive with a live socket; transport heartbeat preserves attribution and never recreates it', async () => {
|
||||
const f = await fixture(), tab = randomUUID(), connection = randomUUID(), native = randomUUID();
|
||||
await f.manager.presence(tab, { sequence: 1, visibility: 'visible', focused: true, interacted: false });
|
||||
await f.manager.connect(connection, 'web', tab); await f.manager.connect(native, 'native');
|
||||
f.candidate(); await f.manager.tick(); f.advance(61_000);
|
||||
await f.manager.heartbeat(connection); await f.manager.heartbeat(native); await f.manager.tick(); assert.equal(f.sent.length, 1);
|
||||
assert.equal((await prisma.agentPushCoordinator.findUniqueOrThrow({ where: { id: 'root' } })).unknownSince, null);
|
||||
const row = await prisma.agentPushConnection.findUniqueOrThrow({ where: { id: connection } });
|
||||
assert.equal(row.kind, 'web'); assert.equal(row.sessionId, tab);
|
||||
assert.equal((await prisma.agentPushConnection.findUniqueOrThrow({ where: { id: native } })).kind, 'native');
|
||||
await f.manager.disconnect(native); await f.manager.heartbeat(native);
|
||||
assert.equal(await prisma.agentPushConnection.findUnique({ where: { id: native } }), null);
|
||||
await f.manager.presence(tab, { sequence: 2, visibility: 'hidden', focused: false, interacted: false });
|
||||
assert.equal((await prisma.agentPushPresence.findUniqueOrThrow({ where: { id: tab } })).inactiveUntil, null, 'stale focus cannot start fresh grace');
|
||||
});
|
||||
|
||||
test('presence classification cannot borrow a session from a different credential generation', async () => {
|
||||
const f = await fixture(), tab = randomUUID();
|
||||
await f.manager.presence(tab, { sequence: 1, visibility: 'hidden', focused: false, interacted: false });
|
||||
await prisma.agentPushPresence.update({ where: { id: tab }, data: { ownerEpoch: 'another-generation' } });
|
||||
await f.manager.connect(randomUUID(), 'web', tab); f.candidate(); await f.manager.tick(); f.advance(2_000); await f.manager.tick();
|
||||
assert.equal(f.sent.length, 0); assert.equal((await prisma.agentPushDelivery.findFirstOrThrow()).lastSuppressionReason, 'unknown_client_grace');
|
||||
});
|
||||
|
||||
test('read at final context refresh cancels without a send and preserves the prior suppression evidence', async () => {
|
||||
const f = await fixture(); await f.manager.connect(randomUUID()); f.candidate(); await f.manager.tick(); f.advance(2_000); await f.manager.tick();
|
||||
const prior = await prisma.agentPushDelivery.findFirstOrThrow(), original = f.manager.options.context; let calls = 0;
|
||||
f.manager.options.context = async (...args) => { if (++calls === 2) await f.manager.advanceRead('11', f.source); return original(...args); };
|
||||
f.advance(4_000); await f.manager.tick();
|
||||
const receipt = await prisma.agentPushDelivery.findFirstOrThrow();
|
||||
assert.equal(f.sent.length, 0); assert.equal(receipt.attempts, 0); assert.equal(receipt.outcome, 'read');
|
||||
assert.equal(receipt.lastSuppressionReason, prior.lastSuppressionReason); assert.deepEqual(receipt.lastSuppressedAt, prior.lastSuppressedAt);
|
||||
});
|
||||
|
||||
test('instrumented hidden web socket is inactive; missing lease and native-runtime unknown veto', async () => {
|
||||
|
||||
Reference in New Issue
Block a user