v0.7.2 Package Runners + CI Gate

Five package-level test runners validating extension API contracts:
- notes-runner: CRUD, folders, tags, search, backlinks (12 tests)
- chat-runner: conversations, messaging, search (9 tests)
- schedules-runner: CRUD + run (5 tests)
- workflow-runner: definitions, instances, stage progression (5 tests)
- renderer-runner: registry contract, block matching (4 tests)

Runner Result API (in-memory, 3 admin endpoints) stores results
from browser runs for CI consumption. Test-runners surface v0.2.0
posts results after each run and fixes suite prefix matching.

CI integration via Playwright: wait-for-healthy.sh, run-surface-tests.sh,
surface-test-driver.js. New test-runners stage in Gitea CI pipeline.

Verified: 169 passed, 0 failed, 9 warned, 8 skipped on fresh install.
Go handler tests: 4/4 passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 23:52:31 +00:00
parent 829caa3b20
commit 448071d6b9
30 changed files with 1397 additions and 26 deletions

View File

@@ -0,0 +1,59 @@
/**
* Chat Runner — chat/conversations suite
*
* Tests conversation CRUD via the chat-core ext API.
*/
(function () {
'use strict';
var api = window.CR.api;
sw.testing.suite('chat/conversations', async function (s) {
var convId;
s.test('create conversation', async function (t) {
var r = await api.post('/conversations', {
title: 'Runner Test Convo ' + Date.now(),
type: 'direct'
});
t.assert.ok(r.id, 'conversation has id');
t.assert.ok(r.title, 'conversation has title');
convId = r.id;
s.track('conversation', convId);
});
s.test('get conversation', async function (t) {
t.assert.ok(convId, 'convId from previous test');
var r = await api.get('/conversations/' + convId);
t.assert.eq(r.id, convId, 'id matches');
});
s.test('update conversation', async function (t) {
t.assert.ok(convId, 'convId from previous test');
var r = await api.put('/conversations/' + convId, {
title: 'Updated Convo Title'
});
t.assert.eq(r.title, 'Updated Convo Title', 'title updated');
});
s.test('list conversations', async function (t) {
var r = await api.get('/conversations');
var list = Array.isArray(r) ? r : (r.data || []);
t.assert.ok(Array.isArray(list), 'response is array');
var found = list.some(function (c) { return c.id === convId; });
t.assert.ok(found, 'created conversation in list');
});
s.test('delete conversation', async function (t) {
t.assert.ok(convId, 'convId from previous test');
await api.del('/conversations/' + convId);
try {
await api.get('/conversations/' + convId);
t.assert.ok(false, 'expected error after delete');
} catch (e) {
t.assert.ok(true, 'conversation not found after delete');
}
convId = null;
});
});
})();

View File

@@ -0,0 +1,63 @@
/**
* Chat Runner — Entry Point
*
* Boot SDK, load test modules in dependency order.
* Each module is an IIFE that registers suites via sw.testing.suite().
*/
(async function () {
'use strict';
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) {
console.warn('[ChatRunner] SDK boot failed:', e.message);
}
window.CR = {
base: window.__BASE__ || '',
api: sw.api.ext('chat-core')
};
var surfaceId = 'chat-runner';
var assetBase = '/surfaces/' + surfaceId + '/js/';
if (window.CR.base) assetBase = window.CR.base + assetBase;
var modules = [
'conversations.js',
'messaging.js'
];
var loaded = 0;
function loadNext() {
if (loaded >= modules.length) { onReady(); 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('[ChatRunner] Failed to load: ' + modules[loaded]);
loaded++; loadNext();
};
document.body.appendChild(script);
}
function onReady() {
console.log('[ChatRunner] All modules loaded — ' + sw.testing.suites().length + ' suites registered');
var manifest = window.__MANIFEST__ || {};
if (manifest.id === surfaceId) {
window.location.href = (window.__BASE__ || '') + '/s/test-runners';
}
}
loadNext();
})();

View File

