import assert from 'node:assert/strict'; import { createHmac, generateKeyPairSync } from 'node:crypto'; import { createServer } from 'node:http'; import test from 'node:test'; const DIST_ROOT = new URL('../dist/src/', import.meta.url); async function importFresh(relativePath) { return import(new URL(`${relativePath}?t=${Date.now()}-${Math.random()}`, DIST_ROOT).href); } function setEnv(updates) { const previous = new Map(); for (const [key, value] of Object.entries(updates)) { previous.set(key, process.env[key]); if (value === undefined || value === null) delete process.env[key]; else process.env[key] = value; } return () => { for (const [key, value] of previous.entries()) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } }; } test('GitHub Agent help exposes routing actions', async () => { const { executeGitHubAgentAction } = await importFresh('./runtime.js'); const help = await executeGitHubAgentAction({ action: 'agent.help' }); assert.equal(help.ok, true); assert.ok(help.actions.includes('sin.github.app.routing.status')); assert.ok(help.actions.includes('sin.github.webhook.route')); assert.ok(help.actions.includes('sin.github.issue.comment.as_app')); }); test('routeGitHubWebhook prefers installation.app_id over shared central path', async () => { const restoreEnv = setEnv({ SIN_GITHUB_APP_ROUTING_JSON: JSON.stringify({ mode: 'central-endpoint-routing', defaultWebhookPath: '/api/webhooks/github-apps', apps: [ { appIdEnv: 'SIN_HERMES_GITHUB_APP_ID', appSlug: 'sin-hermes', preferredAgentSlug: 'sin-hermesbote', botName: 'SIN-Hermes[bot]', webhookPath: '/api/webhooks/github-apps', webhookSecretEnv: 'SIN_HERMES_GITHUB_APP_WEBHOOK_SECRET', }, { appIdEnv: 'SIN_BUGBOUNTY_GITHUB_APP_ID', appSlug: 'sin-bugbounty', preferredAgentSlug: 'sin-bugbounty', botName: 'SIN-BugBounty[bot]', webhookPath: '/api/webhooks/github-apps', webhookSecretEnv: 'SIN_BUGBOUNTY_GITHUB_APP_WEBHOOK_SECRET', }, ], }), SIN_HERMES_GITHUB_APP_ID: '123456', SIN_HERMES_GITHUB_APP_WEBHOOK_SECRET: 'secret-1', SIN_BUGBOUNTY_GITHUB_APP_ID: '345678', SIN_BUGBOUNTY_GITHUB_APP_WEBHOOK_SECRET: 'secret-3', }); try { const { routeGitHubWebhook } = await importFresh('./github-app-routing.js'); const rawBody = JSON.stringify({ action: 'opened', installation: { id: 987654321, app_id: 345678 }, repository: { full_name: 'OpenSIN-AI/OpenSIN-backend' }, }); const signature256 = `sha256=${createHmac('sha256', 'secret-3').update(rawBody).digest('hex')}`; const result = await routeGitHubWebhook({ rawBody, routePath: '/api/webhooks/github-apps', signature256, eventName: 'pull_request', deliveryId: 'delivery-central-test', }); assert.equal(result.ok, true); assert.equal(result.route.agentSlug, 'sin-bugbounty'); assert.equal(result.route.botName, 'SIN-BugBounty[bot]'); } finally { restoreEnv(); } }); test('commentIssueAsGitHubApp exchanges installation token and posts comment', async () => { const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048, privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, publicKeyEncoding: { type: 'pkcs1', format: 'pem' }, }); let commentRequestBody = ''; const server = createServer((req, res) => { if (req.method === 'POST' && req.url === '/app/installations/987654321/access_tokens') { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ token: 'installation-token-1', expires_at: '2030-01-01T00:00:00Z' })); return; } if (req.method === 'POST' && req.url === '/repos/OpenSIN-AI/OpenSIN-backend/issues/42/comments') { const chunks = []; req.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); req.on('end', () => { commentRequestBody = Buffer.concat(chunks).toString('utf8'); res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ id: 9001, html_url: 'https://github.com/OpenSIN-AI/OpenSIN-backend/issues/42#issuecomment-9001' })); }); return; } res.writeHead(404, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'not_found' })); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); const port = typeof address === 'object' && address ? address.port : 0; const restoreEnv = setEnv({ SIN_GITHUB_API_BASE_URL: `http://127.0.0.1:${port}`, SIN_GITHUB_APP_ROUTING_JSON: JSON.stringify({ defaultMode: 'hybrid', defaultWebhookPath: '/github/webhook', apps: [ { agentSlug: 'sin-hermesbote', appSlug: 'sin-hermes', botName: 'SIN-Hermes[bot]', appId: 123456, privateKeyPem: privateKey, webhookSecret: 'secret-1', }, ], }), }); try { const { commentIssueAsGitHubApp } = await importFresh('./github-app-routing.js'); const result = await commentIssueAsGitHubApp({ repo: 'OpenSIN-AI/OpenSIN-backend', issueNumber: 42, body: 'Hello from SIN-Hermes[bot]', agentSlug: 'sin-hermesbote', installationId: 987654321, }); assert.equal(result.ok, true); assert.equal(result.agentSlug, 'sin-hermesbote'); assert.equal(result.commentId, 9001); assert.match(commentRequestBody, /Hello from SIN-Hermes\[bot\]/); } finally { restoreEnv(); await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); } });