mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 15:06:32 +00:00
fix(ci): card points re-typed for the non-strict builds, formatting, strict ratchet and two assistant e2e checks
This commit is contained in:
@@ -101,6 +101,5 @@
|
||||
"src/nest/roadtrip/roadtrip-router.service.ts": 12,
|
||||
"src/nest/settings/settings.service.ts": 1,
|
||||
"src/nest/storage/drivers/s3.driver.ts": 1,
|
||||
"src/nest/trips/trips.controller.ts": 1,
|
||||
"src/nest/trips/trips.service.ts": 1
|
||||
}
|
||||
|
||||
@@ -16,7 +16,11 @@ const SHOWN = 8;
|
||||
|
||||
function listCard(input: AnswerInput, rows: Row[], toRow: (raw: Row) => BookingRow): TrekCard | null {
|
||||
const scope = scopeOfCard(input.card);
|
||||
return bookingCard('none', rows.slice(0, SHOWN).map((raw) => bookingItemOf(toRow(raw), scope)), rows.length);
|
||||
return bookingCard(
|
||||
'none',
|
||||
rows.slice(0, SHOWN).map((raw) => bookingItemOf(toRow(raw), scope)),
|
||||
rows.length,
|
||||
);
|
||||
}
|
||||
|
||||
/** A stay listed once: the list joins each booking that links it, so one stay can come twice. */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IMPORT_BOOKING_TOOL } from '../../assistant-tools.constants';
|
||||
import type { TripCard } from '../../assistant-context';
|
||||
import { IMPORT_BOOKING_TOOL } from '../../assistant-tools.constants';
|
||||
import type { DomainCards, PreviewContext, ToolArgs } from '../card-types';
|
||||
import { argId, clipped, idIn } from '../card-values';
|
||||
import { BOOKING_ANSWERS } from './booking-answer';
|
||||
|
||||
@@ -28,7 +28,10 @@ function expenseName(ctx: PreviewContext, args: ToolArgs): string {
|
||||
|
||||
/** Who shares a stored expense, by name. */
|
||||
function sharedBy(ctx: PreviewContext, row: Row | undefined): string {
|
||||
return memberNames(ctx, rowsOf(row?.members).map((member) => member.user_id));
|
||||
return memberNames(
|
||||
ctx,
|
||||
rowsOf(row?.members).map((member) => member.user_id),
|
||||
);
|
||||
}
|
||||
|
||||
/** "Ana → Ben", from the two people of a payment. */
|
||||
|
||||
@@ -117,7 +117,11 @@ export function splitOf(
|
||||
max: number,
|
||||
): Pick<CardExpense, 'split' | 'shares' | 'moreShares'> {
|
||||
if (members.length === 0) return { split: 'none', shares: [], moreShares: 0 };
|
||||
const equal = splitEqualShares(toMinor(total), members.map((member) => ({ user_id: member.id })), itemId);
|
||||
const equal = splitEqualShares(
|
||||
toMinor(total),
|
||||
members.map((member) => ({ user_id: member.id })),
|
||||
itemId,
|
||||
);
|
||||
const shares = members.map((member) => ({
|
||||
person: personFrom(card, member.id, member.name, member.avatarUrl),
|
||||
amount: member.amount ?? (equal[member.id] ?? 0) / 100,
|
||||
|
||||
@@ -73,12 +73,15 @@ export const COLLECTION_CARDS: DomainCards = {
|
||||
list_collections: { answer: listsAnswer },
|
||||
find_place_in_collections: { answer: membershipAnswer },
|
||||
...WRITE_CARDS,
|
||||
create_collection: { ...WRITE_CARDS.create_collection, undo: DELETE_CREATED_LIST },
|
||||
create_collection: { ...writeCards('create_collection', WRITES.create_collection), undo: DELETE_CREATED_LIST },
|
||||
save_place_to_collection: {
|
||||
...WRITE_CARDS.save_place_to_collection,
|
||||
...writeCards('save_place_to_collection', WRITES.save_place_to_collection),
|
||||
undo: DELETE_SAVED_PLACE,
|
||||
argPolicy: NO_IMAGE,
|
||||
},
|
||||
update_collection_place: { ...WRITE_CARDS.update_collection_place, argPolicy: NO_IMAGE },
|
||||
update_collection_place: {
|
||||
...writeCards('update_collection_place', WRITES.update_collection_place),
|
||||
argPolicy: NO_IMAGE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,12 +24,7 @@ import {
|
||||
type ListItem,
|
||||
type PackingTier,
|
||||
} from './packing-rows';
|
||||
import {
|
||||
resolvePackedState,
|
||||
type PackingCardBag,
|
||||
type PackingCardItem,
|
||||
type PackingListCard,
|
||||
} from '@trek/shared';
|
||||
import { resolvePackedState, type PackingCardBag, type PackingCardItem, type PackingListCard } from '@trek/shared';
|
||||
|
||||
/**
|
||||
* The card each packing change shows inside its change card, from the call's
|
||||
@@ -330,37 +325,56 @@ export function packingAssigneesCard(ctx: PreviewContext, args: ToolArgs, loaded
|
||||
const before = rowsOf(recordOf(recordOf(loaded).assignees)[raw]).flatMap((row) => positiveId(row.user_id) ?? []);
|
||||
const inside = tripPacking(ctx.card).filter((item) => !item.placeholder && item.category === raw);
|
||||
const personal = inside.length > 0 && inside.every((item) => item.isPrivate);
|
||||
return packingCard({ type: 'trip' }, 'category', [], [
|
||||
{
|
||||
key: { by: 'category', name, slot: slotOf(ctx.card, raw, personal) },
|
||||
assignees: peopleOf(ctx.card, chosen, 6),
|
||||
assigneesRemoved: peopleOf(ctx.card, before.filter((id) => !chosen.includes(id)), 6),
|
||||
items: [],
|
||||
count: inside.length,
|
||||
packed: inside.filter((item) => item.checked).length,
|
||||
},
|
||||
]);
|
||||
return packingCard(
|
||||
{ type: 'trip' },
|
||||
'category',
|
||||
[],
|
||||
[
|
||||
{
|
||||
key: { by: 'category', name, slot: slotOf(ctx.card, raw, personal) },
|
||||
assignees: peopleOf(ctx.card, chosen, 6),
|
||||
assigneesRemoved: peopleOf(
|
||||
ctx.card,
|
||||
before.filter((id) => !chosen.includes(id)),
|
||||
6,
|
||||
),
|
||||
items: [],
|
||||
count: inside.length,
|
||||
packed: inside.filter((item) => item.checked).length,
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/** A bag head on a card of its own: what a change to one bag shows. */
|
||||
function bagCard(card: TripCard, bag: PackingCardBag): PackingListCard {
|
||||
const inside = tripPacking(card).filter((item) => !item.placeholder && bag.id !== null && item.bagId === bag.id);
|
||||
return packingCard({ type: 'trip' }, 'bag', [bag], [
|
||||
{
|
||||
key: { by: 'bag', bagKey: bag.key },
|
||||
assignees: [],
|
||||
assigneesRemoved: [],
|
||||
items: [],
|
||||
count: inside.length,
|
||||
packed: inside.filter((item) => item.checked).length,
|
||||
},
|
||||
]);
|
||||
return packingCard(
|
||||
{ type: 'trip' },
|
||||
'bag',
|
||||
[bag],
|
||||
[
|
||||
{
|
||||
key: { by: 'bag', bagKey: bag.key },
|
||||
assignees: [],
|
||||
assigneesRemoved: [],
|
||||
items: [],
|
||||
count: inside.length,
|
||||
packed: inside.filter((item) => item.checked).length,
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/** The bag a call names (`bagId`), as the lookup read it. */
|
||||
function namedBag(card: TripCard, args: ToolArgs, loaded: unknown): PackingCardBag | null {
|
||||
const bagId = argId(args, 'bagId');
|
||||
return bagId === null ? null : bagOf(card, rawBags(loaded).find((bag) => bag.id === bagId));
|
||||
return bagId === null
|
||||
? null
|
||||
: bagOf(
|
||||
card,
|
||||
rawBags(loaded).find((bag) => bag.id === bagId),
|
||||
);
|
||||
}
|
||||
|
||||
/** `create_packing_bag`: the new bag's head, in its colour (indigo without one) and with its limit. */
|
||||
|
||||
@@ -204,7 +204,11 @@ export function slotOf(card: TripCard | null, category: string | null, personal:
|
||||
|
||||
/** The people of an assignee list (`[{ user_id }]`), as trip members. */
|
||||
export function assigneesOf(card: TripCard, raw: unknown): CardPerson[] {
|
||||
return peopleOf(card, rowsOf(raw).flatMap((row) => positiveId(row.user_id) ?? []), 6);
|
||||
return peopleOf(
|
||||
card,
|
||||
rowsOf(raw).flatMap((row) => positiveId(row.user_id) ?? []),
|
||||
6,
|
||||
);
|
||||
}
|
||||
|
||||
/** One bag as `list_packing_bags` answers it. */
|
||||
@@ -302,7 +306,11 @@ export function packingGroups(
|
||||
items: readonly ListItem[],
|
||||
options: GroupOptions,
|
||||
): { groups: PackingCardGroup[]; moreGroups: number } {
|
||||
const buckets = bucketsOf(card, items.filter((item) => !item.placeholder), options);
|
||||
const buckets = bucketsOf(
|
||||
card,
|
||||
items.filter((item) => !item.placeholder),
|
||||
options,
|
||||
);
|
||||
let left = options.maxRows ?? MAX_PACKING_ROWS;
|
||||
const perGroup = options.fillSingleGroup && buckets.length === 1 ? left : ROWS_PER_GROUP;
|
||||
const groups = buckets.slice(0, MAX_CARD_GROUPS).map((bucket): PackingCardGroup => {
|
||||
|
||||
@@ -170,7 +170,11 @@ function todoAssigneesCard(ctx: PreviewContext, args: ToolArgs, loaded: TodoAssi
|
||||
category,
|
||||
slot: todoSlot(ctx.card, args.categoryName),
|
||||
assignees: peopleOf(ctx.card, chosen, 6),
|
||||
assigneesRemoved: peopleOf(ctx.card, before.filter((id) => !chosen.includes(id)), 6),
|
||||
assigneesRemoved: peopleOf(
|
||||
ctx.card,
|
||||
before.filter((id) => !chosen.includes(id)),
|
||||
6,
|
||||
),
|
||||
items: [],
|
||||
count: inList.length,
|
||||
open: inList.filter((todo) => !todo.checked).length,
|
||||
|
||||
@@ -113,7 +113,7 @@ export function describeSettings(ctx: PreviewContext, args: Args, loaded: SavedS
|
||||
after: settingText(change.after),
|
||||
});
|
||||
}
|
||||
return { subject: clip(ctx.card.title), rows, dayId: null, placeIds: [] };
|
||||
return { subject: clip(ctx.card.title), rows, dayId: null, placeIds: [], tool: 'update_roadtrip_settings' };
|
||||
}
|
||||
|
||||
export function describeDayEnd(ctx: PreviewContext, args: Args): Described {
|
||||
@@ -129,7 +129,9 @@ export function describeImportRoute(ctx: PreviewContext, args: Args): Described
|
||||
const names = Array.isArray(args.stops)
|
||||
? args.stops.map((stop: unknown) => text(recordOf(stop).name)).filter((name) => name !== '')
|
||||
: [];
|
||||
const rows = dayRows(ctx, dayId).add('quantity', String(names.length)).add('route', clip(routeText(names)));
|
||||
const rows = dayRows(ctx, dayId)
|
||||
.add('quantity', String(names.length))
|
||||
.add('route', clip(routeText(names)));
|
||||
const ends = names.length > 0 ? routeText([names[0] ?? '', names[names.length - 1] ?? '']) : '';
|
||||
return { subject: ends, rows, dayId, placeIds: [] };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { CardReader, PreviewContext, ToolArgs } from '../card-types';
|
||||
import { canonicalJson, clipped, finite, recordOf, type Row } from '../card-values';
|
||||
import { hikeCard } from './tour-route.cards';
|
||||
import {
|
||||
argText,
|
||||
climbOf,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
straightMetres,
|
||||
whole,
|
||||
} from './tour-card-values';
|
||||
import { hikeCard } from './tour-route.cards';
|
||||
import {
|
||||
hikingPreferencesOf,
|
||||
TOUR_ROUTE_LIMITS,
|
||||
|
||||
@@ -104,7 +104,11 @@ export const VACAY_ANSWER_CARDS = {
|
||||
const stats = statsOf(input.value);
|
||||
if (!stats || stats.length === 0) return null;
|
||||
const viewerId = input.card.viewerId;
|
||||
const people = peopleOf(stats.map((row) => row.personId), knownPeople(null, [], stats), viewerId);
|
||||
const people = peopleOf(
|
||||
stats.map((row) => row.personId),
|
||||
knownPeople(null, [], stats),
|
||||
viewerId,
|
||||
);
|
||||
const order = new Map(people.map((person, index) => [person.id, index]));
|
||||
const balances = keepKnown(stats, people)
|
||||
.sort((a, b) => (order.get(a.personId) ?? 0) - (order.get(b.personId) ?? 0))
|
||||
|
||||
@@ -96,7 +96,10 @@ function toggleProposal(viewerId: number, args: ToolArgs, loaded: VacayLoaded, d
|
||||
const was = existing ? counted(existing.kind, existing.fraction) : 0;
|
||||
const will = outcome === 'remove' ? 0 : outcome === 'blocked' ? was : counted(kind, fraction);
|
||||
const year = Number(date.slice(0, 4));
|
||||
const periodYear = periodOf(date, loaded.stats.find((row) => row.personId === personId && row.year === year));
|
||||
const periodYear = periodOf(
|
||||
date,
|
||||
loaded.stats.find((row) => row.personId === personId && row.year === year),
|
||||
);
|
||||
const counts = kind === 'vacation' && (outcome === 'add' || outcome === 'convert');
|
||||
return {
|
||||
date,
|
||||
|
||||
@@ -51,7 +51,9 @@ function tripDatesOf(card: TripCard): string[] {
|
||||
|
||||
/** The dated days of the trip inside `months`, as many as a card holds. */
|
||||
export function tripDatesIn(card: TripCard, months: readonly CardMonth[]): string[] {
|
||||
return tripDatesOf(card).filter((date) => inMonths(date, months)).slice(0, VACAY_CARD_MAX_DATES);
|
||||
return tripDatesOf(card)
|
||||
.filter((date) => inMonths(date, months))
|
||||
.slice(0, VACAY_CARD_MAX_DATES);
|
||||
}
|
||||
|
||||
/** The company holidays inside `months`, each date once. */
|
||||
|
||||
@@ -130,7 +130,10 @@ export class RoadtripPlanningMcp {
|
||||
});
|
||||
const betweenStops = (alongKm: number): [string, string] | null => {
|
||||
if (anchors.length < 2) return null;
|
||||
const index = insertIndexForAlong(anchors.map((anchor) => anchor.alongKm), alongKm);
|
||||
const index = insertIndexForAlong(
|
||||
anchors.map((anchor) => anchor.alongKm),
|
||||
alongKm,
|
||||
);
|
||||
return [anchors[index - 1].name, anchors[index].name];
|
||||
};
|
||||
const hits = new Map<
|
||||
|
||||
@@ -1131,9 +1131,10 @@ describe('Trip assistant e2e', () => {
|
||||
const args = { question, options: ['Trattoria', 'Night market'] };
|
||||
provider.script = [callTools({ id: 'call_poll', name: 'create_collab_poll', args })];
|
||||
const turn = await post(`${t.base}/chat`, t.userId, { ...CTX, threadId: null, text: 'Ask the group' });
|
||||
const toolCallId = ofType(turn.chunks, 'tool-approval-request')[0]?.toolCallId ?? '';
|
||||
const [change] = ofType(turn.chunks, 'data-trek-change');
|
||||
expect(change.data).toMatchObject({
|
||||
toolCallId: 'call_poll',
|
||||
toolCallId,
|
||||
verbKey: 'genericCreate',
|
||||
tool: 'create_collab_poll',
|
||||
domain: 'collab',
|
||||
@@ -1148,7 +1149,7 @@ describe('Trip assistant e2e', () => {
|
||||
provider.script = [say('The poll is up.')];
|
||||
const decided = await post(`${t.base}/threads/${threadId}/decisions`, t.userId, {
|
||||
...CTX,
|
||||
decisions: [{ toolCallId: 'call_poll', approved: true }],
|
||||
decisions: [{ toolCallId, approved: true }],
|
||||
});
|
||||
expect(decided.status).toBe(200);
|
||||
const poll = await findRow(orm, CollabPolls, { trip: t.tripId });
|
||||
@@ -1156,7 +1157,7 @@ describe('Trip assistant e2e', () => {
|
||||
const [applied] = ofType(decided.chunks, 'data-trek-applied');
|
||||
expect(applied.data).toMatchObject({ entity: 'collab_poll', ids: [poll?.id] });
|
||||
expect(ofType(decided.chunks, 'data-trek-card')[0].data).toMatchObject({
|
||||
toolCallId: 'call_poll',
|
||||
toolCallId,
|
||||
card: { kind: 'collabPolls', polls: [{ pollId: poll?.id, change: 'create', question }] },
|
||||
});
|
||||
expect(broadcasts.broadcast).toHaveBeenCalledWith(t.tripId, 'collab:poll:created', expect.anything());
|
||||
|
||||
@@ -222,7 +222,14 @@ describe('answer cards', () => {
|
||||
it('ASTCARD-BUD-004: an empty list, a broken answer and rows without a name or an amount show no card', () => {
|
||||
expect(answer('list_budget_items', { currency: 'EUR', total: 0, items: [] })).toBeNull();
|
||||
expect(answer('list_budget_items', 'not a list')).toBeNull();
|
||||
expect(answer('list_budget_items', { items: [{ id: 1, total_price: 5 }, { id: 2, name: 'Tip' }] })).toBeNull();
|
||||
expect(
|
||||
answer('list_budget_items', {
|
||||
items: [
|
||||
{ id: 1, total_price: 5 },
|
||||
{ id: 2, name: 'Tip' },
|
||||
],
|
||||
}),
|
||||
).toBeNull();
|
||||
expect(answer('get_settlement_summary', { error: 'nope' })).toBeNull();
|
||||
expect(answer('get_budget_per_person', { summary: [] })).toBeNull();
|
||||
expect(answer('list_settlements', { settlements: [] })).toBeNull();
|
||||
|
||||
@@ -143,7 +143,14 @@ describe('the chat card of a read', () => {
|
||||
const row = message(4, {
|
||||
origin: 'assistant',
|
||||
reactions: [
|
||||
{ emoji: '❤️', count: 2, users: [{ user_id: 7, username: 'anna' }, { user_id: 8, username: 'jon' }] },
|
||||
{
|
||||
emoji: '❤️',
|
||||
count: 2,
|
||||
users: [
|
||||
{ user_id: 7, username: 'anna' },
|
||||
{ user_id: 8, username: 'jon' },
|
||||
],
|
||||
},
|
||||
{ emoji: '👍', count: 1, users: [{ user_id: 8, username: 'jon' }] },
|
||||
],
|
||||
attachments: [
|
||||
|
||||
@@ -195,7 +195,12 @@ describe('collection change cards', () => {
|
||||
const deleted = preview('delete_collection', { collectionId: 12 }, { collection });
|
||||
expect(deleted).toMatchObject({ subject: 'Paris food', verbKey: 'genericRemove' });
|
||||
expect(listsOf(deleted.card).lists[0]).toMatchObject({ placeCount: 10, memberCount: 1, change: 'remove' });
|
||||
const lists = { collections: [{ id: 12, name: 'A' }, { id: 13, name: 'B' }] };
|
||||
const lists = {
|
||||
collections: [
|
||||
{ id: 12, name: 'A' },
|
||||
{ id: 13, name: 'B' },
|
||||
],
|
||||
};
|
||||
const order = preview('reorder_collections', { orderedIds: [13, 12] }, { lists });
|
||||
expect(order.subject).toBe('B, A');
|
||||
expect(listsOf(order.card).lists.map((row) => [row.name, row.change])).toEqual([
|
||||
|
||||
@@ -342,7 +342,17 @@ describe('the words and the way back of the other tour writes', () => {
|
||||
expect(unchangedSinceApplied('create_hike', created, onItsDay)).toBe(true);
|
||||
const twice = {
|
||||
places: { places: [hike] },
|
||||
summary: { days: [{ id: 31, assignments: [{ id: 306, place_id: 64 }, { id: 305, place_id: 64 }] }] },
|
||||
summary: {
|
||||
days: [
|
||||
{
|
||||
id: 31,
|
||||
assignments: [
|
||||
{ id: 306, place_id: 64 },
|
||||
{ id: 305, place_id: 64 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(unchangedSinceApplied('create_hike', created, twice)).toBe(false);
|
||||
// A new way saved since (any route write stamps updated_at) keeps the tour, name and day unchanged or not.
|
||||
|
||||
@@ -13,13 +13,7 @@ import type { AnswerInput } from '../../../../../../src/nest/assistant/cards/car
|
||||
import { answerCardOf } from '../../../../../../src/nest/assistant/cards/registry';
|
||||
import { fitted, hardestStretches } from '../../../../../../src/nest/assistant/cards/tour/tour-card-values';
|
||||
import { sacStretches } from '../../../../../../src/nest/assistant/cards/tour/tour-route.cards';
|
||||
import {
|
||||
MAX_CARD_CHARS,
|
||||
trekCardSchema,
|
||||
type TourRouteCard,
|
||||
type TourRouteStretch,
|
||||
type TrekCard,
|
||||
} from '@trek/shared';
|
||||
import { MAX_CARD_CHARS, trekCardSchema, type TourRouteCard, type TourRouteStretch, type TrekCard } from '@trek/shared';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ export const cardLngSchema = z.number().min(-180).max(180);
|
||||
|
||||
/** A point of a line to draw, `[lat, lng]` at five decimals; single points are `{ lat, lng }` objects. */
|
||||
export type CardPoint = [number, number];
|
||||
// Typed by hand: server and client compile without strictNullChecks, where an inferred tuple makes every item optional.
|
||||
export const cardPointSchema: z.ZodType<CardPoint> = z.tuple([cardLatSchema, cardLngSchema]);
|
||||
// Re-typed on the way out: server and client compile without strictNullChecks, where an inferred tuple
|
||||
// makes every item optional and no longer fits a `[number, number]`.
|
||||
export const cardPointSchema = z.tuple([cardLatSchema, cardLngSchema]).transform(([lat, lng]): CardPoint => [lat, lng]);
|
||||
|
||||
export const cardDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
||||
export const cardClockSchema = z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/);
|
||||
|
||||
@@ -38,12 +38,10 @@ const sacStep = z.number().int().min(1).max(6);
|
||||
|
||||
/** A point of the line, `[lat, lng, height]` at five decimals; every height is null when one is missing. */
|
||||
export type TourRoutePoint = [number, number, number | null];
|
||||
// Typed by hand like cardPointSchema: an inferred tuple reads every item as optional where strictNullChecks is off.
|
||||
export const tourRoutePointSchema: z.ZodType<TourRoutePoint> = z.tuple([
|
||||
cardLatSchema,
|
||||
cardLngSchema,
|
||||
height.nullable(),
|
||||
]);
|
||||
// Re-typed on the way out like cardPointSchema, for the builds without strictNullChecks.
|
||||
export const tourRoutePointSchema = z
|
||||
.tuple([cardLatSchema, cardLngSchema, height.nullable()])
|
||||
.transform(([lat, lng, metres]): TourRoutePoint => [lat, lng, metres]);
|
||||
|
||||
/** What a stop is beyond its role: a kind of place along a hike, its highest point, a city walk's station. */
|
||||
export const TOUR_ROUTE_STOP_KINDS = ['highest', 'station'] as const;
|
||||
|
||||
Reference in New Issue
Block a user