/** * SDK Test Runner — Domain: git-board (v0.38.5) * * Tests the real-world library composition pattern: * gitea-client library (connection types, exports) → git-board consumer * (dependency, tools, delegation). Validates the full package lifecycle * with realistic manifests and Starlark scripts. * Requires admin role. */ (function () { 'use strict'; var T = window.SDKR; if (!T) return; // ── Minimal zip builder ──────────────────────── function buildZip(entries) { var localHeaders = []; var centralHeaders = []; var offset = 0; entries.forEach(function (entry) { var nameBytes = new TextEncoder().encode(entry.name); var dataBytes = new TextEncoder().encode(entry.content); var local = new Uint8Array(30 + nameBytes.length + dataBytes.length); var dv = new DataView(local.buffer); dv.setUint32(0, 0x04034b50, true); dv.setUint16(4, 20, true); dv.setUint16(8, 0, true); dv.setUint16(10, 0, true); dv.setUint16(12, 0, true); dv.setUint32(14, crc32(dataBytes), true); dv.setUint32(18, dataBytes.length, true); dv.setUint32(22, dataBytes.length, true); dv.setUint16(26, nameBytes.length, true); dv.setUint16(28, 0, true); local.set(nameBytes, 30); local.set(dataBytes, 30 + nameBytes.length); localHeaders.push(local); var central = new Uint8Array(46 + nameBytes.length); var cdv = new DataView(central.buffer); cdv.setUint32(0, 0x02014b50, true); cdv.setUint16(4, 20, true); cdv.setUint16(6, 20, true); cdv.setUint16(28, nameBytes.length, true); cdv.setUint32(16, crc32(dataBytes), true); cdv.setUint32(20, dataBytes.length, true); cdv.setUint32(24, dataBytes.length, true); cdv.setUint32(42, offset, true); central.set(nameBytes, 46); centralHeaders.push(central); offset += local.length; }); var centralSize = centralHeaders.reduce(function (s, c) { return s + c.length; }, 0); var eocd = new Uint8Array(22); var edv = new DataView(eocd.buffer); edv.setUint32(0, 0x06054b50, true); edv.setUint16(8, entries.length, true); edv.setUint16(10, entries.length, true); edv.setUint32(12, centralSize, true); edv.setUint32(16, offset, true); var result = new Uint8Array(offset + centralSize + 22); var pos = 0; localHeaders.forEach(function (h) { result.set(h, pos); pos += h.length; }); centralHeaders.forEach(function (h) { result.set(h, pos); pos += h.length; }); result.set(eocd, pos); return new Blob([result], { type: 'application/zip' }); } var crcTable = null; function crc32(bytes) { if (!crcTable) { crcTable = new Uint32Array(256); for (var n = 0; n < 256; n++) { var c = n; for (var k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); crcTable[n] = c; } } var crc = 0xFFFFFFFF; for (var i = 0; i < bytes.length; i++) crc = crcTable[(crc ^ bytes[i]) & 0xFF] ^ (crc >>> 8); return (crc ^ 0xFFFFFFFF) >>> 0; } function makePkgFile(manifest, extraFiles) { var entries = [{ name: 'manifest.json', content: JSON.stringify(manifest) }]; if (extraFiles) entries = entries.concat(extraFiles); var blob = buildZip(entries); return new File([blob], manifest.id + '.pkg', { type: 'application/zip' }); } // ── Starlark script content ──────────────────── // Realistic but self-contained — no real HTTP calls needed. var LIB_HTTP_HELPERS = [ 'def auth_headers(conn):', ' token = conn.get("api_token", "")', ' if not token:', ' return {}', ' return {"Authorization": "token " + token}', '', 'def _base(conn):', ' url = conn.get("base_url", "")', ' if url.endswith("/"):', ' url = url[:-1]', ' return url', '', 'def gitea_get(conn, path):', ' url = _base(conn) + "/api/v1" + path', ' resp = http.get(url=url, headers=auth_headers(conn))', ' if int(resp["status"]) >= 400:', ' return None', ' body = resp.get("body", "")', ' if not body:', ' return None', ' return json.decode(body)', '', 'def gitea_post(conn, path, data):', ' url = _base(conn) + "/api/v1" + path', ' hdrs = dict(auth_headers(conn), **{"Content-Type": "application/json"})', ' resp = http.post(url=url, body=json.encode(data), headers=hdrs)', ' if int(resp["status"]) >= 400:', ' return None', ' body = resp.get("body", "")', ' if not body:', ' return None', ' return json.decode(body)', '', 'def gitea_patch(conn, path, data):', ' url = _base(conn) + "/api/v1" + path', ' hdrs = dict(auth_headers(conn), **{"Content-Type": "application/json", "X-HTTP-Method-Override": "PATCH"})', ' resp = http.post(url=url, body=json.encode(data), headers=hdrs)', ' if int(resp["status"]) >= 400:', ' return None', ' body = resp.get("body", "")', ' if not body:', ' return None', ' return json.decode(body)', '', 'def resp(status, data):', ' return {"status": status, "body": json.encode(data), "headers": {"Content-Type": "application/json"}}', ].join('\n'); var LIB_REPOS = [ 'load("star/http_helpers.star", "gitea_get")', '', 'def get_repos(conn):', ' data = gitea_get(conn, "/repos/search?limit=50&sort=updated")', ' if data == None:', ' return None', ' repos = data if type(data) == "list" else data.get("data", [])', ' out = []', ' for r in repos:', ' out.append({', ' "full_name": r.get("full_name", ""),', ' "name": r.get("name", ""),', ' "owner": r.get("owner", {}).get("login", ""),', ' })', ' return out', ].join('\n'); var LIB_ISSUES = [ 'load("star/http_helpers.star", "gitea_get", "gitea_post", "gitea_patch")', '', 'def get_issues(conn, owner, repo, state):', ' return []', '', 'def get_issue(conn, owner, repo, number):', ' return {"number": number, "title": "stub", "body": "", "state": "open", "labels": [], "assignee": "", "created_at": "", "comments": []}', '', 'def create_issue(conn, owner, repo, title, body, labels):', ' return {"number": 0, "title": title, "html_url": ""}', '', 'def update_issue(conn, owner, repo, number, title, body, state):', ' return {"number": number, "state": state or "open", "title": title or "stub"}', '', 'def add_comment(conn, owner, repo, number, body):', ' return {"id": 0, "body": body}', ].join('\n'); var LIB_CI = [ 'load("star/http_helpers.star", "gitea_get")', '', 'def get_ci_status(conn, owner, repo, ref):', ' return {"ref": ref, "state": "pending", "statuses": [], "count": 0}', ].join('\n'); var LIB_SCRIPT = [ 'load("star/http_helpers.star", "gitea_get", "gitea_post", "gitea_patch", "auth_headers", "resp")', 'load("star/repos.star", _repos_get = "get_repos")', 'load("star/issues.star", _issues_get = "get_issues", _issue_get = "get_issue", _issue_create = "create_issue", _issue_update = "update_issue", _comment_add = "add_comment")', 'load("star/ci.star", _ci_get = "get_ci_status")', '', '# Re-export for lib.require() visibility', 'get_repos = _repos_get', 'get_issues = _issues_get', 'get_issue = _issue_get', 'create_issue = _issue_create', 'update_issue = _issue_update', 'add_comment = _comment_add', 'get_ci_status = _ci_get', '', 'def get_prs(conn, owner, repo, state):', ' return []', '', 'def on_request(req):', ' conn = connections.get("gitea")', ' if conn == None:', ' return resp(400, {"error": "no connection"})', ' return resp(200, {"ok": True})', ].join('\n'); var CONSUMER_SCRIPT = [ 'gitea = lib.require("{LIB_ID}")', '', 'def _conn():', ' return connections.get("gitea")', '', 'def on_request(req):', ' conn = _conn()', ' if conn == None:', ' return {"status": 400, "body": json.encode({"error": "no connection"}), "headers": {"Content-Type": "application/json"}}', ' return {"status": 200, "body": json.encode({"ok": True}), "headers": {"Content-Type": "application/json"}}', '', 'def on_tool_call(tool_name, params):', ' conn = _conn()', ' if conn == None:', ' return {"error": "no connection"}', ' if tool_name == "list_issues":', ' data = gitea.get_issues(conn, params.get("owner",""), params.get("repo",""), "open")', ' return {"issues": data or [], "count": len(data or [])}', ' return {"error": "unknown tool"}', ].join('\n'); // ── Tests ────────────────────────────────────── T.registerDomain('git-board', async function () { if (!window.sw || !sw.isAdmin) { await T.test('git-board', 'guard', 'skip — not admin', async function () { T.assert(true, 'skipped (user is not admin)'); }); return; } var ts = Date.now(); var libId = 'gb-test-lib-' + ts; var consumerId = 'gb-test-board-' + ts; var connType = 'gb-test-gitea-' + ts; var connId = null; // ── 1. Install library with multi-file structure + connection type ── await T.test('git-board', 'install-library', 'install gitea-client-style library with multi-file and connection type', async function () { var manifest = { id: libId, title: 'GB Test Gitea Client', type: 'library', tier: 'starlark', version: '1.0.1', exports: ['get_repos', 'get_issues', 'get_issue', 'create_issue', 'update_issue', 'add_comment', 'get_prs', 'get_ci_status'], permissions: ['api.http', 'connections.read', 'db.read', 'db.write'], connections: [{ type: connType, label: 'Test Gitea Instance', fields: { base_url: { type: 'url', required: 'true', label: 'Server URL' }, api_token: { type: 'secret', required: 'true', label: 'API Token' }, org: { type: 'string', required: 'false', label: 'Default Org' } }, scopes: ['global', 'team', 'personal'] }], api_routes: [ { method: 'GET', path: '/repos' }, { method: 'GET', path: '/issues' } ], db_tables: { repos: { columns: { full_name: 'text', owner: 'text', name: 'text' }, indexes: [] } }, schema_version: 1 }; var file = makePkgFile(manifest, [ { name: 'script.star', content: LIB_SCRIPT }, { name: 'star/http_helpers.star', content: LIB_HTTP_HELPERS }, { name: 'star/repos.star', content: LIB_REPOS }, { name: 'star/issues.star', content: LIB_ISSUES }, { name: 'star/ci.star', content: LIB_CI } ]); var r = await sw.api.admin.packages.install(file); T.assert(r && r.id === libId, 'library should install'); T.assert(r.type === 'library', 'type should be library'); }); T.cleanup.push(async function () { if (libId) try { await sw.api.admin.packages.del(libId); } catch (_) {} }); // ── 2. Grant library permissions ── await T.test('git-board', 'grant-lib-perms', 'grant library permissions', async function () { await sw.api.post('/api/v1/admin/extensions/' + libId + '/permissions/grant-all', {}); T.assert(true, 'permissions granted'); }); // ── 3. Connection type appears ── await T.test('git-board', 'conn-type-registered', 'connection type registered from library manifest', async function () { var types = await sw.api.connectionTypes.list(); T.assert(Array.isArray(types), 'should be array'); var found = types.find(function (t) { return t.type === connType; }); T.assert(found, 'connection type should appear: ' + connType); T.assert(found.package_id === libId, 'package_id should be library'); T.assert(found.fields && found.fields.api_token, 'should have api_token field'); T.assert(found.fields.api_token.type === 'secret', 'api_token should be secret type'); T.assert(found.fields.org, 'should have org field'); }); // ── 4. Install consumer with dependency + tools ── await T.test('git-board', 'install-consumer', 'install git-board-style consumer with dependency and tools', async function () { var manifest = { id: consumerId, title: 'GB Test Board', type: 'full', tier: 'starlark', route: '/s/' + consumerId, auth: 'authenticated', layout: 'single', version: '0.2.0', permissions: ['connections.read'], dependencies: {}, api_routes: [ { method: 'GET', path: '/repos' }, { method: 'GET', path: '/board' }, { method: 'GET', path: '/ci/*' } ], tools: [ { name: 'list_issues', description: 'List issues', parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true } } }, { name: 'get_issue', description: 'Get issue', parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true }, number: { type: 'number', required: true } } }, { name: 'create_issue', description: 'Create issue', parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true }, title: { type: 'string', required: true } } }, { name: 'update_issue', description: 'Update issue', parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true }, number: { type: 'number', required: true } } }, { name: 'add_comment', description: 'Add comment', parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true }, number: { type: 'number', required: true }, body: { type: 'string', required: true } } }, { name: 'list_pull_requests', description: 'List PRs', parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true } } }, { name: 'get_ci_status', description: 'Get CI status', parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true }, ref: { type: 'string', required: true } } } ], settings: { default_owner: { type: 'string', label: 'Default Owner', default: '' }, default_repo: { type: 'string', label: 'Default Repo', default: '' } } }; manifest.dependencies[libId] = '>=1.0.0'; var script = CONSUMER_SCRIPT.replace('{LIB_ID}', libId); var file = makePkgFile(manifest, [ { name: 'script.star', content: script } ]); var r = await sw.api.admin.packages.install(file); T.assert(r && r.id === consumerId, 'consumer should install'); T.assert(r.type === 'full', 'type should be full'); }); T.cleanup.push(async function () { if (consumerId) try { await sw.api.admin.packages.del(consumerId); } catch (_) {} }); // ── 5. Grant consumer permissions ── await T.test('git-board', 'grant-consumer-perms', 'grant consumer permissions', async function () { await sw.api.post('/api/v1/admin/extensions/' + consumerId + '/permissions/grant-all', {}); T.assert(true, 'permissions granted'); }); // ── 6. Dependency recorded ── await T.test('git-board', 'dependency-recorded', 'consumer depends on library', async function () { var r = await sw.api.admin.packages.dependencies(consumerId); // SDK unwraps {data:[...]} to bare array var deps = Array.isArray(r) ? r : (r && r.data) || []; T.assert(deps.length > 0, 'should have dependencies'); var dep = deps.find(function (d) { return d.library_id === libId; }); T.assert(dep, 'dependency on library should exist'); T.assert(dep.version_spec === '>=1.0.0', 'version spec should match'); }); // ── 7. Tools registered ── await T.test('git-board', 'tools-registered', 'consumer has 7 tools registered', async function () { var r = await sw.api.admin.packages.get(consumerId); T.assert(r, 'package should exist'); var tools = r.tools || (r.manifest && r.manifest.tools) || []; T.assert(tools.length === 7, 'should have 7 tools, got ' + tools.length); var names = tools.map(function (t) { return t.name; }).sort(); T.assert(names.indexOf('list_issues') >= 0, 'should include list_issues'); T.assert(names.indexOf('get_ci_status') >= 0, 'should include get_ci_status'); T.assert(names.indexOf('add_comment') >= 0, 'should include add_comment'); }); // ── 8. Library consumers listed ── await T.test('git-board', 'consumers-listed', 'library shows consumer in consumers list', async function () { var r = await sw.api.admin.packages.consumers(libId); // SDK unwraps {data:[...]} to bare array var consumers = Array.isArray(r) ? r : (r && r.data) || []; T.assert(consumers.length > 0, 'should have consumers'); var found = consumers.find(function (c) { return c.consumer_id === consumerId; }); T.assert(found, 'consumer should appear in library consumers list'); }); // ── 9. Uninstall protection ── await T.test('git-board', 'uninstall-protection', 'cannot uninstall library while consumer exists', async function () { var threw = false; try { await sw.api.admin.packages.del(libId); } catch (e) { threw = true; var msg = (e && e.message) || String(e); T.assert(msg.indexOf('409') >= 0 || msg.indexOf('active') >= 0 || msg.indexOf('depend') >= 0, 'should be 409 or dependency error: ' + msg); } T.assert(threw, 'delete should have thrown'); }); // ── 10. Create connection of library type ── await T.test('git-board', 'create-connection', 'create connection using library-declared type', async function () { var r = await sw.api.admin.connections.create({ type: connType, package_id: libId, name: 'GB Test Connection', config: { base_url: 'https://test.example.com', api_token: 'test-token-gb', org: 'test-org' } }); T.assert(r && r.id, 'should create connection'); T.assert(r.type === connType, 'type should match'); connId = r.id; }); T.cleanup.push(async function () { if (connId) try { await sw.api.admin.connections.del(connId); } catch (_) {} }); // ── Cleanup ────────────────────────────────── await T.test('git-board', 'cleanup-connection', 'delete test connection', async function () { if (connId) { await sw.api.admin.connections.del(connId); connId = null; } T.assert(true, 'connection deleted'); }); await T.test('git-board', 'cleanup-consumer', 'uninstall consumer', async function () { await sw.api.admin.packages.del(consumerId); consumerId = null; T.assert(true, 'consumer uninstalled'); }); await T.test('git-board', 'cleanup-library', 'uninstall library', async function () { await sw.api.admin.packages.del(libId); libId = null; T.assert(true, 'library uninstalled'); }); await T.test('git-board', 'types-removed', 'connection type removed after library uninstall', async function () { var types = await sw.api.connectionTypes.list(); var found = types.find(function (t) { return t.type === connType; }); T.assert(!found, 'type should not appear after library uninstall'); }); }); })();