Feat v0.7.2 package runners ci gate (#56)
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
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
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #56.
This commit is contained in:
59
packages/chat-runner/js/conversations.js
Normal file
59
packages/chat-runner/js/conversations.js
Normal 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;
|
||||
});
|
||||
});
|
||||
})();
|
||||
63
packages/chat-runner/js/main.js
Normal file
63
packages/chat-runner/js/main.js
Normal 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();
|
||||
})();
|
||||
60
packages/chat-runner/js/messaging.js
Normal file
60
packages/chat-runner/js/messaging.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
9
packages/chat-runner/manifest.json
Normal file
9
packages/chat-runner/manifest.json
Normal 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."
|
||||
}
|
||||
59
packages/notes-runner/js/folders.js
Normal file
59
packages/notes-runner/js/folders.js
Normal 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');
|
||||
});
|
||||
});
|
||||
})();
|
||||
64
packages/notes-runner/js/main.js
Normal file
64
packages/notes-runner/js/main.js
Normal 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();
|
||||
})();
|
||||
63
packages/notes-runner/js/notes-crud.js
Normal file
63
packages/notes-runner/js/notes-crud.js
Normal 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;
|
||||
});
|
||||
});
|
||||
})();
|
||||
66
packages/notes-runner/js/tags-search.js
Normal file
66
packages/notes-runner/js/tags-search.js
Normal 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
9
packages/notes-runner/manifest.json
Normal file
9
packages/notes-runner/manifest.json
Normal 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."
|
||||
}
|
||||
58
packages/renderer-runner/js/main.js
Normal file
58
packages/renderer-runner/js/main.js
Normal 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();
|
||||
})();
|
||||
51
packages/renderer-runner/js/renderer-contract.js
Normal file
51
packages/renderer-runner/js/renderer-contract.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
9
packages/renderer-runner/manifest.json
Normal file
9
packages/renderer-runner/manifest.json
Normal 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."
|
||||
}
|
||||
62
packages/schedules-runner/js/main.js
Normal file
62
packages/schedules-runner/js/main.js
Normal 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();
|
||||
})();
|
||||
65
packages/schedules-runner/js/schedules-crud.js
Normal file
65
packages/schedules-runner/js/schedules-crud.js
Normal 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;
|
||||
});
|
||||
});
|
||||
})();
|
||||
9
packages/schedules-runner/manifest.json
Normal file
9
packages/schedules-runner/manifest.json
Normal 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."
|
||||
}
|
||||
@@ -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]);
|
||||
@@ -299,7 +326,7 @@
|
||||
<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 (${props.suiteCount} suites)</button>`
|
||||
: 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">
|
||||
@@ -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;
|
||||
}) || [];
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
62
packages/workflow-runner/js/main.js
Normal file
62
packages/workflow-runner/js/main.js
Normal 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();
|
||||
})();
|
||||
87
packages/workflow-runner/js/workflow-lifecycle.js
Normal file
87
packages/workflow-runner/js/workflow-lifecycle.js
Normal 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
9
packages/workflow-runner/manifest.json
Normal file
9
packages/workflow-runner/manifest.json
Normal 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."
|
||||
}
|
||||
Reference in New Issue
Block a user