Files
ApiCore/ApiCore-Backend/test/hive-catalog-seed.e2e-spec.ts
T
Nick SzaunigandClaude Opus 5.5 6e13236d49 feat: holographic 3D hive scene and Futtertasche (Beute L3, first cut)
- 3D dependencies (three, react-three-fiber, drei, postprocessing,
  motion), exact versions, supply-chain checked; scene loaded only on
  demand on the colony page (283 kB gzip), SVG stays as fallback
- glass hologram per owner design 2026-10-16: true Liebig Zander
  shapes (grips, rebate, Wanderboden foot), cool blue edge glow,
  click any part -> it flies to the front while the rest stays as a
  faint ghost; focus shows content (feed levels, Bausperre, pockets);
  frames rise on hover and take their type look (colour + comb pattern)
- full-bleed card with frosted overlays and glass buttons
- Futtertasche (FRAME_FEEDER): new part kind occupying frame slots
  with a fill level, catalog row, validator rules in both ports,
  outline, sheets, follow-ups, settings

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 09:12:03 +02:00

203 lines
5.7 KiB
TypeScript

import { PERMISSION_CATALOG } from '../src/auth/permissions/permission-catalog';
import { PermissionCatalogSyncService } from '../src/auth/permissions/permission-catalog-sync.service';
import { PrismaService } from '../src/prisma/prisma.service';
const SYSTEM_ID = 'hive-system-zander-liebig-10';
const PART_PREFIX = 'hive-part-zl10-';
const PART_SLUGS = [
'floor',
'floor-insert',
'box-10',
'box-empty',
'divider-board',
'queen-excluder',
'feeder',
'cover-abdeckfix',
'cover-lotus',
'propolis-screen',
'inner-lid',
'insulation',
'roof',
'evaporator',
'frame-feeder',
] as const;
interface StateField {
key: string;
kind: string;
feedType?: string;
[extra: string]: unknown;
}
/**
* The Liebig-Zander start catalog arrives with the migration (fixed ids) and
* `hive-systems:manage` joins ADMIN through the boot-time catalog sync.
* Checked against the migrated test database.
*/
describe('Hive catalog seed (e2e)', () => {
let prisma: PrismaService;
beforeAll(() => {
prisma = new PrismaService();
});
afterAll(async () => {
await prisma.$disconnect();
});
async function partType(slug: string) {
return prisma.hivePartType.findUniqueOrThrow({
where: { id: `${PART_PREFIX}${slug}` },
});
}
it('seeds the Zander-Liebig system with its geometry and template', async () => {
const system = await prisma.hiveSystem.findUniqueOrThrow({
where: { id: SYSTEM_ID },
});
expect(system).toMatchObject({
name: 'Zander nach Dr. Liebig, 10 Rähmchen',
frameWidthMm: 477,
frameHeightMm: 220,
footprintLMm: 525,
footprintWMm: 430,
active: true,
});
});
it('seeds 15 active part types for the system under fixed ids', async () => {
const types = await prisma.hivePartType.findMany({
where: { systemId: SYSTEM_ID, active: true },
orderBy: { id: 'asc' },
});
expect(types.map((t) => t.id)).toEqual(
PART_SLUGS.map((slug) => `${PART_PREFIX}${slug}`).sort(),
);
});
it('describes the 10-frame box with 10 slots and 230 mm height', async () => {
const box = await partType('box-10');
expect(box).toMatchObject({
kind: 'BOX',
boxDepth: 'FULL',
frameSlots: 10,
heightMm: 230,
lengthMm: 525,
widthMm: 430,
});
expect((await partType('box-empty')).frameSlots).toBe(0);
});
it('gives the feeder two level fields with a feed type each', async () => {
const feeder = await partType('feeder');
const fields = feeder.stateSchema as unknown as StateField[];
expect(fields).toHaveLength(2);
expect(fields.every((f) => f.kind === 'level')).toBe(true);
expect(fields.map((f) => [f.key, f.feedType])).toEqual([
['syrupLiters', 'SUGAR_WATER'],
['fondantKg', 'FEED_PASTE'],
]);
});
it('seeds the frame feeder over two frame slots with a 2 l syrup level', async () => {
const pocket = await partType('frame-feeder');
const fields = pocket.stateSchema as unknown as StateField[];
expect(pocket).toMatchObject({
kind: 'FRAME_FEEDER',
slotWidth: 2,
frameSlots: null,
lengthMm: 477,
widthMm: 70,
heightMm: 220,
});
expect(fields).toEqual([
{
key: 'syrupLiters',
label: 'Flüssigfutter',
kind: 'level',
unit: 'l',
max: 2,
step: 0.25,
feedType: 'SUGAR_WATER',
},
]);
});
it('gives the other stateful parts their choice and level fields', async () => {
const keysOf = async (slug: string) =>
((await partType(slug)).stateSchema as unknown as StateField[]).map(
(f) => f.key,
);
expect(await keysOf('floor')).toEqual(['entrance', 'varroaTray']);
expect(await keysOf('divider-board')).toEqual(['bungHoles']);
expect(await keysOf('inner-lid')).toEqual(['feedHole']);
expect(await keysOf('evaporator')).toEqual(['acidMl']);
expect(await keysOf('roof')).toEqual([]);
});
it('stores the system template without ids, with 10 foundation frames', async () => {
const system = await prisma.hiveSystem.findUniqueOrThrow({
where: { id: SYSTEM_ID },
});
const template = system.template as unknown as {
schemaVersion: number;
stack: {
id?: string;
partTypeId: string;
state: Record<string, unknown>;
frames?: { slot: number; type: string }[];
}[];
};
expect(template.schemaVersion).toBe(1);
expect(template.stack.map((p) => p.partTypeId)).toEqual([
`${PART_PREFIX}floor`,
`${PART_PREFIX}box-10`,
`${PART_PREFIX}cover-abdeckfix`,
`${PART_PREFIX}inner-lid`,
`${PART_PREFIX}insulation`,
`${PART_PREFIX}roof`,
]);
for (const part of template.stack) {
expect(part.id).toBeUndefined();
expect(part.state).toEqual({});
}
const frames = template.stack[1].frames ?? [];
expect(frames).toHaveLength(10);
expect(frames.map((f) => f.slot)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(frames.every((f) => f.type === 'FOUNDATION')).toBe(true);
});
it('lists hive-systems:manage in the permission catalog', () => {
const entry = PERMISSION_CATALOG.find(
(e) => e.key === 'hive-systems:manage',
);
expect(entry).toMatchObject({
resource: 'hive-systems',
action: 'manage',
labelDe: 'Pflegen',
groupDe: 'Beutensysteme',
});
});
it('grants hive-systems:manage to ADMIN after the catalog sync', async () => {
await new PermissionCatalogSyncService(prisma).onModuleInit();
const grant = await prisma.rolePermission.findFirst({
where: {
role: { key: 'ADMIN' },
permission: { key: 'hive-systems:manage' },
},
});
expect(grant).not.toBeNull();
});
});