0
2
// The location feed, which is really three lookups behind one endpoint:
//
// 1. a display-name query that guesses how "A, B, C" splits into
// locality/region/country and unions every reading that could match,
// 2. a city-level H3 query (resolution <= 5) filtered in JS and paginated by
// hand, because SQLite has no H3 functions,
// 3. a venue-level H3 query (resolution > 5) that matches the cell exactly.
//
// The name path is the interesting one — its comments describe several real
// places that motivated each interpretation, so those are the cases here.
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { startTestServer } from "@hatk/hatk/test";
const ALICE = "did:plc:alice";
const BOB = "did:plc:bob";
const CAROL = "did:plc:carol"; // taken down
// Real H3 indices. SEA_A and SEA_B are distinct venues inside one city cell.
const SEA_5 = "8528d543fffffff";
const SEA_A = "8a28d542d527fff";
const SEA_B = "8a28d542d51ffff";
const DC_10 = "8a2aa845ad17fff";
const NYC_10 = "8a2a10728907fff";
const TOKYO_10 = "8a2f5aad80cffff";
const PDX_10 = "8a28f002358ffff";
const ATHENS_10 = "8a1eda743a8ffff";
let server: Awaited<ReturnType<typeof startTestServer>>;
async function gallery(opts: {
id: string;
did?: string;
createdAt: string;
/** `location.value` — the H3 cell. */
cell: string;
/** `location.name` — the raw label, which may be all a gallery has. */
label?: string;
locality?: string | null;
region?: string | null;
country?: string | null;
/** Omit the structured address entirely. */
noAddress?: boolean;
}) {
const { db } = server;
const did = opts.did ?? ALICE;
const uri = `at://${did}/social.grain.gallery/${opts.id}`;
await db.run(
`INSERT INTO "social.grain.gallery" (uri, cid, did, indexed_at, title, location, address, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[
uri,
`cid-${opts.id}`,
did,
opts.createdAt,
opts.id,
JSON.stringify({ name: opts.label ?? opts.locality ?? "Somewhere", value: opts.cell }),
opts.noAddress
? null
: JSON.stringify({
locality: opts.locality ?? null,
region: opts.region ?? null,
country: opts.country ?? null,
}),
opts.createdAt,
],
);
const photoId = `${opts.id}-p`;
const photoUri = `at://${did}/social.grain.photo/${photoId}`;
await db.run(
`INSERT INTO "social.grain.photo" (uri, cid, did, indexed_at, photo, aspect_ratio, created_at)
VALUES ($1, $2, $3, 'i', $4, '{"width":3,"height":2}', $5)`,
[
photoUri,
`cid-${photoId}`,
did,
JSON.stringify({
$type: "blob",
ref: { $link: `bafy-${photoId}` },
mimeType: "image/jpeg",
size: 1,
}),
opts.createdAt,
],
);
await db.run(
`INSERT INTO "social.grain.gallery.item" (uri, cid, did, indexed_at, created_at, gallery, item, position)
VALUES ($1, $2, $3, 'i', $4, $5, $6, 0)`,
[
`at://${did}/social.grain.gallery.item/${photoId}`,
`cid-i-${photoId}`,
did,
opts.createdAt,
uri,
photoUri,
],
);
return uri;
}
async function feed(query: string, as?: string) {
const path = `/xrpc/dev.hatk.getFeed?feed=location&${query}`;
const res = as ? await server.fetchAs(as, path) : await server.fetch(path);
expect(res.status).toBe(200);
const body = await res.json();
return {
ids: (body.items ?? []).map((i: any) => i.uri.split("/").pop()),
cursor: body.cursor as string | undefined,
};
}
const byName = (name: string, extra = "") => feed(`name=${encodeURIComponent(name)}${extra}`);
beforeAll(async () => {
server = await startTestServer();
const { db } = server;
await db.run(
`CREATE TABLE IF NOT EXISTS _mutes (
did TEXT NOT NULL, subject TEXT NOT NULL, created_at TEXT NOT NULL,
PRIMARY KEY (did, subject)
)`,
);
for (const [did, handle, status] of [
[ALICE, "alice.test", "active"],
[BOB, "bob.test", "active"],
[CAROL, "carol.test", "takendown"],
]) {
await db.run(`INSERT INTO _repos (did, status, handle) VALUES ($1, $2, $3)`, [
did,
status,
handle,
]);
}
// Two Seattle venues in one city cell — newest first is sea-b, then sea-a.
await gallery({
id: "sea-a",
createdAt: "2026-05-01",
cell: SEA_A,
locality: "Seattle",
region: "Washington",
country: "US",
});
await gallery({
id: "sea-b",
createdAt: "2026-05-02",
cell: SEA_B,
locality: "Seattle",
region: "Washington",
country: "US",
});
// Washington DC: locality "Washington" collides with Seattle's region.
await gallery({
id: "dc",
createdAt: "2026-04-01",
cell: DC_10,
locality: "Washington",
region: "District of Columbia",
country: "US",
});
// New York, where locality and region are the same word.
await gallery({
id: "nyc",
createdAt: "2026-03-01",
cell: NYC_10,
locality: "New York",
region: "New York",
country: "US",
});
// Minato, which has no region at all.
await gallery({
id: "tokyo",
createdAt: "2026-02-01",
cell: TOKYO_10,
locality: "Minato",
region: null,
country: "JP",
});
// Stored as ISO-2; the sidebar offers the English name.
await gallery({
id: "athens",
createdAt: "2026-01-01",
cell: ATHENS_10,
locality: "Athens",
region: "Attica",
country: "GR",
});
// A single-word locality that is not a country — the case that makes the
// one-part name ambiguous.
await gallery({
id: "waldport",
createdAt: "2026-06-01",
cell: PDX_10,
locality: "Waldport",
region: "Oregon",
country: "US",
});
// Only a custom label, no structured address.
await gallery({
id: "backyard",
createdAt: "2026-06-02",
cell: PDX_10,
label: "My Backyard",
noAddress: true,
});
// Seattle again, but taken down, and Seattle again from a blockable account.
await gallery({
id: "sea-down",
did: CAROL,
createdAt: "2026-05-03",
cell: SEA_A,
locality: "Seattle",
region: "Washington",
country: "US",
});
await gallery({
id: "sea-bob",
did: BOB,
createdAt: "2026-04-15",
cell: SEA_A,
locality: "Seattle",
region: "Washington",
country: "US",
});
await db.run(`INSERT INTO _mutes (did, subject, created_at) VALUES ($1, $2, 'i')`, [ALICE, BOB]);
});
afterAll(async () => {
await server?.close();
});
describe("location feed, by display name", () => {
test("reads a three-part name as locality, region, country", async () => {
const { ids } = await byName("Seattle, Washington, US");
expect(ids).toEqual(["sea-b", "sea-a", "sea-bob"]);
});
test("does not let Seattle's region pull in Washington DC", async () => {
// The second interpretation reads "Washington, US" as a locality, which DC
// matches on. Only the region guard keeps it out.
const { ids } = await byName("Seattle, Washington, US");
expect(ids).not.toContain("dc");
});
test("ignores a POI prefix and uses the last three parts", async () => {
const { ids } = await byName("The Space Needle, Seattle, Washington, US");
expect(ids).toEqual(["sea-b", "sea-a", "sea-bob"]);
});
test("reads a three-part name as POI, locality, country when that is what it is", async () => {
// "India Street, New York, US" is a POI inside New York, not a locality
// called "India Street" in a region called "New York".
const { ids } = await byName("India Street, New York, US");
expect(ids).toEqual(["nyc"]);
});
test("matches a POI in a locality that has no region", async () => {
const { ids } = await byName("Tokyo Midtown, Minato, JP");
expect(ids).toEqual(["tokyo"]);
});
test("tries a two-part name as both locality and region", async () => {
expect((await byName("Athens, GR")).ids).toEqual(["athens"]);
expect((await byName("Oregon, US")).ids).toEqual(["waldport"]);
});
test("resolves an English country name to the stored ISO-2 code", async () => {
// The record says "GR"; the sidebar offers "Greece".
const { ids } = await byName("Greece");
expect(ids).toEqual(["athens"]);
});
test("falls back to locality when a one-part name is not a country", async () => {
// "Waldport" is tried as a country first and expands to nothing useful;
// the locality reading is what matches.
const { ids } = await byName("Waldport");
expect(ids).toEqual(["waldport"]);
});
test("matches a gallery that has only a custom location label", async () => {
const { ids } = await byName("My Backyard");
expect(ids).toEqual(["backyard"]);
});
test("excludes taken-down repos and, for a viewer, muted accounts", async () => {
const anon = await byName("Seattle, Washington, US");
expect(anon).not.toBeNull();
expect(anon.ids).not.toContain("sea-down");
const viewer = await feed(`name=${encodeURIComponent("Seattle, Washington, US")}`, ALICE);
expect(viewer.ids).toEqual(["sea-b", "sea-a"]);
});
test("falls through to the H3 cell when the name matches nothing", async () => {
// A name that resolves to no gallery still shows the clicked cell rather
// than an empty page.
const { ids } = await byName("Nowhere At All", `&location=${SEA_A}`);
expect(ids).toEqual(["sea-a", "sea-bob"]);
});
test("returns nothing when the name matches nothing and there is no cell", async () => {
const { ids } = await byName("Nowhere At All");
expect(ids).toEqual([]);
});
});
describe("location feed, by H3 cell", () => {
test("a city cell collects every venue inside it", async () => {
const { ids } = await feed(`location=${SEA_5}&limit=50`);
expect(ids).toEqual(["sea-b", "sea-a", "sea-bob"]);
});
test("a city cell excludes taken-down repos", async () => {
const { ids } = await feed(`location=${SEA_5}&limit=50`);
expect(ids).not.toContain("sea-down");
});
test("a city cell excludes muted accounts for a viewer", async () => {
const { ids } = await feed(`location=${SEA_5}&limit=50`, ALICE);
expect(ids).toEqual(["sea-b", "sea-a"]);
});
test("a city cell does not reach venues in other cities", async () => {
const { ids } = await feed(`location=${SEA_5}&limit=50`);
expect(ids).not.toContain("dc");
expect(ids).not.toContain("nyc");
});
test("a venue cell matches only that exact venue", async () => {
expect((await feed(`location=${SEA_B}&limit=50`)).ids).toEqual(["sea-b"]);
expect((await feed(`location=${SEA_A}&limit=50`)).ids).toEqual(["sea-a", "sea-bob"]);
});
test("pages a city cell through its hand-rolled cursor", async () => {
// City-level results are filtered in JS, so pagination is a base64 uri
// cursor rather than the shared paginate() helper.
const first = await feed(`location=${SEA_5}&limit=2`);
expect(first.ids).toEqual(["sea-b", "sea-a"]);
expect(first.cursor).toBeTruthy();
const second = await feed(
`location=${SEA_5}&limit=2&cursor=${encodeURIComponent(first.cursor!)}`,
);
expect(second.ids).toEqual(["sea-bob"]);
expect(second.cursor).toBeUndefined();
});
test("returns nothing with neither a name nor a cell", async () => {
const { ids } = await feed("limit=50");
expect(ids).toEqual([]);
});
});