Changeset 0.28.7 (#193)
This commit is contained in:
314
packages/icd-test-runner/js/crud/tasks.js
Normal file
314
packages/icd-test-runner/js/crud/tasks.js
Normal file
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* ICD Test Runner — CRUD: Tasks
|
||||
* Personal task lifecycle, validation (missing name, invalid cron,
|
||||
* workflow type), webhook-triggered tasks, webhook secret auto-gen,
|
||||
* admin task operations.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.ICD;
|
||||
if (!T) return;
|
||||
if (!T.crud) T.crud = {};
|
||||
|
||||
T.crud.tasks = async function (testTag) {
|
||||
|
||||
// ── Personal Tasks CRUD ──
|
||||
var taskId = null;
|
||||
var taskTriggerToken = null;
|
||||
|
||||
await T.test('crud', 'tasks', 'POST /tasks (create prompt task)', async function () {
|
||||
var d = await T.apiPost('/tasks', {
|
||||
name: testTag + '-task',
|
||||
description: 'ICD integration test task',
|
||||
task_type: 'prompt',
|
||||
schedule: '@daily',
|
||||
user_prompt: 'Summarize test data',
|
||||
model_id: 'test-model',
|
||||
timezone: 'UTC'
|
||||
});
|
||||
T.assertShape(d, T.S.taskFull, 'created task');
|
||||
T.assert(d.task_type === 'prompt', 'task_type should be prompt');
|
||||
T.assert(d.scope === 'personal', 'default scope should be personal');
|
||||
T.assert(d.is_active === true, 'new task should be active');
|
||||
T.assert(d.next_run_at !== null && d.next_run_at !== undefined, 'cron task should have next_run_at');
|
||||
T.assert(d.max_tokens > 0, 'budget defaults should be applied');
|
||||
taskId = d.id;
|
||||
T.registerCleanup(function () { if (taskId) return T.safeDelete('/tasks/' + taskId); });
|
||||
});
|
||||
|
||||
if (taskId) {
|
||||
await T.test('crud', 'tasks', 'GET /tasks/:id (read)', async function () {
|
||||
var d = await T.apiGet('/tasks/' + taskId);
|
||||
T.assertShape(d, T.S.taskFull, 'task');
|
||||
T.assert(d.id === taskId, 'id mismatch');
|
||||
T.assert(d.name.indexOf(testTag) !== -1, 'name mismatch');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'PUT /tasks/:id (update)', async function () {
|
||||
var d = await T.apiPut('/tasks/' + taskId, {
|
||||
name: testTag + '-task-updated',
|
||||
user_prompt: 'Updated prompt'
|
||||
});
|
||||
T.assert(d.name === testTag + '-task-updated', 'name not updated');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'GET /tasks (list mine)', async function () {
|
||||
var d = await T.apiGet('/tasks');
|
||||
T.assertHasKey(d, 'data', '/tasks');
|
||||
T.assert(Array.isArray(d.data), 'data should be array');
|
||||
T.assert(d.data.length >= 1, 'should have at least 1 task');
|
||||
var found = d.data.some(function (t) { return t.id === taskId; });
|
||||
T.assert(found, 'created task should be in list');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'GET /tasks/:id/runs (list runs)', async function () {
|
||||
var d = await T.apiGet('/tasks/' + taskId + '/runs');
|
||||
T.assertHasKey(d, 'data', '/runs');
|
||||
T.assert(Array.isArray(d.data), 'data should be array');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'POST /tasks/:id/run (run now)', async function () {
|
||||
var d = await T.apiPost('/tasks/' + taskId + '/run', {});
|
||||
T.assertHasKey(d, 'scheduled', '/run');
|
||||
T.assert(d.scheduled === true, 'should be scheduled');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'POST /tasks/:id/kill (no active run → 404)', async function () {
|
||||
var token = await T.getAuthToken();
|
||||
var d = await T.authFetch(token, 'POST', '/tasks/' + taskId + '/kill');
|
||||
// 200 = killed (if run started), 404 = no active run — both acceptable
|
||||
T.assert(d._status === 200 || d._status === 404,
|
||||
'expected 200 or 404, got ' + d._status);
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'PUT /tasks/:id (update schedule)', async function () {
|
||||
var d = await T.apiPut('/tasks/' + taskId, { schedule: '@hourly' });
|
||||
T.assert(d.schedule === '@hourly', 'schedule not updated');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'PUT /tasks/:id (deactivate)', async function () {
|
||||
var d = await T.apiPut('/tasks/' + taskId, { is_active: false });
|
||||
T.assert(d.is_active === false, 'should be deactivated');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'DELETE /tasks/:id', async function () {
|
||||
await T.safeDelete('/tasks/' + taskId);
|
||||
taskId = null;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Task Validation Tests ──
|
||||
await T.test('crud', 'tasks', 'POST /tasks (missing name → 400)', async function () {
|
||||
var token = await T.getAuthToken();
|
||||
var d = await T.authFetch(token, 'POST', '/tasks', {
|
||||
task_type: 'prompt', schedule: '@daily',
|
||||
user_prompt: 'x', model_id: 'm'
|
||||
});
|
||||
T.assertStatus(d, 400, 'missing name');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'POST /tasks (invalid cron → 400)', async function () {
|
||||
var token = await T.getAuthToken();
|
||||
var d = await T.authFetch(token, 'POST', '/tasks', {
|
||||
name: testTag + '-bad-cron', task_type: 'prompt',
|
||||
schedule: 'not-a-cron', user_prompt: 'x', model_id: 'm'
|
||||
});
|
||||
T.assertStatus(d, 400, 'invalid cron');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'POST /tasks (workflow type → 400 not implemented)', async function () {
|
||||
var token = await T.getAuthToken();
|
||||
var d = await T.authFetch(token, 'POST', '/tasks', {
|
||||
name: testTag + '-wf', task_type: 'workflow',
|
||||
schedule: '@daily', workflow_id: '00000000-0000-0000-0000-000000000001',
|
||||
model_id: 'm'
|
||||
});
|
||||
T.assertStatus(d, 400, 'workflow type rejected');
|
||||
});
|
||||
|
||||
// ── Webhook-Triggered Task + Trigger Endpoint ──
|
||||
var webhookTaskId = null;
|
||||
var webhookTriggerToken = null;
|
||||
|
||||
await T.test('crud', 'tasks', 'POST /tasks (create webhook task)', async function () {
|
||||
var d = await T.apiPost('/tasks', {
|
||||
name: testTag + '-webhook-task',
|
||||
task_type: 'prompt',
|
||||
schedule: 'webhook',
|
||||
user_prompt: 'Process trigger data',
|
||||
model_id: 'test-model'
|
||||
});
|
||||
T.assertShape(d, T.S.taskFull, 'webhook task');
|
||||
T.assert(d.schedule === 'webhook', 'schedule should be webhook');
|
||||
T.assert(d.trigger_token && d.trigger_token.length > 0, 'webhook task should have trigger_token');
|
||||
T.assert(d.next_run_at === null || d.next_run_at === undefined, 'webhook task should have no next_run_at');
|
||||
webhookTaskId = d.id;
|
||||
webhookTriggerToken = d.trigger_token;
|
||||
T.registerCleanup(function () { if (webhookTaskId) return T.safeDelete('/tasks/' + webhookTaskId); });
|
||||
});
|
||||
|
||||
if (webhookTaskId && webhookTriggerToken) {
|
||||
await T.test('crud', 'tasks', 'POST /hooks/t/:token (fire trigger → 202)', async function () {
|
||||
var d = await T.publicPost('/hooks/t/' + webhookTriggerToken, {
|
||||
build_id: 12345, status: 'failed', repo: 'icd-test'
|
||||
});
|
||||
T.assertStatus(d, 202, 'trigger');
|
||||
T.assert(d.triggered === true, 'should be triggered');
|
||||
T.assert(d.run_id && d.run_id.length > 0, 'should return run_id');
|
||||
T.assert(d.task_id === webhookTaskId, 'task_id should match');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'POST /hooks/t/:token (duplicate → 409)', async function () {
|
||||
var d = await T.publicPost('/hooks/t/' + webhookTriggerToken, { test: true });
|
||||
T.assertStatus(d, 409, 'duplicate trigger');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'POST /hooks/t/bad-token (invalid → 404)', async function () {
|
||||
var d = await T.publicPost('/hooks/t/nonexistent-token-value', { test: true });
|
||||
T.assertStatus(d, 404, 'bad token');
|
||||
});
|
||||
|
||||
// Deactivate then trigger → 410
|
||||
await T.test('crud', 'tasks', 'PUT+POST deactivated webhook task → 410', async function () {
|
||||
await T.apiPut('/tasks/' + webhookTaskId, { is_active: false });
|
||||
var d = await T.publicPost('/hooks/t/' + webhookTriggerToken, { test: true });
|
||||
T.assertStatus(d, 410, 'inactive trigger');
|
||||
// Re-activate for cleanup
|
||||
await T.apiPut('/tasks/' + webhookTaskId, { is_active: true });
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'GET /tasks/:id/runs (webhook task has queued run)', async function () {
|
||||
var d = await T.apiGet('/tasks/' + webhookTaskId + '/runs');
|
||||
T.assertHasKey(d, 'data', '/runs');
|
||||
T.assert(Array.isArray(d.data), 'data should be array');
|
||||
T.assert(d.data.length >= 1, 'webhook task should have at least 1 run from trigger');
|
||||
T.assertShape(d.data[0], T.S.taskRun, 'run[0]');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'DELETE /tasks/:id (webhook task)', async function () {
|
||||
await T.safeDelete('/tasks/' + webhookTaskId);
|
||||
webhookTaskId = null;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Webhook Secret Auto-Generation ──
|
||||
await T.test('crud', 'tasks', 'POST /tasks (webhook_url → auto webhook_secret)', async function () {
|
||||
var d = await T.apiPost('/tasks', {
|
||||
name: testTag + '-secret-task',
|
||||
task_type: 'prompt', schedule: '@daily',
|
||||
user_prompt: 'x', model_id: 'm',
|
||||
webhook_url: 'https://example.com/hook'
|
||||
});
|
||||
T.assert(d.webhook_secret && d.webhook_secret.length > 0,
|
||||
'webhook_secret should be auto-generated when webhook_url is set');
|
||||
if (d.id) await T.safeDelete('/tasks/' + d.id);
|
||||
});
|
||||
|
||||
// ── Admin Task Operations ──
|
||||
if (T.user.role === 'admin') {
|
||||
var adminTaskId = null;
|
||||
|
||||
await T.test('crud', 'tasks', 'POST /tasks (create for admin ops)', async function () {
|
||||
var d = await T.apiPost('/tasks', {
|
||||
name: testTag + '-admin-task',
|
||||
task_type: 'prompt', schedule: '@daily',
|
||||
user_prompt: 'admin test', model_id: 'm'
|
||||
});
|
||||
adminTaskId = d.id;
|
||||
T.registerCleanup(function () { if (adminTaskId) return T.safeDelete('/tasks/' + adminTaskId); });
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'GET /admin/tasks (list all)', async function () {
|
||||
var d = await T.apiGet('/admin/tasks');
|
||||
T.assertHasKey(d, 'data', '/admin/tasks');
|
||||
T.assert(Array.isArray(d.data), 'data should be array');
|
||||
if (adminTaskId) {
|
||||
var found = d.data.some(function (t) { return t.id === adminTaskId; });
|
||||
T.assert(found, 'admin-created task should appear in admin list');
|
||||
}
|
||||
});
|
||||
|
||||
if (adminTaskId) {
|
||||
await T.test('crud', 'tasks', 'POST /admin/tasks/:id/run (admin run)', async function () {
|
||||
var d = await T.apiPost('/admin/tasks/' + adminTaskId + '/run', {});
|
||||
T.assertHasKey(d, 'scheduled', '/admin/run');
|
||||
T.assert(d.scheduled === true, 'should be scheduled');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'POST /admin/tasks/:id/kill (admin kill)', async function () {
|
||||
var token = await T.getAuthToken();
|
||||
var d = await T.authFetch(token, 'POST', '/admin/tasks/' + adminTaskId + '/kill');
|
||||
T.assert(d._status === 200 || d._status === 404,
|
||||
'expected 200 or 404, got ' + d._status);
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'DELETE /admin/tasks/:id (admin delete)', async function () {
|
||||
await T.safeDelete('/admin/tasks/' + adminTaskId);
|
||||
adminTaskId = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── System functions (v0.28.6) ──
|
||||
|
||||
await T.test('crud', 'tasks', 'GET /admin/system-functions', async function () {
|
||||
var d = await T.apiGet('/admin/system-functions');
|
||||
T.assertHasKey(d, 'data', 'system-functions');
|
||||
T.assert(Array.isArray(d.data), 'data should be array');
|
||||
T.assert(d.data.length >= 4, 'should have at least 4 built-in functions, got ' + d.data.length);
|
||||
var names = d.data.map(function(f) { return f.name; });
|
||||
T.assert(names.indexOf('session_cleanup') >= 0, 'should include session_cleanup');
|
||||
T.assert(names.indexOf('staleness_check') >= 0, 'should include staleness_check');
|
||||
T.assert(names.indexOf('retention_sweep') >= 0, 'should include retention_sweep');
|
||||
T.assert(names.indexOf('health_prune') >= 0, 'should include health_prune');
|
||||
// Verify shape
|
||||
T.assert(typeof d.data[0].name === 'string', 'name should be string');
|
||||
T.assert(typeof d.data[0].description === 'string', 'description should be string');
|
||||
});
|
||||
|
||||
var sysTaskId = null;
|
||||
await T.test('crud', 'tasks', 'POST /tasks (create system task)', async function () {
|
||||
var d = await T.apiPost('/tasks', {
|
||||
name: testTag + '-system-task',
|
||||
task_type: 'system',
|
||||
system_function: 'health_prune',
|
||||
schedule: '0 3 * * *',
|
||||
scope: 'global',
|
||||
});
|
||||
T.assert(d.id, 'should return id');
|
||||
T.assert(d.task_type === 'system', 'task_type should be system');
|
||||
T.assert(d.system_function === 'health_prune', 'system_function should be health_prune');
|
||||
sysTaskId = d.id;
|
||||
T.registerCleanup(function () { if (sysTaskId) return T.safeDelete('/tasks/' + sysTaskId); });
|
||||
});
|
||||
|
||||
if (sysTaskId) {
|
||||
await T.test('crud', 'tasks', 'DELETE /tasks/:id (cleanup system task)', async function () {
|
||||
await T.safeDelete('/tasks/' + sysTaskId);
|
||||
sysTaskId = null;
|
||||
});
|
||||
}
|
||||
|
||||
await T.test('crud', 'tasks', 'POST /tasks (system task — invalid function → 400)', async function () {
|
||||
var token = await T.getAuthToken();
|
||||
var d = await T.authFetch(token, 'POST', '/tasks', {
|
||||
name: 'bad-func',
|
||||
task_type: 'system',
|
||||
system_function: 'nonexistent_func',
|
||||
schedule: '0 3 * * *',
|
||||
});
|
||||
T.assertStatus(d, 400, 'invalid system function');
|
||||
});
|
||||
|
||||
await T.test('crud', 'tasks', 'POST /tasks (system task — missing function → 400)', async function () {
|
||||
var token = await T.getAuthToken();
|
||||
var d = await T.authFetch(token, 'POST', '/tasks', {
|
||||
name: 'no-func',
|
||||
task_type: 'system',
|
||||
schedule: '0 3 * * *',
|
||||
});
|
||||
T.assertStatus(d, 400, 'missing system_function');
|
||||
});
|
||||
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user