stage_type no longer validated — starlark_hook presence determines automation. stage_mode collapsed from 4→3 values (form/delegated/ automated); "review" mapped to "form" on input for backward compat. Migration 018 converts existing rows. Review surface removed (~110 lines). 4 package manifests updated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
180 lines
7.0 KiB
JavaScript
180 lines
7.0 KiB
JavaScript
/**
|
|
* ICD Test Runner — CRUD: Team Workflows (v0.31.2)
|
|
* Team-scoped workflow lifecycle via /teams/:teamId/workflows routes.
|
|
* Tests ownership isolation: workflows created via team routes are
|
|
* accessible only through team routes, and cross-team access is denied.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
var T = window.ICD;
|
|
if (!T) return;
|
|
if (!T.crud) T.crud = {};
|
|
|
|
T.crud.teamWorkflows = async function (testTag) {
|
|
// Requires admin user (who is also team admin for fixtures.team)
|
|
if (T.user.role !== 'admin') return;
|
|
|
|
var team = T.fixtures && T.fixtures.team;
|
|
if (!team) {
|
|
await T.test('crud', 'team-workflows', 'SKIP — no fixture team', async function () {
|
|
T.assert(false, 'fixtures.team not provisioned — run fixtures first');
|
|
});
|
|
return;
|
|
}
|
|
|
|
var teamId = team.id;
|
|
var wfId = null;
|
|
var wfSlug = testTag.toLowerCase().replace(/[^a-z0-9-]/g, '-') + '-twf';
|
|
|
|
// ── Create via team route ──
|
|
|
|
await T.test('crud', 'team-workflows', 'POST /teams/:teamId/workflows (create)', async function () {
|
|
var d = await T.apiPost('/teams/' + teamId + '/workflows', {
|
|
name: testTag + '-team-workflow',
|
|
slug: wfSlug,
|
|
description: 'Team workflow CRUD test',
|
|
entry_mode: 'team_only'
|
|
});
|
|
T.assertShape(d, T.S.workflow, 'team workflow');
|
|
T.assert(d.slug === wfSlug, 'slug mismatch: expected ' + wfSlug + ', got ' + d.slug);
|
|
T.assert(d.team_id === teamId, 'team_id should be ' + teamId + ', got ' + d.team_id);
|
|
wfId = d.id;
|
|
T.registerCleanup(function () { if (wfId) return T.safeDelete('/workflows/' + wfId); });
|
|
});
|
|
|
|
if (!wfId) return;
|
|
|
|
// ── Read via team route ──
|
|
|
|
await T.test('crud', 'team-workflows', 'GET /teams/:teamId/workflows/:id (read)', async function () {
|
|
var d = await T.apiGet('/teams/' + teamId + '/workflows/' + wfId);
|
|
T.assertShape(d, T.S.workflow, 'team workflow');
|
|
T.assert(d.id === wfId, 'id mismatch');
|
|
T.assert(d.team_id === teamId, 'team_id mismatch');
|
|
});
|
|
|
|
// ── List via team route ──
|
|
|
|
await T.test('crud', 'team-workflows', 'GET /teams/:teamId/workflows (list)', async function () {
|
|
var d = await T.apiGet('/teams/' + teamId + '/workflows');
|
|
T.assertHasKey(d, 'data', '/workflows');
|
|
T.assert(Array.isArray(d.data), 'data should be array');
|
|
var found = d.data.find(function (w) { return w.id === wfId; });
|
|
T.assert(found, 'team workflow should appear in team list');
|
|
});
|
|
|
|
// ── Update via team route ──
|
|
|
|
await T.test('crud', 'team-workflows', 'PATCH /teams/:teamId/workflows/:id (update)', async function () {
|
|
var d = await T.apiPatch('/teams/' + teamId + '/workflows/' + wfId, {
|
|
description: 'Updated by team admin',
|
|
entry_mode: 'public_link'
|
|
});
|
|
T.assert(typeof d === 'object', 'expected object');
|
|
});
|
|
|
|
// ── Stage CRUD via team routes ──
|
|
|
|
var stageIds = [];
|
|
|
|
await T.test('crud', 'team-workflows', 'POST .../stages (create stage #1)', async function () {
|
|
var d = await T.apiPost('/teams/' + teamId + '/workflows/' + wfId + '/stages', {
|
|
name: 'Team Intake',
|
|
ordinal: 0,
|
|
history_mode: 'full',
|
|
stage_mode: 'form'
|
|
});
|
|
T.assertShape(d, T.S.workflowStage, 'stage');
|
|
T.assert(d.name === 'Team Intake', 'name mismatch');
|
|
stageIds.push(d.id);
|
|
});
|
|
|
|
await T.test('crud', 'team-workflows', 'POST .../stages (create stage #2)', async function () {
|
|
var d = await T.apiPost('/teams/' + teamId + '/workflows/' + wfId + '/stages', {
|
|
name: 'Team Review',
|
|
ordinal: 1,
|
|
history_mode: 'summary',
|
|
stage_mode: 'form'
|
|
});
|
|
T.assertShape(d, T.S.workflowStage, 'stage');
|
|
stageIds.push(d.id);
|
|
});
|
|
|
|
await T.test('crud', 'team-workflows', 'GET .../stages (list)', async function () {
|
|
var d = await T.apiGet('/teams/' + teamId + '/workflows/' + wfId + '/stages');
|
|
T.assertHasKey(d, 'data', '/stages');
|
|
T.assert(d.data.length >= 2, 'expected at least 2 stages, got ' + d.data.length);
|
|
});
|
|
|
|
await T.test('crud', 'team-workflows', 'PUT .../stages/:sid (update)', async function () {
|
|
var d = await T.apiPut('/teams/' + teamId + '/workflows/' + wfId + '/stages/' + stageIds[0], {
|
|
name: 'Team Intake (updated)',
|
|
ordinal: 0,
|
|
history_mode: 'full',
|
|
stage_mode: 'form',
|
|
form_template: { fields: [{ key: 'name', type: 'text', label: 'Name', required: true }] }
|
|
});
|
|
T.assert(typeof d === 'object', 'expected object');
|
|
});
|
|
|
|
if (stageIds.length >= 2) {
|
|
await T.test('crud', 'team-workflows', 'PATCH .../stages/reorder', async function () {
|
|
var reversed = stageIds.slice().reverse();
|
|
var d = await T.apiPatch('/teams/' + teamId + '/workflows/' + wfId + '/stages/reorder', {
|
|
ordered_ids: reversed
|
|
});
|
|
T.assert(typeof d === 'object', 'expected object');
|
|
});
|
|
// Restore order
|
|
await T.apiPatch('/teams/' + teamId + '/workflows/' + wfId + '/stages/reorder', {
|
|
ordered_ids: stageIds
|
|
});
|
|
}
|
|
|
|
// ── Activate + Publish via team route ──
|
|
|
|
var versionNum = null;
|
|
|
|
await T.test('crud', 'team-workflows', 'PATCH activate + POST publish', async function () {
|
|
await T.apiPatch('/teams/' + teamId + '/workflows/' + wfId, { is_active: true });
|
|
var v = await T.apiPost('/teams/' + teamId + '/workflows/' + wfId + '/publish', {});
|
|
T.assertShape(v, T.S.workflowVersion, 'version');
|
|
T.assertHasKey(v, 'snapshot', 'version');
|
|
versionNum = v.version_number;
|
|
});
|
|
|
|
await T.test('crud', 'team-workflows', 'GET .../versions/:v (read)', async function () {
|
|
T.assert(versionNum !== null, 'no version number from publish');
|
|
var d = await T.apiGet('/teams/' + teamId + '/workflows/' + wfId + '/versions/' + versionNum);
|
|
T.assertShape(d, T.S.workflowVersion, 'version');
|
|
T.assert(d.version_number === versionNum, 'version should be ' + versionNum);
|
|
});
|
|
|
|
// ── Ownership isolation: cross-team access denied ──
|
|
|
|
await T.test('crud', 'team-workflows', 'GET /teams/fake-id/workflows/:id → 403', async function () {
|
|
var d = await T.authFetch(await T.getAuthToken(), 'GET',
|
|
'/teams/00000000-0000-0000-0000-000000000099/workflows/' + wfId, null);
|
|
// Should be 403 (not a team member) or 404
|
|
T.assert(d._status === 403 || d._status === 404,
|
|
'cross-team access should be denied, got ' + d._status);
|
|
});
|
|
|
|
// ── Stage delete via team route ──
|
|
|
|
if (stageIds.length >= 2) {
|
|
await T.test('crud', 'team-workflows', 'DELETE .../stages/:sid', async function () {
|
|
await T.apiDelete('/teams/' + teamId + '/workflows/' + wfId + '/stages/' + stageIds.pop());
|
|
});
|
|
}
|
|
|
|
// ── Workflow delete via team route ──
|
|
|
|
await T.test('crud', 'team-workflows', 'DELETE /teams/:teamId/workflows/:id', async function () {
|
|
await T.apiDelete('/teams/' + teamId + '/workflows/' + wfId);
|
|
wfId = null;
|
|
});
|
|
|
|
};
|
|
})();
|