mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
fix(ci): typed card points, the managed take route, and the failing card tests and ratchets
This commit is contained in:
@@ -16,8 +16,8 @@
|
||||
"no-misleading-character-class": 1
|
||||
},
|
||||
"tests": {
|
||||
"@typescript-eslint/no-explicit-any": 1564,
|
||||
"@typescript-eslint/no-non-null-assertion": 1757,
|
||||
"@typescript-eslint/no-explicit-any": 1562,
|
||||
"@typescript-eslint/no-non-null-assertion": 1755,
|
||||
"@typescript-eslint/no-require-imports": 25,
|
||||
"@typescript-eslint/no-unsafe-function-type": 7,
|
||||
"@typescript-eslint/no-unused-vars": 96,
|
||||
|
||||
@@ -51,26 +51,29 @@ export class TrekPhotoSignalsRepository extends TrekRepository<TrekPhotoSignals>
|
||||
|
||||
/** The measurements stored for these photos, whatever version made them; a photo without one is left out. */
|
||||
async listForPhotos(photoIds: number[]): Promise<TrekPhotoSignalRow[]> {
|
||||
const out: TrekPhotoSignalRow[] = [];
|
||||
for (let i = 0; i < photoIds.length; i += IN_CHUNK) {
|
||||
const rows = await this.db()
|
||||
.selectFrom('trek_photo_signals')
|
||||
.select([
|
||||
'photo_id',
|
||||
'algo',
|
||||
'phash',
|
||||
'sharpness',
|
||||
'luma',
|
||||
'clipped',
|
||||
'orig_width',
|
||||
'orig_height',
|
||||
'computed_at',
|
||||
])
|
||||
.where('photo_id', 'in', photoIds.slice(i, i + IN_CHUNK))
|
||||
.execute();
|
||||
out.push(...rows);
|
||||
}
|
||||
return out;
|
||||
const chunks = Array.from({ length: Math.ceil(photoIds.length / IN_CHUNK) }, (_, i) =>
|
||||
photoIds.slice(i * IN_CHUNK, (i + 1) * IN_CHUNK),
|
||||
);
|
||||
const found = await Promise.all(
|
||||
chunks.map((chunk) =>
|
||||
this.db()
|
||||
.selectFrom('trek_photo_signals')
|
||||
.select([
|
||||
'photo_id',
|
||||
'algo',
|
||||
'phash',
|
||||
'sharpness',
|
||||
'luma',
|
||||
'clipped',
|
||||
'orig_width',
|
||||
'orig_height',
|
||||
'computed_at',
|
||||
])
|
||||
.where('photo_id', 'in', chunk)
|
||||
.execute(),
|
||||
),
|
||||
);
|
||||
return found.flat();
|
||||
}
|
||||
|
||||
/** Stores a measurement, replacing the photo's earlier one. */
|
||||
|
||||
@@ -11,6 +11,9 @@ import type { CollectionDetailResponse, CollectionItemsRequest, CollectionItemsR
|
||||
/** The most lists one lookup opens: 50 places and 20 labels can sit in no more than 70. */
|
||||
const MAX_LISTS = 70;
|
||||
|
||||
/** A list the lookup opened, with whether the user may take places out of it. */
|
||||
type OpenedList = CollectionDetailResponse & { canDelete: boolean };
|
||||
|
||||
/**
|
||||
* Saved places and labels by their own ids, wherever they are filed (the
|
||||
* get_collection_items tool): what an assistant's card needs to name a place
|
||||
@@ -30,23 +33,13 @@ export class CollectionsItemsService {
|
||||
async getItems(userId: number, request: CollectionItemsRequest): Promise<CollectionItemsResponse> {
|
||||
const placeIds = [...new Set(request.placeIds ?? [])];
|
||||
const labelIds = [...new Set(request.labelIds ?? [])];
|
||||
const listIds = new Set<number>();
|
||||
for (const placeId of placeIds) {
|
||||
const listId = await this.collectionsRepo.collectionIdOfPlace(placeId);
|
||||
if (listId !== undefined) listIds.add(listId);
|
||||
}
|
||||
for (const labelId of labelIds) {
|
||||
const listId = await this.labels.collectionIdOf(labelId);
|
||||
if (listId !== undefined) listIds.add(listId);
|
||||
}
|
||||
const read: Array<CollectionDetailResponse & { canDelete: boolean }> = [];
|
||||
for (const listId of [...listIds].slice(0, MAX_LISTS)) {
|
||||
const detail = await this.readable(userId, listId);
|
||||
if (!detail) continue;
|
||||
// Places leave a list only by its owner or an admin (assertCanDelete); an editor adds.
|
||||
const role = await this.collections.roleOf(userId, listId);
|
||||
read.push({ ...detail, canDelete: role === 'owner' || role === 'admin' });
|
||||
}
|
||||
const [ofPlaces, ofLabels] = await Promise.all([
|
||||
Promise.all(placeIds.map((placeId) => this.collectionsRepo.collectionIdOfPlace(placeId))),
|
||||
Promise.all(labelIds.map((labelId) => this.labels.collectionIdOf(labelId))),
|
||||
]);
|
||||
const listIds = new Set([...ofPlaces, ...ofLabels].filter((listId): listId is number => listId !== undefined));
|
||||
const opened = await Promise.all([...listIds].slice(0, MAX_LISTS).map((listId) => this.withDelete(userId, listId)));
|
||||
const read = opened.filter((list): list is OpenedList => list !== null);
|
||||
const wantedPlaces = new Set(placeIds);
|
||||
const wantedLabels = new Set(labelIds);
|
||||
return {
|
||||
@@ -71,6 +64,15 @@ export class CollectionsItemsService {
|
||||
};
|
||||
}
|
||||
|
||||
/** One list as its members read it, with whether the user may take places out of it. */
|
||||
private async withDelete(userId: number, listId: number): Promise<OpenedList | null> {
|
||||
const detail = await this.readable(userId, listId);
|
||||
if (!detail) return null;
|
||||
// Places leave a list only by its owner or an admin (assertCanDelete); an editor adds.
|
||||
const role = await this.collections.roleOf(userId, listId);
|
||||
return { ...detail, canDelete: role === 'owner' || role === 'admin' };
|
||||
}
|
||||
|
||||
/** One list as its members read it; null for one the user may not see or that is gone. */
|
||||
private async readable(userId: number, listId: number): Promise<CollectionDetailResponse | null> {
|
||||
try {
|
||||
|
||||
@@ -93,6 +93,7 @@ export const MANAGED_ROUTE_ALLOW_LIST: string[] = [
|
||||
'AssistantController.deleteThread',
|
||||
'AssistantController.share',
|
||||
'AssistantController.stop',
|
||||
'AssistantController.takeProposal',
|
||||
'AssistantController.thread',
|
||||
'AssistantController.threads',
|
||||
'AssistantController.undo',
|
||||
|
||||
@@ -299,16 +299,19 @@ describe('journey read tools', () => {
|
||||
const { user } = createUser(testDb);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const journey = await seedJourney(h, 'Berlin 2026');
|
||||
for (const [entry_date, title] of [
|
||||
const days = [
|
||||
['2026-10-20', 'Before'],
|
||||
['2026-10-21', 'Yesterday'],
|
||||
['2026-10-22', 'Today'],
|
||||
] as const) {
|
||||
] as const;
|
||||
// One after the other, in this order.
|
||||
await days.reduce(async (previous, [entry_date, title]) => {
|
||||
await previous;
|
||||
await h.client.callTool({
|
||||
name: 'create_journey_entry',
|
||||
arguments: { journeyId: journey.id, entry_date, title },
|
||||
});
|
||||
}
|
||||
}, Promise.resolve());
|
||||
const read = async (range: Record<string, string>) =>
|
||||
parseToolResult(
|
||||
await h.client.callTool({ name: 'list_journey_entries', arguments: { journeyId: journey.id, ...range } }),
|
||||
|
||||
@@ -218,7 +218,6 @@ describe('AssistantProposalsService', () => {
|
||||
await expect(take(c, 'w1')).rejects.toMatchObject({ status: 429, code: 'too_many_requests' });
|
||||
expect(await calls(c.threadId)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('ASTPROP-008: a walk stored cut down and masked is created from the tool asked again, named and on no day', async () => {
|
||||
const c = await conversation();
|
||||
|
||||
@@ -72,7 +72,8 @@ beforeAll(async () => {
|
||||
orm = app.get(MikroORM);
|
||||
registry = app.get(McpRegistryService);
|
||||
tools = app.get(AssistantToolsService);
|
||||
await Promise.all(['packing', 'budget', 'collections'].map((addon) => setAddonEnabled(orm, addon, true)));
|
||||
const addons = ['packing', 'budget', 'collections', 'journey', 'roadtrip'];
|
||||
await Promise.all(addons.map((addon) => setAddonEnabled(orm, addon, true)));
|
||||
const { user } = await makeUser(orm);
|
||||
listed = await listRaw(user.id);
|
||||
});
|
||||
|
||||
@@ -129,7 +129,8 @@ describe('the to-do change cards', () => {
|
||||
});
|
||||
|
||||
it("ASTTODO-007: a list's default people show the ones it will have and the ones it drops", () => {
|
||||
const loaded = { assignees: { Bookings: [{ user_id: 2 }] } };
|
||||
// What the loader hands on: the read's assignees by list name, unwrapped.
|
||||
const loaded = { Bookings: [{ user_id: 2 }] };
|
||||
const card = todos(change('set_todo_category_assignees', { categoryName: 'Bookings', userIds: [1] }, loaded));
|
||||
expect(card.groups).toEqual([
|
||||
{
|
||||
|
||||
@@ -150,7 +150,9 @@ describe('via change cards', () => {
|
||||
['Seefeld', true],
|
||||
['Bolzano', true],
|
||||
]);
|
||||
const applied = appliedChange('import_google_maps_route', 'call_9', args, { place_ids: [41, 42] }, 31, null);
|
||||
// The way back takes the visits it added, not the places.
|
||||
const result = { place_ids: [51, 52], assignment_ids: [41, 42] };
|
||||
const applied = appliedChange('import_google_maps_route', 'call_9', args, result, 31, null);
|
||||
expect(applied).toMatchObject({ entity: 'assignment', ids: [41, 42], dayId: 31 });
|
||||
});
|
||||
|
||||
|
||||
@@ -186,8 +186,9 @@ describe('calculate_roadtrip card', () => {
|
||||
const built = roadtripAnswerCard(input(answer([day(3)]))) as RoadtripCard;
|
||||
const lines = built.days[0].lines;
|
||||
expect(lines.length).toBe(2);
|
||||
// No line reaches from Bavaria to Rome: the flight between the two terminals is no drive.
|
||||
expect(lines.some((line) => line.some(([lat]) => lat > 47) && line.some(([lat]) => lat < 42))).toBe(false);
|
||||
const points = lines.flat();
|
||||
expect(points.some(([lat]) => lat > 42 && lat < 48)).toBe(false);
|
||||
for (const [lat, lng] of points) {
|
||||
expect(Math.round(lat * 1e5) / 1e5).toBe(lat);
|
||||
expect(Math.round(lng * 1e5) / 1e5).toBe(lng);
|
||||
|
||||
@@ -452,8 +452,8 @@ describe('a held company holiday and allowance', () => {
|
||||
expect(whole.proposal?.notes).toEqual([]);
|
||||
// Company holidays off in the plan: the planner would refuse it, the tool writes it, the card warns.
|
||||
const companyOff = plan({ company_holidays_enabled: false });
|
||||
const off = await loaded({ ...CALENDAR_YEAR, [readKey('get_vacay_plan')]: companyOff });
|
||||
const unseen = change('toggle_company_holiday', { date: '2026-10-03' }, off);
|
||||
const offFound = await loaded({ ...CALENDAR_YEAR, [readKey('get_vacay_plan')]: companyOff });
|
||||
const unseen = change('toggle_company_holiday', { date: '2026-10-03' }, offFound);
|
||||
expect(unseen.proposal?.notes).toEqual(['companyOff', 'publicHoliday']);
|
||||
});
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ import type { McpContext } from '../../../src/nest-mcp';
|
||||
import { getEntry, type ClassRef } from '../../../src/nest-mcp/metadata';
|
||||
import { AddonsService } from '../../../src/nest/addons/addons.service';
|
||||
import { JourneyBookPhotosMcp } from '../../../src/nest/journey/journey-book-photos.mcp';
|
||||
import { JourneyBookModule } from '../../../src/nest/journey/journey-book.module';
|
||||
import { JourneyBookMcp } from '../../../src/nest/journey/journey-book.mcp';
|
||||
import { JourneyBookModule } from '../../../src/nest/journey/journey-book.module';
|
||||
import { PhotoResolverService, type PhotoSignalSource } from '../../../src/nest/memories/photo-resolver.service';
|
||||
import { createSnapshotTestDb } from '../../helpers/db-mock';
|
||||
import { addJourneyContributor, makeJourney, makeJourneyEntry } from '../../helpers/factories/journeys';
|
||||
@@ -49,7 +49,8 @@ beforeAll(async () => {
|
||||
});
|
||||
mcp = t.get(JourneyBookPhotosMcp);
|
||||
orm = await createTestOrm(db);
|
||||
jpeg = Buffer.from(await new Jimp({ width: 400, height: 300, color: 0x6699ccff }).getBuffer('image/jpeg'));
|
||||
// Large enough to print on a page, so a measured photo is not set aside as low resolution.
|
||||
jpeg = Buffer.from(await new Jimp({ width: 1600, height: 1200, color: 0x6699ccff }).getBuffer('image/jpeg'));
|
||||
});
|
||||
beforeEach(() => {
|
||||
resetTestDb(db);
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
import type { McpContext } from '../../../src/nest-mcp';
|
||||
import { getEntry, type ClassRef } from '../../../src/nest-mcp/metadata';
|
||||
import { AddonsService } from '../../../src/nest/addons/addons.service';
|
||||
import { JourneyBookModule } from '../../../src/nest/journey/journey-book.module';
|
||||
import { JourneyBookMcp } from '../../../src/nest/journey/journey-book.mcp';
|
||||
import { JourneyBookModule } from '../../../src/nest/journey/journey-book.module';
|
||||
import { createSnapshotTestDb } from '../../helpers/db-mock';
|
||||
import { addJourneyContributor, makeJourney } from '../../helpers/factories/journeys';
|
||||
import { makeUser } from '../../helpers/factories/users';
|
||||
@@ -75,17 +75,19 @@ describe('JourneyBookMcp', () => {
|
||||
it('JBMCP-001: every tool waits for the Journey addon and reads or writes in the journey group as it is annotated', async () => {
|
||||
const { user } = await makeUser(orm);
|
||||
const enabled = vi.spyOn(t.get(AddonsService), 'isAddonEnabled');
|
||||
for (const method of [...READS, ...WRITES]) {
|
||||
const tools = [...READS, ...WRITES].map((method) => {
|
||||
const entry = getEntry(JourneyBookMcp as unknown as ClassRef, method);
|
||||
if (entry?.kind !== 'tool') throw new Error(`${method} is no tool`);
|
||||
const reads = READS.includes(method);
|
||||
expect(entry.options.access, method).toEqual({ group: 'journey', mode: reads ? 'read' : 'write' });
|
||||
expect(entry.options.annotations?.readOnlyHint, method).toBe(reads);
|
||||
enabled.mockResolvedValue(false);
|
||||
expect(await entry.options.when?.(ctx(user.id), mcp), method).toBe(false);
|
||||
enabled.mockResolvedValue(true);
|
||||
expect(await entry.options.when?.(ctx(user.id), mcp), method).toBe(true);
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
const gates = () => Promise.all(tools.map((entry) => entry.options.when?.(ctx(user.id), mcp)));
|
||||
enabled.mockResolvedValue(false);
|
||||
expect(await gates()).toEqual(tools.map(() => false));
|
||||
enabled.mockResolvedValue(true);
|
||||
expect(await gates()).toEqual(tools.map(() => true));
|
||||
const remove = getEntry(JourneyBookMcp as unknown as ClassRef, 'deleteBookPages');
|
||||
expect(remove?.kind === 'tool' && remove.options.annotations?.destructiveHint).toBe(true);
|
||||
});
|
||||
|
||||
@@ -394,9 +394,8 @@ describe('readForSignals: what the quality signals are measured on', () => {
|
||||
const outside = await insertPhoto({ provider: 'local', file_path: 'elsewhere/p.jpg' });
|
||||
const clip = await insertPhoto({ provider: 'local', file_path: 'journey/c.mp4', media_type: 'video' });
|
||||
const bare = await insertPhoto({ provider: 'local' });
|
||||
for (const photoId of [outside, clip, bare, 999999]) {
|
||||
expect(await svc.readForSignals(photoId, 100)).toEqual({ kind: 'none' });
|
||||
}
|
||||
const unread = await Promise.all([outside, clip, bare, 999999].map((photoId) => svc.readForSignals(photoId, 100)));
|
||||
expect(unread).toEqual([{ kind: 'none' }, { kind: 'none' }, { kind: 'none' }, { kind: 'none' }]);
|
||||
expect(storage.withLocalFile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from '../../../../src/nest/photos/photo-signals.service';
|
||||
import { createSnapshotTestDb } from '../../../helpers/db-mock';
|
||||
import { makeJourney } from '../../../helpers/factories/journeys';
|
||||
import { countRows, insertRow } from '../../../helpers/factories/rows';
|
||||
import { countRows, insertRow, insertRows } from '../../../helpers/factories/rows';
|
||||
import { makeUser } from '../../../helpers/factories/users';
|
||||
import { resetTestDb } from '../../../helpers/test-db';
|
||||
import { createTestModule, type TestModule } from '../../../helpers/test-module';
|
||||
@@ -76,17 +76,16 @@ afterAll(async () => {
|
||||
async function gallery(kinds: Array<'image' | 'video'>) {
|
||||
const { user } = await makeUser(orm);
|
||||
const journey = await makeJourney(orm, user.id);
|
||||
const ids: number[] = [];
|
||||
for (const [i, kind] of kinds.entries()) {
|
||||
const id = await insertRow(orm, TrekPhotos, {
|
||||
provider: 'local',
|
||||
owner: user.id,
|
||||
file_path: 'journey/p.jpg',
|
||||
media_type: kind,
|
||||
});
|
||||
await insertRow(orm, JourneyPhotos, { journey: journey.id, photo: id, sort_order: i, created_at: Date.now() });
|
||||
ids.push(id);
|
||||
}
|
||||
const ids = await insertRows(
|
||||
orm,
|
||||
TrekPhotos,
|
||||
kinds.map((kind) => ({ provider: 'local', owner: user.id, file_path: 'journey/p.jpg', media_type: kind })),
|
||||
);
|
||||
await insertRows(
|
||||
orm,
|
||||
JourneyPhotos,
|
||||
ids.map((id, i) => ({ journey: journey.id, photo: id, sort_order: i, created_at: Date.now() })),
|
||||
);
|
||||
return { journeyId: journey.id, ids };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user