Changeset 0.31.2 (#205)
This commit is contained in:
@@ -86,8 +86,8 @@
|
||||
|
||||
// Verify persistence
|
||||
var check = await T.apiGet('/admin/packages/' + dashPkgId + '/settings');
|
||||
T.assert(check.refresh_interval === 30 || check.data?.refresh_interval === 30,
|
||||
'refresh_interval should persist as 30');
|
||||
var vals = check.values || {};
|
||||
T.assert(vals.refresh_interval === 30, 'refresh_interval should persist as 30');
|
||||
});
|
||||
|
||||
// ── Export/Import Round-Trip ──
|
||||
|
||||
@@ -99,8 +99,8 @@
|
||||
|
||||
// Verify persistence
|
||||
var check = await T.apiGet('/admin/packages/' + editorPkgId + '/settings');
|
||||
T.assert(check.font_size === 16 || check.data?.font_size === 16,
|
||||
'font_size should persist as 16');
|
||||
var vals = check.values || {};
|
||||
T.assert(vals.font_size === 16, 'font_size should persist as 16');
|
||||
});
|
||||
|
||||
// ── Export/Import Round-Trip ──
|
||||
|
||||
179
packages/icd-test-runner/js/crud/team-workflows.js
Normal file
179
packages/icd-test-runner/js/crud/team-workflows.js
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 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: 'chat_only'
|
||||
});
|
||||
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: 'review'
|
||||
});
|
||||
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_chat',
|
||||
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;
|
||||
});
|
||||
|
||||
};
|
||||
})();
|
||||
@@ -73,6 +73,7 @@
|
||||
'crud/memory.js',
|
||||
'crud/admin.js',
|
||||
'crud/workflows.js',
|
||||
'crud/team-workflows.js',
|
||||
'crud/tasks.js',
|
||||
'crud/teams.js',
|
||||
'crud/personas.js',
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
if (C.memory) await C.memory(testTag);
|
||||
if (C.admin) await C.admin(testTag);
|
||||
if (C.workflows) await C.workflows(testTag);
|
||||
if (C.teamWorkflows) await C.teamWorkflows(testTag);
|
||||
if (C.tasks) await C.tasks(testTag);
|
||||
if (C.teams) await C.teams(testTag);
|
||||
if (C.personas) await C.personas(testTag);
|
||||
|
||||
@@ -78,8 +78,14 @@
|
||||
try {
|
||||
var evt = JSON.parse(raw);
|
||||
// Handler emits OpenAI-format: choices[0].delta.content
|
||||
if (evt.choices && evt.choices[0] && evt.choices[0].delta && evt.choices[0].delta.content) {
|
||||
content += evt.choices[0].delta.content;
|
||||
// v0.31.2: Also capture reasoning_content (DeepSeek, etc.)
|
||||
if (evt.choices && evt.choices[0] && evt.choices[0].delta) {
|
||||
if (evt.choices[0].delta.content) {
|
||||
content += evt.choices[0].delta.content;
|
||||
}
|
||||
if (evt.choices[0].delta.reasoning_content) {
|
||||
content += evt.choices[0].delta.reasoning_content;
|
||||
}
|
||||
} else if (evt.content) {
|
||||
content += evt.content; // fallback for non-OpenAI format
|
||||
}
|
||||
@@ -99,6 +105,8 @@
|
||||
T.pickCheapestChat = function (models) {
|
||||
var skipPatterns = ['embed', 'image', 'dall-e', 'tts', 'whisper', 'moderation'];
|
||||
var chatModels = models.filter(function (m) {
|
||||
// v0.31.2: Skip E2EE models — require client-side decryption we don't support
|
||||
if (m.supportsE2EE || m.supports_e2ee) return false;
|
||||
var t = m.model_type || m.type || '';
|
||||
if (t === 'embedding' || t === 'image') return false;
|
||||
// When type is missing, exclude by model ID pattern
|
||||
|
||||
Reference in New Issue
Block a user