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]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-16 09:12:03 +02:00
1 parent c608f5d405
commit 6e13236d49
100 files changed
+9432 -209

No files matched your search

@@ -0,0 +1,10 @@
-- Beute in 3D: Futtertasche (FRAME_FEEDER, spec 2026-10-15 sections 4.1, 4.3,
-- 5.1). A frame feeder hangs in a box in place of frames and covers
-- `slotWidth` frame slots. The enum value cannot be used in the migration that
-- adds it, so the catalog row comes in 20261018100100.
-- AlterEnum
ALTER TYPE "HivePartKind" ADD VALUE 'FRAME_FEEDER';
-- AlterTable
ALTER TABLE "HivePartType" ADD COLUMN "slotWidth" INTEGER;
@@ -0,0 +1,20 @@
-- Beute in 3D: start catalog row for the Futtertasche (owner wish 2026-10-16,
-- spec sections 4.3 and 4.1). Fixed id like the other start data; dimensions and
-- the 2 l capacity are assumptions the owner confirms (catalog data, editable).
INSERT INTO "HivePartType" ("id", "systemId", "kind", "name", "lengthMm", "widthMm", "heightMm", "boxDepth", "frameSlots", "slotWidth", "stateSchema", "active", "updatedAt")
VALUES (
'hive-part-zl10-frame-feeder',
'hive-system-zander-liebig-10',
'FRAME_FEEDER',
'Holz-Futtertasche Zander, doppelte Wabenbreite',
477,
70,
220,
NULL,
NULL,
2,
'[{"key":"syrupLiters","label":"Flüssigfutter","kind":"level","unit":"l","max":2,"step":0.25,"feedType":"SUGAR_WATER"}]'::jsonb,
true,
CURRENT_TIMESTAMP
);
+2
View File
@@ -2217,6 +2217,7 @@ model HivePartType {
heightMm Int
boxDepth HiveBoxDepth?
frameSlots Int? // nur BOX
slotWidth Int? // nur FRAME_FEEDER: belegte Rähmchen-Plätze
stateSchema Json // Abschnitt 4.2
active Boolean @default(true)
createdAt DateTime @default(now())
@@ -2269,6 +2270,7 @@ enum HivePartKind {
ROOF
FLOOR_INSERT
EVAPORATOR
FRAME_FEEDER
}
enum HiveBoxDepth {
@@ -26,12 +26,14 @@ export const HIVE_PART_KINDS = [
'ROOF',
'FLOOR_INSERT',
'EVAPORATOR',
'FRAME_FEEDER',
] as const;
export const HIVE_BOX_DEPTHS = ['FULL', 'SHALLOW'] as const;
const MAX_MM = 5000;
const MAX_SCHEMA_FIELDS = 30;
const MAX_FRAME_SLOTS = 20;
const MAX_SLOT_WIDTH = 5;
export class CreateHivePartTypeDto {
@ApiProperty({ enum: HIVE_PART_KINDS })
@@ -80,6 +82,15 @@ export class CreateHivePartTypeDto {
@Max(MAX_FRAME_SLOTS)
frameSlots?: number;
@ApiPropertyOptional({
description: 'Required for kind FRAME_FEEDER, forbidden otherwise (1-5)',
})
@IsOptional()
@IsInt()
@Min(1)
@Max(MAX_SLOT_WIDTH)
slotWidth?: number;
@ApiPropertyOptional({ description: 'State fields, spec 4.2' })
@IsOptional()
@IsArray()
@@ -454,5 +454,61 @@ describe('HiveCatalogService', () => {
),
).rejects.toBeInstanceOf(BadRequestException);
});
it('requires slotWidth for a FRAME_FEEDER', async () => {
await expect(
service.createPartType(
SYSTEM_ID,
{
kind: 'FRAME_FEEDER',
name: 'Tasche',
lengthMm: 1,
widthMm: 1,
heightMm: 1,
},
USER,
),
).rejects.toBeInstanceOf(BadRequestException);
});
it('rejects slotWidth on a part type that is no FRAME_FEEDER', async () => {
await expect(
service.createPartType(
SYSTEM_ID,
{
kind: 'ROOF',
name: 'Dach',
lengthMm: 1,
widthMm: 1,
heightMm: 1,
slotWidth: 2,
},
USER,
),
).rejects.toBeInstanceOf(BadRequestException);
await expect(
service.updatePartType('pt-1', { slotWidth: 2 }, USER),
).rejects.toBeInstanceOf(BadRequestException);
});
it('creates a FRAME_FEEDER with its slotWidth', async () => {
tx.hivePartType.create.mockResolvedValue({ id: 'new' });
await service.createPartType(
SYSTEM_ID,
{
kind: 'FRAME_FEEDER',
name: 'Tasche',
lengthMm: 477,
widthMm: 70,
heightMm: 220,
slotWidth: 2,
},
USER,
);
const [{ data }] = tx.hivePartType.create.mock.calls[0] as [
{ data: { kind: string; slotWidth: number } },
];
expect(data).toMatchObject({ kind: 'FRAME_FEEDER', slotWidth: 2 });
});
});
});
@@ -92,6 +92,7 @@ export class HiveCatalogService {
widthMm: row.widthMm,
heightMm: row.heightMm,
frameSlots: row.frameSlots,
slotWidth: row.slotWidth,
stateSchema: parseStateSchema(row.stateSchema),
active: row.active,
})),
@@ -191,6 +192,7 @@ export class HiveCatalogService {
if (dto.kind === 'BOX' && dto.frameSlots === undefined) {
throw new BadRequestException('frameSlots is required for a BOX');
}
this.assertSlotWidth(dto.kind, dto.slotWidth);
const stateSchema = dto.stateSchema ?? [];
this.assertSchemaShape(stateSchema);
return this.prisma
@@ -233,6 +235,7 @@ export class HiveCatalogService {
const before = await tx.hivePartType.findUnique({ where: { id } });
if (!before) throw new NotFoundException('Hive part type not found');
const { stateSchema, ...rest } = dto;
this.assertSlotWidth(before.kind, dto.slotWidth ?? before.slotWidth);
if (stateSchema !== undefined) {
this.assertSchemaShape(stateSchema);
this.assertAdditive(before.stateSchema, stateSchema);
@@ -262,6 +265,21 @@ export class HiveCatalogService {
.catch((error: unknown) => conflictOnDuplicate(error, 'Part type'));
}
/** slotWidth belongs to FRAME_FEEDER part types and to no other kind. */
private assertSlotWidth(
kind: string,
slotWidth: number | null | undefined,
): void {
if (kind === 'FRAME_FEEDER' && slotWidth == null) {
throw new BadRequestException('slotWidth is required for a FRAME_FEEDER');
}
if (kind !== 'FRAME_FEEDER' && slotWidth != null) {
throw new BadRequestException(
'slotWidth is only allowed for a FRAME_FEEDER',
);
}
}
private assertSchemaShape(stateSchema: unknown): void {
const problems = stateSchemaProblems(stateSchema);
if (problems.length > 0) {
@@ -115,4 +115,32 @@ describe('deriveHiveFacts', () => {
};
expect(deriveHiveFacts(state, catalog).evaporatorAcidMl).toBe(0);
});
it('reports a frame feeder level with its feed type', () => {
const state = stackOf({
...makeBox(['NONE', 'NONE', ...Array<FrameType>(8).fill('BROOD')]),
inserts: [
{
id: 'ff1',
partTypeId: T('frame-feeder'),
slot: 1,
state: { syrupLiters: 1.5 },
},
],
});
const facts = deriveHiveFacts(state, catalog);
expect(facts.levels).toEqual([
{
partId: 'ff1',
key: 'syrupLiters',
label: 'Flüssigfutter',
unit: 'l',
value: 1.5,
max: 2,
feedType: 'SUGAR_WATER',
},
]);
// The covered NONE slots are not built frames.
expect(facts.builtFrames).toBe(8);
});
});
+6 -1
View File
@@ -13,7 +13,8 @@ export type HivePartKind =
| 'INSULATION'
| 'ROOF'
| 'FLOOR_INSERT'
| 'EVAPORATOR';
| 'EVAPORATOR'
| 'FRAME_FEEDER';
// Mirrors the Prisma enum HiveBoxDepth; used from Task 3 on.
export type HiveBoxDepth = 'FULL' | 'SHALLOW';
@@ -61,6 +62,8 @@ export interface HivePart {
state: Record<string, number | string>;
inserts?: HivePart[];
frames?: HiveFrame[];
// Only on FRAME_FEEDER inserts: first frame slot (1-based) the pocket covers.
slot?: number;
}
export interface HiveState {
@@ -112,6 +115,8 @@ export interface CatalogPartType {
widthMm: number;
heightMm: number;
frameSlots: number | null;
// Only FRAME_FEEDER: number of consecutive frame slots the pocket covers.
slotWidth: number | null;
stateSchema: StateField[];
active: boolean;
}
+113 -13
View File
@@ -10,10 +10,21 @@ import type { CatalogPartType, HiveCatalog, StateField } from './hive-types';
// keep it: root shape (schemaVersion must be 1 -> STATE_OUT_OF_RANGE; extra
// root keys -> UNKNOWN_FIELD), LIMIT_PARTS, then per stack part: id, unknown
// fields (UNKNOWN_FIELD), type, label, state, frames (per frame: slot, unknown
// fields, type, name), inserts (each insert in the same order), then FLOOR, then ROOF.
// fields, type, name), inserts (each insert in the same order; then per insert
// RULE_INSERT_HOST, nested inserts and `slot` / RULE_FEEDER_SLOT; after all
// inserts of a box the FRAME_FEEDER overlap check, also RULE_FEEDER_SLOT),
// then FLOOR, then ROOF.
// Text lengths are UTF-16 code units (`.length`); the frontend must do the same.
const ROOT_KEYS = ['schemaVersion', 'stack'];
const PART_KEYS = ['id', 'partTypeId', 'label', 'state', 'inserts', 'frames'];
const PART_KEYS = [
'id',
'partTypeId',
'label',
'state',
'inserts',
'frames',
'slot',
];
const FRAME_KEYS = ['slot', 'type', 'name'];
function unknownFields(
@@ -168,6 +179,80 @@ function validatePartCommon(
return { part, type };
}
const INSERT_KINDS: Partial<Record<string, string[]>> = {
FLOOR: ['FLOOR_INSERT'],
BOX: ['EVAPORATOR', 'FRAME_FEEDER'],
};
interface SlotRange {
from: number;
to: number;
}
/**
* RULE_FEEDER_SLOT checks for one insert: `slot` only on FRAME_FEEDER, an
* integer inside 1..frameSlots for the covered width, and every covered frame
* NONE. Returns the covered range for the overlap check, or null.
*/
function validateFeederSlot(
insert: Obj,
type: CatalogPartType,
host: Obj,
hostType: CatalogPartType,
where: string,
out: string[],
): SlotRange | null {
if (type.kind !== 'FRAME_FEEDER') {
if (insert.slot !== undefined) {
out.push(`RULE_FEEDER_SLOT: ${where} has a slot but is no frame feeder`);
}
return null;
}
if (hostType.kind !== 'BOX') return null;
const slot = insert.slot;
const width = type.slotWidth;
const slots = hostType.frameSlots ?? 0;
if (typeof slot !== 'number' || !Number.isInteger(slot) || slot < 1) {
out.push(`RULE_FEEDER_SLOT: ${where} needs an integer slot of 1 or more`);
return null;
}
if (width === null || !Number.isInteger(width) || width < 1) {
out.push(`RULE_FEEDER_SLOT: ${where} part type has no slot width`);
return null;
}
const range = { from: slot, to: slot + width - 1 };
if (range.to > slots) {
out.push(
`RULE_FEEDER_SLOT: ${where} covers slots ${range.from}-${range.to} but the box has ${slots}`,
);
return null;
}
const frames = Array.isArray(host.frames) ? (host.frames as Obj[]) : [];
for (let at = range.from; at <= range.to; at++) {
if (frames[at - 1]?.type !== 'NONE') {
out.push(
`RULE_FEEDER_SLOT: ${where} covers frame ${at} which is not NONE`,
);
return null;
}
}
return range;
}
function validateFeederOverlap(
ranges: SlotRange[],
where: string,
out: string[],
): void {
const sorted = [...ranges].sort((a, b) => a.from - b.from);
for (let i = 1; i < sorted.length; i++) {
if (sorted[i].from <= sorted[i - 1].to) {
out.push(`RULE_FEEDER_SLOT: ${where} has overlapping frame feeders`);
return;
}
}
}
function validateInserts(
host: Obj,
hostType: CatalogPartType,
@@ -181,24 +266,25 @@ function validateInserts(
out.push(`RULE_INSERT_HOST: ${where} inserts must be a list`);
return;
}
const allowed =
hostType.kind === 'FLOOR'
? 'FLOOR_INSERT'
: hostType.kind === 'BOX'
? 'EVAPORATOR'
: null;
const allowed = INSERT_KINDS[hostType.kind] ?? null;
if (inserts.length > 0 && allowed === null) {
out.push(`RULE_INSERT_HOST: ${where} cannot hold inserts`);
}
if (allowed !== null && inserts.length > 1) {
out.push(`RULE_INSERT_LIMIT: ${where} holds at most one ${allowed}`);
if (hostType.kind === 'FLOOR' && inserts.length > 1) {
out.push(`RULE_INSERT_LIMIT: ${where} holds at most one FLOOR_INSERT`);
}
const ranges: SlotRange[] = [];
let evaporators = 0;
inserts.forEach((insert: unknown, index) => {
const here = `${where}.inserts[${index}]`;
const checked = validatePartCommon(insert, here, ctx);
if (!checked) return;
if (allowed !== null && checked.type.kind !== allowed) {
out.push(`RULE_INSERT_HOST: ${here} must be ${allowed}`);
if (checked.type.kind === 'EVAPORATOR') evaporators += 1;
if (evaporators > 1 && checked.type.kind === 'EVAPORATOR') {
out.push(`RULE_INSERT_LIMIT: ${where} holds at most one EVAPORATOR`);
}
if (allowed !== null && !allowed.includes(checked.type.kind)) {
out.push(`RULE_INSERT_HOST: ${here} must be ${allowed.join(' or ')}`);
}
if (checked.part.inserts !== undefined) {
const nested = checked.part.inserts;
@@ -206,7 +292,17 @@ function validateInserts(
out.push(`RULE_INSERT_HOST: ${here} cannot hold inserts`);
}
}
const range = validateFeederSlot(
checked.part,
checked.type,
host,
hostType,
here,
out,
);
if (range) ranges.push(range);
});
validateFeederOverlap(ranges, where, out);
}
function validateStackRules(
@@ -256,12 +352,16 @@ export function validateHiveState(
if (!checked) return;
if (
checked.type.kind === 'FLOOR_INSERT' ||
checked.type.kind === 'EVAPORATOR'
checked.type.kind === 'EVAPORATOR' ||
checked.type.kind === 'FRAME_FEEDER'
) {
out.push(
`RULE_INSERT_HOST: ${where} must be an insert, not a stack part`,
);
}
if (checked.part.slot !== undefined) {
out.push(`RULE_FEEDER_SLOT: ${where} has a slot but is no frame feeder`);
}
validateInserts(checked.part, checked.type, where, ctx);
});
validateStackRules(types, out);
+6
View File
@@ -689,6 +689,12 @@ describe('Backup round trip (e2e)', () => {
expect(exportedHive.hiveSystem.some((row) => row.id === systemId)).toBe(
true,
);
// The frame feeder's slotWidth travels with the part type rows.
expect(
exportedHive.hivePartType.find(
(row) => row.id === 'hive-part-zl10-frame-feeder',
)?.slotWidth,
).toBe(2);
// The colony is lost; the catalog rows stay, as a fresh install has
// them from the migration.
@@ -0,0 +1,81 @@
{
"name": "frame feeder without slot fails",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addInsert",
"hostId": "b1",
"part": {
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {}
}
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "RULE_FEEDER_SLOT"
}
}
@@ -0,0 +1,82 @@
{
"name": "frame feeder slot 0 fails",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addInsert",
"hostId": "b1",
"part": {
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 0
}
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "RULE_FEEDER_SLOT"
}
}
@@ -0,0 +1,82 @@
{
"name": "frame feeder with a fractional slot fails",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "NONE"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addInsert",
"hostId": "b1",
"part": {
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1.5
}
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "RULE_FEEDER_SLOT"
}
}
@@ -0,0 +1,126 @@
{
"name": "setFrames putting a type on a covered slot fails",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "setFrames",
"boxId": "b1",
"frames": [
{
"slot": 1,
"type": "FOUNDATION"
},
{
"slot": 2,
"type": "FOUNDATION"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
]
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "RULE_FEEDER_SLOT"
}
}
@@ -0,0 +1,156 @@
{
"name": "setState on a frame feeder sets the level",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "setState",
"id": "p1",
"key": "syrupLiters",
"value": 2
}
],
"expect": {
"ok": true,
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {
"syrupLiters": 2
},
"slot": 1
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
}
}
}
@@ -0,0 +1,86 @@
{
"name": "setState on a frame feeder above max fails",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "setState",
"id": "p1",
"key": "syrupLiters",
"value": 2.25
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "STATE_OUT_OF_RANGE"
}
}
@@ -0,0 +1,162 @@
{
"name": "evaporator and frame feeder share a box",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addInsert",
"hostId": "b1",
"part": {
"id": "e1",
"partTypeId": "hive-part-zl10-evaporator",
"state": {}
}
}
],
"expect": {
"ok": true,
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
},
{
"id": "e1",
"partTypeId": "hive-part-zl10-evaporator",
"state": {}
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
}
}
}
@@ -0,0 +1,82 @@
{
"name": "slot on a non-feeder insert fails",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "FOUNDATION"
},
{
"slot": 2,
"type": "FOUNDATION"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addInsert",
"hostId": "b1",
"part": {
"id": "e1",
"partTypeId": "hive-part-zl10-evaporator",
"state": {},
"slot": 1
}
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "RULE_FEEDER_SLOT"
}
}
@@ -0,0 +1,40 @@
{
"name": "frame feeder in a box without slots fails",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b0",
"partTypeId": "hive-part-zl10-box-empty",
"state": {}
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addInsert",
"hostId": "b0",
"part": {
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "RULE_FEEDER_SLOT"
}
}
@@ -0,0 +1,94 @@
{
"name": "two evaporators still fail next to a frame feeder",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
},
{
"id": "e1",
"partTypeId": "hive-part-zl10-evaporator",
"state": {}
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addInsert",
"hostId": "b1",
"part": {
"id": "e2",
"partTypeId": "hive-part-zl10-evaporator",
"state": {}
}
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "RULE_INSERT_LIMIT"
}
}
@@ -0,0 +1,150 @@
{
"name": "frame feeder at slot 1 over two NONE frames is ok",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addInsert",
"hostId": "b1",
"part": {
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
}
],
"expect": {
"ok": true,
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
}
}
}
@@ -0,0 +1,200 @@
{
"name": "setFrames then addInsert places a frame feeder",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "FOUNDATION"
},
{
"slot": 2,
"type": "FOUNDATION"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "setFrames",
"boxId": "b1",
"frames": [
{
"slot": 1,
"type": "FOUNDATION"
},
{
"slot": 2,
"type": "FOUNDATION"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "NONE"
},
{
"slot": 5,
"type": "NONE"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
]
},
{
"op": "addInsert",
"hostId": "b1",
"part": {
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {
"syrupLiters": 1.5
},
"slot": 4
}
}
],
"expect": {
"ok": true,
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "FOUNDATION"
},
{
"slot": 2,
"type": "FOUNDATION"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "NONE"
},
{
"slot": 5,
"type": "NONE"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {
"syrupLiters": 1.5
},
"slot": 4
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
}
}
}
@@ -0,0 +1,82 @@
{
"name": "frame feeder over frames that are not NONE fails",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "FOUNDATION"
},
{
"slot": 2,
"type": "FOUNDATION"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addInsert",
"hostId": "b1",
"part": {
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "RULE_FEEDER_SLOT"
}
}
@@ -0,0 +1,90 @@
{
"name": "overlapping frame feeders fail",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "NONE"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addInsert",
"hostId": "b1",
"part": {
"id": "p2",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 2
}
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "RULE_FEEDER_SLOT"
}
}
@@ -0,0 +1,82 @@
{
"name": "frame feeder beyond frameSlots fails",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "FOUNDATION"
},
{
"slot": 2,
"type": "FOUNDATION"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "NONE"
},
{
"slot": 10,
"type": "NONE"
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addInsert",
"hostId": "b1",
"part": {
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 10
}
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "RULE_FEEDER_SLOT"
}
}
@@ -0,0 +1,86 @@
{
"name": "setFrame on a covered slot fails",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "setFrame",
"boxId": "b1",
"slot": 2,
"type": "BROOD"
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "RULE_FEEDER_SLOT"
}
}
@@ -0,0 +1,150 @@
{
"name": "removing the frame feeder frees its slots",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "removePart",
"id": "p1"
},
{
"op": "setFrame",
"boxId": "b1",
"slot": 1,
"type": "BROOD"
}
],
"expect": {
"ok": true,
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "BROOD"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
}
}
}
@@ -0,0 +1,164 @@
{
"name": "two frame feeders without overlap are ok",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "NONE"
},
{
"slot": 6,
"type": "NONE"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addInsert",
"hostId": "b1",
"part": {
"id": "p2",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 5
}
}
],
"expect": {
"ok": true,
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "NONE"
},
{
"slot": 2,
"type": "NONE"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "NONE"
},
{
"slot": 6,
"type": "NONE"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
],
"inserts": [
{
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
},
{
"id": "p2",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 5
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
}
}
}
@@ -0,0 +1,82 @@
{
"name": "frame feeder cannot sit in the stack",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "FOUNDATION"
},
{
"slot": 2,
"type": "FOUNDATION"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addPart",
"at": 2,
"part": {
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "RULE_INSERT_HOST"
}
}
@@ -0,0 +1,82 @@
{
"name": "frame feeder cannot go into a floor",
"state": {
"schemaVersion": 1,
"stack": [
{
"id": "f1",
"partTypeId": "hive-part-zl10-floor",
"state": {}
},
{
"id": "b1",
"partTypeId": "hive-part-zl10-box-10",
"state": {},
"frames": [
{
"slot": 1,
"type": "FOUNDATION"
},
{
"slot": 2,
"type": "FOUNDATION"
},
{
"slot": 3,
"type": "FOUNDATION"
},
{
"slot": 4,
"type": "FOUNDATION"
},
{
"slot": 5,
"type": "FOUNDATION"
},
{
"slot": 6,
"type": "FOUNDATION"
},
{
"slot": 7,
"type": "FOUNDATION"
},
{
"slot": 8,
"type": "FOUNDATION"
},
{
"slot": 9,
"type": "FOUNDATION"
},
{
"slot": 10,
"type": "FOUNDATION"
}
]
},
{
"id": "r1",
"partTypeId": "hive-part-zl10-roof",
"state": {}
}
]
},
"ops": [
{
"op": "addInsert",
"hostId": "f1",
"part": {
"id": "p1",
"partTypeId": "hive-part-zl10-frame-feeder",
"state": {},
"slot": 1
}
}
],
"expect": {
"ok": false,
"opIndex": 0,
"reasonCode": "RULE_INSERT_HOST"
}
}
+36
View File
@@ -9,6 +9,7 @@
"widthMm": 430,
"heightMm": 120,
"frameSlots": null,
"slotWidth": null,
"stateSchema": [
{
"key": "entrance",
@@ -49,6 +50,7 @@
"widthMm": 375,
"heightMm": 5,
"frameSlots": null,
"slotWidth": null,
"stateSchema": [],
"active": true
},
@@ -60,6 +62,7 @@
"widthMm": 430,
"heightMm": 230,
"frameSlots": 10,
"slotWidth": null,
"stateSchema": [],
"active": true
},
@@ -71,6 +74,7 @@
"widthMm": 430,
"heightMm": 230,
"frameSlots": 0,
"slotWidth": null,
"stateSchema": [],
"active": true
},
@@ -82,6 +86,7 @@
"widthMm": 430,
"heightMm": 50,
"frameSlots": null,
"slotWidth": null,
"stateSchema": [
{
"key": "bungHoles",
@@ -109,6 +114,7 @@
"widthMm": 376,
"heightMm": 2,
"frameSlots": null,
"slotWidth": null,
"stateSchema": [],
"active": true
},
@@ -120,6 +126,7 @@
"widthMm": 420,
"heightMm": 70,
"frameSlots": null,
"slotWidth": null,
"stateSchema": [
{
"key": "syrupLiters",
@@ -150,6 +157,7 @@
"widthMm": 378,
"heightMm": 1,
"frameSlots": null,
"slotWidth": null,
"stateSchema": [],
"active": true
},
@@ -161,6 +169,7 @@
"widthMm": 378,
"heightMm": 1,
"frameSlots": null,
"slotWidth": null,
"stateSchema": [],
"active": true
},
@@ -172,6 +181,7 @@
"widthMm": 378,
"heightMm": 3,
"frameSlots": null,
"slotWidth": null,
"stateSchema": [],
"active": true
},
@@ -183,6 +193,7 @@
"widthMm": 430,
"heightMm": 35,
"frameSlots": null,
"slotWidth": null,
"stateSchema": [
{
"key": "feedHole",
@@ -208,6 +219,7 @@
"widthMm": 365,
"heightMm": 20,
"frameSlots": null,
"slotWidth": null,
"stateSchema": [],
"active": true
},
@@ -219,6 +231,7 @@
"widthMm": 430,
"heightMm": 60,
"frameSlots": null,
"slotWidth": null,
"stateSchema": [],
"active": true
},
@@ -230,6 +243,7 @@
"widthMm": 140,
"heightMm": 120,
"frameSlots": null,
"slotWidth": null,
"stateSchema": [
{
"key": "acidMl",
@@ -241,6 +255,28 @@
}
],
"active": true
},
{
"id": "hive-part-zl10-frame-feeder",
"kind": "FRAME_FEEDER",
"name": "Holz-Futtertasche Zander, doppelte Wabenbreite",
"lengthMm": 477,
"widthMm": 70,
"heightMm": 220,
"frameSlots": null,
"slotWidth": 2,
"stateSchema": [
{
"key": "syrupLiters",
"label": "Flüssigfutter",
"kind": "level",
"unit": "l",
"max": 2,
"step": 0.25,
"feedType": "SUGAR_WATER"
}
],
"active": true
}
]
}
@@ -20,6 +20,7 @@ const PART_SLUGS = [
'insulation',
'roof',
'evaporator',
'frame-feeder',
] as const;
interface StateField {
@@ -66,7 +67,7 @@ describe('Hive catalog seed (e2e)', () => {
});
});
it('seeds 14 active part types for the system under fixed ids', async () => {
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' },
@@ -103,6 +104,31 @@ describe('Hive catalog seed (e2e)', () => {
]);
});
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(
@@ -20,6 +20,7 @@ interface FixturePartType {
widthMm: number;
heightMm: number;
frameSlots: number | null;
slotWidth: number | null;
stateSchema: unknown[];
active: boolean;
}
@@ -156,6 +157,7 @@ describe('Hive catalog (e2e)', () => {
widthMm: row?.widthMm,
heightMm: row?.heightMm,
frameSlots: row?.frameSlots,
slotWidth: row?.slotWidth,
stateSchema: row?.stateSchema,
active: row?.active,
}).toEqual(expected);