Files
TREK/server/tests/unit/plugins/rpc-coverage.test.ts
T
Maurice 2ec6d006b0 Move the trip surface onto the decorators
Twelve more methods, so the registry owns 50 of 113. rpc-host.ts is down from 1332
lines to 830 and the deps factory from 944 to 500.

trips.update keeps its two field-level gates: archiving and changing the cover each
need their own admin-configurable permission on top of trip_edit, so a plugin
cannot do either on a trip it may otherwise edit. Member management keeps its own
grant for the same kind of reason, since adding a member grants trip access, and
the owner still cannot be removed through it because that would orphan the trip.

canEditTrip stays in HostDeps for now: the db:meta gate still resolves an entity's
edit permission through it and has not moved yet.
2026-08-09 14:13:56 +02:00

116 lines
4.2 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(50);
expect(legacy.size).toBe(total - 50);
});
it('RPCLEDGER-006 the migrated methods are exactly the ones this PR claims', () => {
expect([...fromRegistry].sort()).toEqual([
'categories.list',
'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.listMine',
'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',
]);
});
});