Files
extension/packages/imanage/test/parse.test.ts
T
Hanzo Dev 8de63fa05f feat(imanage): Hanzo AI for iManage Work — legal DMS panel + OAuth/Work-API proxy
New package @hanzo/imanage: an embedded-app panel over @hanzo/ai for iManage
Work (the dominant legal document management system), mirroring the
packages/procore OAuth + API-proxy + pure-core shape.

- OAuth2 Authorization Code against the iManage Control Center
  (/auth/oauth2/authorize + /token); server-side token exchange + refresh, the
  client secret never reaches the browser.
- Work API v2 wrappers (workspaces, folder children, document profile +
  content, search, gated profile write-back) scoped by customer + library in
  the path, X-Auth-Token auth, envelope/offset-limit pagination. Pure.
- Pure legal document-context assembly with text extraction (binary/OCR out of
  scope, reported honestly) and one truncation-honest windowing algorithm.
- Four AI actions — summarize document, extract key clauses/dates/parties,
  search-and-synthesize a matter, compare a document set — plus freeform ask,
  over a single grounded ask() path.
- Same-origin proxy keeps tokens + secret server-side; NEVER logs document
  content. Legal confidentiality posture documented in the README.
- 121 vitest tests (config/oauth/api/parse/documents/hanzo/actions/panel),
  tsc --noEmit clean, node build.js green.

Registers packages/imanage in pnpm-workspace.yaml (one line).
2026-07-04 02:01:19 -07:00

162 lines
5.6 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
parseLibraries,
parseWorkspaces,
parseWorkspaceDetail,
parseFolderChildren,
parseDocument,
parseSearchResults,
} from '../src/imanage-api.js';
// A realistic iManage Work API v2 document profile (envelope-wrapped, trimmed to
// the fields we surface). iManage separates coded fields from *_description labels.
const documentEnvelope = {
data: {
id: 'ACTIVE_US!4567.1',
name: 'Master Services Agreement - Acme Corp',
extension: 'DOCX',
size: 48213,
author: 'jsmith',
author_description: 'Jane Smith',
class: 'AGR',
class_description: 'Agreement',
type: 'DOC',
type_description: 'Document',
version: 1,
edit_date: '2026-06-20T14:00:00Z',
comment: 'Final executed copy',
},
};
describe('parse: libraries', () => {
it('reads id + name from an envelope and drops idless rows', () => {
const libs = parseLibraries({ data: [{ id: 'ACTIVE_US', name: 'Active (US)' }, { name: 'no id' }] });
expect(libs).toEqual([{ id: 'ACTIVE_US', name: 'Active (US)' }]);
});
it('falls id back to name (and vice versa)', () => {
expect(parseLibraries([{ id: 'X' }])[0]).toEqual({ id: 'X', name: 'X' });
});
});
describe('parse: workspaces', () => {
it('reads the matter fields from an envelope array and drops idless rows', () => {
const wss = parseWorkspaces({
data: [
{
id: 'ACTIVE_US!12',
name: 'Acme Corp / Project Falcon',
wstype: 'workspace',
description: 'M&A due diligence',
client: 'Acme Corp',
matter: '2026-0042',
database: 'ACTIVE_US',
},
{ name: 'no id' },
],
});
expect(wss).toHaveLength(1);
expect(wss[0]).toEqual({
id: 'ACTIVE_US!12',
name: 'Acme Corp / Project Falcon',
description: 'M&A due diligence',
client: 'Acme Corp',
matter: '2026-0042',
database: 'ACTIVE_US',
});
});
it('reads client/matter from custom1/custom2 fallbacks', () => {
const w = parseWorkspaces([{ id: 'W1', name: 'W', custom1: 'ClientCo', custom2: 'M-9' }])[0];
expect(w.client).toBe('ClientCo');
expect(w.matter).toBe('M-9');
});
it('parseWorkspaceDetail unwraps a single workspace envelope', () => {
const w = parseWorkspaceDetail({ data: { id: 'W2', name: 'Solo' } });
expect(w.id).toBe('W2');
expect(w.name).toBe('Solo');
});
});
describe('parse: document profile', () => {
it('unwraps the envelope and prefers *_description labels; lowercases the extension', () => {
const doc = parseDocument(documentEnvelope);
expect(doc.id).toBe('ACTIVE_US!4567.1');
expect(doc.name).toBe('Master Services Agreement - Acme Corp');
expect(doc.extension).toBe('docx');
expect(doc.size).toBe(48213);
expect(doc.author).toBe('Jane Smith');
expect(doc.class).toBe('Agreement');
expect(doc.type).toBe('Document');
expect(doc.version).toBe(1);
expect(doc.editDate).toBe('2026-06-20T14:00:00Z');
expect(doc.comment).toBe('Final executed copy');
});
it('tolerates a bare (unwrapped) profile and missing fields', () => {
const doc = parseDocument({ id: 'D1', name: 'X' });
expect(doc.id).toBe('D1');
expect(doc.extension).toBe('');
expect(doc.size).toBe(0);
expect(doc.author).toBe('');
});
});
describe('parse: folder children (folders + documents split)', () => {
const children = {
data: [
{ id: 'FOLDER!1', name: 'Contracts', type: 'folder', wstype: 'folder' },
{
id: 'ACTIVE_US!4567.1',
name: 'MSA - Acme',
type: 'document',
extension: 'DOCX',
author_description: 'Jane Smith',
edit_date: '2026-06-20',
},
{ id: 'ACTIVE_US!4568.2', name: 'NDA', extension: 'PDF' },
],
};
it('separates typed folders from documents and untyped-with-extension rows', () => {
const { folders, documents } = parseFolderChildren(children);
expect(folders).toEqual([{ id: 'FOLDER!1', name: 'Contracts' }]);
expect(documents.map((d) => d.id)).toEqual(['ACTIVE_US!4567.1', 'ACTIVE_US!4568.2']);
expect(documents[0].author).toBe('Jane Smith');
expect(documents[1].extension).toBe('pdf');
});
it('treats an untyped row with no extension as a folder (never guessed a document)', () => {
const { folders, documents } = parseFolderChildren([{ id: 'C1', name: 'Correspondence' }]);
expect(folders).toEqual([{ id: 'C1', name: 'Correspondence' }]);
expect(documents).toEqual([]);
});
it('reads a nested { document } child object', () => {
const { documents } = parseFolderChildren([
{ type: 'document', document: { id: 'D9', name: 'Deed', extension: 'PDF' } },
]);
expect(documents[0]).toMatchObject({ id: 'D9', name: 'Deed', extension: 'pdf' });
});
it('drops idless rows from both buckets', () => {
const { folders, documents } = parseFolderChildren([{ type: 'folder', name: 'nameless' }, { extension: 'DOCX', name: 'no id doc' }]);
expect(folders).toEqual([]);
expect(documents).toEqual([]);
});
});
describe('parse: search results', () => {
it('reads document rows from an envelope and drops idless rows', () => {
const results = parseSearchResults({
data: [
{ id: 'ACTIVE_US!4567.1', name: 'MSA - Acme', extension: 'DOCX', author_description: 'Jane Smith', class_description: 'Agreement' },
{ name: 'no id' },
],
});
expect(results).toHaveLength(1);
expect(results[0].name).toBe('MSA - Acme');
expect(results[0].class).toBe('Agreement');
});
it('handles a bare array shape', () => {
expect(parseSearchResults([{ id: 'D1', name: 'X', extension: 'PDF' }])).toHaveLength(1);
});
});