@curless/hotel-storefront 0.18.2 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/__tests__/demo-hotels.test.js +21 -2
  2. package/dist/__tests__/demo-hotels.test.js.map +1 -1
  3. package/dist/__tests__/server.test.js +17 -3
  4. package/dist/__tests__/server.test.js.map +1 -1
  5. package/dist/__tests__/tools.test.js +282 -5
  6. package/dist/__tests__/tools.test.js.map +1 -1
  7. package/dist/backend.d.ts +12 -1
  8. package/dist/backend.d.ts.map +1 -1
  9. package/dist/backend.js +7 -1
  10. package/dist/backend.js.map +1 -1
  11. package/dist/card.generated.d.ts +1 -1
  12. package/dist/card.generated.js +2 -2
  13. package/dist/card.generated.js.map +1 -1
  14. package/dist/data.d.ts +2 -0
  15. package/dist/data.d.ts.map +1 -1
  16. package/dist/data.js +24 -7
  17. package/dist/data.js.map +1 -1
  18. package/dist/demo-hotels.d.ts +24 -2
  19. package/dist/demo-hotels.d.ts.map +1 -1
  20. package/dist/demo-hotels.js +313 -95
  21. package/dist/demo-hotels.js.map +1 -1
  22. package/dist/legal.d.ts +1 -1
  23. package/dist/legal.d.ts.map +1 -1
  24. package/dist/legal.js +31 -10
  25. package/dist/legal.js.map +1 -1
  26. package/dist/limits.d.ts +12 -0
  27. package/dist/limits.d.ts.map +1 -0
  28. package/dist/limits.js +47 -0
  29. package/dist/limits.js.map +1 -0
  30. package/dist/members.d.ts +9 -0
  31. package/dist/members.d.ts.map +1 -1
  32. package/dist/members.js +17 -0
  33. package/dist/members.js.map +1 -1
  34. package/dist/money.d.ts +6 -1
  35. package/dist/money.d.ts.map +1 -1
  36. package/dist/money.js +6 -1
  37. package/dist/money.js.map +1 -1
  38. package/dist/present.d.ts +2 -1
  39. package/dist/present.d.ts.map +1 -1
  40. package/dist/present.js +3 -2
  41. package/dist/present.js.map +1 -1
  42. package/dist/server.d.ts +8 -8
  43. package/dist/server.d.ts.map +1 -1
  44. package/dist/server.js +13 -4
  45. package/dist/server.js.map +1 -1
  46. package/dist/surface.d.ts +2 -2
  47. package/dist/surface.d.ts.map +1 -1
  48. package/dist/surface.js +2 -2
  49. package/dist/surface.js.map +1 -1
  50. package/dist/tools.d.ts +39 -13
  51. package/dist/tools.d.ts.map +1 -1
  52. package/dist/tools.js +376 -95
  53. package/dist/tools.js.map +1 -1
  54. package/dist/types.d.ts +12 -4
  55. package/dist/types.d.ts.map +1 -1
  56. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { fileURLToPath } from 'node:url';
2
2
  import { beforeEach, describe, expect, it } from 'vitest';
3
3
  import { loadHotelData } from '../data.js';
4
4
  import { createDemoHotels } from '../demo-hotels.js';
5
+ import { MAX_MISSES, Misses } from '../limits.js';
5
6
  import { MemberStore } from '../members.js';
6
7
  import { handleTool } from '../tools.js';
7
8
  import { TripStore } from '../trip.js';
