v0.7.1 Surface Runner Framework
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 23s
CI/CD / test-frontend (pull_request) Successful in 26s
CI/CD / test-go-pg (pull_request) Successful in 2m35s
CI/CD / test-sqlite (pull_request) Successful in 3m19s
CI/CD / build-and-deploy (pull_request) Successful in 1m46s

sw.testing SDK module — structured test framework for surface runners:
- suite/test registration, lifecycle hooks (beforeAll/afterAll/beforeEach/afterEach)
- Assertion library (ok/eq/neq/gt/match/throws/status/shape/arrayOf)
- Auto-cleanup via track(type, id) with LIFO deletion
- Three result statuses: passed/failed/warned
- Structured JSON results with timing, warnings, cleanup stats

test-runner manifest type:
- ValidateManifest accepts "test-runner" packages
- Excluded from sidebar nav (extensionNavItems filters surface/full only)
- DB migrations: SQLite 013, Postgres 014 (CHECK constraint)

ICD runner migration (kernel-only):
- Migrated from T.test()/T.assert() to sw.testing.suite()
- Stripped extension-dependent tests (channels, notes, personas, etc.)
- Kernel suites: smoke, crud, authz, security, providers, packaging, sdk
- Deleted ui.js + css (rendering delegated to registry surface)

SDK runner migration (kernel-only):
- Migrated from T.dualTest()/T.domains to sw.testing.suite()
- Stripped extension domains (belong in v0.7.2 package runners)
- Kernel suites: misc, workflows, admin, packages, connections, deps, composition

Runner registry surface (/s/test-runners):
- Admin-only dashboard discovering test-runner packages
- Run All / per-runner Run buttons, real-time results
- Export Failures / Export Full Results (JSON download)
- Dark mode styling using kernel CSS variables
- Suite count polling for async runner script loading

141 passed, 0 failed on fresh minimal install.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 22:49:55 +00:00
parent e916ed41ea
commit f1c47002aa
59 changed files with 2509 additions and 6525 deletions

View File

