mirror of
https://github.com/hanzoai/extension.git
synced 2026-10-31 21:37:51 +00:00
A GitHub App (Octokit + webhooks) built on @hanzo/ai (headless model client) and @hanzo/iam (identity). Reviews pull requests, triages new issues, and answers @hanzo mentions in issue/PR comments. Pure cores (network-free, exhaustively unit-tested): - webhook.ts X-Hub-Signature-256 HMAC verify (constant time) + event routing - review.ts diff windowing/truncation, prompt assembly, review formatting - triage.ts issue prompt, severity/label parsing (labels ∩ repo labels) - mention.ts thread-context answer prompt Impure edges compose them: config (env boundary), hanzo (thin @hanzo/ai — the one model path), github-api (thin Octokit .request wrappers), app (App + handlers → route → dispatch), server (health + graceful shutdown). 84 vitest tests green; tsc --noEmit clean; build succeeds. Webhook path /v1/github/webhooks (Hanzo /v1, never /api/). Secrets env-only.
116 lines
3.7 KiB
TypeScript
116 lines
3.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { createHmac } from 'node:crypto';
|
|
import {
|
|
sign,
|
|
verifySignature,
|
|
parseMention,
|
|
routeEvent,
|
|
SIGNATURE_HEADER,
|
|
type RoutablePayload,
|
|
} from '../src/webhook.js';
|
|
|
|
const SECRET = 'topsecret';
|
|
const BODY = JSON.stringify({ action: 'opened', hello: 'world' });
|
|
|
|
describe('sign / verifySignature (HMAC)', () => {
|
|
it('sign matches a manual sha256 HMAC in GitHub format', () => {
|
|
const expected = 'sha256=' + createHmac('sha256', SECRET).update(BODY, 'utf8').digest('hex');
|
|
expect(sign(BODY, SECRET)).toBe(expected);
|
|
});
|
|
|
|
it('accepts a valid signature', () => {
|
|
expect(verifySignature(BODY, sign(BODY, SECRET), SECRET)).toBe(true);
|
|
});
|
|
|
|
it('rejects a tampered body', () => {
|
|
const sig = sign(BODY, SECRET);
|
|
expect(verifySignature(BODY + 'x', sig, SECRET)).toBe(false);
|
|
});
|
|
|
|
it('rejects the wrong secret', () => {
|
|
expect(verifySignature(BODY, sign(BODY, 'other'), SECRET)).toBe(false);
|
|
});
|
|
|
|
it('rejects a missing signature or secret without throwing', () => {
|
|
expect(verifySignature(BODY, undefined, SECRET)).toBe(false);
|
|
expect(verifySignature(BODY, sign(BODY, SECRET), '')).toBe(false);
|
|
});
|
|
|
|
it('rejects a wrong-length signature (constant-time guard)', () => {
|
|
expect(verifySignature(BODY, 'sha256=deadbeef', SECRET)).toBe(false);
|
|
});
|
|
|
|
it('exposes the header name it verifies', () => {
|
|
expect(SIGNATURE_HEADER).toBe('x-hub-signature-256');
|
|
});
|
|
});
|
|
|
|
describe('parseMention', () => {
|
|
it('extracts the question after @hanzo', () => {
|
|
expect(parseMention('hey @hanzo why is this failing?')).toBe('why is this failing?');
|
|
});
|
|
|
|
it('is case-insensitive and matches at the start', () => {
|
|
expect(parseMention('@Hanzo explain the diff')).toBe('explain the diff');
|
|
});
|
|
|
|
it('returns empty string for a bare mention', () => {
|
|
expect(parseMention('@hanzo')).toBe('');
|
|
});
|
|
|
|
it('returns null when @hanzo is absent', () => {
|
|
expect(parseMention('this mentions nobody')).toBeNull();
|
|
});
|
|
|
|
it('does not match an email-like substring', () => {
|
|
expect(parseMention('mail me at [email protected]')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('routeEvent', () => {
|
|
it('routes pull_request opened/synchronize/reopened to review', () => {
|
|
for (const action of ['opened', 'synchronize', 'reopened']) {
|
|
const p: RoutablePayload = { action, pull_request: {} };
|
|
expect(routeEvent('pull_request', p)).toEqual({ kind: 'review_pr' });
|
|
}
|
|
});
|
|
|
|
it('ignores other pull_request actions', () => {
|
|
const r = routeEvent('pull_request', { action: 'closed', pull_request: {} });
|
|
expect(r.kind).toBe('ignore');
|
|
});
|
|
|
|
it('routes issues opened to triage', () => {
|
|
expect(routeEvent('issues', { action: 'opened', issue: {} })).toEqual({ kind: 'triage_issue' });
|
|
});
|
|
|
|
it('ignores non-opened issues actions', () => {
|
|
expect(routeEvent('issues', { action: 'edited', issue: {} }).kind).toBe('ignore');
|
|
});
|
|
|
|
it('routes issue_comment created with @hanzo to answer_mention', () => {
|
|
const r = routeEvent('issue_comment', {
|
|
action: 'created',
|
|
comment: { body: '@hanzo what is this?' },
|
|
});
|
|
expect(r).toEqual({ kind: 'answer_mention', question: 'what is this?' });
|
|
});
|
|
|
|
it('ignores an issue_comment without @hanzo', () => {
|
|
const r = routeEvent('issue_comment', { action: 'created', comment: { body: 'nice work' } });
|
|
expect(r.kind).toBe('ignore');
|
|
});
|
|
|
|
it('ignores edited/deleted comments', () => {
|
|
const r = routeEvent('issue_comment', {
|
|
action: 'edited',
|
|
comment: { body: '@hanzo hi' },
|
|
});
|
|
expect(r.kind).toBe('ignore');
|
|
});
|
|
|
|
it('ignores unknown events', () => {
|
|
expect(routeEvent('star', {}).kind).toBe('ignore');
|
|
});
|
|
});
|