Changeset 0.37.17 (#229)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
222
packages/sdk-test-runner/css/main.css
Normal file
222
packages/sdk-test-runner/css/main.css
Normal file
@@ -0,0 +1,222 @@
|
||||
/* SDK Test Runner — Styles */
|
||||
|
||||
.sdkr-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.sdkr-header h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--text, #e0e0e0);
|
||||
}
|
||||
.sdkr-version {
|
||||
font-size: 12px;
|
||||
color: var(--text3, #888);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.sdkr-desc {
|
||||
font-size: 13px;
|
||||
color: var(--text2, #aaa);
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Fixtures panel */
|
||||
.sdkr-fixtures {
|
||||
margin-bottom: 16px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border, #333);
|
||||
border-radius: 6px;
|
||||
background: var(--bg1, #1a1a1a);
|
||||
}
|
||||
.sdkr-fixture-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.sdkr-fixture-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text2, #aaa);
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sdkr-select {
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
background: var(--bg2, #222);
|
||||
color: var(--text, #e0e0e0);
|
||||
border: 1px solid var(--border, #444);
|
||||
border-radius: 4px;
|
||||
min-width: 130px;
|
||||
}
|
||||
.sdkr-input {
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
background: var(--bg2, #222);
|
||||
color: var(--text, #e0e0e0);
|
||||
border: 1px solid var(--border, #444);
|
||||
border-radius: 4px;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 400px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.sdkr-input::placeholder {
|
||||
color: var(--text3, #666);
|
||||
font-family: inherit;
|
||||
}
|
||||
.sdkr-fixture-status {
|
||||
font-size: 12px;
|
||||
color: var(--text3, #888);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.sdkr-fixture-note {
|
||||
font-size: 11px;
|
||||
color: var(--text3, #666);
|
||||
margin-top: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.sdkr-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.sdkr-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.sdkr-filter-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text2, #aaa);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.sdkr-check {
|
||||
font-size: 13px;
|
||||
color: var(--text, #e0e0e0);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
.sdkr-check input { cursor: pointer; }
|
||||
.sdkr-btn-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.sdkr-btn {
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border, #444);
|
||||
background: var(--bg2, #2a2a2a);
|
||||
color: var(--text, #e0e0e0);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.sdkr-btn:hover { background: var(--bg3, #383838); }
|
||||
.sdkr-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.sdkr-btn-primary {
|
||||
background: var(--accent, #3b82f6);
|
||||
border-color: var(--accent, #3b82f6);
|
||||
color: #fff;
|
||||
}
|
||||
.sdkr-btn-primary:hover { filter: brightness(1.15); }
|
||||
.sdkr-btn-danger {
|
||||
background: var(--danger, #ef4444);
|
||||
border-color: var(--danger, #ef4444);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Summary */
|
||||
.sdkr-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 12px;
|
||||
background: var(--bg2, #1e1e1e);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border, #333);
|
||||
}
|
||||
.sdkr-badge {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* Table */
|
||||
.sdkr-table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border, #333);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.sdkr-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
.sdkr-table th {
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
background: var(--bg2, #1e1e1e);
|
||||
color: var(--text2, #aaa);
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 1px solid var(--border, #333);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.sdkr-table td {
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--border-subtle, #2a2a2a);
|
||||
color: var(--text, #e0e0e0);
|
||||
vertical-align: top;
|
||||
}
|
||||
.sdkr-table tbody tr:hover {
|
||||
background: var(--bg-hover, rgba(255,255,255,0.03));
|
||||
}
|
||||
.sdkr-test-name {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
.sdkr-verdict {
|
||||
font-weight: 700;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
.sdkr-ms {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--text3, #888);
|
||||
text-align: right;
|
||||
}
|
||||
.sdkr-detail {
|
||||
font-size: 11px;
|
||||
color: var(--text3, #888);
|
||||
max-width: 400px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Row verdict highlights */
|
||||
.sdkr-row-sdk_bug td { background: rgba(245, 158, 11, 0.06); }
|
||||
.sdkr-row-icd_bug td { background: rgba(239, 68, 68, 0.06); }
|
||||
.sdkr-row-shape_bug td { background: rgba(168, 85, 247, 0.06); }
|
||||
.sdkr-row-error td { background: rgba(239, 68, 68, 0.08); }
|
||||
153
packages/sdk-test-runner/js/domains/admin.js
Normal file
153
packages/sdk-test-runner/js/domains/admin.js
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* SDK Test Runner — Domain: admin
|
||||
*
|
||||
* Tests admin-scoped endpoints. Requires admin role.
|
||||
* KNOWN ISSUES: some admin list endpoints use non-standard envelope
|
||||
* keys (§E in ICD drift audit).
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.SDKR;
|
||||
if (!T) return;
|
||||
|
||||
T.registerDomain('admin', async function () {
|
||||
|
||||
// Guard: skip if not admin
|
||||
if (!window.sw || !sw.isAdmin) {
|
||||
await T.test('admin', 'guard', 'skip — not admin', async function () {
|
||||
T.assert(true, 'skipped (user is not admin)');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Users ──
|
||||
|
||||
await T.test('admin', 'users', 'sw.api.admin.users.list()', {
|
||||
sdk: function () { return sw.api.admin.users.list(); },
|
||||
raw: { method: 'GET', path: '/admin/users' },
|
||||
validate: function (r) {
|
||||
// KNOWN: backend returns {users: [...]} not {data: [...]}
|
||||
var arr = r;
|
||||
if (r && r.users) arr = r.users;
|
||||
if (r && r.data) arr = r.data;
|
||||
T.assert(Array.isArray(arr), 'expected array (maybe wrapped in non-standard key)');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Configs (provider configs) ──
|
||||
|
||||
await T.test('admin', 'configs', 'sw.api.admin.configs.list()', {
|
||||
sdk: function () { return sw.api.admin.configs.list(); },
|
||||
raw: { method: 'GET', path: '/admin/configs' },
|
||||
validate: function (r) {
|
||||
// KNOWN: backend returns {configs: [...]} not {data: [...]}
|
||||
var arr = r;
|
||||
if (r && r.configs) arr = r.configs;
|
||||
if (r && r.data) arr = r.data;
|
||||
T.assert(Array.isArray(arr), 'expected array');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Models ──
|
||||
|
||||
await T.test('admin', 'models', 'sw.api.admin.models.list()', {
|
||||
sdk: function () { return sw.api.admin.models.list(); },
|
||||
raw: { method: 'GET', path: '/admin/models' },
|
||||
validate: function (r) {
|
||||
// KNOWN: backend returns {models: [...]} not {data: [...]}
|
||||
var arr = r;
|
||||
if (r && r.models) arr = r.models;
|
||||
if (r && r.data) arr = r.data;
|
||||
T.assert(Array.isArray(arr), 'expected array');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Settings ──
|
||||
|
||||
await T.test('admin', 'settings', 'sw.api.admin.settings.get()', {
|
||||
sdk: function () { return sw.api.admin.settings.get(); },
|
||||
raw: { method: 'GET', path: '/admin/settings' },
|
||||
validate: function (r) { T.assert(typeof r === 'object', 'expected object'); }
|
||||
});
|
||||
|
||||
// ── Packages ──
|
||||
|
||||
await T.test('admin', 'packages', 'sw.api.admin.packages.list()', {
|
||||
sdk: function () { return sw.api.admin.packages.list(); },
|
||||
raw: { method: 'GET', path: '/admin/packages' },
|
||||
validate: function (r) { T.assert(Array.isArray(r) || (r && Array.isArray(r.data)), 'expected array'); }
|
||||
});
|
||||
|
||||
// ── Surfaces ──
|
||||
|
||||
await T.test('admin', 'surfaces', 'sw.api.admin.surfaces.list()', {
|
||||
sdk: function () { return sw.api.admin.surfaces.list(); },
|
||||
raw: { method: 'GET', path: '/admin/surfaces' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Extensions ──
|
||||
|
||||
await T.test('admin', 'extensions', 'sw.api.admin.extensions.list()', {
|
||||
sdk: function () { return sw.api.admin.extensions.list(); },
|
||||
raw: { method: 'GET', path: '/admin/extensions' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Tasks ──
|
||||
|
||||
await T.test('admin', 'tasks', 'sw.api.admin.tasks.list()', {
|
||||
sdk: function () { return sw.api.admin.tasks.list(); },
|
||||
raw: { method: 'GET', path: '/admin/tasks' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Routing ──
|
||||
|
||||
await T.test('admin', 'routing', 'sw.api.admin.routing.policies()', {
|
||||
sdk: function () { return sw.api.admin.routing.policies(); },
|
||||
raw: { method: 'GET', path: '/admin/routing/policies' },
|
||||
validate: function (r) { T.assert(Array.isArray(r) || (r && Array.isArray(r.data)), 'expected array'); }
|
||||
});
|
||||
|
||||
// ── Provider health ──
|
||||
|
||||
await T.test('admin', 'providers', 'sw.api.admin.providers.health()', {
|
||||
sdk: function () { return sw.api.admin.providers.health(); },
|
||||
raw: { method: 'GET', path: '/admin/providers/health' },
|
||||
validate: function (r) { T.assert(typeof r === 'object' || Array.isArray(r), 'expected object or array'); }
|
||||
});
|
||||
|
||||
// ── Storage ──
|
||||
|
||||
await T.test('admin', 'storage', 'sw.api.admin.storage.status()', {
|
||||
sdk: function () { return sw.api.admin.storage.status(); },
|
||||
raw: { method: 'GET', path: '/admin/storage/status' },
|
||||
validate: function (r) { T.assert(typeof r === 'object', 'expected object'); }
|
||||
});
|
||||
|
||||
// ── Vault ──
|
||||
|
||||
await T.test('admin', 'vault', 'sw.api.admin.vault.status()', {
|
||||
sdk: function () { return sw.api.admin.vault.status(); },
|
||||
raw: { method: 'GET', path: '/admin/vault/status' },
|
||||
validate: function (r) { T.assert(typeof r === 'object', 'expected object'); }
|
||||
});
|
||||
|
||||
// ── Channels (archived) ──
|
||||
|
||||
await T.test('admin', 'channels', 'sw.api.admin.channels.archived()', {
|
||||
sdk: function () { return sw.api.admin.channels.archived(); },
|
||||
raw: { method: 'GET', path: '/admin/channels/archived' },
|
||||
validate: function (r) { T.assert(Array.isArray(r) || (r && Array.isArray(r.data)), 'expected array'); }
|
||||
});
|
||||
|
||||
// ── Projects ──
|
||||
|
||||
await T.test('admin', 'projects', 'sw.api.admin.projects.list()', {
|
||||
sdk: function () { return sw.api.admin.projects.list(); },
|
||||
raw: { method: 'GET', path: '/admin/projects' },
|
||||
validate: function (r) { T.assert(Array.isArray(r) || (r && Array.isArray(r.data)), 'expected array'); }
|
||||
});
|
||||
});
|
||||
})();
|
||||
158
packages/sdk-test-runner/js/domains/channels.js
Normal file
158
packages/sdk-test-runner/js/domains/channels.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* SDK Test Runner — Domain: channels
|
||||
*
|
||||
* Tests: list, create, get, update, delete, messages, participants,
|
||||
* models, kbs, files, markRead, generateTitle, path, siblings.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.SDKR;
|
||||
if (!T) return;
|
||||
|
||||
T.registerDomain('channels', async function () {
|
||||
var tag = 'sdkr-' + Date.now();
|
||||
var channelId = null;
|
||||
|
||||
// ── List ──
|
||||
|
||||
await T.test('channels', 'list', 'sw.api.channels.list() returns array', {
|
||||
sdk: function () { return sw.api.channels.list(); },
|
||||
raw: { method: 'GET', path: '/channels' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Create ──
|
||||
|
||||
await T.test('channels', 'crud', 'sw.api.channels.create() returns channel shape', {
|
||||
sdk: function () {
|
||||
return sw.api.channels.create({ title: tag + '-ch', type: 'direct' });
|
||||
},
|
||||
raw: { method: 'POST', path: '/channels', body: { title: tag + '-ch-raw', type: 'direct' } },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.channel, 'channel');
|
||||
channelId = r.id;
|
||||
T.registerCleanup(function () { if (channelId) return T.safeDelete('/channels/' + channelId); });
|
||||
}
|
||||
});
|
||||
|
||||
if (!channelId) return;
|
||||
|
||||
// ── Get ──
|
||||
|
||||
await T.test('channels', 'crud', 'sw.api.channels.get(id) returns full shape', {
|
||||
sdk: function () { return sw.api.channels.get(channelId); },
|
||||
raw: { method: 'GET', path: '/channels/' + channelId },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.channelFull, 'channelFull');
|
||||
T.assert(r.id === channelId, 'id mismatch');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Update ──
|
||||
|
||||
await T.test('channels', 'crud', 'sw.api.channels.update(id, data)', {
|
||||
sdk: function () { return sw.api.channels.update(channelId, { title: tag + '-updated' }); },
|
||||
raw: { method: 'PUT', path: '/channels/' + channelId, body: { title: tag + '-updated' } },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.channelFull, 'channel');
|
||||
T.assert(r.title === tag + '-updated', 'title not updated');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Messages ──
|
||||
|
||||
await T.test('channels', 'messages', 'sw.api.channels.messages(id) returns array', {
|
||||
sdk: function () { return sw.api.channels.messages(channelId); },
|
||||
raw: { method: 'GET', path: '/channels/' + channelId + '/messages' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Send message ──
|
||||
|
||||
var msgId = null;
|
||||
await T.test('channels', 'messages', 'sw.api.channels.send(id, data)', {
|
||||
sdk: function () {
|
||||
return sw.api.channels.send(channelId, { content: 'sdk-test-msg', role: 'user' });
|
||||
},
|
||||
raw: { method: 'POST', path: '/channels/' + channelId + '/messages',
|
||||
body: { content: 'sdk-test-msg', role: 'user' } },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.message, 'message');
|
||||
msgId = r.id;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Siblings ──
|
||||
|
||||
if (msgId) {
|
||||
await T.test('channels', 'messages', 'sw.api.channels.siblings(id, msgId)', {
|
||||
sdk: function () { return sw.api.channels.siblings(channelId, msgId); },
|
||||
raw: { method: 'GET', path: '/channels/' + channelId + '/messages/' + msgId + '/siblings' },
|
||||
validate: function (r) {
|
||||
// Siblings returns { siblings: [...], current_index, total }
|
||||
var arr = r.siblings || T.unwrapList(r);
|
||||
T.assert(Array.isArray(arr), 'expected siblings array');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Path ──
|
||||
|
||||
await T.test('channels', 'tree', 'sw.api.channels.path(id)', {
|
||||
sdk: function () { return sw.api.channels.path(channelId); },
|
||||
raw: { method: 'GET', path: '/channels/' + channelId + '/path' },
|
||||
validate: function (r) { T.assert(Array.isArray(r), 'expected array'); }
|
||||
});
|
||||
|
||||
// ── Mark Read ──
|
||||
|
||||
await T.test('channels', 'read', 'sw.api.channels.markRead(id)', {
|
||||
sdk: function () { return sw.api.channels.markRead(channelId); },
|
||||
raw: { method: 'POST', path: '/channels/' + channelId + '/mark-read', body: {} },
|
||||
validate: function () { /* 200 OK is sufficient */ }
|
||||
});
|
||||
|
||||
// ── KBs ──
|
||||
|
||||
await T.test('channels', 'kbs', 'sw.api.channels.kbs(id) returns array', {
|
||||
sdk: function () { return sw.api.channels.kbs(channelId); },
|
||||
raw: { method: 'GET', path: '/channels/' + channelId + '/knowledge-bases' },
|
||||
validate: function (r) { T.assert(Array.isArray(r), 'expected array'); }
|
||||
});
|
||||
|
||||
// ── Files ──
|
||||
|
||||
await T.test('channels', 'files', 'sw.api.channels.files(id)', {
|
||||
sdk: function () { return sw.api.channels.files(channelId); },
|
||||
raw: { method: 'GET', path: '/channels/' + channelId + '/files' },
|
||||
validate: function (r) {
|
||||
// May be array or {files: [], count: N}
|
||||
T.assert(typeof r === 'object' || Array.isArray(r), 'expected object or array');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Participants ──
|
||||
|
||||
await T.test('channels', 'participants', 'sw.api.channels.participants(id)', {
|
||||
sdk: function () { return sw.api.channels.participants(channelId); },
|
||||
raw: { method: 'GET', path: '/channels/' + channelId + '/participants' },
|
||||
validate: function (r) { T.assert(Array.isArray(r), 'expected array'); }
|
||||
});
|
||||
|
||||
// ── Models ──
|
||||
|
||||
await T.test('channels', 'models', 'sw.api.channels.models(id)', {
|
||||
sdk: function () { return sw.api.channels.models(channelId); },
|
||||
raw: { method: 'GET', path: '/channels/' + channelId + '/models' },
|
||||
validate: function (r) { T.assert(Array.isArray(r), 'expected array'); }
|
||||
});
|
||||
|
||||
// ── Delete ──
|
||||
|
||||
await T.test('channels', 'crud', 'sw.api.channels.del(id)', {
|
||||
sdk: function () { return sw.api.channels.del(channelId); },
|
||||
raw: { method: 'DELETE', path: '/channels/' + channelId },
|
||||
validate: function () { channelId = null; }
|
||||
});
|
||||
});
|
||||
})();
|
||||
82
packages/sdk-test-runner/js/domains/knowledge.js
Normal file
82
packages/sdk-test-runner/js/domains/knowledge.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* SDK Test Runner — Domain: knowledge
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.SDKR;
|
||||
if (!T) return;
|
||||
|
||||
T.registerDomain('knowledge', async function () {
|
||||
var tag = 'sdkr-' + Date.now();
|
||||
var kbId = null;
|
||||
|
||||
await T.test('knowledge', 'list', 'sw.api.knowledge.list()', {
|
||||
sdk: function () { return sw.api.knowledge.list(); },
|
||||
raw: { method: 'GET', path: '/knowledge-bases' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
await T.test('knowledge', 'crud', 'sw.api.knowledge.create()', {
|
||||
sdk: function () {
|
||||
return sw.api.knowledge.create({ name: tag + '-kb', scope: 'personal' });
|
||||
},
|
||||
raw: { method: 'POST', path: '/knowledge-bases', body: { name: tag + '-kb', scope: 'personal' } },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.kb, 'kb');
|
||||
kbId = r.id;
|
||||
T.registerCleanup(function () { if (kbId) return T.safeDelete('/knowledge-bases/' + kbId); });
|
||||
}
|
||||
});
|
||||
|
||||
if (!kbId) return;
|
||||
|
||||
await T.test('knowledge', 'crud', 'sw.api.knowledge.get(id)', {
|
||||
sdk: function () { return sw.api.knowledge.get(kbId); },
|
||||
raw: { method: 'GET', path: '/knowledge-bases/' + kbId },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.kb, 'kb');
|
||||
T.assert(r.id === kbId, 'id mismatch');
|
||||
}
|
||||
});
|
||||
|
||||
await T.test('knowledge', 'crud', 'sw.api.knowledge.update(id, data)', {
|
||||
sdk: function () { return sw.api.knowledge.update(kbId, { name: tag + '-kb-upd' }); },
|
||||
raw: { method: 'PUT', path: '/knowledge-bases/' + kbId, body: { name: tag + '-kb-upd' } },
|
||||
validate: function (r) { T.assertShape(r, T.S.kb, 'kb'); }
|
||||
});
|
||||
|
||||
// ── Discoverable ──
|
||||
|
||||
await T.test('knowledge', 'discover', 'sw.api.knowledge.discoverable()', {
|
||||
sdk: function () { return sw.api.knowledge.discoverable(); },
|
||||
raw: { method: 'GET', path: '/knowledge-bases-discoverable' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Documents ──
|
||||
|
||||
await T.test('knowledge', 'docs', 'sw.api.knowledge.documents(id)', {
|
||||
sdk: function () { return sw.api.knowledge.documents(kbId); },
|
||||
raw: { method: 'GET', path: '/knowledge-bases/' + kbId + '/documents' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Search ──
|
||||
|
||||
// Search requires an embedding provider — only run if fixture provisioned one
|
||||
var hasEmbedding = T.fixtures && T.fixtures.embeddingModel;
|
||||
await T.test('knowledge', 'search', 'sw.api.knowledge.search(id, query)', {
|
||||
sdk: function () { return sw.api.knowledge.search(kbId, 'test', 5); },
|
||||
raw: { method: 'POST', path: '/knowledge-bases/' + kbId + '/search',
|
||||
body: { query: 'test', limit: 5 } },
|
||||
validate: function (r) { T.assert(typeof r === 'object', 'expected object'); },
|
||||
skip: hasEmbedding ? null : 'no embedding provider configured — set up a provider with embedding models'
|
||||
});
|
||||
|
||||
await T.test('knowledge', 'crud', 'sw.api.knowledge.del(id)', {
|
||||
sdk: function () { return sw.api.knowledge.del(kbId); },
|
||||
raw: { method: 'DELETE', path: '/knowledge-bases/' + kbId },
|
||||
validate: function () { kbId = null; }
|
||||
});
|
||||
});
|
||||
})();
|
||||
196
packages/sdk-test-runner/js/domains/misc.js
Normal file
196
packages/sdk-test-runner/js/domains/misc.js
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* SDK Test Runner — Domain: misc
|
||||
*
|
||||
* Covers smaller domains: notifications, folders, profile,
|
||||
* data portability, health, models, users, presence, usage, groups,
|
||||
* tools, git.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.SDKR;
|
||||
if (!T) return;
|
||||
|
||||
T.registerDomain('misc', async function () {
|
||||
var tag = 'sdkr-' + Date.now();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// HEALTH
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('misc', 'health', 'sw.api.health()', {
|
||||
sdk: function () { return sw.api.health(); },
|
||||
raw: { method: 'GET', path: '/health' },
|
||||
validate: function (r) { T.assert(r.status === 'ok', 'status should be ok'); }
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// PROFILE
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('misc', 'profile', 'sw.api.profile.get()', {
|
||||
sdk: function () { return sw.api.profile.get(); },
|
||||
raw: { method: 'GET', path: '/profile' },
|
||||
validate: function (r) { T.assertShape(r, T.S.profile, 'profile'); }
|
||||
});
|
||||
|
||||
await T.test('misc', 'profile', 'sw.api.profile.permissions()', {
|
||||
sdk: function () {
|
||||
if (sw.api.profile.permissions) return sw.api.profile.permissions();
|
||||
throw new Error('sw.api.profile.permissions not defined');
|
||||
},
|
||||
raw: { method: 'GET', path: '/profile/permissions' },
|
||||
validate: function (r) {
|
||||
T.assert(Array.isArray(r.permissions), 'permissions should be array');
|
||||
}
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// MODELS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('misc', 'models', 'sw.api.models.enabled()', {
|
||||
sdk: function () {
|
||||
if (sw.api.models && sw.api.models.enabled) return sw.api.models.enabled();
|
||||
throw new Error('sw.api.models.enabled not defined');
|
||||
},
|
||||
raw: { method: 'GET', path: '/models/enabled' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// FOLDERS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
var folderId = null;
|
||||
|
||||
await T.test('misc', 'folders', 'sw.api.folders.list()', {
|
||||
sdk: function () { return sw.api.folders.list(); },
|
||||
raw: { method: 'GET', path: '/folders' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
await T.test('misc', 'folders', 'sw.api.folders.create()', {
|
||||
sdk: function () {
|
||||
return sw.api.folders.create(tag + '-folder');
|
||||
},
|
||||
raw: { method: 'POST', path: '/folders', body: { name: tag + '-folder', parent_id: null, sort_order: 0 } },
|
||||
validate: function (r) {
|
||||
// Backend returns { folder: {...} } — SDK passes envelope through
|
||||
var obj = r.folder || r;
|
||||
T.assertShape(obj, T.S.folder, 'folder');
|
||||
folderId = obj.id;
|
||||
T.registerCleanup(function () { if (folderId) return T.safeDelete('/folders/' + folderId); });
|
||||
}
|
||||
});
|
||||
|
||||
if (folderId) {
|
||||
await T.test('misc', 'folders', 'sw.api.folders.update(id, data)', {
|
||||
sdk: function () { return sw.api.folders.update(folderId, { name: tag + '-fold-upd' }); },
|
||||
raw: { method: 'PUT', path: '/folders/' + folderId, body: { name: tag + '-fold-upd' } },
|
||||
validate: function (r) { T.assert(r && (r.ok || r.id || r.folder), 'expected ok or folder'); }
|
||||
});
|
||||
|
||||
await T.test('misc', 'folders', 'sw.api.folders.del(id)', {
|
||||
sdk: function () { return sw.api.folders.del(folderId); },
|
||||
raw: { method: 'DELETE', path: '/folders/' + folderId },
|
||||
validate: function () { folderId = null; }
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// NOTIFICATIONS (duplicate key bug fixed — prefs/setPref/delPref restored)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('misc', 'notifications', 'sw.api.notifications.list()', {
|
||||
sdk: function () { return sw.api.notifications.list(); },
|
||||
raw: { method: 'GET', path: '/notifications' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
await T.test('misc', 'notifications', 'sw.api.notifications.unreadCount()', {
|
||||
sdk: function () { return sw.api.notifications.unreadCount(); },
|
||||
raw: { method: 'GET', path: '/notifications/unread-count' },
|
||||
validate: function (r) { T.assert(typeof r === 'object', 'expected object'); }
|
||||
});
|
||||
|
||||
await T.test('misc', 'notifications', 'sw.api.notifications.prefs()', {
|
||||
sdk: function () {
|
||||
if (typeof sw.api.notifications.prefs === 'function') return sw.api.notifications.prefs();
|
||||
throw new Error('sw.api.notifications.prefs is ' + typeof sw.api.notifications.prefs);
|
||||
},
|
||||
raw: { method: 'GET', path: '/notifications/preferences' },
|
||||
validate: function () { /* existence is sufficient */ }
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// USERS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('misc', 'users', 'sw.api.users.search(q)', {
|
||||
sdk: function () { return sw.api.users.search('admin'); },
|
||||
raw: { method: 'GET', path: '/users/search?q=admin' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// PRESENCE
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('misc', 'presence', 'sw.api.presence.heartbeat()', {
|
||||
sdk: function () { return sw.api.presence.heartbeat(); },
|
||||
raw: { method: 'POST', path: '/presence/heartbeat', body: {} },
|
||||
validate: function () { /* 200 OK is sufficient */ }
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// USAGE
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('misc', 'usage', 'sw.api.usage.mine()', {
|
||||
sdk: function () { return sw.api.usage.mine(); },
|
||||
raw: { method: 'GET', path: '/usage' },
|
||||
validate: function (r) { T.assert(typeof r === 'object' || Array.isArray(r), 'expected object or array'); }
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// GROUPS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('misc', 'groups', 'sw.api.groups.mine()', {
|
||||
sdk: function () { return sw.api.groups.mine(); },
|
||||
raw: { method: 'GET', path: '/groups/mine' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// TOOLS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('misc', 'tools', 'sw.api.tools.list()', {
|
||||
sdk: function () { return sw.api.tools.list(); },
|
||||
raw: { method: 'GET', path: '/tools' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// DATA PORTABILITY (KNOWN BUG: deleteAccount wrong method+path)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('misc', 'export', 'sw.api.dataPortability.exportMe()', {
|
||||
sdk: function () { return sw.api.dataPortability.exportMe(); },
|
||||
raw: { method: 'GET', path: '/export/me' },
|
||||
validate: function (r) { T.assert(typeof r === 'object', 'expected object'); },
|
||||
skip: 'empty DB export may 500 — expected in dev'
|
||||
});
|
||||
|
||||
// deleteAccount: SDK uses POST /profile/delete, backend is DELETE /me
|
||||
// We don't actually call delete — just verify the method exists
|
||||
await T.test('misc', 'export', 'sw.api.dataPortability.deleteAccount exists', async function () {
|
||||
T.assert(typeof sw.api.dataPortability.deleteAccount === 'function',
|
||||
'deleteAccount should be a function');
|
||||
// NOTE: SDK calls POST /profile/delete — backend is DELETE /me.
|
||||
// This is a known SDK_BUG from the ICD drift audit (§A).
|
||||
// We don't actually invoke it because it would delete the account.
|
||||
});
|
||||
});
|
||||
})();
|
||||
106
packages/sdk-test-runner/js/domains/notes.js
Normal file
106
packages/sdk-test-runner/js/domains/notes.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* SDK Test Runner — Domain: notes
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.SDKR;
|
||||
if (!T) return;
|
||||
|
||||
T.registerDomain('notes', async function () {
|
||||
var tag = 'sdkr-' + Date.now();
|
||||
var noteId = null;
|
||||
|
||||
// ── List ──
|
||||
|
||||
await T.test('notes', 'list', 'sw.api.notes.list()', {
|
||||
sdk: function () { return sw.api.notes.list(); },
|
||||
raw: { method: 'GET', path: '/notes' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Create ──
|
||||
|
||||
await T.test('notes', 'crud', 'sw.api.notes.create()', {
|
||||
sdk: function () {
|
||||
return sw.api.notes.create({ title: tag + '-note', content: '# Test\nBody text' });
|
||||
},
|
||||
raw: { method: 'POST', path: '/notes', body: { title: tag + '-note', content: '# Test\nBody text' } },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.note, 'note');
|
||||
noteId = r.id;
|
||||
T.registerCleanup(function () { if (noteId) return T.safeDelete('/notes/' + noteId); });
|
||||
}
|
||||
});
|
||||
|
||||
if (!noteId) return;
|
||||
|
||||
// ── Get ──
|
||||
|
||||
await T.test('notes', 'crud', 'sw.api.notes.get(id)', {
|
||||
sdk: function () { return sw.api.notes.get(noteId); },
|
||||
raw: { method: 'GET', path: '/notes/' + noteId },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.note, 'note');
|
||||
T.assert(r.id === noteId, 'id mismatch');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Update ──
|
||||
|
||||
await T.test('notes', 'crud', 'sw.api.notes.update(id, data)', {
|
||||
sdk: function () { return sw.api.notes.update(noteId, { title: tag + '-note-upd', content: 'updated' }); },
|
||||
raw: { method: 'PUT', path: '/notes/' + noteId, body: { title: tag + '-note-upd', content: 'updated' } },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.note, 'note');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Search ──
|
||||
|
||||
await T.test('notes', 'search', 'sw.api.notes.search(query)', {
|
||||
sdk: function () { return sw.api.notes.search(tag, 5); },
|
||||
raw: { method: 'GET', path: '/notes/search?q=' + encodeURIComponent(tag) + '&limit=5' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Search Titles ──
|
||||
|
||||
await T.test('notes', 'search', 'sw.api.notes.searchTitles(query)', {
|
||||
sdk: function () { return sw.api.notes.searchTitles(tag, 5); },
|
||||
raw: { method: 'GET', path: '/notes/search-titles?q=' + encodeURIComponent(tag) + '&limit=5' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Folders ──
|
||||
|
||||
await T.test('notes', 'folders', 'sw.api.notes.folders()', {
|
||||
sdk: function () { return sw.api.notes.folders(); },
|
||||
raw: { method: 'GET', path: '/notes/folders' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Backlinks ──
|
||||
|
||||
await T.test('notes', 'links', 'sw.api.notes.backlinks(id)', {
|
||||
sdk: function () { return sw.api.notes.backlinks(noteId); },
|
||||
raw: { method: 'GET', path: '/notes/' + noteId + '/backlinks' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Graph ──
|
||||
|
||||
await T.test('notes', 'graph', 'sw.api.notes.graph()', {
|
||||
sdk: function () { return sw.api.notes.graph(); },
|
||||
raw: { method: 'GET', path: '/notes/graph' },
|
||||
validate: function (r) { T.assert(typeof r === 'object' || Array.isArray(r), 'expected object or array'); }
|
||||
});
|
||||
|
||||
// ── Delete ──
|
||||
|
||||
await T.test('notes', 'crud', 'sw.api.notes.del(id)', {
|
||||
sdk: function () { return sw.api.notes.del(noteId); },
|
||||
raw: { method: 'DELETE', path: '/notes/' + noteId },
|
||||
validate: function () { noteId = null; }
|
||||
});
|
||||
});
|
||||
})();
|
||||
66
packages/sdk-test-runner/js/domains/personas.js
Normal file
66
packages/sdk-test-runner/js/domains/personas.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* SDK Test Runner — Domain: personas
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.SDKR;
|
||||
if (!T) return;
|
||||
|
||||
T.registerDomain('personas', async function () {
|
||||
var tag = 'sdkr-' + Date.now();
|
||||
var personaId = null;
|
||||
|
||||
// Policy: allow_user_personas is toggled by fixtures.js.
|
||||
// If fixtures didn't run (or user isn't admin), create will 403.
|
||||
|
||||
await T.test('personas', 'list', 'sw.api.personas.list()', {
|
||||
sdk: function () { return sw.api.personas.list(); },
|
||||
raw: { method: 'GET', path: '/personas' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
await T.test('personas', 'crud', 'sw.api.personas.create()', {
|
||||
sdk: function () {
|
||||
return sw.api.personas.create({
|
||||
name: tag + '-persona', system_prompt: 'You are a test bot.',
|
||||
scope: 'personal'
|
||||
});
|
||||
},
|
||||
raw: { method: 'POST', path: '/personas', body: {
|
||||
name: tag + '-persona', system_prompt: 'You are a test bot.',
|
||||
scope: 'personal'
|
||||
}},
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.persona, 'persona');
|
||||
personaId = r.id;
|
||||
T.registerCleanup(function () { if (personaId) return T.safeDelete('/personas/' + personaId); });
|
||||
}
|
||||
});
|
||||
|
||||
if (!personaId) return;
|
||||
|
||||
// NOTE: User-scoped personas only have LIST + CREATE routes.
|
||||
// GET/PUT/DELETE /personas/:id are admin-only (ICD gap).
|
||||
// Use admin endpoints for full CRUD testing.
|
||||
|
||||
await T.test('personas', 'crud', 'sw.api.admin.personas.update(id, data)', {
|
||||
sdk: function () { return sw.api.admin.personas.update(personaId, { name: tag + '-pers-upd' }); },
|
||||
raw: { method: 'PUT', path: '/admin/personas/' + personaId, body: { name: tag + '-pers-upd' } },
|
||||
validate: function (r) { T.assert(typeof r === 'object', 'expected object'); }
|
||||
});
|
||||
|
||||
// ── KBs ──
|
||||
|
||||
await T.test('personas', 'kbs', 'sw.api.admin.personas.kbs(id)', {
|
||||
sdk: function () { return sw.api.admin.personas.kbs(personaId); },
|
||||
raw: { method: 'GET', path: '/admin/personas/' + personaId + '/knowledge-bases' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
await T.test('personas', 'crud', 'sw.api.admin.personas.del(id)', {
|
||||
sdk: function () { return sw.api.admin.personas.del(personaId); },
|
||||
raw: { method: 'DELETE', path: '/admin/personas/' + personaId },
|
||||
validate: function () { personaId = null; }
|
||||
});
|
||||
});
|
||||
})();
|
||||
134
packages/sdk-test-runner/js/domains/projects.js
Normal file
134
packages/sdk-test-runner/js/domains/projects.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* SDK Test Runner — Domain: projects
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.SDKR;
|
||||
if (!T) return;
|
||||
|
||||
T.registerDomain('projects', async function () {
|
||||
var tag = 'sdkr-' + Date.now();
|
||||
var projectId = null;
|
||||
var channelId = null;
|
||||
var noteId = null;
|
||||
|
||||
// ── List ──
|
||||
|
||||
await T.test('projects', 'list', 'sw.api.projects.list()', {
|
||||
sdk: function () { return sw.api.projects.list(); },
|
||||
raw: { method: 'GET', path: '/projects' },
|
||||
validate: function (r) { T.assert(Array.isArray(r), 'expected array'); }
|
||||
});
|
||||
|
||||
// ── Create ──
|
||||
|
||||
await T.test('projects', 'crud', 'sw.api.projects.create()', {
|
||||
sdk: function () {
|
||||
return sw.api.projects.create({ name: tag + '-proj', description: 'sdk test' });
|
||||
},
|
||||
raw: { method: 'POST', path: '/projects', body: { name: tag + '-proj', description: 'sdk test' } },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.project, 'project');
|
||||
projectId = r.id;
|
||||
T.registerCleanup(function () { if (projectId) return T.safeDelete('/projects/' + projectId); });
|
||||
}
|
||||
});
|
||||
|
||||
if (!projectId) return;
|
||||
|
||||
// ── Get ──
|
||||
|
||||
await T.test('projects', 'crud', 'sw.api.projects.get(id)', {
|
||||
sdk: function () { return sw.api.projects.get(projectId); },
|
||||
raw: { method: 'GET', path: '/projects/' + projectId },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.project, 'project');
|
||||
T.assert(r.id === projectId, 'id mismatch');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Update ──
|
||||
|
||||
await T.test('projects', 'crud', 'sw.api.projects.update(id, data)', {
|
||||
sdk: function () { return sw.api.projects.update(projectId, { name: tag + '-proj-upd' }); },
|
||||
raw: { method: 'PUT', path: '/projects/' + projectId, body: { name: tag + '-proj-upd' } },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.project, 'project');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Channel association ──
|
||||
|
||||
await T.test('projects', 'assoc', 'sw.api.projects.addChannel()', async function () {
|
||||
var ch = await T.raw.post('/channels', { title: tag + '-proj-ch', type: 'direct' });
|
||||
channelId = ch.id;
|
||||
T.registerCleanup(function () { if (channelId) return T.safeDelete('/channels/' + channelId); });
|
||||
});
|
||||
|
||||
if (channelId) {
|
||||
await T.test('projects', 'assoc', 'sw.api.projects.addChannel(id, chId)', {
|
||||
sdk: function () { return sw.api.projects.addChannel(projectId, channelId, 0); },
|
||||
raw: { method: 'POST', path: '/projects/' + projectId + '/channels',
|
||||
body: { channel_id: channelId, position: 0 } },
|
||||
validate: function () { /* ok */ }
|
||||
});
|
||||
|
||||
await T.test('projects', 'assoc', 'sw.api.projects.channels(id)', {
|
||||
sdk: function () { return sw.api.projects.channels(projectId); },
|
||||
raw: { method: 'GET', path: '/projects/' + projectId + '/channels' },
|
||||
validate: function (r) { T.assert(Array.isArray(r), 'expected array'); }
|
||||
});
|
||||
|
||||
await T.test('projects', 'assoc', 'sw.api.projects.removeChannel()', {
|
||||
sdk: function () { return sw.api.projects.removeChannel(projectId, channelId); },
|
||||
raw: { method: 'DELETE', path: '/projects/' + projectId + '/channels/' + channelId },
|
||||
validate: function () { /* ok */ }
|
||||
});
|
||||
}
|
||||
|
||||
// ── Note association ──
|
||||
|
||||
await T.test('projects', 'assoc', 'create scratch note for project', async function () {
|
||||
var n = await T.raw.post('/notes', { title: tag + '-proj-note', content: 'test' });
|
||||
noteId = n.id;
|
||||
T.registerCleanup(function () { if (noteId) return T.safeDelete('/notes/' + noteId); });
|
||||
});
|
||||
|
||||
if (noteId) {
|
||||
await T.test('projects', 'assoc', 'sw.api.projects.addNote()', {
|
||||
sdk: function () { return sw.api.projects.addNote(projectId, noteId); },
|
||||
raw: { method: 'POST', path: '/projects/' + projectId + '/notes',
|
||||
body: { note_id: noteId } },
|
||||
validate: function () { /* ok */ }
|
||||
});
|
||||
|
||||
await T.test('projects', 'assoc', 'sw.api.projects.notes(id)', {
|
||||
sdk: function () { return sw.api.projects.notes(projectId); },
|
||||
raw: { method: 'GET', path: '/projects/' + projectId + '/notes' },
|
||||
validate: function (r) { T.assert(Array.isArray(r), 'expected array'); }
|
||||
});
|
||||
|
||||
await T.test('projects', 'assoc', 'sw.api.projects.removeNote()', {
|
||||
sdk: function () { return sw.api.projects.removeNote(projectId, noteId); },
|
||||
raw: { method: 'DELETE', path: '/projects/' + projectId + '/notes/' + noteId },
|
||||
validate: function () { /* ok */ }
|
||||
});
|
||||
}
|
||||
|
||||
// ── Files ──
|
||||
|
||||
await T.test('projects', 'files', 'sw.api.projects.files(id)', {
|
||||
sdk: function () { return sw.api.projects.files(projectId); },
|
||||
raw: { method: 'GET', path: '/projects/' + projectId + '/files' },
|
||||
validate: function (r) { T.assert(typeof r === 'object' || Array.isArray(r), 'expected obj/arr'); }
|
||||
});
|
||||
|
||||
// ── Delete ──
|
||||
|
||||
await T.test('projects', 'crud', 'sw.api.projects.del(id)', {
|
||||
sdk: function () { return sw.api.projects.del(projectId); },
|
||||
raw: { method: 'DELETE', path: '/projects/' + projectId },
|
||||
validate: function () { projectId = null; }
|
||||
});
|
||||
});
|
||||
})();
|
||||
107
packages/sdk-test-runner/js/domains/tasks.js
Normal file
107
packages/sdk-test-runner/js/domains/tasks.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* SDK Test Runner — Domain: tasks
|
||||
*
|
||||
* KNOWN BUGS:
|
||||
* sw.api.tasks.start(id) → POST /tasks/:id/start (backend: /tasks/:id/run)
|
||||
* sw.api.tasks.stop(id) → POST /tasks/:id/stop (backend: /tasks/:id/kill)
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.SDKR;
|
||||
if (!T) return;
|
||||
|
||||
T.registerDomain('tasks', async function () {
|
||||
var tag = 'sdkr-' + Date.now();
|
||||
var taskId = null;
|
||||
|
||||
// ── List ──
|
||||
|
||||
await T.test('tasks', 'list', 'sw.api.tasks.list()', {
|
||||
sdk: function () { return sw.api.tasks.list(); },
|
||||
raw: { method: 'GET', path: '/tasks' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Create ──
|
||||
|
||||
await T.test('tasks', 'crud', 'sw.api.tasks.create()', {
|
||||
sdk: function () {
|
||||
return sw.api.tasks.create({
|
||||
name: tag + '-task', task_type: 'prompt',
|
||||
schedule: '@daily', user_prompt: 'SDK test prompt', is_active: false
|
||||
});
|
||||
},
|
||||
raw: { method: 'POST', path: '/tasks', body: {
|
||||
name: tag + '-task', task_type: 'prompt',
|
||||
schedule: '@daily', user_prompt: 'SDK test prompt', is_active: false
|
||||
}},
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.task, 'task');
|
||||
taskId = r.id;
|
||||
T.registerCleanup(function () { if (taskId) return T.safeDelete('/tasks/' + taskId); });
|
||||
}
|
||||
});
|
||||
|
||||
if (!taskId) return;
|
||||
|
||||
// ── Get ──
|
||||
|
||||
await T.test('tasks', 'crud', 'sw.api.tasks.get(id)', {
|
||||
sdk: function () { return sw.api.tasks.get(taskId); },
|
||||
raw: { method: 'GET', path: '/tasks/' + taskId },
|
||||
validate: function (r) {
|
||||
T.assert(r.id === taskId, 'id mismatch');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Update ──
|
||||
|
||||
await T.test('tasks', 'crud', 'sw.api.tasks.update(id, data)', {
|
||||
sdk: function () { return sw.api.tasks.update(taskId, { name: tag + '-task-upd' }); },
|
||||
raw: { method: 'PUT', path: '/tasks/' + taskId, body: { name: tag + '-task-upd' } },
|
||||
validate: function (r) { T.assert(typeof r === 'object', 'expected object'); }
|
||||
});
|
||||
|
||||
// ── Start ──
|
||||
|
||||
await T.test('tasks', 'lifecycle', 'sw.api.tasks.start(id)', {
|
||||
sdk: function () {
|
||||
if (sw.api.tasks.start) return sw.api.tasks.start(taskId);
|
||||
throw new Error('sw.api.tasks.start not defined');
|
||||
},
|
||||
raw: { method: 'POST', path: '/tasks/' + taskId + '/run', body: {} },
|
||||
validate: function () { /* 200/202 is sufficient */ }
|
||||
});
|
||||
|
||||
// ── Stop ── (404 expected when no active run)
|
||||
|
||||
await T.test('tasks', 'lifecycle', 'sw.api.tasks.stop(id)', {
|
||||
sdk: function () {
|
||||
if (sw.api.tasks.stop) return sw.api.tasks.stop(taskId);
|
||||
throw new Error('sw.api.tasks.stop not defined');
|
||||
},
|
||||
raw: { method: 'POST', path: '/tasks/' + taskId + '/kill', body: {} },
|
||||
validate: function () { /* 200/202 is sufficient */ },
|
||||
skip: 'no active run to cancel — 404 expected without task execution'
|
||||
});
|
||||
|
||||
// ── Runs ──
|
||||
|
||||
await T.test('tasks', 'runs', 'sw.api.tasks.runs(id)', {
|
||||
sdk: function () {
|
||||
if (sw.api.tasks.runs) return sw.api.tasks.runs(taskId);
|
||||
throw new Error('sw.api.tasks.runs not defined');
|
||||
},
|
||||
raw: { method: 'GET', path: '/tasks/' + taskId + '/runs' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Delete ──
|
||||
|
||||
await T.test('tasks', 'crud', 'sw.api.tasks.del(id)', {
|
||||
sdk: function () { return sw.api.tasks.del(taskId); },
|
||||
raw: { method: 'DELETE', path: '/tasks/' + taskId },
|
||||
validate: function () { taskId = null; }
|
||||
});
|
||||
});
|
||||
})();
|
||||
93
packages/sdk-test-runner/js/domains/workflows.js
Normal file
93
packages/sdk-test-runner/js/domains/workflows.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* SDK Test Runner — Domain: workflows
|
||||
*
|
||||
* Tests: list, create, get, update, delete, stages.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.SDKR;
|
||||
if (!T) return;
|
||||
|
||||
T.registerDomain('workflows', async function () {
|
||||
var tag = 'sdkr-' + Date.now();
|
||||
var wfId = null;
|
||||
|
||||
// ── List ──
|
||||
|
||||
await T.test('workflows', 'list', 'sw.api.workflows.list() returns array', {
|
||||
sdk: function () { return sw.api.workflows.list(); },
|
||||
raw: { method: 'GET', path: '/workflows' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Create ──
|
||||
|
||||
await T.test('workflows', 'crud', 'sw.api.workflows.create()', {
|
||||
sdk: function () {
|
||||
return sw.api.workflows.create({
|
||||
name: tag + '-wf', slug: tag + '-wf',
|
||||
entry_mode: 'team_only', scope: 'personal'
|
||||
});
|
||||
},
|
||||
raw: { method: 'POST', path: '/workflows', body: {
|
||||
name: tag + '-wf', slug: tag + '-wf',
|
||||
entry_mode: 'team_only', scope: 'personal'
|
||||
}},
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.workflow, 'workflow');
|
||||
wfId = r.id;
|
||||
T.registerCleanup(function () { if (wfId) return T.safeDelete('/workflows/' + wfId); });
|
||||
}
|
||||
});
|
||||
|
||||
if (!wfId) return;
|
||||
|
||||
// ── Get ──
|
||||
|
||||
await T.test('workflows', 'crud', 'sw.api.workflows.get(id)', {
|
||||
sdk: function () { return sw.api.workflows.get(wfId); },
|
||||
raw: { method: 'GET', path: '/workflows/' + wfId },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.workflow, 'workflow');
|
||||
T.assert(r.id === wfId, 'id mismatch');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Update (KNOWN BUG: SDK uses PUT, backend expects PATCH) ──
|
||||
|
||||
await T.test('workflows', 'crud', 'sw.api.workflows.update(id, data)', {
|
||||
sdk: function () {
|
||||
return sw.api.workflows.update(wfId, { name: tag + '-wf-updated' });
|
||||
},
|
||||
raw: { method: 'PATCH', path: '/workflows/' + wfId,
|
||||
body: { name: tag + '-wf-updated' } },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.workflow, 'workflow');
|
||||
T.assert(r.name === tag + '-wf-updated', 'name not updated');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Stages ──
|
||||
|
||||
var stageId = null;
|
||||
|
||||
// stages.list via SDK (if wired) vs raw
|
||||
await T.test('workflows', 'stages', 'GET /workflows/:id/stages (raw — SDK may not have this)', {
|
||||
sdk: function () {
|
||||
// SDK may or may not expose stages — test raw path
|
||||
if (sw.api.workflows.stages) return sw.api.workflows.stages(wfId);
|
||||
throw new Error('sw.api.workflows.stages not defined');
|
||||
},
|
||||
raw: { method: 'GET', path: '/workflows/' + wfId + '/stages' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Delete ──
|
||||
|
||||
await T.test('workflows', 'crud', 'sw.api.workflows.del(id)', {
|
||||
sdk: function () { return sw.api.workflows.del(wfId); },
|
||||
raw: { method: 'DELETE', path: '/workflows/' + wfId },
|
||||
validate: function () { wfId = null; }
|
||||
});
|
||||
});
|
||||
})();
|
||||
83
packages/sdk-test-runner/js/domains/workspaces.js
Normal file
83
packages/sdk-test-runner/js/domains/workspaces.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* SDK Test Runner — Domain: workspaces
|
||||
*
|
||||
* Tests: list, create, get, update, delete, files.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.SDKR;
|
||||
if (!T) return;
|
||||
|
||||
T.registerDomain('workspaces', async function () {
|
||||
var tag = 'sdkr-' + Date.now();
|
||||
var wsId = null;
|
||||
|
||||
// Get current user for owner_type/owner_id
|
||||
var me = await sw.api.profile.get();
|
||||
var userId = me.id;
|
||||
|
||||
// ── List ──
|
||||
|
||||
await T.test('workspaces', 'list', 'sw.api.workspaces.list()', {
|
||||
sdk: function () { return sw.api.workspaces.list(); },
|
||||
raw: { method: 'GET', path: '/workspaces' },
|
||||
validate: function (r) { var arr = T.unwrapList(r); T.assert(arr.length >= 0, 'expected list'); }
|
||||
});
|
||||
|
||||
// ── Create ──
|
||||
|
||||
await T.test('workspaces', 'crud', 'sw.api.workspaces.create()', {
|
||||
sdk: function () {
|
||||
return sw.api.workspaces.create({ name: tag + '-ws', owner_type: 'user', owner_id: userId });
|
||||
},
|
||||
raw: { method: 'POST', path: '/workspaces', body: { name: tag + '-ws', owner_type: 'user', owner_id: userId } },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.workspace, 'workspace');
|
||||
wsId = r.id;
|
||||
T.registerCleanup(function () { if (wsId) return T.safeDelete('/workspaces/' + wsId); });
|
||||
}
|
||||
});
|
||||
|
||||
if (!wsId) return;
|
||||
|
||||
// ── Get ──
|
||||
|
||||
await T.test('workspaces', 'crud', 'sw.api.workspaces.get(id)', {
|
||||
sdk: function () { return sw.api.workspaces.get(wsId); },
|
||||
raw: { method: 'GET', path: '/workspaces/' + wsId },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.workspace, 'workspace');
|
||||
T.assert(r.id === wsId, 'id mismatch');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Update (KNOWN BUG: SDK uses PUT, backend expects PATCH) ──
|
||||
|
||||
await T.test('workspaces', 'crud', 'sw.api.workspaces.update(id, data)', {
|
||||
sdk: function () {
|
||||
return sw.api.workspaces.update(wsId, { name: tag + '-ws-updated' });
|
||||
},
|
||||
raw: { method: 'PATCH', path: '/workspaces/' + wsId,
|
||||
body: { name: tag + '-ws-updated' } },
|
||||
validate: function (r) {
|
||||
T.assertShape(r, T.S.workspace, 'workspace');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Files list ──
|
||||
|
||||
await T.test('workspaces', 'files', 'sw.api.workspaces.files(id)', {
|
||||
sdk: function () { return sw.api.workspaces.files(wsId); },
|
||||
raw: { method: 'GET', path: '/workspaces/' + wsId + '/files' },
|
||||
validate: function (r) { T.assert(typeof r === 'object' || Array.isArray(r), 'expected object or array'); }
|
||||
});
|
||||
|
||||
// ── Delete ──
|
||||
|
||||
await T.test('workspaces', 'crud', 'sw.api.workspaces.del(id)', {
|
||||
sdk: function () { return sw.api.workspaces.del(wsId); },
|
||||
raw: { method: 'DELETE', path: '/workspaces/' + wsId },
|
||||
validate: function () { wsId = null; }
|
||||
});
|
||||
});
|
||||
})();
|
||||
239
packages/sdk-test-runner/js/fixtures.js
Normal file
239
packages/sdk-test-runner/js/fixtures.js
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* SDK Test Runner — Fixtures
|
||||
*
|
||||
* Provisions reusable test resources before domains run and tears
|
||||
* them down afterward. Modeled on the ICD test runner's fixture
|
||||
* pattern.
|
||||
*
|
||||
* Fixture resources (all optional — domains degrade gracefully):
|
||||
*
|
||||
* providerConfig Global API config created from the first admin
|
||||
* config that has at least one embedding model.
|
||||
* Enables: knowledge.search, model preference tests.
|
||||
*
|
||||
* embeddingModel {model_id, provider_config_id} picked from the
|
||||
* enabled models list after the config is created.
|
||||
*
|
||||
* policies Policy toggles applied at setup, reverted at
|
||||
* teardown (e.g. allow_user_personas).
|
||||
*
|
||||
* team Test team (with current user as admin).
|
||||
*
|
||||
* Access: window.SDKR.fixtures.*
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.SDKR;
|
||||
if (!T) return;
|
||||
|
||||
T.fixtures = {
|
||||
ready: false,
|
||||
providerConfig: null, // { id, name, provider }
|
||||
embeddingModel: null, // { model_id, provider_config_id }
|
||||
team: null, // { id, name }
|
||||
policies: {}, // key → original value (for revert)
|
||||
errors: []
|
||||
};
|
||||
|
||||
// ─── Policy helpers ──────────────────────────────────────
|
||||
|
||||
async function setPolicy(key, value) {
|
||||
var fix = T.fixtures;
|
||||
try {
|
||||
// Save original for revert
|
||||
if (!(key in fix.policies)) {
|
||||
try {
|
||||
var settings = await T.raw.get('/admin/settings');
|
||||
fix.policies[key] = settings.policies?.[key] ?? null;
|
||||
} catch (_) {
|
||||
fix.policies[key] = null;
|
||||
}
|
||||
}
|
||||
await T.raw.put('/admin/settings/' + key, { value: String(value) });
|
||||
} catch (e) {
|
||||
fix.errors.push('policy ' + key + ': ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function revertPolicies() {
|
||||
var fix = T.fixtures;
|
||||
var keys = Object.keys(fix.policies);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var val = fix.policies[keys[i]];
|
||||
try {
|
||||
await T.raw.put('/admin/settings/' + keys[i], {
|
||||
value: val === null ? 'false' : String(val)
|
||||
});
|
||||
} catch (_) { /* best effort */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Provider / model discovery ──────────────────────────
|
||||
|
||||
async function provisionProvider() {
|
||||
var fix = T.fixtures;
|
||||
|
||||
// ── Read UI inputs (provider + API key) ──
|
||||
var ui = T._fixtureInput || {};
|
||||
var selectedProvider = ui.provSelect ? ui.provSelect.value : '';
|
||||
var apiKey = ui.keyInput ? ui.keyInput.value.trim() : '';
|
||||
var statusEl = ui.statusEl;
|
||||
|
||||
function setStatus(msg) {
|
||||
if (statusEl) statusEl.textContent = msg;
|
||||
console.log('[SDKR:fixtures] ' + msg);
|
||||
}
|
||||
|
||||
// Check if any configs already exist
|
||||
try {
|
||||
var configs = await T.raw.get('/admin/configs');
|
||||
var list = configs.data || configs.configs || [];
|
||||
if (list.length > 0) {
|
||||
fix.providerConfig = { id: list[0].id, name: list[0].name, provider: list[0].provider };
|
||||
setStatus('Using existing config: ' + list[0].name);
|
||||
}
|
||||
} catch (e) {
|
||||
fix.errors.push('config list: ' + e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no config exists and user provided a provider + key, create one
|
||||
if (!fix.providerConfig && selectedProvider && apiKey) {
|
||||
try {
|
||||
var configData = {
|
||||
name: 'sdkr-fixture-' + selectedProvider,
|
||||
provider: selectedProvider,
|
||||
api_key: apiKey,
|
||||
scope: 'global'
|
||||
};
|
||||
setStatus('Creating ' + selectedProvider + ' config...');
|
||||
var created = await T.raw.post('/admin/configs', configData);
|
||||
if (created && created.id) {
|
||||
fix.providerConfig = { id: created.id, name: configData.name, provider: selectedProvider };
|
||||
fix._createdConfig = true; // mark for cleanup
|
||||
setStatus('Config created: ' + configData.name);
|
||||
T.registerCleanup(function () {
|
||||
if (fix._createdConfig && fix.providerConfig) {
|
||||
return T.safeDelete('/admin/configs/' + fix.providerConfig.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus('Config create failed: ' + e.message);
|
||||
fix.errors.push('config create: ' + e.message);
|
||||
}
|
||||
} else if (!fix.providerConfig && selectedProvider && !apiKey) {
|
||||
// Provider selected but no key — try creating without key (some providers
|
||||
// use server-side keys configured via env vars)
|
||||
try {
|
||||
var configData2 = {
|
||||
name: 'sdkr-fixture-' + selectedProvider,
|
||||
provider: selectedProvider,
|
||||
scope: 'global'
|
||||
};
|
||||
var created2 = await T.raw.post('/admin/configs', configData2);
|
||||
if (created2 && created2.id) {
|
||||
fix.providerConfig = { id: created2.id, name: configData2.name, provider: selectedProvider };
|
||||
fix._createdConfig = true;
|
||||
setStatus('Config created (server key): ' + configData2.name);
|
||||
T.registerCleanup(function () {
|
||||
if (fix._createdConfig && fix.providerConfig) {
|
||||
return T.safeDelete('/admin/configs/' + fix.providerConfig.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
setStatus('No config — skipping model tests');
|
||||
}
|
||||
} else if (!fix.providerConfig) {
|
||||
setStatus('No provider selected — model tests will be skipped');
|
||||
}
|
||||
|
||||
// Fetch models if we have a config
|
||||
if (fix.providerConfig) {
|
||||
try {
|
||||
await T.raw.post('/admin/models/fetch', {});
|
||||
// Wait briefly for async model fetch
|
||||
await new Promise(function (r) { setTimeout(r, 1500); });
|
||||
} catch (_) { /* fetch may fail if no key */ }
|
||||
|
||||
// Look for an embedding model
|
||||
try {
|
||||
var models = await T.raw.get('/models/enabled');
|
||||
var list2 = models.data || [];
|
||||
for (var m = 0; m < list2.length; m++) {
|
||||
var caps = list2[m].capabilities || {};
|
||||
if (caps.embedding || list2[m].model_type === 'embedding') {
|
||||
fix.embeddingModel = {
|
||||
model_id: list2[m].model_id,
|
||||
provider_config_id: list2[m].provider_config_id
|
||||
};
|
||||
console.log('[SDKR:fixtures] Embedding model: ' + list2[m].model_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (_) { /* no models available */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Team ────────────────────────────────────────────────
|
||||
|
||||
async function provisionTeam() {
|
||||
var fix = T.fixtures;
|
||||
try {
|
||||
var team = await T.raw.post('/admin/teams', {
|
||||
name: 'sdkr-fixture-' + Date.now(),
|
||||
description: 'SDK test runner fixture team'
|
||||
});
|
||||
if (team && team.id) {
|
||||
fix.team = { id: team.id, name: team.name };
|
||||
T.registerCleanup(function () {
|
||||
if (fix.team) return T.safeDelete('/admin/teams/' + fix.team.id);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
fix.errors.push('team: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Provision / Teardown ────────────────────────────────
|
||||
|
||||
T.provisionFixtures = async function () {
|
||||
var fix = T.fixtures;
|
||||
if (fix.ready) return;
|
||||
|
||||
console.log('[SDKR:fixtures] Provisioning...');
|
||||
|
||||
// Policies
|
||||
await setPolicy('allow_user_personas', 'true');
|
||||
|
||||
// Provider + models
|
||||
await provisionProvider();
|
||||
|
||||
// Team
|
||||
await provisionTeam();
|
||||
|
||||
fix.ready = true;
|
||||
|
||||
if (fix.errors.length > 0) {
|
||||
console.warn('[SDKR:fixtures] ' + fix.errors.length + ' errors:', fix.errors);
|
||||
}
|
||||
console.log('[SDKR:fixtures] Ready —',
|
||||
'config:', fix.providerConfig ? fix.providerConfig.name : 'none',
|
||||
'| embedding:', fix.embeddingModel ? fix.embeddingModel.model_id : 'none',
|
||||
'| team:', fix.team ? fix.team.name : 'none'
|
||||
);
|
||||
};
|
||||
|
||||
T.teardownFixtures = async function () {
|
||||
await revertPolicies();
|
||||
// Cleanup functions registered via T.registerCleanup handle
|
||||
// the config + team deletion.
|
||||
T.fixtures = {
|
||||
ready: false, providerConfig: null, embeddingModel: null,
|
||||
team: null, policies: {}, errors: []
|
||||
};
|
||||
console.log('[SDKR:fixtures] Torn down');
|
||||
};
|
||||
|
||||
})();
|
||||
357
packages/sdk-test-runner/js/framework.js
Normal file
357
packages/sdk-test-runner/js/framework.js
Normal file
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* 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 = ['sb_auth_' + env, 'sb_auth', 'sb_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; };
|
||||
|
||||
})();
|
||||
107
packages/sdk-test-runner/js/main.js
Normal file
107
packages/sdk-test-runner/js/main.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* SDK Test Runner — Entry Point
|
||||
*
|
||||
* Load order:
|
||||
* 1. framework.js — dual-path harness, assertions, raw fetch
|
||||
* 2. shapes.js — ICD response shapes
|
||||
* 3. domains/*.js — per-domain test suites (register on T.domains)
|
||||
* 4. ui.js — render functions
|
||||
* 5. (this file) — boot
|
||||
*/
|
||||
(async function () {
|
||||
'use strict';
|
||||
|
||||
var mount = document.getElementById('extension-mount');
|
||||
if (!mount) return;
|
||||
|
||||
// ─── Boot SDK ──────────────────────────────────────────────
|
||||
|
||||
try {
|
||||
var base = window.__BASE__ || '';
|
||||
var ver = window.__VERSION__ || '0';
|
||||
|
||||
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('[SDKR] SDK booted — window.sw ready');
|
||||
} catch (e) {
|
||||
console.warn('[SDKR] SDK boot failed:', e.message);
|
||||
}
|
||||
|
||||
// ─── Shared Namespace ──────────────────────────────────────
|
||||
|
||||
window.SDKR = {
|
||||
mount: mount,
|
||||
manifest: window.__MANIFEST__ || {},
|
||||
base: window.__BASE__ || '',
|
||||
// Populated by framework.js
|
||||
results: [],
|
||||
cleanup: [],
|
||||
stats: {},
|
||||
domains: {},
|
||||
S: {}
|
||||
};
|
||||
|
||||
// ─── Script Loader ─────────────────────────────────────────
|
||||
|
||||
var surfaceId = window.SDKR.manifest.id || 'sdk-test-runner';
|
||||
var assetBase = '/surfaces/' + surfaceId + '/js/';
|
||||
|
||||
if (window.SDKR.base) {
|
||||
assetBase = window.SDKR.base + assetBase;
|
||||
}
|
||||
|
||||
var modules = [
|
||||
'framework.js',
|
||||
'shapes.js',
|
||||
'fixtures.js',
|
||||
// Domain test suites
|
||||
'domains/channels.js',
|
||||
'domains/notes.js',
|
||||
'domains/projects.js',
|
||||
'domains/personas.js',
|
||||
'domains/knowledge.js',
|
||||
'domains/workflows.js',
|
||||
'domains/workspaces.js',
|
||||
'domains/tasks.js',
|
||||
'domains/misc.js',
|
||||
'domains/admin.js',
|
||||
// UI (must come last)
|
||||
'ui.js'
|
||||
];
|
||||
|
||||
var loaded = 0;
|
||||
|
||||
function loadNext() {
|
||||
if (loaded >= modules.length) {
|
||||
boot();
|
||||
return;
|
||||
}
|
||||
var script = document.createElement('script');
|
||||
script.src = assetBase + modules[loaded] + '?v=' + (window.__VERSION__ || '0') + '.' + Date.now();
|
||||
script.onload = function () { loaded++; loadNext(); };
|
||||
script.onerror = function () {
|
||||
console.error('[SDKR] Failed to load: ' + modules[loaded]);
|
||||
loaded++; loadNext();
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
function boot() {
|
||||
if (typeof window.SDKR.renderShell === 'function') {
|
||||
window.SDKR.renderShell();
|
||||
} else {
|
||||
mount.innerHTML = '<p style="color:var(--danger);padding:24px;">SDK Test Runner failed to load modules. Check console.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
loadNext();
|
||||
})();
|
||||
112
packages/sdk-test-runner/js/shapes.js
Normal file
112
packages/sdk-test-runner/js/shapes.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* SDK Test Runner — ICD Response Shapes
|
||||
*
|
||||
* Mirrors the shapes from the ICD test runner. Each shape is a map
|
||||
* of field names to type strings. Types:
|
||||
* string, number, bool, array, object, uuid
|
||||
* string? / number? / object? = nullable/optional
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.SDKR;
|
||||
if (!T) return;
|
||||
|
||||
T.S = {};
|
||||
|
||||
// ── Core ──────────────────────────────────────────────────
|
||||
|
||||
T.S.channel = {
|
||||
id: 'string', title: 'string', type: 'string',
|
||||
created_at: 'string', updated_at: 'string'
|
||||
};
|
||||
T.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'
|
||||
};
|
||||
T.S.message = {
|
||||
id: 'string', channel_id: 'string', role: 'string',
|
||||
content: 'string', created_at: 'string'
|
||||
};
|
||||
T.S.persona = {
|
||||
id: 'string', name: 'string', scope: 'string',
|
||||
created_at: 'string', updated_at: 'string'
|
||||
};
|
||||
T.S.note = {
|
||||
id: 'string', title: 'string',
|
||||
created_at: 'string', updated_at: 'string'
|
||||
};
|
||||
T.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?'
|
||||
};
|
||||
T.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'
|
||||
};
|
||||
T.S.workspace = {
|
||||
id: 'string', name: 'string', owner_type: 'string',
|
||||
owner_id: 'string', status: 'string',
|
||||
created_at: 'string', updated_at: 'string'
|
||||
};
|
||||
T.S.folder = { id: 'string', name: 'string', created_at: 'string' };
|
||||
T.S.notification = {
|
||||
id: 'string', type: 'string', title: 'string',
|
||||
read: 'bool', created_at: 'string'
|
||||
};
|
||||
T.S.memory = {
|
||||
id: 'string', scope: 'string', owner_id: 'string',
|
||||
key: 'string', value: 'string', confidence: 'number',
|
||||
status: 'string', created_at: 'string', updated_at: 'string'
|
||||
};
|
||||
T.S.profile = {
|
||||
id: 'string', username: 'string', email: 'string',
|
||||
role: 'string', settings: 'object', created_at: 'string'
|
||||
};
|
||||
T.S.task = {
|
||||
id: 'string', name: 'string', task_type: 'string',
|
||||
schedule: 'string', is_active: 'bool', created_at: 'string'
|
||||
};
|
||||
T.S.workflow = {
|
||||
id: 'string', name: 'string', slug: 'string',
|
||||
entry_mode: 'string', is_active: 'bool',
|
||||
created_at: 'string', updated_at: 'string'
|
||||
};
|
||||
T.S.workflowStage = {
|
||||
id: 'string', workflow_id: 'string', ordinal: 'number',
|
||||
name: 'string', history_mode: 'string', created_at: 'string',
|
||||
surface_pkg_id: 'string?'
|
||||
};
|
||||
T.S.team = { id: 'string', name: 'string', created_at: 'string?' };
|
||||
T.S.teamMember = { user_id: 'string', role: 'string' };
|
||||
T.S.group = { id: 'string', name: 'string' };
|
||||
T.S.surface = { id: 'string', title: 'string', source: 'string', enabled: 'bool' };
|
||||
T.S.providerConfig = {
|
||||
id: 'string', name: 'string', provider: 'string', scope: 'string'
|
||||
};
|
||||
T.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'
|
||||
};
|
||||
T.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'
|
||||
};
|
||||
T.S.gitCredSummary = {
|
||||
id: 'string', name: 'string', auth_type: 'string', created_at: 'string'
|
||||
};
|
||||
|
||||
})();
|
||||
262
packages/sdk-test-runner/js/ui.js
Normal file
262
packages/sdk-test-runner/js/ui.js
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* SDK Test Runner — UI
|
||||
*
|
||||
* Renders: summary bar, domain filter, live results table, export.
|
||||
* Follows ICD runner visual conventions.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var T = window.SDKR;
|
||||
if (!T) return;
|
||||
|
||||
// ─── Verdict Colors ────────────────────────────────────────
|
||||
|
||||
var COLORS = {
|
||||
PASS: 'var(--success, #22c55e)',
|
||||
SDK_BUG: 'var(--warning, #f59e0b)',
|
||||
ICD_BUG: 'var(--danger, #ef4444)',
|
||||
SHAPE_BUG: '#a855f7',
|
||||
SKIP: 'var(--text3, #888)',
|
||||
ERROR: 'var(--danger, #ef4444)'
|
||||
};
|
||||
|
||||
var LABELS = {
|
||||
PASS: 'PASS',
|
||||
SDK_BUG: 'SDK BUG',
|
||||
ICD_BUG: 'ICD BUG',
|
||||
SHAPE_BUG: 'SHAPE',
|
||||
SKIP: 'SKIP',
|
||||
ERROR: 'ERROR'
|
||||
};
|
||||
|
||||
// ─── DOM Helpers ──────────────────────────────────────────
|
||||
|
||||
function el(tag, attrs, children) {
|
||||
var e = document.createElement(tag);
|
||||
if (attrs) Object.keys(attrs).forEach(function (k) {
|
||||
if (k === 'className') e.className = attrs[k];
|
||||
else if (k === 'style' && typeof attrs[k] === 'object') Object.assign(e.style, attrs[k]);
|
||||
else if (k.indexOf('on') === 0) e.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
|
||||
else e.setAttribute(k, attrs[k]);
|
||||
});
|
||||
if (children) {
|
||||
if (!Array.isArray(children)) children = [children];
|
||||
children.forEach(function (c) {
|
||||
if (typeof c === 'string') e.appendChild(document.createTextNode(c));
|
||||
else if (c) e.appendChild(c);
|
||||
});
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
function esc(s) { return String(s || ''); }
|
||||
|
||||
// ─── Shell ────────────────────────────────────────────────
|
||||
|
||||
T.renderShell = function () {
|
||||
var mount = T.mount;
|
||||
mount.innerHTML = '';
|
||||
|
||||
// Header
|
||||
var header = el('div', { className: 'sdkr-header' }, [
|
||||
el('h1', {}, 'SDK Test Runner'),
|
||||
el('span', { className: 'sdkr-version' }, 'v' + (T.manifest.version || '?'))
|
||||
]);
|
||||
mount.appendChild(header);
|
||||
|
||||
// Description
|
||||
mount.appendChild(el('p', { className: 'sdkr-desc' },
|
||||
'Dual-path validation: SDK call → raw ICD fetch. ' +
|
||||
'SDK fail + ICD pass = SDK bug. Both fail = ICD bug.'));
|
||||
|
||||
// ── Fixtures Panel ──
|
||||
var fixturePanel = el('div', { className: 'sdkr-fixtures' });
|
||||
|
||||
var fixRow = el('div', { className: 'sdkr-fixture-row' });
|
||||
fixRow.appendChild(el('span', { className: 'sdkr-fixture-label' }, 'FIXTURES:'));
|
||||
|
||||
// Provider dropdown
|
||||
var provSelect = el('select', { className: 'sdkr-select' });
|
||||
provSelect.appendChild(el('option', { value: '' }, '— provider —'));
|
||||
['openrouter', 'openai', 'anthropic', 'venice'].forEach(function (p) {
|
||||
provSelect.appendChild(el('option', { value: p }, p));
|
||||
});
|
||||
fixRow.appendChild(provSelect);
|
||||
|
||||
// API key input
|
||||
var keyInput = el('input', {
|
||||
type: 'password', className: 'sdkr-input',
|
||||
placeholder: 'API key (optional — enables model tests)'
|
||||
});
|
||||
fixRow.appendChild(keyInput);
|
||||
|
||||
// Fixture status
|
||||
var fixStatus = el('span', { className: 'sdkr-fixture-status' }, '');
|
||||
|
||||
fixRow.appendChild(fixStatus);
|
||||
fixturePanel.appendChild(fixRow);
|
||||
|
||||
var fixNote = el('div', { className: 'sdkr-fixture-note' },
|
||||
'Provide a provider + API key to unlock embedding/model tests. ' +
|
||||
'Without a key, those tests are skipped. Key is never stored.');
|
||||
fixturePanel.appendChild(fixNote);
|
||||
|
||||
mount.appendChild(fixturePanel);
|
||||
|
||||
// ── Fixture wiring: store user input for provisionFixtures to use ──
|
||||
T._fixtureInput = { provSelect: provSelect, keyInput: keyInput, statusEl: fixStatus };
|
||||
|
||||
// Controls row
|
||||
var controlRow = el('div', { className: 'sdkr-controls' });
|
||||
|
||||
// Domain filter checkboxes
|
||||
var filterBox = el('div', { className: 'sdkr-filters' });
|
||||
filterBox.appendChild(el('span', { className: 'sdkr-filter-label' }, 'Domains: '));
|
||||
var domainNames = Object.keys(T.domains).sort();
|
||||
var domainChecks = {};
|
||||
|
||||
domainNames.forEach(function (name) {
|
||||
var label = el('label', { className: 'sdkr-check' });
|
||||
var cb = el('input', { type: 'checkbox', checked: 'checked' });
|
||||
domainChecks[name] = cb;
|
||||
label.appendChild(cb);
|
||||
label.appendChild(document.createTextNode(' ' + name));
|
||||
filterBox.appendChild(label);
|
||||
});
|
||||
controlRow.appendChild(filterBox);
|
||||
|
||||
// Buttons
|
||||
var btnRow = el('div', { className: 'sdkr-btn-row' });
|
||||
var runBtn = el('button', { className: 'sdkr-btn sdkr-btn-primary', onClick: onRun }, 'Run All');
|
||||
var abortBtn = el('button', { className: 'sdkr-btn sdkr-btn-danger', onClick: function () { T.abort(); } }, 'Abort');
|
||||
abortBtn.style.display = 'none';
|
||||
var exportBtn = el('button', { className: 'sdkr-btn', onClick: onExport }, 'Export JSON');
|
||||
btnRow.appendChild(runBtn);
|
||||
btnRow.appendChild(abortBtn);
|
||||
btnRow.appendChild(exportBtn);
|
||||
controlRow.appendChild(btnRow);
|
||||
mount.appendChild(controlRow);
|
||||
|
||||
// Summary bar
|
||||
var summaryBar = el('div', { className: 'sdkr-summary' });
|
||||
mount.appendChild(summaryBar);
|
||||
|
||||
// Results table
|
||||
var tableWrap = el('div', { className: 'sdkr-table-wrap' });
|
||||
var table = el('table', { className: 'sdkr-table' });
|
||||
var thead = el('thead', {}, [
|
||||
el('tr', {}, [
|
||||
el('th', {}, '#'),
|
||||
el('th', {}, 'Domain'),
|
||||
el('th', {}, 'Group'),
|
||||
el('th', {}, 'Test'),
|
||||
el('th', {}, 'Verdict'),
|
||||
el('th', {}, 'ms'),
|
||||
el('th', {}, 'Detail')
|
||||
])
|
||||
]);
|
||||
table.appendChild(thead);
|
||||
var tbody = el('tbody', {});
|
||||
table.appendChild(tbody);
|
||||
tableWrap.appendChild(table);
|
||||
mount.appendChild(tableWrap);
|
||||
|
||||
// ── Callbacks ──
|
||||
|
||||
function updateSummary() {
|
||||
var s = T.stats;
|
||||
var total = T.results.length;
|
||||
summaryBar.innerHTML = '';
|
||||
var items = [
|
||||
['PASS', s.pass],
|
||||
['SDK_BUG', s.sdk_bug],
|
||||
['ICD_BUG', s.icd_bug],
|
||||
['SHAPE_BUG', s.shape_bug],
|
||||
['SKIP', s.skip]
|
||||
];
|
||||
items.forEach(function (item) {
|
||||
var badge = el('span', {
|
||||
className: 'sdkr-badge',
|
||||
style: {
|
||||
background: item[1] > 0 ? COLORS[item[0]] : 'var(--bg2, #333)',
|
||||
color: item[1] > 0 ? '#fff' : 'var(--text3, #888)'
|
||||
}
|
||||
}, LABELS[item[0]] + ': ' + item[1]);
|
||||
summaryBar.appendChild(badge);
|
||||
});
|
||||
summaryBar.appendChild(el('span', { className: 'sdkr-badge',
|
||||
style: { background: 'var(--bg2, #333)' }
|
||||
}, 'Total: ' + total));
|
||||
}
|
||||
|
||||
T.onResult = function (entry) {
|
||||
var idx = T.results.length;
|
||||
var v = entry.verdict;
|
||||
var detail = '';
|
||||
if (entry.sdkErr) detail += 'SDK: ' + esc(entry.sdkErr);
|
||||
if (entry.rawErr) detail += (detail ? ' | ' : '') + 'ICD: ' + esc(entry.rawErr);
|
||||
if (entry.note) detail += (detail ? ' | ' : '') + esc(entry.note);
|
||||
|
||||
var row = el('tr', { className: 'sdkr-row sdkr-row-' + v.toLowerCase() }, [
|
||||
el('td', {}, String(idx)),
|
||||
el('td', {}, esc(entry.domain)),
|
||||
el('td', {}, esc(entry.group)),
|
||||
el('td', { className: 'sdkr-test-name' }, esc(entry.name)),
|
||||
el('td', {}, [
|
||||
el('span', {
|
||||
className: 'sdkr-verdict',
|
||||
style: { color: COLORS[v] || '#fff' }
|
||||
}, LABELS[v] || v)
|
||||
]),
|
||||
el('td', { className: 'sdkr-ms' }, String(entry.durationMs)),
|
||||
el('td', { className: 'sdkr-detail' }, detail)
|
||||
]);
|
||||
tbody.appendChild(row);
|
||||
updateSummary();
|
||||
|
||||
// Auto-scroll
|
||||
row.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
};
|
||||
|
||||
T.onStart = function () {
|
||||
tbody.innerHTML = '';
|
||||
runBtn.disabled = true;
|
||||
runBtn.textContent = 'Running...';
|
||||
abortBtn.style.display = '';
|
||||
updateSummary();
|
||||
};
|
||||
|
||||
T.onComplete = function () {
|
||||
runBtn.disabled = false;
|
||||
runBtn.textContent = 'Run All';
|
||||
abortBtn.style.display = 'none';
|
||||
updateSummary();
|
||||
};
|
||||
|
||||
function onRun() {
|
||||
var selected = [];
|
||||
domainNames.forEach(function (n) {
|
||||
if (domainChecks[n].checked) selected.push(n);
|
||||
});
|
||||
T.runAll(selected.length === domainNames.length ? null : selected);
|
||||
}
|
||||
|
||||
function onExport() {
|
||||
var data = {
|
||||
timestamp: new Date().toISOString(),
|
||||
version: T.manifest.version,
|
||||
stats: T.stats,
|
||||
results: T.results
|
||||
};
|
||||
var blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
var a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'sdk-test-results-' + Date.now() + '.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
updateSummary();
|
||||
};
|
||||
|
||||
})();
|
||||
9
packages/sdk-test-runner/manifest.json
Normal file
9
packages/sdk-test-runner/manifest.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "sdk-test-runner",
|
||||
"title": "SDK Test Runner",
|
||||
"route": "/s/sdk-test-runner",
|
||||
"auth": "authenticated",
|
||||
"layout": "single",
|
||||
"version": "0.37.15.0",
|
||||
"description": "Dual-path SDK validation: tests every sw.api.* domain method against the ICD. SDK fail + ICD pass = SDK bug. Both fail = ICD bug."
|
||||
}
|
||||
Reference in New Issue
Block a user