Files
hermes-agent/apps/desktop/src/lib/listing-embed.test.ts
T
Brooklyn Nicholson 9b16719e17 Render property listings as browsable transcript cards
Adds a `listing` fence to the rich-embed registry: JSON in, a native card
out — photo mosaic, address and price, specs, and the catches worth
verifying before a tour.

The rental portals publish no embeddable surface (public APIs deprecated or
broker-gated, no oEmbed, X-Frame-Options on every listing page), so a URL
cannot be framed. The data the agent already gathered can be, and the
portals' image CDNs hotlink fine — so the card is authored rather than
fetched, alongside the existing mermaid and svg fences.

Parsing treats every field as untrusted model output: one object, a bare
array, or `{listings: []}` all work, beds accept `3` and `"3.5"`, non-http
URLs are rejected so a card cannot smuggle a javascript: href, and lists
are capped. Anything unparseable falls back to the plain code block.
2026-09-04 15:00:43 -05:00

119 lines
4.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { hostLabel, parseListings, specLine } from './listing-embed'
// Shaped like what the agent actually emits after a portal sweep.
const REAL = JSON.stringify({
listings: [
{
address: '51 Via Los Altos, Tiburon, CA 94920',
baths: '3.5',
beds: 3,
catches: ['No pets', 'Fireplace decorative only'],
facts: ['0.65 acre', 'Panoramic bay views', '12-mo lease'],
images: ['https://images.homes.com/a.jpg', 'https://images.homes.com/b.jpg'],
links: [
{ label: 'homes.com', url: 'https://www.homes.com/property/51-via-los-altos/' },
{ url: 'https://www.redfin.com/CA/Tiburon/51-Via-Los-Altos-94920/home/1244581' }
],
note: 'Bay plus Belvedere Island plus Richmond Bridge.',
price: '$12,500/mo',
size: '4,290 sqft'
}
]
})
describe('parseListings', () => {
it('normalizes a real sweep payload', () => {
const [listing] = parseListings(REAL)
expect(listing.address).toBe('51 Via Los Altos, Tiburon, CA 94920')
expect(listing.beds).toBe(3)
// String decimals survive: half-baths are real.
expect(listing.baths).toBe(3.5)
expect(listing.images).toHaveLength(2)
expect(listing.facts).toHaveLength(3)
expect(listing.catches).toHaveLength(2)
// A bare URL gets the portal host as its label.
expect(listing.links[1].label).toBe('redfin.com')
})
it('accepts a lone object and a bare array', () => {
expect(parseListings('{"address":"1 Main St"}')).toHaveLength(1)
expect(parseListings('[{"address":"1 Main St"},{"address":"2 Main St"}]')).toHaveLength(2)
})
it('drops entries with no address — a card needs an identity', () => {
expect(parseListings('[{"price":"$1"},{"address":"1 Main St"}]')).toHaveLength(1)
})
it('returns nothing for non-JSON so the plain code block wins', () => {
expect(parseListings('not json at all')).toEqual([])
expect(parseListings('{"address":"x"')).toEqual([])
})
it('refuses non-http URLs in images and links', () => {
const [listing] = parseListings(
JSON.stringify({
address: 'x',
images: ['javascript:alert(1)', 'https://ok.example/a.jpg'],
links: ['data:text/html,hi', 'https://ok.example/listing']
})
)
expect(listing.images).toEqual(['https://ok.example/a.jpg'])
expect(listing.links.map(link => link.url)).toEqual(['https://ok.example/listing'])
})
it('dedupes repeated images and links', () => {
const [listing] = parseListings(
JSON.stringify({
address: 'x',
images: ['https://a.example/1.jpg', 'https://a.example/1.jpg'],
links: ['https://a.example/l', 'https://a.example/l']
})
)
expect(listing.images).toHaveLength(1)
expect(listing.links).toHaveLength(1)
})
it('caps a pathological payload', () => {
const images = Array.from({ length: 500 }, (_, i) => `https://a.example/${i}.jpg`)
const [listing] = parseListings(JSON.stringify({ address: 'x', images }))
expect(listing.images.length).toBeLessThanOrEqual(40)
})
it('rejects absurd bed counts but keeps the listing', () => {
const [listing] = parseListings('{"address":"x","beds":9999,"baths":-2}')
expect(listing.beds).toBeUndefined()
expect(listing.baths).toBeUndefined()
expect(listing.address).toBe('x')
})
it('takes a single string where a list was expected', () => {
const [listing] = parseListings('{"address":"x","facts":"private dock"}')
expect(listing.facts).toEqual(['private dock'])
})
})
describe('specLine', () => {
it('joins only the specs that exist', () => {
const [full] = parseListings('{"address":"x","beds":3,"baths":2,"size":"2,136 sqft"}')
const [sparse] = parseListings('{"address":"x","beds":3}')
expect(specLine(full)).toBe('3 bd · 2 ba · 2,136 sqft')
expect(specLine(sparse)).toBe('3 bd')
})
})
describe('hostLabel', () => {
it('strips www so the host reads as the portal name', () => {
expect(hostLabel('https://www.homes.com/property/x')).toBe('homes.com')
expect(hostLabel('https://www.har.com/homedetail/y')).toBe('har.com')
})
})