Changeset 0.35.0 (#209)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit is contained in:
180
packages/icd-test-runner/js/crud/workflow-product.js
Normal file
180
packages/icd-test-runner/js/crud/workflow-product.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* ICD Test Runner — CRUD: Workflow Product (v0.35.0)
|
||||
* Tests for conditional routing, progressive forms, conditional fields,
|
||||
* review comments, monitoring dashboard, and SLA computation.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.ICD;
|
||||
if (!T) return;
|
||||
if (!T.crud) T.crud = {};
|
||||
|
||||
T.crud.workflowProduct = async function (testTag) {
|
||||
|
||||
if (T.user.role !== 'admin') return;
|
||||
|
||||
var wfId = null;
|
||||
var wfSlug = testTag.toLowerCase().replace(/[^a-z0-9-]/g, '-') + '-wp';
|
||||
var channelId = null;
|
||||
|
||||
// ── Setup: Create workflow with conditions ──
|
||||
|
||||
await T.test('crud', 'workflow-product', 'Create workflow for routing tests', async function () {
|
||||
var d = await T.apiPost('/workflows', {
|
||||
name: testTag + '-routing-wf',
|
||||
slug: wfSlug,
|
||||
description: 'Workflow product routing test',
|
||||
entry_mode: 'team_only',
|
||||
});
|
||||
T.assert(d.id, 'workflow created');
|
||||
wfId = d.id;
|
||||
T.registerCleanup(function () { if (wfId) return T.safeDelete('/workflows/' + wfId); });
|
||||
});
|
||||
|
||||
if (!wfId) return;
|
||||
|
||||
// Create 3 stages: Intake → Review (condition: category=billing) → Fallback
|
||||
await T.test('crud', 'workflow-product', 'Create stage 0: Intake', async function () {
|
||||
var d = await T.apiPost('/workflows/' + wfId + '/stages', {
|
||||
name: 'Intake',
|
||||
ordinal: 0,
|
||||
stage_mode: 'form_only',
|
||||
form_template: { fields: [
|
||||
{ key: 'category', type: 'select', label: 'Category', required: true,
|
||||
options: [{ value: 'billing', label: 'Billing' }, { value: 'general', label: 'General' }] },
|
||||
{ key: 'notes', type: 'textarea', label: 'Notes', condition: { when: 'category', op: 'eq', value: 'general' } }
|
||||
] },
|
||||
history_mode: 'full',
|
||||
auto_transition: true,
|
||||
transition_rules: {
|
||||
conditions: [
|
||||
{ field: 'category', op: 'eq', value: 'billing', target_stage: 'Billing Review' }
|
||||
]
|
||||
},
|
||||
sla_seconds: 3600,
|
||||
});
|
||||
T.assert(d.id || d.name, 'stage created');
|
||||
});
|
||||
|
||||
await T.test('crud', 'workflow-product', 'Create stage 1: Billing Review', async function () {
|
||||
var d = await T.apiPost('/workflows/' + wfId + '/stages', {
|
||||
name: 'Billing Review',
|
||||
ordinal: 1,
|
||||
stage_mode: 'review',
|
||||
history_mode: 'full',
|
||||
sla_seconds: 7200,
|
||||
});
|
||||
T.assert(d.id || d.name, 'stage created');
|
||||
});
|
||||
|
||||
await T.test('crud', 'workflow-product', 'Create stage 2: General Fallback', async function () {
|
||||
var d = await T.apiPost('/workflows/' + wfId + '/stages', {
|
||||
name: 'General Fallback',
|
||||
ordinal: 2,
|
||||
stage_mode: 'chat_only',
|
||||
history_mode: 'full',
|
||||
});
|
||||
T.assert(d.id || d.name, 'stage created');
|
||||
});
|
||||
|
||||
// Activate and publish
|
||||
await T.test('crud', 'workflow-product', 'Activate + publish', async function () {
|
||||
await T.apiPatch('/workflows/' + wfId, { is_active: true });
|
||||
var d = await T.apiPost('/workflows/' + wfId + '/publish', {});
|
||||
T.assert(d.version_number >= 1, 'published');
|
||||
});
|
||||
|
||||
// ── Conditional routing test ──
|
||||
|
||||
await T.test('crud', 'workflow-product', 'Start instance + advance with condition match → routes to Billing Review', async function () {
|
||||
var start = await T.apiPost('/workflows/' + wfId + '/start', {});
|
||||
channelId = start.channel_id;
|
||||
T.assert(channelId, 'instance started');
|
||||
T.assert(start.current_stage === 0, 'starts at stage 0');
|
||||
|
||||
// Advance with category=billing → should route to stage 1 (Billing Review)
|
||||
var adv = await T.apiPost('/channels/' + channelId + '/workflow/advance', {
|
||||
data: { category: 'billing' }
|
||||
});
|
||||
T.assert(adv.current_stage === 1, 'routed to stage 1 (Billing Review), got: ' + adv.current_stage);
|
||||
T.assert(adv.stage && adv.stage.name === 'Billing Review', 'stage name is Billing Review');
|
||||
});
|
||||
|
||||
// ── Conditional field validation ──
|
||||
|
||||
await T.test('crud', 'workflow-product', 'Conditional field: hidden field skipped in validation', async function () {
|
||||
// The "notes" field has condition {when: "category", eq: "general"}
|
||||
// When category=billing, "notes" should be skipped even though it exists
|
||||
// This test verifies the server-side validation logic
|
||||
var start = await T.apiPost('/workflows/' + wfId + '/start', {});
|
||||
var ch = start.channel_id;
|
||||
T.assert(ch, 'second instance started');
|
||||
|
||||
// Submit form with category=billing and no notes → should succeed
|
||||
// (notes field condition not met, so it's skipped)
|
||||
var adv = await T.apiPost('/channels/' + ch + '/workflow/advance', {
|
||||
data: { category: 'billing' }
|
||||
});
|
||||
T.assert(adv.status === 'active' || adv.status === 'completed', 'advance succeeded without notes field');
|
||||
});
|
||||
|
||||
// ── Review comments ──
|
||||
|
||||
await T.test('crud', 'workflow-product', 'POST /workflow-assignments/:id/comment (add review comment)', async function () {
|
||||
// Create a new instance and advance to the review stage
|
||||
var start = await T.apiPost('/workflows/' + wfId + '/start', {});
|
||||
var ch = start.channel_id;
|
||||
|
||||
// Advance to Billing Review (stage 1)
|
||||
await T.apiPost('/channels/' + ch + '/workflow/advance', {
|
||||
data: { category: 'billing' }
|
||||
});
|
||||
|
||||
// Check status to verify we're at review stage
|
||||
var status = await T.apiGet('/channels/' + ch + '/workflow/status');
|
||||
T.assert(status.current_stage === 1, 'at review stage');
|
||||
});
|
||||
|
||||
// ── Monitoring dashboard ──
|
||||
|
||||
await T.test('crud', 'workflow-product', 'GET /admin/workflows/monitor/instances', async function () {
|
||||
var d = await T.apiGet('/admin/workflows/monitor/instances');
|
||||
T.assert(Array.isArray(d.data), 'returns array');
|
||||
// Should have at least the instances we created above
|
||||
});
|
||||
|
||||
await T.test('crud', 'workflow-product', 'GET /admin/workflows/monitor/funnel/:id', async function () {
|
||||
var d = await T.apiGet('/admin/workflows/monitor/funnel/' + wfId);
|
||||
T.assert(Array.isArray(d.data), 'returns array');
|
||||
T.assert(d.data.length === 3, 'has 3 stages in funnel, got: ' + d.data.length);
|
||||
});
|
||||
|
||||
await T.test('crud', 'workflow-product', 'GET /admin/workflows/monitor/stale', async function () {
|
||||
var d = await T.apiGet('/admin/workflows/monitor/stale?threshold_hours=0');
|
||||
T.assert(Array.isArray(d.data), 'returns array');
|
||||
// With threshold=0, all active instances should be "stale"
|
||||
});
|
||||
|
||||
// ── SLA fields ──
|
||||
|
||||
await T.test('crud', 'workflow-product', 'Monitor instances include SLA fields', async function () {
|
||||
var d = await T.apiGet('/admin/workflows/monitor/instances');
|
||||
var myInstances = d.data.filter(function (i) { return i.workflow_id === wfId; });
|
||||
if (myInstances.length > 0) {
|
||||
var inst = myInstances[0];
|
||||
T.assert('sla_seconds' in inst, 'has sla_seconds field');
|
||||
T.assert('sla_breached' in inst, 'has sla_breached field');
|
||||
T.assert('stage_age_seconds' in inst, 'has stage_age_seconds field');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Stage entered_at tracking ──
|
||||
|
||||
await T.test('crud', 'workflow-product', 'Workflow status includes stage_entered_at', async function () {
|
||||
if (!channelId) return;
|
||||
var status = await T.apiGet('/channels/' + channelId + '/workflow/status');
|
||||
T.assert('stage_entered_at' in status, 'stage_entered_at present in status');
|
||||
});
|
||||
|
||||
};
|
||||
})();
|
||||
@@ -82,6 +82,7 @@
|
||||
'crud/editor-package.js',
|
||||
'crud/dashboard-package.js',
|
||||
'crud/observability.js',
|
||||
'crud/workflow-product.js',
|
||||
// Orchestrator (must come after all crud/*.js)
|
||||
'tier-crud.js',
|
||||
'tier-authz.js',
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
if (C.personas) await C.personas(testTag);
|
||||
if (C.extensions) await C.extensions(testTag);
|
||||
if (C.surfaces) await C.surfaces(testTag);
|
||||
if (C.editorPackage) await C.editorPackage(testTag);
|
||||
if (C.dashboardPackage) await C.dashboardPackage(testTag);
|
||||
if (C.editorPackage) await C.editorPackage(testTag);
|
||||
if (C.dashboardPackage) await C.dashboardPackage(testTag);
|
||||
if (C.workflowProduct) await C.workflowProduct(testTag);
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user