mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
f72aba0c91
The last 20 wire methods move onto the decorators: db.* and the three unconditional methods (DbRpc), meta.* (MetaRpc) and the host-mediated surface of users/broadcasts/notify/ai/oauth/scheduler (HostSurfaceRpc). That closes the ledger at 113/113, so the legacy has() blocks in rpc-host.ts are gone and the router is down to what was never domain-specific: build the granted subset of the dispatch map, dispatch, audit, map errors. PluginHostDepsFactory was the 26-argument wiring sheet behind those blocks. With the handlers in the domains it has two dependencies left, so it is now PluginRpcHostFactory: the plugin's own sqlite handle, the inter-plugin router bound with this plugin as the caller, and the audit sink. The 18 host-to-plugin hook calls get the same treatment. They used to spell out the fn name and the timeout at each controller, which meant nothing checked that a hook:* grant on the consent screen had a consumer behind it, that the fn matched what the SDK documents, or that two call sites agreed on the budget. PluginHooks declares all 18 with @PluginHook and performs them, and the controllers call it by name. With both halves complete, requireTotalCoverage goes on: a KNOWN_METHOD without a handler, or a hook without a consumer, now fails app boot instead of surfacing as a PERMISSION_DENIED nobody can explain.
103 lines
5.0 KiB
TypeScript
103 lines
5.0 KiB
TypeScript
/**
|
|
* DayScheduleController (#plugins): bounded time contributions plugins attach to
|
|
* the day plan via the dayScheduleProvider hook. Mirrors the map-markers
|
|
* hardening — gate + fail-safe + server-side normalization — plus the schedule
|
|
* specifics: dayIds are validated against the trip's own days and minutes are
|
|
* clamped to a day, because this output feeds displayed timing totals.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const { canAccessTrip, pluginsEnabled, tripDays } = vi.hoisted(() => ({
|
|
canAccessTrip: vi.fn((tripId: number, userId: number) => (tripId === 1 && userId === 5 ? { id: 1 } : undefined)),
|
|
pluginsEnabled: vi.fn(() => true),
|
|
tripDays: { value: [{ id: 10 }, { id: 11 }] as Array<{ id: number }> },
|
|
}));
|
|
vi.mock('../../../src/db/database', () => ({
|
|
db: { prepare: () => ({ all: () => tripDays.value }) },
|
|
canAccessTrip,
|
|
}));
|
|
import { db as dbConn } from '../../../src/db/database';
|
|
import { DatabaseService } from '../../../src/nest/database/database.service';
|
|
vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
|
|
|
|
import { DayScheduleController } from '../../../src/nest/plugins/day-schedule.controller';
|
|
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
|
|
function controller(invoke: (id: string) => unknown, providers = ['p1']) {
|
|
const runtime = {
|
|
providersOf: vi.fn(() => providers),
|
|
daySchedule: vi.fn(async (id: string) => invoke(id)),
|
|
} as unknown as PluginHooks;
|
|
return { c: new DayScheduleController(runtime, new DatabaseService(dbConn)), runtime };
|
|
}
|
|
const item = (over: Record<string, unknown> = {}) => ({ id: 's1', dayId: 10, label: 'Charging', ...over });
|
|
|
|
describe('DayScheduleController', () => {
|
|
beforeEach(() => { pluginsEnabled.mockReturnValue(true); canAccessTrip.mockReturnValue({ id: 1 } as never); });
|
|
|
|
it('gates: disabled / no user / non-member all return [] (no plugin calls on the first)', async () => {
|
|
pluginsEnabled.mockReturnValue(false);
|
|
const x = controller(() => [item()]);
|
|
expect(await x.c.get('1', req(5))).toEqual({ items: [] });
|
|
expect(x.runtime.providersOf).not.toHaveBeenCalled();
|
|
pluginsEnabled.mockReturnValue(true);
|
|
|
|
expect((await controller(() => [item()]).c.get('1', req(undefined))).items).toEqual([]);
|
|
canAccessTrip.mockReturnValue(undefined as never);
|
|
expect((await controller(() => [item()]).c.get('1', req(5))).items).toEqual([]);
|
|
});
|
|
|
|
it('keeps a valid item, stamps pluginId, rounds + clamps minutes, defaults the tone', async () => {
|
|
const { c } = controller(() => [
|
|
item({ assignmentId: 42, minutes: 35.6, tone: 'success' }),
|
|
item({ id: 's2', minutes: 999999, position: 'start' }),
|
|
]);
|
|
const out = (await c.get('1', req(5))).items;
|
|
expect(out[0]).toMatchObject({ pluginId: 'p1', dayId: 10, assignmentId: 42, minutes: 36, tone: 'success' });
|
|
expect(out[1]).toMatchObject({ minutes: 1440, position: 'start', tone: 'default' });
|
|
});
|
|
|
|
it("drops items anchored to another trip's day, without id/label, or non-objects", async () => {
|
|
const { c } = controller(() => [
|
|
item({ dayId: 999 }), // not a day of this trip
|
|
item({ id: '' }), // no id
|
|
item({ label: '' }), // no label
|
|
item({ dayId: 'x' }), // non-numeric day
|
|
null, // non-object
|
|
item({ id: 'good', dayId: 11 }),
|
|
]);
|
|
const out = (await c.get('1', req(5))).items;
|
|
expect(out.map(i => i.id)).toEqual(['good']);
|
|
});
|
|
|
|
it('ignores bogus anchors and positions instead of failing the item', async () => {
|
|
const { c } = controller(() => [item({ assignmentId: 'x', reservationId: 1.5, position: 'middle', minutes: -10 })]);
|
|
const out = (await c.get('1', req(5))).items;
|
|
expect(out[0].assignmentId).toBeUndefined();
|
|
expect(out[0].reservationId).toBeUndefined();
|
|
expect(out[0].position).toBeUndefined();
|
|
expect(out[0].minutes).toBeUndefined();
|
|
});
|
|
|
|
it('caps items at 60 per provider and skips a failing provider', async () => {
|
|
const many = Array.from({ length: 80 }, (_, i) => item({ id: `s${i}` }));
|
|
const { c } = controller((id) => (id === 'bad' ? (() => { throw new Error('boom'); })() : many), ['good', 'bad']);
|
|
const out = (await c.get('1', req(5))).items;
|
|
expect(out).toHaveLength(60);
|
|
});
|
|
|
|
it('bounds work on an all-invalid oversized payload', async () => {
|
|
const huge = Array.from({ length: 100_000 }, () => ({ id: 's', dayId: 999, label: 'x' })); // foreign day -> all dropped
|
|
const { c } = controller(() => huge);
|
|
expect((await c.get('1', req(5))).items).toEqual([]);
|
|
});
|
|
|
|
it('skips the day lookup entirely when no provider is active', async () => {
|
|
const { c, runtime } = controller(() => [item()], []);
|
|
expect((await c.get('1', req(5))).items).toEqual([]);
|
|
expect(runtime.daySchedule).not.toHaveBeenCalled();
|
|
});
|
|
});
|