Files
TREK/server/tests/unit/mcp/tools-atlas-expanded.test.ts
T
Maurice 36b8baf259 fix(atlas): removing the last region keeps a country the user marked visited itself
Marking a region wrote its country with source 'manual', the same as ticking
the country off by hand, and removing the last region then took the country
away whatever its origin, Dawarich confirmations included. A country that
only comes in through a region is now stored as 'region', marking it visited
afterwards turns it into the user's own mark, and the cascade in unmarkRegion
only runs for a country without such a mark. Readers still see 'manual' for
those rows, so the country detail, the MCP resource and the plugin RPC answer
as before. Rows written before this change stay 'manual' and keep their
country.

REST, MCP and plugin RPC share the service. The atlas popup on desktop and
phone now asks the server whether the country is still marked before it
drops it from the map instead of guessing.

Refs #2597
2026-10-12 09:55:52 +02:00

673 lines
27 KiB
TypeScript

/**
* Unit tests for MCP atlas expanded tools (atlas addon-gated):
* get_atlas_stats, list_visited_regions, locate_atlas_region, mark_region_visited,
* unmark_region_visited, get_country_atlas_places, update_bucket_list_item.
* Also covers resources trek://atlas/stats and trek://atlas/regions.
*/
import { ADDON_IDS } from '../../../src/addons';
import { db as testDb } from '../../../src/db/database';
import { BucketList } from '../../../src/db/entities/BucketList.entity';
import { HiddenRegions } from '../../../src/db/entities/HiddenRegions.entity';
import { PlaceRegions } from '../../../src/db/entities/PlaceRegions.entity';
import { Places } from '../../../src/db/entities/Places.entity';
import { ReservationEndpoints } from '../../../src/db/entities/ReservationEndpoints.entity';
import { VisitedCountries } from '../../../src/db/entities/VisitedCountries.entity';
import { VisitedRegions } from '../../../src/db/entities/VisitedRegions.entity';
import {
createUser,
createBucketListItem,
createVisitedCountry,
createTrip,
createReservation,
} from '../../helpers/factories';
import { markCountryVisited } from '../../helpers/factories/atlas';
import { makePlace } from '../../helpers/factories/places';
import { countRows, findRow, insertRow, upsertRow } from '../../helpers/factories/rows';
import { makeTrip } from '../../helpers/factories/trips';
import { FakeRealtimeService } from '../../helpers/fake-realtime';
import { createMcpHarness, parseToolResult, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
import { resetTestDb } from '../../helpers/test-db';
import { setAddonEnabled } from '../../helpers/test-db';
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
vi.mock('../../../src/db/database', async () => {
const { createSnapshotTestDb, buildDbMock } = await import('../../helpers/db-mock');
return buildDbMock(createSnapshotTestDb());
});
const realtime = new FakeRealtimeService();
const broadcastMock = realtime.broadcastMock;
beforeEach(() => {
setAddonEnabled(testDb, ADDON_IDS.ATLAS, true);
resetTestDb(testDb);
broadcastMock.mockClear();
delete process.env.DEMO_MODE;
});
let orm: TestOrm;
beforeAll(async () => {
orm = await createTestOrm(testDb);
});
afterAll(async () => {
await orm.close();
testDb.close();
});
async function withHarness(userId: number, fn: (h: McpHarness) => Promise<void>) {
const h = await createMcpHarness({ realtime, userId, withResources: false });
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// The atlas resources register via the nest-mcp registry inside registerTools
// (AtlasMcp @Resource), so the harness must keep tools on for them to attach
// (same shape as tools-vacay.test.ts's withResourceHarness).
async function withResourceHarness(userId: number, fn: (h: McpHarness) => Promise<void>) {
const h = await createMcpHarness({ realtime, userId, withResources: true });
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// A place carrying an address and coordinates, which createPlace does not write.
async function insertPlace(
tripId: number,
name: string,
address: string | null,
lat: number,
lng: number,
): Promise<number> {
return (await makePlace(orm, tripId, { name, address, lat, lng, category: null })).id;
}
// The geocoder's answer for a place, pre-seeded so visitedRegions() reads the cache
// and never reaches for Nominatim (same trick as ATLAS-UNIT-020).
async function cacheRegion(
placeId: number,
countryCode: string,
regionCode: string,
regionName: string,
): Promise<void> {
await upsertRow(orm, PlaceRegions, {
place: placeId,
country_code: countryCode,
region_code: regionCode,
region_name: regionName,
});
}
async function insertEndpoint(
reservationId: number,
role: 'from' | 'to',
sequence: number,
lat: number,
lng: number,
): Promise<void> {
await insertRow(orm, ReservationEndpoints, {
reservation: reservationId,
role,
sequence,
name: `Endpoint ${sequence}`,
lat,
lng,
});
}
/** Marks a region by hand, the way the region route stores it. */
async function markRegion(userId: number, regionCode: string, regionName: string, countryCode: string): Promise<void> {
await insertRow(orm, VisitedRegions, {
user: userId,
region_code: regionCode,
region_name: regionName,
country_code: countryCode,
});
}
/** A wish with no coordinates; returns its id. */
async function insertWish(userId: number, name: string, targetDate?: string): Promise<number> {
return insertRow(orm, BucketList, {
user: userId,
name,
...(targetDate !== undefined ? { target_date: targetDate } : { lat: null, lng: null }),
});
}
// Trips have to sit in the past for their countries and regions to count as
// visited rather than planned (#1048).
const PAST_START = '2023-05-01';
const PAST_END = '2023-05-10';
// ---------------------------------------------------------------------------
// get_atlas_stats
// ---------------------------------------------------------------------------
describe('Tool: get_atlas_stats', () => {
it('returns stats object without error for empty data', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'get_atlas_stats', arguments: {} });
expect(result.isError).toBeFalsy();
const data = parseToolResult(result) as any;
expect(data.stats).toBeDefined();
expect(data.travel).toEqual({
countries: [],
cities: [],
totalTrips: 0,
totalDays: 0,
totalPlaces: 0,
totalDistanceKm: 0,
});
});
});
it('carries the passport figures REST answers with: the cities by name and the distance flown', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id, { title: 'Rome', start_date: PAST_START, end_date: PAST_END });
await insertPlace(trip.id, 'Colosseum', 'Colosseum, Rome, Italy', 41.8902, 12.4922);
const flight = createReservation(testDb, trip.id, { type: 'flight', title: 'FCO-JFK' });
await insertEndpoint(flight.id, 'from', 0, 41.8003, 12.2389);
await insertEndpoint(flight.id, 'to', 1, 40.6413, -73.7781);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'get_atlas_stats', arguments: {} });
const data = parseToolResult(result) as any;
// The count was always there; the names behind it were not.
expect(data.stats.stats.totalCities).toBe(1);
expect(data.travel.cities).toEqual(['Rome']);
expect(data.travel.countries).toContain('IT');
expect(data.travel.countries).toContain('US');
// Rome to New York, so a four-figure number rather than a rounding artefact.
expect(data.travel.totalDistanceKm).toBeGreaterThan(6000);
expect(data.travel.totalPlaces).toBe(await countRows(orm, Places, { trip: trip.id }));
// Rendering data stays out unless asked for.
expect(data.travel.coords).toBeUndefined();
});
});
it('include_coords adds the per-place coordinates the dashboard map plots', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id, { title: 'Rome', start_date: PAST_START, end_date: PAST_END });
await insertPlace(trip.id, 'Colosseum', 'Colosseum, Rome, Italy', 41.8902, 12.4922);
await insertPlace(trip.id, 'Pantheon', 'Pantheon, Rome, Italy', 41.8986, 12.4769);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'get_atlas_stats', arguments: { include_coords: true } });
const data = parseToolResult(result) as any;
const withCoords = await countRows(orm, Places, { trip: trip.id, lat: { $ne: null }, lng: { $ne: null } });
expect(data.travel.coords).toHaveLength(withCoords);
expect(data.travel.coords[0]).toEqual({ lat: 41.8902, lng: 12.4922 });
});
});
it('refuses a non-boolean include_coords', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'get_atlas_stats', arguments: { include_coords: 'yes' } });
expect(result.isError).toBe(true);
expect((result.content[0] as any).text).toContain('include_coords');
});
});
});
// ---------------------------------------------------------------------------
// list_visited_regions
// ---------------------------------------------------------------------------
describe('Tool: list_visited_regions', () => {
it('returns an empty grouping initially', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'list_visited_regions', arguments: {} });
const data = parseToolResult(result) as any;
expect(data.regions).toEqual({});
});
});
it('groups a manual mark under its country, in the shape the map reads', async () => {
const { user } = createUser(testDb);
await markRegion(user.id, 'FR-75', 'Paris', 'FR');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'list_visited_regions', arguments: {} });
const data = parseToolResult(result) as any;
expect(data.regions.FR).toEqual([
{ code: 'FR-75', name: 'Paris', placeCount: 0, status: 'visited', manuallyMarked: true },
]);
});
});
it('reports regions derived from trip places, which the manual table never held', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id, { title: 'Rome', start_date: PAST_START, end_date: PAST_END });
await cacheRegion(await insertPlace(trip.id, 'Colosseum', null, 41.8902, 12.4922), 'IT', 'IT-62', 'Lazio');
await cacheRegion(await insertPlace(trip.id, 'Pantheon', null, 41.8986, 12.4769), 'IT', 'IT-62', 'Lazio');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'list_visited_regions', arguments: {} });
const data = parseToolResult(result) as any;
expect(data.regions.IT).toEqual([{ code: 'IT-62', name: 'Lazio', placeCount: 2, status: 'visited' }]);
// Nothing was ever marked by hand, so the old manual-only read had no row
// to answer with at all.
expect(await countRows(orm, VisitedRegions, { user: user.id })).toBe(0);
});
});
it('drops a region the user dismissed, matching the map', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id, { title: 'Rome', start_date: PAST_START, end_date: PAST_END });
await cacheRegion(await insertPlace(trip.id, 'Colosseum', null, 41.8902, 12.4922), 'IT', 'IT-62', 'Lazio');
await withHarness(user.id, async (h) => {
const unmark = await h.client.callTool({ name: 'unmark_region_visited', arguments: { regionCode: 'IT-62' } });
expect(parseToolResult(unmark)).toEqual({ success: true });
const tombstone = await findRow(orm, HiddenRegions, { user: user.id, region_code: 'IT-62' });
expect(tombstone).toBeTruthy();
const result = await h.client.callTool({ name: 'list_visited_regions', arguments: {} });
const data = parseToolResult(result) as any;
expect(data.regions.IT).toBeUndefined();
});
});
});
// ---------------------------------------------------------------------------
// locate_atlas_region
// ---------------------------------------------------------------------------
describe('Tool: locate_atlas_region', () => {
it('resolves a coordinate to the country and the region the map can highlight', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'locate_atlas_region',
arguments: { lat: 41.9028, lng: 12.4964 },
});
expect(result.isError).toBeFalsy();
const data = parseToolResult(result) as any;
// Rome. Italy has admin1 coverage in the bundle, so the region resolves too,
// and the code comes back in the form mark_region_visited takes.
expect(data.country_code).toBe('IT');
expect(typeof data.region_code).toBe('string');
expect(typeof data.region_name).toBe('string');
});
});
it('feeds mark_region_visited without the caller knowing any codes', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const located = parseToolResult(
await h.client.callTool({ name: 'locate_atlas_region', arguments: { lat: 41.9028, lng: 12.4964 } }),
) as any;
expect(typeof located.region_code).toBe('string');
await h.client.callTool({
name: 'mark_region_visited',
arguments: {
regionCode: located.region_code,
regionName: located.region_name,
countryCode: located.country_code,
},
});
const row = await findRow(orm, VisitedRegions, { user: user.id });
expect({ region_code: row?.region_code, country_code: row?.country_code }).toEqual({
region_code: located.region_code,
country_code: 'IT',
});
});
});
it('answers with nulls out at sea rather than failing', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'locate_atlas_region', arguments: { lat: 0, lng: -140 } });
expect(result.isError).toBeFalsy();
expect(parseToolResult(result)).toEqual({ country_code: null, region_code: null, region_name: null });
});
});
it('refuses an out-of-range coordinate, like the REST route does', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const badLat = await h.client.callTool({ name: 'locate_atlas_region', arguments: { lat: 91, lng: 12.5 } });
expect(badLat.isError).toBe(true);
expect((badLat.content[0] as any).text).toContain('lat');
const badLng = await h.client.callTool({ name: 'locate_atlas_region', arguments: { lat: 41.9, lng: 181 } });
expect(badLng.isError).toBe(true);
expect((badLng.content[0] as any).text).toContain('lng');
});
});
});
// ---------------------------------------------------------------------------
// mark_region_visited
// ---------------------------------------------------------------------------
describe('Tool: mark_region_visited', () => {
it('uppercases both codes like REST does (post-fold quirk fix)', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'mark_region_visited',
arguments: { regionCode: 'jp-13', regionName: 'Tokyo', countryCode: 'jp' },
});
const data = parseToolResult(result) as any;
// Legacy stored 'jp-13'/'jp' verbatim, creating rows REST's uppercased
// unmark could never hit; both codes now normalize.
expect(data.region.code).toBe('JP-13');
expect(data.region.country_code).toBe('JP');
const unmark = await h.client.callTool({
name: 'unmark_region_visited',
arguments: { regionCode: 'jp-13' },
});
expect(parseToolResult(unmark)).toEqual({ success: true });
expect(await findRow(orm, VisitedRegions, { user: user.id })).toBeNull();
});
});
it('inserts region and returns region object', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'mark_region_visited',
arguments: { regionCode: 'US-CA', regionName: 'California', countryCode: 'US' },
});
const data = parseToolResult(result) as any;
// Echoed in the client-facing shape ({ code, name, ... }), not raw DB columns.
expect(data.region).toBeDefined();
expect(data.region.code).toBe('US-CA');
expect(data.region.name).toBe('California');
expect(data.region.country_code).toBe('US');
expect(data.region.manuallyMarked).toBe(true);
const row = await findRow(orm, VisitedRegions, { user: user.id, region_code: 'US-CA' });
expect(row).toBeTruthy();
});
});
it('blocks demo user', async () => {
process.env.DEMO_MODE = 'true';
const { user } = createUser(testDb, { email: 'demo@nomad.app' });
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'mark_region_visited',
arguments: { regionCode: 'DE-BY', regionName: 'Bavaria', countryCode: 'DE' },
});
expect(result.isError).toBe(true);
});
});
});
// ---------------------------------------------------------------------------
// unmark_region_visited
// ---------------------------------------------------------------------------
describe('Tool: unmark_region_visited', () => {
it('removes region and returns success', async () => {
const { user } = createUser(testDb);
await markRegion(user.id, 'IT-LO', 'Lombardy', 'IT');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'unmark_region_visited',
arguments: { regionCode: 'IT-LO' },
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
expect(await findRow(orm, VisitedRegions, { user: user.id, region_code: 'IT-LO' })).toBeNull();
});
});
it('keeps a country the user marked itself when its last region goes, like REST (#2597)', async () => {
const { user } = createUser(testDb);
await markCountryVisited(orm, user.id, 'NO');
await markRegion(user.id, 'NO-03', 'Oslo', 'NO');
await insertRow(orm, VisitedCountries, { user: user.id, country_code: 'SE', source: 'region' });
await markRegion(user.id, 'SE-AB', 'Stockholm', 'SE');
await withHarness(user.id, async (h) => {
await h.client.callTool({ name: 'unmark_region_visited', arguments: { regionCode: 'NO-03' } });
await h.client.callTool({ name: 'unmark_region_visited', arguments: { regionCode: 'SE-AB' } });
});
expect(await findRow(orm, VisitedCountries, { user: user.id, country_code: 'NO' })).toBeTruthy();
// A country that only came in through its region still goes with it.
expect(await findRow(orm, VisitedCountries, { user: user.id, country_code: 'SE' })).toBeNull();
});
it('succeeds even when region was not marked (no-op)', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'unmark_region_visited',
arguments: { regionCode: 'XX-YY' },
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
});
});
});
// ---------------------------------------------------------------------------
// get_country_atlas_places
// ---------------------------------------------------------------------------
describe('Tool: get_country_atlas_places', () => {
it('returns empty places array for a new user', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'get_country_atlas_places',
arguments: { countryCode: 'JP' },
});
const data = parseToolResult(result) as any;
expect(data.places).toBeDefined();
expect(Array.isArray(data.places)).toBe(true);
});
});
it('uppercases the country code like REST does (post-fold quirk fix)', async () => {
const { user } = createUser(testDb);
await markCountryVisited(orm, user.id, 'JP');
// A trip so countryPlaces reaches the visited_countries lookup pre-quirk-fix too.
await makeTrip(orm, user.id, { title: 'Japan' });
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'get_country_atlas_places',
arguments: { countryCode: 'jp' },
});
const data = parseToolResult(result) as any;
// Legacy passed 'jp' through verbatim and the lookup matched nothing.
expect(data.manually_marked).toBe(true);
});
});
});
// ---------------------------------------------------------------------------
// update_bucket_list_item
// ---------------------------------------------------------------------------
describe('Tool: update_bucket_list_item', () => {
it('updates notes and returns item', async () => {
const { user } = createUser(testDb);
const itemId = await insertWish(user.id, 'Visit Tokyo');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_bucket_list_item',
arguments: { itemId, notes: 'Cherry blossom season preferred' },
});
const data = parseToolResult(result) as any;
expect(data.item).toBeDefined();
expect(data.item.notes).toBe('Cherry blossom season preferred');
});
});
it('updates name of existing item', async () => {
const { user } = createUser(testDb);
const itemId = await insertWish(user.id, 'Old Name');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_bucket_list_item',
arguments: { itemId, name: 'New Name' },
});
const data = parseToolResult(result) as any;
expect(data.item.name).toBe('New Name');
});
});
it('returns isError for non-existent item', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_bucket_list_item',
arguments: { itemId: 99999, notes: 'Will not work' },
});
expect(result.isError).toBe(true);
});
});
it('returns isError when the edit would land on another wish (#1898)', async () => {
const { user } = createUser(testDb);
await insertWish(user.id, 'Japan', '2027-05');
const itemId = await insertWish(user.id, 'Japan', '2028-09');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_bucket_list_item',
arguments: { itemId, target_date: '2027-05' },
});
expect(result.isError).toBe(true);
const row = await findRow(orm, BucketList, { id: itemId });
expect(row?.target_date).toBe('2028-09');
});
});
it('blocks demo user', async () => {
process.env.DEMO_MODE = 'true';
const { user } = createUser(testDb, { email: 'demo@nomad.app' });
const itemId = await insertWish(user.id, 'Bucket Item');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_bucket_list_item',
arguments: { itemId, notes: 'blocked' },
});
expect(result.isError).toBe(true);
});
});
});
// ---------------------------------------------------------------------------
// Resource: trek://atlas/stats
// ---------------------------------------------------------------------------
describe('Resource: trek://atlas/stats', () => {
it('returns stats object', async () => {
const { user } = createUser(testDb);
await withResourceHarness(user.id, async (h) => {
const result = await h.client.readResource({ uri: 'trek://atlas/stats' });
const data = parseResourceResult(result) as any;
expect(data).toBeDefined();
});
});
});
// ---------------------------------------------------------------------------
// Resource: trek://atlas/regions
// ---------------------------------------------------------------------------
describe('Resource: trek://atlas/regions', () => {
it('returns the country grouping', async () => {
const { user } = createUser(testDb);
await withResourceHarness(user.id, async (h) => {
const result = await h.client.readResource({ uri: 'trek://atlas/regions' });
const data = parseResourceResult(result) as any;
expect(data).toEqual({});
});
});
it('returns marked and derived regions together, as the tool does', async () => {
const { user } = createUser(testDb);
await markRegion(user.id, 'ES-CT', 'Catalonia', 'ES');
const trip = createTrip(testDb, user.id, { title: 'Rome', start_date: PAST_START, end_date: PAST_END });
await cacheRegion(await insertPlace(trip.id, 'Colosseum', null, 41.8902, 12.4922), 'IT', 'IT-62', 'Lazio');
await withResourceHarness(user.id, async (h) => {
const result = await h.client.readResource({ uri: 'trek://atlas/regions' });
const data = parseResourceResult(result) as any;
expect(data.ES).toEqual([
{ code: 'ES-CT', name: 'Catalonia', placeCount: 0, status: 'visited', manuallyMarked: true },
]);
expect(data.IT).toEqual([{ code: 'IT-62', name: 'Lazio', placeCount: 1, status: 'visited' }]);
});
});
});
// ---------------------------------------------------------------------------
// Resource: trek://bucket-list (moved from resources.test.ts with the fold —
// that suite's withTools:false harness never attaches registry resources)
// ---------------------------------------------------------------------------
describe('Resource: trek://bucket-list', () => {
it("returns only the current user's bucket list items", async () => {
const { user } = createUser(testDb);
const { user: other } = createUser(testDb);
createBucketListItem(testDb, user.id, { name: 'Tokyo' });
createBucketListItem(testDb, other.id, { name: 'Rome' });
await withResourceHarness(user.id, async (h) => {
const result = await h.client.readResource({ uri: 'trek://bucket-list' });
const items = parseResourceResult(result) as any[];
expect(items).toHaveLength(1);
expect(items[0].name).toBe('Tokyo');
});
});
it('returns empty array for user with no items', async () => {
const { user } = createUser(testDb);
await withResourceHarness(user.id, async (h) => {
const result = await h.client.readResource({ uri: 'trek://bucket-list' });
const items = parseResourceResult(result) as any[];
expect(items).toEqual([]);
});
});
});
// ---------------------------------------------------------------------------
// Resource: trek://visited-countries (moved from resources.test.ts, same reason)
// ---------------------------------------------------------------------------
describe('Resource: trek://visited-countries', () => {
it("returns only the current user's visited countries", async () => {
const { user } = createUser(testDb);
const { user: other } = createUser(testDb);
createVisitedCountry(testDb, user.id, 'FR');
createVisitedCountry(testDb, user.id, 'JP');
createVisitedCountry(testDb, other.id, 'DE');
await withResourceHarness(user.id, async (h) => {
const result = await h.client.readResource({ uri: 'trek://visited-countries' });
const countries = parseResourceResult(result) as any[];
expect(countries).toHaveLength(2);
const codes = countries.map((c) => c.country_code);
expect(codes).toContain('FR');
expect(codes).toContain('JP');
expect(codes).not.toContain('DE');
});
});
it('returns empty array for user with no visited countries', async () => {
const { user } = createUser(testDb);
await withResourceHarness(user.id, async (h) => {
const result = await h.client.readResource({ uri: 'trek://visited-countries' });
const countries = parseResourceResult(result) as any[];
expect(countries).toEqual([]);
});
});
});