mirror of
https://github.com/hanzoai/extension.git
synced 2026-10-31 21:57:06 +00:00
Extension-App panel + Connect webhook that summarize, extract key terms, risk-flag clauses, and draft memos over DocuSign envelopes, on the shared @hanzo/ai + @hanzo/iam foundation over api.hanzo.ai /v1. - OAuth Authorization Code Grant (account-d/account.docusign.com), server-side client_secret, base_uri discovery via /oauth/userinfo (pure shaping). - eSignature REST v2.1 wrappers (envelopes, documents, combined PDF). - Connect webhook: X-DocuSign-Signature-N HMAC-SHA256 verify over raw body, constant-time, multi-key; envelope-completed auto-summarize pipeline. - PDF text extraction (pdf.js, pure itemsToText), document windowing/truncation, one 'ask' path shared by the four actions, the panel, and the webhook. - 103 vitest tests (config, oauth, api, webhook HMAC, pdf, hanzo, actions, envelope pipeline, panel). tsc clean, esbuild build green. - Registers packages/docusign in pnpm-workspace.yaml (one line).
85 lines
3.3 KiB
TypeScript
85 lines
3.3 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { analyzeEnvelope } from '../src/envelope';
|
|
import { apiBaseUrl } from '../src/docusign-oauth';
|
|
import type { PdfDoc, TextRun } from '../src/pdf-text';
|
|
import type { AiClient } from '../src/client';
|
|
|
|
const API = apiBaseUrl('https://demo.docusign.net');
|
|
const ACCT = 'acct-1';
|
|
const ENV = 'env-9';
|
|
|
|
// Fake DocuSign REST: the envelope metadata GET returns JSON; the combined
|
|
// document GET returns opaque PDF bytes (the fake pdf loader ignores them).
|
|
function fakeFetch(): typeof fetch {
|
|
return (async (url: any) => {
|
|
const u = String(url);
|
|
if (u.endsWith(`/envelopes/${ENV}`)) {
|
|
return new Response(JSON.stringify({ envelopeId: ENV, status: 'completed', emailSubject: 'Master Services Agreement' }), { status: 200 });
|
|
}
|
|
if (u.endsWith(`/envelopes/${ENV}/documents/combined`)) {
|
|
return new Response(new Uint8Array([1, 2, 3, 4]), { status: 200, headers: { 'Content-Type': 'application/pdf' } });
|
|
}
|
|
return new Response('not found', { status: 404 });
|
|
}) as unknown as typeof fetch;
|
|
}
|
|
|
|
// Fake pdf.js loader: two pages of agreement text.
|
|
const pdfLoader = async (): Promise<PdfDoc> => ({
|
|
numPages: 2,
|
|
async getPage(n: number) {
|
|
const map: Record<number, TextRun[]> = {
|
|
1: [{ str: 'This' }, { str: 'Agreement' }, { str: 'governs.', hasEOL: true }],
|
|
2: [{ str: 'Payment' }, { str: 'is' }, { str: 'net-30.', hasEOL: true }],
|
|
};
|
|
return { async getTextContent() { return { items: map[n] }; } };
|
|
},
|
|
});
|
|
|
|
// Fake Hanzo client: echoes the task + how much agreement text it saw.
|
|
function fakeClient(seen: { userContent?: string }): AiClient {
|
|
return {
|
|
chat: {
|
|
completions: {
|
|
async create(params) {
|
|
seen.userContent = (params.messages[1] as { content: string }).content;
|
|
return { choices: [{ message: { role: 'assistant', content: 'SUMMARY' } }] };
|
|
},
|
|
},
|
|
},
|
|
models: { async list() { return []; } },
|
|
};
|
|
}
|
|
|
|
describe('analyzeEnvelope (full pipeline, all effects injected)', () => {
|
|
it('fetches metadata + combined PDF, extracts text, and runs the action', async () => {
|
|
const seen: { userContent?: string } = {};
|
|
const out = await analyzeEnvelope({
|
|
apiBase: API,
|
|
accountId: ACCT,
|
|
envelopeId: ENV,
|
|
accessToken: 'AT',
|
|
actionId: 'summarize',
|
|
fetch: fakeFetch(),
|
|
pdfLoader,
|
|
ask: { client: fakeClient(seen) },
|
|
});
|
|
|
|
expect(out.result).toBe('SUMMARY');
|
|
expect(out.envelope.emailSubject).toBe('Master Services Agreement');
|
|
expect(out.totalPages).toBe(2);
|
|
expect(out.includedPages).toBe(2);
|
|
expect(out.truncated).toBe(false);
|
|
// the extracted agreement text reached the model, titled by the envelope
|
|
expect(seen.userContent).toContain('This Agreement governs.');
|
|
expect(seen.userContent).toContain('Payment is net-30.');
|
|
expect(seen.userContent).toContain('Agreement: Master Services Agreement');
|
|
});
|
|
|
|
it('surfaces a DocuSign error when the envelope fetch fails', async () => {
|
|
const failing = (async () => new Response('nope', { status: 401 })) as unknown as typeof fetch;
|
|
await expect(
|
|
analyzeEnvelope({ apiBase: API, accountId: ACCT, envelopeId: ENV, accessToken: 'AT', actionId: 'summarize', fetch: failing, pdfLoader }),
|
|
).rejects.toThrow(/401/);
|
|
});
|
|
});
|