feat: enhance discovery, vibe sessions, and library enrichment
This commit is contained in:
@@ -8,6 +8,22 @@ function makeService(): { service: DbService; mockQuery: ReturnType<typeof vi.fn
|
||||
}
|
||||
|
||||
describe('DbService v2 methods', () => {
|
||||
describe('createAlbum', () => {
|
||||
it('persists the canonical release date instead of discarding it', async () => {
|
||||
const { service, mockQuery } = makeService();
|
||||
mockQuery.mockResolvedValue({ rows: [{ id: 'album-1' }] });
|
||||
|
||||
await service.createAlbum({
|
||||
id: 'album-1', artist_id: 'artist-1', title: 'Album',
|
||||
year: 2026, release_date: '2026-07-15', artwork_id: null,
|
||||
});
|
||||
|
||||
const [sql, params] = mockQuery.mock.calls[0];
|
||||
expect(sql).toContain('release_date');
|
||||
expect(params[3]).toBe('2026-07-15');
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsertClaim', () => {
|
||||
it('calls INSERT ... ON CONFLICT with correct parameters', async () => {
|
||||
const { service, mockQuery } = makeService();
|
||||
@@ -168,6 +184,75 @@ describe('DbService v2 methods', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('track evidence propagation', () => {
|
||||
it('projects a favourite onto artist, genre, and present audio dimensions', async () => {
|
||||
const { service, mockQuery } = makeService();
|
||||
let evidenceNumber = 0;
|
||||
mockQuery.mockImplementation((sql: string) => {
|
||||
if (sql.includes('INSERT INTO evidence')) return Promise.resolve({ rows: [{ id: `ev-${++evidenceNumber}` }] });
|
||||
if (sql.includes('WITH artist_ids')) {
|
||||
return Promise.resolve({ rows: [
|
||||
{ entity_type: 'artist', entity_id: 'artist-1' },
|
||||
{ entity_type: 'genre', entity_id: 'genre-1' },
|
||||
] });
|
||||
}
|
||||
if (sql.includes('SELECT energy, bpm, valence')) {
|
||||
return Promise.resolve({ rows: [{ energy: 0.81, bpm: 128, valence: 0.22 }] });
|
||||
}
|
||||
return Promise.resolve({ rowCount: 1, rows: [] });
|
||||
});
|
||||
|
||||
await service.recordTrackEvidence({
|
||||
user_id: 'user-1', track_id: 'track-1', signal: 'add_to_favorites',
|
||||
profile: 'longterm', weight: 0.60,
|
||||
});
|
||||
|
||||
const evidenceWrites = mockQuery.mock.calls
|
||||
.filter(([sql]) => (sql as string).includes('INSERT INTO evidence'))
|
||||
.map(([, params]) => params as unknown[]);
|
||||
expect(evidenceWrites.map(params => params[1])).toEqual([
|
||||
'track', 'artist', 'genre', 'audio', 'audio', 'audio',
|
||||
]);
|
||||
|
||||
const beliefWrites = mockQuery.mock.calls
|
||||
.filter(([sql]) => (sql as string).includes('INSERT INTO listener_beliefs'))
|
||||
.map(([, params]) => params as unknown[]);
|
||||
const artistBelief = beliefWrites.find(params => params[2] === 'artist');
|
||||
const genreBelief = beliefWrites.find(params => params[2] === 'genre');
|
||||
expect(artistBelief?.[3]).toBe('artist-1');
|
||||
expect(artistBelief?.[5]).toBeCloseTo(0.54); // one favourite is a usable comfort signal
|
||||
expect(genreBelief?.[5]).toBeCloseTo(0.27);
|
||||
expect(beliefWrites.filter(params => params[2] === 'audio')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('rebuilds shared beliefs from local completed plays and feedback without appending evidence', async () => {
|
||||
const clientQuery = vi.fn((sql: string) => {
|
||||
if (sql === 'BEGIN' || sql === 'COMMIT' || sql === 'ROLLBACK') return Promise.resolve({ rows: [] });
|
||||
if (sql.includes('DELETE FROM listener_beliefs')) return Promise.resolve({ rowCount: 0, rows: [] });
|
||||
if (sql.includes('FROM play_history ph')) {
|
||||
return Promise.resolve({ rows: [{
|
||||
track_id: 'track-1', signal: 'playback_completed', profile: 'longterm', weight: 0.10,
|
||||
}] });
|
||||
}
|
||||
if (sql.includes('WITH artist_ids')) return Promise.resolve({ rows: [{ entity_type: 'artist', entity_id: 'artist-1' }] });
|
||||
if (sql.includes('SELECT energy, bpm, valence')) return Promise.resolve({ rows: [] });
|
||||
return Promise.resolve({ rowCount: 1, rows: [] });
|
||||
});
|
||||
const connect = vi.fn().mockResolvedValue({ query: clientQuery, release: vi.fn() });
|
||||
const service = new DbService({ query: vi.fn(), connect } as any);
|
||||
|
||||
const result = await service.rebuildDerivedListenerBeliefs('user-1');
|
||||
|
||||
expect(result).toEqual({ interactions: 1, beliefs: 1 });
|
||||
expect(clientQuery.mock.calls.some(([sql]) => (sql as string).includes('FROM feedback f'))).toBe(true);
|
||||
expect(clientQuery.mock.calls.some(([sql]) => (sql as string).includes('INSERT INTO evidence'))).toBe(false);
|
||||
const beliefCall = (clientQuery.mock.calls as unknown as Array<[string, unknown[]]>).
|
||||
find(([sql]) => sql.includes('INSERT INTO listener_beliefs'));
|
||||
const beliefParams = beliefCall?.[1] ?? [];
|
||||
expect(beliefParams.slice(1, 5)).toEqual(['longterm', 'artist', 'artist-1', 'affinity']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFusedTrackArtists', () => {
|
||||
it('reads from claim_fusion view', async () => {
|
||||
const { service, mockQuery } = makeService();
|
||||
|
||||
Reference in New Issue
Block a user