Files
TREK/server/tests/helpers/mcp-test-controllers.ts
T
Maurice 2d0eca7938 Let MCP addon gates read an injected service
Nine *.mcp.ts files reached through addons.bridge for one boolean each, and
platform.routes plus the legacy registrars still do. That was never a
preference: a decorator's options object is built while the class is being
defined, so the `when:` closure had no `this` to reach an injected service
through, and the only thing in scope was a module-level singleton that
builds its own AddonsService outside the container. Nine domains hid an edge
to the addons domain from the module graph to answer "is this addon on".

@trek/nest-mcp hands the gate its declaring instance now, resolved at
attach rather than captured at module load, and addons/addon-gate.ts turns
that into addonGate(ADDON_IDS.X) over the injected service. The option is
declared with method syntax so a predicate written against its own
controller class stays assignable.

Two more edges went the same way, both checked for cycles first:
reservations.mcp injects AssignmentsService (AssignmentsModule never
reaches back), atlas.mcp and journey.mcp inject AuthService.

The tests that forced a toggle by mocking the bridge module now write the
addon row the admin panel writes, so they exercise the real read.

Four in-container uses stay, each documented in the nest README: places.mcp
(a real Days -> Places -> Assignments cycle), budget/packing/costs against
trips.bridge, user-cleanup against budget.bridge, and the two multer
interceptor options, whose fix is MulterModule.registerAsync.
2026-08-10 17:08:54 +02:00

159 lines
11 KiB
TypeScript

