/** * ICD Test Runner — Framework * DOM helpers, assertion library, ICD shapes, API wrappers, test harness. */ (function () { 'use strict'; var T = window.ICD; if (!T) return; // ─── DOM Helpers ──────────────────────────────────────────── T.esc = function (s) { var el = document.createElement('span'); el.textContent = String(s); return el.innerHTML; }; T.$ = function (tag, attrs, children) { var el = document.createElement(tag); if (attrs) Object.keys(attrs).forEach(function (k) { if (k === 'className') el.className = attrs[k]; else if (k === 'style' && typeof attrs[k] === 'object') Object.assign(el.style, attrs[k]); else if (k.indexOf('on') === 0) el.addEventListener(k.slice(2).toLowerCase(), attrs[k]); else el.setAttribute(k, attrs[k]); }); if (children) { if (!Array.isArray(children)) children = [children]; children.forEach(function (c) { if (typeof c === 'string') el.appendChild(document.createTextNode(c)); else if (c) el.appendChild(c); }); } return el; }; // ─── Assertion Library ────────────────────────────────────── function assertType(val, type, path) { var actual = val === null ? 'null' : Array.isArray(val) ? 'array' : typeof val; if (type === 'array') { if (!Array.isArray(val)) return path + ': expected array, got ' + actual; } else if (type === 'uuid') { if (typeof val !== 'string' || val.length < 8) return path + ': expected uuid string'; } else if (type === 'string?') { if (val !== null && val !== undefined && typeof val !== 'string') return path + ': expected string|null, got ' + actual; } else if (type === 'number?') { if (val !== null && val !== undefined && typeof val !== 'number') return path + ': expected number|null, got ' + actual; } else if (type === 'bool') { if (typeof val !== 'boolean') return path + ': expected boolean, got ' + actual; } else if (type === 'object') { if (typeof val !== 'object' || val === null || Array.isArray(val)) return path + ': expected object, got ' + actual; } else if (type === 'any') { // always passes } else { if (typeof val !== type) return path + ': expected ' + type + ', got ' + actual; } return null; } function validateShape(obj, schema, prefix) { var errors = []; prefix = prefix || '$'; if (typeof obj !== 'object' || obj === null) { errors.push(prefix + ': expected object, got ' + (obj === null ? 'null' : typeof obj)); return errors; } Object.keys(schema).forEach(function (key) { var spec = schema[key]; var val = obj[key]; var path = prefix + '.' + key; if (typeof spec === 'string') { if (spec.charAt(spec.length - 1) === '?') { if (val === undefined || val === null) return; spec = spec.slice(0, -1); } else if (val === undefined) { errors.push(path + ': missing required field'); return; } var e = assertType(val, spec, path); if (e) errors.push(e); } else if (typeof spec === 'object' && spec._type) { if (spec._optional && (val === undefined || val === null)) return; if (val === undefined) { errors.push(path + ': missing required field'); return; } var e2 = assertType(val, spec._type, path); if (e2) errors.push(e2); } }); return errors; } T.assert = function (condition, msg) { if (!condition) throw new Error(msg || 'assertion failed'); }; T.assertShape = function (obj, schema, label) { var errs = validateShape(obj, schema, label || '$'); if (errs.length) throw new Error(errs.join('; ')); }; T.assertArrayOf = function (arr, schema, label) { T.assert(Array.isArray(arr), (label || '$') + ': expected array'); if (arr.length > 0) T.assertShape(arr[0], schema, (label || '$') + '[0]'); }; T.assertHasKey = function (obj, key, label) { T.assert(obj && obj.hasOwnProperty(key), (label || '$') + ': missing key "' + key + '"'); }; T.assertEnum = function (val, allowed, path) { if (allowed.indexOf(val) === -1) throw new Error(path + ': "' + val + '" not in [' + allowed.join(', ') + ']'); }; T.assertStatus = function (data, expected, label) { T.assert(data._status === expected, (label || '$') + ': expected HTTP ' + expected + ', got ' + data._status + (data.error ? ' (' + data.error + ')' : '')); }; // ─── ICD Shapes ───────────────────────────────────────────── var S = T.S; S.channel = { id: 'string', title: 'string', type: 'string', created_at: 'string', updated_at: 'string' }; S.channelFull = { id: 'string', user_id: 'string', title: 'string', type: 'string', ai_mode: 'string?', topic: 'string?', description: 'string?', model: 'string?', provider_config_id: 'string?', system_prompt: 'string?', is_archived: 'bool', is_pinned: 'bool', folder: 'string?', folder_id: 'string?', project_id: 'string?', workspace_id: 'string?', tags: 'array', created_at: 'string', updated_at: 'string' }; S.channelModel = { id: 'string', channel_id: 'string', model_id: 'string', is_default: 'bool', created_at: 'string' }; S.message = { id: 'string', channel_id: 'string', role: 'string', content: 'string', created_at: 'string' }; S.persona = { id: 'string', name: 'string', scope: 'string', created_at: 'string', updated_at: 'string' }; S.note = { id: 'string', title: 'string', created_at: 'string', updated_at: 'string' }; S.project = { id: 'string', name: 'string', scope: 'string', owner_id: 'string', is_archived: 'bool', created_at: 'string', updated_at: 'string', description: 'string?', color: 'string?', icon: 'string?', team_id: 'string?', workspace_id: 'string?', settings: 'object?', channel_count: 'number?', kb_count: 'number?', note_count: 'number?' }; S.kb = { id: 'string', name: 'string', scope: 'string', embedding_config: 'object', document_count: 'number', chunk_count: 'number', total_bytes: 'number', status: 'string', created_at: 'string', updated_at: 'string' }; S.folder = { id: 'string', name: 'string', created_at: 'string' }; S.workspace = { id: 'string', name: 'string', owner_type: 'string', owner_id: 'string', status: 'string', created_at: 'string', updated_at: 'string' }; S.gitCredSummary = { id: 'string', name: 'string', auth_type: 'string', created_at: 'string' }; S.notification = { id: 'string', type: 'string', title: 'string', read: 'bool', created_at: 'string' }; S.memory = { id: 'string', scope: 'string', owner_id: 'string', key: 'string', value: 'string', confidence: 'number', status: 'string', created_at: 'string', updated_at: 'string' }; S.profile = { id: 'string', username: 'string', email: 'string', role: 'string', settings: 'object', created_at: 'string' }; S.surface = { id: 'string', title: 'string', source: 'string', enabled: 'bool' }; S.surfaceNav = { id: 'string', title: 'string', route: 'string' }; S.surfaceAdmin = { id: 'string', title: 'string', manifest: 'object', enabled: 'bool', source: 'string', installed_at: 'string', updated_at: 'string' }; S.providerConfig = { id: 'string', name: 'string', provider: 'string', scope: 'string' }; S.safeConfig = { id: 'string', name: 'string', provider: 'string', is_active: 'bool', has_key: 'bool' }; S.catalogModel = { model_id: 'string', provider: 'string' }; S.modelEnabled = { id: 'string', model_id: 'string', display_name: 'string', model_type: 'string', source: 'string', provider_config_id: 'string', provider_name: 'string', provider_type: 'string', capabilities: 'object', scope: 'string', is_persona: 'bool', hidden: 'bool' }; S.modelPreference = { id: 'string', user_id: 'string', model_id: 'string', provider_config_id: 'string', hidden: 'bool', sort_order: 'number', created_at: 'string', updated_at: 'string' }; S.task = { id: 'string', name: 'string', task_type: 'string', schedule: 'string', is_active: 'bool', created_at: 'string' }; S.taskFull = { id: 'string', owner_id: 'string', name: 'string', task_type: 'string', scope: 'string', schedule: 'string', timezone: 'string', is_active: 'bool', max_tokens: 'number', max_tool_calls: 'number', max_wall_clock: 'number', output_mode: 'string', run_count: 'number', created_at: 'string', updated_at: 'string' }; S.taskRun = { id: 'string', task_id: 'string', status: 'string', started_at: 'string' }; S.workflow = { id: 'string', name: 'string', slug: 'string', entry_mode: 'string', is_active: 'bool', created_at: 'string', updated_at: 'string' }; S.workflowStage = { id: 'string', workflow_id: 'string', ordinal: 'number', name: 'string', history_mode: 'string', created_at: 'string', surface_pkg_id: 'string?' }; S.workflowVersion = { id: 'string', workflow_id: 'string', version_number: 'number', created_at: 'string' }; S.workflowStatus = { workflow_id: 'string', workflow_version: 'number', current_stage: 'number', status: 'string' }; S.assignment = { id: 'string', channel_id: 'string', status: 'string', created_at: 'string' }; S.team = { id: 'string', name: 'string', created_at: 'string?' }; S.teamMember = { user_id: 'string', role: 'string' }; S.group = { id: 'string', name: 'string' }; S.extension = { id: 'string', title: 'string', type: 'string', version: 'string', tier: 'string', enabled: 'bool', is_system: 'bool', scope: 'string', source: 'string', installed_at: 'string', updated_at: 'string' }; S.auditEntry = { id: 'string', action: 'string', created_at: 'string' }; S.file = { id: 'string', filename: 'string', content_type: 'string', origin: 'string', created_at: 'string' }; S.participant = { id: 'string', channel_id: 'string', participant_type: 'string', role: 'string', joined_at: 'string' }; S.adminUser = { id: 'string', username: 'string', role: 'string', created_at: 'string' }; S.loginResponse = { access_token: 'string', refresh_token: 'string' }; // ─── Test Harness ─────────────────────────────────────────── T.registerCleanup = function (fn) { T.cleanup.push(fn); }; T.runCleanup = async function () { for (var i = T.cleanup.length - 1; i >= 0; i--) { try { await T.cleanup[i](); } catch (e) { /* best effort */ } } T.cleanup = []; }; T.test = async function (tier, domain, name, fn) { var t0 = performance.now(); var entry = { tier: tier, domain: domain, name: name, status: 'pass', duration: 0, detail: '' }; try { await fn(); } catch (e) { if (e && e._skip) { entry.status = 'skip'; entry.detail = String(e.message || 'skipped'); } else { entry.status = 'fail'; entry.detail = String(e && e.message ? e.message : e); } } entry.duration = Math.round(performance.now() - t0); T.results.push(entry); if (typeof T.renderProgress === 'function') T.renderProgress(); return entry; }; /** * Skip a test with a reason. Call inside a T.test() fn body. * Skipped tests appear in results as status='skip' — not pass, not fail. * @param {string} reason — why this test was skipped */ T.skip = function (reason) { var e = new Error(reason || 'skipped'); e._skip = true; throw e; }; // ─── API Wrappers ─────────────────────────────────────────── // Raw fetch — kernel provides no global API wrappers; extensions use fetch directly. async function _fetchJSON(method, path, body) { var token = await T.getAuthToken(); var opts = { method: method, headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, credentials: 'same-origin' }; if (body !== undefined && method !== 'GET') opts.body = JSON.stringify(body); var resp = await fetch(T.base + '/api/v1' + path, opts); if (resp.status === 204) return { _status: 204 }; var text = await resp.text(); var data; try { data = text ? JSON.parse(text) : {}; } catch (e) { data = { _raw: text }; } data._status = resp.status; if (!resp.ok) { var err = new Error(method + ' ' + path + ' → ' + resp.status + (data.error ? ': ' + data.error : '')); err.status = resp.status; err.data = data; throw err; } return data; } T.apiGet = async function (path) { return await _fetchJSON('GET', path); }; T.apiPost = async function (path, body) { return await _fetchJSON('POST', path, body); }; T.apiPut = async function (path, body) { return await _fetchJSON('PUT', path, body); }; T.apiPatch = async function (path, body) { return await _fetchJSON('PATCH', path, body); }; // ─── Token Capture ────────────────────────────────────────── var _capturedToken = ''; T.captureAuthToken = async function () { if (_capturedToken) return _capturedToken; // v0.37.14: read directly from localStorage (old API._get interceptor removed) _capturedToken = getAuthTokenFromStorage(); return _capturedToken; }; function getAuthTokenFromStorage() { var env = (T.base || '').replace(/^\//, '') || 'default'; var keys = ['arm_auth_' + env, 'arm_auth', 'arm_token']; for (var i = 0; i < keys.length; i++) { try { var raw = localStorage.getItem(keys[i]); if (!raw) continue; try { var parsed = JSON.parse(raw); if (parsed) { var tok = parsed.access_token || parsed.token || parsed.accessToken || ''; if (tok) return tok; } } catch (e2) { /* not JSON */ } if (raw.length > 20 && raw.indexOf('{') !== 0) return raw; } catch (e) { /* skip */ } } return ''; } T.getAuthToken = async function () { return _capturedToken || (await T.captureAuthToken()) || getAuthTokenFromStorage(); }; // ─── DELETE (needs raw fetch fallback) ────────────────────── T.apiDelete = async function (path) { return await _fetchJSON('DELETE', path); }; T.safeDelete = async function (path, retries) { retries = retries || 2; for (var attempt = 0; attempt <= retries; attempt++) { try { return await T.apiDelete(path); } catch (e) { // On 502/503, wait and retry (proxy may be recovering) if (attempt < retries && e.message && (e.message.indexOf('502') !== -1 || e.message.indexOf('503') !== -1)) { await new Promise(function (r) { setTimeout(r, 500 * (attempt + 1)); }); continue; } // Last resort: try without auth header (some DELETE endpoints accept this) try { var resp = await fetch(T.base + '/api/v1' + path, { method: 'DELETE', credentials: 'same-origin' }); if (resp.ok || resp.status === 204) return {}; } catch (e2) { /* give up */ } // Swallow cleanup errors on final attempt — don't let cleanup failures mask test results if (attempt >= retries) { console.warn('[ICD] cleanup failed: DELETE ' + path + ' — ' + e.message); return {}; } } } return {}; }; // ── File Upload (multipart/form-data) ────────────────────── T.apiUpload = async function (path, file, filename) { var token = await T.getAuthToken(); var fd = new FormData(); fd.append('file', file, filename || 'test.txt'); var resp = await fetch(T.base + '/api/v1' + path, { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, credentials: 'same-origin', body: fd }); var text = await resp.text(); var data; try { data = text ? JSON.parse(text) : {}; } catch (e) { data = { _raw: text }; } data._status = resp.status; if (!resp.ok) throw new Error('Upload ' + path + ' → ' + resp.status + (data.error ? ': ' + data.error : '')); return data; }; // ─── Auth Fetch (explicit token for test users) ───────────── T.authFetch = async function (token, method, path, body) { var opts = { method: method, headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, credentials: 'same-origin' }; if (body !== undefined && method !== 'GET') opts.body = JSON.stringify(body); var resp = await fetch(T.base + '/api/v1' + path, opts); var text = await resp.text(); var data; try { data = text ? JSON.parse(text) : {}; } catch (e) { data = { _raw: text }; } data._status = resp.status; return data; }; T.publicPost = async function (path, body) { var resp = await fetch(T.base + '/api/v1' + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); var text = await resp.text(); var data; try { data = text ? JSON.parse(text) : {}; } catch (e) { data = { _raw: text }; } data._status = resp.status; return data; }; // Session-scoped fetch: uses cookies only (no JWT). For visitor workflow tests. T.sessionGet = async function (path) { var resp = await fetch(T.base + '/api/v1' + path, { method: 'GET', credentials: 'include' }); var text = await resp.text(); var data; try { data = text ? JSON.parse(text) : {}; } catch (e) { data = { _raw: text }; } data._status = resp.status; return data; }; T.sessionPost = async function (path, body) { var resp = await fetch(T.base + '/api/v1' + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(body) }); var text = await resp.text(); var data; try { data = text ? JSON.parse(text) : {}; } catch (e) { data = { _raw: text }; } data._status = resp.status; return data; }; })();