Files
TREK/server/tests/unit/plugins/rpc-coverage.test.ts
T
Maurice 010242b107 Move costs, reservations and accommodations onto the decorators
Eleven more methods, so the registry owns 61 of 113. rpc-host.ts is at 771 lines,
down from 1332.

costs is the first addon-gated domain, and the addon check sits in a different
place in each of the two reads: inside the membership callback for getByTrip, so a
caller without trip access never learns whether the addon is on, and before
everything for listMine, which has no trip to check. Both orderings are asserted.
The writes keep their own refusal wording ("no permission to edit costs on trip N")
instead of going through requireTripEdit, which words it differently.

The booking paths are cascade-heavy and stay that way: creating a reservation can
create an accommodation and a budget item, deleting one removes both again, and
deleting a lodging block takes its partner reservation and budget item with it.
Each cascade is a broadcast the web app relies on, so they are asserted by event
name and order.

CostsRpc reaches the trip list through trips.bridge rather than by injection.
BudgetModule cannot import TripsModule, because TripsModule already imports
BudgetModule; budget.mcp.ts uses the same escape for the same reason.
2026-08-09 16:38:43 +02:00

127 lines
4.4 KiB
TypeScript

/**
* The coverage ledger: the anchor for the whole decorator rollout.
*
* Every wire method is owned exactly once, either by the legacy router blocks in
* rpc-host.ts or by a decorated *.rpc.ts class in the registry. Both sets are derived
* by construction, so no list is maintained by hand and the test cannot drift.
*
* It fails the moment a migration PR moves a method and forgets to delete the legacy
* registration, deletes one without adding it, or forgets to list a new *.rpc.ts class
* in allRpcControllers().
*/
import { describe, it, expect } from 'vitest';
import { PluginRpcHost } from '../../../src/nest/plugins/host/rpc-host';
import { createTestPluginRegistry } from '../../../src/nest/plugins/host/rpc-kit/testing';
import {
KNOWN_METHODS,
KNOWN_PERMISSIONS,
UNCONDITIONAL_METHODS,
} from '../../../src/nest/plugins/protocol/envelope';
import { allRpcControllers, makeDeps } from '../../helpers/rpc-host-deps';
/** Reads the router's private dispatch map. That map IS the thing under test. */
const boundMethods = (host: PluginRpcHost): Set<string> =>
new Set((host as unknown as { methods: Map<string, unknown> }).methods.keys());
describe('plugin RPC coverage ledger', () => {
const registry = createTestPluginRegistry(allRpcControllers());
// Every permission granted, so the router registers everything it can.
const host = new PluginRpcHost('probe', new Set<string>(KNOWN_PERMISSIONS), makeDeps(), registry);
const all = boundMethods(host);
const fromRegistry = registry.methodNames();
const legacy = new Set([...all].filter((m) => !fromRegistry.has(m)));
it('RPCLEDGER-001 no method is owned by both the registry and the legacy router', () => {
expect([...fromRegistry].filter((m) => legacy.has(m))).toEqual([]);
});
it('RPCLEDGER-002 every registered method is one the wire protocol knows', () => {
const vocabulary = new Set<string>([...KNOWN_METHODS, ...UNCONDITIONAL_METHODS]);
expect([...all].filter((m) => !vocabulary.has(m))).toEqual([]);
});
it('RPCLEDGER-003 no method in the vocabulary is orphaned', () => {
const vocabulary = new Set<string>([...KNOWN_METHODS, ...UNCONDITIONAL_METHODS]);
expect([...vocabulary].filter((m) => !all.has(m))).toEqual([]);
});
it('RPCLEDGER-004 the two owner sets partition the vocabulary exactly', () => {
expect(all).toEqual(new Set<string>([...KNOWN_METHODS, ...UNCONDITIONAL_METHODS]));
expect(fromRegistry.size + legacy.size).toBe(all.size);
});
it('RPCLEDGER-005 the migration ledger reads as expected for this PR', () => {
// Update these two numbers in every rollout PR. They make the diff state the size
// of the move out loud, which is the number a reviewer wants to see.
const total = KNOWN_METHODS.length + UNCONDITIONAL_METHODS.length;
expect(fromRegistry.size).toBe(61);
expect(legacy.size).toBe(total - 61);
});
it('RPCLEDGER-006 the migrated methods are exactly the ones this PR claims', () => {
expect([...fromRegistry].sort()).toEqual([
'accommodations.create',
'accommodations.delete',
'accommodations.update',
'categories.list',
'costs.create',
'costs.delete',
'costs.getByTrip',
'costs.listMine',
'costs.update',
'daynotes.create',
'daynotes.delete',
'daynotes.list',
'daynotes.update',
'days.create',
'days.delete',
'days.update',
'files.create',
'files.createLink',
'files.getContent',
'files.list',
'files.softDelete',
'files.update',
'itinerary.assign',
'itinerary.unassign',
'packing.create',
'packing.createBag',
'packing.delete',
'packing.deleteBag',
'packing.list',
'packing.listBags',
'packing.setBagMembers',
'packing.update',
'packing.updateBag',
'places.create',
'places.delete',
'places.update',
'rates.get',
'reservations.create',
'reservations.delete',
'reservations.listMine',
'reservations.update',
'tags.create',
'tags.delete',
'tags.list',
'tags.update',
'todos.create',
'todos.delete',
'todos.list',
'todos.update',
'trips.addMember',
'trips.create',
'trips.getAccommodations',
'trips.getById',
'trips.getDays',
'trips.getPlaces',
'trips.getReservations',
'trips.listMine',
'trips.members',
'trips.removeMember',
'trips.update',
'weather.get',
]);
});
});