315 lines
13 KiB
JavaScript
315 lines
13 KiB
JavaScript
/**
|
|
* 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');
|
|
});
|
|
|
|
};
|
|
})();
|