@@ -1,149 +1,54 @@
/**
* ICD Test Runner — Smoke Tier
* Read-only GET tests for every endpoint domain.
* Read-only GET tests for kernel endpoints only.
* Extension-specific endpoints belong in package runners, not here.
*/
(function () {
'use strict';
var T = window.ICD;
if (!T) return;
if (!window.sw || !window.sw.testing) return;
T.runSmoke = async function () {
sw.testing.suite('icd/smoke', async function (s) {
// -- Health --
await T.test('smoke', 'utility', 'GET /health', async function () {
s.test('utility: GET /health', async function (t) {
var d = await T.apiGet('/health');
T.assert(d.status === 'ok', 'expected status "ok", got "' + d.status + '"');
});
// -- Public Settings --
await T.test('smoke', 'admin', 'GET /settings/public', async function () {
s.test('admin: GET /settings/public', async function (t) {
var d = await T.apiGet('/settings/public');
T.assertHasKey(d, 'policies', 'public_settings');
T.assert(typeof d.policies === 'object', 'policies should be object');
});
// -- Profile --
await T.test('smoke', 'profile', 'GET /profile', async function () {
s.test('profile: GET /profile', async function (t) {
var d = await T.apiGet('/profile');
T.assertShape(d, T.S.profile, 'profile');
T.assert(d.username.length > 0, 'username should be non-empty');
});
// -- User Settings --
await T.test('smoke', 'profile', 'GET /settings (user)', async function () {
s.test('profile: GET /settings (user)', async function (t) {
var d = await T.apiGet('/settings');
T.assertHasKey(d, 'settings', 'user_settings');
});
// -- Models --
await T.test('smoke', 'models', 'GET /models/enabled', async function () {
var d = await T.apiGet('/models/enabled');
T.assertHasKey(d, 'data', '/models/enabled');
T.assert(Array.isArray(d.data), 'data should be array');
if (d.data.length > 0) {
T.assertShape(d.data[0], T.S.modelEnabled, 'data[0]');
}
});
await T.test('smoke', 'models', 'GET /models/preferences', async function () {
var d = await T.apiGet('/models/preferences');
T.assertHasKey(d, 'data', '/models/preferences');
T.assert(Array.isArray(d.data), 'data should be array');
if (d.data.length > 0) {
T.assertShape(d.data[0], T.S.modelPreference, 'data[0]');
}
});
// -- Surfaces --
await T.test('smoke', 'surfaces', 'GET /surfaces', async function () {
s.test('surfaces: GET /surfaces', async function (t) {
var d = await T.apiGet('/surfaces');
T.assertHasKey(d, 'data', '/surfaces');
T.assertArrayOf(d.data, T.S.surfaceNav, 'surfaces');
// Our own surface should be in the list
var found = d.data.some(function (s) { return s.id === 'icd-test-runner'; });
T.assert(found, 'icd-test-runner not found in surfaces list');
});
// -- Channels --
await T.test('smoke', 'channels', 'GET /channels', async function () {
var d = await T.apiGet('/channels');
// Paginated envelope or { data: [...] }
var arr = d.data || d.channels;
T.assert(Array.isArray(arr), 'expected data array from /channels');
});
// -- Personas --
await T.test('smoke', 'personas', 'GET /personas', async function () {
var d = await T.apiGet('/personas');
T.assertHasKey(d, 'data', '/personas');
T.assert(Array.isArray(d.data), 'personas should be array');
});
// -- Persona Groups --
await T.test('smoke', 'personas', 'GET /persona-groups', async function () {
var d = await T.apiGet('/persona-groups');
T.assertHasKey(d, 'data', '/persona-groups');
T.assert(Array.isArray(d.data), 'data should be array');
});
// -- Knowledge Bases --
await T.test('smoke', 'knowledge', 'GET /knowledge-bases', async function () {
var d = await T.apiGet('/knowledge-bases');
T.assertHasKey(d, 'data', '/knowledge-bases');
T.assert(Array.isArray(d.data), 'data should be array');
});
await T.test('smoke', 'knowledge', 'GET /knowledge-bases-discoverable', async function () {
var d = await T.apiGet('/knowledge-bases-discoverable');
T.assertHasKey(d, 'data', '/kb-discoverable');
T.assert(Array.isArray(d.data), 'data should be array');
});
// -- Notes --
await T.test('smoke', 'notes', 'GET /notes', async function () {
var d = await T.apiGet('/notes');
// Paginated or { data: [...] }
var arr = d.data || d.notes;
T.assert(Array.isArray(arr), 'expected array from /notes');
});
await T.test('smoke', 'notes', 'GET /notes/folders', async function () {
var d = await T.apiGet('/notes/folders');
T.assertHasKey(d, 'folders', '/notes/folders');
T.assert(Array.isArray(d.folders), 'folders should be array');
});
await T.test('smoke', 'notes', 'GET /notes/graph', async function () {
var d = await T.apiGet('/notes/graph');
T.assertHasKey(d, 'nodes', '/notes/graph');
T.assertHasKey(d, 'edges', '/notes/graph');
T.assert(Array.isArray(d.nodes), 'nodes should be array');
T.assert(Array.isArray(d.edges), 'edges should be array');
});
// -- Projects --
await T.test('smoke', 'projects', 'GET /projects', async function () {
var d = await T.apiGet('/projects');
T.assertHasKey(d, 'data', '/projects');
T.assert(Array.isArray(d.data), 'data should be array');
});
// -- Memories --
await T.test('smoke', 'memory', 'GET /memories', async function () {
var d = await T.apiGet('/memories');
T.assertHasKey(d, 'data', '/memories');
T.assert(Array.isArray(d.data), 'data should be array');
});
await T.test('smoke', 'memory', 'GET /memories/count', async function () {
var d = await T.apiGet('/memories/count');
T.assertHasKey(d, 'active', '/memories/count');
T.assertHasKey(d, 'pending', '/memories/count');
T.assert(typeof d.active === 'number', 'active should be number');
T.assert(typeof d.pending === 'number', 'pending should be number');
// test-runners registry surface should be in the list (type "surface")
var found = d.data.some(function (s) { return s.id === 'test-runners'; });
T.assert(found, 'test-runners not found in surfaces list');
// icd-test-runner is type "test-runner" — correctly excluded from /surfaces
});
// -- Notifications --
await T.test('smoke', 'notifications', 'GET /notifications', async function () {
s.test('notifications: GET /notifications', async function (t) {
var d = await T.apiGet('/notifications');
T.assertHasKey(d, 'data', '/notifications');
T.assert(Array.isArray(d.data), 'data should be array');
@@ -151,329 +56,61 @@
T.assert(typeof d.total === 'number', 'total should be number');
});
await T.test('smoke', 'notifications', 'GET /notifications/unread-count', async function () {
s.test('notifications: GET /notifications/unread-count', async function (t) {
var d = await T.apiGet('/notifications/unread-count');
T.assertHasKey(d, 'count', '/notifications/unread-count');
T.assert(typeof d.count === 'number', 'count should be number');
});
await T.test('smoke', 'notifications', 'GET /notifications/preferences', async function () {
s.test('notifications: GET /notifications/preferences', async function (t) {
var d = await T.apiGet('/notifications/preferences');
T.assertHasKey(d, 'data', '/notifications/preferences');
T.assert(Array.isArray(d.data), 'data should be array');
});
// -- Extensions --
await T.test('smoke', 'extensions', 'GET /extensions', async function () {
s.test('extensions: GET /extensions', async function (t) {
var d = await T.apiGet('/extensions');
T.assertHasKey(d, 'data', '/extensions');
T.assert(Array.isArray(d.data), 'extensions should be array');
});
await T.test('smoke', 'extensions', 'GET /extensions/tools', async function () {
s.test('extensions: GET /extensions/tools', async function (t) {
var d = await T.apiGet('/extensions/tools');
T.assertHasKey(d, 'data', '/extensions/tools');
T.assert(Array.isArray(d.data), 'tools should be array');
});
// -- Tools --
await T.test('smoke', 'channels', 'GET /tools', async function () {
var d = await T.apiGet('/tools');
T.assertHasKey(d, 'tools', '/tools');
T.assert(Array.isArray(d.tools), 'tools should be array');
});
// -- Provider Configs (BYOK) --
await T.test('smoke', 'providers', 'GET /api-configs', async function () {
var d = await T.apiGet('/api-configs');
T.assertHasKey(d, 'configs', '/api-configs');
T.assert(Array.isArray(d.configs), 'configs should be array');
});
// -- Teams --
await T.test('smoke', 'teams', 'GET /teams/mine', async function () {
s.test('teams: GET /teams/mine', async function (t) {
var d = await T.apiGet('/teams/mine');
T.assertHasKey(d, 'data', '/teams/mine');
T.assert(Array.isArray(d.data), 'data should be array');
});
// -- Groups --
await T.test('smoke', 'teams', 'GET /groups/mine', async function () {
s.test('teams: GET /groups/mine', async function (t) {
var d = await T.apiGet('/groups/mine');
T.assertHasKey(d, 'data', '/groups/mine');
T.assert(Array.isArray(d.data), 'data should be array');
});
// -- Tasks --
await T.test('smoke', 'tasks', 'GET /tasks', async function () {
var d = await T.apiGet('/tasks');
T.assertHasKey(d, 'data', '/tasks');
T.assert(Array.isArray(d.data), 'data should be array');
});
// -- Workspaces --
await T.test('smoke', 'workspaces', 'GET /workspaces', async function () {
var d = await T.apiGet('/workspaces');
T.assertHasKey(d, 'data', '/workspaces');
T.assert(Array.isArray(d.data), 'data should be array');
});
// -- Folders --
await T.test('smoke', 'channels', 'GET /folders', async function () {
var d = await T.apiGet('/folders');
// May be { data: [...] } or { folders: [...] }
var arr = d.data || d.folders;
T.assert(arr === undefined || Array.isArray(arr), 'expected array from /folders');
});
// -- Files --
await T.test('smoke', 'channels', 'GET /files', async function () {
var d = await T.apiGet('/files');
T.assertHasKey(d, 'files', '/files');
T.assert(Array.isArray(d.files), 'files should be array');
T.assert(typeof d.total === 'number', 'expected total count');
});
// -- Usage --
await T.test('smoke', 'admin', 'GET /usage', async function () {
var d = await T.apiGet('/usage');
// personal usage — may have totals, results
T.assert(typeof d === 'object', 'expected object from /usage');
});
// -- Workflow Assignments --
await T.test('smoke', 'workflows', 'GET /workflow-assignments/mine', async function () {
var d = await T.apiGet('/workflow-assignments/mine');
// May be { data: [...] } or { assignments: [...] }
var arr = d.data || d.assignments;
T.assert(arr === undefined || Array.isArray(arr), 'expected array from /workflow-assignments/mine');
});
// -- Workflows --
await T.test('smoke', 'workflows', 'GET /workflows', async function () {
s.test('workflows: GET /workflows', async function (t) {
var d = await T.apiGet('/workflows');
T.assertHasKey(d, 'data', '/workflows');
T.assert(Array.isArray(d.data), 'data should be array');
});
// -- Git Credentials --
await T.test('smoke', 'workspaces', 'GET /git-credentials', async function () {
var d = await T.apiGet('/git-credentials');
T.assertHasKey(d, 'data', '/git-credentials');
T.assert(Array.isArray(d.data), 'data should be array');
});
// -- Presence --
await T.test('smoke', 'channels', 'POST /presence/heartbeat', async function () {
var d = await T.apiPost('/presence/heartbeat', {});
T.assert(d.ok === true, 'expected { ok: true }');
});
await T.test('smoke', 'channels', 'GET /presence (query)', async function () {
var d = await T.apiGet('/presence?users=' + encodeURIComponent(T.user.id));
T.assertHasKey(d, 'presence', '/presence');
T.assert(typeof d.presence === 'object', 'presence should be object');
// After heartbeat, our own user should be online
if (d.presence[T.user.id]) {
T.assert(d.presence[T.user.id] === 'online', 'expected self online after heartbeat');
}
});
// -- Admin routes (only if admin) --
if (T.user.role === 'admin') {
await T.test('smoke', 'admin', 'GET /admin/stats', async function () {
var d = await T.apiGet('/admin/stats');
T.assertHasKey(d, 'users', '/admin/stats');
T.assertHasKey(d, 'channels', '/admin/stats');
T.assertHasKey(d, 'messages', '/admin/stats');
});
await T.test('smoke', 'admin', 'GET /admin/users', async function () {
var d = await T.apiGet('/admin/users');
T.assertHasKey(d, 'users', '/admin/users');
T.assert(Array.isArray(d.users), 'users should be array');
});
await T.test('smoke', 'admin', 'GET /admin/settings', async function () {
var d = await T.apiGet('/admin/settings');
T.assert(typeof d === 'object', 'expected object');
});
await T.test('smoke', 'admin', 'GET /admin/configs', async function () {
var d = await T.apiGet('/admin/configs');
T.assertHasKey(d, 'configs', '/admin/configs');
T.assert(Array.isArray(d.configs), 'configs should be array');
});
await T.test('smoke', 'admin', 'GET /admin/models', async function () {
var d = await T.apiGet('/admin/models');
T.assertHasKey(d, 'models', '/admin/models');
T.assert(Array.isArray(d.models), 'models should be array');
});
await T.test('smoke', 'admin', 'GET /admin/personas', async function () {
var d = await T.apiGet('/admin/personas');
T.assertHasKey(d, 'data', '/admin/personas');
});
await T.test('smoke', 'admin', 'GET /admin/teams', async function () {
var d = await T.apiGet('/admin/teams');
T.assertHasKey(d, 'data', '/admin/teams');
});
await T.test('smoke', 'admin', 'GET /admin/groups', async function () {
var d = await T.apiGet('/admin/groups');
T.assertHasKey(d, 'data', '/admin/groups');
});
await T.test('smoke', 'admin', 'GET /admin/packages', async function () {
var d = await T.apiGet('/admin/packages');
T.assertHasKey(d, 'data', '/admin/packages');
T.assert(Array.isArray(d.data), 'surfaces should be array');
if (d.data.length > 0) {
T.assertShape(d.data[0], T.S.surfaceAdmin, 'surfaces[0]');
}
});
await T.test('smoke', 'admin', 'GET /admin/providers/health', async function () {
var d = await T.apiGet('/admin/providers/health');
T.assertHasKey(d, 'data', '/admin/providers/health');
});
await T.test('smoke', 'admin', 'GET /admin/capability-overrides', async function () {
var d = await T.apiGet('/admin/capability-overrides');
T.assertHasKey(d, 'data', '/admin/capability-overrides');
});
await T.test('smoke', 'admin', 'GET /admin/routing/policies', async function () {
var d = await T.apiGet('/admin/routing/policies');
T.assertHasKey(d, 'data', '/admin/routing/policies');
});
await T.test('smoke', 'admin', 'GET /admin/permissions', async function () {
var d = await T.apiGet('/admin/permissions');
T.assertHasKey(d, 'permissions', '/admin/permissions');
});
await T.test('smoke', 'admin', 'GET /admin/roles', async function () {
var d = await T.apiGet('/admin/roles');
// Returns a raw JSONMap of role configs (e.g. { chat: {...}, summary: {...} })
T.assert(typeof d === 'object' && d !== null, 'expected object');
});
await T.test('smoke', 'admin', 'GET /admin/vault/status', async function () {
var d = await T.apiGet('/admin/vault/status');
T.assert(typeof d === 'object', 'expected object');
});
await T.test('smoke', 'admin', 'GET /admin/storage/status', async function () {
var d = await T.apiGet('/admin/storage/status');
T.assert(typeof d === 'object', 'expected object');
});
await T.test('smoke', 'admin', 'GET /admin/audit', async function () {
var d = await T.apiGet('/admin/audit');
T.assertHasKey(d, 'data', '/admin/audit');
T.assert(Array.isArray(d.data), 'data should be array');
T.assertHasKey(d, 'total', '/admin/audit');
T.assertHasKey(d, 'page', '/admin/audit');
T.assertHasKey(d, 'per_page', '/admin/audit');
});
await T.test('smoke', 'admin', 'GET /admin/audit/actions', async function () {
var d = await T.apiGet('/admin/audit/actions');
T.assertHasKey(d, 'actions', '/admin/audit/actions');
});
await T.test('smoke', 'admin', 'GET /admin/usage', async function () {
var d = await T.apiGet('/admin/usage');
T.assert(typeof d === 'object', 'expected object');
});
await T.test('smoke', 'admin', 'GET /admin/pricing', async function () {
var d = await T.apiGet('/admin/pricing');
// Returns a bare array (no envelope)
T.assert(Array.isArray(d), '/admin/pricing should return array');
});
await T.test('smoke', 'admin', 'GET /admin/provider-types', async function () {
var d = await T.apiGet('/admin/provider-types');
T.assertHasKey(d, 'data', '/admin/provider-types');
T.assert(Array.isArray(d.data), 'data should be array');
});
await T.test('smoke', 'admin', 'GET /admin/tasks', async function () {
var d = await T.apiGet('/admin/tasks');
T.assertHasKey(d, 'data', '/admin/tasks');
T.assert(Array.isArray(d.data), 'data should be array');
});
await T.test('smoke', 'admin', 'GET /admin/projects', async function () {
var d = await T.apiGet('/admin/projects');
T.assertHasKey(d, 'data', '/admin/projects');
});
await T.test('smoke', 'admin', 'GET /admin/channels/archived', async function () {
var d = await T.apiGet('/admin/channels/archived');
T.assertHasKey(d, 'channels', '/admin/channels/archived');
T.assert(Array.isArray(d.channels), 'channels should be array');
T.assertHasKey(d, 'total', '/admin/channels/archived');
});
await T.test('smoke', 'admin', 'GET /admin/storage/orphans', async function () {
var d = await T.apiGet('/admin/storage/orphans');
T.assertHasKey(d, 'count', '/admin/storage/orphans');
});
await T.test('smoke', 'admin', 'GET /admin/settings/:key (banner)', async function () {
var d = await T.apiGet('/admin/settings/banner');
T.assertHasKey(d, 'key', '/admin/settings/:key');
T.assertHasKey(d, 'value', '/admin/settings/:key');
T.assert(d.key === 'banner', 'key should be "banner"');
});
await T.test('smoke', 'admin', 'GET /admin/storage/extraction', async function () {
var d = await T.apiGet('/admin/storage/extraction');
T.assert(typeof d === 'object', 'expected object');
});
await T.test('smoke', 'memory', 'GET /admin/memories/pending', async function () {
var d = await T.apiGet('/admin/memories/pending');
T.assertHasKey(d, 'data', '/admin/memories/pending');
T.assert(Array.isArray(d.data), 'data should be array');
});
// -- v0.6.x Admin endpoints --
await T.test('smoke', 'admin', 'GET /admin/metrics', async function () {
var d = await T.apiGet('/admin/metrics');
T.assert(typeof d === 'object', 'expected object');
T.assertHasKey(d, 'runtime', '/admin/metrics');
T.assertHasKey(d, 'database', '/admin/metrics');
});
await T.test('smoke', 'admin', 'GET /admin/cluster', async function () {
var d = await T.apiGet('/admin/cluster');
T.assertHasKey(d, 'data', '/admin/cluster');
T.assert(Array.isArray(d.data), 'cluster nodes should be array');
});
await T.test('smoke', 'admin', 'GET /admin/backups', async function () {
var d = await T.apiGet('/admin/backups');
T.assertHasKey(d, 'data', '/admin/backups');
T.assert(Array.isArray(d.data), 'backups should be array');
});
}
// -- Docs API --
await T.test('smoke', 'utility', 'GET /docs', async function () {
s.test('utility: GET /docs', async function (t) {
var d = await T.apiGet('/docs');
T.assertHasKey(d, 'data', '/docs');
T.assert(Array.isArray(d.data), 'docs should be array');
});
// -- Dynamic OpenAPI spec (outside /api/v1 prefix) --
await T.test('smoke', 'utility', 'GET /api/docs/openapi.json', async function () {
s.test('utility: GET /api/docs/openapi.json', async function (t) {
var token = await T.getAuthToken();
var resp = await fetch(T.base + '/api/docs/openapi.json', {
headers: { 'Authorization': 'Bearer ' + token },
@@ -485,5 +122,85 @@
T.assertHasKey(d, 'paths', 'openapi spec');
T.assertHasKey(d, 'info', 'openapi spec');
});
};
// -- Admin routes (only if admin) --
if (sw.isAdmin) {
s.test('admin: GET /admin/stats', async function (t) {
var d = await T.apiGet('/admin/stats');
T.assert(typeof d === 'object', 'expected object from /admin/stats');
});
s.test('admin: GET /admin/users', async function (t) {
var d = await T.apiGet('/admin/users');
var arr = d.users || d.data;
T.assert(Array.isArray(arr), 'expected array from /admin/users');
});
s.test('admin: GET /admin/settings', async function (t) {
var d = await T.apiGet('/admin/settings');
T.assert(typeof d === 'object', 'expected object');
});
s.test('admin: GET /admin/settings/:key (banner)', async function (t) {
var d = await T.apiGet('/admin/settings/banner');
T.assertHasKey(d, 'key', '/admin/settings/:key');
T.assertHasKey(d, 'value', '/admin/settings/:key');
T.assert(d.key === 'banner', 'key should be "banner"');
});
s.test('admin: GET /admin/teams', async function (t) {
var d = await T.apiGet('/admin/teams');
T.assertHasKey(d, 'data', '/admin/teams');
});
s.test('admin: GET /admin/groups', async function (t) {
var d = await T.apiGet('/admin/groups');
T.assertHasKey(d, 'data', '/admin/groups');
});
s.test('admin: GET /admin/packages', async function (t) {
var d = await T.apiGet('/admin/packages');
T.assertHasKey(d, 'data', '/admin/packages');
T.assert(Array.isArray(d.data), 'surfaces should be array');
if (d.data.length > 0) {
T.assertShape(d.data[0], T.S.surfaceAdmin, 'surfaces[0]');
}
});
s.test('admin: GET /admin/permissions', async function (t) {
var d = await T.apiGet('/admin/permissions');
T.assertHasKey(d, 'permissions', '/admin/permissions');
});
s.test('admin: GET /admin/audit', async function (t) {
var d = await T.apiGet('/admin/audit');
T.assertHasKey(d, 'data', '/admin/audit');
T.assert(Array.isArray(d.data), 'data should be array');
T.assertHasKey(d, 'total', '/admin/audit');
T.assertHasKey(d, 'page', '/admin/audit');
T.assertHasKey(d, 'per_page', '/admin/audit');
});
s.test('admin: GET /admin/audit/actions', async function (t) {
var d = await T.apiGet('/admin/audit/actions');
T.assertHasKey(d, 'actions', '/admin/audit/actions');
});
s.test('admin: GET /admin/metrics', async function (t) {
var d = await T.apiGet('/admin/metrics');
T.assert(typeof d === 'object', 'expected object from /admin/metrics');
});
s.test('admin: GET /admin/backups', async function (t) {
var d = await T.apiGet('/admin/backups');
T.assertHasKey(d, 'data', '/admin/backups');
T.assert(Array.isArray(d.data), 'backups should be array');
});
s.test('admin: GET /admin/storage/status', async function (t) {
var d = await T.apiGet('/admin/storage/status');
T.assert(typeof d === 'object', 'expected object');
});
}
});
})();