mirror of
https://github.com/hanzoai/extension.git
synced 2026-10-31 21:57:06 +00:00
/v1/ask is metered and answers 401 "sign in to ask" to anyone without an IAM identity. That is right for the product and wrong as the whole behaviour of a NEW TAB: this surface replaces the browser's search box, so a signed-out user typing a query and receiving a sign-in card has been handed a broken address bar, not a paywall. run() now resolves identity BEFORE any spinner. With a token the query goes to Hanzo exactly as before. Without one it goes to the configured engine — DuckDuckGo by default, with Google, Brave, Startpage and Bing one setting away (hanzo_config_fallback_search). The fallback is NOT a Hanzo call: no token, no proxy, no telemetry to us. The query travels from the user's browser straight to the engine they chose, exactly as it would with no extension installed. That is why the test asserts the absence of "hanzo" in the URL rather than its presence somewhere. DuckDuckGo is the default deliberately. The fallback runs for people with no Hanzo account, so it is the one path where we hand a stranger's query to a third party on their behalf; the default should be the engine that profiles them least, not the one with the largest share. The expired-session card gained the same escape hatch. It is now reachable only when a token EXISTED and the call still 401'd — a revoked or expired session, since a signed-out query never reaches it — so it offers both doors: sign in again, or take the same query to the configured engine. loadProviderId cannot throw. It sits directly under "the user pressed enter", and an exception there strands the query, which is the exact failure this exists to prevent — a rejected read, a non-string value and an engine we do not ship all resolve to the default. Verified in a real browser against the built bundle: signed out, Enter navigates off-page to duckduckgo.com; with a token present the page STAYS and calls Hanzo. 233 tests pass (7 new). The authed path's answer itself is not provable from a file:// harness — that origin has no host permissions, so the cross-origin call is blocked there and only there. Co-authored-by: Hanzo Dev <[email protected]>
69 lines
3.1 KiB
TypeScript
69 lines
3.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
DEFAULT_PROVIDER_ID,
|
|
PROVIDERS,
|
|
fallbackUrl,
|
|
loadProviderId,
|
|
providerById,
|
|
} from '../src/answer/websearch.js';
|
|
|
|
describe('external search fallback', () => {
|
|
it('sends a signed-out query straight to the engine, not through Hanzo', () => {
|
|
for (const p of PROVIDERS) {
|
|
const url = fallbackUrl(p.id, 'what is a raft consensus');
|
|
// The whole point: no api.hanzo.ai in the path. A "fallback" that proxied the
|
|
// query would still need the identity we just established the user lacks.
|
|
expect(url).not.toContain('hanzo');
|
|
expect(url.startsWith('https://')).toBe(true);
|
|
expect(url).toContain('what+is+a+raft+consensus'.replace(/\+/g, '%20'));
|
|
}
|
|
});
|
|
|
|
it('encodes a query that would otherwise break the URL', () => {
|
|
const url = fallbackUrl('duckduckgo', 'a&b=c #d ?e');
|
|
expect(url).toBe('https://duckduckgo.com/?q=a%26b%3Dc%20%23d%20%3Fe');
|
|
// Exactly one `?` — the query's own must not open a second parameter section.
|
|
expect(url.split('?').length).toBe(2);
|
|
});
|
|
|
|
it('returns nothing for an empty query rather than a bare results page', () => {
|
|
// Navigating to a provider's results for "" is worse than staying put: the user
|
|
// loses the tab they were typing in and gets nothing for it.
|
|
expect(fallbackUrl('google', '')).toBe('');
|
|
expect(fallbackUrl('google', ' ')).toBe('');
|
|
});
|
|
|
|
it('falls back to the default engine for an unknown or absent id', () => {
|
|
expect(providerById('not-an-engine').id).toBe(DEFAULT_PROVIDER_ID);
|
|
expect(providerById(null).id).toBe(DEFAULT_PROVIDER_ID);
|
|
expect(providerById(undefined).id).toBe(DEFAULT_PROVIDER_ID);
|
|
});
|
|
|
|
it('defaults to the engine that profiles a signed-out user least', () => {
|
|
// This default is a deliberate choice, not an accident of ordering: the
|
|
// fallback runs for people with no Hanzo account, so it is the one path where
|
|
// we hand a stranger's query to a third party on their behalf.
|
|
expect(DEFAULT_PROVIDER_ID).toBe('duckduckgo');
|
|
expect(PROVIDERS[0]!.id).toBe(DEFAULT_PROVIDER_ID);
|
|
});
|
|
|
|
it('never throws on the enter-key path, whatever storage does', async () => {
|
|
// loadProviderId sits directly under "the user pressed enter". An exception
|
|
// there strands the query, which is the exact failure the fallback exists to
|
|
// prevent — so every storage failure resolves to the default instead.
|
|
const rejects = { get: () => Promise.reject(new Error('storage is gone')) };
|
|
await expect(loadProviderId(rejects)).resolves.toBe(DEFAULT_PROVIDER_ID);
|
|
|
|
const junk = { get: async () => ({ hanzo_config_fallback_search: 42 }) };
|
|
await expect(loadProviderId(junk as never)).resolves.toBe(DEFAULT_PROVIDER_ID);
|
|
|
|
const unknown = { get: async () => ({ hanzo_config_fallback_search: 'altavista' }) };
|
|
await expect(loadProviderId(unknown)).resolves.toBe(DEFAULT_PROVIDER_ID);
|
|
});
|
|
|
|
it('honours a configured engine', async () => {
|
|
const chosen = { get: async () => ({ hanzo_config_fallback_search: 'brave' }) };
|
|
await expect(loadProviderId(chosen)).resolves.toBe('brave');
|
|
});
|
|
});
|