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,27 +1,38 @@
/**
* 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:
* Dual-path validation utilities. Each domain file registers suites
* directly with sw.testing.suite(). This module provides:
*
* 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)
* T.dualTest — run SDK + raw ICD, return verdict
* T.raw — raw ICD fetch helpers
* T.assert* — assertion utilities
* T.unwrapList — envelope unwrapper
* T.getAuthToken / T.safeDelete / T.registerCleanup / T.runCleanup
*
* Verdicts:
* PASS — SDK call succeeded + shape valid
* SDK_BUG — SDK failed, raw ICD succeeded
* ICD_BUG — both failed
* SHAPE_BUG — SDK call succeeded but validate() threw
*/
(function () {
'use strict';
var T = window.SDKR;
if (!T) return;
// ─── Results ───────────────────────────────────────────────
// ─── Cleanup ───────────────────────────────────────────────
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;
T.cleanup = [];
T.registerCleanup = function (fn) { T.cleanup.push(fn); };
T.runCleanup = async function () {
var fns = T.cleanup.splice(0);
for (var i = fns.length - 1; i >= 0; i--) {
try { await fns[i](); } catch (_) { /* swallow */ }
}
};
// ─── Assertion Library ────────────────────────────────────
@@ -161,17 +172,6 @@
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.*
@@ -236,122 +236,4 @@
};
};
// ─── 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; };
})();