@@ -195,7 +196,7 @@ describe('building a trip', () => {
195
196
  expect(both.structured.currencies).toEqual(['HKD', 'CNY']);
196
197
  const groups = both.structured.groups;
197
198
  expect(groups.map((g) => g.currency)).toEqual(['HKD', 'CNY']);
198
- expect(both.structured.total).toBe(`${groups[0]?.total} + ${groups[1]?.total}`);
199
+ expect(both.structured.total).toBe(`${groups[0]?.total} · ${groups[1]?.total}`);
199
200
  expect(both.structured.dropped).toBeUndefined();
200
201
  expect(both.text).toMatch(/2 currencies, priced separately and not converted/);
201
202
  const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
@@ -835,7 +836,16 @@ describe('add-ons on a booking already made', () => {
835
836
  expect(again.structured.forBooking.held).toEqual({
836
837
  'airport-transfer': 1,
837
838
  });
838
- // And off again.
839
+ // And off again - once the guest has seen what that does. A confirmation
840
+ // with no preview before it IS the preview (2026-09-28 audit).
841
+ const unseen = await call(t.changeBooking, {
842
+ booking: code,
843
+ extra: 'airport-transfer',
844
+ quantity: 0,
845
+ confirmed: true,
846
+ });
847
+ expect(unseen.structured.view).toBe('change-preview');
848
+ expect(unseen.text).toMatch(/Nothing has changed yet/);
839
849
  const off = await call(t.changeBooking, {
840
850
  booking: code,
841
851
  extra: 'airport-transfer',
@@ -1024,7 +1034,9 @@ describe('choosing where before which hotel', () => {
1024
1034
  // Every hotel counted once, down to its city.
1025
1035
  const cities = china?.countries.flatMap((c) => c.cities) ?? [];
1026
1036
  expect(cities.reduce((n, c) => n + c.count, 0)).toBe(res.structured.total);
1027
- expect(res.text).toMatch(/the card sets them out by region, country and city/);
1037
+ expect(res.text).toMatch(/to choose from by region, country and city - /);
1038
+ // Every place, for a host with no card.
1039
+ expect(res.text).toMatch(/China \(5\)/);
1028
1040
  expect(res.structured.hotels).toBeUndefined();
1029
1041
  });
1030
1042
  it('answers a country of many cities with its cities, and `all` with its hotels', async () => {
@@ -1053,7 +1065,7 @@ describe('choosing where before which hotel', () => {
1053
1065
  const scope = res.structured.scope;
1054
1066
  expect(scope.name).toBe('China');
1055
1067
  expect(scope.cities.length).toBeGreaterThanOrEqual(4);
1056
- expect(res.text).toMatch(/`all` lists the hotels themselves/);
1068
+ expect(res.text).toMatch(/`all` lists every one in China/);
1057
1069
  const all = await call(t.listHotels, { city: 'China', all: true });
1058
1070
  expect(all.structured.view).toBe('hotels');
1059
1071
  });
@@ -1079,9 +1091,61 @@ describe('finding a booking made elsewhere', () => {
1079
1091
  expect(byEmail.structured.count).toBe(0);
1080
1092
  const both = await handleTool(deps, t.listBookings, { email: 'robin@example.com', booking: code }, elsewhere);
1081
1093
  expect(both.structured.count).toBe(1);
1082
- const wrong = await handleTool(deps, t.listBookings, { email: 'someone@example.com', booking: code }, elsewhere);
1094
+ // Found, it is this conversation's now: its code alone opens it here, so
1095
+ // the card's Cancel - which sends nothing more - works on it.
1096
+ const cancel = await handleTool(deps, t.previewCancellation, { booking: code }, elsewhere);
1097
+ expect(cancel.structured.view).toBe('cancellation-preview');
1098
+ // And nothing in any answer carries the conversation it was made in.
1099
+ expect(JSON.stringify(both.structured)).not.toContain('session-');
1100
+ expect(JSON.stringify(both.structured)).not.toContain('owners');
1101
+ const third = { session: 'session-third' };
1102
+ const wrong = await handleTool(deps, t.listBookings, { email: 'someone@example.com', booking: code }, third);
1083
1103
  expect(wrong.structured.count).toBe(0);
1104
+ expect(wrong.structured.notFound).toBe(code);
1084
1105
  expect(wrong.text).toMatch(/Both are needed/);
1106
+ // The id is not a code: with an email, only the code opens a booking.
1107
+ const id = both.structured.bookings[0]?.id ?? '';
1108
+ const byId = await handleTool(deps, t.listBookings, { email: 'robin@example.com', booking: id }, third);
1109
+ expect(byId.structured.count).toBe(0);
1110
+ });
1111
+ it('says a miss even when the conversation has bookings to list', async () => {
1112
+ await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'deluxe-city', ...STAY });
1113
+ await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
1114
+ const res = await call(t.listBookings, { booking: 'SL-NOPE00', email: 'robin@example.com' });
1115
+ expect(res.structured.count).toBe(1);
1116
+ expect(res.structured.notFound).toBe('SL-NOPE00');
1117
+ expect(res.text).toMatch(/^No booking matches that confirmation code and email/);
1118
+ });
1119
+ it('stops taking guesses at a code and an email after a few misses', async () => {
1120
+ await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'deluxe-city', ...STAY });
1121
+ const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
1122
+ const code = made.structured.booking.confirmationCode;
1123
+ const guessing = { deps: { ...deps, misses: new Misses(() => clock) } };
1124
+ const guess = (booking, session) => handleTool(guessing.deps, t.listBookings, { email: 'robin@example.com', booking }, { session });
1125
+ for (let i = 0; i < MAX_MISSES; i += 1)
1126
+ await guess(`SL-WRONG${i}`, 'session-guesser');
1127
+ // The right code now waits too - from this conversation, and for this
1128
+ // email from another one.
1129
+ const blocked = await guess(code, 'session-guesser');
1130
+ expect(blocked.structured.count).toBe(0);
1131
+ expect(blocked.structured.tooMany).toBe(true);
1132
+ expect(blocked.text).toMatch(/paused for a quarter of an hour/);
1133
+ expect((await guess(code, 'session-other')).structured.tooMany).toBe(true);
1134
+ // A conversation's own booking, with no email, is never a guess.
1135
+ expect((await call(t.listBookings, {})).structured.count).toBe(1);
1136
+ });
1137
+ });
1138
+ // ⚠️ A CONFIRMATION CONFIRMS WHAT WAS SHOWN (2026-09-28 audit).
1139
+ describe('confirming a cancellation', () => {
1140
+ it('shows the fee first when nothing was previewed, then cancels on the next confirmation', async () => {
1141
+ await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'deluxe-city', ...STAY });
1142
+ const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
1143
+ const code = made.structured.booking.confirmationCode;
1144
+ const first = await call(t.cancelBooking, { booking: code, confirmed: true });
1145
+ expect(first.structured.view).toBe('cancellation-preview');
1146
+ expect(first.text).toMatch(/Nothing is cancelled yet/);
1147
+ const second = await call(t.cancelBooking, { booking: code, confirmed: true });
1148
+ expect(second.structured.view).toBe('cancelled');
1085
1149
  });
1086
1150
  });
