fix: reject ambiguous queue job identities
This commit is contained in:
+58
-23
@@ -723,12 +723,10 @@ async function applyAutomationEvaluation(evaluation) {
|
|||||||
if (await isAutomationPaused()) {
|
if (await isAutomationPaused()) {
|
||||||
return freezeAutomationValue({ ok: false, error: 'Automatik ist pausiert', warning: null, admittedFiles: [], deferredFiles, paused: true, dryRun: false });
|
return freezeAutomationValue({ ok: false, error: 'Automatik ist pausiert', warning: null, admittedFiles: [], deferredFiles, paused: true, dryRun: false });
|
||||||
}
|
}
|
||||||
const cleanupPreparation = prepareSourceCleanup(newJobs);
|
const addRequest = createAddJobsRequest(newJobs);
|
||||||
|
const cleanupPreparation = addRequest.cleanupPreparation;
|
||||||
try {
|
try {
|
||||||
const result = await window.api.addJobsToBatch({
|
const result = await submitAddJobsRequest(addRequest);
|
||||||
jobs: newJobs.map(serializeUploadJob),
|
|
||||||
sourceCleanupGroups: cleanupPreparation.groups
|
|
||||||
});
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||||
const error = sanitizeUploadControlError(result.error, 'Jobs konnten nicht hinzugefügt werden.');
|
const error = sanitizeUploadControlError(result.error, 'Jobs konnten nicht hinzugefügt werden.');
|
||||||
@@ -1529,12 +1527,10 @@ async function applyHosterSelection() {
|
|||||||
if (uploading && pendingPaths.size > 0 && !(await isAutomationPaused())) {
|
if (uploading && pendingPaths.size > 0 && !(await isAutomationPaused())) {
|
||||||
const newJobs = queueJobs.filter(j => j.status === 'preview' && pendingPaths.has(j.file));
|
const newJobs = queueJobs.filter(j => j.status === 'preview' && pendingPaths.has(j.file));
|
||||||
if (newJobs.length > 0) {
|
if (newJobs.length > 0) {
|
||||||
const cleanupPreparation = prepareSourceCleanup(newJobs);
|
const addRequest = createAddJobsRequest(newJobs);
|
||||||
|
const cleanupPreparation = addRequest.cleanupPreparation;
|
||||||
try {
|
try {
|
||||||
const result = await window.api.addJobsToBatch({
|
const result = await submitAddJobsRequest(addRequest);
|
||||||
jobs: newJobs.map(serializeUploadJob),
|
|
||||||
sourceCleanupGroups: cleanupPreparation.groups
|
|
||||||
});
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
regularInjectionFailure = sanitizeUploadControlError(result.error, 'Jobs konnten nicht hinzugefügt werden.');
|
regularInjectionFailure = sanitizeUploadControlError(result.error, 'Jobs konnten nicht hinzugefügt werden.');
|
||||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||||
@@ -3789,13 +3785,54 @@ function sanitizeUploadControlError(error, fallback) {
|
|||||||
return /pausiert/i.test(message) ? 'Automatik ist pausiert' : fallback;
|
return /pausiert/i.test(message) ? 'Automatik ist pausiert' : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAddJobsOutcome(jobs, result) {
|
function analyzeAddJobsInput(jobs) {
|
||||||
const inputJobs = Array.isArray(jobs) ? jobs : [];
|
const inputJobs = Array.isArray(jobs) ? jobs : [];
|
||||||
const jobsById = new Map(inputJobs.filter(job => job?.id).map(job => [job.id, job]));
|
const idCounts = new Map();
|
||||||
|
for (const job of inputJobs) {
|
||||||
|
const id = job?.id;
|
||||||
|
if (typeof id !== 'string' || id.trim().length === 0) continue;
|
||||||
|
idCounts.set(id, (idCounts.get(id) || 0) + 1);
|
||||||
|
}
|
||||||
|
const sendableJobs = [];
|
||||||
|
const unconfirmableJobs = [];
|
||||||
|
const jobsById = new Map();
|
||||||
|
for (const job of inputJobs) {
|
||||||
|
if (typeof job?.id === 'string' && job.id.trim().length > 0 && idCounts.get(job.id) === 1) {
|
||||||
|
sendableJobs.push(job);
|
||||||
|
jobsById.set(job.id, job);
|
||||||
|
} else {
|
||||||
|
unconfirmableJobs.push(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { inputJobs, sendableJobs, unconfirmableJobs, jobsById };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAddJobsRequest(jobs) {
|
||||||
|
const analysis = analyzeAddJobsInput(jobs);
|
||||||
|
const cleanupPreparation = analysis.sendableJobs.length > 0
|
||||||
|
? prepareSourceCleanup(analysis.sendableJobs)
|
||||||
|
: { groups: [], rollbackStates: [] };
|
||||||
|
return {
|
||||||
|
...analysis,
|
||||||
|
cleanupPreparation,
|
||||||
|
payload: {
|
||||||
|
jobs: analysis.sendableJobs.map(serializeUploadJob),
|
||||||
|
sourceCleanupGroups: cleanupPreparation.groups
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitAddJobsRequest(request) {
|
||||||
|
if (request.sendableJobs.length === 0) return { added: 0 };
|
||||||
|
return window.api.addJobsToBatch(request.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAddJobsOutcome(jobs, result) {
|
||||||
|
const { sendableJobs, unconfirmableJobs, jobsById } = analyzeAddJobsInput(jobs);
|
||||||
const skippedEntries = [];
|
const skippedEntries = [];
|
||||||
const skippedIds = new Set();
|
const skippedIds = new Set();
|
||||||
const alreadyIds = new Set();
|
const alreadyIds = new Set();
|
||||||
let valid = jobsById.size === inputJobs.length;
|
let valid = true;
|
||||||
for (const entry of Array.isArray(result?.skippedJobs) ? result.skippedJobs : []) {
|
for (const entry of Array.isArray(result?.skippedJobs) ? result.skippedJobs : []) {
|
||||||
const id = entry?.jobId;
|
const id = entry?.jobId;
|
||||||
if (!jobsById.has(id) || skippedIds.has(id)) {
|
if (!jobsById.has(id) || skippedIds.has(id)) {
|
||||||
@@ -3812,17 +3849,17 @@ function resolveAddJobsOutcome(jobs, result) {
|
|||||||
}
|
}
|
||||||
alreadyIds.add(id);
|
alreadyIds.add(id);
|
||||||
}
|
}
|
||||||
const remainingJobs = inputJobs.filter(job => !skippedIds.has(job.id) && !alreadyIds.has(job.id));
|
const remainingJobs = sendableJobs.filter(job => !skippedIds.has(job.id) && !alreadyIds.has(job.id));
|
||||||
const added = Number(result?.added);
|
const added = Number(result?.added);
|
||||||
const consistent = valid && Number.isInteger(added) && added >= 0 && added === remainingJobs.length;
|
const confirmationConsistent = valid && Number.isInteger(added) && added >= 0 && added === remainingJobs.length;
|
||||||
return {
|
return {
|
||||||
consistent,
|
consistent: unconfirmableJobs.length === 0 && confirmationConsistent,
|
||||||
added: Number.isInteger(added) && added >= 0 ? added : 0,
|
added: Number.isInteger(added) && added >= 0 ? added : 0,
|
||||||
addedJobs: consistent ? remainingJobs : [],
|
addedJobs: confirmationConsistent ? remainingJobs : [],
|
||||||
alreadyJobs: [...alreadyIds].map(id => jobsById.get(id)),
|
alreadyJobs: [...alreadyIds].map(id => jobsById.get(id)),
|
||||||
skippedEntries,
|
skippedEntries,
|
||||||
skippedJobs: skippedEntries.map(entry => jobsById.get(entry.jobId)),
|
skippedJobs: skippedEntries.map(entry => jobsById.get(entry.jobId)),
|
||||||
unconfirmedJobs: consistent ? [] : remainingJobs
|
unconfirmedJobs: [...unconfirmableJobs, ...(confirmationConsistent ? [] : remainingJobs)]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3963,7 +4000,8 @@ async function startSelectedUpload(explicitJobs) {
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
const originalStates = captureUploadJobStates(addable);
|
const originalStates = captureUploadJobStates(addable);
|
||||||
const cleanupPreparation = prepareSourceCleanup(addable);
|
const addRequest = createAddJobsRequest(addable);
|
||||||
|
const cleanupPreparation = addRequest.cleanupPreparation;
|
||||||
addable.forEach(j => {
|
addable.forEach(j => {
|
||||||
j.status = 'queued'; j.error = null; j.result = null;
|
j.status = 'queued'; j.error = null; j.result = null;
|
||||||
j.bytesUploaded = 0; j.speedKbs = 0; j.progress = 0; j.uploadId = null;
|
j.bytesUploaded = 0; j.speedKbs = 0; j.progress = 0; j.uploadId = null;
|
||||||
@@ -3971,10 +4009,7 @@ async function startSelectedUpload(explicitJobs) {
|
|||||||
renderQueueTable();
|
renderQueueTable();
|
||||||
let result = null;
|
let result = null;
|
||||||
try {
|
try {
|
||||||
result = await window.api.addJobsToBatch({
|
result = await submitAddJobsRequest(addRequest);
|
||||||
jobs: addable.map(serializeUploadJob),
|
|
||||||
sourceCleanupGroups: cleanupPreparation.groups
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||||
restoreUploadJobStates(originalStates);
|
restoreUploadJobStates(originalStates);
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
},
|
},
|
||||||
addJobsToBatch(payload) {
|
addJobsToBatch(payload) {
|
||||||
folderMonitorProbeCalls.push(['inject', payload?.jobs?.length || 0]);
|
folderMonitorProbeCalls.push(['inject', payload?.jobs?.length || 0]);
|
||||||
automationProbe.mutationCalls.push(['inject', payload?.jobs?.length || 0]);
|
automationProbe.mutationCalls.push(['inject', payload?.jobs?.length || 0, (payload?.jobs || []).map(job => job.id)]);
|
||||||
if (automationProbe.addError) return Promise.reject(new Error(automationProbe.addError));
|
if (automationProbe.addError) return Promise.reject(new Error(automationProbe.addError));
|
||||||
if (automationProbe.addMode === 'partial-consistent' && payload?.jobs?.length >= 4) {
|
if (automationProbe.addMode === 'partial-consistent' && payload?.jobs?.length >= 4) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
@@ -1179,6 +1179,137 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolverMissingJob = makePauseRaceJob('resolver-missing.mkv');
|
||||||
|
delete resolverMissingJob.id;
|
||||||
|
const collisionJobs = [
|
||||||
|
{ ...makePauseRaceJob('resolver-duplicate-a.mkv'), id: 'resolver-duplicate' },
|
||||||
|
{ ...makePauseRaceJob('resolver-duplicate-b.mkv'), id: 'resolver-duplicate' },
|
||||||
|
resolverMissingJob,
|
||||||
|
{ ...makePauseRaceJob('resolver-unique.mkv'), id: 'resolver-unique' }
|
||||||
|
];
|
||||||
|
const cleanCollisionOutcome = resolveAddJobsOutcome(collisionJobs, { added: 1 });
|
||||||
|
const hostileCollisionOutcome = resolveAddJobsOutcome(collisionJobs, {
|
||||||
|
added: 1,
|
||||||
|
alreadyInBatchJobIds: ['resolver-duplicate'],
|
||||||
|
skippedJobs: [{ reason: 'Ungültig' }]
|
||||||
|
});
|
||||||
|
const summarizeCollisionOutcome = outcome => ({
|
||||||
|
consistent: outcome.consistent,
|
||||||
|
added: outcome.addedJobs.map(job => job.fileName),
|
||||||
|
already: outcome.alreadyJobs.map(job => job.fileName),
|
||||||
|
skipped: outcome.skippedJobs.map(job => job.fileName),
|
||||||
|
unconfirmed: outcome.unconfirmedJobs.map(job => job.fileName)
|
||||||
|
});
|
||||||
|
const collisionResolver = {
|
||||||
|
clean: summarizeCollisionOutcome(cleanCollisionOutcome),
|
||||||
|
hostile: summarizeCollisionOutcome(hostileCollisionOutcome)
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyCollisionCleanupFixture = (job, index) => {
|
||||||
|
job.sourceCleanupToken = 'collision-token-' + index;
|
||||||
|
job.sourceCleanupRequiredHosters = ['required-' + index];
|
||||||
|
job.sourceCleanupCompletedHosters = ['completed-' + index];
|
||||||
|
job.sourceCleanupFingerprint = { index, nested: ['before-' + index] };
|
||||||
|
return job;
|
||||||
|
};
|
||||||
|
const summarizeCollisionPath = async (jobs, result, before) => {
|
||||||
|
const probe = await window.api.getAutomationProbeState();
|
||||||
|
const inject = probe.mutationCalls.find(call => call[0] === 'inject');
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
sentIds: inject?.[2] || [],
|
||||||
|
statuses: jobs.map(job => job.status),
|
||||||
|
invalidRestored: jobs.slice(0, 3).map((job, index) => JSON.stringify(job) === before[index])
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
configureAtomicState(0);
|
||||||
|
config.globalSettings.deleteSourceAfterSuccessfulUpload = true;
|
||||||
|
uploading = true;
|
||||||
|
const activeMissingJob = makePauseRaceJob('active-missing.mkv');
|
||||||
|
delete activeMissingJob.id;
|
||||||
|
const activeCollisionJobs = [
|
||||||
|
{ ...makePauseRaceJob('active-duplicate-a.mkv'), id: 'active-duplicate' },
|
||||||
|
{ ...makePauseRaceJob('active-duplicate-b.mkv'), id: 'active-duplicate' },
|
||||||
|
activeMissingJob,
|
||||||
|
{ ...makePauseRaceJob('active-unique.mkv'), id: 'active-unique' }
|
||||||
|
].map(applyCollisionCleanupFixture);
|
||||||
|
queueJobs = activeCollisionJobs;
|
||||||
|
rebuildJobIndex();
|
||||||
|
const activeCollisionBefore = activeCollisionJobs.slice(0, 3).map(job => JSON.stringify(job));
|
||||||
|
window.api.configureAutomationProbe({ paused: false, addResult: { added: 1 } });
|
||||||
|
const activeCollisionResult = await startSelectedUpload(activeCollisionJobs);
|
||||||
|
const activeCollision = await summarizeCollisionPath(activeCollisionJobs, activeCollisionResult, activeCollisionBefore);
|
||||||
|
|
||||||
|
configureAtomicState(0);
|
||||||
|
config.globalSettings.deleteSourceAfterSuccessfulUpload = true;
|
||||||
|
selectedFiles = [];
|
||||||
|
uploading = true;
|
||||||
|
const manualCollisionFile = { path: 'C:\\collision\\manual.mkv', name: 'manual.mkv', size: 1 };
|
||||||
|
_pendingFiles = [manualCollisionFile];
|
||||||
|
_pendingImportInspection = { candidateCount: 1, duplicateCount: 0, unavailableCount: 0, accepted: [manualCollisionFile] };
|
||||||
|
_pendingImportInspections = 0;
|
||||||
|
_pendingFolderMonitorAutoStart.clear();
|
||||||
|
const manualCollisionInputs = hosters.map(hoster => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'checkbox';
|
||||||
|
input.dataset.hosterModal = hoster;
|
||||||
|
input.checked = true;
|
||||||
|
return input;
|
||||||
|
});
|
||||||
|
document.getElementById('hosterModalList').replaceChildren(...manualCollisionInputs);
|
||||||
|
const originalBuildQueuePreviewForCollision = buildQueuePreview;
|
||||||
|
let manualCollisionJobs = [];
|
||||||
|
let manualCollisionBefore = [];
|
||||||
|
buildQueuePreview = () => {
|
||||||
|
originalBuildQueuePreviewForCollision();
|
||||||
|
manualCollisionJobs = queueJobs.filter(job => job.file === manualCollisionFile.path);
|
||||||
|
manualCollisionJobs[0].id = 'manual-duplicate';
|
||||||
|
manualCollisionJobs[1].id = 'manual-duplicate';
|
||||||
|
delete manualCollisionJobs[2].id;
|
||||||
|
manualCollisionJobs[3].id = 'manual-unique';
|
||||||
|
manualCollisionJobs.forEach(applyCollisionCleanupFixture);
|
||||||
|
manualCollisionBefore = manualCollisionJobs.slice(0, 3).map(job => JSON.stringify(job));
|
||||||
|
rebuildJobIndex();
|
||||||
|
};
|
||||||
|
window.api.configureAutomationProbe({ paused: false, addResult: { added: 1 } });
|
||||||
|
const manualCollisionResult = await applyHosterSelection();
|
||||||
|
buildQueuePreview = originalBuildQueuePreviewForCollision;
|
||||||
|
const manualCollision = await summarizeCollisionPath(manualCollisionJobs, manualCollisionResult, manualCollisionBefore);
|
||||||
|
|
||||||
|
configureAtomicState(0);
|
||||||
|
config.globalSettings.deleteSourceAfterSuccessfulUpload = true;
|
||||||
|
config.globalSettings.folderMonitor.hosters = hosters.slice();
|
||||||
|
config.globalSettings.folderMonitor.autoStart = true;
|
||||||
|
selectedFiles = [];
|
||||||
|
uploading = true;
|
||||||
|
const automationCollisionFile = { path: 'C:\\collision\\automation.mkv', name: 'automation.mkv', size: 1, mtimeMs: 1 };
|
||||||
|
const originalCreateAutomationPreviewJobForCollision = createAutomationPreviewJob;
|
||||||
|
const automationCollisionJobs = [];
|
||||||
|
const automationCollisionBefore = [];
|
||||||
|
createAutomationPreviewJob = (file, hoster) => {
|
||||||
|
const job = originalCreateAutomationPreviewJobForCollision(file, hoster);
|
||||||
|
const index = automationCollisionJobs.length;
|
||||||
|
if (index < 2) job.id = 'automation-duplicate';
|
||||||
|
else if (index === 2) delete job.id;
|
||||||
|
else job.id = 'automation-unique';
|
||||||
|
applyCollisionCleanupFixture(job, index);
|
||||||
|
automationCollisionJobs.push(job);
|
||||||
|
if (index < 3) automationCollisionBefore.push(JSON.stringify(job));
|
||||||
|
return job;
|
||||||
|
};
|
||||||
|
window.api.configureAutomationProbe({ paused: false, addResult: { added: 1 } });
|
||||||
|
const automationCollisionEvaluation = await evaluateAutomationCandidates([automationCollisionFile], { dryRun: false, trigger: 'watcher' });
|
||||||
|
const automationCollisionResult = await applyAutomationEvaluation(automationCollisionEvaluation);
|
||||||
|
createAutomationPreviewJob = originalCreateAutomationPreviewJobForCollision;
|
||||||
|
const automationCollision = await summarizeCollisionPath(automationCollisionJobs, {
|
||||||
|
ok: automationCollisionResult.ok,
|
||||||
|
error: automationCollisionResult.error,
|
||||||
|
admitted: automationCollisionResult.admittedFiles.map(file => file.name)
|
||||||
|
}, automationCollisionBefore);
|
||||||
|
uploading = false;
|
||||||
|
const collisionAdmission = { active: activeCollision, manual: manualCollision, automation: automationCollision };
|
||||||
|
|
||||||
configureAtomicState(0);
|
configureAtomicState(0);
|
||||||
selectedFiles = [];
|
selectedFiles = [];
|
||||||
config.globalSettings.folderMonitor.autoStart = true;
|
config.globalSettings.folderMonitor.autoStart = true;
|
||||||
@@ -1371,7 +1502,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
startCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'start').length,
|
startCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'start').length,
|
||||||
injectCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'inject').length
|
injectCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'inject').length
|
||||||
};
|
};
|
||||||
return { dry, manualTest, historyEvidence, pendingDedup, manualHostTransactional, atomic, status, persistedQueueExactness, stale, replannedEligibility, mainPauseResponses, cleanupRollback, crossPathCleanupRollback, partialAddOutcomes, pauseBetweenApplyAndStart, startAcceptance, fulfilledFeedback, injectionOutcomes, paused };
|
return { dry, manualTest, historyEvidence, pendingDedup, manualHostTransactional, atomic, status, persistedQueueExactness, stale, replannedEligibility, mainPauseResponses, cleanupRollback, crossPathCleanupRollback, partialAddOutcomes, collisionResolver, collisionAdmission, pauseBetweenApplyAndStart, startAcceptance, fulfilledFeedback, injectionOutcomes, paused };
|
||||||
})()`;
|
})()`;
|
||||||
const onlineBackupBehaviorScript = `(async () => {
|
const onlineBackupBehaviorScript = `(async () => {
|
||||||
const ids = {
|
const ids = {
|
||||||
@@ -1950,6 +2081,42 @@ app.whenReady().then(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.collisionResolver, {
|
||||||
|
clean: {
|
||||||
|
consistent: false,
|
||||||
|
added: ['resolver-unique.mkv'],
|
||||||
|
already: [],
|
||||||
|
skipped: [],
|
||||||
|
unconfirmed: ['resolver-duplicate-a.mkv', 'resolver-duplicate-b.mkv', 'resolver-missing.mkv']
|
||||||
|
},
|
||||||
|
hostile: {
|
||||||
|
consistent: false,
|
||||||
|
added: [],
|
||||||
|
already: [],
|
||||||
|
skipped: [],
|
||||||
|
unconfirmed: ['resolver-duplicate-a.mkv', 'resolver-duplicate-b.mkv', 'resolver-missing.mkv', 'resolver-unique.mkv']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.collisionAdmission, {
|
||||||
|
active: {
|
||||||
|
result: { ok: false, error: 'Jobs konnten nicht eindeutig bestätigt werden.' },
|
||||||
|
sentIds: ['active-unique'],
|
||||||
|
statuses: ['preview', 'preview', 'preview', 'queued'],
|
||||||
|
invalidRestored: [true, true, true]
|
||||||
|
},
|
||||||
|
manual: {
|
||||||
|
result: { ok: false, error: 'Jobs konnten nicht eindeutig bestätigt werden.' },
|
||||||
|
sentIds: ['manual-unique'],
|
||||||
|
statuses: ['preview', 'preview', 'preview', 'queued'],
|
||||||
|
invalidRestored: [true, true, true]
|
||||||
|
},
|
||||||
|
automation: {
|
||||||
|
result: { ok: false, error: 'Jobs konnten nicht eindeutig bestätigt werden.', admitted: [] },
|
||||||
|
sentIds: ['automation-unique'],
|
||||||
|
statuses: ['preview', 'preview', 'preview', 'queued'],
|
||||||
|
invalidRestored: [true, true, true]
|
||||||
|
}
|
||||||
|
});
|
||||||
assert.deepEqual(result.automationPipeline.pauseBetweenApplyAndStart, {
|
assert.deepEqual(result.automationPipeline.pauseBetweenApplyAndStart, {
|
||||||
result: {
|
result: {
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user