Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions backend/__tests__/service/recentContentByUser.realPg.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* `recentContentByUser` on real PostgreSQL. The pg-mem arm
* (unit/models/recentContentByUser.pgmem.test.js) proves the ordering only in
* pg-mem's own planner. Here the same rules run on the real schema: the TASK-503
* history copy gives months-old rows high ids, created_at is nullable, and DESC
* sorts NULL first unless the query says NULLS LAST.
*/
const { setupPgDb, closePgDb } = require('../utils/testUtils');

const { pool } = require('../../config/db-pg');
const PGMessage = require('../../models/pg/Message');

const d = process.env.INTEGRATION_TEST === 'true' && process.env.PG_HOST ? describe : describe.skip;
const POD = 'aaaaaaaaaaaaaaaaaaaaaa01';
const LIN = 'cccccccccccccccccccccc01';

const insert = (content, createdAt) => pool.query(
'INSERT INTO messages (pod_id, user_id, content, message_type, created_at) VALUES ($1, $2, $3, $4, $5)',
[POD, LIN, content, 'text', createdAt],
);

d('recentContentByUser on real PostgreSQL', () => {
beforeAll(async () => {
await setupPgDb();
await pool.query("INSERT INTO pods (id, name, type, created_by) VALUES ($1, 'History', 'chat', $2)", [POD, LIN]);
await insert('written live this week', new Date('2026-10-28T10:00:00Z'));
await insert('copied from months ago', new Date('2026-06-01T10:00:00Z'));
// Two rows at the same instant: the later insert (higher id) comes first.
await insert('same instant, lower id', new Date('2026-05-01T10:00:00Z'));
await insert('same instant, higher id', new Date('2026-05-01T10:00:00Z'));
await insert('undated, highest id', null);
});
afterAll(async () => {
await pool.query('TRUNCATE TABLE messages, pods RESTART IDENTITY CASCADE');
await closePgDb();
await pool.end();
});

test('orders by created_at, ties by id, and puts an undated row last', async () => {
await expect(PGMessage.recentContentByUser(LIN)).resolves.toEqual([
'written live this week',
'copied from months ago',
'same instant, higher id',
'same instant, lower id',
'undated, highest id',
]);
});

test('the row cap keeps the newest dated rows', async () => {
await expect(PGMessage.recentContentByUser(LIN, 1)).resolves.toEqual(['written live this week']);
});
});
65 changes: 65 additions & 0 deletions backend/__tests__/unit/models/recentContentByUser.pgmem.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* `recentContentByUser`, EXECUTED against pg-mem. firstContactService picks the
* language of a person's first greeting from it. The TASK-503 history copy
* inserts months-old rows into PostgreSQL with fresh, high ids, so "recent"
* must mean newest by created_at, not newest by insert order.
*/

const { newDb } = require('pg-mem');

const mockDb = newDb();
const mockPool = new (mockDb.adapters.createPg().Pool)();

jest.mock('../../../config/db-pg', () => ({ pool: mockPool }));

const PGMessage = require('../../../models/pg/Message');

const POD = 'aaaaaaaaaaaaaaaaaaaaaa01';
const LIN = 'cccccccccccccccccccccc01';
const OTHER = 'cccccccccccccccccccccc02';

const insert = (userId, content, createdAt, type = 'text') => mockPool.query(
'INSERT INTO messages (pod_id, user_id, content, message_type, created_at) VALUES ($1, $2, $3, $4, $5)',
[POD, userId, content, type, createdAt],
);

beforeAll(async () => {
await mockPool.query(`CREATE TABLE messages (
id SERIAL PRIMARY KEY,
pod_id VARCHAR(24) NOT NULL,
user_id VARCHAR(24) NOT NULL,
content TEXT NOT NULL,
message_type VARCHAR(20) DEFAULT 'text' NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)`);
// Written live, so its id is low.
await insert(LIN, 'hello, this week in English', new Date('2026-10-28T10:00:00Z'));
await insert(OTHER, 'someone else', new Date('2026-10-29T10:00:00Z'));
await insert(LIN, 'joined the pod', new Date('2026-10-29T11:00:00Z'), 'system');
// Copied from MongoDB afterwards: an old row with a higher id.
await insert(LIN, '你好,这是几个月前的', new Date('2026-06-01T10:00:00Z'));
// An undated row, inserted last so it also has the highest id.
await insert(LIN, 'bonjour, sans date', null);
});

describe('recentContentByUser', () => {
test('an old row with a higher id loses to a newer row with a lower id', async () => {
await expect(PGMessage.recentContentByUser(LIN, 1)).resolves.toEqual(['hello, this week in English']);
await expect(PGMessage.recentContentByUser(LIN))
.resolves.toEqual(['hello, this week in English', '你好,这是几个月前的', 'bonjour, sans date']);
});

test('an undated row loses to every dated one', async () => {
const rows = await PGMessage.recentContentByUser(LIN, 2);
expect(rows).not.toContain('bonjour, sans date');
});

test("returns only this person's text rows", async () => {
await expect(PGMessage.recentContentByUser(OTHER)).resolves.toEqual(['someone else']);
await expect(PGMessage.recentContentByUser(LIN)).resolves.not.toContain('joined the pod');
});

test('refuses a blank user', async () => {
await expect(PGMessage.recentContentByUser('')).resolves.toEqual([]);
});
});
8 changes: 6 additions & 2 deletions backend/models/pg/Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,13 +417,17 @@ class Message {
return (result.rows as Array<{ content?: unknown }>).map((row) => String(row.content || ''));
}

/** Recent words by one person across pods, for their first greeting language. */
/**
* Recent words by one person across pods, for their first greeting language.
* Newest by created_at, not id: the TASK-503 history copy gives old rows high ids.
* created_at is nullable, and DESC puts NULL first, so an undated row sorts last.
*/
static async recentContentByUser(userId: unknown, limit = 10): Promise<string[]> {
if (!userId) return [];
const result = await (pool as PgPool).query(
`SELECT content FROM messages
WHERE user_id = $1 AND message_type = 'text' AND content <> ''
ORDER BY id DESC LIMIT $2`,
ORDER BY created_at DESC NULLS LAST, id DESC LIMIT $2`,
[String(userId), limit],
);
return (result.rows as Array<{ content?: unknown }>).map((row) => String(row.content || ''));
Expand Down
Loading