@@ -0,0 +1,60 @@
/**
* Chat Runner — chat/messaging suite
*
* Tests message CRUD and search within conversations.
*/
(function () {
'use strict';
var api = window.CR.api;
sw.testing.suite('chat/messaging', async function (s) {
var convId, msgId;
s.beforeAll(async function () {
var r = await api.post('/conversations', {
title: 'Messaging Test ' + Date.now(),
type: 'direct'
});
convId = r.id;
s.track('conversation', convId);
});
s.test('send message', async function (t) {
var r = await api.post('/messages/' + convId, {
content: 'Hello from chat-runner test!',
content_type: 'text'
});
t.assert.ok(r.id, 'message has id');
t.assert.ok(r.content, 'message has content');
msgId = r.id;
});
s.test('list messages', async function (t) {
var r = await api.get('/messages/' + convId);
var list = Array.isArray(r) ? r : (r.data || r.messages || []);
t.assert.ok(Array.isArray(list), 'messages is array');
t.assert.ok(list.length > 0, 'at least one message');
var found = list.some(function (m) { return m.id === msgId; });
t.assert.ok(found, 'sent message appears in list');
});
s.test('search conversations', async function (t) {
var r = await api.get('/search?q=chat-runner');
var list = Array.isArray(r) ? r : (r.data || r.conversations || r.results || []);
t.assert.ok(Array.isArray(list), 'search returns array');
if (list.length === 0) {
t.warn('Search returned empty — may need time for indexing');
}
});
s.test('mark read', async function (t) {
try {
await api.post('/read/' + convId, {});
t.assert.ok(true, 'mark read succeeded');
} catch (e) {
t.warn('mark read failed: ' + e.message);
}
});
});
})();

View File

@@ -0,0 +1,9 @@
{
"id": "chat-runner",
"icon": "💬",
"type": "test-runner",
"title": "Chat Runner",
"auth": "admin",
"version": "0.1.0",
"description": "Integration tests for Chat package — conversations, messaging, search."
}

View File

@@ -0,0 +1,59 @@
/**
* Notes Runner — notes/folders suite
*
* Tests folder CRUD and note-folder association.
*/
(function () {
'use strict';
var api = window.NR.api;
sw.testing.suite('notes/folders', async function (s) {
var folderId, noteId;
s.test('create folder', async function (t) {
var r = await api.post('/folders', {
name: 'runner-test-folder-' + Date.now()
});
t.assert.ok(r.id, 'folder has id');
t.assert.ok(r.name, 'folder has name');
folderId = r.id;
s.track('folder', folderId);
});
s.test('create note in folder', async function (t) {
t.assert.ok(folderId, 'folderId from previous test');
var r = await api.post('/notes', {
title: 'Folder Note',
body: 'Note in test folder',
folder_id: folderId
});
t.assert.ok(r.id, 'note has id');
t.assert.eq(r.folder_id, folderId, 'note assigned to folder');
noteId = r.id;
s.track('note', noteId);
});
s.test('move note to folder', async function (t) {
var f2 = await api.post('/folders', {
name: 'runner-move-target-' + Date.now()
});
s.track('folder', f2.id);
t.assert.ok(noteId, 'noteId from previous test');
await api.post('/notes/move', {
note_id: noteId,
folder_id: f2.id
});
var note = await api.get('/notes/' + noteId);
t.assert.eq(note.folder_id, f2.id, 'note moved to new folder');
});
s.test('list folders', async function (t) {
var r = await api.get('/folders');
t.assert.ok(Array.isArray(r), 'response is array');
var found = r.some(function (f) { return f.id === folderId; });
t.assert.ok(found, 'created folder appears in list');
});
});
})();

View File

@@ -0,0 +1,64 @@
/**
* Notes Runner — Entry Point
*
* Boot SDK, load test modules in dependency order.
* Each module is an IIFE that registers suites via sw.testing.suite().
*/
(async function () {
'use strict';
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) {
console.warn('[NotesRunner] SDK boot failed:', e.message);
}
window.NR = {
base: window.__BASE__ || '',
api: sw.api.ext('notes')
};
var surfaceId = 'notes-runner';
var assetBase = '/surfaces/' + surfaceId + '/js/';
if (window.NR.base) assetBase = window.NR.base + assetBase;
var modules = [
'notes-crud.js',
'folders.js',
'tags-search.js'
];
var loaded = 0;
function loadNext() {
if (loaded >= modules.length) { onReady(); 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('[NotesRunner] Failed to load: ' + modules[loaded]);
loaded++; loadNext();
};
document.body.appendChild(script);
}
function onReady() {
console.log('[NotesRunner] All modules loaded — ' + sw.testing.suites().length + ' suites registered');
var manifest = window.__MANIFEST__ || {};
if (manifest.id === surfaceId) {
window.location.href = (window.__BASE__ || '') + '/s/test-runners';
}
}
loadNext();
})();

