Files
zeekay 94476b43e2 feat(hubspot): Hanzo AI for HubSpot — CRM card + AI over records (@hanzo/hubspot)
A HubSpot public app on the developer-projects platform: a CRM card UI
extension (crm.record.tab, on contacts/companies/deals) backed by two
serverless functions. Summarize a record, draft an email, suggest next
steps, or ask — grounded in the record + its associated records — and
write the result back as a Note or Task.

Built on the published @hanzo/ai@^0.2.0 headless client (createAiClient,
/v1 chat.completions + models.list) + @hanzo/iam. The Hanzo API key
(HANZO_API_KEY secret) and the per-portal OAuth token live only in the
serverless backend; the browser never sees them.

Pure, unit-tested core (config, hanzo call + context windowing + the four
actions, HubSpot CRM v3 request shaping/parsing, OAuth token exchange,
record→context assembly, Note/Task write-back payloads). 75 vitest tests
green, tsc --noEmit clean, dual ESM+CJS build so the CJS serverless
runtime can require it.
2026-07-03 20:53:24 -07:00

49 lines
1.4 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
HANZO_API_BASE_URL,
DEFAULT_MODEL,
crmApiRoot,
hubspotAuthorizeURL,
hubspotTokenURL,
isObjectType,
SUPPORTED_OBJECT_TYPES,
RECORD_CHAR_BUDGET,
} from '../src/config.js';
describe('Hanzo gateway config', () => {
it('points at api.hanzo.ai with no /api/ prefix', () => {
expect(HANZO_API_BASE_URL).toBe('https://api.hanzo.ai');
expect(HANZO_API_BASE_URL).not.toContain('/api/');
});
it('defaults to a zen model', () => {
expect(DEFAULT_MODEL).toMatch(/^zen/);
});
});
describe('HubSpot endpoints', () => {
it('crmApiRoot is /crm/v3 on the api host', () => {
expect(crmApiRoot()).toBe('https://api.hubapi.com/crm/v3');
});
it('authorize is on the app host, token is on the api host', () => {
expect(hubspotAuthorizeURL()).toBe('https://app.hubspot.com/oauth/authorize');
expect(hubspotTokenURL()).toBe('https://api.hubapi.com/oauth/v1/token');
});
});
describe('isObjectType', () => {
it('accepts the three supported types', () => {
for (const t of SUPPORTED_OBJECT_TYPES) expect(isObjectType(t)).toBe(true);
});
it('rejects anything else', () => {
expect(isObjectType('tickets')).toBe(false);
expect(isObjectType(undefined)).toBe(false);
expect(isObjectType('')).toBe(false);
});
});
describe('RECORD_CHAR_BUDGET', () => {
it('is a sane positive cap', () => {
expect(RECORD_CHAR_BUDGET).toBeGreaterThan(1000);
});
});