Files
TREK/server/tests/unit/nest/assistant/assistant-home-session.test.ts
T
Maurice 0050072b76 feat(assistant): photo changes of a journal get their own card and ways back
The four new photo link tools join the journal's tools: the journey chat
offers them to editors, the trip chat loads them with the journey group and
the home chat keeps none. A journeyPhotos card shows the photos as
thumbnails with the entries they leave and reach, the first one marked as
the cover in a new order, and a caption old to new; a library add shows the
photos once they landed. A linked photo comes off again, an unlinked one
goes back, moved photos go back and a caption is written back, each while
nothing changed since. A new order of an entry's photos and a library add
have no way back yet. The journal prompt names the photo tools.
2026-10-24 22:54:45 +02:00

194 lines
8.7 KiB
TypeScript

/**
* The assistant's tool session for the home chat, over the container's real
* MCP registry: it reads any trip the user may read, on the id the model
* passes, and holds no write that takes a trip or a journey, nor one of a
* journey's own that names only its entry; the writes that
* belong to no trip (the atlas, collections) come with their groups; the way
* into a trip's chat answers from the user's own trips only; an undo's
* session holds its target.
*/
import { OPEN_TRIP_CHAT_TOOL } from '../../../../src/nest/assistant/assistant-home';
import { OPEN_JOURNEY_CHAT_TOOL } from '../../../../src/nest/assistant/assistant-home-journeys';
import { USE_TOOLS_TOOL } from '../../../../src/nest/assistant/assistant-tool-offer';
import { AssistantToolsService } from '../../../../src/nest/assistant/assistant-tools.service';
import { makeJourney } from '../../../helpers/factories/journeys';
import { setAddonEnabled } from '../../../helpers/factories/settings';
import { makeTrip } from '../../../helpers/factories/trips';
import { makeUser } from '../../../helpers/factories/users';
import { FakeRealtimeService } from '../../../helpers/fake-realtime';
import { bootTestApp } from '../../../helpers/test-app';
import { MikroORM } from '@mikro-orm/core';
import { ASSISTANT_FOLLOW_UPS_TOOL } from '@trek/shared';
import { beforeAll, describe, expect, it } from 'vitest';
const realtime = new FakeRealtimeService();
let orm: MikroORM;
let tools: AssistantToolsService;
beforeAll(async () => {
const app = await bootTestApp(realtime);
orm = app.get(MikroORM);
tools = app.get(AssistantToolsService);
await setAddonEnabled(orm, 'atlas', true);
await setAddonEnabled(orm, 'collections', true);
await setAddonEnabled(orm, 'journey', true);
await setAddonEnabled(orm, 'dawarich', true);
});
async function ownerWithTrip() {
const { user } = await makeUser(orm);
const trip = await makeTrip(orm, user.id, { title: 'Kyoto' });
return { userId: user.id, tripId: trip.id };
}
describe('a home tool session', () => {
it('ASTHSES-001: offers the trip reads with their trip id, no trip write, and the server steps of the home chat', async () => {
const { userId } = await ownerWithTrip();
const session = await tools.openHome(userId);
try {
const specs = session.specs();
const names = specs.map((spec) => spec.name);
expect(names).toEqual(expect.arrayContaining(['list_trips', 'get_trip_summary', 'list_places']));
expect(names).toEqual(expect.arrayContaining([OPEN_TRIP_CHAT_TOOL, USE_TOOLS_TOOL, ASSISTANT_FOLLOW_UPS_TOOL]));
for (const name of ['create_place', 'update_trip', 'create_budget_item', 'remember_for_trip', 'get_book']) {
expect(names, name).not.toContain(name);
}
// Nothing is pinned: the model names the trip it reads.
const summary = specs.find((spec) => spec.name === 'get_trip_summary');
expect(Object.keys((summary?.parameters.properties as Record<string, unknown>) ?? {})).toContain('tripId');
expect(session.kindOf('get_trip_summary')).toBe('read');
expect(session.kindOf(OPEN_TRIP_CHAT_TOOL)).toBe('read');
expect(session.kindOf(USE_TOOLS_TOOL)).toBe('read');
expect(session.kindOf('remember_for_trip')).toBeNull();
expect(session.kindOf('create_place')).toBeNull();
} finally {
await session.close();
}
});
it('ASTHSES-002: a loaded group brings its writes that belong to no trip, held as changes', async () => {
const { userId } = await ownerWithTrip();
const session = await tools.openHome(userId);
try {
expect(session.specs().map((spec) => spec.name)).not.toContain('mark_country_visited');
const loaded = await session.call(USE_TOOLS_TOOL, { groups: ['atlas', 'collections'] });
expect(loaded).toMatchObject({ ok: true, value: { loaded: ['atlas', 'collections'] } });
const names = session.specs().map((spec) => spec.name);
expect(names).toEqual(expect.arrayContaining(['mark_country_visited', 'create_collection']));
expect(names).not.toContain('copy_collection_places_to_trip');
expect(session.kindOf('mark_country_visited')).toBe('write');
} finally {
await session.close();
}
});
it("ASTHSES-006: the journey group brings a journey's reads and none of its writes", async () => {
const { userId } = await ownerWithTrip();
const session = await tools.openHome(userId);
try {
await session.call(USE_TOOLS_TOOL, { groups: ['journey'] });
const names = session.specs().map((spec) => spec.name);
expect(names).toEqual(expect.arrayContaining(['list_journeys', 'list_journey_entries']));
const journeyWrites = [
'update_journey_entry',
'delete_journey_entry',
'reorder_journey_entry_photos',
'accept_dawarich_suggestion_as_journal_entry',
'link_journey_photo',
'move_journey_photos',
];
for (const name of journeyWrites) {
expect(names, name).not.toContain(name);
expect(session.kindOf(name), name).toBeNull();
}
// A Dawarich suggestion waved away belongs to no journey and stays.
expect(session.kindOf('dismiss_dawarich_suggestion')).toBe('write');
} finally {
await session.close();
}
});
it('ASTHSES-007: the journey group brings the start and the deletion of a journal, held as changes', async () => {
const { userId } = await ownerWithTrip();
const session = await tools.openHome(userId);
try {
await session.call(USE_TOOLS_TOOL, { groups: ['journey'] });
const names = session.specs().map((spec) => spec.name);
expect(names).toEqual(expect.arrayContaining(['create_journey', 'delete_journey', OPEN_JOURNEY_CHAT_TOOL]));
expect(session.kindOf('create_journey')).toBe('write');
expect(session.kindOf('delete_journey')).toBe('write');
expect(session.kindOf('update_journey')).toBeNull();
expect(session.kindOf(OPEN_JOURNEY_CHAT_TOOL)).toBe('read');
} finally {
await session.close();
}
});
it("ASTHSES-008: offers a journal's page only for a journal of the user's", async () => {
const { userId } = await ownerWithTrip();
const journey = await makeJourney(orm, userId, { title: 'Japan 2026' });
const { user: stranger } = await makeUser(orm);
const own = await tools.openHome(userId);
const theirs = await tools.openHome(stranger.id);
try {
expect(await own.call(OPEN_JOURNEY_CHAT_TOOL, { journeyId: journey.id, question: 'New entry' })).toMatchObject({
ok: true,
value: { journey: { id: journey.id, title: 'Japan 2026' }, question: 'New entry' },
});
expect(await theirs.call(OPEN_JOURNEY_CHAT_TOOL, { journeyId: journey.id })).toMatchObject({ ok: false });
} finally {
await own.close();
await theirs.close();
}
});
it('ASTHSES-003: reads a trip of the user by its id, and only with the user rights', async () => {
const { userId, tripId } = await ownerWithTrip();
const { user: stranger } = await makeUser(orm);
const own = await tools.openHome(userId);
const theirs = await tools.openHome(stranger.id);
try {
expect(own.pinArgs('get_trip_summary', { tripId })).toEqual({ args: { tripId } });
expect(await own.call('get_trip_summary', { tripId })).toMatchObject({
ok: true,
value: { trip: { id: tripId, title: 'Kyoto' } },
});
expect((await theirs.call('get_trip_summary', { tripId })).ok).toBe(false);
} finally {
await own.close();
await theirs.close();
}
});
it("ASTHSES-004: offers a trip's chat only for a trip of the user's", async () => {
const { userId, tripId } = await ownerWithTrip();
const { user: stranger } = await makeUser(orm);
const own = await tools.openHome(userId);
const theirs = await tools.openHome(stranger.id);
try {
expect(await own.call(OPEN_TRIP_CHAT_TOOL, { tripId, question: ' What is missing? ' })).toMatchObject({
ok: true,
value: { trip: { id: tripId, title: 'Kyoto', phase: 'undated' }, question: 'What is missing?' },
});
expect(await theirs.call(OPEN_TRIP_CHAT_TOOL, { tripId })).toMatchObject({ ok: false });
} finally {
await own.close();
await theirs.close();
}
});
it('ASTHSES-005: an undo session holds its target and nothing of the home steps', async () => {
const { userId } = await ownerWithTrip();
const session = await tools.openHome(userId, new Set(['delete_collection']));
try {
expect(session.specs().map((spec) => spec.name)).toEqual(['delete_collection']);
expect(session.kindOf(OPEN_TRIP_CHAT_TOOL)).toBeNull();
expect(session.kindOf(OPEN_JOURNEY_CHAT_TOOL)).toBeNull();
expect(session.kindOf(USE_TOOLS_TOOL)).toBeNull();
} finally {
await session.close();
}
});
});