View File

@@ -0,0 +1,63 @@
/**
* Notes Runner — notes/crud suite
*
* Tests basic note CRUD operations via the Notes ext API.
*/
(function () {
'use strict';
var api = window.NR.api;
sw.testing.suite('notes/crud', async function (s) {
var noteId;
s.test('create note', async function (t) {
var r = await api.post('/notes', {
title: 'Runner Test Note',
body: '# Hello from notes-runner\n\nTest body content.'
});
t.assert.ok(r.id, 'note has id');
t.assert.eq(r.title, 'Runner Test Note', 'title matches');
t.assert.ok(r.body, 'body present');
t.assert.ok(r.created_at, 'created_at present');
noteId = r.id;
s.track('note', noteId);
});
s.test('get note by id', async function (t) {
t.assert.ok(noteId, 'noteId from previous test');
var r = await api.get('/notes/' + noteId);
t.assert.eq(r.id, noteId, 'id matches');
t.assert.eq(r.title, 'Runner Test Note', 'title matches');
});
s.test('update note', async function (t) {
t.assert.ok(noteId, 'noteId from previous test');
var r = await api.put('/notes/' + noteId, {
title: 'Updated Test Note',
body: '# Updated\n\nNew body.'
});
t.assert.eq(r.title, 'Updated Test Note', 'title updated');
});
s.test('list notes', async function (t) {
var r = await api.get('/notes');
t.assert.ok(Array.isArray(r), 'response is array');
var found = r.some(function (n) { return n.id === noteId; });
t.assert.ok(found, 'created note appears in list');
});
s.test('delete note', async function (t) {
t.assert.ok(noteId, 'noteId from previous test');
await api.del('/notes/' + noteId);
// Verify gone
try {
await api.get('/notes/' + noteId);
t.assert.ok(false, 'expected 404 after delete');
} catch (e) {
t.assert.ok(true, 'note not found after delete');
}
noteId = null;
});
});
})();

View File

@@ -0,0 +1,66 @@
/**
* Notes Runner — notes/tags-search suite
*
* Tests tags, search, and backlinks.
*/
(function () {
'use strict';
var api = window.NR.api;
sw.testing.suite('notes/tags-search', async function (s) {
var noteA, noteB;
s.beforeAll(async function () {
noteA = await api.post('/notes', {
title: 'Tag Test A',
body: 'First note with [[Tag Test B]] link and some searchable content: xyzzy.'
});
s.track('note', noteA.id);
noteB = await api.post('/notes', {
title: 'Tag Test B',
body: 'Second note — backlink target.'
});
s.track('note', noteB.id);
});
s.test('set tags on note', async function (t) {
await api.put('/tags/' + noteA.id, {
tags: ['runner-test', 'integration']
});
var tags = await api.get('/tags/' + noteA.id);
t.assert.ok(Array.isArray(tags), 'tags is array');
t.assert.ok(tags.indexOf('runner-test') !== -1, 'runner-test tag present');
t.assert.ok(tags.indexOf('integration') !== -1, 'integration tag present');
});
s.test('list all tags', async function (t) {
var tags = await api.get('/tags');
t.assert.ok(Array.isArray(tags), 'tags is array');
var found = tags.some(function (tag) {
var name = typeof tag === 'string' ? tag : (tag.tag || tag.name || '');
return name === 'runner-test';
});
t.assert.ok(found, 'runner-test tag appears in global list');
});
s.test('search notes', async function (t) {
var r = await api.get('/search?q=xyzzy');
t.assert.ok(Array.isArray(r), 'search returns array');
var found = r.some(function (n) { return n.id === noteA.id; });
t.assert.ok(found, 'noteA found by search term');
});
s.test('backlinks', async function (t) {
var links = await api.get('/backlinks/' + noteB.id);
t.assert.ok(Array.isArray(links), 'backlinks is array');
if (links.length === 0) {
t.warn('No backlinks found — wikilink resolution may not have matched by title');
} else {
var found = links.some(function (l) { return l.source_id === noteA.id; });
t.assert.ok(found, 'noteA is a backlink source for noteB');
}
});
});
})();

View File

