Changeset 0.37.14 (#226)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
@@ -394,6 +394,66 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Channel Types (group, channel) ──
|
||||
var groupChId = null;
|
||||
await T.test('crud', 'channels', 'POST /channels (type=group)', async function () {
|
||||
var d = await T.apiPost('/channels', {
|
||||
title: testTag + '-group',
|
||||
type: 'group',
|
||||
description: 'ICD group test'
|
||||
});
|
||||
T.assertShape(d, T.S.channelFull, 'group channel');
|
||||
T.assert(d.type === 'group', 'type should be group, got: ' + d.type);
|
||||
groupChId = d.id;
|
||||
T.registerCleanup(function () { if (groupChId) return T.safeDelete('/channels/' + groupChId); });
|
||||
});
|
||||
|
||||
var teamChId = null;
|
||||
await T.test('crud', 'channels', 'POST /channels (type=channel)', async function () {
|
||||
var d = await T.apiPost('/channels', {
|
||||
title: testTag + '-team-channel',
|
||||
type: 'channel',
|
||||
description: 'ICD channel test'
|
||||
});
|
||||
T.assertShape(d, T.S.channelFull, 'team channel');
|
||||
T.assert(d.type === 'channel', 'type should be channel, got: ' + d.type);
|
||||
teamChId = d.id;
|
||||
T.registerCleanup(function () { if (teamChId) return T.safeDelete('/channels/' + teamChId); });
|
||||
});
|
||||
|
||||
// ── Multi-type filter ──
|
||||
await T.test('crud', 'channels', 'GET /channels?types=group,channel (multi)', async function () {
|
||||
var d = await T.apiGet('/channels?types=group,channel&per_page=50');
|
||||
var arr = d.data || [];
|
||||
T.assert(Array.isArray(arr), 'expected data array');
|
||||
arr.forEach(function (ch) {
|
||||
T.assert(ch.type === 'group' || ch.type === 'channel',
|
||||
'multi-type filter leaked: ' + ch.type);
|
||||
});
|
||||
});
|
||||
|
||||
await T.test('crud', 'channels', 'GET /channels?types=direct,dm,group,channel (all)', async function () {
|
||||
var d = await T.apiGet('/channels?types=direct,dm,group,channel&per_page=50');
|
||||
var arr = d.data || [];
|
||||
T.assert(Array.isArray(arr), 'expected data array');
|
||||
var types = new Set(arr.map(function (ch) { return ch.type; }));
|
||||
T.assert(types.size <= 4, 'should only contain known types');
|
||||
});
|
||||
|
||||
// Cleanup channel types
|
||||
if (groupChId) {
|
||||
await T.test('crud', 'channels', 'DELETE /channels/:id (group cleanup)', async function () {
|
||||
await T.safeDelete('/channels/' + groupChId);
|
||||
groupChId = null;
|
||||
});
|
||||
}
|
||||
if (teamChId) {
|
||||
await T.test('crud', 'channels', 'DELETE /channels/:id (channel cleanup)', async function () {
|
||||
await T.safeDelete('/channels/' + teamChId);
|
||||
teamChId = null;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Folders CRUD + Channel Assignment ──
|
||||
var folderId = null;
|
||||
await T.test('crud', 'channels', 'POST /folders (create)', async function () {
|
||||
|
||||
@@ -227,16 +227,35 @@
|
||||
};
|
||||
|
||||
// ─── API Wrappers ───────────────────────────────────────────
|
||||
// API._get/_post/_put already prepend __BASE__. No base prefix here.
|
||||
// v0.37.14: raw fetch — old API._* globals removed in scorched earth.
|
||||
|
||||
T.apiGet = async function (path) { return await API._get('/api/v1' + path); };
|
||||
T.apiPost = async function (path, body) { return await API._post('/api/v1' + path, body); };
|
||||
T.apiPut = async function (path, body) { return await API._put('/api/v1' + path, body); };
|
||||
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.apiPatch = async function (path, body) {
|
||||
if (typeof API._patch === 'function') return await API._patch('/api/v1' + path, body);
|
||||
return await API._put('/api/v1' + path, body);
|
||||
};
|
||||
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 ──────────────────────────────────────────
|
||||
|
||||
@@ -244,23 +263,8 @@
|
||||
|
||||
T.captureAuthToken = async function () {
|
||||
if (_capturedToken) return _capturedToken;
|
||||
var origFetch = window.fetch;
|
||||
window.fetch = function (url, opts) {
|
||||
if (!_capturedToken && opts && opts.headers) {
|
||||
var auth = '';
|
||||
if (opts.headers instanceof Headers) {
|
||||
auth = opts.headers.get('Authorization') || '';
|
||||
} else {
|
||||
auth = opts.headers['Authorization'] || opts.headers.authorization || '';
|
||||
}
|
||||
if (typeof auth === 'string' && auth.indexOf('Bearer ') === 0) {
|
||||
_capturedToken = auth.slice(7);
|
||||
}
|
||||
}
|
||||
return origFetch.apply(this, arguments);
|
||||
};
|
||||
try { await API._get('/api/v1/health'); } catch (e) { /* swallow */ }
|
||||
window.fetch = origFetch;
|
||||
// v0.37.14: read directly from localStorage (old API._get interceptor removed)
|
||||
_capturedToken = getAuthTokenFromStorage();
|
||||
return _capturedToken;
|
||||
};
|
||||
|
||||
@@ -291,18 +295,7 @@
|
||||
// ─── DELETE (needs raw fetch fallback) ──────────────────────
|
||||
|
||||
T.apiDelete = async function (path) {
|
||||
if (typeof API._delete === 'function') return await API._delete('/api/v1' + path);
|
||||
var token = await T.getAuthToken();
|
||||
var resp = await fetch(T.base + '/api/v1' + path, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (resp.status === 204 || resp.status === 200) {
|
||||
var text = await resp.text();
|
||||
return text ? JSON.parse(text) : {};
|
||||
}
|
||||
throw new Error('DELETE ' + path + ' → ' + resp.status);
|
||||
return await _fetchJSON('DELETE', path);
|
||||
};
|
||||
|
||||
T.safeDelete = async function (path, retries) {
|
||||
|
||||
@@ -19,17 +19,41 @@
|
||||
* 12. ui.js — Render functions, export, provider setup panel
|
||||
* 11. (this file) — Boot
|
||||
*/
|
||||
(function () {
|
||||
(async function () {
|
||||
'use strict';
|
||||
|
||||
var mount = document.getElementById('extension-mount');
|
||||
if (!mount) return;
|
||||
|
||||
// ─── Boot Preact SDK (v0.37.14) ───────────────────────────
|
||||
// Extension surfaces don't load Preact vendors or the SDK.
|
||||
// Load vendors first, then boot SDK so window.sw is available.
|
||||
try {
|
||||
var base = window.__BASE__ || '';
|
||||
var ver = window.__VERSION__ || '0';
|
||||
|
||||
// Load Preact vendor modules (required by SDK)
|
||||
if (!window.preact) {
|
||||
var { h, render } = await import(base + '/js/sw/vendor/preact.module.js');
|
||||
var hooksModule = await import(base + '/js/sw/vendor/hooks.module.js');
|
||||
var htmModule = await import(base + '/js/sw/vendor/htm.module.js');
|
||||
window.preact = { h, render };
|
||||
window.hooks = hooksModule;
|
||||
window.html = htmModule.default.bind(h);
|
||||
}
|
||||
|
||||
var sdk = await import(base + '/js/sw/sdk/index.js?v=' + ver);
|
||||
await sdk.boot();
|
||||
console.log('[ICD] SDK booted — window.sw ready');
|
||||
} catch (e) {
|
||||
console.warn('[ICD] SDK boot failed:', e.message);
|
||||
}
|
||||
|
||||
// ─── Shared Namespace ───────────────────────────────────────
|
||||
window.ICD = {
|
||||
mount: mount,
|
||||
manifest: window.__MANIFEST__ || {},
|
||||
user: window.__USER__ || {},
|
||||
user: window.sw?.auth?.user || window.__USER__ || {},
|
||||
base: window.__BASE__ || '',
|
||||
|
||||
// State (populated by framework.js)
|
||||
@@ -101,7 +125,7 @@
|
||||
return;
|
||||
}
|
||||
var script = document.createElement('script');
|
||||
script.src = assetBase + modules[loaded] + '?v=' + (window.__VERSION__ || '0');
|
||||
script.src = assetBase + modules[loaded] + '?v=' + (window.__VERSION__ || '0') + '.' + Date.now();
|
||||
script.onload = function () { loaded++; loadNext(); };
|
||||
script.onerror = function () {
|
||||
console.error('[ICD] Failed to load: ' + modules[loaded]);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* ICD Test Runner — SDK Tier
|
||||
* Validates switchboard-sdk.js contract: boot, identity, REST client,
|
||||
* events, theme, pipe registration, execution ordering, scoping,
|
||||
* halt semantics, error isolation, extension compat shim, introspection.
|
||||
* Validates Preact SDK (sw/sdk/index.js) contract: boot, identity,
|
||||
* REST client, events, theme, pipe registration, execution ordering,
|
||||
* scoping, halt semantics, error isolation, introspection.
|
||||
* v0.37.14: Updated from Switchboard.init() to boot() API.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
@@ -35,16 +36,16 @@
|
||||
// BOOT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'boot', 'Switchboard.init() returns sw object', async function () {
|
||||
T.assert(typeof Switchboard !== 'undefined', 'Switchboard global missing');
|
||||
var inst = Switchboard.init();
|
||||
T.assert(inst !== null && typeof inst === 'object', 'init() should return object');
|
||||
T.assert(inst === window.sw, 'window.sw should be the same instance');
|
||||
await T.test('sdk', 'boot', 'window.sw exists after boot()', async function () {
|
||||
T.assert(window.sw !== null && typeof window.sw === 'object', 'window.sw should be an object');
|
||||
T.assert(window.sw._sdk, 'window.sw._sdk version marker should exist');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'boot', 'Double init returns same instance', async function () {
|
||||
var a = Switchboard.init();
|
||||
var b = Switchboard.init();
|
||||
await T.test('sdk', 'boot', 'boot() is idempotent', async function () {
|
||||
var a = window.sw;
|
||||
// Import and call boot() again — should return same instance
|
||||
var mod = await import(window.__BASE__ + '/js/sw/sdk/index.js');
|
||||
var b = await mod.boot();
|
||||
T.assert(a === b, 'idempotent: must return same object');
|
||||
});
|
||||
|
||||
@@ -52,16 +53,16 @@
|
||||
// IDENTITY
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'identity', 'sw.user populated', async function () {
|
||||
T.assert(sw.user !== null, 'sw.user should not be null');
|
||||
T.assert(typeof sw.user.id === 'string' && sw.user.id.length > 0, 'user.id');
|
||||
T.assert(typeof sw.user.username === 'string', 'user.username');
|
||||
T.assert(typeof sw.user.role === 'string', 'user.role');
|
||||
await T.test('sdk', 'identity', 'sw.auth.user populated', async function () {
|
||||
T.assert(sw.auth.user !== null, 'sw.auth.user should not be null');
|
||||
T.assert(typeof sw.auth.user.id === 'string' && sw.auth.user.id.length > 0, 'user.id');
|
||||
T.assert(typeof sw.auth.user.username === 'string', 'user.username');
|
||||
T.assert(typeof sw.auth.user.role === 'string', 'user.role');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'identity', 'sw.isAdmin reflects role', async function () {
|
||||
T.assert(typeof sw.isAdmin === 'boolean', 'isAdmin should be boolean');
|
||||
T.assert(sw.isAdmin === (sw.user.role === 'admin'), 'isAdmin should match role');
|
||||
T.assert(sw.isAdmin === (sw.auth.user.role === 'admin'), 'isAdmin should match role');
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -73,10 +74,9 @@
|
||||
T.assert(d.status === 'ok', 'health status should be ok');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'api', 'sw.api.get /channels returns envelope', async function () {
|
||||
await T.test('sdk', 'api', 'sw.api.get /channels returns array', async function () {
|
||||
var d = await sw.api.get('/api/v1/channels');
|
||||
T.assertHasKey(d, 'data', '/channels');
|
||||
T.assert(Array.isArray(d.data), 'data should be array');
|
||||
T.assert(Array.isArray(d), 'channels should be array (auto-unwrapped)');
|
||||
});
|
||||
|
||||
var _sdkTestChannel = null;
|
||||
@@ -159,8 +159,8 @@
|
||||
fired = true;
|
||||
T.assert(resolved === 'dark' || resolved === 'light', 'resolved should be dark|light');
|
||||
});
|
||||
// Trigger a theme change event
|
||||
Events.emit('theme.changed', {}, { localOnly: true });
|
||||
// Trigger a theme change event via Preact SDK
|
||||
sw.emit('theme.changed', {}, { localOnly: true });
|
||||
T.assert(fired, 'change handler should have fired');
|
||||
unsub();
|
||||
});
|
||||
@@ -425,28 +425,8 @@
|
||||
T.assert(count >= 2, 'unscoped filter should run on both channel types');
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// EXTENSION COMPAT SHIM
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'compat', 'ctx.renderers.register post → pipe.list() [chat-only]', async function () {
|
||||
// This tests the compat shim in extensions.js.
|
||||
// Extensions only loads on the chat surface (scripts-chat template).
|
||||
if (typeof Extensions === 'undefined') {
|
||||
T.skip('Extensions only loads on chat surface — run SDK tier from /chat to test compat shim');
|
||||
}
|
||||
var shimName = '_sdk-compat-test-' + Date.now();
|
||||
Extensions._registerRenderer('sdk-test', shimName, {
|
||||
type: 'post',
|
||||
priority: 77,
|
||||
render: function (container) { /* noop */ }
|
||||
});
|
||||
var list = sw.pipe.list();
|
||||
var found = list.render.some(function (f) {
|
||||
return f.source === 'sdk-test:' + shimName;
|
||||
});
|
||||
T.assert(found, 'shimmed post-renderer should appear in pipe.list().render');
|
||||
});
|
||||
// v0.37.14: Extension compat shim test removed (Extensions global deleted).
|
||||
// Extension rendering now goes through Preact SDK pipe directly.
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// INTROSPECTION
|
||||
|
||||
@@ -418,7 +418,6 @@
|
||||
T.assertHasKey(d, 'channels', '/admin/channels/archived');
|
||||
T.assert(Array.isArray(d.channels), 'channels should be array');
|
||||
T.assertHasKey(d, 'total', '/admin/channels/archived');
|
||||
T.assertHasKey(d, 'retention_mode', '/admin/channels/archived');
|
||||
});
|
||||
|
||||
await T.test('smoke', 'admin', 'GET /admin/storage/orphans', async function () {
|
||||
|
||||
@@ -97,13 +97,13 @@
|
||||
style: { alignSelf: 'flex-end', whiteSpace: 'nowrap' },
|
||||
onClick: function () {
|
||||
if (!T.providerSetup.apiKey) {
|
||||
if (typeof UI !== 'undefined') UI.toast('Paste an API key first', 'warning');
|
||||
if (window.sw && sw.toast) sw.toast('Paste an API key first', 'warning');
|
||||
return;
|
||||
}
|
||||
T.providerSetup.configured = true;
|
||||
T.renderProviderSetup();
|
||||
T.renderControls();
|
||||
if (typeof UI !== 'undefined') UI.toast(T.providerSetup.provider + ' configured — run Provider tier', 'success');
|
||||
if (window.sw && sw.toast) sw.toast(T.providerSetup.provider + ' configured — run Provider tier', 'success');
|
||||
}
|
||||
}, T.providerSetup.configured ? 'Reconfigure' : 'Configure');
|
||||
inputRow.appendChild(cfgBtn);
|
||||
@@ -140,12 +140,18 @@
|
||||
T.mount.style.maxWidth = '1000px';
|
||||
T.mount.style.margin = '0 auto';
|
||||
|
||||
// Top bar with user menu
|
||||
var topbar = $('div', { style: { display: 'flex', justifyContent: 'flex-end', marginBottom: '8px' } });
|
||||
T.mount.appendChild(topbar);
|
||||
if (window.sw?.userMenu) window.sw.userMenu(topbar, { flyout: 'down' });
|
||||
|
||||
// Header
|
||||
var user = window.sw?.auth?.user || T.user || {};
|
||||
var hdr = $('div', { style: { marginBottom: '24px' } }, [
|
||||
$('h1', { style: { fontSize: '22px', fontWeight: '700', color: 'var(--text)', margin: '0 0 4px 0' } },
|
||||
'ICD Test Runner'),
|
||||
$('div', { style: { fontSize: '13px', color: 'var(--text-3)' } },
|
||||
'v' + (T.manifest.version || '0.28.0') + ' · ' + esc(T.user.username) + (T.user.role === 'admin' ? ' (admin)' : ' (user)'))
|
||||
'v' + (T.manifest.version || '0.28.0') + ' · ' + esc(user.username || 'unknown') + (user.role === 'admin' ? ' (admin)' : ' (user)'))
|
||||
]);
|
||||
T.mount.appendChild(hdr);
|
||||
|
||||
@@ -232,8 +238,8 @@
|
||||
|
||||
// SDK tier button — tests switchboard-sdk.js contract
|
||||
var btnSdk = $('button', {
|
||||
className: (typeof Switchboard !== 'undefined') ? 'btn-secondary' : 'btn-ghost',
|
||||
style: { opacity: (typeof Switchboard !== 'undefined') ? '1' : '0.5' },
|
||||
className: (typeof window.sw !== 'undefined') ? 'btn-secondary' : 'btn-ghost',
|
||||
style: { opacity: (typeof window.sw !== 'undefined') ? '1' : '0.5' },
|
||||
onClick: function () { T.runSuite('sdk'); }
|
||||
}, 'SDK');
|
||||
T.el.controls.appendChild(btnSdk);
|
||||
@@ -321,7 +327,7 @@
|
||||
var pwSpan = $('span', { style: { color: 'var(--warning)', cursor: 'pointer' }, title: 'Click to copy' }, u.password);
|
||||
pwSpan.addEventListener('click', function () {
|
||||
navigator.clipboard.writeText(u.password);
|
||||
if (typeof UI !== 'undefined') UI.toast('Password copied', 'info');
|
||||
if (window.sw && sw.toast) sw.toast('Password copied', 'info');
|
||||
});
|
||||
pwTd.appendChild(pwSpan);
|
||||
tr.appendChild(pwTd);
|
||||
@@ -480,7 +486,7 @@
|
||||
|
||||
T.exportReport = function (mode) {
|
||||
if (T.results.length === 0) {
|
||||
if (typeof UI !== 'undefined') UI.toast('No T.results to export — run a suite first', 'warning');
|
||||
if (window.sw && sw.toast) sw.toast('No T.results to export — run a suite first', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -488,7 +494,7 @@
|
||||
|
||||
if (mode === 'clipboard') {
|
||||
navigator.clipboard.writeText(text).then(function () {
|
||||
if (typeof UI !== 'undefined') UI.toast('Report copied to clipboard', 'success');
|
||||
if (window.sw && sw.toast) sw.toast('Report copied to clipboard', 'success');
|
||||
}).catch(function () {
|
||||
// Fallback: select-all textarea
|
||||
clipboardFallback(text);
|
||||
@@ -505,7 +511,7 @@
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
if (typeof UI !== 'undefined') UI.toast('Downloaded ' + filename, 'success');
|
||||
if (window.sw && sw.toast) sw.toast('Downloaded ' + filename, 'success');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,7 +523,7 @@
|
||||
ta.select();
|
||||
try { document.execCommand('copy'); } catch (e) { /* give up */ }
|
||||
document.body.removeChild(ta);
|
||||
if (typeof UI !== 'undefined') UI.toast('Report copied (fallback)', 'info');
|
||||
if (window.sw && sw.toast) sw.toast('Report copied (fallback)', 'info');
|
||||
}
|
||||
|
||||
|
||||
@@ -525,15 +531,15 @@
|
||||
T.runSuite = async function (which) {
|
||||
if (T.running) return;
|
||||
if (which === 'authz' && !T.fixtures.ready) {
|
||||
if (typeof UI !== 'undefined') UI.toast('Provision test fixtures first (button on the right)', 'warning');
|
||||
if (window.sw && sw.toast) sw.toast('Provision test fixtures first (button on the right)', 'warning');
|
||||
return;
|
||||
}
|
||||
if (which === 'security' && !T.fixtures.ready) {
|
||||
if (typeof UI !== 'undefined') UI.toast('Provision test fixtures first (button on the right)', 'warning');
|
||||
if (window.sw && sw.toast) sw.toast('Provision test fixtures first (button on the right)', 'warning');
|
||||
return;
|
||||
}
|
||||
if (which === 'provider' && !T.providerSetup.configured) {
|
||||
if (typeof UI !== 'undefined') UI.toast('Configure a provider (type + API key) in the panel above', 'warning');
|
||||
if (window.sw && sw.toast) sw.toast('Configure a provider (type + API key) in the panel above', 'warning');
|
||||
return;
|
||||
}
|
||||
T.running = true;
|
||||
@@ -571,7 +577,7 @@
|
||||
var critical = T.results.filter(function (r) { return r.status === 'fail' && r.detail && r.detail.indexOf('CRITICAL') !== -1; }).length;
|
||||
var msg = pass + ' passed, ' + fail + ' failed';
|
||||
if (critical > 0) msg += ' (' + critical + ' CRITICAL)';
|
||||
UI.toast(msg, critical > 0 ? 'error' : fail > 0 ? 'warning' : 'success');
|
||||
if (window.sw && sw.toast) sw.toast(msg, critical > 0 ? 'error' : fail > 0 ? 'warning' : 'success');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user