import { createTestRegistry, type McpRegistry } from '@trek/nest-mcp';
import { db } from '../../src/db/database';
import { trekMcpAccessPolicy, trekMcpValidateAccess } from '../../src/mcp/nest-mcp-policy';
import { AssignmentsMcp } from '../../src/nest/assignments/assignments.mcp';
import { AssignmentsService } from '../../src/nest/assignments/assignments.service';
import { AtlasMcp } from '../../src/nest/atlas/atlas.mcp';
import { AtlasService } from '../../src/nest/atlas/atlas.service';
import { AuthService } from '../../src/nest/auth/auth.service';
import { BudgetMcp } from '../../src/nest/budget/budget.mcp';
import { BudgetService } from '../../src/nest/budget/budget.service';
import { ExchangeRatesService } from '../../src/nest/budget/exchange-rates.service';
import { CategoriesMcp } from '../../src/nest/categories/categories.mcp';
import { CategoriesService } from '../../src/nest/categories/categories.service';
import { CollabMcp } from '../../src/nest/collab/collab.mcp';
import { CollabService } from '../../src/nest/collab/collab.service';
import { CollectionsMcp } from '../../src/nest/collections/collections.mcp';
import { CollectionsService } from '../../src/nest/collections/collections.service';
import { DatabaseService } from '../../src/nest/database/database.service';
import { DayNotesMcp } from '../../src/nest/day-notes/day-notes.mcp';
import { DayNotesService } from '../../src/nest/day-notes/day-notes.service';
import { DaysMcp } from '../../src/nest/days/days.mcp';
import { DaysService } from '../../src/nest/days/days.service';
import { MapsMcp } from '../../src/nest/maps/maps.mcp';
import { WeatherMcp } from '../../src/nest/weather/weather.mcp';
import { WeatherService } from '../../src/nest/weather/weather.service';
import { AirportsMcp } from '../../src/nest/airports/airports.mcp';
import { AuthMcp } from '../../src/nest/auth/auth.mcp';
import { MapsService } from '../../src/nest/maps/maps.service';
import { NotificationsMcp } from '../../src/nest/notifications/notifications.mcp';
import { NotificationsService } from '../../src/nest/notifications/notifications.service';
import { PackingMcp } from '../../src/nest/packing/packing.mcp';
import { PackingService } from '../../src/nest/packing/packing.service';
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
import { PlacesMcp } from '../../src/nest/places/places.mcp';
import { PlacesService } from '../../src/nest/places/places.service';
import { ReservationsMcp } from '../../src/nest/reservations/reservations.mcp';
import { ReservationsService } from '../../src/nest/reservations/reservations.service';
import { TagsMcp } from '../../src/nest/tags/tags.mcp';
import { TagsService } from '../../src/nest/tags/tags.service';
import { SettingsService } from '../../src/nest/settings/settings.service';
import { ShareMcp } from '../../src/nest/share/share.mcp';
import { ShareService } from '../../src/nest/share/share.service';
import { TodoMcp } from '../../src/nest/todo/todo.mcp';
import { TodoService } from '../../src/nest/todo/todo.service';
import { TransitMcp } from '../../src/nest/transit/transit.mcp';
import { TransitService } from '../../src/nest/transit/transit.service';
import { FilesService } from '../../src/nest/files/files.service';
import { TripsMcp } from '../../src/nest/trips/trips.mcp';
import { TripsService } from '../../src/nest/trips/trips.service';
import { VacayMcp } from '../../src/nest/vacay/vacay.mcp';
import { VacayService } from '../../src/nest/vacay/vacay.service';
import { RealtimeService } from '../../src/nest/realtime/realtime.service';
import { QueryHelpersService } from '../../src/nest/query-helpers/query-helpers.service';
import { JourneyMcp } from '../../src/nest/journey/journey.mcp';
import { JourneyDomainService } from '../../src/nest/journey/journey-domain.service';
import { JourneyShareService } from '../../src/nest/journey/journey-share.service';
import { TrekPhotosRepository } from '../../src/nest/photos/trek-photos.repository';
import { UnsplashService } from '../../src/nest/unsplash/unsplash.service';
import { UserCleanupService } from '../../src/nest/auth/user-cleanup.service';
import { CalendarService } from '../../src/nest/calendar/calendar.service';
import { AccommodationsService } from '../../src/nest/accommodations/accommodations.service';
import { AccommodationsMcp } from '../../src/nest/accommodations/accommodations.mcp';
import { TripMembersService } from '../../src/nest/trip-members/trip-members.service';
import { TripReadModelService } from '../../src/nest/trip-read-model/trip-read-model.service';
import { PlacePhotoCacheService } from '../../src/nest/place-photos/place-photo-cache.service';
import { RuntimeEnvService } from '../../src/nest/app-config/runtime-env.service';
import { makeNotificationsService, makeNotificationPreferencesService } from './notifications';
import { AddonsService } from '../../src/nest/addons/addons.service';
/**
* Hand-wired counterpart of the boot-time discovery in McpRegistryService,
* for the no-Nest MCP harness. One line per migrated domain — add the new
* @McpController instance here when a domain moves off the legacy registrar
* fan-out. Constructing against the `db` Proxy keeps per-file vi.mock's of
* src/db/database flowing through (same pattern as todo.bridge.ts).
*/
export function createMcpTestRegistry(): McpRegistry {
const dbService = new DatabaseService(db);
const permissionsService = new PermissionsService(dbService);
const authService = new AuthService(dbService, permissionsService, new AtlasService(dbService));
const realtimeService = new RealtimeService();
const queryHelpersService = new QueryHelpersService(dbService);
const daysService = new DaysService(dbService, permissionsService, realtimeService, queryHelpersService);
const exchangeRatesService = new ExchangeRatesService();
const budgetService = new BudgetService(dbService, permissionsService, exchangeRatesService, realtimeService);
const todoService = new TodoService(dbService, permissionsService, realtimeService);
const packingService = new PackingService(dbService, permissionsService, realtimeService);
const collabService = new CollabService(dbService, permissionsService, realtimeService);
const mapsService = new MapsService(dbService);
const journeyDomain = new JourneyDomainService(dbService, realtimeService, new TrekPhotosRepository(dbService));
// The last three were previously omitted, which left them `undefined` at
// runtime — silently fine while nothing called them, a TypeError the moment
// the journey skeleton hooks landed on the place write paths. tsconfig only
// includes `src`, so nothing here is typechecked; pass them for real.
const placesService = new PlacesService(
dbService, permissionsService, realtimeService, mapsService, queryHelpersService,
new UnsplashService(dbService, new RuntimeEnvService()),
new PlacePhotoCacheService(dbService, new RuntimeEnvService()),
journeyDomain,
);
const reservationsService = new ReservationsService(dbService, permissionsService, budgetService, realtimeService);
const accommodationsService = new AccommodationsService(dbService, permissionsService, realtimeService);
const membersService = new TripMembersService(dbService, budgetService, new UserCleanupService(dbService), permissionsService, realtimeService);
const tripsService = new TripsService(
dbService,
reservationsService,
daysService,
permissionsService,
budgetService,
new VacayService(dbService, realtimeService),
realtimeService,
new UnsplashService(dbService, new RuntimeEnvService()),
);
const readModelService = new TripReadModelService(
dbService, membersService, daysService, accommodationsService, budgetService,
packingService, reservationsService, collabService, placesService, todoService,
new FilesService(dbService, permissionsService, realtimeService),
);
const calendarService = new CalendarService(dbService, reservationsService);
// The nine addon-gated surfaces read their toggle off an injected service now
// rather than off addons.bridge's own instance, so the harness has to supply
// one — against the same test DB, which is what makes the `when:` gates
// answer truthfully here instead of against the process-wide singleton.
const addonsService = new AddonsService(dbService);
// One instance, two consumers: AssignmentsMcp and, since it stopped reaching
// through assignments.bridge, ReservationsMcp.
const assignmentsService = new AssignmentsService(dbService, permissionsService, realtimeService, queryHelpersService, journeyDomain);
return createTestRegistry(
[
new TagsMcp(new TagsService(dbService), authService),
new CategoriesMcp(new CategoriesService(dbService)),
// The weather and airport tools left the legacy mapsWeather registrar.
new WeatherMcp(new WeatherService()),
new AirportsMcp(),
new AuthMcp(),
new TodoMcp(todoService, authService, addonsService),
new PackingMcp(packingService, authService, addonsService),
new BudgetMcp(budgetService, exchangeRatesService, dbService, authService, addonsService),
new ReservationsMcp(reservationsService, daysService, budgetService, authService, assignmentsService),
new DayNotesMcp(new DayNotesService(dbService, permissionsService, realtimeService), authService),
new DaysMcp(daysService, authService),
new AccommodationsMcp(accommodationsService, dbService, placesService, authService),
new AssignmentsMcp(assignmentsService, daysService, authService),
new CollabMcp(collabService, authService, addonsService),
new VacayMcp(new VacayService(dbService, realtimeService), authService, addonsService),
new TripsMcp(tripsService, todoService, collabService, authService, calendarService, membersService, readModelService, addonsService),
new ShareMcp(new ShareService(dbService, new SettingsService(dbService), permissionsService, queryHelpersService), authService),
new MapsMcp(mapsService),
new PlacesMcp(placesService, mapsService, dbService, authService, journeyDomain),
new CollectionsMcp(new CollectionsService(dbService, permissionsService, realtimeService), dbService, authService, addonsService),
new TransitMcp(new TransitService(), daysService, reservationsService, dbService, authService),
new AtlasMcp(new AtlasService(dbService), addonsService, authService),
new JourneyMcp(journeyDomain, new JourneyShareService(dbService, journeyDomain), addonsService, authService),
new NotificationsMcp(makeNotificationsService(dbService, realtimeService), authService),
],
{ accessPolicy: trekMcpAccessPolicy, validateAccess: trekMcpValidateAccess },
);
}