Files
TREK/server/tests/unit/mcp/tools-leg-transport-mode.test.ts
T
Maurice e2ce2ad05c test(server): seed the service, MCP and route specs through the ORM factories
Service, MCP, integration and e2e suites with up to a dozen raw
statements now seed and read through the factories. A buildApp() suite
uses the app's own ORM, a partial-module e2e or a unit suite a
createTestOrm() or sharedTestOrm() handle on its database. Reads that
used to come back undefined now come back null, and the assertions say
so; where a projected SELECT was compared whole, the same columns are
picked off the row.

Two reads inside the scheduler's delivery callback stay synchronous raw
statements, marked, so they keep seeing the row at the moment of
delivery. Two more database mocks lose a canAccessTrip/isOwner stand-in
the module no longer exports.
2026-10-08 18:46:25 +02:00

321 lines
14 KiB
TypeScript

/**
* Unit tests for MCP leg-mode tools: set_leg_transport_mode,
* set_day_default_transport_mode.
*/
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
import { db as testDb } from '../../../src/db/database';
vi.mock('../../../src/db/database', async () => {
const { createSnapshotTestDb, buildDbMock } = await import('../../helpers/db-mock');
return buildDbMock(createSnapshotTestDb());
});
vi.mock('../../../src/config', () => ({
JWT_SECRET: 'test-jwt-secret-for-trek-testing-only',
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
updateJwtSecret: () => {},
}));
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createDay, createPlace, createDayAssignment } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
import { findRow, updateRows } from '../../helpers/factories/rows';
import { DayAssignments } from '../../../src/db/entities/DayAssignments.entity';
import { Days } from '../../../src/db/entities/Days.entity';
beforeEach(() => {
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({ userId, withResources: false });
try { await fn(h); } finally { await h.cleanup(); }
}
// ---------------------------------------------------------------------------
// set_leg_transport_mode
// ---------------------------------------------------------------------------
describe('Tool: set_assignment_route_excluded (#2532)', () => {
it('takes a stop out of the route and puts it back, broadcasting each change', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
const out = parseToolResult(await h.client.callTool({ name: 'set_assignment_route_excluded', arguments: { tripId: trip.id, assignmentId: assignment.id, excluded: true } })) as any;
expect(out.assignment.route_excluded).toBe(true);
expect((await findRow(orm, DayAssignments, { id: assignment.id }))?.route_excluded).toBe(1);
const back = parseToolResult(await h.client.callTool({ name: 'set_assignment_route_excluded', arguments: { tripId: trip.id, assignmentId: assignment.id, excluded: false } })) as any;
expect(back.assignment.route_excluded).toBe(false);
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'assignment:updated', expect.any(Object));
});
});
it('rejects an assignment of another trip', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const other = createTrip(testDb, user.id);
const day = createDay(testDb, other.id);
const place = createPlace(testDb, other.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'set_assignment_route_excluded', arguments: { tripId: trip.id, assignmentId: assignment.id, excluded: true } });
expect(result.isError).toBe(true);
});
});
});
describe('Tool: set_leg_transport_mode', () => {
it('sets the outgoing leg mode (default direction)', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
// Seed the incoming column so the assertion below proves it survives; the factory
// leaves it NULL, so without this it would pass whether or not the tool touched it.
await updateRows(orm, DayAssignments, { id: assignment.id }, { incoming_leg_transport_mode: 'walking' });
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_leg_transport_mode',
arguments: { tripId: trip.id, assignmentId: assignment.id, transport_mode: 'cycling' },
});
const data = parseToolResult(result) as any;
expect(data.assignment.leg_transport_mode).toBe('cycling');
expect(data.assignment.incoming_leg_transport_mode).toBe('walking'); // outgoing must not touch incoming (panel item 4)
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'assignment:updated', expect.any(Object));
});
});
it('sets the incoming leg mode without clobbering the outgoing column (column independence)', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
// Seed the outgoing column so we prove incoming does NOT touch it (panel item 4).
await updateRows(orm, DayAssignments, { id: assignment.id }, { leg_transport_mode: 'driving' });
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_leg_transport_mode',
arguments: { tripId: trip.id, assignmentId: assignment.id, transport_mode: 'walking', direction: 'incoming' },
});
const data = parseToolResult(result) as any;
expect(data.assignment.incoming_leg_transport_mode).toBe('walking');
expect(data.assignment.leg_transport_mode).toBe('driving'); // outgoing untouched
});
});
it('rejects an assignment belonging to another trip (cross-trip join)', async () => {
// Proves the `AND d.trip_id = ?` join in getAssignmentForTrip; a 99999 id cannot.
const { user } = createUser(testDb);
const trip1 = createTrip(testDb, user.id);
const trip2 = createTrip(testDb, user.id);
const day2 = createDay(testDb, trip2.id);
const place2 = createPlace(testDb, trip2.id);
const assignment2 = createDayAssignment(testDb, day2.id, place2.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_leg_transport_mode',
arguments: { tripId: trip1.id, assignmentId: assignment2.id, transport_mode: 'walking' },
});
expect(result.isError).toBe(true);
});
});
it('clears the leg mode with null (inherit day default)', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await updateRows(orm, DayAssignments, { id: assignment.id }, { leg_transport_mode: 'cycling' });
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_leg_transport_mode',
arguments: { tripId: trip.id, assignmentId: assignment.id, transport_mode: null },
});
const data = parseToolResult(result) as any;
expect(data.assignment.leg_transport_mode).toBeNull();
});
});
it('returns error when assignment not found', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_leg_transport_mode',
arguments: { tripId: trip.id, assignmentId: 99999, transport_mode: 'walking' },
});
expect(result.isError).toBe(true);
});
});
it('returns access denied for non-member', async () => {
const { user } = createUser(testDb);
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_leg_transport_mode',
arguments: { tripId: trip.id, assignmentId: assignment.id, transport_mode: 'walking' },
});
expect(result.isError).toBe(true);
});
});
it('blocks demo user', async () => {
process.env.DEMO_MODE = 'true';
const { user } = createUser(testDb, { email: 'demo@nomad.app' });
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_leg_transport_mode',
arguments: { tripId: trip.id, assignmentId: assignment.id, transport_mode: 'walking' },
});
expect(result.isError).toBe(true);
});
});
});
// ---------------------------------------------------------------------------
// set_day_default_transport_mode
// ---------------------------------------------------------------------------
describe('Tool: set_day_default_transport_mode', () => {
it('sets the day default transport mode', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_day_default_transport_mode',
arguments: { tripId: trip.id, dayId: day.id, transport_mode: 'driving' },
});
const data = parseToolResult(result) as any;
expect(data.day.default_transport_mode).toBe('driving');
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'day:updated', expect.any(Object));
});
});
it('clears the day default with null', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
await updateRows(orm, Days, { id: day.id }, { default_transport_mode: 'driving' });
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_day_default_transport_mode',
arguments: { tripId: trip.id, dayId: day.id, transport_mode: null },
});
const data = parseToolResult(result) as any;
expect(data.day.default_transport_mode).toBeNull();
});
});
it('returns the day with its populated assignments array', async () => {
// setDefaultTransportMode returns { ...day, assignments }; exercise the non-empty case (panel item 4).
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_day_default_transport_mode',
arguments: { tripId: trip.id, dayId: day.id, transport_mode: 'driving' },
});
const data = parseToolResult(result) as any;
expect(Array.isArray(data.day.assignments)).toBe(true);
expect(data.day.assignments).toHaveLength(1);
expect(data.day.assignments[0].id).toBe(assignment.id);
});
});
it('rejects a day belonging to another trip (cross-trip join)', async () => {
const { user } = createUser(testDb);
const trip1 = createTrip(testDb, user.id);
const trip2 = createTrip(testDb, user.id);
const day2 = createDay(testDb, trip2.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_day_default_transport_mode',
arguments: { tripId: trip1.id, dayId: day2.id, transport_mode: 'walking' },
});
expect(result.isError).toBe(true);
});
});
it('returns error when day not found', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_day_default_transport_mode',
arguments: { tripId: trip.id, dayId: 99999, transport_mode: 'walking' },
});
expect(result.isError).toBe(true);
});
});
it('returns access denied for non-member', async () => {
const { user } = createUser(testDb);
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
const day = createDay(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_day_default_transport_mode',
arguments: { tripId: trip.id, dayId: day.id, transport_mode: 'walking' },
});
expect(result.isError).toBe(true);
});
});
it('blocks demo user', async () => {
process.env.DEMO_MODE = 'true';
const { user } = createUser(testDb, { email: 'demo@nomad.app' });
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_day_default_transport_mode',
arguments: { tripId: trip.id, dayId: day.id, transport_mode: 'walking' },
});
expect(result.isError).toBe(true);
});
});
});