feat(vibe): reconcile mutable session previews in playback
This commit is contained in:
@@ -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: [{
|
||||
|
||||
Reference in New Issue
Block a user