chore: wire TripsService into the plugin RPC host and retire the legacy tripService

Swap the factory's six tripService imports for the injected TripsService; delete services/tripService.ts and the collab/vacay bridges with it; prune the days/budget bridges to their surviving exports; update the migration docs and regenerate migration-graph.md.
This commit is contained in:
jubnl
2026-07-29 01:53:54 +02:00
parent 530c7cb005
commit 287dceb532
18 changed files with 179 additions and 1370 deletions
+4 -31
View File
@@ -1,7 +1,7 @@
/**
* Unit tests for the DI-native CollabService — COLLAB-SVC-001 to COLLAB-SVC-038
* (001–030 moved 1:1 from the legacy tests/unit/services/collabService.test.ts;
* 031–033 pin the collab.bridge delegation; 034–038 pin the post-migration
* 031–033 (collab.bridge delegation) died with the bridge; 034–038 pin the post-migration
* hardening: transactional writes, trip-scoped getFormattedNoteById, the
* integer vote guard and malformed-URL absorption). Covers votePoll edge
* cases, listMessages pagination, deleteMessage ownership, updateNote partial
@@ -61,7 +61,6 @@ import { avatarUrl } from '../../../src/services/avatarUrl';
import { DatabaseService } from '../../../src/nest/database/database.service';
import { PermissionsService } from '../../../src/nest/permissions/permissions.service';
import { CollabService } from '../../../src/nest/collab/collab.service';
import { listNotes as bridgeListNotes, listPolls as bridgeListPolls, countMessages as bridgeCountMessages } from '../../../src/nest/collab/collab.bridge';
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
const svc = new CollabService(new DatabaseService(testDb), new PermissionsService(new DatabaseService(testDb)), new RealtimeService());
@@ -406,35 +405,9 @@ describe('linkPreview', () => {
});
});
// ── collab.bridge delegation (out-of-container consumers) ─────────────────────
describe('collab.bridge', () => {
it('COLLAB-SVC-031: listNotes delegates to CollabService over the shared db', () => {
const { user1, trip } = setup();
svc.createNote(trip.id, user1.id, { title: 'Bridged note' });
const notes = bridgeListNotes(trip.id);
expect(notes).toHaveLength(1);
expect(notes[0].title).toBe('Bridged note');
});
it('COLLAB-SVC-032: listPolls delegates to CollabService over the shared db', () => {
const { user1, trip } = setup();
svc.createPoll(trip.id, user1.id, { question: 'Bridged?', options: ['A', 'B'] });
const polls = bridgeListPolls(trip.id);
expect(polls).toHaveLength(1);
expect(polls[0]!.question).toBe('Bridged?');
});
it('COLLAB-SVC-033: countMessages delegates to CollabService over the shared db', () => {
const { user1, trip } = setup();
svc.createMessage(trip.id, user1.id, 'one');
svc.createMessage(trip.id, user1.id, 'two');
expect(bridgeCountMessages(trip.id)).toBe(2);
});
});
// COLLAB-SVC-031..033 (collab.bridge delegation) were deleted with the bridge —
// its last consumers (the legacy tripService and the legacy get_trip_summary
// registrar) migrated into the DI-native TripsService/TripsMcp.
// ── Post-migration hardening (transactions, scoping, guards) ──────────────────
+9 -11
View File
@@ -52,13 +52,7 @@ import { createUser, createTrip, createDay, createPlace, createDayAssignment, cr
import { DatabaseService } from '../../../src/nest/database/database.service';
import { PermissionsService } from '../../../src/nest/permissions/permissions.service';
import { DaysService, addDays } from '../../../src/nest/days/days.service';
import {
getDay as bridgeGetDay,
listDays as bridgeListDays,
listAccommodations as bridgeListAccommodations,
restampReservationDates as bridgeRestampReservationDates,
resyncAccommodationDays as bridgeResyncAccommodationDays,
} from '../../../src/nest/days/days.bridge';
import { getDay as bridgeGetDay, listDays as bridgeListDays } from '../../../src/nest/days/days.bridge';
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
const svc = new DaysService(new DatabaseService(testDb), new PermissionsService(new DatabaseService(testDb)), new RealtimeService());
@@ -406,6 +400,10 @@ describe('deleteAccommodation', () => {
});
// ── days.bridge delegation (out-of-container consumers) ───────────────────────
// The listAccommodations / restampReservationDates / resyncAccommodationDays /
// addDays bridge exports were pruned when their last outside-container
// consumer (legacy tripService) folded into the DI-native TripsService —
// 029/031/032 pin the same behavior on the service.
describe('days.bridge', () => {
it('DAY-SVC-027 — getDay delegates to DaysService.getDay', () => {
@@ -425,13 +423,13 @@ describe('days.bridge', () => {
expect(Array.isArray(result.days[0].assignments)).toBe(true);
});
it('DAY-SVC-029 — listAccommodations delegates to DaysService.listAccommodations', () => {
it('DAY-SVC-029 — listAccommodations returns the hydrated stays', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id, { name: 'Ryokan' });
createDayAccommodation(testDb, trip.id, place.id, day.id, day.id);
const rows = bridgeListAccommodations(trip.id) as { place_name: string }[];
const rows = svc.listAccommodations(trip.id) as { place_name: string }[];
expect(rows).toHaveLength(1);
expect(rows[0].place_name).toBe('Ryokan');
});
@@ -450,7 +448,7 @@ describe('days.bridge', () => {
'INSERT INTO reservations (trip_id, day_id, title, reservation_time) VALUES (?, ?, ?, ?)'
).run(trip.id, day.id, 'Dinner', '2026-01-01T19:00');
bridgeRestampReservationDates(
svc.restampReservationDates(
trip.id,
new Map([[day.id, '2026-01-01']]),
new Map([[day.id, '2026-01-05']]),
@@ -475,7 +473,7 @@ describe('days.bridge', () => {
testDb.prepare('UPDATE days SET date = ? WHERE id = ?').run('2026-01-01', d2.id);
testDb.prepare('UPDATE days SET day_number = 0 WHERE id = ?').run(d2.id);
bridgeResyncAccommodationDays(trip.id, new Map([[d1.id, '2026-01-01'], [d2.id, '2026-01-02']]));
svc.resyncAccommodationDays(trip.id, new Map([[d1.id, '2026-01-01'], [d2.id, '2026-01-02']]));
const row = testDb.prepare('SELECT start_day_id, end_day_id FROM day_accommodations WHERE id = ?').get(accom.id) as { start_day_id: number; end_day_id: number };
expect(row.start_day_id).toBe(d2.id);
+3 -17
View File
@@ -35,14 +35,14 @@ import { createUser } from '../../helpers/factories';
import { DatabaseService } from '../../../src/nest/database/database.service';
import { VacayService } from '../../../src/nest/vacay/vacay.service';
import { shiftOwnerEntriesForTripWindow } from '../../../src/nest/vacay/vacay.bridge';
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
// VACAY-SVC-001 through VACAY-SVC-066 moved 1:1 from the legacy
// tests/unit/services/vacayService.test.ts (the named-function imports became
// method calls on a directly constructed VacayService; the legacy
// updateUserYearSettings is the class's updateYearSettings). VACAY-SVC-067
// pins the vacay.bridge delegation for the one outside-container consumer.
// updateUserYearSettings is the class's updateYearSettings).
// VACAY-SVC-067 (vacay.bridge delegation) died with the bridge — its only
// consumer, the legacy tripService, folded into the DI-native TripsService.
const svc = new VacayService(new DatabaseService(testDb), new RealtimeService());
// ── Lifecycle ─────────────────────────────────────────────────────────────────
@@ -1367,20 +1367,6 @@ describe('getSharedCalendars', () => {
});
});
// ── vacay.bridge delegation ───────────────────────────────────────────────────
describe('vacay.bridge', () => {
it('VACAY-SVC-067: shiftOwnerEntriesForTripWindow shifts the owner entries through the bridge', () => {
const { user, plan } = setupUserWithPlan();
svc.toggleEntry(user.id, plan.id, '2025-05-10', 1);
shiftOwnerEntriesForTripWindow(user.id, '2025-05-08', '2025-05-12', '2025-05-15');
const dates = testDb.prepare('SELECT date FROM vacay_entries WHERE plan_id = ? ORDER BY date').all(plan.id);
expect(dates).toEqual([{ date: '2025-05-17' }]);
});
});
// ── Quirk fixes (transactions, fetch hygiene, cache TTL, addYear errors) ──────
describe('quirk fixes', () => {
@@ -66,27 +66,10 @@ const budgetStub = {
const checkPermission = vi.fn((..._a: any[]) => true as boolean);
const permissionsStub = { checkPermission } as unknown as PermissionsService;
// The core write services are delegated to; mock them so the host deps'
// wiring + error branches run without the full core schema. The error classes must
// be defined INSIDE the factory (vi.mock is hoisted above module-scope code).
vi.mock('../../../src/services/tripService', () => {
class NotFoundError extends Error {}
class ValidationError extends Error {}
return {
updateTrip: (tripId: number, _u: number, input: Record<string, unknown>) => {
if (input.title === 'boom') throw new ValidationError('bad dates');
if (input.title === 'gone') throw new NotFoundError('no trip');
if (input.title === 'crash') throw new Error('unexpected');
return { updatedTrip: { id: tripId, ...input } };
},
createTrip: (userId: number, input: Record<string, unknown>) => {
if (input.title === 'boom') throw new ValidationError('bad dates');
return { trip: { id: 99, user_id: userId, ...input }, tripId: 99, reminderDays: 3 };
},
listTrips: () => [{ id: 1 }],
NotFoundError, ValidationError,
};
});
// Trips are a constructor-injected stub since the trip fold (same behaviors as
// the old services/tripService path mock). The throws use the REAL error
// classes from trips.service so the factory's instanceof mapping keeps
// matching — see the imports below the mocks.
// Exchange rates are a constructor-injected stub since the budget-domain fold
// (was a path mock of the deleted services/exchangeRateService — same behavior).
const exchangeRatesStub = {
@@ -96,6 +79,9 @@ vi.mock('../../../src/services/placeService', () => ({
createPlace: vi.fn((tid: string, body: Record<string, unknown>) => ({ id: 10, trip_id: Number(tid), ...body })),
updatePlace: vi.fn((_tid: string, pid: string) => (pid === '99' ? null : { id: Number(pid) })),
deletePlace: vi.fn((_tid: string, pid: string) => pid !== '99'),
// trips.service (loaded for real since the trip fold) imports listPlaces for
// its offline bundle — the mock must carry every named export it pulls.
listPlaces: vi.fn(() => []),
}));
// Days are a constructor-injected stub (same behaviors as the old path mock,
// mapped onto the DaysService method names).
@@ -320,12 +306,27 @@ import type { VacayService } from '../../../src/nest/vacay/vacay.service';
import type { PermissionsService } from '../../../src/nest/permissions/permissions.service';
import { DatabaseService } from '../../../src/nest/database/database.service';
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
import { NotFoundError, ValidationError } from '../../../src/nest/trips/trips.service';
// The factory under test, wired exactly like PluginsModule does — but with the
// DI-native domain services replaced by the stubs above. The shim keeps the
// ~45 historical call sites unchanged and supplies a default no-op router.
const addonsStub = { isAddonEnabled } as unknown as import('../../../src/nest/addons/addons.service').AddonsService;
const factory = new PluginHostDepsFactory(budgetStub, reservationsStub, tagsStub, categoriesStub, todoStub, packingStub, oauthStub, dayNotesStub, assignmentsStub, llmConfigStub, new DatabaseService(mockDb), filesStub, collabStub, vacayStub, daysStub, permissionsStub, exchangeRatesStub, addonsStub, new RealtimeService());
const tripsStub = {
updateTrip: (tripId: number, _u: number, input: Record<string, unknown>) => {
if (input.title === 'boom') throw new ValidationError('bad dates');
if (input.title === 'gone') throw new NotFoundError('no trip');
if (input.title === 'crash') throw new Error('unexpected');
return { updatedTrip: { id: tripId, ...input } };
},
create: (userId: number, input: Record<string, unknown>) => {
if (input.title === 'boom') throw new ValidationError('bad dates');
return { trip: { id: 99, user_id: userId, ...input }, tripId: 99, reminderDays: 3 };
},
list: () => [{ id: 1 }],
removeMember: vi.fn(),
} as unknown as import('../../../src/nest/trips/trips.service').TripsService;
const factory = new PluginHostDepsFactory(budgetStub, reservationsStub, tagsStub, categoriesStub, todoStub, packingStub, oauthStub, dayNotesStub, assignmentsStub, llmConfigStub, new DatabaseService(mockDb), filesStub, collabStub, vacayStub, daysStub, permissionsStub, exchangeRatesStub, addonsStub, new RealtimeService(), tripsStub);
const stubRouter: PluginCallRouter = { callPlugin: async () => undefined, emitPluginEvent: () => {} };
const createRealRpcHost = (id: string, granted: ReadonlySet<string>, router: PluginCallRouter = stubRouter) => factory.create(id, granted, router);