feat(vibe): plan musical arcs and callbacks

This commit is contained in:
kami
2026-08-02 00:49:27 +04:00
parent 89a23e3703
commit fe13798c99
7 changed files with 910 additions and 42 deletions
@@ -68,6 +68,23 @@ describe('durable Vibe session routes', () => {
await app.close();
});
it('reserves track_served for the authoritative /next operation', async () => {
const { app, coordinator } = await appWithCoordinator();
const result = await app.inject({
method: 'POST', url: `/v2/vibe/sessions/${SESSION_ID}/events`,
payload: {
type: 'track_served',
trackId: TRACK_ID,
payload: { planVersionId: '33333333-3333-4333-8333-333333333333', ordinal: 0 },
},
});
expect(result.statusCode).toBe(400);
expect(result.json()).toEqual({ error: 'track_served is reserved for the server /next operation' });
expect(coordinator.appendEvent).not.toHaveBeenCalled();
await app.close();
});
it('returns a lifecycle conflict when an initial plan race ends or replaces the session', async () => {
const { app, coordinator } = await appWithCoordinator();
coordinator.start.mockRejectedValueOnce(
@@ -101,6 +101,12 @@ export default async function vibeSessionsRoutes(
if (!body || !VIBE_EVENT_TYPES.includes(body.type as typeof VIBE_EVENT_TYPES[number])) {
return reply.code(400).send({ error: 'type must be a supported Vibe event type' });
}
// Delivery is an authoritative state transition performed only by /next.
// Accepting this event from the public ledger endpoint would let a client
// fabricate exposure rows and consume the server-side surprise budget.
if (body.type === 'track_served') {
return reply.code(400).send({ error: 'track_served is reserved for the server /next operation' });
}
if (body.eventId !== undefined && !validUuid(body.eventId)) {
return reply.code(400).send({ error: 'eventId must be a UUID' });
}