feat(automation): improve definition and trigger editing

This commit is contained in:
Carlos Valente
2026-09-13 09:51:47 +02:00
committed by Carlos Valente
parent 79c21daf73
commit b07544ab84
33 changed files with 865 additions and 370 deletions
@@ -51,4 +51,34 @@ describe('automation controllers', () => {
);
});
it('persists definitions using the not_contains filter operator', async () => {
const request = {
body: {
...requestBody,
filters: [{ field: 'eventNow.title', operator: 'not_contains', value: 'break' }],
},
} as Request;
await postAutomation(request, makeResponse());
expect(automationDao.addAutomation).toHaveBeenCalledWith(
expect.objectContaining({ filters: [expect.objectContaining({ operator: 'not_contains' })] }),
);
});
it('rejects trigger bindings when editing a definition', async () => {
const request = {
body: { ...requestBody, triggers: [{ trigger: 'onStart', automationId: 'other-definition' }] },
params: { id: 'automation-id' },
} as unknown as Request;
const response = makeResponse();
await editAutomation(request, response);
expect(automationDao.editAutomation).not.toHaveBeenCalled();
expect(response.status).toHaveBeenCalledWith(400);
expect(response.send).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Automation definitions cannot include triggers' }),
);
});
});
@@ -146,7 +146,7 @@ describe('editAutomation()', () => {
title: 'test-osc',
filterRule: 'all',
filters: [],
outputs: [],
outputs: [makeOSCAction(), makeHTTPAction()],
});
await addAutomation({
title: 'test-http',
@@ -164,7 +164,7 @@ describe('editAutomation()', () => {
title: 'test-osc',
filterRule: 'all',
filters: expect.any(Array),
outputs: expect.any(Array),
outputs: [makeOSCAction(), makeHTTPAction()],
});
const editedOSC = await editAutomation(firstAutomation.id, {
@@ -179,9 +179,34 @@ describe('editAutomation()', () => {
title: 'edited-title',
filterRule: 'any',
filters: expect.any(Array),
outputs: expect.any(Array),
outputs: [],
});
});
it('replaces all outputs when an automation definition is edited', async () => {
const editedWithOneOutput = await editAutomation(firstAutomation.id, {
title: 'edited-title',
filterRule: 'any',
filters: [],
outputs: [makeHTTPAction()],
});
expect(editedWithOneOutput.outputs).toEqual([makeHTTPAction()]);
});
it('preserves global triggers when a definition is edited', async () => {
await addTrigger({ title: 'On Start', trigger: TimerLifeCycle.onStart, automationId: firstAutomation.id });
await editAutomation(firstAutomation.id, {
title: 'edited-title',
filterRule: 'all',
filters: [],
outputs: [],
});
expect(getAutomationTriggers()).toEqual([
expect.objectContaining({ automationId: firstAutomation.id, trigger: TimerLifeCycle.onStart }),
]);
});
});
describe('deleteAutomation()', () => {
@@ -225,4 +250,35 @@ describe('deleteAutomation()', () => {
const removed = getAutomations();
expect(Object.keys(removed).length).toEqual(0);
});
it('refuses an automation attached to a global trigger', async () => {
const automationId = Object.keys(getAutomations())[0];
await addTrigger({ title: 'On Start', trigger: TimerLifeCycle.onStart, automationId });
await expect(deleteAutomation({}, automationId)).rejects.toThrow(/used in trigger/);
expect(getAutomationTriggers()).toHaveLength(1);
expect(getAutomations()[automationId]).toBeDefined();
});
it('refuses an automation attached to an event', async () => {
const automationId = Object.keys(getAutomations())[0];
const projectRundowns: ProjectRundowns = {
'rundown-1': {
id: 'rundown-1',
title: 'Rundown 1',
order: ['1'],
flatOrder: ['1'],
entries: {
'1': makeOntimeEvent({
id: '1',
triggers: [{ id: 'trigger-1', title: 'Trigger 1', trigger: TimerLifeCycle.onClock, automationId }],
}),
},
revision: 1,
},
};
await expect(deleteAutomation(projectRundowns, automationId)).rejects.toThrow(/used in rundown/);
expect(getAutomations()[automationId]).toBeDefined();
});
});
@@ -1,7 +1,17 @@
import { ProjectRundowns, TimerLifeCycle } from 'ontime-types';
import { makeOntimeEvent } from '../../rundown/__mocks__/rundown.mocks.js';
import { isAutomationUsed, parseTemplateNested, stringToOSCArgs } from '../automation.utils.js';
import { isAutomationUsed, isHostname, parseTemplateNested, stringToOSCArgs } from '../automation.utils.js';
describe('isHostname()', () => {
it.each(['localhost', 'qlab', 'osc.example.com', 'osc-target.example'])('accepts %s', (hostname) => {
expect(isHostname(hostname)).toBe(true);
});
it.each(['not a host', '-osc.example', 'osc-.example', 'osc..example', 'osc.example.'])('rejects %s', (hostname) => {
expect(isHostname(hostname)).toBe(false);
});
});
describe('parseTemplateNested()', () => {
it('parses string with a single-level variable name', () => {
@@ -1,4 +1,12 @@
import { parseOutput } from '../automation.validation.js';
import { parseAutomation, parseOutput } from '../automation.validation.js';
describe('parseAutomation', () => {
it('rejects trigger bindings from definition payloads', () => {
expect(() =>
parseAutomation({ title: 'Definition', filterRule: 'all', filters: [], outputs: [], triggers: [] }),
).toThrow('Automation definitions cannot include triggers');
});
});
describe('parseOutput', () => {
describe('handles OSC outputs', () => {
@@ -0,0 +1,57 @@
import { runtimeStorePlaceholder } from 'ontime-types';
const { send } = vi.hoisted(() => ({ send: vi.fn() }));
vi.mock('node:dgram', () => ({
createSocket: vi.fn(() => ({ send })),
}));
import { emitOSC } from '../clients/osc.client.js';
describe('emitOSC()', () => {
beforeEach(() => {
send.mockClear();
});
it('resolves templates in the target host before sending', () => {
emitOSC(
{
type: 'osc',
targetIP: '{{eventNow.custom.oscTarget}}',
targetPort: 53000,
address: '/cue/start',
args: '',
},
{
...runtimeStorePlaceholder,
eventNow: {
id: 'current-event',
type: 'event',
cue: '1',
title: 'Opening',
note: '',
timeStart: 0,
timeEnd: 0,
duration: 0,
timerType: 'count-down',
colour: '',
delay: 0,
isPublic: true,
skip: false,
endAction: 'none',
revision: 0,
custom: { oscTarget: '192.0.2.10' },
},
},
);
expect(send).toHaveBeenCalledWith(
expect.anything(),
0,
expect.any(Number),
53000,
'192.0.2.10',
expect.any(Function),
);
});
});