← Public packages
@charlesmatte/granola
Search and read Granola meeting notes, summaries and transcripts from any agent; manage webhooks and legal holds safely.
test/unit.test.ts
388 lines · 22.9 KB · TypeScriptimport { describe, expect, mock, test } from 'bun:test'
const secretSets: Array<Record<string, unknown>> = []
let secretSetFails = false
const mcpHub: Record<string, Record<string, (a: unknown) => Promise<unknown>>> = {}
const mcpCalls: Array<unknown> = []
mock.module('kody:runtime', () => ({
kody: {
mcp: mcpHub,
async secretList() {
return { secrets: [
{ name: 'granola-work', scope: 'user', allowed_hosts: ['public-api.granola.ai'] },
{ name: 'granola-side', scope: 'user', allowed_hosts: [] },
{ name: 'github-token', scope: 'user', allowed_hosts: ['api.github.com'] },
] }
},
async secretSet(input: Record<string, unknown>) {
if (secretSetFails) throw new Error('package not allowed to write secrets')
secretSets.push({ name: input.name, hasValue: typeof input.value === 'string' })
return { ok: true }
},
async mcpServerList() { return { servers: [] } },
async mcpServerAdd(i: Record<string, unknown>) { return { state: 'pending_auth', authUrl: 'https://kody.example/auth', name: i.name } },
},
}))
const { paginate } = await import('../src/lib/paginate.ts')
const { transcriptToText, speakerLabel } = await import('../src/lib/transcript.ts')
const { checkWebhookUrl, mutationMode } = await import('../src/lib/guard.ts')
const { custodianInputs } = await import('../src/lib/holds.ts')
const { accountFromSecretName, resolveAccount } = await import('../src/lib/account.ts')
const { errorDetail, retryDelayMs, transport, buildUrl } = await import('../src/lib/client.ts')
const { DATE_PATTERN } = await import('../src/lib/validate.ts')
const { folderTree } = await import('../src/folders/list.ts')
const { makeFake } = await import('./fake-granola.ts')
const { parseQuery, matchNote, foldWithMap } = await import('../src/lib/search.ts')
const { TokenBucket, mapLimit } = await import('../src/lib/ratelimit.ts')
const { limiter } = await import('../src/notes/search.ts')
const { extractCitations, mcpText } = await import('../src/lib/mcp.ts')
limiter.bucket = new TokenBucket(1e9, 1e9)
const use = (fake: ReturnType<typeof makeFake>) => {
transport.fetch = fake.fetchImpl as typeof fetch
return fake
}
describe('pure helpers', () => {
test('account ↔ secret name', () => {
expect(resolveAccount({ account: 'work' })).toEqual({ account: 'work', secretName: 'granola-work' })
expect(resolveAccount({ account: 'work', secretName: 'my-key' }).secretName).toBe('my-key')
expect(accountFromSecretName('granola-acme-legal')).toBe('acme-legal')
expect(accountFromSecretName('github-token')).toBeNull()
})
test('paginate follows hasMore/cursor and trims to limit', async () => {
const data = Array.from({ length: 70 }, (_, i) => i)
const sizes: Array<number> = []
const r = await paginate<number>(async (cursor, size) => {
sizes.push(size)
const start = cursor ? Number(cursor) : 0
return { items: data.slice(start, start + size), hasMore: start + size < data.length, cursor: start + size < data.length ? String(start + size) : null }
}, { limit: 65, maxPageSize: 30, paginate: true })
expect(r.items.length).toBe(65)
expect(sizes).toEqual([30, 30, 5])
expect(r.hasMore).toBe(true)
const one = await paginate<number>(async () => ({ items: [1, 2], hasMore: true, cursor: 'x' }), { limit: 100, maxPageSize: 30, paginate: false })
expect(one.pages).toBe(1)
const stuck = await paginate<number>(async () => ({ items: [1], hasMore: true, cursor: 'same' }), { limit: 100, maxPageSize: 30, cursor: 'same', paginate: true })
expect(stuck.pages).toBe(1)
})
test('transcript text: labels, merge, offsets', () => {
expect(speakerLabel({ speaker: { source: 'microphone', diarization_label: 'Speaker A' } })).toBe('Speaker A')
expect(speakerLabel({ speaker: { source: 'speaker' } })).toBe('Other participants')
const t = transcriptToText([
{ speaker: { source: 'microphone', attribution: 'me' }, text: 'Hi', start_time: '2026-01-01T10:00:00Z' },
{ speaker: { source: 'microphone', attribution: 'me' }, text: 'there', start_time: '2026-01-01T10:00:03Z' },
{ speaker: { source: 'speaker', attribution: 'them' }, text: 'Hello', start_time: '2026-01-01T10:01:05Z' },
])
expect(t).toBe('[0:00] Me: Hi there\n[1:05] Them: Hello')
expect(transcriptToText([{ speaker: { source: 'speaker', name: 'Ana' }, text: 'x' }], { timestamps: false })).toBe('Ana: x')
})
test('folder tree paths', () => {
const t = folderTree([
{ id: 'fol_1', object: 'folder', name: 'Clients', parent_folder_id: null },
{ id: 'fol_2', object: 'folder', name: 'Acme', parent_folder_id: 'fol_1' },
])
expect(t.flat.find((f) => f.id === 'fol_2')?.path).toBe('Clients / Acme')
expect(t.roots[0]?.children.length).toBe(1)
})
test('webhook url guard', () => {
expect(checkWebhookUrl('https://example.com/x')).toBe('https://example.com/x')
for (const bad of ['http://example.com', 'https://localhost/x', 'https://10.0.0.5/x', 'https://192.168.1.2', 'https://u:p@example.com', 'nope']) {
expect(() => checkWebhookUrl(bad)).toThrow()
}
})
test('mutation mode', () => {
expect(mutationMode({}).execute).toBe(false)
expect(mutationMode({ confirm: true }).execute).toBe(true)
expect(mutationMode({ confirm: true, dryRun: true }).execute).toBe(false)
})
test('custodians, dates, errors, urls', () => {
expect(custodianInputs(['a@example.com', 'usr_ABCDEFGHIJKLMN'])).toEqual([{ email: 'a@example.com' }, { id: 'usr_ABCDEFGHIJKLMN' }])
expect(() => custodianInputs(['bob'])).toThrow()
expect(DATE_PATTERN.test('2026-01-27')).toBe(true)
expect(DATE_PATTERN.test('2026-01-27T15:30:00Z')).toBe(true)
expect(DATE_PATTERN.test('last week')).toBe(false)
expect(errorDetail({ error: { code: 'TRANSCRIPT_TOO_LARGE', message: 'big' } })).toEqual({ code: 'TRANSCRIPT_TOO_LARGE', message: 'big' })
expect(retryDelayMs('2', 0)).toBe(2000)
expect(retryDelayMs(null, 2)).toBe(4000)
expect(() => buildUrl('https://evil.example/v1/notes')).toThrow()
expect(buildUrl('/v1/notes', { page_size: 5, cursor: undefined })).toBe('https://public-api.granola.ai/v1/notes?page_size=5')
})
})
describe('exports against a fake Granola API', () => {
test('notes/list paginates, sends bearer placeholder, filters locally', async () => {
const fake = use(makeFake())
const listNotes = (await import('../src/notes/list.ts')).default
const r = await listNotes({ account: 'work', limit: 65 })
expect(r.count).toBe(65)
expect(r.pages).toBe(3)
expect(fake.calls.map((c) => c.query.page_size)).toEqual(['30', '30', '5'])
expect(fake.calls[0]?.auth).toBe('Bearer {{secret:granola-work|scope=user}}')
const f = await listNotes({ account: 'work', titleContains: 'customer', ownerEmail: 'OTHER@example.com', limit: 65 })
expect(f.notes.every((n: any) => n.title.startsWith('Customer') && n.owner.email === 'other@example.com')).toBe(true)
await expect(listNotes({ limit: 5 })).rejects.toThrow(/"account" is required/)
await expect(listNotes({ account: 'work', bogus: 1 })).rejects.toThrow(/unknown input key/)
await expect(listNotes({ account: 'work', createdAfter: 'yesterday' })).rejects.toThrow()
})
test('notes/get falls back to paged transcript on 413', async () => {
const fake = use(makeFake({ tooLarge: true, transcriptItems: 250 }))
const getNote = (await import('../src/notes/get.ts')).default
const id = fake.notes[0]!.id
const note = await getNote({ account: 'work', noteId: id, includeTranscript: true, transcriptFormat: 'both' })
expect(note.transcriptSource).toBe('paged')
expect(note.transcriptItemCount).toBe(250)
expect(note.summary_markdown).toBe('## Summary')
expect(String(note.transcript_text).split('\n')[0]).toBe('[0:00] Me: line 0')
})
test('notes/get inline transcript and 404 hint', async () => {
const fake = use(makeFake())
const getNote = (await import('../src/notes/get.ts')).default
const note = await getNote({ account: 'work', noteId: fake.notes[1]!.id, includeTranscript: true })
expect(note.transcriptSource).toBe('inline')
expect(note.transcript).toBeUndefined()
await expect(getNote({ account: 'work', noteId: 'not_ZZZZZZZZZZZZZZ' })).rejects.toThrow(/404 NOT_FOUND.*AI summary and transcript/)
})
test('notes/transcript pages at 100', async () => {
const fake = use(makeFake({ transcriptItems: 250 }))
const transcript = (await import('../src/notes/transcript.ts')).default
const t = await transcript({ account: 'work', noteId: fake.notes[0]!.id, format: 'items' })
expect(t.count).toBe(250)
expect(t.items?.length).toBe(250)
expect(fake.calls.map((c) => c.query.page_size)).toEqual(['100', '100', '100'])
})
test('429 is retried', async () => {
const fake = use(makeFake({ status429Once: true }))
const listFolders = (await import('../src/folders/list.ts')).default
const r = await listFolders({ account: 'work', tree: true })
expect(r.count).toBe(3)
expect(fake.calls.length).toBe(2)
expect(r.folders.find((f: any) => f.name === 'Acme')?.path).toBe('Clients / Acme')
})
test('verify reports per-endpoint status', async () => {
use(makeFake())
const verify = (await import('../src/verify.ts')).default
const v = await verify({ account: 'work' })
expect(v.keyWorks).toBe(true)
expect(v.keyKind).toBe('personal or workspace API key')
expect(v.probes.audit?.status).toBe(403)
expect(v.probes.legalHolds?.status).toBe(404)
})
test('webhooks/create previews, refuses without secret handling, saves secret', async () => {
const fake = use(makeFake())
const create = (await import('../src/webhooks/create.ts')).default
const pv = await create({ account: 'work', url: 'https://example.com/h', scopes: ['personal'] })
expect(pv.dryRun).toBe(true)
expect(fake.calls.length).toBe(0)
await expect(create({ account: 'work', url: 'https://example.com/h', scopes: ['personal'], confirm: true })).rejects.toThrow(/only once/)
expect(fake.calls.length).toBe(0)
await expect(create({ account: 'work', url: 'https://example.com/h', scopes: ['everyone'], dryRun: true })).rejects.toThrow(/unsupported/)
const saved = await create({ account: 'work', url: 'https://example.com/h', scopes: ['personal'], saveSigningSecretAs: 'granola-webhook-secret', confirm: true })
expect(saved.signingSecret).toEqual({ savedAs: 'granola-webhook-secret' })
expect(JSON.stringify(saved)).not.toContain('whsec_')
secretSetFails = true
const fallback = await create({ account: 'work', url: 'https://example.com/h', scopes: ['personal'], saveSigningSecretAs: 'x', confirm: true })
expect((fallback.signingSecret as any).value).toBe('whsec_FAKEFAKEFAKE')
secretSetFails = false
})
test('webhooks/delete and update preview with current endpoint, no write', async () => {
const fake = use(makeFake())
const del = (await import('../src/webhooks/delete.ts')).default
const upd = (await import('../src/webhooks/update.ts')).default
const id = 'whe_' + 'A'.repeat(13) + '1'
const d = await del({ account: 'work', webhookId: id })
const u = await upd({ account: 'work', webhookId: id, enabled: false, dryRun: true, confirm: true })
expect(d.dryRun && u.dryRun).toBe(true)
expect(fake.calls.every((c) => c.method === 'GET')).toBe(true)
await expect(upd({ account: 'work', webhookId: id })).rejects.toThrow(/at least one/)
})
test('legal holds: previews never write; get 404 hint', async () => {
const fake = use(makeFake())
const create = (await import('../src/legal-holds/create.ts')).default
const release = (await import('../src/legal-holds/release.ts')).default
const add = (await import('../src/legal-holds/custodians/add.ts')).default
const rm = (await import('../src/legal-holds/custodians/remove.ts')).default
const hold = 'lgh_Abc123XyZ456Dq'
const c = await create({ account: 'legal', name: 'Matter 1', coversEntireWorkspace: false, custodians: ['person@example.com'] })
expect(c.request.body).toEqual({ name: 'Matter 1', covers_entire_workspace: false, custodians: [{ email: 'person@example.com' }] })
const r = await release({ account: 'legal', holdId: hold })
expect(r.warning).toMatch(/permanent/)
await add({ account: 'legal', holdId: hold, custodians: ['usr_ABCDEFGHIJKLMN'] })
await rm({ account: 'legal', holdId: hold, custodianId: 'lhc_7Yt3nBv8Kq2mXb' })
expect(fake.calls.every((x) => x.method === 'GET')).toBe(true)
await expect(create({ account: 'legal', name: 'x' })).rejects.toThrow(/coversEntireWorkspace/)
})
test('accounts discovers granola-* secrets with host approval', async () => {
const accounts = (await import('../src/accounts.ts')).default
const a = await accounts({})
expect(a.accounts.map((x: any) => [x.account, x.ready])).toEqual([['side', false], ['work', true]])
expect(a.accounts[0]?.hostApprovalUrl).toBe('https://kody.codes/connect/secrets?name=granola-side&hosts=public-api.granola.ai')
const one = await accounts({ account: 'newco' })
expect(one.accounts[0]?.setupUrl).toContain('name=granola-newco')
})
test('mcp lane: status, connect preview, call refuses unknown tool', async () => {
const tools = (await import('../src/mcp/tools.ts')).default
const connect = (await import('../src/mcp/connect.ts')).default
const call = (await import('../src/mcp/call.ts')).default
expect((await tools({})).connected).toBe(false)
expect((await connect({})).dryRun).toBe(true)
expect((await connect({ confirm: true })).authUrl).toBe('https://kody.example/auth')
await expect(call({ tool: 'delete_everything' })).rejects.toThrow()
await expect(call({ tool: 'get_account_info' })).rejects.toThrow(/not connected/)
})
})
describe('keyword search', () => {
test('query parsing: phrases, exclusions, accents, case', () => {
expect(parseQuery('Pricing "Next Steps" -internal Réunion')).toEqual({ terms: ['pricing', 'next steps', 'reunion'], excluded: ['internal'] })
expect(parseQuery('-"do not" a')).toEqual({ terms: ['a'], excluded: ['do not'] })
const { folded, map } = foldWithMap('Été café')
expect(folded).toBe('ete cafe')
expect(map.length).toBe(folded.length + 1)
})
test('matchNote: all/any, weights, exclusion, snippets keep original text', () => {
const fields = [
{ field: 'title', text: 'Acme pricing review', weight: 3 },
{ field: 'summary', text: 'We agreed on the Réunion budget. Pricing will change next quarter; pricing tiers stay.', weight: 1 },
]
const all = matchNote(fields, parseQuery('pricing reunion'), { mode: 'all', radius: 20, maxSnippets: 3 })
expect(all.matched).toBe(true)
expect(all.score).toBe(3 + 2 + 1)
expect(all.hits).toEqual({ title: 1, summary: 3 })
expect(all.snippets.some((sn) => sn.text.includes('Réunion'))).toBe(true)
expect(matchNote(fields, parseQuery('pricing zebra'), { mode: 'all', radius: 20, maxSnippets: 3 }).matched).toBe(false)
expect(matchNote(fields, parseQuery('pricing zebra'), { mode: 'any', radius: 20, maxSnippets: 3 }).matched).toBe(true)
expect(matchNote(fields, parseQuery('pricing -budget'), { mode: 'all', radius: 20, maxSnippets: 3 }).matched).toBe(false)
})
test('token bucket: burst then refill', async () => {
let t = 0
const sleeps: Array<number> = []
const b = new TokenBucket(3, 5, () => t, async (ms) => { sleeps.push(ms); t += ms })
for (let i = 0; i < 5; i++) await b.take()
expect(sleeps).toEqual([200, 200])
expect(await mapLimit([1, 2, 3, 4, 5], 2, async (x) => x * 2)).toEqual([2, 4, 6, 8, 10])
})
const content = {
0: { title: 'Acme kickoff', summary_markdown: '## Decisions\nAcme wants **pricing** by Friday. Next steps: send the proposal.' },
1: { title: 'Weekly sync', summary_markdown: 'Internal planning. Pricing mentioned once.', private_notes_markdown: 'remember pricing deck' },
2: { title: 'Board prep', summary_markdown: 'Budget only.', transcript: [{ text: 'We should revisit pricing with Acme', attribution: 'them' as const }] },
3: { title: 'Réunion équipe', summary_markdown: 'Planification du trimestre.', attendees: [{ name: 'Zoé Tremblay', email: 'zoe@example.com' }] },
4: { title: 'Broken note', status: 500 },
}
test('notes/search ranks, snippets, coverage, transcripts opt-in', async () => {
const fake = use(makeFake({ notes: 10, content }))
const search = (await import('../src/notes/search.ts')).default
const r = await search({ account: 'work', query: 'pricing' })
expect(r.scanned).toBe(10)
expect(r.capHit).toBe(false)
expect(r.results.map((x: any) => x.title)).toEqual(['Weekly sync', 'Acme kickoff'])
expect(r.results[0].hits).toEqual({ summary: 1, privateNotes: 1 })
expect(r.results[0].score).toBe(2)
expect(r.results[0].web_url).toContain('not_')
expect(r.results[0].snippets[0].text).toContain('Pricing')
expect(r.fetchErrors.map((e: any) => e.status)).toEqual([500])
expect(fake.calls.filter((c) => c.path.startsWith('/v1/notes/')).every((c) => c.query.include === undefined)).toBe(true)
const withT = await search({ account: 'work', query: 'pricing acme', includeTranscripts: true })
expect(withT.results.map((x: any) => x.title).sort()).toEqual(['Acme kickoff', 'Board prep'])
expect(withT.searchedFields).toContain('transcript')
const phrase = await search({ account: 'work', query: '"next steps"' })
expect(phrase.results.map((x: any) => x.title)).toEqual(['Acme kickoff'])
const accent = await search({ account: 'work', query: 'reunion zoe' })
expect(accent.results[0]?.title).toBe('Réunion équipe')
expect(accent.results[0]?.hits).toEqual({ title: 1, attendees: 2 })
const priv = await search({ account: 'work', query: 'deck', includePrivateNotes: false })
expect(priv.matchedCount).toBe(0)
})
test('notes/search scan cap and continuation cursor', async () => {
use(makeFake({ notes: 65, content }))
const search = (await import('../src/notes/search.ts')).default
const r = await search({ account: 'work', query: 'sync', maxNotes: 40, maxResults: 5 })
expect(r.listed).toBe(40)
expect(r.capHit).toBe(true)
expect(typeof r.nextCursor).toBe('string')
expect(r.results.length).toBe(5)
expect(r.resultsTruncated).toBe(true)
expect(r.coverage).toMatch(/older notes were not searched/)
const more = await search({ account: 'work', query: 'sync', maxNotes: 40, cursor: r.nextCursor })
expect(more.listed).toBe(25)
expect(more.capHit).toBe(false)
})
test('notes/search errors cleanly', async () => {
use(makeFake({ badKey: true }))
const search = (await import('../src/notes/search.ts')).default
await expect(search({ account: 'work', query: 'x' })).rejects.toThrow(/401 INVALID_API_KEY/)
await expect(search({ account: 'work', query: '-only' })).rejects.toThrow(/at least one search term/)
await expect(search({ account: 'work', query: 'x', maxNotes: 5000 })).rejects.toThrow(/maxNotes/)
await expect(search({ query: 'x' })).rejects.toThrow(/"account" is required/)
transport.fetch = (async () => { throw new Error('Secret "granola-work" not found') }) as typeof fetch
await expect(search({ account: 'work', query: 'x' })).rejects.toThrow(/does not exist.*secrets\/new\?name=granola-work/)
})
})
describe('mcp/query', () => {
test('helpers', () => {
expect(mcpText({ content: [{ type: 'text', text: 'a' }, { type: 'text', text: 'b' }], isError: false })).toEqual({ text: 'a\nb', isError: false })
expect(extractCitations('x [[0]](https://notes.granola.ai/d/1) y [1](https://notes.granola.ai/d/2) [[0]](https://notes.granola.ai/d/1)')).toEqual([
{ ref: '0', url: 'https://notes.granola.ai/d/1' },
{ ref: '1', url: 'https://notes.granola.ai/d/2' },
])
})
test('not connected → setup instructions; connected → answer + citations', async () => {
const query = (await import('../src/mcp/query.ts')).default
await expect(query({ question: 'What did we decide?' })).rejects.toThrow(/kody\.codes\/account\/mcp-servers.*mcp\.granola\.ai\/mcp/)
mcpHub.granola = {
async query_granola_meetings(args: unknown) {
mcpCalls.push(args)
return { content: [{ type: 'text', text: 'You agreed to send pricing [[0]](https://notes.granola.ai/d/abc).' }], isError: false }
},
}
const r = await query({ question: 'What did we decide?', documentIds: ['abc'] })
expect(mcpCalls[0]).toEqual({ query: 'What did we decide?', document_ids: ['abc'] })
expect(r.answer).toContain('pricing')
expect(r.citations).toEqual([{ ref: '0', url: 'https://notes.granola.ai/d/abc' }])
await expect(query({})).rejects.toThrow(/question/)
mcpHub.granola = { async query_granola_meetings() { throw new Error('Unknown MCP server "granola". Available MCP servers: "other".') } }
await expect(query({ question: 'q' })).rejects.toThrow(/not connected.*Kody said: Unknown MCP server.*account\/mcp-servers/)
mcpHub.granola = { async query_granola_meetings() { throw new Error('MCP server requires authorization') } }
await expect(query({ question: 'q' })).rejects.toThrow(/sign in again.*Reconnect/)
delete mcpHub.granola
})
})
describe('live-test fixes', () => {
const acct = { account: 'work', secretName: 'granola-work' }
test('404 hints depend on the endpoint', async () => {
const { statusHint } = await import('../src/lib/client.ts')
expect(statusHint(404, acct as any, 'NOT_FOUND', '/v1/audit')).toMatch(/Audit API key.*Settings → Connectors → Audit API keys/)
expect(statusHint(404, acct as any, 'NOT_FOUND', '/v1/legal-holds')).toMatch(/Legal hold API key.*Settings → Connectors → API → Legal hold API keys/)
expect(statusHint(404, acct as any, 'NOT_FOUND', '/v1/legal-holds/lgh_AAAAAAAAAAAAAA')).toMatch(/No such legal hold/)
expect(statusHint(404, acct as any, 'NOT_FOUND', '/v1/notes/not_AAAAAAAAAAAAAA')).toMatch(/AI summary and transcript/)
expect(statusHint(404, acct as any, 'NOT_FOUND', '/v1/webhook-endpoints/wh_x')).toMatch(/webhook endpoints/)
})
test('speaker badges are stripped from text labels only', async () => {
const { cleanSpeakerName, transcriptToText } = await import('../src/lib/transcript.ts')
expect(cleanSpeakerName('Jane Doe External unfamiliar')).toBe('Jane Doe')
expect(cleanSpeakerName('Jane Doe Unverified')).toBe('Jane Doe')
expect(cleanSpeakerName('Jane External')).toBe('Jane')
expect(cleanSpeakerName('Unverified')).toBe('Unverified')
expect(cleanSpeakerName('Guestline Corp')).toBe('Guestline Corp')
const text = transcriptToText([{ text: 'hi', start_time: '2026-09-01T15:00:00Z', speaker: { source: 'speaker', attribution: 'them', name: 'Jane Doe External unfamiliar' } } as any], { timestamps: true })
expect(text).toBe('[0:00] Jane Doe: hi')
})
test('audit 404 from a personal key is a clean, documented error', async () => {
const fake = makeFake({ notes: 3 })
const inner = fake.fetchImpl
transport.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const u = new URL(String(input instanceof Request ? input.url : input))
if (u.pathname === '/v1/audit') return new Response(JSON.stringify({ error: { code: 'NOT_FOUND', message: 'Not found' } }), { status: 404, headers: { 'content-type': 'application/json' } })
return inner(input as any, init)
}) as typeof fetch
const audit = (await import('../src/audit/list.ts')).default
await expect(audit({ account: 'work' })).rejects.toThrow(/404 NOT_FOUND on GET \/v1\/audit.*Audit API key/)
const verify = (await import('../src/verify.ts')).default
const v = await verify({ account: 'work', probes: ['notes', 'audit'] })
expect(v.keyKind).toBe('personal or workspace API key')
expect(v.keyKindNote).toMatch(/Audit API key/)
})
})