refactor(vibe): simplify durable session flow
This commit is contained in:
@@ -75,9 +75,7 @@ describe('DbService v2 methods', () => {
|
||||
.mockResolvedValueOnce({ rows: [session] })
|
||||
.mockResolvedValueOnce({ rows: [{ ...session, status: 'ended' }] });
|
||||
|
||||
await expect(service.createVibeSession({
|
||||
userId: 'user-1', policyVersion: 'v2.1', context: { activity: 'focus' },
|
||||
})).resolves.toEqual(session);
|
||||
await expect(service.createVibeSession({ userId: 'user-1', policyVersion: 'v2.1' })).resolves.toEqual(session);
|
||||
await expect(service.getVibeSession('session-1', 'user-1')).resolves.toEqual(session);
|
||||
await expect(service.endVibeSession('session-1', 'user-1')).resolves.toMatchObject({ status: 'ended' });
|
||||
|
||||
@@ -88,17 +86,13 @@ describe('DbService v2 methods', () => {
|
||||
'user-1', null, expect.any(String), 'v2.1',
|
||||
JSON.stringify({ type: 'discovery', target: 1, progress: 0 }), 0.3, 0.38,
|
||||
]));
|
||||
expect(JSON.parse(clientQuery.mock.calls[3][1][2])).toEqual(expect.objectContaining({
|
||||
activity: 'focus',
|
||||
}));
|
||||
expect(JSON.parse(clientQuery.mock.calls[3][1][2])).toEqual({});
|
||||
expect(poolQuery.mock.calls[0][0]).toContain('id = $1 AND user_id = $2');
|
||||
expect(poolQuery.mock.calls[1][0]).toContain('COALESCE(ended_at, NOW())');
|
||||
expect(poolQuery.mock.calls[1][0]).toContain('CASE WHEN ended_at IS NULL THEN $3 ELSE status END');
|
||||
});
|
||||
|
||||
it('normalizes context at the persistence boundary for direct callers', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-08-02T19:00:00.000Z'));
|
||||
it('starts sessions without inventing client context', async () => {
|
||||
const { service, clientQuery } = makeTransactionalService();
|
||||
const session = { id: 'session-1', user_id: 'user-1', status: 'active' };
|
||||
clientQuery
|
||||
@@ -108,33 +102,13 @@ describe('DbService v2 methods', () => {
|
||||
.mockResolvedValueOnce({ rows: [session] }) // insert
|
||||
.mockResolvedValueOnce({ rows: [] }); // COMMIT
|
||||
|
||||
try {
|
||||
await service.createVibeSession({
|
||||
userId: 'user-1',
|
||||
policyVersion: 'v2.1',
|
||||
context: {
|
||||
timeZone: 'UTC',
|
||||
activity: 'walking',
|
||||
device: 'phone',
|
||||
exactCoordinates: '53.1959,50.1002',
|
||||
browserTelemetry: { batteryPercent: 4, ipAddress: '192.0.2.1' },
|
||||
localHour: 3,
|
||||
weekday: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const insertParameters = clientQuery.mock.calls[3][1];
|
||||
expect(insertParameters.slice(0, 2)).toEqual(['user-1', null]);
|
||||
expect(JSON.parse(insertParameters[2])).toEqual({
|
||||
timeZone: 'UTC', localHour: 19, weekday: 0, dayKind: 'weekend',
|
||||
activity: 'walking', device: 'phone',
|
||||
});
|
||||
expect(insertParameters.slice(3)).toEqual([
|
||||
'v2.1', JSON.stringify({ type: 'discovery', target: 1, progress: 0 }), 0.3, 0.38,
|
||||
]);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
await service.createVibeSession({ userId: 'user-1', policyVersion: 'v2.1' });
|
||||
const insertParameters = clientQuery.mock.calls[3][1];
|
||||
expect(insertParameters.slice(0, 2)).toEqual(['user-1', null]);
|
||||
expect(JSON.parse(insertParameters[2])).toEqual({});
|
||||
expect(insertParameters.slice(3)).toEqual([
|
||||
'v2.1', JSON.stringify({ type: 'discovery', target: 1, progress: 0 }), 0.3, 0.38,
|
||||
]);
|
||||
});
|
||||
|
||||
it('replaces an owned active session and writes its terminal event before starting another', async () => {
|
||||
@@ -189,45 +163,38 @@ describe('DbService v2 methods', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('sanitizes and projects an inserted context change atomically with its ledger event', async () => {
|
||||
it('records a non-material event without mutating session context', async () => {
|
||||
const { service, clientQuery } = makeTransactionalService();
|
||||
const event = {
|
||||
id: 'event-1', client_event_id: null, session_id: 'session-1', user_id: 'user-1', track_id: null,
|
||||
type: 'context_changed', occurred_at: new Date(), position_ms: null, duration_ms: null,
|
||||
payload: { context: { activity: 'walking', localHour: 12 } },
|
||||
type: 'progress', occurred_at: new Date(), position_ms: null, duration_ms: null,
|
||||
payload: { source: 'player' },
|
||||
};
|
||||
clientQuery
|
||||
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [{ id: 'session-1', status: 'active' }] }) // lock
|
||||
.mockResolvedValueOnce({ rows: [event] }) // insert
|
||||
.mockResolvedValueOnce({ rows: [] }) // session context projection
|
||||
.mockResolvedValueOnce({ rows: [] }) // legacy session-state projection
|
||||
.mockResolvedValueOnce({ rows: [] }) // last event timestamp
|
||||
.mockResolvedValueOnce({ rows: [] }); // COMMIT
|
||||
|
||||
await service.recordVibeEvent({
|
||||
sessionId: 'session-1', userId: 'user-1', type: 'context_changed',
|
||||
payload: {
|
||||
context: { activity: 'walking', exactCoordinates: '53.2,50.1' },
|
||||
rawBrowserTelemetry: { battery: 4 },
|
||||
},
|
||||
sessionId: 'session-1', userId: 'user-1', type: 'progress', payload: { source: 'player' },
|
||||
});
|
||||
|
||||
const values = clientQuery.mock.calls[2][1];
|
||||
const storedPayload = JSON.parse(values[8]);
|
||||
expect(storedPayload).toEqual({ context: expect.objectContaining({ activity: 'walking' }) });
|
||||
expect(storedPayload.context).not.toHaveProperty('exactCoordinates');
|
||||
expect(storedPayload).not.toHaveProperty('rawBrowserTelemetry');
|
||||
expect(clientQuery.mock.calls[3][0]).toContain('SET context = $3::jsonb');
|
||||
expect(clientQuery.mock.calls[4][0]).toContain("jsonb_build_object('context'");
|
||||
expect(storedPayload).toEqual({ source: 'player' });
|
||||
expect(clientQuery.mock.calls.map(([sql]) => String(sql))).not.toContain(
|
||||
expect.stringContaining('SET context = $3::jsonb'),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not apply a retry body to an existing context-change event', async () => {
|
||||
it('does not apply a retry body to an existing event', async () => {
|
||||
const { service, clientQuery } = makeTransactionalService();
|
||||
const canonicalEvent = {
|
||||
id: 'event-1', client_event_id: 'client-event-1', session_id: 'session-1', user_id: 'user-1', track_id: null,
|
||||
type: 'context_changed', occurred_at: new Date(), position_ms: null, duration_ms: null,
|
||||
payload: { context: { activity: 'focus', localHour: 12 } },
|
||||
type: 'progress', occurred_at: new Date(), position_ms: null, duration_ms: null,
|
||||
payload: { source: 'player' },
|
||||
};
|
||||
clientQuery
|
||||
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
||||
@@ -236,8 +203,8 @@ describe('DbService v2 methods', () => {
|
||||
.mockResolvedValueOnce({ rows: [] }); // COMMIT
|
||||
|
||||
const result = await service.recordVibeEvent({
|
||||
sessionId: 'session-1', userId: 'user-1', clientEventId: 'client-event-1', type: 'context_changed',
|
||||
payload: { context: { activity: 'workout', exactCoordinates: '53.2,50.1' } },
|
||||
sessionId: 'session-1', userId: 'user-1', clientEventId: 'client-event-1', type: 'progress',
|
||||
payload: { source: 'retry' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ event: canonicalEvent, inserted: false });
|
||||
|
||||
Reference in New Issue
Block a user