This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/packages/test-runners/js/main.js
Jeffrey Smith d6c7b21713
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m39s
CI/CD / test-sqlite (push) Successful in 2m55s
CI/CD / build-and-deploy (push) Successful in 29s
Feat v0.7.2 package runners ci gate (#56)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-04-02 12:10:57 +00:00

457 lines
18 KiB
JavaScript

/**
* Test Runners — Registry Surface
*
* Discovers installed test-runner packages, loads their suites into
* sw.testing, and provides a dashboard to run and view results.
*/
(async function () {
'use strict';
var mount = document.getElementById('extension-mount');
if (!mount) return;
mount.classList.add('ext-test-runners-root');
// ─── 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();
} catch (e) {
mount.innerHTML = '<p style="color:var(--danger);padding:24px;">SDK boot failed: ' + e.message + '</p>';
return;
}
var { h, render } = preact;
var { useState, useEffect, useCallback } = hooks;
// ─── Shell Topbar ─────────────────────────────────────────
if (sw.shell && sw.shell.topbar) {
sw.shell.topbar.setTitle('Test Runners');
}
// ─── Status Icons ─────────────────────────────────────────
var STATUS_ICON = {
passed: '\u2713', failed: '\u2717', warned: '\u26A0', skipped: '\u2014'
};
// ─── App Component ────────────────────────────────────────
function App() {
var _s = useState(null); var runners = _s[0], setRunners = _s[1];
var _s2 = useState(null); var allPkgs = _s2[0], setAllPkgs = _s2[1];
var _s3 = useState(null); var results = _s3[0], setResults = _s3[1];
var _s4 = useState(false); var running = _s4[0], setRunning = _s4[1];
var _s5 = useState(null); var runningSuite = _s5[0], setRunningSuite = _s5[1];
var _s6 = useState(''); var error = _s6[0], setError = _s6[1];
var _s7 = useState(false); var loaded = _s7[0], setLoaded = _s7[1];
// Discover runners and load their scripts
useEffect(function () {
discoverAndLoad().catch(function (e) {
setError('Failed to discover runners: ' + e.message);
});
}, []);
async function discoverAndLoad() {
// Fetch all packages (admin endpoint)
var pkgsResp = await sw.api.get('/api/v1/admin/packages');
var pkgs = pkgsResp.data || pkgsResp;
setAllPkgs(pkgs);
// Filter test-runner type
var testRunners = pkgs.filter(function (p) {
return p.type === 'test-runner' && p.enabled;
});
setRunners(testRunners);
// Clear any previously registered suites
sw.testing.clear();
// Load each runner's main.js to register suites.
// Runner main.js scripts are async IIFEs that load sub-modules sequentially,
// so the script onload fires before suites are registered. We load all runner
// scripts, then poll until suite count stabilizes.
var surfaceBase = (window.__BASE__ || '') + '/surfaces/';
for (var i = 0; i < testRunners.length; i++) {
var r = testRunners[i];
await loadRunnerScript(surfaceBase + r.id + '/js/main.js?v=' + (r.version || '0') + '.' + Date.now());
}
// Wait for suites to stabilize (runners load sub-modules async)
await waitForSuites();
setLoaded(true);
console.log('[TestRunners] Loaded ' + testRunners.length + ' runners, ' + sw.testing.suites().length + ' suites');
}
function loadRunnerScript(src) {
return new Promise(function (resolve) {
var s = document.createElement('script');
s.src = src;
s.onload = resolve;
s.onerror = function () {
console.warn('[TestRunners] Failed to load: ' + src);
resolve();
};
document.body.appendChild(s);
});
}
function waitForSuites() {
return new Promise(function (resolve) {
var lastCount = 0;
var stableCount = 0;
var interval = setInterval(function () {
var count = sw.testing.suites().length;
if (count === lastCount && count > 0) {
stableCount++;
if (stableCount >= 3) { clearInterval(interval); resolve(); }
} else {
stableCount = 0;
lastCount = count;
}
}, 500);
// Timeout after 30s regardless
setTimeout(function () { clearInterval(interval); resolve(); }, 30000);
});
}
// Runner dependency declarations.
// Stored here (not in manifests) to avoid the package system marking
// runners as dormant due to install-order timing during bundled install.
var RUNNER_REQUIRES = {
'notes-runner': ['notes'],
'chat-runner': ['chat', 'chat-core'],
'schedules-runner': ['schedules'],
'workflow-runner': ['content-approval'],
'renderer-runner': ['mermaid-renderer']
};
// Check requires for a runner
function checkRequires(runner) {
var requires = RUNNER_REQUIRES[runner.id] || runner.manifest?.requires || runner.requires || [];
if (!requires.length || !allPkgs) return { met: requires, missing: [] };
var installedIds = allPkgs.filter(function (p) { return p.enabled; }).map(function (p) { return p.id; });
var met = [], missing = [];
for (var i = 0; i < requires.length; i++) {
if (installedIds.indexOf(requires[i]) !== -1) met.push(requires[i]);
else missing.push(requires[i]);
}
return { met: met, missing: missing };
}
// Run all suites
// Post results to kernel API for CI consumption (fire-and-forget)
function postResults(runnerId, data) {
sw.api.post('/api/v1/admin/test-runners/results', {
runner_id: runnerId || 'all',
results: data
}).catch(function (e) { console.warn('[TestRunners] Failed to store results:', e.message); });
}
var runAll = useCallback(async function () {
if (running) return;
setRunning(true);
setResults(null);
setRunningSuite('all');
try {
var r = await sw.testing.run();
setResults(r);
postResults('all', r);
} catch (e) {
setError('Run failed: ' + e.message);
}
setRunning(false);
setRunningSuite(null);
}, [running]);
// Run suites for a single runner
var runRunner = useCallback(async function (runner) {
if (running) return;
setRunning(true);
setRunningSuite(runner.id);
// Map runner package ID to suite name prefix.
// ICD/SDK runners use 'icd/'/'sdk/' prefixes.
// Package runners use the package name they test: 'notes/', 'chat/', etc.
var prefix;
if (runner.id === 'icd-test-runner') prefix = 'icd/';
else if (runner.id === 'sdk-test-runner') prefix = 'sdk/';
else prefix = runner.id.replace(/-runner$/, '') + '/';
var suiteNames = sw.testing.suites().filter(function (n) { return n.indexOf(prefix) === 0; });
var allSuiteResults = [];
var summary = { total: 0, passed: 0, failed: 0, warned: 0, skipped: 0 };
var start = performance.now();
for (var i = 0; i < suiteNames.length; i++) {
var r = await sw.testing.run(suiteNames[i]);
for (var j = 0; j < r.suites.length; j++) {
allSuiteResults.push(r.suites[j]);
for (var k = 0; k < r.suites[j].tests.length; k++) {
var t = r.suites[j].tests[k];
summary.total++;
if (t.status === 'passed') summary.passed++;
else if (t.status === 'failed') summary.failed++;
else if (t.status === 'warned') summary.warned++;
else if (t.status === 'skipped') summary.skipped++;
}
}
if (r.suites[0] && r.suites[0].status === 'skipped') summary.skipped++;
}
var runnerResults = {
timestamp: new Date().toISOString(),
duration_ms: Math.round(performance.now() - start),
summary: summary,
suites: allSuiteResults
};
setResults(runnerResults);
postResults(runner.id, runnerResults);
setRunning(false);
setRunningSuite(null);
}, [running]);
if (error) {
return html`<div class="sw-inline-error"><p>${error}</p></div>`;
}
if (!loaded) {
return html`<div class="ext-test-runners-loading">Loading test runners...</div>`;
}
if (!runners || runners.length === 0) {
return html`<div class="ext-test-runners-empty">
<p>No test-runner packages installed.</p>
<p style="font-size:0.85rem;color:var(--text-muted)">Install packages with <code>"type": "test-runner"</code> to see them here.</p>
</div>`;
}
var suiteCount = sw.testing.suites().length;
return html`
<div>
<${RunAllBar}
suiteCount=${suiteCount}
results=${results}
running=${running}
runningSuite=${runningSuite}
onRunAll=${runAll}
/>
<div class="ext-test-runners-grid">
${runners.map(function (r) {
return html`<${RunnerCard}
key=${r.id}
runner=${r}
requires=${checkRequires(r)}
results=${results}
running=${running}
runningSuite=${runningSuite}
onRun=${function () { runRunner(r); }}
/>`;
})}
</div>
</div>
`;
}
// ─── Export Helpers ─────────────────────────────────────────
function exportFailures(results) {
if (!results) return;
var failures = [];
for (var i = 0; i < results.suites.length; i++) {
var suite = results.suites[i];
for (var j = 0; j < suite.tests.length; j++) {
var t = suite.tests[j];
if (t.status === 'failed' || t.status === 'warned') {
failures.push({
suite: suite.name,
test: t.name,
status: t.status,
detail: t.detail || null,
warnings: t.warnings && t.warnings.length > 0 ? t.warnings : undefined,
duration_ms: t.duration_ms,
});
}
}
}
var report = {
exported_at: new Date().toISOString(),
run_at: results.timestamp,
duration_ms: results.duration_ms,
summary: results.summary,
failures: failures,
};
var json = JSON.stringify(report, null, 2);
var blob = new Blob([json], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'test-failures-' + new Date().toISOString().slice(0, 19).replace(/:/g, '-') + '.json';
a.click();
URL.revokeObjectURL(url);
}
function exportFull(results) {
if (!results) return;
var json = JSON.stringify(results, null, 2);
var blob = new Blob([json], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'test-results-' + new Date().toISOString().slice(0, 19).replace(/:/g, '-') + '.json';
a.click();
URL.revokeObjectURL(url);
}
// ─── RunAllBar ────────────────────────────────────────────
function RunAllBar(props) {
var s = props.results?.summary;
var hasFailures = s && (s.failed > 0 || s.warned > 0);
return html`
<div class="ext-test-runners-bar">
<h2>Test Runners</h2>
${props.running
? html`<span class="ext-test-runners-running"><span class="ext-test-runners-spinner"></span> Running${props.runningSuite === 'all' ? ' all' : ''}...</span>`
: html`<button class="sw-btn sw-btn--primary" onClick=${props.onRunAll}>Run All</button> <span class="ext-test-runners-suite-count">${props.suiteCount} suites</span>`
}
${s ? html`
<div class="ext-test-runners-summary">
<span class="ext-test-runners-stat ext-test-runners-stat--pass">${s.passed} passed</span>
${s.failed > 0 ? html`<span class="ext-test-runners-stat ext-test-runners-stat--fail">${s.failed} failed</span>` : null}
${s.warned > 0 ? html`<span class="ext-test-runners-stat ext-test-runners-stat--warn">${s.warned} warned</span>` : null}
${s.skipped > 0 ? html`<span class="ext-test-runners-stat ext-test-runners-stat--skip">${s.skipped} skipped</span>` : null}
<span style="color:var(--text-3);font-size:0.8rem">${props.results.duration_ms}ms</span>
</div>
` : null}
</div>
${s && !props.running ? html`
<div class="ext-test-runners-export">
${hasFailures ? html`<button class="sw-btn sw-btn--sm" onClick=${function () { exportFailures(props.results); }}>Export Failures</button>` : null}
<button class="sw-btn sw-btn--sm" onClick=${function () { exportFull(props.results); }}>Export Full Results</button>
</div>
` : null}
`;
}
// ─── RunnerCard ───────────────────────────────────────────
function RunnerCard(props) {
var r = props.runner;
var manifest = r.manifest || {};
var icon = manifest.icon || r.icon || '📦';
var title = r.title || manifest.title || r.id;
var desc = manifest.description || r.description || '';
var req = props.requires;
// Filter results for this runner's suites
var prefix = r.id === 'icd-test-runner' ? 'icd/' :
r.id === 'sdk-test-runner' ? 'sdk/' :
r.id.replace(/-runner$/, '') + '/';
var runnerSuites = props.results?.suites?.filter(function (s) {
return s.name.indexOf(prefix) === 0;
}) || [];
return html`
<div class="ext-test-runners-card">
<div class="ext-test-runners-card__header">
<span class="ext-test-runners-card__icon">${icon}</span>
<h3 class="ext-test-runners-card__title">${title}</h3>
${req.missing.length > 0
? null
: props.running
? (props.runningSuite === r.id
? html`<span class="ext-test-runners-running"><span class="ext-test-runners-spinner"></span></span>`
: null)
: html`<button class="sw-btn sw-btn--sm" onClick=${props.onRun}>Run</button>`
}
</div>
${desc ? html`<p class="ext-test-runners-card__desc">${desc}</p>` : null}
${req.missing.length > 0
? html`<p class="ext-test-runners-card__requires ext-test-runners-card__requires--missing">
Missing required packages: ${req.missing.join(', ')}
</p>`
: req.met.length > 0
? html`<p class="ext-test-runners-card__requires">Requires: ${req.met.join(', ')}</p>`
: null
}
${runnerSuites.length > 0 ? html`<${ResultsPanel} suites=${runnerSuites} />` : null}
</div>
`;
}
// ─── ResultsPanel ─────────────────────────────────────────
function ResultsPanel(props) {
return html`
<div class="ext-test-runners-results">
${props.suites.map(function (suite) {
return html`<${SuiteResult} key=${suite.name} suite=${suite} />`;
})}
</div>
`;
}
function SuiteResult(props) {
var _s = useState(true); var expanded = _s[0], setExpanded = _s[1];
var suite = props.suite;
var statusClass = 'ext-test-runners-status--' + suite.status;
var counts = { passed: 0, failed: 0, warned: 0, skipped: 0 };
for (var i = 0; i < suite.tests.length; i++) {
var s = suite.tests[i].status;
if (counts[s] !== undefined) counts[s]++;
}
return html`
<div class="ext-test-runners-suite">
<div class="ext-test-runners-suite__name ${statusClass}"
onClick=${function () { setExpanded(!expanded); }}>
${expanded ? '▼' : '▶'} ${suite.name}
${' '}(${counts.passed}/${suite.tests.length} passed${counts.failed ? ', ' + counts.failed + ' failed' : ''}${suite.status === 'skipped' ? ' — skipped' + (suite.reason ? ': ' + suite.reason : '') : ''})
${' '}${suite.duration_ms}ms
</div>
${expanded && suite.tests.length > 0 ? suite.tests.map(function (t) {
return html`<${TestResult} key=${t.name} test=${t} />`;
}) : null}
</div>
`;
}
function TestResult(props) {
var t = props.test;
var statusClass = 'ext-test-runners-status--' + t.status;
var icon = STATUS_ICON[t.status] || '?';
return html`
<div>
<div class="ext-test-runners-test">
<span class="ext-test-runners-test__status ${statusClass}">${icon}</span>
<span class="ext-test-runners-test__name">${t.name}</span>
<span class="ext-test-runners-test__time">${t.duration_ms}ms</span>
</div>
${t.detail ? html`<div class="ext-test-runners-test__detail">${t.detail}</div>` : null}
${t.warnings && t.warnings.length > 0 ? t.warnings.map(function (w) {
return html`<div class="ext-test-runners-test__detail" style="color:var(--warning,#856404)">${w}</div>`;
}) : null}
</div>
`;
}
// ─── Render ───────────────────────────────────────────────
render(html`<${App} />`, mount);
})();