feat(vibe): reconcile mutable session previews in playback

This commit is contained in:
kami
2026-08-01 23:47:02 +04:00
parent 51ef7c84db
commit 57df1cfe9f
20 changed files with 1311 additions and 312 deletions
@@ -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));