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

56
ci/run-surface-tests.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# ═══════════════════════════════════════════════
# Surface Test Runner — CI Entrypoint
# ═══════════════════════════════════════════════
#
# Authenticates as admin, navigates to the test-runners surface
# via Playwright, runs all test suites, and asserts zero failures.
#
# Prerequisites:
# - Server running at $SERVER_URL (default: http://localhost:3000)
# - npx playwright install chromium
# - ADMIN_USER / ADMIN_PASS env vars (default: admin/admin)
#
# Exit codes: 0 = all tests passed, 1 = failures detected
set -euo pipefail
SERVER_URL="${SERVER_URL:-http://localhost:3000}"
ADMIN_USER="${ADMIN_USER:-admin}"
ADMIN_PASS="${ADMIN_PASS:-admin}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${YELLOW}═══ Surface Test Runner ═══${NC}"
echo " Server: ${SERVER_URL}"
# ── Authenticate ─────────────────────────────
echo -e "${YELLOW}Authenticating...${NC}"
TOKEN=$(curl -sf -X POST "${SERVER_URL}/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${ADMIN_USER}\",\"password\":\"${ADMIN_PASS}\"}" \
| node -e "process.stdin.resume(); let d=''; process.stdin.on('data',c=>d+=c); process.stdin.on('end',()=>{try{console.log(JSON.parse(d).token)}catch(e){process.exit(1)}})")
if [ -z "$TOKEN" ]; then
echo -e "${RED}Failed to authenticate${NC}"
exit 1
fi
echo -e "${GREEN}Authenticated${NC}"
# ── Run via Playwright ───────────────────────
echo -e "${YELLOW}Running surface tests via Playwright...${NC}"
node "$(dirname "$0")/surface-test-driver.js" \
--server="${SERVER_URL}" \
--token="${TOKEN}"
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
echo -e "${GREEN}═══ All surface tests passed ═══${NC}"
else
echo -e "${RED}═══ Surface tests FAILED ═══${NC}"
fi
exit $EXIT_CODE

141
ci/surface-test-driver.js Normal file
View File

@@ -0,0 +1,141 @@
#!/usr/bin/env node
/**
* Surface Test Driver — Playwright-based CI runner
*
* Navigates to /s/test-runners as admin, clicks "Run All",
* waits for completion, then fetches results from the API.
*
* Usage:
* node ci/surface-test-driver.js --server=http://localhost:3000 --token=TOKEN
*
* Exit codes: 0 = all passed, 1 = failures
*/
'use strict';
const { chromium } = require('playwright');
// Parse args
const args = {};
process.argv.slice(2).forEach(a => {
const [k, v] = a.replace(/^--/, '').split('=');
args[k] = v;
});
const SERVER = args.server || 'http://localhost:3000';
const TOKEN = args.token || '';
if (!TOKEN) {
console.error('ERROR: --token is required');
process.exit(1);
}
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
// Set auth cookie/token
await context.addCookies([{
name: 'token',
value: TOKEN,
domain: new URL(SERVER).hostname,
path: '/',
}]);
const page = await context.newPage();
// Collect console errors
const consoleErrors = [];
page.on('console', msg => {
if (msg.type() === 'error') consoleErrors.push(msg.text());
});
try {
// Navigate to test runners surface
console.log('Navigating to test-runners surface...');
await page.goto(SERVER + '/s/test-runners', { waitUntil: 'networkidle', timeout: 30000 });
// Wait for runners to load
console.log('Waiting for runners to load...');
await page.waitForSelector('[data-testid="run-all"], button', { timeout: 30000 });
// Wait a bit for all runner scripts to finish loading
await page.waitForTimeout(3000);
// Click "Run All" button
console.log('Clicking Run All...');
const runAllBtn = await page.$('button:has-text("Run All")');
if (!runAllBtn) {
console.error('ERROR: "Run All" button not found');
await browser.close();
process.exit(1);
}
await runAllBtn.click();
// Wait for results — poll until running state clears
console.log('Waiting for test completion...');
// The running indicator disappears when tests finish
// Max wait: 5 minutes
const maxWait = 300000;
const start = Date.now();
let done = false;
while (!done && (Date.now() - start) < maxWait) {
await page.waitForTimeout(2000);
// Check if results are available via API
try {
const resp = await page.evaluate(async (serverUrl) => {
const r = await fetch(serverUrl + '/api/v1/admin/test-runners/results', {
credentials: 'include'
});
if (r.status === 200) return await r.json();
return null;
}, SERVER);
if (resp && resp.summary) {
done = true;
console.log('\n═══ Results ═══');
console.log(` Total: ${resp.summary.total}`);
console.log(` Passed: ${resp.summary.passed}`);
console.log(` Failed: ${resp.summary.failed}`);
console.log(` Warned: ${resp.summary.warned}`);
console.log(` Skipped: ${resp.summary.skipped}`);
console.log(` Duration: ${resp.duration_ms}ms`);
if (resp.summary.failed > 0) {
console.log('\n═══ Failures ═══');
for (const suite of (resp.suites || [])) {
for (const test of (suite.tests || [])) {
if (test.status === 'failed') {
console.log(`${suite.name} > ${test.name}: ${test.detail || 'failed'}`);
}
}
}
await browser.close();
process.exit(1);
}
await browser.close();
process.exit(0);
}
} catch (e) {
// Results not ready yet
}
}
if (!done) {
console.error('ERROR: Tests did not complete within timeout');
await browser.close();
process.exit(1);
}
} catch (e) {
console.error('Driver error:', e.message);
if (consoleErrors.length > 0) {
console.error('\nBrowser console errors:');
consoleErrors.forEach(e => console.error(' ' + e));
}
await browser.close();
process.exit(1);
}
})();

21
ci/wait-for-healthy.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# ═══════════════════════════════════════════════
# Wait for Armature server to be healthy
# ═══════════════════════════════════════════════
# Usage: ./ci/wait-for-healthy.sh [URL] [MAX_RETRIES]
set -euo pipefail
HOST="${1:-http://localhost:3000}"
MAX_RETRIES="${2:-60}"
echo "Waiting for ${HOST} to be healthy..."
for i in $(seq 1 "$MAX_RETRIES"); do
if curl -sf "${HOST}/api/v1/auth/login" -o /dev/null 2>/dev/null; then
echo "Server healthy after ${i}s"
exit 0
fi
sleep 1
done
echo "Server not healthy after ${MAX_RETRIES}s"
exit 1