refactor(vibe): simplify durable session flow
Typecheck / typecheck (backend) (push) Has been cancelled
Typecheck / typecheck (workers) (push) Has been cancelled
Typecheck / typecheck (backend) (pull_request) Has been cancelled
Typecheck / typecheck (workers) (pull_request) Has been cancelled

This commit is contained in:
kami
2026-08-03 12:44:08 +04:00
parent 61a1373ca9
commit 9eba247a58
16 changed files with 191 additions and 520 deletions
+23 -56
View File
@@ -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 });