mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 04:43:35 +00:00
fix(automation): review fixes across the automations rework
Reviewing the branch turned up defects, several of them worse than the problems the original commits set out to solve. Destructive: deleting an automation removed its global triggers and only then discovered a rundown event also referenced it. The delete failed, the user cancelled, and their triggers were gone for good. The server reports trigger references before event ones, so this was reachable whenever both existed. Deleted triggers are now recreated when the delete is refused. Silently wrong: a template read the project from disk while writes are debounced by three seconds, so "make some automations, save them as a template" could produce a template without them. Verified by hand: an automation created milliseconds earlier now appears. The demo shipped half a pair. The danger warning set a stage message visible and nothing ever cleared it, so from the first event that hit its danger window the message covered the countdown for the rest of the session. The recipe library ships the clearing counterpart; the demo now does too, and the whole cycle is verified against a running server. Flood control undid itself. resetAutomationLogState ran on every onLoad, and roll mode loads at every event boundary, so the "logging suppressed" notice was re-emitted once per cue: exactly the flooding it exists to prevent. It also wiped the throttle for onLoad and onStop immediately before writing to it. Reset now happens on stop only, outside the early returns that made the first attempt at this a no-op. Trigger reconciliation diffed a mount-time selection against a live prop. Settings are polled, so a trigger created in another tab while the form was open would be deleted by a save that never saw it. It now diffs against the snapshot, and says which triggers a save will remove rather than removing several same-lifecycle triggers silently. Also: a rundowns template no longer carries a phantom empty rundown from makeNewProject; the partial-duplicate endpoint returns the name it actually used, since collisions get renamed; the template flow invalidates the project list rather than relying on a refetch on mount; the last-fired label drops to a one minute cadence instead of holding a 1Hz timer per automation forever; e2e cleanup moved to afterEach so a mid-test failure stops leaking state. The e2e spec has now been run against a real server, green on both the demo project and a blank one, leaving no residue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpbLJVVT26tzWkduck1M9H
This commit is contained in:
@@ -12,11 +12,34 @@ const templateName = 'e2e-automations-template';
|
||||
* the template carries the automation and its trigger while leaving the rundown behind.
|
||||
*/
|
||||
test.describe('automations', () => {
|
||||
/**
|
||||
* Everything created is torn down in afterEach rather than at the end of the test body:
|
||||
* a mid-test failure would otherwise leak an automation into the project, and CI retries twice.
|
||||
*
|
||||
* The template is tracked by the name the server actually used, since it resolves collisions.
|
||||
*/
|
||||
let createdTemplate: string | null = null;
|
||||
let createdTriggers: string[] = [];
|
||||
let createdAutomations: string[] = [];
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
try {
|
||||
await request.delete(`${dbURL}/${templateName}.json`);
|
||||
// triggers first, the server refuses to delete an automation that is still referenced
|
||||
for (const id of createdTriggers) {
|
||||
await request.delete(`${automationsURL}/trigger/${id}`);
|
||||
}
|
||||
for (const id of createdAutomations) {
|
||||
await request.delete(`${automationsURL}/automation/${id}`);
|
||||
}
|
||||
if (createdTemplate !== null) {
|
||||
await request.delete(`${dbURL}/${createdTemplate}`);
|
||||
}
|
||||
} catch {
|
||||
/** nothing to do here */
|
||||
// cleanup is best effort, it must not turn a passing test red
|
||||
} finally {
|
||||
createdTriggers = [];
|
||||
createdAutomations = [];
|
||||
createdTemplate = null;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -32,13 +55,14 @@ test.describe('automations', () => {
|
||||
});
|
||||
expect(createAutomation.status()).toBe(201);
|
||||
const automation = await createAutomation.json();
|
||||
createdAutomations.push(automation.id);
|
||||
|
||||
// 2. bind it to a lifecycle
|
||||
const createTrigger = await request.post(`${automationsURL}/trigger`, {
|
||||
data: { title: 'e2e trigger', trigger: 'onStart', automationId: automation.id },
|
||||
});
|
||||
expect(createTrigger.status()).toBe(201);
|
||||
const trigger = await createTrigger.json();
|
||||
createdTriggers.push((await createTrigger.json()).id);
|
||||
|
||||
// 3. save the automations as a template, without touching the loaded project
|
||||
const projectList = await (await request.get(`${dbURL}/all`)).json();
|
||||
@@ -48,22 +72,20 @@ test.describe('automations', () => {
|
||||
data: { newFilename: templateName, sections: ['automation'] },
|
||||
});
|
||||
expect(makeTemplate.status()).toBe(201);
|
||||
createdTemplate = (await makeTemplate.json()).filename;
|
||||
expect(createdTemplate).toBeTruthy();
|
||||
|
||||
// the running project is untouched
|
||||
const afterTemplate = await (await request.get(`${dbURL}/all`)).json();
|
||||
expect(afterTemplate.lastLoadedProject).toBe(currentProject);
|
||||
|
||||
// 4. the template carries the automation and its trigger, and nothing else
|
||||
const template = await (await request.post(`${dbURL}/download`, { data: { filename: templateName } })).json();
|
||||
const template = await (await request.post(`${dbURL}/download`, { data: { filename: createdTemplate } })).json();
|
||||
expect(Object.values(template.automation.automations)).toContainEqual(
|
||||
expect.objectContaining({ title: 'e2e automation' }),
|
||||
);
|
||||
expect(template.automation.triggers).toContainEqual(expect.objectContaining({ title: 'e2e trigger' }));
|
||||
expect(template.urlPresets).toEqual([]);
|
||||
|
||||
// 5. clean up: the trigger has to go first, the server refuses to delete a referenced automation
|
||||
expect((await request.delete(`${automationsURL}/trigger/${trigger.id}`)).status()).toBe(204);
|
||||
expect((await request.delete(`${automationsURL}/automation/${automation.id}`)).status()).toBe(204);
|
||||
});
|
||||
|
||||
test('refuses to delete an automation that a trigger still points at', async ({ request }) => {
|
||||
@@ -77,18 +99,21 @@ test.describe('automations', () => {
|
||||
},
|
||||
})
|
||||
).json();
|
||||
createdAutomations.push(automation.id);
|
||||
|
||||
const trigger = await (
|
||||
await request.post(`${automationsURL}/trigger`, {
|
||||
data: { title: 'e2e blocking trigger', trigger: 'onFinish', automationId: automation.id },
|
||||
})
|
||||
).json();
|
||||
createdTriggers.push(trigger.id);
|
||||
|
||||
const refused = await request.delete(`${automationsURL}/automation/${automation.id}`);
|
||||
expect(refused.status()).toBe(400);
|
||||
expect((await refused.json()).message).toContain('e2e blocking trigger');
|
||||
|
||||
await request.delete(`${automationsURL}/trigger/${trigger.id}`);
|
||||
// and it goes through once the reference is removed
|
||||
expect((await request.delete(`${automationsURL}/trigger/${trigger.id}`)).status()).toBe(204);
|
||||
expect((await request.delete(`${automationsURL}/automation/${automation.id}`)).status()).toBe(204);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user