/** * SDK Test Runner — Framework * * Dual-path validation: every test runs through the SDK first. If it * fails, a raw fetch against the ICD-specified endpoint isolates the * fault: * * SDK pass → PASS (both layers good) * SDK fail + ICD pass → SDK_BUG (wrong method/path/unwrap) * SDK fail + ICD fail → ICD_BUG (backend broken) * Validate fail → SHAPE_BUG (response doesn't match ICD contract) */ (function () { 'use strict'; var T = window.SDKR; if (!T) return; // ─── Results ─────────────────────────────────────────────── T.results = []; T.cleanup = []; T.stats = { pass: 0, sdk_bug: 0, icd_bug: 0, shape_bug: 0, skip: 0 }; T.running = false; T.aborted = false; // ─── 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'; } 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 bool, got ' + actual; } else if (type === 'object') { if (typeof val !== 'object' || val === null || Array.isArray(val)) return path + ': expected object, got ' + actual; } else if (type === 'object?') { if (val !== null && val !== undefined && (typeof val !== 'object' || Array.isArray(val))) return path + ': expected object|null, 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) { return [prefix + ': expected object, got ' + (obj === null ? 'null' : typeof obj)]; } 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); } }); 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, got ' + (typeof arr)); if (arr.length > 0) T.assertShape(arr[0], schema, (label || '$') + '[0]'); }; /** * Unwrap SDK list response — handles both bare array and paginated envelope. * SDK _unwrap returns { data: [...], total, ... } when pagination metadata * exists, or a bare array when the envelope has only { data: [...] }. */ T.unwrapList = function (r) { if (Array.isArray(r)) return r; if (r && Array.isArray(r.data)) return r.data; throw new Error('expected array or { data: [...] } envelope, got ' + (typeof r)); }; T.assertEnum = function (val, allowed, path) { if (allowed.indexOf(val) === -1) throw new Error(path + ': "' + val + '" not in [' + allowed.join(', ') + ']'); }; // ─── Raw Fetch (ICD path) ───────────────────────────────── 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' && method !== 'DELETE') 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.raw = { get: function (path) { return _fetchJSON('GET', path); }, post: function (path, body) { return _fetchJSON('POST', path, body); }, put: function (path, body) { return _fetchJSON('PUT', path, body); }, patch: function (path, body) { return _fetchJSON('PATCH', path, body); }, del: function (path) { return _fetchJSON('DELETE', path); }, }; T.safeDelete = async function (path) { try { await T.raw.del(path); } catch (e) { console.warn('[SDKR] cleanup failed: DELETE ' + path + ' — ' + e.message); } }; // ─── Auth Token ──────────────────────────────────────────── var _token = ''; function _readToken() { 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 p = JSON.parse(raw); if (p) { var t = p.access_token || p.token || p.accessToken || ''; if (t) return t; } } catch (_) { /* not JSON */ } if (raw.length > 20 && raw.indexOf('{') !== 0) return raw; } catch (_) { /* skip */ } } return ''; } T.getAuthToken = async function () { if (!_token) _token = _readToken(); return _token; }; // ─── Cleanup ─────────────────────────────────────────────── T.registerCleanup = function (fn) { T.cleanup.push(fn); }; T.runCleanup = async function () { var fns = T.cleanup.splice(0); for (var i = fns.length - 1; i >= 0; i--) { try { await fns[i](); } catch (_) { /* swallow */ } } }; // ─── Dual-Path Test ──────────────────────────────────────── // // sdkFn: async () => result — calls sw.api.* // rawSpec: { method, path, body? } — ICD-correct HTTP call // validate: (result) => void — throws if shape is wrong // // Returns: { verdict, sdkErr?, rawErr?, result? } T.dualTest = async function (sdkFn, rawSpec, validate) { // 1. Try SDK var sdkResult, sdkErr; try { sdkResult = await sdkFn(); } catch (e) { sdkErr = e; } if (!sdkErr) { // SDK call succeeded — validate shape try { if (validate) validate(sdkResult); return { verdict: 'PASS', result: sdkResult }; } catch (e) { // Shape mismatch — might be _unwrap bug. Confirm with raw. var rawResult2; try { rawResult2 = await _fetchJSON(rawSpec.method, rawSpec.path, rawSpec.body); } catch (_) { /* ignore raw errors in this branch */ } return { verdict: 'SHAPE_BUG', sdkErr: e.message, sdkResult: sdkResult, rawResult: rawResult2 }; } } // 2. SDK failed — try raw ICD fetch if (!rawSpec) { return { verdict: 'SDK_BUG', sdkErr: sdkErr.message, note: 'no raw spec provided' }; } var rawResult, rawErr; try { rawResult = await _fetchJSON(rawSpec.method, rawSpec.path, rawSpec.body); } catch (e) { rawErr = e; } if (rawErr) { // Both failed → ICD/backend issue return { verdict: 'ICD_BUG', sdkErr: sdkErr.message, rawErr: rawErr.message }; } // Raw succeeded, SDK failed → SDK has a bug // Optionally validate the raw result too var rawValid = true; if (validate) { try { validate(rawResult); } catch (e) { rawValid = false; } } return { verdict: 'SDK_BUG', sdkErr: sdkErr.message, rawResult: rawResult, rawValid: rawValid }; }; // ─── Test Harness ────────────────────────────────────────── /** * Register and run a single test. * * @param {string} domain — e.g. 'channels', 'workflows' * @param {string} group — e.g. 'crud', 'list', 'update' * @param {string} name — human-readable test name * @param {object} spec — { sdk, raw, validate } * sdk: async () => result * raw: { method: 'GET'|'POST'|..., path: '/channels', body?: {...} } * validate: (result) => void (throw on failure) * * For tests that don't use dual-path (e.g. setup/cleanup), pass a * plain async function as the 4th argument instead of a spec object. */ T.test = async function (domain, group, name, specOrFn) { if (T.aborted) return; var entry = { domain: domain, group: group, name: name, verdict: null, sdkErr: null, rawErr: null, note: null, durationMs: 0 }; var t0 = performance.now(); try { // Skip support — mark as SKIP with reason if (typeof specOrFn === 'object' && specOrFn.skip) { entry.verdict = 'SKIP'; entry.note = specOrFn.skip; } else if (typeof specOrFn === 'function') { // Simple test — no dual-path await specOrFn(); entry.verdict = 'PASS'; } else { // Dual-path test var r = await T.dualTest(specOrFn.sdk, specOrFn.raw, specOrFn.validate); entry.verdict = r.verdict; entry.sdkErr = r.sdkErr || null; entry.rawErr = r.rawErr || null; entry.note = r.note || null; } } catch (e) { entry.verdict = 'ERROR'; entry.sdkErr = e.message; } entry.durationMs = Math.round(performance.now() - t0); // Update stats var v = entry.verdict.toLowerCase().replace('error', 'icd_bug'); if (T.stats.hasOwnProperty(v)) T.stats[v]++; T.results.push(entry); // Live update if (typeof T.onResult === 'function') T.onResult(entry); }; // ─── Domain Registry ─────────────────────────────────────── T.domains = {}; T.registerDomain = function (name, fn) { T.domains[name] = fn; }; // ─── Run All ─────────────────────────────────────────────── T.runAll = async function (domainFilter) { T.running = true; T.aborted = false; T.results = []; T.cleanup = []; T.stats = { pass: 0, sdk_bug: 0, icd_bug: 0, shape_bug: 0, skip: 0 }; if (typeof T.onStart === 'function') T.onStart(); // Provision fixtures (policies, provider, team) before domains if (typeof T.provisionFixtures === 'function') { try { await T.provisionFixtures(); } catch (e) { console.warn('[SDKR] Fixture provisioning failed:', e.message); } } var names = Object.keys(T.domains).sort(); if (domainFilter) { names = names.filter(function (n) { return domainFilter.indexOf(n) !== -1; }); } for (var i = 0; i < names.length; i++) { if (T.aborted) break; try { await T.domains[names[i]](); } catch (e) { T.results.push({ domain: names[i], group: 'setup', name: 'domain crashed', verdict: 'ERROR', sdkErr: e.message, rawErr: null, note: null, durationMs: 0 }); } // Run cleanup between domains await T.runCleanup(); } // Teardown fixtures (revert policies, etc.) if (typeof T.teardownFixtures === 'function') { try { await T.teardownFixtures(); } catch (_) {} } T.running = false; if (typeof T.onComplete === 'function') T.onComplete(); }; T.abort = function () { T.aborted = true; }; })();