feat(vibe): reconcile mutable session previews in playback
This commit is contained in:
@@ -22,6 +22,7 @@ async function appWithCoordinator(identityResolver: VibeIdentityResolver = () =>
|
||||
appendEvent: vi.fn().mockResolvedValue({ ...response(), event: { id: 'event-1' }, idempotent: false }),
|
||||
end: vi.fn().mockResolvedValue(response()),
|
||||
serveNext: vi.fn().mockResolvedValue(response()),
|
||||
advancePastUnplayable: vi.fn().mockResolvedValue(response()),
|
||||
} as any;
|
||||
const app = Fastify();
|
||||
await app.register(vibeSessionsRoutes, { coordinator, identityResolver });
|
||||
@@ -133,4 +134,31 @@ describe('durable Vibe session routes', () => {
|
||||
expect(coordinator.serveNext).toHaveBeenCalledTimes(1);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('uses the explicit versioned advancement protocol for a served unplayable item', async () => {
|
||||
const { app, coordinator } = await appWithCoordinator();
|
||||
const result = await app.inject({
|
||||
method: 'POST', url: `/v2/vibe/sessions/${SESSION_ID}/next`,
|
||||
payload: {
|
||||
expectedPlanVersion: 2,
|
||||
unplayable: {
|
||||
eventId: '33333333-3333-4333-8333-333333333333',
|
||||
planVersionId: '44444444-4444-4444-8444-444444444444',
|
||||
ordinal: 0,
|
||||
trackId: TRACK_ID,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(coordinator.advancePastUnplayable).toHaveBeenCalledWith(USER_ID, SESSION_ID, {
|
||||
expectedPlanVersion: 2,
|
||||
eventId: '33333333-3333-4333-8333-333333333333',
|
||||
planVersionId: '44444444-4444-4444-8444-444444444444',
|
||||
ordinal: 0,
|
||||
trackId: TRACK_ID,
|
||||
});
|
||||
expect(coordinator.serveNext).not.toHaveBeenCalled();
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,8 +156,33 @@ export default async function vibeSessionsRoutes(
|
||||
&& (!Number.isInteger(body.expectedPlanVersion) || (body.expectedPlanVersion as number) < 1)) {
|
||||
return reply.code(400).send({ error: 'expectedPlanVersion must be a positive integer' });
|
||||
}
|
||||
const unplayable = body.unplayable;
|
||||
if (unplayable !== undefined && !isObject(unplayable)) {
|
||||
return reply.code(400).send({ error: 'unplayable must be an object' });
|
||||
}
|
||||
if (isObject(unplayable)) {
|
||||
if (body.expectedPlanVersion === undefined) {
|
||||
return reply.code(400).send({ error: 'expectedPlanVersion is required when advancing an unplayable item' });
|
||||
}
|
||||
if (!validUuid(unplayable.eventId)
|
||||
|| !validUuid(unplayable.planVersionId)
|
||||
|| !validUuid(unplayable.trackId)
|
||||
|| !Number.isInteger(unplayable.ordinal)
|
||||
|| (unplayable.ordinal as number) < 0) {
|
||||
return reply.code(400).send({ error: 'unplayable requires UUID eventId, planVersionId, trackId and a non-negative integer ordinal' });
|
||||
}
|
||||
}
|
||||
try {
|
||||
const expectedPlanVersion = body.expectedPlanVersion as number | undefined;
|
||||
if (isObject(unplayable)) {
|
||||
return reply.send(await coordinator.advancePastUnplayable(userId, sessionId, {
|
||||
expectedPlanVersion: expectedPlanVersion as number,
|
||||
eventId: unplayable.eventId as string,
|
||||
planVersionId: unplayable.planVersionId as string,
|
||||
ordinal: unplayable.ordinal as number,
|
||||
trackId: unplayable.trackId as string,
|
||||
}));
|
||||
}
|
||||
return reply.send(expectedPlanVersion === undefined
|
||||
? await coordinator.serveNext(userId, sessionId)
|
||||
: await coordinator.serveNext(userId, sessionId, expectedPlanVersion));
|
||||
|
||||
Reference in New Issue
Block a user