/**
* v1-parity tests for formatter behavior.
*
* Port of src/v1/formatting.test.ts (at commit 27c5220, parent of the v1
* deletion commit 86becf8). Covers: context timezone header, reply_to +
* quoted_message rendering, XML escaping, and stripInternalTags.
*
* Timestamp-format assertions use `formatLocalTime()` output format, which
* is host locale-dependent for decorators (month abbr, "," separator) but
* stable for the numeric parts we assert on (hour, minute, year).
*/
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { initTestSessionDb, closeSessionDb, getInboundDb } from './db/connection.js';
import { getPendingMessages } from './db/messages-in.js';
import { formatMessages, stripInternalTags } from './formatter.js';
import { TIMEZONE } from './timezone.js';
beforeEach(() => {
initTestSessionDb();
});
afterEach(() => {
closeSessionDb();
});
function insertMessage(
id: string,
kind: string,
content: object,
opts?: { timestamp?: string },
) {
const timestamp = opts?.timestamp ?? new Date().toISOString();
getInboundDb()
.prepare(
`INSERT INTO messages_in (id, kind, timestamp, status, content)
VALUES (?, ?, ?, 'pending', ?)`,
)
.run(id, kind, timestamp, JSON.stringify(content));
}
describe('context timezone header', () => {
it('prepends to formatted output', () => {
insertMessage('m1', 'chat', { sender: 'Alice', text: 'hello' });
const result = formatMessages(getPendingMessages());
expect(result).toContain(` {
const result = formatMessages([]);
expect(result).toContain(` block', () => {
insertMessage('m1', 'chat', { sender: 'Alice', text: 'one' });
insertMessage('m2', 'chat', { sender: 'Bob', text: 'two' });
const result = formatMessages(getPendingMessages());
const ctxIdx = result.indexOf('');
expect(ctxIdx).toBeGreaterThanOrEqual(0);
expect(msgsIdx).toBeGreaterThan(ctxIdx);
});
});
describe('timestamp formatting', () => {
it('renders time via formatLocalTime (user TZ)', () => {
// 2026-06-15T12:00:00Z — timezone-agnostic assertions (year is stable)
insertMessage('m1', 'chat', { sender: 'Alice', text: 'hi' }, { timestamp: '2026-06-15T12:00:00.000Z' });
const result = formatMessages(getPendingMessages());
// formatLocalTime's format in en-US contains the year and a month abbrev
expect(result).toContain('2026');
expect(result).toMatch(/Jun/);
});
it('uses 12-hour AM/PM format', () => {
// 15:30 UTC — some hour will show with AM or PM depending on TZ
insertMessage('m1', 'chat', { sender: 'Alice', text: 'hi' }, { timestamp: '2026-06-15T15:30:00.000Z' });
const result = formatMessages(getPendingMessages());
expect(result).toMatch(/(AM|PM)/);
});
});
describe('reply_to + quoted_message rendering', () => {
it('renders reply_to attribute and quoted_message when all fields present', () => {
insertMessage('m1', 'chat', {
sender: 'Alice',
text: 'Yes, on my way!',
replyTo: { id: '42', sender: 'Bob', text: 'Are you coming tonight?' },
});
const result = formatMessages(getPendingMessages());
expect(result).toContain('reply_to="42"');
expect(result).toContain('Are you coming tonight?');
expect(result).toContain('Yes, on my way!');
});
it('omits reply_to and quoted_message when no reply context', () => {
insertMessage('m1', 'chat', { sender: 'Alice', text: 'plain' });
const result = formatMessages(getPendingMessages());
expect(result).not.toContain('reply_to');
expect(result).not.toContain('quoted_message');
});
it('renders reply_to but omits quoted_message when original content is missing', () => {
insertMessage('m1', 'chat', {
sender: 'Alice',
text: 'ack',
replyTo: { id: '42', sender: 'Bob' }, // no text
});
const result = formatMessages(getPendingMessages());
expect(result).toContain('reply_to="42"');
expect(result).not.toContain('quoted_message');
});
it('XML-escapes reply context', () => {
insertMessage('m1', 'chat', {
sender: 'Alice',
text: 'reply',
replyTo: { id: '1', sender: 'A & B', text: '' },
});
const result = formatMessages(getPendingMessages());
expect(result).toContain('from="A & B"');
expect(result).toContain('<script>');
expect(result).toContain('"xss"');
});
});
describe('XML escaping', () => {
it('escapes <, >, &, " in sender and body', () => {
insertMessage('m1', 'chat', {
sender: 'A & B ',
text: '',
});
const result = formatMessages(getPendingMessages());
expect(result).toContain('sender="A & B <Co>"');
expect(result).toContain('<script>alert("xss")</script>');
});
});
describe('stripInternalTags', () => {
it('strips single-line internal tags and trims', () => {
expect(stripInternalTags('hello secret world')).toBe('hello world');
});
it('strips multi-line internal tags', () => {
expect(stripInternalTags('hello \nsecret\nstuff\n world')).toBe(
'hello world',
);
});
it('strips multiple internal tag blocks', () => {
expect(stripInternalTags('ahellob')).toBe('hello');
});
it('returns empty string when input is only internal tags', () => {
expect(stripInternalTags('only this')).toBe('');
});
it('returns input unchanged when there are no internal tags', () => {
expect(stripInternalTags('hello world')).toBe('hello world');
});
it('preserves content that surrounds internal tags', () => {
expect(stripInternalTags('thinkingThe answer is 42')).toBe(
'The answer is 42',
);
});
});