@@ -0,0 +1,9 @@
{
"id": "notes-runner",
"icon": "📝",
"type": "test-runner",
"title": "Notes Runner",
"auth": "admin",
"version": "0.1.0",
"description": "Integration tests for Notes package — CRUD, folders, tags, backlinks, search."
}

View File

@@ -0,0 +1,58 @@
/**
* Renderer Runner — Entry Point
*
* Boot SDK, load test modules.
* Each module is an IIFE that registers suites via sw.testing.suite().
*/
(async function () {
'use strict';
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) {
console.warn('[RendRunner] SDK boot failed:', e.message);
}
var surfaceId = 'renderer-runner';
var assetBase = '/surfaces/' + surfaceId + '/js/';
var rbase = window.__BASE__ || '';
if (rbase) assetBase = rbase + assetBase;
var modules = [
'renderer-contract.js'
];
var loaded = 0;
function loadNext() {
if (loaded >= modules.length) { onReady(); 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('[RendRunner] Failed to load: ' + modules[loaded]);
loaded++; loadNext();
};
document.body.appendChild(script);
}
function onReady() {
console.log('[RendRunner] All modules loaded — ' + sw.testing.suites().length + ' suites registered');
var manifest = window.__MANIFEST__ || {};
if (manifest.id === surfaceId) {
window.location.href = (window.__BASE__ || '') + '/s/test-runners';
}
}
loadNext();
})();

View File

@@ -0,0 +1,51 @@
/**
* Renderer Runner — renderer/contract suite
*
* Tests the sw.renderers registry contract.
* Requires mermaid-renderer to be installed and loaded.
*/
(function () {
'use strict';
sw.testing.suite('renderer/contract', async function (s) {
s.test('sw.renderers exists', async function (t) {
t.assert.ok(sw.renderers, 'sw.renderers is defined');
t.assert.ok(typeof sw.renderers.register === 'function', 'register is a function');
t.assert.ok(typeof sw.renderers.list === 'function', 'list is a function');
t.assert.ok(typeof sw.renderers.runBlockRenderers === 'function', 'runBlockRenderers is a function');
t.assert.ok(typeof sw.renderers.runPostRenderers === 'function', 'runPostRenderers is a function');
});
s.test('mermaid renderer registered', async function (t) {
var info = sw.renderers.list();
t.assert.ok(info.block, 'block renderers list exists');
t.assert.ok(Array.isArray(info.block), 'block is array');
var mermaid = info.block.find(function (r) { return r.name === 'mermaid'; });
if (!mermaid) {
t.warn('Mermaid renderer not found in block list — extension may not have loaded yet');
return;
}
t.assert.ok(mermaid, 'mermaid renderer found');
t.assert.eq(mermaid.pattern, 'mermaid', 'pattern matches "mermaid"');
});
s.test('block renderer match', async function (t) {
// Test that runBlockRenderers returns true for 'mermaid' lang
// We need a container element for the render call
t.browserOnly(function () {
var container = document.createElement('div');
var handled = sw.renderers.runBlockRenderers('mermaid', 'graph LR; A-->B', container);
t.assert.ok(handled, 'mermaid block was handled by a renderer');
});
});
s.test('unmatched block returns false', async function (t) {
t.browserOnly(function () {
var container = document.createElement('div');
var handled = sw.renderers.runBlockRenderers('nonexistent-lang-xyz', 'code', container);
t.assert.eq(handled, false, 'unmatched lang returns false');
});
});
});
})();

View File

@@ -0,0 +1,9 @@
{
"id": "renderer-runner",
"icon": "🎨",
"type": "test-runner",
"title": "Renderer Runner",
"auth": "admin",
"version": "0.1.0",
"description": "Contract tests for renderer registry — registration, matching, block rendering."
}

View File

@@ -0,0 +1,62 @@
/**
* Schedules Runner — Entry Point
*
* Boot SDK, load test modules.
* Each module is an IIFE that registers suites via sw.testing.suite().
*/
(async function () {
'use strict';
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) {
console.warn('[SchedRunner] SDK boot failed:', e.message);
}
window.SR = {
base: window.__BASE__ || '',
api: '/api/v1/schedules'
};
var surfaceId = 'schedules-runner';
var assetBase = '/surfaces/' + surfaceId + '/js/';
if (window.SR.base) assetBase = window.SR.base + assetBase;
var modules = [
'schedules-crud.js'
];
var loaded = 0;
function loadNext() {
if (loaded >= modules.length) { onReady(); 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('[SchedRunner] Failed to load: ' + modules[loaded]);
loaded++; loadNext();
};
document.body.appendChild(script);
}
function onReady() {
console.log('[SchedRunner] All modules loaded — ' + sw.testing.suites().length + ' suites registered');
var manifest = window.__MANIFEST__ || {};
if (manifest.id === surfaceId) {
window.location.href = (window.__BASE__ || '') + '/s/test-runners';
}
}
loadNext();
})();

