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>
251 lines
8.7 KiB
JavaScript
251 lines
8.7 KiB
JavaScript
/**
|
|
* 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() : '';
|
|
|
|
function setStatus(msg) {
|
|
console.log('[SDKR:fixtures] ' + msg);
|
|
}
|
|
|
|
// ── Resolve default endpoint for selected provider ──
|
|
var defaultEndpoint = '';
|
|
if (selectedProvider) {
|
|
try {
|
|
var typesResp = await T.raw.get('/admin/provider-types');
|
|
var types = typesResp.types || typesResp.data || typesResp || [];
|
|
var match = types.find ? types.find(function (t) { return t.id === selectedProvider; }) : null;
|
|
if (match) defaultEndpoint = match.default_endpoint || '';
|
|
} catch (_) { /* best effort — endpoint will be required in payload */ }
|
|
}
|
|
|
|
// 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,
|
|
endpoint: defaultEndpoint,
|
|
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,
|
|
endpoint: defaultEndpoint,
|
|
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');
|
|
};
|
|
|
|
})();
|