mirror of
https://github.com/hanzoai/extension.git
synced 2026-11-01 18:02:17 +00:00
A Microsoft Outlook mail add-in embedding Hanzo AI into email — the highest-value law-office surface after Word. Mail add-in manifest (MailApp), Read + Compose command surfaces, ribbon button opens the task pane. Quick actions: Summarize thread, Draft reply, Extract deadlines & action items, Explain plainly — all streamed (askStream/SSE). - Reuses the Office add-in's pure core identically: config.ts (api.hanzo.ai /v1 gateway), chat.ts (ask/askStream/listModels), auth.ts (@hanzo/auth OAuth2+PKCE via roamingSettings + hk- API-key path). Shares the hanzo-office IAM client — no new auth surface. - New outlook-host.ts: Office.context.mailbox.item glue (read body/subject/ from; compose prepend/setSelectedData; displayReplyForm) + pure shaping helpers (stripHtml, collapseWhitespace, shapeMessage, formatFrom, mailMode). - build.js mirrors office (esbuild -> dist/, base-URL stamp, dev HTTPS serve). - 39 vitest tests pass; manifest validated by office-addin-manifest. - Registered packages/outlook in pnpm-workspace.yaml.
93 lines
3.2 KiB
TypeScript
93 lines
3.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
stripHtml,
|
|
collapseWhitespace,
|
|
shapeMessage,
|
|
formatFrom,
|
|
mailMode,
|
|
} from '../src/outlook-host';
|
|
|
|
describe('collapseWhitespace', () => {
|
|
it('trims each line and collapses inner runs of spaces/tabs', () => {
|
|
expect(collapseWhitespace(' a b \t c ')).toBe('a b c');
|
|
});
|
|
it('drops runs of blank lines down to one', () => {
|
|
expect(collapseWhitespace('a\n\n\n\nb')).toBe('a\n\nb');
|
|
});
|
|
it('trims leading and trailing blank lines', () => {
|
|
expect(collapseWhitespace('\n\n hi \n\n')).toBe('hi');
|
|
});
|
|
});
|
|
|
|
describe('stripHtml', () => {
|
|
it('turns <br> and block-close tags into newlines', () => {
|
|
expect(stripHtml('<p>Hi</p><p>Bye</p>')).toBe('Hi\nBye');
|
|
expect(stripHtml('one<br>two')).toBe('one\ntwo');
|
|
});
|
|
it('removes script/style blocks entirely', () => {
|
|
expect(stripHtml('<style>.a{color:red}</style>text<script>x=1</script>')).toBe('text');
|
|
});
|
|
it('decodes the common entities', () => {
|
|
expect(stripHtml('a & b <c> "d" 'e' f')).toBe(
|
|
'a & b <c> "d" \'e\' f',
|
|
);
|
|
});
|
|
it('strips arbitrary tags and collapses whitespace', () => {
|
|
expect(stripHtml('<div> <span>deadline</span> is <b>Friday</b> </div>')).toBe(
|
|
'deadline is Friday',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('formatFrom', () => {
|
|
it('renders "Name <addr>" when both present and differ', () => {
|
|
expect(formatFrom({ displayName: 'Jane Doe', emailAddress: '[email protected]' })).toBe(
|
|
'Jane Doe <[email protected]>',
|
|
);
|
|
});
|
|
it('falls back to the address when there is no display name', () => {
|
|
expect(formatFrom({ emailAddress: '[email protected]' })).toBe('[email protected]');
|
|
});
|
|
it('does not duplicate when name equals address', () => {
|
|
expect(formatFrom({ displayName: '[email protected]', emailAddress: '[email protected]' })).toBe(
|
|
'[email protected]',
|
|
);
|
|
});
|
|
it('undefined → empty string', () => {
|
|
expect(formatFrom(undefined)).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('shapeMessage', () => {
|
|
it('labels subject, from, then a body block separated by a blank line', () => {
|
|
const out = shapeMessage({
|
|
subject: 'Motion deadline',
|
|
from: 'Jane Doe <[email protected]>',
|
|
body: 'Please file by Friday.',
|
|
});
|
|
expect(out).toBe('Subject: Motion deadline\nFrom: Jane Doe <[email protected]>\n\nPlease file by Friday.');
|
|
});
|
|
it('omits empty fields (compose mode: no subject/from)', () => {
|
|
expect(shapeMessage({ subject: '', from: '', body: 'draft in progress' })).toBe('draft in progress');
|
|
});
|
|
it('omits the body block when the body is empty', () => {
|
|
expect(shapeMessage({ subject: 'Hi', from: '', body: ' ' })).toBe('Subject: Hi');
|
|
});
|
|
it('everything empty → empty string', () => {
|
|
expect(shapeMessage({ subject: '', from: '', body: '' })).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('mailMode', () => {
|
|
it('read when the item exposes a from', () => {
|
|
expect(mailMode(true, false)).toBe('read');
|
|
expect(mailMode(true, true)).toBe('read');
|
|
});
|
|
it('compose when there is no from but the body is writable', () => {
|
|
expect(mailMode(false, true)).toBe('compose');
|
|
});
|
|
it('unknown when neither', () => {
|
|
expect(mailMode(false, false)).toBe('unknown');
|
|
});
|
|
});
|