Files
TREK/server/tests/unit/plugins/manifest.test.ts
T
mauriceboe 17f3c86967 feat(plugins): hero widget slot + Koffi reference plugin
Widget plugins can now declare capabilities.widget.slot 'hero' to render as a
transparent, click-through overlay sitting on the boarding-pass bar's top edge
(migration 157 persists capabilities; manifest validation server+SDK, feed
exposes the slot, dashboard mounts hero frames above the pass). Sidebar stays
the default slot.

Replaces the trip-countdown example with Koffi, the TREK mascot: an animated
suitcase with a 14-state behavior engine driven by real trip data — walking,
waving, napping, trolley rolls, passport-stamp stickers, a split-flap luggage-
tag countdown under 7 days, and sunglasses while the trip runs. Validated by
the SDK suite like any author plugin; published as mauriceboe/trek-plugin-koffi
in the registry.

Migrations 156/157 follow the idempotent ALTER pattern (the reconciliation
test re-runs everything from v135, so duplicate-column must stay non-fatal).
2026-07-04 19:11:23 +02:00

64 lines
2.8 KiB
TypeScript

/**
* trek-plugin.json validation (#plugins, M4). Rejects invalid ids/versions/types,
* unknown permissions, native modules, and http:outbound without egress.
*/
import { describe, it, expect } from 'vitest';
import { parseManifest, ManifestError } from '../../../src/nest/plugins/install/manifest';
const base = { id: 'flight-tracker', name: 'Flight', version: '1.2.0', type: 'widget', apiVersion: 1 };
describe('parseManifest', () => {
it('parses a valid manifest with defaults', () => {
const m = parseManifest({ ...base });
expect(m.id).toBe('flight-tracker');
expect(m.icon).toBe('Blocks');
expect(m.nativeModules).toBe(false);
expect(m.permissions).toEqual([]);
});
it('derives minTrekVersion from the trek range', () => {
expect(parseManifest({ ...base, trek: '>=3.2.0 <4.0.0' }).minTrekVersion).toBe('3.2.0');
});
it('keeps known permissions + parses settings', () => {
const m = parseManifest({
...base,
permissions: ['db:own', 'http:outbound:api.x.com'],
egress: ['api.x.com'],
settings: [{ key: 'api_key', input_type: 'password', scope: 'instance', secret: true }, { bad: 1 }],
});
expect(m.permissions).toContain('db:own');
expect(m.settings).toHaveLength(1);
expect(m.settings[0]).toMatchObject({ key: 'api_key', secret: true, scope: 'instance' });
});
it.each([
['Bad-Id', { ...base, id: 'Bad-Id' }, /invalid id/],
['bad version', { ...base, version: '1.x' }, /invalid version/],
['bad type', { ...base, type: 'thing' }, /invalid type/],
['native', { ...base, nativeModules: true }, /native modules/],
['unknown perm', { ...base, permissions: ['fs:read'] }, /unknown permission/],
['outbound no egress', { ...base, permissions: ['http:outbound'] }, /egress\[\] is empty/],
['wildcard egress', { ...base, permissions: ['http:outbound'], egress: ['*'] }, /bare "\*"/],
['not an object', 'nope', /not an object/],
['missing name', { id: 'x-plugin', version: '1.0.0', type: 'page' }, /missing\/invalid "name"/],
])('rejects: %s', (_label, input, re) => {
expect(() => parseManifest(input)).toThrow(ManifestError);
expect(() => parseManifest(input)).toThrow(re as RegExp);
});
});
describe('parseManifest capabilities', () => {
it('parses a hero widget slot and defaults to sidebar', () => {
const hero = parseManifest({ ...base, capabilities: { widget: { slot: 'hero', title: 'T' } } });
expect(hero.capabilities.widget?.slot).toBe('hero');
const plain = parseManifest({ ...base, capabilities: { widget: {} } });
expect(plain.capabilities.widget?.slot).toBe('sidebar');
expect(parseManifest(base).capabilities).toEqual({});
});
it('rejects an unknown widget slot', () => {
expect(() => parseManifest({ ...base, capabilities: { widget: { slot: 'floating' } } })).toThrow(ManifestError);
});
});