Changeset 0.29.3 (#198)

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit is contained in:
2026-03-18 00:15:18 +00:00
committed by xcaliber
parent 115004a3ab
commit 7f191e18cd
22 changed files with 1625 additions and 77 deletions

View File

@@ -399,5 +399,205 @@
}
})();
// ── Form Submission Lifecycle (v0.29.3) ──────────────────
// Tests typed form_template with stage_mode, form validation,
// and the /w/:id/form-submit endpoint.
if (T.user.role === 'admin') {
await (async function () {
var fwfId = null;
var fwfSlug = testTag + '-form-wf';
var fChannelId = null;
await T.test('crud', 'workflows', 'form: create form_only workflow', async function () {
var wf = await T.apiPost('/workflows', {
name: testTag + ' Form WF',
slug: fwfSlug,
entry_mode: 'public_link'
});
T.assertShape(wf, T.S.workflow, 'form workflow');
fwfId = wf.id;
T.registerCleanup(function () { if (fwfId) return T.safeDelete('/workflows/' + fwfId); });
await T.apiPost('/workflows/' + fwfId + '/stages', {
name: 'Contact Info',
ordinal: 0,
history_mode: 'full',
stage_mode: 'form_only',
form_template: {
fields: [
{ key: 'name', type: 'text', label: 'Full Name', required: true, validation: { min_length: 2 } },
{ key: 'email', type: 'email', label: 'Email', required: true },
{ key: 'dept', type: 'select', label: 'Department', options: [
{ value: 'eng', label: 'Engineering' },
{ value: 'sales', label: 'Sales' }
]}
]
}
});
await T.apiPost('/workflows/' + fwfId + '/stages', {
name: 'Done', ordinal: 1, history_mode: 'full', stage_mode: 'chat_only'
});
await T.apiPatch('/workflows/' + fwfId, { is_active: true });
await T.apiPost('/workflows/' + fwfId + '/publish', {});
});
if (fwfId) {
await T.test('crud', 'workflows', 'form: start instance', async function () {
var inst = await T.apiPost('/workflows/' + fwfId + '/start', {});
fChannelId = inst.channel_id || inst.id;
T.assert(fChannelId, 'no channel_id in form start');
T.registerCleanup(function () { if (fChannelId) return T.safeDelete('/channels/' + fChannelId); });
});
if (fChannelId) {
await T.test('crud', 'workflows', 'form: GET /w/:id/form (template)', async function () {
var d = await T.sessionGet('/w/' + fChannelId + '/form');
T.assert(d._status === 200, 'expected 200, got ' + d._status);
T.assertHasKey(d, 'stage_mode', 'form response');
T.assert(d.stage_mode === 'form_only', 'expected form_only, got ' + d.stage_mode);
T.assertHasKey(d, 'form_template', 'form response');
T.assert(d.form_template.fields && d.form_template.fields.length === 3,
'expected 3 fields, got ' + (d.form_template.fields ? d.form_template.fields.length : 0));
});
await T.test('crud', 'workflows', 'form: submit missing required → 400', async function () {
var d = await T.sessionPost('/w/' + fChannelId + '/form-submit', {
dept: 'eng'
});
T.assert(d._status === 400, 'expected 400, got ' + d._status);
T.assertHasKey(d, 'errors', 'validation response');
T.assert(Array.isArray(d.errors) && d.errors.length > 0, 'expected field errors');
});
await T.test('crud', 'workflows', 'form: submit invalid email → 400', async function () {
var d = await T.sessionPost('/w/' + fChannelId + '/form-submit', {
name: 'Test User',
email: 'not-an-email'
});
T.assert(d._status === 400, 'expected 400, got ' + d._status);
T.assertHasKey(d, 'errors', 'validation response');
var emailErr = d.errors.find(function (e) { return e.key === 'email'; });
T.assert(emailErr, 'expected email validation error');
});
await T.test('crud', 'workflows', 'form: submit valid → 200', async function () {
var d = await T.sessionPost('/w/' + fChannelId + '/form-submit', {
name: 'ICD Test User',
email: 'test@icd.local',
dept: 'eng'
});
T.assert(d._status === 200, 'expected 200, got ' + d._status);
});
await T.test('crud', 'workflows', 'form: verify stage_data after submit', async function () {
var d = await T.apiGet('/channels/' + fChannelId + '/workflow/status');
T.assertShape(d, T.S.workflowStatus, 'status');
// stage_data should contain submitted form values
if (d.stage_data) {
var sd = typeof d.stage_data === 'string' ? JSON.parse(d.stage_data) : d.stage_data;
T.assert(sd.name === 'ICD Test User' || sd.form_name === 'ICD Test User',
'stage_data should contain form values');
}
});
}
await T.test('crud', 'workflows', 'form: cleanup', async function () {
if (fChannelId) { await T.safeDelete('/channels/' + fChannelId); fChannelId = null; }
if (fwfId) { await T.safeDelete('/workflows/' + fwfId); fwfId = null; }
});
}
})();
}
// ── Cross-Visitor Isolation (v0.29.3) ─────────────────────
// Two instances of the same workflow should have isolated stage_data
// and one visitor cannot access the other's form endpoint.
if (T.user.role === 'admin') {
await (async function () {
var xwfId = null;
var xwfSlug = testTag + '-xvisitor';
var chA = null, chB = null;
await T.test('crud', 'workflows', 'xvisitor: setup form_only workflow', async function () {
var wf = await T.apiPost('/workflows', {
name: testTag + ' XVisitor',
slug: xwfSlug,
entry_mode: 'public_link'
});
xwfId = wf.id;
T.registerCleanup(function () { if (xwfId) return T.safeDelete('/workflows/' + xwfId); });
await T.apiPost('/workflows/' + xwfId + '/stages', {
name: 'Form', ordinal: 0, stage_mode: 'form_only', history_mode: 'full',
form_template: {
fields: [{ key: 'name', type: 'text', label: 'Name', required: true }]
}
});
await T.apiPost('/workflows/' + xwfId + '/stages', {
name: 'Done', ordinal: 1, history_mode: 'full'
});
await T.apiPatch('/workflows/' + xwfId, { is_active: true });
await T.apiPost('/workflows/' + xwfId + '/publish', {});
});
if (xwfId) {
await T.test('crud', 'workflows', 'xvisitor: start two instances', async function () {
var a = await T.apiPost('/workflows/' + xwfId + '/start', {});
chA = a.channel_id || a.id;
T.assert(chA, 'no channel A');
T.registerCleanup(function () { if (chA) return T.safeDelete('/channels/' + chA); });
var b = await T.apiPost('/workflows/' + xwfId + '/start', {});
chB = b.channel_id || b.id;
T.assert(chB, 'no channel B');
T.registerCleanup(function () { if (chB) return T.safeDelete('/channels/' + chB); });
T.assert(chA !== chB, 'channels should be different');
});
if (chA && chB) {
await T.test('crud', 'workflows', 'xvisitor: submit different data to each', async function () {
// Submit to A (admin token)
var dA = await T.apiPost('/channels/' + chA + '/workflow/advance', {
data: { name: 'Alice' }
});
T.assert(typeof dA === 'object', 'advance A');
// Submit to B
var dB = await T.apiPost('/channels/' + chB + '/workflow/advance', {
data: { name: 'Bob' }
});
T.assert(typeof dB === 'object', 'advance B');
});
await T.test('crud', 'workflows', 'xvisitor: verify stage_data isolated', async function () {
var stA = await T.apiGet('/channels/' + chA + '/workflow/status');
var stB = await T.apiGet('/channels/' + chB + '/workflow/status');
// Parse stage_data
var sdA = stA.stage_data;
if (typeof sdA === 'string') sdA = JSON.parse(sdA);
var sdB = stB.stage_data;
if (typeof sdB === 'string') sdB = JSON.parse(sdB);
// A should have Alice, B should have Bob
var aName = sdA && (sdA.name || sdA.form_name);
var bName = sdB && (sdB.name || sdB.form_name);
T.assert(aName === 'Alice', 'channel A stage_data should have Alice, got ' + aName);
T.assert(bName === 'Bob', 'channel B stage_data should have Bob, got ' + bName);
});
}
await T.test('crud', 'workflows', 'xvisitor: cleanup', async function () {
if (chA) { await T.safeDelete('/channels/' + chA); chA = null; }
if (chB) { await T.safeDelete('/channels/' + chB); chB = null; }
if (xwfId) { await T.safeDelete('/workflows/' + xwfId); xwfId = null; }
});
}
})();
}
};
})();