This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/packages/sdk-test-runner/js/framework.js
Jeffrey Smith 829caa3b20
All checks were successful
CI/CD / detect-changes (push) Successful in 3s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m51s
CI/CD / test-sqlite (push) Successful in 3m2s
CI/CD / build-and-deploy (push) Successful in 1m26s
Feat v0.7.1 surface runner framework (#55)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-04-01 23:01:38 +00:00

240 lines
8.8 KiB
JavaScript

/**
* SDK Test Runner — Framework
*
* Dual-path validation utilities. Each domain file registers suites
* directly with sw.testing.suite(). This module provides:
*
* T.dualTest — run SDK + raw ICD, return verdict
* T.raw — raw ICD fetch helpers
* T.assert* — assertion utilities
* T.unwrapList — envelope unwrapper
* T.getAuthToken / T.safeDelete / T.registerCleanup / T.runCleanup
*
* Verdicts:
* PASS — SDK call succeeded + shape valid
* SDK_BUG — SDK failed, raw ICD succeeded
* ICD_BUG — both failed
* SHAPE_BUG — SDK call succeeded but validate() threw
*/
(function () {
'use strict';
var T = window.SDKR;
if (!T) return;
// ─── Cleanup ───────────────────────────────────────────────
T.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 */ }
}
};
// ─── 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;
};
// ─── 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
};
};
})();