feat: add search API key configuration

This commit is contained in:
云服务部-叶林立
2026-08-19 12:45:47 +08:00
parent 6293ef1c63
commit 198584daf8
10 changed files with 390 additions and 45 deletions
+39
View File
@@ -0,0 +1,39 @@
import assert from "node:assert/strict";
import { mkdtemp, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
import test from "node:test";
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const hasPi = spawnSync("pi", ["--version"], { encoding: "utf8" }).status === 0;
test("all package extensions load together without global registration conflicts", { skip: !hasPi }, async () => {
const packageJson = JSON.parse(await readFile(join(repositoryRoot, "package.json"), "utf8")) as {
pi: { extensions: string[] };
};
const home = await mkdtemp(join(tmpdir(), "my-pi-extension-load-"));
const args = ["--no-extensions"];
for (const entry of packageJson.pi.extensions) args.push("-e", resolve(repositoryRoot, entry));
args.push("--help");
const result = spawnSync("pi", args, {
encoding: "utf8",
timeout: 60_000,
env: {
...process.env,
HOME: home,
XDG_CONFIG_HOME: join(home, ".config"),
PI_CODING_AGENT_DIR: join(home, ".pi-agent"),
MY_PI_SEARCH_CONFIG: join(home, "missing-search.env"),
PI_OFFLINE: "1",
TAVILY_API_KEY: "",
EXA_API_KEY: "",
KEENABLE_API_KEY: "",
},
});
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /Failed to load extension|conflicts with/u);
});
+33 -7
View File
@@ -2,10 +2,10 @@ import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import { EXA_TOOLS, MCP_CONFIG } from "../extensions/mcp-config.ts";
import { createMcpConfig, EXA_TOOLS } from "../extensions/mcp-config.ts";
test("one MCP adapter config contains both Exa and CodeGraph", () => {
assert.deepEqual(MCP_CONFIG, {
assert.deepEqual(createMcpConfig({}), {
settings: {
disableProxyTool: true,
scriptMode: false,
@@ -35,13 +35,39 @@ test("one MCP adapter config contains both Exa and CodeGraph", () => {
});
});
test("the package loads only the shared MCP extension entry", async () => {
test("Exa API key is resolved from the environment into a header and never placed in the URL", () => {
const config = createMcpConfig({ EXA_API_KEY: "exa-test-key" });
assert.deepEqual(config.mcpServers.exa.headers, { "x-api-key": "$env:EXA_API_KEY" });
assert.equal(config.mcpServers.exa.url?.includes("exa-test-key"), false);
assert.equal(JSON.stringify(config).includes("exa-test-key"), false);
});
test("the package has exactly one local MCP adapter owner", async () => {
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as {
pi: { extensions: string[] };
};
const localMcpEntries = packageJson.pi.extensions.filter((entry) =>
entry === "./extensions/mcp.ts" || entry === "./extensions/exa.ts" || entry === "./extensions/codegraph.ts"
);
const localEntries = packageJson.pi.extensions.filter((entry) => entry.startsWith("./extensions/"));
const adapterOwners: string[] = [];
for (const entry of localEntries) {
const source = await readFile(new URL(`../${entry.slice(2)}`, import.meta.url), "utf8");
if (source.includes("createMcpAdapter") || source.includes('from "pi-mcp-adapter"')) {
adapterOwners.push(entry);
}
}
assert.deepEqual(adapterOwners, ["./extensions/mcp.ts"]);
});
assert.deepEqual(localMcpEntries, ["./extensions/mcp.ts"]);
test("search credentials load before all search providers", async () => {
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as {
pi: { extensions: string[] };
};
const entries = packageJson.pi.extensions;
const loaderIndex = entries.indexOf("./extensions/search-config.ts");
for (const provider of [
"./extensions/tavily-override.ts",
"./node_modules/@keenable/pi-search/src/index.ts",
"./extensions/mcp.ts",
]) {
assert.ok(loaderIndex >= 0 && loaderIndex < entries.indexOf(provider), `${provider} must load after search config`);
}
});
+67
View File
@@ -0,0 +1,67 @@
import assert from "node:assert/strict";
import { mkdtemp, readFile, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import test from "node:test";
import {
getSearchConfigPath,
loadSearchConfigEnv,
parseSearchConfig,
} from "../extensions/search-config.ts";
test("search config parses only supported safe assignments", () => {
assert.deepEqual(
parseSearchConfig(`
export TAVILY_API_KEY=tvly-test_1
EXA_API_KEY=exa-test.2
UNKNOWN_KEY=ignored
KEENABLE_API_KEY=$(not-allowed)
`),
{
TAVILY_API_KEY: "tvly-test_1",
EXA_API_KEY: "exa-test.2",
},
);
});
test("search config does not override explicit environment values", async () => {
const root = await mkdtemp(join(tmpdir(), "my-pi-search-config-"));
const configPath = join(root, "search.env");
await import("node:fs/promises").then(({ writeFile }) =>
writeFile(configPath, "export TAVILY_API_KEY=file-value\nexport EXA_API_KEY=exa-file\n"),
);
const env: Record<string, string | undefined> = { TAVILY_API_KEY: "explicit-value" };
const effective = loadSearchConfigEnv(env, configPath);
assert.equal(env.TAVILY_API_KEY, "explicit-value");
assert.equal(env.EXA_API_KEY, "exa-file");
assert.deepEqual(effective, { TAVILY_API_KEY: "explicit-value", EXA_API_KEY: "exa-file" });
});
test("search config path follows XDG_CONFIG_HOME", () => {
assert.equal(getSearchConfigPath({ XDG_CONFIG_HOME: "/tmp/config" }), "/tmp/config/my-pi/search.env");
});
test("search_config.sh supports noninteractive updates and preserves omitted keys", async () => {
const home = await mkdtemp(join(tmpdir(), "my-pi-search-script-"));
const script = new URL("../search_config.sh", import.meta.url);
let result = spawnSync("sh", [script.pathname, "--tavily", "tvly-test", "--exa", "exa-test"], {
env: { ...process.env, HOME: home, XDG_CONFIG_HOME: join(home, ".config") },
encoding: "utf8",
});
assert.equal(result.status, 0, result.stderr);
result = spawnSync("sh", [script.pathname, "--keenable", "keen-test"], {
env: { ...process.env, HOME: home, XDG_CONFIG_HOME: join(home, ".config") },
encoding: "utf8",
});
assert.equal(result.status, 0, result.stderr);
const configPath = join(home, ".config", "my-pi", "search.env");
const content = await readFile(configPath, "utf8");
assert.match(content, /^export TAVILY_API_KEY=tvly-test$/m);
assert.match(content, /^export EXA_API_KEY=exa-test$/m);
assert.match(content, /^export KEENABLE_API_KEY=keen-test$/m);
assert.equal((await stat(configPath)).mode & 0o777, 0o600);
});