View File

@@ -0,0 +1,65 @@
/**
* Schedules Runner — schedules/crud suite
*
* Tests schedule CRUD via the kernel schedules API.
*/
(function () {
'use strict';
var API = window.SR.api;
sw.testing.suite('schedules/crud', async function (s) {
var schedId;
s.test('create schedule', async function (t) {
var r = await sw.api.post(API, {
name: 'runner-test-' + Date.now(),
cron_expr: '0 0 * * *',
script: 'print("hello from schedule runner")',
enabled: false
});
t.assert.ok(r.id, 'schedule has id');
t.assert.ok(r.name, 'schedule has name');
schedId = r.id;
s.track('schedule', schedId);
});
s.test('get schedule', async function (t) {
t.assert.ok(schedId, 'schedId from previous test');
var r = await sw.api.get(API + '/' + schedId);
t.assert.eq(r.id, schedId, 'id matches');
t.assert.ok(r.cron_expr, 'cron_expr present');
});
s.test('update schedule', async function (t) {
t.assert.ok(schedId, 'schedId from previous test');
var r = await sw.api.put(API + '/' + schedId, {
name: 'runner-test-updated',
cron_expr: '30 2 * * *'
});
t.assert.eq(r.name, 'runner-test-updated', 'name updated');
});
s.test('run schedule', async function (t) {
t.assert.ok(schedId, 'schedId from previous test');
try {
await sw.api.post(API + '/' + schedId + '/run', {});
t.assert.ok(true, 'run endpoint returned successfully');
} catch (e) {
t.warn('Schedule run failed: ' + e.message);
}
});
s.test('delete schedule', async function (t) {
t.assert.ok(schedId, 'schedId from previous test');
await sw.api.del(API + '/' + schedId);
try {
await sw.api.get(API + '/' + schedId);
t.assert.ok(false, 'expected error after delete');
} catch (e) {
t.assert.ok(true, 'schedule not found after delete');
}
schedId = null;
});
});
})();

View File

@@ -0,0 +1,9 @@
{
"id": "schedules-runner",
"icon": "⏰",
"type": "test-runner",
"title": "Schedules Runner",
"auth": "admin",
"version": "0.1.0",
"description": "Integration tests for Schedules package — CRUD, run, enable/disable."
}

View File

@@ -127,11 +127,22 @@
});
}
// 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.manifest?.requires || runner.requires || [];
var requires = RUNNER_REQUIRES[runner.id] || runner.manifest?.requires || runner.requires || [];
if (!requires.length || !allPkgs) return { met: requires, missing: [] };
var installedIds = allPkgs.map(function (p) { return p.id; });
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]);
@@ -141,6 +152,14 @@
}
// 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);
@@ -149,6 +168,7 @@
try {
var r = await sw.testing.run();
setResults(r);
postResults('all', r);
} catch (e) {
setError('Run failed: ' + e.message);
}
@@ -162,8 +182,13 @@
setRunning(true);
setRunningSuite(runner.id);
var prefix = runner.id === 'icd-test-runner' ? 'icd/' :
runner.id === 'sdk-test-runner' ? 'sdk/' : 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 = [];
@@ -186,12 +211,14 @@
if (r.suites[0] && r.suites[0].status === 'skipped') summary.skipped++;
}
setResults({
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]);
@@ -332,7 +359,8 @@
// Filter results for this runner's suites
var prefix = r.id === 'icd-test-runner' ? 'icd/' :
r.id === 'sdk-test-runner' ? 'sdk/' : r.id + '/';
r.id === 'sdk-test-runner' ? 'sdk/' :
r.id.replace(/-runner$/, '') + '/';
var runnerSuites = props.results?.suites?.filter(function (s) {
return s.name.indexOf(prefix) === 0;
}) || [];

View File

@@ -6,6 +6,6 @@
"route": "/s/test-runners",
"auth": "admin",
"layout": "single",
"version": "0.1.0",
"version": "0.2.0",
"description": "Run and view results from all installed test-runner packages."
}

