mirror of
https://github.com/hanzoai/extension.git
synced 2026-10-31 21:57:06 +00:00
28 apps told the user to paste an `hk-` key — input placeholders, settings help text, READMEs, the error thrown when the field is empty — and five of them validated that paste against /^hk-/ before sending it as a bearer. Nothing mints that prefix any more: cloud accepts pk- and sk-, IAM mints sk-. So the guard admitted a credential that authenticates nothing, and the panel failed later at the gateway with nothing on screen to explain it. The three leak detectors keep hk- and gain sk-. Salesforce's source scan and the two Zendesk tests exist to catch a key committed to source or echoed back through a request; retired keys are still in circulation and leak exactly as badly, while a detector that only knew hk- would have gone blind to every key minted from here on. `sk-` used to be the example of the foreign key the guard screened out — an OpenAI key. It cannot mean that now, and shape alone cannot tell one vendor's sk- from another's, so those comments say what the check really proves and point at the /v1/models call that proves the rest. The three tests that pinned "sk- is rejected" now pin "hk- is rejected", which is the invariant that survived. Verified: 2029 tests pass across the 28 packages; typecheck clean on 25 of them (hubspot, jupyter and teams fail identically at HEAD on JSX and ESM type conflicts in files this does not touch); salesforce validate-source green. Co-authored-by: Hanzo Dev <[email protected]>
110 lines
4.2 KiB
TypeScript
110 lines
4.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import {
|
||
chatCompletionsURL, modelsURL, API_BASE_URL, pickBearer,
|
||
buildContext, contextNote, CONTEXT_CHAR_BUDGET,
|
||
type PageText,
|
||
} from '../src/config';
|
||
|
||
describe('model gateway endpoints', () => {
|
||
it('chat + models go through api.hanzo.ai/v1 (no /api/ prefix)', () => {
|
||
expect(chatCompletionsURL()).toBe('https://api.hanzo.ai/v1/chat/completions');
|
||
expect(modelsURL()).toBe('https://api.hanzo.ai/v1/models');
|
||
expect(API_BASE_URL).toBe('https://api.hanzo.ai');
|
||
expect(chatCompletionsURL()).not.toContain('/api/');
|
||
});
|
||
});
|
||
|
||
describe('pickBearer', () => {
|
||
it('API key wins over the OAuth token', () => {
|
||
expect(pickBearer('sk-abc', 'jwt-xyz')).toBe('sk-abc');
|
||
});
|
||
it('falls back to the OAuth token when no key', () => {
|
||
expect(pickBearer('', 'jwt-xyz')).toBe('jwt-xyz');
|
||
expect(pickBearer(' ', 'jwt-xyz')).toBe('jwt-xyz');
|
||
});
|
||
it('empty when neither is present (anonymous)', () => {
|
||
expect(pickBearer('', '')).toBe('');
|
||
});
|
||
});
|
||
|
||
// Helper: N pages of `perPage` chars each.
|
||
function pages(n: number, perPage: number): PageText[] {
|
||
return Array.from({ length: n }, (_, i) => ({ page: i + 1, text: 'x'.repeat(perPage) }));
|
||
}
|
||
|
||
describe('buildContext (large-PDF windowing)', () => {
|
||
it('empty document → empty, not truncated', () => {
|
||
const ctx = buildContext([]);
|
||
expect(ctx).toEqual({ text: '', firstPage: 0, lastPage: 0, totalPages: 0, truncated: false });
|
||
});
|
||
|
||
it('small document fits whole → not truncated, covers every page', () => {
|
||
const ctx = buildContext(pages(3, 100), 10_000);
|
||
expect(ctx.truncated).toBe(false);
|
||
expect(ctx.firstPage).toBe(1);
|
||
expect(ctx.lastPage).toBe(3);
|
||
expect(ctx.totalPages).toBe(3);
|
||
expect(ctx.text).toContain('===== Page 2 =====');
|
||
expect(ctx.text).toContain('===== Page 3 =====');
|
||
});
|
||
|
||
it('stops before exceeding the budget and marks truncated', () => {
|
||
// 10 pages × 1000 chars, budget 3500 → only the first few pages fit.
|
||
const ctx = buildContext(pages(10, 1000), 3500);
|
||
expect(ctx.truncated).toBe(true);
|
||
expect(ctx.firstPage).toBe(1);
|
||
expect(ctx.lastPage).toBeLessThan(10);
|
||
expect(ctx.text.length).toBeLessThanOrEqual(3500);
|
||
});
|
||
|
||
it('windows from a start page and truncates on both ends', () => {
|
||
const ctx = buildContext(pages(10, 1000), 2500, 5);
|
||
expect(ctx.firstPage).toBe(5);
|
||
expect(ctx.truncated).toBe(true); // dropped pages 1–4 (before) and some after
|
||
expect(ctx.text).toContain('x'); // has the start page's body
|
||
});
|
||
|
||
it('always includes at least the start page, hard-cutting an oversized one', () => {
|
||
const ctx = buildContext(pages(3, 5000), 1000); // page 1 alone (5000) > budget
|
||
expect(ctx.firstPage).toBe(1);
|
||
expect(ctx.lastPage).toBe(1);
|
||
expect(ctx.truncated).toBe(true);
|
||
expect(ctx.text.length).toBe(1000); // hard-cut to the budget
|
||
});
|
||
|
||
it('clamps an out-of-range start page into the document', () => {
|
||
const ctx = buildContext(pages(3, 100), 10_000, 99);
|
||
expect(ctx.firstPage).toBe(3); // clamped to the last page
|
||
expect(ctx.lastPage).toBe(3);
|
||
expect(ctx.truncated).toBe(true); // pages 1–2 were before the start
|
||
});
|
||
|
||
it('uses CONTEXT_CHAR_BUDGET by default', () => {
|
||
const ctx = buildContext(pages(1, CONTEXT_CHAR_BUDGET + 5000));
|
||
expect(ctx.text.length).toBe(CONTEXT_CHAR_BUDGET);
|
||
expect(ctx.truncated).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('contextNote (honest scope)', () => {
|
||
it('says full document when nothing was dropped', () => {
|
||
const note = contextNote(buildContext(pages(2, 50), 10_000));
|
||
expect(note.toLowerCase()).toContain('full document');
|
||
expect(note).toContain('2 pages');
|
||
});
|
||
it('names the exact truncated range and warns', () => {
|
||
const note = contextNote(buildContext(pages(10, 1000), 3500));
|
||
expect(note.toLowerCase()).toContain('truncated');
|
||
expect(note).toMatch(/pages \d+–\d+ of 10/);
|
||
});
|
||
it('flags a scanned/empty PDF', () => {
|
||
const note = contextNote(buildContext([]));
|
||
expect(note.toLowerCase()).toContain('no selectable text');
|
||
});
|
||
it('singular "page N" for a single-page context', () => {
|
||
const note = contextNote(buildContext(pages(1, 50), 10_000));
|
||
expect(note).toContain('1 page');
|
||
expect(note).not.toContain('pages');
|
||
});
|
||
});
|