fix(config): apply defaults to perplexity UI

This commit is contained in:
Ivan Pereira
2026-03-21 20:54:41 +00:00
parent 11c543c0de
commit 041d30651a
13 changed files with 600 additions and 45 deletions
+3 -1
View File
@@ -56,6 +56,8 @@ Once installed, the agent automatically calls `perplexity_search` whenever it ne
| `query` | string | ✅ | The search query |
| `recency` | string | — | Filter by age: `hour` · `day` · `week` · `month` · `year` |
| `limit` | number | — | Max sources to include (150) |
| `model` | string | — | Model preference, e.g. `pplx_pro_upgraded`, `pplx_pro`, `experimental`, `gpt54`, `gpt54_thinking`, `claude46sonnet`, `claude46sonnetthinking`, `gemini31pro_high`, `nv_nemotron_3_super`, `pplx_reasoning`, `pplx_alpha` |
| `incognito` | boolean | — | Whether to hide the search from Perplexity history; defaults to `true` |
### Output format
@@ -80,7 +82,7 @@ Provider: perplexity (oauth)
Model: pplx_pro_upgraded
```
All queries use `is_incognito: true` — nothing shows up in your Perplexity history.
Queries default to `is_incognito: true`, but you can override that per call or via config.
## How It Works
+95
View File
@@ -0,0 +1,95 @@
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import {
getConfigPath as defaultGetConfigPath,
loadConfig as defaultLoadConfig,
saveConfig as defaultSaveConfig,
type PerplexityConfig,
} from "../config.js";
const KNOWN_MODELS: { value: string; label: string }[] = [
{ value: "pplx_pro_upgraded", label: "Best (auto)" },
{ value: "pplx_pro", label: "Default Pro" },
{ value: "experimental", label: "Sonar" },
{ value: "gpt54", label: "GPT-5.4" },
{ value: "gpt54_thinking", label: "GPT-5.4 Thinking" },
{ value: "claude46sonnet", label: "Claude 4.6 Sonnet" },
{ value: "claude46sonnetthinking", label: "Claude 4.6 Sonnet Thinking" },
{ value: "gemini31pro_high", label: "Gemini 3.1 Pro" },
{ value: "nv_nemotron_3_super", label: "Nemotron 3 Super" },
{ value: "pplx_reasoning", label: "Default Reasoning" },
{ value: "pplx_alpha", label: "Deep Research" },
];
function formatCurrentConfig(config: { model?: string; incognito?: boolean }): string {
const model = config.model ?? "pplx_pro_upgraded (default)";
const modelLabel = KNOWN_MODELS.find((m) => m.value === config.model)?.label;
const modelDisplay = modelLabel ? `${model} (${modelLabel})` : model;
const incognito = config.incognito ?? true;
return `Model: ${modelDisplay}\nIncognito: ${incognito}`;
}
interface ConfigCommandDeps {
getConfigPath: () => string;
loadConfig: () => Promise<PerplexityConfig>;
saveConfig: (config: PerplexityConfig) => Promise<void>;
}
export function registerPerplexityConfigCommand(
pi: ExtensionAPI,
deps: ConfigCommandDeps = {
getConfigPath: defaultGetConfigPath,
loadConfig: defaultLoadConfig,
saveConfig: defaultSaveConfig,
},
): void {
pi.registerCommand("perplexity-config", {
description: "Configure Perplexity search defaults",
handler: async (args, ctx) => {
if (args.trim() === "--help" || args.trim() === "-h") {
ctx.ui.notify(
`Usage: /perplexity-config [--show]\n\nInteractively set default model and incognito mode.\nConfig stored at: ${deps.getConfigPath()}`,
"info",
);
return;
}
try {
const config = await deps.loadConfig();
if (args.trim() === "--show") {
ctx.ui.notify(`Perplexity config (${deps.getConfigPath()}):\n${formatCurrentConfig(config)}`, "info");
return;
}
const modelLabels = KNOWN_MODELS.map((m) => `${m.label} (${m.value})`);
const selected = await ctx.ui.select("Default model", modelLabels);
if (selected === undefined || selected === null) {
ctx.ui.notify("Perplexity config unchanged.", "info");
return;
}
const selectedModel = KNOWN_MODELS[modelLabels.indexOf(selected)]?.value ?? selected;
const incognito = await ctx.ui.confirm(
"Incognito mode",
"Hide searches from Perplexity web history? (recommended)",
);
if (incognito === undefined || incognito === null) {
ctx.ui.notify("Perplexity config unchanged.", "info");
return;
}
config.model = selectedModel;
config.incognito = incognito;
await deps.saveConfig(config);
ctx.ui.notify(`Perplexity config saved:\n${formatCurrentConfig(config)}`, "info");
} catch (error) {
ctx.ui.notify(
`Failed to save Perplexity config: ${error instanceof Error ? error.message : String(error)}`,
"error",
);
}
},
});
}
+73
View File
@@ -0,0 +1,73 @@
import { mkdir, readFile, writeFile, chmod } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
export interface PerplexityConfig {
model?: string;
incognito?: boolean;
}
const CONFIG_PATH = join(homedir(), ".config", "pi-perplexity", "config.json");
export function getConfigPath(): string {
return CONFIG_PATH;
}
function isEnoent(error: unknown): boolean {
return error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT";
}
function parseConfig(raw: string): PerplexityConfig {
const parsed = JSON.parse(raw) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Config file must contain a JSON object");
}
const obj = parsed as Record<string, unknown>;
const config: PerplexityConfig = {};
if (typeof obj.model === "string" && obj.model.length > 0) config.model = obj.model;
if (typeof obj.incognito === "boolean") config.incognito = obj.incognito;
return config;
}
/** Load config from ~/.config/pi-perplexity/config.json. Returns {} if file doesn't exist. Throws on parse/IO errors. */
export async function loadConfig(configPath: string = CONFIG_PATH): Promise<PerplexityConfig> {
let raw: string;
try {
raw = await readFile(configPath, "utf8");
} catch (error) {
if (isEnoent(error)) return {};
throw error;
}
return parseConfig(raw);
}
/** Save config to disk with 0600 permissions. */
export async function saveConfig(config: PerplexityConfig, configPath: string = CONFIG_PATH): Promise<void> {
await mkdir(dirname(configPath), { recursive: true });
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
await chmod(configPath, 0o600);
}
/**
* Resolve effective values using priority: per-call param > env var > config file > default.
* Returns the model and incognito values to use for a search.
*/
export function resolveSearchDefaults(
params: { model?: string; incognito?: boolean },
config: PerplexityConfig,
): { model: string; incognito: boolean } {
const envModel = process.env.PI_PERPLEXITY_MODEL || undefined;
const envIncognito = process.env.PI_PERPLEXITY_INCOGNITO || undefined;
const model = params.model
?? envModel
?? config.model
?? "pplx_pro_upgraded";
const incognito = params.incognito
?? (envIncognito !== undefined ? envIncognito !== "false" && envIncognito !== "0" : undefined)
?? config.incognito
?? true;
return { model, incognito };
}
+35 -31
View File
@@ -2,23 +2,25 @@ import { StringEnum } from "@mariozechner/pi-ai";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { registerPerplexityConfigCommand } from "./commands/config.js";
import { registerPerplexityCommands } from "./commands/login.js";
import { authenticate } from "./auth/login.js";
import { clearToken } from "./auth/storage.js";
import { loadConfig, resolveSearchDefaults } from "./config.js";
import { formatForLLM } from "./search/format.js";
import { searchPerplexity } from "./search/client.js";
import { renderPerplexityCall } from "./render/call.js";
import { renderPerplexityResult } from "./render/result.js";
import { AuthError, SearchError } from "./search/types.js";
import { errorMessage } from "./render/util.js";
export default function (pi: ExtensionAPI) {
registerPerplexityCommands(pi);
registerPerplexityConfigCommand(pi);
pi.registerTool({
name: "perplexity_search",
label: "Perplexity Search",
description: "Search the web with your Perplexity subscription.",
description: "Search the web using Perplexity",
parameters: Type.Object({
query: Type.String({ description: "Search query" }),
recency: Type.Optional(
@@ -29,6 +31,8 @@ export default function (pi: ExtensionAPI) {
limit: Type.Optional(
Type.Number({ description: "Max sources to return", minimum: 1, maximum: 50 }),
),
model: Type.Optional(Type.String({ description: "Model preference" })),
incognito: Type.Optional(Type.Boolean({ description: "Hide search from Perplexity history" })),
}),
renderCall: renderPerplexityCall,
renderResult: renderPerplexityResult,
@@ -50,13 +54,11 @@ export default function (pi: ExtensionAPI) {
return ctx.ui.input(label, placeholder);
};
const authOptions: Parameters<typeof authenticate>[0] = {
const jwt = await authenticate({
...(signal !== undefined ? { signal } : {}),
promptForEmail: async () => promptInput("Perplexity email", "you@example.com"),
promptForOtp: async (email) => promptInput(`Enter OTP sent to ${email}`, "123456"),
};
if (signal) authOptions.signal = signal;
const jwt = await authenticate(authOptions);
});
if (signal?.aborted) {
return {
@@ -70,13 +72,26 @@ export default function (pi: ExtensionAPI) {
details: { toolCallId },
});
const searchParams: Parameters<typeof searchPerplexity>[0] = {
query: params.query,
};
if (params.recency) searchParams.recency = params.recency;
if (typeof params.limit === "number") searchParams.limit = params.limit;
const config = await loadConfig();
const { model, incognito } = resolveSearchDefaults(
{
...(params.model !== undefined ? { model: params.model } : {}),
...(params.incognito !== undefined ? { incognito: params.incognito } : {}),
},
config,
);
const result = await searchPerplexity(searchParams, jwt, signal);
const result = await searchPerplexity(
{
query: params.query,
model,
incognito,
...(params.recency !== undefined ? { recency: params.recency } : {}),
...(params.limit !== undefined ? { limit: params.limit } : {}),
},
jwt,
signal,
);
const formatted = formatForLLM(result, params.limit);
sourceCount =
@@ -88,6 +103,7 @@ export default function (pi: ExtensionAPI) {
content: [{ type: "text", text: formatted }],
details: {
model: result.displayModel,
incognito,
sourceCount,
queryMs: Date.now() - start,
uuid: result.uuid,
@@ -99,29 +115,17 @@ export default function (pi: ExtensionAPI) {
if (error instanceof AuthError) {
return {
content: [{ type: "text", text: `Authentication failed: ${error.message}` }],
details: { sourceCount, queryMs, isError: true },
details: { sourceCount, queryMs },
};
}
if (error instanceof SearchError) {
if (error.code === "AUTH") {
// Do NOT clear the token here. The user must re-login explicitly via
// /perplexity-login --force. Clearing automatically would silently discard
// a token that may still be valid (e.g. a transient 401), and removes the
// user's ability to inspect or recover the cached credential themselves.
return {
content: [
{
type: "text",
text: `Perplexity authentication failed. Run /perplexity-login --force to re-authenticate.`,
},
],
details: { sourceCount, queryMs, isError: true },
};
await clearToken().catch(() => undefined);
}
return {
content: [{ type: "text", text: `Perplexity search failed: ${error.message}` }],
details: { sourceCount, queryMs, isError: true },
details: { sourceCount, queryMs },
};
}
@@ -129,10 +133,10 @@ export default function (pi: ExtensionAPI) {
content: [
{
type: "text",
text: `Perplexity search failed: ${errorMessage(error)}`,
text: `Perplexity search failed: ${(error as Error).message || "Unknown error"}`,
},
],
details: { sourceCount, queryMs, isError: true },
details: { sourceCount, queryMs },
};
}
},
+12
View File
@@ -6,6 +6,8 @@ interface PerplexityCallArgs {
query?: unknown;
recency?: unknown;
limit?: unknown;
model?: unknown;
incognito?: unknown;
}
const RECENCY_VALUES = new Set(["hour", "day", "week", "month", "year"] as const);
@@ -16,10 +18,20 @@ export function renderPerplexityCall(args: PerplexityCallArgs, theme: Theme): Te
? recencyRaw
: undefined;
const limit = asPositiveInteger(args?.limit);
const model = asString(args?.model)?.trim();
const incognito = typeof args?.incognito === "boolean" ? args.incognito : undefined;
let text = theme.fg("toolTitle", theme.bold("perplexity_search "));
text += query ? theme.fg("muted", truncate(query, 90)) : theme.fg("warning", "(missing query)");
if (model) {
text += theme.fg("dim", `${model}`);
}
if (typeof incognito === "boolean") {
text += theme.fg("dim", ` • incognito ${incognito ? "on" : "off"}`);
}
if (recency) {
text += theme.fg("dim", `${recency}`);
}
+8 -3
View File
@@ -4,6 +4,7 @@ import { asString, asNumber, truncate } from "./util.js";
interface PerplexityResultDetails {
model?: unknown;
incognito?: unknown;
sourceCount?: unknown;
queryMs?: unknown;
uuid?: unknown;
@@ -69,9 +70,16 @@ export function renderPerplexityResult(
const sourceCount = asNumber(details.sourceCount);
const queryMs = asNumber(details.queryMs);
const model = asString(details.model)?.trim();
const incognito = typeof details.incognito === "boolean" ? details.incognito : undefined;
const uuid = asString(details.uuid)?.trim();
let text = theme.fg("success", "✓ Perplexity");
if (model) {
text += theme.fg("dim", `${model}`);
}
if (typeof incognito === "boolean") {
text += theme.fg("dim", ` • incognito ${incognito ? "on" : "off"}`);
}
if (typeof sourceCount === "number") {
text += theme.fg("muted", `${sourceCount} source${sourceCount === 1 ? "" : "s"}`);
}
@@ -90,9 +98,6 @@ export function renderPerplexityResult(
return new Text(text, 0, 0);
}
if (model) {
text += `\n${theme.fg("dim", `model: ${model}`)}`;
}
if (uuid) {
text += `\n${theme.fg("dim", `id: ${uuid}`)}`;
}
+4 -2
View File
@@ -105,6 +105,8 @@ export interface SearchParams {
query: string;
recency?: "hour" | "day" | "week" | "month" | "year";
limit?: number;
model: string;
incognito: boolean;
}
function normalizeUrl(url: string): string {
@@ -209,7 +211,7 @@ function buildRequestBody(params: SearchParams): Record<string, unknown> {
query_str: query,
search_focus: "internet",
mode: "copilot",
model_preference: "pplx_pro_upgraded",
model_preference: params.model,
sources: ["web"],
attachments: [],
frontend_uuid: crypto.randomUUID(),
@@ -218,7 +220,7 @@ function buildRequestBody(params: SearchParams): Record<string, unknown> {
language: "en-US",
timezone,
search_recency_filter: params.recency ?? null,
is_incognito: true,
is_incognito: params.incognito,
use_schematized_api: true,
skip_search_enabled: true,
},
+57
View File
@@ -0,0 +1,57 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { registerPerplexityConfigCommand } from "../src/commands/config.js";
import { loadConfig, saveConfig } from "../src/config.js";
let tempDir: string;
let configPath: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "pi-perplexity-command-test-"));
configPath = join(tempDir, "config.json");
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
describe("perplexity-config command", () => {
test("writes selected config to disk", async () => {
let handler: ((args: string, ctx: any) => Promise<void>) | undefined;
registerPerplexityConfigCommand(
{
registerCommand(name: string, command: { handler: (args: string, ctx: any) => Promise<void> }) {
expect(name).toBe("perplexity-config");
handler = command.handler;
},
} as any,
{
getConfigPath: () => configPath,
loadConfig: () => loadConfig(configPath),
saveConfig: (config) => saveConfig(config, configPath),
},
);
expect(handler).toBeDefined();
const notifications: Array<{ message: string; level: string }> = [];
await handler!("", {
ui: {
select: async () => "GPT-5.4 (gpt54)",
confirm: async () => false,
notify: (message: string, level: string) => notifications.push({ message, level }),
},
});
const raw = await readFile(configPath, "utf8");
expect(JSON.parse(raw)).toEqual({ model: "gpt54", incognito: false });
expect(notifications).toContainEqual({
message: "Perplexity config saved:\nModel: gpt54 (GPT-5.4)\nIncognito: false",
level: "info",
});
});
});
+138
View File
@@ -0,0 +1,138 @@
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { loadConfig, saveConfig, resolveSearchDefaults } from "../src/config.js";
let tempDir: string;
let configPath: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "pi-perplexity-test-"));
configPath = join(tempDir, "config.json");
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
describe("loadConfig", () => {
test("returns empty object when file is missing", async () => {
const config = await loadConfig(configPath);
expect(config).toEqual({});
});
test("returns parsed config from file", async () => {
await writeFile(configPath, JSON.stringify({ model: "gpt54", incognito: false }));
const config = await loadConfig(configPath);
expect(config.model).toBe("gpt54");
expect(config.incognito).toBe(false);
});
test("throws on invalid JSON", async () => {
await writeFile(configPath, "not json");
await expect(loadConfig(configPath)).rejects.toThrow();
});
test("throws on non-object JSON", async () => {
await writeFile(configPath, '"just a string"');
await expect(loadConfig(configPath)).rejects.toThrow("must contain a JSON object");
});
test("ignores unknown fields", async () => {
await writeFile(configPath, JSON.stringify({ model: "gpt54", unknown: true }));
const config = await loadConfig(configPath);
expect(config.model).toBe("gpt54");
expect(config).not.toHaveProperty("unknown");
});
test("ignores empty model string", async () => {
await writeFile(configPath, JSON.stringify({ model: "" }));
const config = await loadConfig(configPath);
expect(config).not.toHaveProperty("model");
});
});
describe("saveConfig", () => {
test("writes file with 0600 permissions", async () => {
await saveConfig({ model: "claude46sonnetthinking", incognito: true }, configPath);
const raw = await readFile(configPath, "utf8");
const parsed = JSON.parse(raw);
expect(parsed.model).toBe("claude46sonnetthinking");
expect(parsed.incognito).toBe(true);
const stats = await stat(configPath);
expect(stats.mode & 0o777).toBe(0o600);
});
test("creates parent directories", async () => {
const nested = join(tempDir, "a", "b", "config.json");
await saveConfig({ model: "gpt54" }, nested);
const raw = await readFile(nested, "utf8");
expect(JSON.parse(raw).model).toBe("gpt54");
});
});
describe("resolveSearchDefaults", () => {
test("returns hardcoded defaults when no config, env, or params", () => {
const result = resolveSearchDefaults({}, {});
expect(result.model).toBe("pplx_pro_upgraded");
expect(result.incognito).toBe(true);
});
test("config file values override defaults", () => {
const result = resolveSearchDefaults({}, { model: "gpt54", incognito: false });
expect(result.model).toBe("gpt54");
expect(result.incognito).toBe(false);
});
test("env var '0' disables incognito", () => {
const original = process.env.PI_PERPLEXITY_INCOGNITO;
try {
process.env.PI_PERPLEXITY_INCOGNITO = "0";
const result = resolveSearchDefaults({}, {});
expect(result.incognito).toBe(false);
} finally {
if (original === undefined) delete process.env.PI_PERPLEXITY_INCOGNITO;
else process.env.PI_PERPLEXITY_INCOGNITO = original;
}
});
test("env vars override config file", () => {
const originalModel = process.env.PI_PERPLEXITY_MODEL;
const originalIncognito = process.env.PI_PERPLEXITY_INCOGNITO;
try {
process.env.PI_PERPLEXITY_MODEL = "experimental";
process.env.PI_PERPLEXITY_INCOGNITO = "false";
const result = resolveSearchDefaults({}, { model: "gpt54", incognito: true });
expect(result.model).toBe("experimental");
expect(result.incognito).toBe(false);
} finally {
if (originalModel === undefined) delete process.env.PI_PERPLEXITY_MODEL;
else process.env.PI_PERPLEXITY_MODEL = originalModel;
if (originalIncognito === undefined) delete process.env.PI_PERPLEXITY_INCOGNITO;
else process.env.PI_PERPLEXITY_INCOGNITO = originalIncognito;
}
});
test("per-call params override everything", () => {
const originalModel = process.env.PI_PERPLEXITY_MODEL;
try {
process.env.PI_PERPLEXITY_MODEL = "experimental";
const result = resolveSearchDefaults(
{ model: "claude46sonnetthinking", incognito: false },
{ model: "gpt54", incognito: true },
);
expect(result.model).toBe("claude46sonnetthinking");
expect(result.incognito).toBe(false);
} finally {
if (originalModel === undefined) delete process.env.PI_PERPLEXITY_MODEL;
else process.env.PI_PERPLEXITY_MODEL = originalModel;
}
});
});
+59
View File
@@ -0,0 +1,59 @@
import { afterEach, describe, expect, mock, test } from "bun:test";
afterEach(() => {
mock.restore();
});
describe("perplexity_search execute", () => {
test("includes effective config values in the search request and result details", async () => {
const authenticate = mock(async () => "jwt-token");
const loadConfig = mock(async () => ({ model: "gpt54", incognito: false }));
const resolveSearchDefaults = mock(() => ({ model: "gpt54", incognito: false }));
const searchPerplexity = mock(async () => ({
answer: "answer",
sources: [{ url: "https://example.com" }],
displayModel: "gpt54",
uuid: "req-123",
}));
mock.module("../src/auth/login.js", () => ({ authenticate }));
mock.module("../src/config.js", () => ({
getConfigPath: () => "/tmp/pi-perplexity-config.json",
loadConfig,
resolveSearchDefaults,
saveConfig: mock(async () => undefined),
}));
mock.module("../src/search/client.js", () => ({ searchPerplexity }));
const { default: registerExtension } = await import(`../src/index.ts?test=${crypto.randomUUID()}`);
let execute: ((toolCallId: string, params: any, signal?: AbortSignal, onUpdate?: any, ctx?: any) => Promise<any>) | undefined;
registerExtension({
registerCommand() {
return undefined;
},
registerTool(tool: { execute: typeof execute }) {
execute = tool.execute;
},
} as any);
expect(execute).toBeDefined();
const result = await execute!("tool-1", { query: "how many planets" }, undefined, undefined, { ui: {} });
expect(loadConfig).toHaveBeenCalledTimes(1);
expect(resolveSearchDefaults).toHaveBeenCalledWith({}, { model: "gpt54", incognito: false });
expect(searchPerplexity).toHaveBeenCalledWith(
{
query: "how many planets",
model: "gpt54",
incognito: false,
},
"jwt-token",
undefined,
);
expect(result.details.incognito).toBe(false);
expect(result.details.model).toBe("gpt54");
});
});
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, test } from "bun:test";
describe("extension entrypoint", () => {
test("registers the config command", async () => {
const { default: registerExtension } = await import(`../src/index.ts?test=${crypto.randomUUID()}`);
const commands: string[] = [];
registerExtension({
registerCommand(name: string) {
commands.push(name);
},
registerTool() {
return undefined;
},
} as any);
expect(commands).toContain("perplexity-login");
expect(commands).toContain("perplexity-config");
});
});
+47
View File
@@ -0,0 +1,47 @@
import { describe, expect, test } from "bun:test";
import { renderPerplexityCall } from "../src/render/call.js";
import { renderPerplexityResult } from "../src/render/result.js";
const theme = {
fg: (_color: string, text: string) => text,
bold: (text: string) => text,
} as any;
describe("renderPerplexityCall", () => {
test("shows the selected model in the tool call row", () => {
const rendered = renderPerplexityCall(
{
query: "latest bun release notes",
model: "claude46sonnetthinking",
recency: "week",
limit: 5,
},
theme,
).render(200).join("\n");
expect(rendered).toContain("claude46sonnetthinking");
expect(rendered).toContain("week");
expect(rendered).toContain("limit 5");
});
});
describe("renderPerplexityResult", () => {
test("shows the model in the collapsed success row", () => {
const rendered = renderPerplexityResult(
{
content: [{ type: "text", text: "Result summary" }],
details: {
model: "gpt54",
sourceCount: 3,
queryMs: 800,
},
} as any,
{ expanded: false, isPartial: false } as any,
theme,
).render(200).join("\n");
expect(rendered).toContain("gpt54");
expect(rendered).toContain("3 sources");
});
});
+48 -8
View File
@@ -51,7 +51,7 @@ describe("searchPerplexity", () => {
const controller = new AbortController();
const result = await searchPerplexity(
{ query: "latest bun release notes", recency: "week" },
{ query: "latest bun release notes", recency: "week", model: "pplx_pro_upgraded", incognito: true },
"jwt-token",
controller.signal,
);
@@ -92,11 +92,51 @@ describe("searchPerplexity", () => {
expect(result.sources).toHaveLength(1);
});
test("passes model and incognito through to request body", async () => {
let capturedInit: RequestInit | undefined;
globalThis.fetch = (async (_url: RequestInfo | URL, init?: RequestInit) => {
capturedInit = init;
return createSseResponse([
{ status: "COMPLETED", final: true, text: "answer", blocks: [] },
]);
}) as unknown as typeof fetch;
await searchPerplexity(
{ query: "q", model: "claude46sonnetthinking", incognito: false },
"jwt-token",
);
const body = JSON.parse(String(capturedInit?.body)) as {
params: { model_preference: string; is_incognito: boolean };
};
expect(body.params.model_preference).toBe("claude46sonnetthinking");
expect(body.params.is_incognito).toBe(false);
});
test("passes incognito true through to request body", async () => {
let capturedInit: RequestInit | undefined;
globalThis.fetch = (async (_url: RequestInfo | URL, init?: RequestInit) => {
capturedInit = init;
return createSseResponse([
{ status: "COMPLETED", final: true, text: "answer", blocks: [] },
]);
}) as unknown as typeof fetch;
await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt-token");
const body = JSON.parse(String(capturedInit?.body)) as {
params: { is_incognito: boolean };
};
expect(body.params.is_incognito).toBe(true);
});
test("maps 401 and 403 responses to AUTH error", async () => {
for (const status of [401, 403]) {
globalThis.fetch = (async () => new Response("auth fail", { status })) as unknown as typeof fetch;
await expect(searchPerplexity({ query: "q" }, "jwt")).rejects.toMatchObject({
await expect(searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt")).rejects.toMatchObject({
name: "SearchError",
code: "AUTH",
});
@@ -106,7 +146,7 @@ describe("searchPerplexity", () => {
test("maps 429 responses to RATE_LIMIT error", async () => {
globalThis.fetch = (async () => new Response("rate limited", { status: 429 })) as unknown as typeof fetch;
await expect(searchPerplexity({ query: "q" }, "jwt")).rejects.toMatchObject({
await expect(searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt")).rejects.toMatchObject({
name: "SearchError",
code: "RATE_LIMIT",
});
@@ -134,7 +174,7 @@ describe("searchPerplexity", () => {
},
])) as unknown as typeof fetch;
const result = await searchPerplexity({ query: "q" }, "jwt");
const result = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt");
expect(result.sources).toHaveLength(2);
expect(result.sources[0].url).toBe("https://example.com/path");
@@ -156,7 +196,7 @@ describe("searchPerplexity", () => {
},
])) as unknown as typeof fetch;
const result = await searchPerplexity({ query: "q" }, "jwt");
const result = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt");
expect(result.answer).toBe("markdown answer");
});
@@ -174,7 +214,7 @@ describe("searchPerplexity", () => {
},
])) as unknown as typeof fetch;
const askTextResult = await searchPerplexity({ query: "q" }, "jwt");
const askTextResult = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt");
expect(askTextResult.answer).toBe("ask answer");
globalThis.fetch = (async () =>
@@ -187,7 +227,7 @@ describe("searchPerplexity", () => {
},
])) as unknown as typeof fetch;
const textResult = await searchPerplexity({ query: "q" }, "jwt");
const textResult = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt");
expect(textResult.answer).toBe("text fallback");
});
@@ -197,7 +237,7 @@ describe("searchPerplexity", () => {
let thrown: unknown;
try {
await searchPerplexity({ query: "q" }, "jwt");
await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt");
} catch (error) {
thrown = error;
}