1087
1151
  // ⚠️ A PLACE, THEN THE DATES, THEN HOTELS PRICED FOR THEM (robin, 2026-09-28,
@@ -1177,4 +1241,217 @@ describe('a list one filter emptied', () => {
1177
1241
  expect(res.text).not.toMatch(/together/);
1178
1242
  });
1179
1243
  });
1244
+ // ⚠️ THE 2026-09-28 FULL AUDIT, server side: each of these was reproduced
1245
+ // against the running connector before it was fixed.
1246
+ describe('the audit of 2026-09-28', () => {
1247
+ const pressed = (c, group) => group === 'brands'
1248
+ ? { brand: c.name }
1249
+ : group === 'facilities'
1250
+ ? { facilities: [c.key] }
1251
+ : { [String(c.key)]: true };
1252
+ it.each([
1253
+ ['a ceiling', { city: 'Hong Kong', maxNightly: 3000 }],
1254
+ ['a ceiling on named nights', { city: 'Hong Kong', maxNightly: 3000, ...STAY }],
1255
+ ])('counts every filter chip as what pressing it lists, under %s', async (_, args) => {
1256
+ const res = await call(t.listHotels, args);
1257
+ const refine = res.structured.refine;
1258
+ for (const group of ['brands', 'facilities', 'room']) {
1259
+ for (const chip of refine[group]) {
1260
+ if (chip.on)
1261
+ continue;
1262
+ const after = await call(t.listHotels, { ...args, ...pressed(chip, group) });
1263
+ expect([group, chip.name ?? chip.key, after.structured.total ?? 0]).toEqual([
1264
+ group,
1265
+ chip.name ?? chip.key,
1266
+ chip.count,
1267
+ ]);
1268
+ }
1269
+ }
1270
+ });
1271
+ it('says a ceiling emptied a filtered list, not the filter', async () => {
1272
+ const res = await call(t.listHotels, { city: 'Hong Kong', brand: 'Kerry', maxNightly: 1000 });
1273
+ expect(res.text).toMatch(/^Nothing under HK\$1,000 a night/);
1274
+ expect(res.text).not.toMatch(/None of the/);
1275
+ });
1276
+ it('keeps the order and the ceiling on the way out of a filter that emptied the list', async () => {
1277
+ const res = await call(t.listHotels, {
1278
+ city: 'Hong Kong',
1279
+ brand: 'Kerry',
1280
+ facilities: ['spa'],
1281
+ sort: 'rating',
1282
+ ...STAY,
1283
+ });
1284
+ if (res.structured.ask) {
1285
+ expect(res.structured.ask).toMatchObject({ sort: 'rating', city: 'Hong Kong' });
1286
+ expect(res.structured.stay).toMatchObject(STAY);
1287
+ }
1288
+ });
1289
+ it('keeps `all` on the search it echoes, so a re-query stays a list', async () => {
1290
+ const res = await call(t.listHotels, { all: true });
1291
+ expect(res.structured.view).toBe('hotels');
1292
+ expect(res.structured.ask).toMatchObject({ all: true });
1293
+ const again = await call(t.listHotels, res.structured.ask);
1294
+ expect(again.structured.view).toBe('hotels');
1295
+ });
1296
+ it('puts hotels with no room for the nights last, and never counts them over a ceiling', async () => {
1297
+ const three = { ...STAY, guests: 3 };
1298
+ const high = await call(t.listHotels, { city: 'Hong Kong', ...three, sort: 'price-high' });
1299
+ const tiles = high.structured.hotels;
1300
+ const firstFull = tiles.findIndex((h) => h.noRoomsForStay);
1301
+ expect(firstFull).toBeGreaterThan(0);
1302
+ expect(tiles.slice(firstFull).every((h) => h.noRoomsForStay)).toBe(true);
1303
+ const capped = await call(t.listHotels, { city: 'Hong Kong', ...three, maxNightly: 100000 });
1304
+ expect(capped.structured.overBudget).toBeUndefined();
1305
+ expect(capped.text).not.toMatch(/over the ceiling/);
1306
+ });
1307
+ it('quotes the misses on named nights at what those nights cost', async () => {
1308
+ const weekend = { checkIn: '2026-10-23', checkOut: '2026-10-25', guests: 2 };
1309
+ const res = await call(t.listHotels, { city: 'Hong Kong', ...weekend, maxNightly: 500 });
1310
+ const [first] = res.structured.nearest;
1311
+ expect(first?.forStay?.nightly).toBeDefined();
1312
+ expect(res.text).toContain(`at ${first?.forStay?.nightly}`);
1313
+ });
1314
+ it('says a widened search came back over the ceiling rather than empty', async () => {
1315
+ const res = await call(t.listHotels, { city: 'Hong Kong', query: 'helipad', maxNightly: 500 });
1316
+ expect(res.text).toMatch(/Nothing matched Hong Kong and "helipad", so this looked in Hong Kong/);
1317
+ expect(res.text).toMatch(/Nothing under HK\$500 a night/);
1318
+ expect(res.text).not.toMatch(/found nothing either/);
1319
+ });
1320
+ it('offers no free cancellation once the window has closed', async () => {
1321
+ // Arriving tomorrow: a 48-hour rule closed yesterday; a 24-hour one has not.
1322
+ const soon = { checkIn: '2026-10-02', checkOut: '2026-10-03', guests: 2 };
1323
+ const island = await call(t.getHotel, { hotel: 'hong-kong-island', ...soon });
1324
+ const rooms = island.structured.rooms;
1325
+ expect(rooms.some((r) => r.refundable)).toBe(true);
1326
+ expect(rooms.every((r) => !r.freeCancellationUntil)).toBe(true);
1327
+ expect(island.text).not.toMatch(/free to cancel/);
1328
+ const free = await call(t.listHotels, { city: 'Hong Kong', ...soon, freeCancellation: true });
1329
+ const ids = free.structured.hotels.map((h) => h.id).sort();
1330
+ expect(ids).toEqual(['hong-kong-jen', 'hong-kong-kowloon']);
1331
+ });
1332
+ it('adds an add-on without re-pricing the room, whoever is signed in now', async () => {
1333
+ await call(t.signIn, { number: 'SLC-4471-8820', surname: 'Ruan' });
1334
+ await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'horizon-club', ...STAY });
1335
+ const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
1336
+ const booking = made.structured.booking;
1337
+ await call(t.signOut);
1338
+ const extras = await call(t.listExtras, { booking: booking.confirmationCode });
1339
+ const transfer = extras.structured.extras.find((e) => e.id === 'airport-transfer');
1340
+ const pre = await call(t.previewChange, {
1341
+ booking: booking.confirmationCode,
1342
+ extra: 'airport-transfer',
1343
+ quantity: 1,
1344
+ });
1345
+ // The difference is the transfer and its tax - nothing from the room.
1346
+ const price = Number(String(transfer?.price).replace(/[^\d.]/g, ''));
1347
+ const diff = Number(String(pre.structured.difference).replace(/[^\d.]/g, ''));
1348
+ expect(diff).toBe(price + Math.round((price * 13) / 100));
1349
+ expect(pre.structured.refundDue).toBe(false);
1350
+ });
1351
+ it('names what a change changes', async () => {
1352
+ await call(t.addToTrip, { hotel: 'hong-kong-kowloon', room: 'deluxe', rooms: 2, ...STAY });
1353
+ const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
1354
+ const code = made.structured.booking.confirmationCode;
1355
+ const fewer = await call(t.previewChange, { booking: code, rooms: 1 });
1356
+ expect(fewer.text).toMatch(/would go from 2 rooms to 1 room\./);
1357
+ const none = await call(t.previewChange, { booking: code, rooms: 0 });
1358
+ expect(none.structured.changeable).toBe(false);
1359
+ expect(none.text).toMatch(/at least one room/);
1360
+ const same = await call(t.previewChange, {
1361
+ booking: code,
1362
+ checkOut: '2026-10-22',
1363
+ extra: 'airport-transfer',
1364
+ quantity: 0,
1365
+ });
1366
+ expect(same.text).toMatch(/would move to 2026-10-19 - 2026-10-22\./);
1367
+ });
1368
+ it('charges a non-refundable cancellation exactly what was booked', async () => {
1369
+ await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'horizon-club', ...STAY });
1370
+ const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
1371
+ const booking = made.structured.booking;
1372
+ const pre = await call(t.previewCancellation, { booking: booking.confirmationCode });
1373
+ expect(pre.structured.fee).toBe(booking.quote.total);
1374
+ expect(pre.structured.refund).toBe('HK$0');
1375
+ });
1376
+ it('makes a trip total exactly the sum of the bookings it becomes', async () => {
1377
+ await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'deluxe-city', ...STAY });
1378
+ await call(t.addToTrip, { hotel: 'hong-kong-island', extra: 'airport-transfer', quantity: 1 });
1379
+ const later = { checkIn: '2026-10-21', checkOut: '2026-10-23', guests: 2 };
1380
+ await call(t.addToTrip, { hotel: 'hong-kong-kowloon', room: 'deluxe', ...later });
1381
+ await call(t.addToTrip, { hotel: 'hong-kong-kowloon', extra: 'airport-transfer', quantity: 1 });
1382
+ const trip = await call(t.viewTrip);
1383
+ const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
1384
+ const amount = (x) => Number(x.replace(/[^\d.]/g, ''));
1385
+ const sum = made.structured.bookings.reduce((n, b) => n + amount(b.total), 0);
1386
+ expect(sum).toBe(amount(String(trip.structured.total)));
1387
+ });
1388
+ it('forgets preferences on every conversation signed in as that member', async () => {
1389
+ const other = { session: 'session-other' };
1390
+ const member = { number: 'SLC-4471-8820', surname: 'Ruan' };
1391
+ await call(t.signIn, member);
1392
+ await handleTool(deps, t.signIn, member, other);
1393
+ await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'deluxe-city', ...STAY });
1394
+ await call(t.requestBooking, {
1395
+ name: 'Robin Ruan',
1396
+ email: 'robin@example.com',
1397
+ requests: ['king-bed', 'high-floor'],
1398
+ rememberRequests: true,
1399
+ });
1400
+ await handleTool(deps, t.forgetPreferences, {}, other);
1401
+ const here = await call(t.viewTrip);
1402
+ expect(here.structured.member.preferences).toBeUndefined();
1403
+ });
1404
+ it('adds to saved preferences rather than replacing them, and never keeps an accessible room', async () => {
1405
+ await call(t.signIn, { number: 'SLC-4471-8820', surname: 'Ruan' });
1406
+ const book = async (requests) => {
1407
+ await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'deluxe-city', ...STAY });
1408
+ return call(t.requestBooking, {
1409
+ name: 'Robin Ruan',
1410
+ email: 'robin@example.com',
1411
+ requests,
1412
+ rememberRequests: true,
1413
+ });
1414
+ };
1415
+ await book(['king-bed', 'high-floor']);
1416
+ const second = await book(['quiet-room', 'accessible-room', 'twin-beds']);
1417
+ expect(second.structured.remembered).toEqual(['high-floor', 'quiet-room', 'twin-beds']);
1418
+ });
1419
+ it('compares at the member rate the list and the hotel show', async () => {
1420
+ await call(t.signIn, { number: 'SLC-4471-8820', surname: 'Ruan' });
1421
+ const hotel = await call(t.getHotel, { hotel: 'hong-kong-kerry', ...STAY });
1422
+ const cheapest = hotel.structured.rooms
1423
+ .map((r) => r.stayTotal)
1424
+ .sort((a, b) => Number(a.replace(/\D/g, '')) - Number(b.replace(/\D/g, '')))[0];
1425
+ const compared = await call(t.compare, {
1426
+ entries: [{ hotel: 'hong-kong-kerry' }, { hotel: 'hong-kong-jen' }],
1427
+ ...STAY,
1428
+ });
1429
+ const columns = compared.structured.columns;
1430
+ expect(columns.find((c) => c.hotel.id === 'hong-kong-kerry')?.price?.stayTotal).toBe(cheapest);
1431
+ });
1432
+ it('clamps the room count it says and echoes, and spells a brand as the data does', async () => {
1433
+ const many = await call(t.listHotels, { city: 'Hong Kong', ...STAY, rooms: 9 });
1434
+ expect(many.structured.ask).toMatchObject({ rooms: 5 });
1435
+ expect(many.text).toMatch(/5 rooms/);
1436
+ const kerry = await call(t.listHotels, { city: 'Hong Kong', brand: 'kerry' });
1437
+ const chip = kerry.structured.refine.brands.find((b) => b.name === 'Kerry');
1438
+ expect(chip?.on).toBe(true);
1439
+ });
1440
+ it('says a page past the end is past the end', async () => {
1441
+ const res = await call(t.listHotels, { city: 'Hong Kong', offset: 10 });
1442
+ expect(res.text).toMatch(/past the end: 4 match/);
1443
+ });
1444
+ it('lets a lapsed hold go before pricing a list', async () => {
1445
+ const three = { ...STAY, guests: 3 };
1446
+ await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'suite', ...three });
1447
+ await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
1448
+ const held = await call(t.listHotels, { city: 'Hong Kong', ...three });
1449
+ const island = () => held.structured.hotels;
1450
+ expect(island().find((h) => h.id === 'hong-kong-island')?.noRoomsForStay).toBeDefined();
1451
+ clock += 3 * 3_600_000;
1452
+ const lapsed = await call(t.listHotels, { city: 'Hong Kong', ...three });
1453
+ const tile = lapsed.structured.hotels.find((h) => h.id === 'hong-kong-island');
1454
+ expect(tile?.noRoomsForStay).toBeUndefined();
1455
+ });
1456
+ });
1180
1457
  //# sourceMappingURL=tools.test.js.map