mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
feat: 接入 Pi 内置 codemode,并修复全面审查发现的问题
- codemode:Tool Search 常驻(alwaysEnabled)、权限 allow(嵌套调用仍逐个过权限门)、 tools.jsonc 登记使用规则;RTK 跳过 parentToolCallId 嵌套调用,避免丢失 bash structuredContent;Tool Search 目录过滤 hidden / codemode(script-only)exposure; 扩展加载测试加载 builtin:codemode 并断言其常驻激活 - 搜索 key 不再经命令行参数传递:search-auth.mjs 改为 --set-stdin(先读校验再加锁), configure.sh 交互输入经管道、非交互用 --<provider> - - CodeGraph 客户端监听子进程 stdin error,避免 EPIPE 未处理导致崩溃 - 移除 PI_TOOL_SEARCH/HERMES_MEMORY/RTK_CONFIG_PATH 环境变量入口,测试改用工厂 configPath 注入 - pi-subagents 固定 vitest / coverage-istanbul 4.1.10,修复 npm ls 版本冲突 - 补 pi-web-search / pi-worktree UPSTREAM.md 与 pi-minimal-footer AGENTS.md, files 白名单补齐并加测试;.codegraph/.gitignore 移出跟踪 - detect_kitty 优先识别官方安装目录 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
56cbc83296
commit
f9a7f743dd
+15
-30
@@ -13,10 +13,7 @@ import {
|
||||
getToolSearchCatalogPath,
|
||||
getToolSearchConfigPath,
|
||||
} from "../pi-tool-search/extensions/config.ts";
|
||||
import {
|
||||
CONFIG_PATH as RTK_CONFIG_PATH,
|
||||
resolveConfigPath as resolveRtkConfigPath,
|
||||
} from "../pi-rtk-optimizer/src/constants.ts";
|
||||
import { CONFIG_PATH as RTK_CONFIG_PATH } from "../pi-rtk-optimizer/src/constants.ts";
|
||||
import { CONFIG_PATH as COMPACTION_CONFIG_PATH } from "../pi-compaction/src/config.ts";
|
||||
import { WORKTREE_CONFIG_PATH } from "../pi-worktree/src/settings.ts";
|
||||
import { resolvePreamblePath } from "../extensions/system-prompt-config.ts";
|
||||
@@ -75,33 +72,21 @@ test("every bundle JSON/JSONC config file parses", async () => {
|
||||
});
|
||||
|
||||
test("each extension resolves its config path inside the bundle config/ dir", () => {
|
||||
const savedToolSearch = process.env.PI_TOOL_SEARCH_CONFIG_PATH;
|
||||
const savedRtk = process.env.PI_RTK_CONFIG_PATH;
|
||||
delete process.env.PI_TOOL_SEARCH_CONFIG_PATH;
|
||||
delete process.env.PI_RTK_CONFIG_PATH;
|
||||
try {
|
||||
const inBundleConfig = (resolved: string, file: string) =>
|
||||
assert.equal(
|
||||
resolved,
|
||||
join(bundleConfigDir, file),
|
||||
`expected the bundle config path for ${file}`,
|
||||
);
|
||||
const inBundleConfig = (resolved: string, file: string) =>
|
||||
assert.equal(
|
||||
resolved,
|
||||
join(bundleConfigDir, file),
|
||||
`expected the bundle config path for ${file}`,
|
||||
);
|
||||
|
||||
inBundleConfig(getGlobalConfigPath(), "pi-permission-policy.jsonc");
|
||||
inBundleConfig(getHardGuardConfigPath(), "pi-permission-hard-guards.jsonc");
|
||||
inBundleConfig(getToolSearchConfigPath(), "tool-search.json");
|
||||
inBundleConfig(RTK_CONFIG_PATH, "rtk-optimizer.json");
|
||||
inBundleConfig(resolveRtkConfigPath(), "rtk-optimizer.json");
|
||||
inBundleConfig(COMPACTION_CONFIG_PATH, "pi-compaction.json");
|
||||
inBundleConfig(WORKTREE_CONFIG_PATH, "pi-worktree.json");
|
||||
inBundleConfig(HERMES_CONFIG_PATH, "pi-hermes-memory.json");
|
||||
inBundleConfig(resolvePreamblePath(), "prompt/preamble.md");
|
||||
} finally {
|
||||
if (savedToolSearch === undefined) delete process.env.PI_TOOL_SEARCH_CONFIG_PATH;
|
||||
else process.env.PI_TOOL_SEARCH_CONFIG_PATH = savedToolSearch;
|
||||
if (savedRtk === undefined) delete process.env.PI_RTK_CONFIG_PATH;
|
||||
else process.env.PI_RTK_CONFIG_PATH = savedRtk;
|
||||
}
|
||||
inBundleConfig(getGlobalConfigPath(), "pi-permission-policy.jsonc");
|
||||
inBundleConfig(getHardGuardConfigPath(), "pi-permission-hard-guards.jsonc");
|
||||
inBundleConfig(getToolSearchConfigPath(), "tool-search.json");
|
||||
inBundleConfig(RTK_CONFIG_PATH, "rtk-optimizer.json");
|
||||
inBundleConfig(COMPACTION_CONFIG_PATH, "pi-compaction.json");
|
||||
inBundleConfig(WORKTREE_CONFIG_PATH, "pi-worktree.json");
|
||||
inBundleConfig(HERMES_CONFIG_PATH, "pi-hermes-memory.json");
|
||||
inBundleConfig(resolvePreamblePath(), "prompt/preamble.md");
|
||||
});
|
||||
|
||||
test("disposable runtime state stays under the agent-dir my-pi tree", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { EventEmitter } from "node:events";
|
||||
import {
|
||||
execFileSync,
|
||||
spawn,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
@@ -278,6 +280,38 @@ test("client close() terminates the child process", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("a stdin EPIPE after child death rejects pending requests without throwing", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "my-pi-codegraph-"));
|
||||
const child = new EventEmitter() as ChildProcess;
|
||||
child.stdin = new PassThrough();
|
||||
child.stdout = new PassThrough();
|
||||
child.stderr = new PassThrough();
|
||||
child.exitCode = null;
|
||||
child.signalCode = null;
|
||||
const client = new CodegraphRpcClient({
|
||||
root: dir,
|
||||
spawn: () => child,
|
||||
requestTimeoutMs: 5_000,
|
||||
});
|
||||
try {
|
||||
const start = client.start();
|
||||
await sleep(20);
|
||||
// Without the stdin "error" listener this emit throws uncaught.
|
||||
child.stdin.emit(
|
||||
"error",
|
||||
Object.assign(new Error("write EPIPE"), { code: "EPIPE" }),
|
||||
);
|
||||
child.exitCode = 1;
|
||||
child.emit("exit", 1, null);
|
||||
child.emit("close", 1, null);
|
||||
await assert.rejects(start, /子进程退出/);
|
||||
assert.equal(client.dead, true);
|
||||
} finally {
|
||||
await client.close();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ── handler ─────────────────────────────────────────────────────────────────
|
||||
|
||||
test("indexed project explores through the serve child", async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, mkdtemp, readFile } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -292,9 +292,31 @@ test("all package extensions load together without global registration conflicts
|
||||
const home = await mkdtemp(join(tmpdir(), "my-pi-extension-load-"));
|
||||
const mockBin = join(home, "bin");
|
||||
await mkdir(mockBin, { recursive: true });
|
||||
const args = ["--no-extensions"];
|
||||
// Probe loaded LAST: records every registered tool (name + exposure) and the
|
||||
// final active set so the test can assert the pinned built-in codemode.
|
||||
const probeOutput = join(home, "probe-tools.json");
|
||||
const probePath = join(home, "probe.ts");
|
||||
await writeFile(
|
||||
probePath,
|
||||
[
|
||||
'import { writeFileSync } from "node:fs";',
|
||||
"export default function (pi: any): void {",
|
||||
'\tpi.on("session_start", () => {',
|
||||
"\t\twriteFileSync(",
|
||||
`\t\t\t${JSON.stringify(probeOutput)},`,
|
||||
"\t\t\tJSON.stringify({",
|
||||
"\t\t\t\tall: pi.getAllTools().map((tool: any) => ({ name: tool.name, exposure: tool.exposure })),",
|
||||
"\t\t\t\tactive: pi.getActiveTools(),",
|
||||
"\t\t\t}),",
|
||||
"\t\t);",
|
||||
"\t});",
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
const args = ["--no-extensions", "-e", "builtin:codemode"];
|
||||
for (const entry of packageJson.pi.extensions) args.push("-e", resolve(repositoryRoot, entry));
|
||||
args.push("--mode", "rpc");
|
||||
args.push("-e", probePath, "--mode", "rpc");
|
||||
|
||||
const loadEnvironment = {
|
||||
...process.env,
|
||||
@@ -313,4 +335,12 @@ test("all package extensions load together without global registration conflicts
|
||||
|
||||
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
||||
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /Failed to load extension|conflicts with/u);
|
||||
|
||||
const probe = JSON.parse(await readFile(probeOutput, "utf8")) as {
|
||||
all: { name: string; exposure?: string }[];
|
||||
active: string[];
|
||||
};
|
||||
const codemode = probe.all.find((tool) => tool.name === "codemode");
|
||||
assert.equal(codemode?.exposure, "model-only", "Pi's built-in codemode must load with model-only exposure");
|
||||
assert.ok(probe.active.includes("codemode"), "Tool Search must pin codemode into the active set");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { accessSync, chmodSync, constants } from "node:fs";
|
||||
import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
const LIB_SH = fileURLToPath(new URL("../scripts/lib.sh", import.meta.url));
|
||||
const SYSTEM_KITTY = "/Applications/kitty.app/Contents/MacOS/kitty";
|
||||
|
||||
function systemKittyPath(): string | undefined {
|
||||
try {
|
||||
accessSync(SYSTEM_KITTY, constants.X_OK);
|
||||
return SYSTEM_KITTY;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function runDetectKitty(home: string, pathDir: string) {
|
||||
return spawnSync(
|
||||
"sh",
|
||||
["-c", `. "$1"; detect_kitty; status=$?; printf '%s' "$kitty_bin"; exit "$status"`, "sh", LIB_SH],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, HOME: home, PATH: `${pathDir}:${process.env.PATH}` },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
test("detect_kitty prefers an official install over a PATH kitty", async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), "my-pi-libsh-"));
|
||||
try {
|
||||
const official = join(home, ".local", "kitty.app", "bin", "kitty");
|
||||
const pathDir = join(home, "pathbin");
|
||||
await mkdir(dirname(official), { recursive: true });
|
||||
await mkdir(pathDir, { recursive: true });
|
||||
await writeFile(official, "#!/bin/sh\n");
|
||||
chmodSync(official, 0o755);
|
||||
await symlink(official, join(pathDir, "kitty"));
|
||||
|
||||
const result = runDetectKitty(home, pathDir);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
// A real /Applications kitty also wins over $HOME/.local and PATH.
|
||||
assert.equal(result.stdout, systemKittyPath() ?? official);
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("detect_kitty falls back to a PATH kitty when no official install exists", async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), "my-pi-libsh-"));
|
||||
try {
|
||||
const pathDir = join(home, "pathbin");
|
||||
const pathKitty = join(pathDir, "kitty");
|
||||
await mkdir(pathDir, { recursive: true });
|
||||
await writeFile(pathKitty, "#!/bin/sh\n");
|
||||
chmodSync(pathKitty, 0o755);
|
||||
|
||||
const result = runDetectKitty(home, pathDir);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
// The isolated HOME has no ~/.local install; only a machine-level
|
||||
// /Applications kitty (when present) still takes precedence.
|
||||
assert.equal(result.stdout, systemKittyPath() ?? pathKitty);
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -43,6 +43,19 @@ test("every declared extension entry and file: dependency manifest is inside the
|
||||
}
|
||||
});
|
||||
|
||||
test("every file: dependency ships an UPSTREAM.md source record", () => {
|
||||
for (const [name, specifier] of Object.entries(packageJson.dependencies)) {
|
||||
const match = /^file:\.\/(.+)$/u.exec(specifier);
|
||||
if (!match) continue;
|
||||
const upstream = `${match[1]}/UPSTREAM.md`;
|
||||
assert.ok(
|
||||
readFileSync(join(repositoryRoot, upstream), "utf8").trim().length > 0,
|
||||
`${name} is a file: dependency — ${upstream} must exist and be non-empty`,
|
||||
);
|
||||
assert.ok(packedFiles.has(upstream), `${name} is a file: dependency — ${upstream} must be packed`);
|
||||
}
|
||||
});
|
||||
|
||||
test("relative import specifiers inside packed sources resolve to packed files", () => {
|
||||
const specifierPatterns = [
|
||||
/\bfrom\s+["'](\.{1,2}\/[^"']+)["']/gu,
|
||||
|
||||
+66
-12
@@ -1,4 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdtemp, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -142,19 +143,25 @@ test("template parser handles env names, braces and escapes", () => {
|
||||
const AUTH_MJS = new URL("../scripts/search-auth.mjs", import.meta.url).pathname;
|
||||
const CONFIGURE_SH = new URL("../configure.sh", import.meta.url).pathname;
|
||||
|
||||
function runMjs(args: string[], env: Record<string, string> = {}) {
|
||||
return spawnSync("node", [AUTH_MJS, ...args], { encoding: "utf8", env: { ...process.env, ...env } });
|
||||
function runMjs(args: string[], env: Record<string, string> = {}, input?: string) {
|
||||
return spawnSync("node", [AUTH_MJS, ...args], {
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, ...env },
|
||||
...(input === undefined ? {} : { input }),
|
||||
});
|
||||
}
|
||||
|
||||
test("search-auth.mjs --set writes api_key entries and preserves other providers", async () => {
|
||||
test("search-auth.mjs --set-stdin writes api_key entries and preserves other providers", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
|
||||
const authPath = join(dir, "auth.json");
|
||||
await writeFile(
|
||||
authPath,
|
||||
JSON.stringify({ anthropic: { type: "oauth", access: "tok", refresh: "ref", expires: 42, accountId: "acc" } }),
|
||||
);
|
||||
const result = runMjs(["--auth", authPath, "--set", "tavily=tvly-new", "--set", "exa=!op read x"]);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const first = runMjs(["--auth", authPath, "--set-stdin", "tavily"], {}, "tvly-new\n");
|
||||
assert.equal(first.status, 0, first.stderr);
|
||||
const second = runMjs(["--auth", authPath, "--set-stdin", "exa"], {}, " !op read x \n");
|
||||
assert.equal(second.status, 0, second.stderr);
|
||||
const written = JSON.parse(await readFile(authPath, "utf8")) as Record<string, any>;
|
||||
assert.deepEqual(written.anthropic, { type: "oauth", access: "tok", refresh: "ref", expires: 42, accountId: "acc" });
|
||||
assert.deepEqual(written.tavily, { type: "api_key", key: "tvly-new" });
|
||||
@@ -163,14 +170,33 @@ test("search-auth.mjs --set writes api_key entries and preserves other providers
|
||||
assert.equal(getSearchApiKey("tavily", { authPath }), "tvly-new");
|
||||
});
|
||||
|
||||
test("search-auth.mjs preserves an existing credential env map on --set", async () => {
|
||||
test("search-auth.mjs --set is rejected: keys never travel in argv", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
|
||||
const authPath = join(dir, "auth.json");
|
||||
const result = runMjs(["--auth", authPath, "--set", "tavily=tvly-leak"]);
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stderr, /命令行参数|--set-stdin/);
|
||||
assert.equal(existsSync(authPath), false);
|
||||
});
|
||||
|
||||
test("search-auth.mjs allows only one --set-stdin and rejects empty stdin", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
|
||||
const authPath = join(dir, "auth.json");
|
||||
const dup = runMjs(["--auth", authPath, "--set-stdin", "tavily", "--set-stdin", "exa"], {}, "key");
|
||||
assert.notEqual(dup.status, 0);
|
||||
const empty = runMjs(["--auth", authPath, "--set-stdin", "tavily"], {}, " \n");
|
||||
assert.notEqual(empty.status, 0);
|
||||
assert.equal(existsSync(authPath), false);
|
||||
});
|
||||
|
||||
test("search-auth.mjs preserves an existing credential env map on --set-stdin", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
|
||||
const authPath = join(dir, "auth.json");
|
||||
await writeFile(
|
||||
authPath,
|
||||
JSON.stringify({ keenable: { type: "api_key", key: "old", env: { KEENABLE_API_URL: "https://k.example" } } }),
|
||||
);
|
||||
const result = runMjs(["--auth", authPath, "--set", "keenable=new-key"]);
|
||||
const result = runMjs(["--auth", authPath, "--set-stdin", "keenable"], {}, "new-key");
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const written = JSON.parse(await readFile(authPath, "utf8")) as Record<string, any>;
|
||||
assert.deepEqual(written.keenable, {
|
||||
@@ -183,7 +209,7 @@ test("search-auth.mjs preserves an existing credential env map on --set", async
|
||||
test("search-auth.mjs creates a missing auth.json and honors PI_CODING_AGENT_DIR", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
|
||||
const agentDir = join(dir, "agent");
|
||||
const result = runMjs(["--set", "keenable=keen-1"], { PI_CODING_AGENT_DIR: agentDir });
|
||||
const result = runMjs(["--set-stdin", "keenable"], { PI_CODING_AGENT_DIR: agentDir }, "keen-1");
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const written = JSON.parse(await readFile(join(agentDir, "auth.json"), "utf8")) as Record<string, any>;
|
||||
assert.deepEqual(written.keenable, { type: "api_key", key: "keen-1" });
|
||||
@@ -204,7 +230,7 @@ test("search-auth.mjs refuses to clobber a malformed auth.json", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
|
||||
const authPath = join(dir, "auth.json");
|
||||
await writeFile(authPath, "{oops");
|
||||
const result = runMjs(["--auth", authPath, "--set", "tavily=x"]);
|
||||
const result = runMjs(["--auth", authPath, "--set-stdin", "tavily"], {}, "x");
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stderr, /不是合法 JSON|abort|clobber/i);
|
||||
assert.equal(await readFile(authPath, "utf8"), "{oops");
|
||||
@@ -238,15 +264,43 @@ test("search-auth.mjs --has and --list never print key material", async () => {
|
||||
assert.doesNotMatch(list.stdout, /supersecret/);
|
||||
});
|
||||
|
||||
test("configure.sh search writes keys through search-auth.mjs noninteractively", async () => {
|
||||
test("configure.sh search writes keys through search-auth.mjs via stdin", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
|
||||
const authPath = join(dir, "auth.json");
|
||||
const result = spawnSync("sh", [CONFIGURE_SH, "search", "--auth", authPath, "--tavily", "tvly-sh", "--exa", "exa-sh"], {
|
||||
const first = spawnSync("sh", [CONFIGURE_SH, "search", "--auth", authPath, "--tavily", "-"], {
|
||||
encoding: "utf8",
|
||||
input: "tvly-sh\n",
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(first.status, 0, first.stderr);
|
||||
const second = spawnSync("sh", [CONFIGURE_SH, "search", "--auth", authPath, "--exa", "-"], {
|
||||
encoding: "utf8",
|
||||
input: "exa-sh",
|
||||
});
|
||||
assert.equal(second.status, 0, second.stderr);
|
||||
const written = JSON.parse(await readFile(authPath, "utf8")) as Record<string, any>;
|
||||
assert.deepEqual(written.tavily, { type: "api_key", key: "tvly-sh" });
|
||||
assert.deepEqual(written.exa, { type: "api_key", key: "exa-sh" });
|
||||
assert.equal((await stat(authPath)).mode & 0o777, 0o600);
|
||||
});
|
||||
|
||||
test("configure.sh search rejects a plaintext key argument", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
|
||||
const authPath = join(dir, "auth.json");
|
||||
const result = spawnSync("sh", [CONFIGURE_SH, "search", "--auth", authPath, "--tavily", "tvly-leak"], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stderr, /命令行参数/);
|
||||
assert.equal(existsSync(authPath), false);
|
||||
});
|
||||
|
||||
test("configure.sh search allows at most one stdin provider per call", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
|
||||
const authPath = join(dir, "auth.json");
|
||||
const result = spawnSync("sh", [CONFIGURE_SH, "search", "--auth", authPath, "--tavily", "-", "--exa", "-"], {
|
||||
encoding: "utf8",
|
||||
input: "one-key",
|
||||
});
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stderr, /最多一个/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user