Files
zeekay 4ae120919c feat(github): Hanzo AI GitHub App — PR review, issue triage, @hanzo mentions
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.
2026-07-03 21:24:23 -07:00

61 lines
1.9 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
buildMentionPrompt,
formatMentionComment,
mentionSystemPrompt,
type MentionContext,
} from '../src/mention.js';
const ctx: MentionContext = {
repo: 'hanzoai/extension',
number: 9,
isPullRequest: false,
title: 'How do I configure X?',
body: 'I cannot find the setting.',
comments: [
{ author: 'alice', body: 'Same here.' },
{ author: 'bob', body: 'Try the config file.' },
],
};
describe('buildMentionPrompt', () => {
it('carries the issue metadata, thread, and the question', () => {
const p = buildMentionPrompt(ctx, 'where is the setting?');
expect(p).toContain('issue hanzoai/extension#9');
expect(p).toContain('How do I configure X?');
expect(p).toContain('@alice: Same here.');
expect(p).toContain('@bob: Try the config file.');
expect(p).toContain('where is the setting?');
});
it('labels a PR thread as a pull request', () => {
const p = buildMentionPrompt({ ...ctx, isPullRequest: true }, 'q');
expect(p).toContain('pull request hanzoai/extension#9');
});
it('falls back to summarize for a bare mention', () => {
const p = buildMentionPrompt(ctx, '');
expect(p).toContain('Summarize the thread');
});
it('handles an empty thread', () => {
const p = buildMentionPrompt({ ...ctx, comments: [] }, 'q');
expect(p).toContain('(no prior comments)');
});
});
describe('formatMentionComment', () => {
it('appends the Hanzo attribution with model id', () => {
const out = formatMentionComment('Here is the answer.', 'zen5');
expect(out).toContain('Here is the answer.');
expect(out).toContain('_Answered by Hanzo AI (`zen5`)._');
});
});
describe('mentionSystemPrompt', () => {
it('frames a context-bound answer', () => {
expect(mentionSystemPrompt()).toContain('@hanzo');
expect(mentionSystemPrompt()).toContain('Do not invent facts');
});
});