Make English the repository working language (#5)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,40 +2,40 @@ import { describe, expect, it } from "vitest";
|
||||
import { decideSigint } from "../src/commands/chat.js";
|
||||
import { parseApprovalAnswer } from "../src/approval.js";
|
||||
|
||||
describe("decideSigint (Ctrl-C 行为状态机)", () => {
|
||||
it("approving → deny(无论缓冲区是否有内容)", () => {
|
||||
describe("decideSigint (Ctrl-C behavior state machine)", () => {
|
||||
it("approving -> deny (whether or not the buffer has content)", () => {
|
||||
expect(decideSigint("approving", false)).toBe("deny");
|
||||
expect(decideSigint("approving", true)).toBe("deny");
|
||||
});
|
||||
|
||||
it("running → abort(中断当前 Task,不退出)", () => {
|
||||
it("running -> abort (interrupts the current Task, does not exit)", () => {
|
||||
expect(decideSigint("running", false)).toBe("abort");
|
||||
expect(decideSigint("running", true)).toBe("abort");
|
||||
});
|
||||
|
||||
it("idle + 有输入 → clear(清空缓冲区)", () => {
|
||||
it("idle + input -> clear (empties the buffer)", () => {
|
||||
expect(decideSigint("idle", true)).toBe("clear");
|
||||
});
|
||||
|
||||
it("idle + 无输入 → confirm-exit(弹出 y/N 退出确认)", () => {
|
||||
it("idle + no input -> confirm-exit (shows the y/N exit confirmation)", () => {
|
||||
expect(decideSigint("idle", false)).toBe("confirm-exit");
|
||||
});
|
||||
|
||||
it("confirming-exit → exit(确认中再次 Ctrl-C 直接退出)", () => {
|
||||
it("confirming-exit -> exit (a second Ctrl-C during confirmation exits immediately)", () => {
|
||||
expect(decideSigint("confirming-exit", false)).toBe("exit");
|
||||
expect(decideSigint("confirming-exit", true)).toBe("exit");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseApprovalAnswer", () => {
|
||||
it("y / yes(trim、不区分大小写)→ allow;n / no → deny", () => {
|
||||
it("y / yes (trimmed, case-insensitive) -> allow; n / no -> deny", () => {
|
||||
expect(parseApprovalAnswer("y")).toBe("allow");
|
||||
expect(parseApprovalAnswer(" YES \n")).toBe("allow");
|
||||
expect(parseApprovalAnswer("Y")).toBe("allow");
|
||||
expect(parseApprovalAnswer("n")).toBe("deny");
|
||||
expect(parseApprovalAnswer("NO")).toBe("deny");
|
||||
});
|
||||
it("空/无关输入用 fallback(缺省 deny;工具审批传 allow)", () => {
|
||||
it("empty/unrelated input uses the fallback (default deny; tool approval passes allow)", () => {
|
||||
expect(parseApprovalAnswer("")).toBe("deny"); // default fallback
|
||||
expect(parseApprovalAnswer("nope")).toBe("deny");
|
||||
expect(parseApprovalAnswer("", "allow")).toBe("allow"); // tool approval defaults to allow
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("formatModelRows", () => {
|
||||
],
|
||||
};
|
||||
|
||||
it("provider 与 model_id 双列展示;预置模型 vision 经目录成对匹配,默认模型以 * 标记", () => {
|
||||
it("provider and model_id shown as two columns; preset model vision resolved by catalog pair match, default model marked with *", () => {
|
||||
const lines = formatModelRows(cfg);
|
||||
expect(lines).toHaveLength(2);
|
||||
expect(lines[0]).toMatch(/^\* anthropic\s+claude-sonnet-4-6\s+vision=Y/);
|
||||
@@ -39,7 +39,7 @@ describe("formatModelRows", () => {
|
||||
expect(lines[0]).not.toContain("anthropic/claude-sonnet-4-6");
|
||||
});
|
||||
|
||||
it("自定义模型 vision 按标注(显式 false 记 -);内联 api_key 掩码显示", () => {
|
||||
it("custom model vision follows its annotation (explicit false shown as -); inline api_key displayed masked", () => {
|
||||
const lines = formatModelRows(cfg);
|
||||
expect(lines[1]).toMatch(/^ {2}custom\s+my-proxy-model\s+vision=-/);
|
||||
expect(lines[1]).toContain("client_type=openai");
|
||||
@@ -47,7 +47,7 @@ describe("formatModelRows", () => {
|
||||
expect(lines[1]).not.toContain("sk-test-abcd-1234");
|
||||
});
|
||||
|
||||
it("同名 model_id 双 provider 并存时各占一行,默认标记只落在成对命中的那行", () => {
|
||||
it("two providers sharing a model_id each get their own row; the default marker lands only on the exact pair match", () => {
|
||||
const lines = formatModelRows({
|
||||
default_model: { provider: "deepseek", model_id: "m1" },
|
||||
models: [
|
||||
@@ -59,7 +59,7 @@ describe("formatModelRows", () => {
|
||||
expect(lines[1]).toMatch(/^ {2}siliconflow\s+m1\s+vision=Y/);
|
||||
});
|
||||
|
||||
it("无标注按「缺省=支持」记 Y;全空列省略", () => {
|
||||
it('unannotated vision defaults to "supported" and shows Y; fully empty columns are omitted', () => {
|
||||
const lines = formatModelRows({
|
||||
models: [{ provider: "custom", model_id: "m1" }],
|
||||
});
|
||||
|
||||
@@ -78,8 +78,8 @@ async function runModel(args: string[]): Promise<{ out: string; err: string; cod
|
||||
}
|
||||
}
|
||||
|
||||
describe("penguin config model add/list(--root 与 provider / model_id 分列存储)", () => {
|
||||
it("--root 优先于 PENGUIN_HOME:落盘到指定根目录的隐藏 .project_config.toml(0600)", async () => {
|
||||
describe("penguin config model add/list (--root plus provider / model_id stored as separate fields)", () => {
|
||||
it("--root beats PENGUIN_HOME: written to the hidden .project_config.toml under the given root (0600)", async () => {
|
||||
const add = await runModel([
|
||||
"add",
|
||||
"--model-id",
|
||||
@@ -119,7 +119,7 @@ describe("penguin config model add/list(--root 与 provider / model_id 分列
|
||||
expect(list.out).not.toContain("sk-root-secret-1");
|
||||
});
|
||||
|
||||
it("内置目录推断分组:上游 id 命中目录时条目落该 provider;--set-default 写成对引用", async () => {
|
||||
it("built-in catalog infers the grouping: an upstream id matching the catalog lands under that provider; --set-default writes a pair reference", async () => {
|
||||
const add = await runModel([
|
||||
"add",
|
||||
"--model-id",
|
||||
@@ -144,7 +144,7 @@ describe("penguin config model add/list(--root 与 provider / model_id 分列
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it("--provider 显式指定分组:同名上游 id 与预置条目互不冲突(各自独立条目)", async () => {
|
||||
it("--provider picks the grouping explicitly: a same-name upstream id does not clash with the preset entry (independent entries)", async () => {
|
||||
const add = await runModel([
|
||||
"add",
|
||||
"--model-id",
|
||||
@@ -167,7 +167,7 @@ describe("penguin config model add/list(--root 与 provider / model_id 分列
|
||||
expect(list.out.split("\n").some((l) => /anthropic\s+claude-sonnet-4-6/.test(l))).toBe(true);
|
||||
});
|
||||
|
||||
it("client_type 缺省按分组语义(PRN-021):custom / 自建 / 网关落 openai,一方厂商不落", async () => {
|
||||
it("client_type defaults by grouping semantics (PRN-021): custom / self-hosted / gateway get openai, first-party providers get none", async () => {
|
||||
// custom (catalog inference fails) and self-hosted groups (--provider not a catalog value): default to client_type=openai.
|
||||
await runModel(["add", "--model-id", "my-openai-proxy", "--root", tmpRoot]);
|
||||
await runModel(["add", "--model-id", "in-house-1", "--provider", "mylab", "--root", tmpRoot]);
|
||||
@@ -217,7 +217,7 @@ describe("penguin config model add/list(--root 与 provider / model_id 分列
|
||||
expect(by("mylab", "special-1").client_type).toBe("verbatim-type");
|
||||
});
|
||||
|
||||
it("model default 经 --root 指定根目录设置默认模型(--model-id 上游 id + --provider 成对)", async () => {
|
||||
it("model default sets the default model under the --root data root (--model-id upstream id + --provider as a pair)", async () => {
|
||||
const set = await runModel([
|
||||
"default",
|
||||
"--model-id",
|
||||
@@ -241,14 +241,14 @@ describe("penguin config model add/list(--root 与 provider / model_id 分列
|
||||
});
|
||||
});
|
||||
|
||||
describe("model default/vision:--provider 必填,(provider, model_id) 成对引用", () => {
|
||||
it("缺 --provider:commander 用法报错,非零退出码", async () => {
|
||||
describe("model default/vision: --provider is required, (provider, model_id) pair reference", () => {
|
||||
it("missing --provider: commander usage error, nonzero exit code", async () => {
|
||||
const bad = await runModel(["default", "--model-id", "deepseek-v4-flash", "--root", tmpRoot]);
|
||||
expect(bad.code).not.toBe(0);
|
||||
expect(bad.err).toContain("--provider");
|
||||
});
|
||||
|
||||
it("引用落空:成对引用不在 models 中,报错带成对引用与 model list 提示", async () => {
|
||||
it("dangling reference: the pair is not in models; the error carries the pair reference and a model list hint", async () => {
|
||||
const bad = await runModel([
|
||||
"default",
|
||||
"--model-id",
|
||||
@@ -276,7 +276,7 @@ describe("model default/vision:--provider 必填,(provider, model_id) 成对
|
||||
expect(wrongGroup.err).toContain("penguin config model list");
|
||||
});
|
||||
|
||||
it("model vision 成对引用命中:设置视觉模型(落盘内联表)", async () => {
|
||||
it("model vision pair reference hit: sets the vision model (written as an inline table)", async () => {
|
||||
const ok = await runModel([
|
||||
"vision",
|
||||
"--model-id",
|
||||
|
||||
@@ -57,7 +57,7 @@ async function runVault(args: string[]): Promise<{ out: string; err: string; cod
|
||||
}
|
||||
|
||||
describe("penguin config vault", () => {
|
||||
it("set → list(掩码)→ remove 全链路;落盘为隐藏 .vault.toml 且 0600", async () => {
|
||||
it("set -> list (masked) -> remove full round trip; stored as hidden .vault.toml with 0600", async () => {
|
||||
const set = await runVault(["set", "--key", "MY_KEY", "--value", "vault-secret-9876"]);
|
||||
expect(set.code).toBe(0);
|
||||
expect(set.out).toContain("Saved vault entry MY_KEY.");
|
||||
@@ -81,7 +81,7 @@ describe("penguin config vault", () => {
|
||||
expect(empty.out).toContain("The vault is empty.");
|
||||
});
|
||||
|
||||
it("--agent-id 定向到目标 Agent 的 vault,不影响 default_agent", async () => {
|
||||
it("--agent-id targets that Agent's vault without touching default_agent", async () => {
|
||||
const set = await runVault([
|
||||
"set",
|
||||
"--key",
|
||||
@@ -99,7 +99,7 @@ describe("penguin config vault", () => {
|
||||
expect(defaultList.out).toContain("The vault is empty.");
|
||||
});
|
||||
|
||||
it("--root 指定数据根目录(优先于 PENGUIN_HOME),set/list 均定向到该根目录", async () => {
|
||||
it("--root picks the data root (beats PENGUIN_HOME); set/list both target that root", async () => {
|
||||
const otherRoot = await fs.mkdtemp(path.join(os.tmpdir(), "penguin-cli-vault-root-"));
|
||||
try {
|
||||
const set = await runVault([
|
||||
@@ -125,7 +125,7 @@ describe("penguin config vault", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("非法键名 / 超长值以非零码退出并打印原因;remove 不存在的键报错", async () => {
|
||||
it("invalid key name / oversized value exit nonzero and print the reason; removing a missing key errors", async () => {
|
||||
const badKey = await runVault(["set", "--key", "1BAD", "--value", "v"]);
|
||||
expect(badKey.code).toBe(1);
|
||||
expect(badKey.err).toContain("Invalid vault key");
|
||||
|
||||
@@ -292,7 +292,7 @@ describe("StreamRenderer", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("轮结束后的压缩:压缩完成行展示本次消耗,但不计入本轮统计增量;不更新上下文", () => {
|
||||
it("compaction after the turn ends: the completion line shows its own cost, excluded from the turn stats delta; context not updated", () => {
|
||||
const { stream, text } = collector();
|
||||
const r = new StreamRenderer(stream, t);
|
||||
// Ordinary request: context 5000.
|
||||
@@ -325,7 +325,7 @@ describe("StreamRenderer", () => {
|
||||
expect(s).toContain("tokens 14k (+5k)");
|
||||
});
|
||||
|
||||
it("轮途中的压缩(其后还有普通 request_end):用时含压缩跨度,Token 增量计入压缩", () => {
|
||||
it("mid-turn compaction (a normal request_end follows): elapsed time includes the compaction span, Token delta includes compaction", () => {
|
||||
const { stream, text } = collector();
|
||||
const r = new StreamRenderer(stream, t);
|
||||
// own1: ordinary request, request 5000, 00:00 -> 00:02.
|
||||
@@ -362,7 +362,7 @@ describe("StreamRenderer", () => {
|
||||
expect(s).toContain("16s (+16s)");
|
||||
});
|
||||
|
||||
it("轮结束后的压缩(带 request 事件):用时止于压缩前的最后一个 request_end", () => {
|
||||
it("compaction after the turn ends (with request events): elapsed time stops at the last request_end before compaction", () => {
|
||||
const { stream, text } = collector();
|
||||
const r = new StreamRenderer(stream, t);
|
||||
// own1: 00:00 -> 00:03.
|
||||
|
||||
@@ -10,19 +10,19 @@ import {
|
||||
} from "../src/commands/serve.js";
|
||||
import { getMessages } from "../src/i18n.js";
|
||||
|
||||
describe("resolvePort(选项 > 环境变量 > 缺省 7364)", () => {
|
||||
it("都未给时用缺省 7364", () => {
|
||||
describe("resolvePort (option > env var > default 7364)", () => {
|
||||
it("uses the default 7364 when neither is given", () => {
|
||||
expect(DEFAULT_PORT).toBe(7364);
|
||||
expect(resolvePort(undefined, undefined)).toBe(7364);
|
||||
expect(resolvePort(undefined, "")).toBe(7364); // an empty string counts as unset
|
||||
});
|
||||
it("只有环境变量时取环境变量", () => {
|
||||
it("takes the env var when only the env var is set", () => {
|
||||
expect(resolvePort(undefined, "8080")).toBe(8080);
|
||||
});
|
||||
it("选项优先于环境变量", () => {
|
||||
it("the option beats the env var", () => {
|
||||
expect(resolvePort("9000", "8080")).toBe(9000);
|
||||
});
|
||||
it("非法值(非整数 / 越界)抛错", () => {
|
||||
it("throws on invalid values (non-integer / out of range)", () => {
|
||||
expect(() => resolvePort("abc", undefined)).toThrow(/abc/);
|
||||
expect(() => resolvePort("3.14", undefined)).toThrow();
|
||||
expect(() => resolvePort("-1", undefined)).toThrow();
|
||||
@@ -31,36 +31,36 @@ describe("resolvePort(选项 > 环境变量 > 缺省 7364)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("browserCommand(按平台选择打开命令)", () => {
|
||||
describe("browserCommand (picks the open command per platform)", () => {
|
||||
const url = "http://127.0.0.1:7364/";
|
||||
it("darwin → open", () => {
|
||||
expect(browserCommand("darwin", url)).toEqual({ command: "open", args: [url] });
|
||||
});
|
||||
it("win32 → cmd /c start(空标题占位在 URL 前)", () => {
|
||||
it("win32 -> cmd /c start (empty title placeholder before the URL)", () => {
|
||||
expect(browserCommand("win32", url)).toEqual({
|
||||
command: "cmd",
|
||||
args: ["/c", "start", "", url],
|
||||
});
|
||||
});
|
||||
it("其他平台(linux 等)→ xdg-open", () => {
|
||||
it("other platforms (linux etc.) -> xdg-open", () => {
|
||||
expect(browserCommand("linux", url)).toEqual({ command: "xdg-open", args: [url] });
|
||||
expect(browserCommand("freebsd", url)).toEqual({ command: "xdg-open", args: [url] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("browserUrl(通配监听地址转 127.0.0.1)", () => {
|
||||
it("常规 host 原样拼接", () => {
|
||||
describe("browserUrl (wildcard listen addresses map to 127.0.0.1)", () => {
|
||||
it("a regular host is joined as-is", () => {
|
||||
expect(browserUrl(DEFAULT_HOST, 7364)).toBe("http://127.0.0.1:7364/");
|
||||
expect(browserUrl("192.168.1.2", 8080)).toBe("http://192.168.1.2:8080/");
|
||||
});
|
||||
it("0.0.0.0 / :: 时浏览器 URL 用 127.0.0.1", () => {
|
||||
it("the browser URL uses 127.0.0.1 for 0.0.0.0 / ::", () => {
|
||||
expect(browserUrl("0.0.0.0", 7364)).toBe("http://127.0.0.1:7364/");
|
||||
expect(browserUrl("::", 7364)).toBe("http://127.0.0.1:7364/");
|
||||
});
|
||||
});
|
||||
|
||||
describe("registerServeCommands(命令注册)", () => {
|
||||
it("注册 server 与 web 两个顶层命令,web 缺省 open=true(--no-open 可关)", () => {
|
||||
describe("registerServeCommands (command registration)", () => {
|
||||
it("registers the server and web top-level commands; web defaults to open=true (--no-open turns it off)", () => {
|
||||
const program = new Command();
|
||||
registerServeCommands(program, getMessages("en"));
|
||||
const names = program.commands.map((c) => c.name());
|
||||
|
||||
Reference in New Issue
Block a user