View File

@@ -0,0 +1,62 @@
/**
* Workflow Runner — Entry Point
*
* Boot SDK, load test modules.
* Each module is an IIFE that registers suites via sw.testing.suite().
*/
(async function () {
'use strict';
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) {
console.warn('[WfRunner] SDK boot failed:', e.message);
}
window.WR = {
base: window.__BASE__ || '',
api: '/api/v1/workflows'
};
var surfaceId = 'workflow-runner';
var assetBase = '/surfaces/' + surfaceId + '/js/';
if (window.WR.base) assetBase = window.WR.base + assetBase;
var modules = [
'workflow-lifecycle.js'
];
var loaded = 0;
function loadNext() {
if (loaded >= modules.length) { onReady(); 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('[WfRunner] Failed to load: ' + modules[loaded]);
loaded++; loadNext();
};
document.body.appendChild(script);
}
function onReady() {
console.log('[WfRunner] All modules loaded — ' + sw.testing.suites().length + ' suites registered');
var manifest = window.__MANIFEST__ || {};
if (manifest.id === surfaceId) {
window.location.href = (window.__BASE__ || '') + '/s/test-runners';
}
}
loadNext();
})();

View File

@@ -0,0 +1,87 @@
/**
* Workflow Runner — workflow/lifecycle suite
*
* Tests workflow definitions, instance creation, and stage progression.
* Requires the content-approval workflow package to be installed.
*/
(function () {
'use strict';
var API = window.WR.api;
sw.testing.suite('workflow/lifecycle', async function (s) {
var defId, instanceId;
s.test('list workflow definitions', async function (t) {
var r = await sw.api.get(API);
var list = Array.isArray(r) ? r : (r.data || []);
t.assert.ok(Array.isArray(list), 'definitions is array');
// Find content-approval definition
var def = list.find(function (d) {
return d.slug === 'content-approval' || d.name === 'Content Approval';
});
if (!def) {
t.warn('content-approval definition not found — package may need activation');
// Try to find any definition
if (list.length > 0) {
def = list[0];
t.warn('Using first available definition: ' + (def.name || def.slug));
} else {
t.skip('No workflow definitions available');
return;
}
}
t.assert.ok(def.id, 'definition has id');
defId = def.id;
});
s.test('create workflow instance', async function (t) {
if (!defId) { t.skip('No definition from previous test'); return; }
var r = await sw.api.post(API + '/' + defId + '/instances', {
data: {
title: 'Runner test submission',
body: 'Test content from workflow-runner',
category: 'blog'
}
});
t.assert.ok(r.id, 'instance has id');
t.assert.ok(r.status || r.current_stage !== undefined, 'instance has status/stage');
instanceId = r.id;
s.track('workflow', instanceId);
});
s.test('get workflow instance', async function (t) {
if (!defId || !instanceId) { t.skip('No instance from previous test'); return; }
var r = await sw.api.get(API + '/' + defId + '/instances/' + instanceId);
t.assert.eq(r.id, instanceId, 'instance id matches');
});
s.test('advance stage', async function (t) {
if (!defId || !instanceId) { t.skip('No instance from previous test'); return; }
try {
var r = await sw.api.post(API + '/' + defId + '/instances/' + instanceId + '/advance', {
data: {
title: 'Runner test submission',
body: 'Test content from workflow-runner',
category: 'blog'
}
});
t.assert.ok(true, 'advance succeeded');
} catch (e) {
// Stage advance may fail if form validation requires specific fields
t.warn('Stage advance failed: ' + e.message);
}
});
s.test('list instances', async function (t) {
if (!defId) { t.skip('No definition from previous test'); return; }
var r = await sw.api.get(API + '/' + defId + '/instances');
var list = Array.isArray(r) ? r : (r.data || []);
t.assert.ok(Array.isArray(list), 'instances is array');
if (instanceId) {
var found = list.some(function (i) { return i.id === instanceId; });
t.assert.ok(found, 'created instance appears in list');
}
});
});
})();

View File

@@ -0,0 +1,9 @@
{
"id": "workflow-runner",
"icon": "🔄",
"type": "test-runner",
"title": "Workflow Runner",
"auth": "admin",
"version": "0.1.0",
"description": "Integration tests for Workflow package — definitions, instances, stage progression."
}