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
+89
View File
@@ -361,6 +361,95 @@ describe('DbService v2 methods', () => {
expect(clientQuery.mock.calls.map(([sql]) => sql)).not.toContain(expect.stringContaining('SET committed = true'));
});
it('advances a served unplayable item with a separate idempotent event and commits one replacement', async () => {
const { service, clientQuery } = makeTransactionalService();
const replacement = {
plan_version_id: 'plan-1', ordinal: 1, track_id: 'track-2', slot_role: null,
candidate_source: 'discovery', score: 0.8, score_breakdown: {}, explanation: [], committed: true,
};
clientQuery
.mockResolvedValueOnce({ rows: [] }) // BEGIN
.mockResolvedValueOnce({ rows: [{ status: 'active' }] }) // session lock
.mockResolvedValueOnce({ rows: [{ id: 'plan-1', version: 1 }] }) // latest plan
.mockResolvedValueOnce({ rows: [] }) // no prior playback_error event
.mockResolvedValueOnce({ rows: [{ track_id: 'track-1', ordinal: 0 }] }) // current served cursor
.mockResolvedValueOnce({ rows: [{ id: 'error-1' }] }) // playback_error event
.mockResolvedValueOnce({ rows: [replacement] }) // commit replacement
.mockResolvedValueOnce({ rows: [] }) // replacement track_served event
.mockResolvedValueOnce({ rows: [] }) // playback_error result payload
.mockResolvedValueOnce({ rows: [] }) // session timestamp
.mockResolvedValueOnce({ rows: [] }); // COMMIT
await expect(service.advancePastUnplayableVibePlanItem('session-1', 'user-1', {
expectedPlanVersion: 1,
planVersionId: 'plan-1',
ordinal: 0,
trackId: 'track-1',
eventId: 'event-1',
})).resolves.toEqual({ item: replacement, stale: false });
expect(clientQuery.mock.calls[5][0]).toContain("'playback_error'");
expect(clientQuery.mock.calls[6][0]).toContain('SET committed = true');
expect(clientQuery.mock.calls[7][0]).toContain("'track_served'");
expect(clientQuery.mock.calls[8][0]).toContain('UPDATE vibe_events SET payload');
});
it('refuses an old served cursor when events share a timestamp by ordering the immutable plan ordinal', async () => {
const { service, clientQuery } = makeTransactionalService();
clientQuery
.mockResolvedValueOnce({ rows: [] }) // BEGIN
.mockResolvedValueOnce({ rows: [{ status: 'active' }] }) // session lock
.mockResolvedValueOnce({ rows: [{ id: 'plan-1', version: 1 }] }) // latest plan
.mockResolvedValueOnce({ rows: [] }) // no prior playback_error event
// The lower-ordinal event can have a lexically greater UUID at the
// same occurred_at. The cursor must still be the highest immutable
// plan ordinal, never whichever UUID sorts last.
.mockResolvedValueOnce({ rows: [{ track_id: 'track-2', ordinal: 1 }] }) // current served cursor
.mockResolvedValueOnce({ rows: [] }); // ROLLBACK
await expect(service.advancePastUnplayableVibePlanItem('session-1', 'user-1', {
expectedPlanVersion: 1,
planVersionId: 'plan-1',
ordinal: 0,
trackId: 'track-1',
eventId: 'event-1',
})).rejects.toThrow('not the current served cursor');
expect(clientQuery.mock.calls[4][0]).toContain('JOIN vibe_plan_items');
expect(clientQuery.mock.calls[4][0]).toContain('ORDER BY i.ordinal DESC');
expect(clientQuery.mock.calls[4][0]).not.toContain('id DESC');
expect(clientQuery.mock.calls.map(([sql]) => sql)).not.toContain(expect.stringContaining('SET committed = true'));
});
it('retries an unplayable advancement with the same event id without consuming another item', async () => {
const { service, clientQuery } = makeTransactionalService();
const replacement = {
plan_version_id: 'plan-1', ordinal: 1, track_id: 'track-2', slot_role: null,
candidate_source: 'discovery', score: 0.8, score_breakdown: {}, explanation: [], committed: true,
};
clientQuery
.mockResolvedValueOnce({ rows: [] }) // BEGIN
.mockResolvedValueOnce({ rows: [{ status: 'active' }] }) // session lock
.mockResolvedValueOnce({ rows: [{ id: 'plan-1', version: 1 }] }) // latest plan
.mockResolvedValueOnce({ rows: [{ type: 'playback_error', payload: {
planVersionId: 'plan-1', ordinal: 0, trackId: 'track-1',
advancedTo: { planVersionId: 'plan-1', ordinal: 1 },
} }] }) // prior explicit advancement
.mockResolvedValueOnce({ rows: [replacement] }) // canonical replacement
.mockResolvedValueOnce({ rows: [] }); // COMMIT
await expect(service.advancePastUnplayableVibePlanItem('session-1', 'user-1', {
expectedPlanVersion: 1,
planVersionId: 'plan-1',
ordinal: 0,
trackId: 'track-1',
eventId: 'event-1',
})).resolves.toEqual({ item: replacement, stale: false });
expect(clientQuery.mock.calls.map(([sql]) => sql)).not.toContain(expect.stringContaining('SET committed = true'));
expect(clientQuery.mock.calls).toHaveLength(6);
});
it('reads the latest revision and reconstructs ordered plan items', async () => {
const { service, mockQuery } = makeService();
mockQuery.mockResolvedValue({ rows: [{