feat: T4 wave 4 - rules suggest, takeover, chains, autopilot A1-A3, suggestions page, varroa bands UI
- Automation rules propose suggestions instead of creating tasks; notify per rule config; corrected varroa measurements refresh or resolve their suggestion - Inspection values can be taken over into the colony card (occupiedFrames 0-40, strength deprecated); warning counts use the same visibility rule as the notification list - Biotech chains: swarm check after queen cells, queen acceptance check, split schedules (raise own, queen cell, queen) - Season autopilot core with A1 inspection due, A2 varroa measurement, A3 recheck after treatment; colony sync and daily step - Frontend: suggestions page with groups, bulk accept, undo, paging, focus management and permission gating - Frontend: varroa traffic light from seasonal bands, sample size, one range request for the colony chart - Notifications: mark-read answers 404 for rows the user may no longer see Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
64a2b7dad9
commit
02eb16924e
142 files changed
+11847
-1251
No files matched your search
@@ -154,12 +154,17 @@ describe('AutomationEventHandlers', () => {
|
||||
async (method, type, model, core) => {
|
||||
const record = { id: 'record-1', colonyId: 'colony-1' };
|
||||
tx[model].findUnique.mockResolvedValueOnce(record);
|
||||
tx.colony.findUnique.mockResolvedValueOnce({ status: 'ACTIVE' });
|
||||
|
||||
await run(method, type);
|
||||
|
||||
expect(tx[model].findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 'record-1' },
|
||||
});
|
||||
expect(tx.colony.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 'colony-1' },
|
||||
select: { status: true },
|
||||
});
|
||||
expect(automation[core]).toHaveBeenCalledWith(tx, record);
|
||||
},
|
||||
);
|
||||
@@ -174,8 +179,41 @@ describe('AutomationEventHandlers', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each(cases)(
|
||||
'rule handlers write nothing for an ended colony: %s on %s',
|
||||
async (method, type, model, core) => {
|
||||
tx[model].findUnique.mockResolvedValueOnce({
|
||||
id: 'record-1',
|
||||
colonyId: 'colony-1',
|
||||
});
|
||||
tx.colony.findUnique.mockResolvedValueOnce({ status: 'DISSOLVED' });
|
||||
|
||||
await run(method, type);
|
||||
|
||||
expect(automation[core]).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(cases)(
|
||||
'rule handlers write nothing for a colony that is gone: %s on %s',
|
||||
async (method, type, model, core) => {
|
||||
tx[model].findUnique.mockResolvedValueOnce({
|
||||
id: 'record-1',
|
||||
colonyId: 'colony-1',
|
||||
});
|
||||
|
||||
await run(method, type);
|
||||
|
||||
expect(automation[core]).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('lets an evaluation failure propagate, so the delivery is retried', async () => {
|
||||
tx.inspection.findUnique.mockResolvedValueOnce({ id: 'record-1' });
|
||||
tx.inspection.findUnique.mockResolvedValueOnce({
|
||||
id: 'record-1',
|
||||
colonyId: 'colony-1',
|
||||
});
|
||||
tx.colony.findUnique.mockResolvedValueOnce({ status: 'ACTIVE' });
|
||||
automation.evaluateInspectionIn.mockRejectedValue(
|
||||
new Error('notifications down'),
|
||||
);
|
||||
@@ -192,22 +230,24 @@ describe('AutomationEventHandlers', () => {
|
||||
const event = () =>
|
||||
envelope('ColonyStatusChanged', 'colony-1', { from, to });
|
||||
|
||||
it('hands the transition of the event to evaluateColonyHealthIn', async () => {
|
||||
tx.colony.findUnique.mockResolvedValueOnce({ id: 'colony-1' });
|
||||
it('hands the transition and the event to evaluateColonyHealthIn', async () => {
|
||||
tx.colony.findUnique.mockResolvedValueOnce({ status: 'ACTIVE' });
|
||||
|
||||
await handlers.colonyHealth(event(), asTx());
|
||||
|
||||
expect(tx.colony.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 'colony-1' },
|
||||
select: { id: true },
|
||||
select: { status: true },
|
||||
});
|
||||
// The rule decides on the snapshots of the event, not on the colony
|
||||
// as it is now: a later change must not hide the transition.
|
||||
// as it is now: a later change must not hide the transition. The
|
||||
// event is the anchor of the suggestion.
|
||||
expect(automation.evaluateColonyHealthIn).toHaveBeenCalledWith(
|
||||
tx,
|
||||
'colony-1',
|
||||
from,
|
||||
to,
|
||||
{ id: 'e1', occurredAt: new Date('2026-10-13T08:00:00.000Z') },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -216,6 +256,14 @@ describe('AutomationEventHandlers', () => {
|
||||
|
||||
expect(automation.evaluateColonyHealthIn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips a colony that ended', async () => {
|
||||
tx.colony.findUnique.mockResolvedValueOnce({ status: 'SOLD' });
|
||||
|
||||
await handlers.colonyHealth(event(), asTx());
|
||||
|
||||
expect(automation.evaluateColonyHealthIn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('raw-material-low', () => {
|
||||
|
||||
Reference in new issue
Block a user