Initialize repository with harness code and assets

Initial import of all source code, config, and README assets: the
packages workspace (cli, core, server, web, docs, landing, skills),
build scripts, tooling config, and CI workflows.

Includes the data-layout revision made on this branch: the local data
root defaults to ~/.penguin/data (PENGUIN_HOME still overrides; the
installer keeps its binaries in ~/.penguin), and every Agent lives
under <project>/agents/<agent>/ — path helpers, the three
agent-enumeration scans, the system prompt, built-in Skills, tests
and docs all follow the new layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ihk8iQuo3kv2aPjAYEPuR
This commit is contained in:
Yaowei Zheng
2026-07-19 14:06:53 +08:00
committed by GitHub
parent 056bed7aeb
commit 45bfae6e94
543 changed files with 92949 additions and 0 deletions
+301
View File
@@ -0,0 +1,301 @@
/**
* Integration tests for `penguin config model add|default|vision|list` (run through
* commander's parseAsync for the full command path): --model-id always takes the
* upstream id, paired with --provider to form a (provider, model_id) reference (add's
* --provider defaults to catalog-based inference, falling back to custom when
* inference fails; default / vision require --provider and raise an error when the
* reference isn't found in models — no string concatenation is ever performed); --root
* specifies the data root directory (takes priority over PENGUIN_HOME); persisted to a
* single hidden .project_config.toml (mode 0600, credentials inline, provider and
* model_id as separate columns); list displays provider and model_id as separate
* columns.
*/
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Command } from "commander";
import { parse as parseToml } from "smol-toml";
import { DEFAULT_PROJECT_ID, projectConfigPath } from "@prismshadow/penguin-core";
import { registerConfigCommand } from "../src/commands/config.js";
import { getMessages } from "../src/i18n.js";
let tmpHome: string;
let tmpRoot: string;
let prevHome: string | undefined;
beforeEach(async () => {
prevHome = process.env.PENGUIN_HOME;
tmpHome = await fs.mkdtemp(path.join(os.tmpdir(), "penguin-cli-home-"));
tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "penguin-cli-root-"));
process.env.PENGUIN_HOME = tmpHome;
});
afterEach(async () => {
if (prevHome === undefined) delete process.env.PENGUIN_HOME;
else process.env.PENGUIN_HOME = prevHome;
await fs.rm(tmpHome, { recursive: true, force: true });
await fs.rm(tmpRoot, { recursive: true, force: true });
});
interface TomlModelRef {
provider: string;
model_id: string;
}
/**
* Runs a `penguin config model …` command, capturing stdout / stderr and the exit code
* (without actually exiting the process; under exitOverride, commander usage errors —
* such as a missing required option — are thrown as a CommanderError, which is
* converted to a non-zero exit code).
*/
async function runModel(args: string[]): Promise<{ out: string; err: string; code: number }> {
const program = new Command();
program.exitOverride();
registerConfigCommand(program, getMessages("en"));
const out: string[] = [];
const err: string[] = [];
const outSpy = vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
out.push(String(chunk));
return true;
});
const errSpy = vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
err.push(String(chunk));
return true;
});
const prevExitCode = process.exitCode;
process.exitCode = undefined;
try {
await program.parseAsync(["node", "penguin", "config", "model", ...args]);
return { out: out.join(""), err: err.join(""), code: Number(process.exitCode ?? 0) };
} catch (e) {
const exitCode = (e as { exitCode?: number }).exitCode;
return { out: out.join(""), err: err.join(""), code: exitCode || 1 };
} finally {
outSpy.mockRestore();
errSpy.mockRestore();
process.exitCode = prevExitCode;
}
}
describe("penguin config model add/list(--root 与 provider / model_id 分列存储)", () => {
it("--root 优先于 PENGUIN_HOME:落盘到指定根目录的隐藏 .project_config.toml(0600)", async () => {
const add = await runModel([
"add",
"--model-id",
"my-own-model",
"--api-key",
"sk-root-secret-1",
"--root",
tmpRoot,
]);
expect(add.code).toBe(0);
// Catalog inference fails -> falls back to the custom group (provider is a separate field, never concatenated into the id).
expect(add.out).toContain("Added model (provider=custom, model_id=my-own-model).");
const file = projectConfigPath(tmpRoot, DEFAULT_PROJECT_ID);
expect(path.basename(file)).toBe(".project_config.toml");
expect((await fs.stat(file)).mode & 0o777).toBe(0o600);
const parsed = parseToml(await fs.readFile(file, "utf8")) as {
models: Array<Record<string, unknown>>;
};
const entry = parsed.models.find(
(m) => m.provider === "custom" && m.model_id === "my-own-model",
);
expect(entry).toBeDefined();
expect(entry?.api_key).toBe("sk-root-secret-1");
// Concatenated storage id and request_model_id have been removed.
expect(entry?.request_model_id).toBeUndefined();
// The root directory pointed to by PENGUIN_HOME is unaffected.
await expect(fs.access(projectConfigPath(tmpHome, DEFAULT_PROJECT_ID))).rejects.toThrow();
// list also reads --root: provider and model_id as separate columns + masked api_key (the request column has been removed).
const list = await runModel(["list", "--root", tmpRoot]);
expect(list.code).toBe(0);
const line = list.out.split("\n").find((l) => l.includes("my-own-model"));
expect(line).toMatch(/custom\s+my-own-model/);
expect(line).toContain("api_key=****et-1");
expect(list.out).not.toContain("request=");
expect(list.out).not.toContain("sk-root-secret-1");
});
it("内置目录推断分组:上游 id 命中目录时条目落该 provider;--set-default 写成对引用", async () => {
const add = await runModel([
"add",
"--model-id",
"claude-sonnet-4-6",
"--set-default",
"--root",
tmpRoot,
]);
expect(add.code).toBe(0);
expect(add.out).toContain("Updated model (provider=anthropic, model_id=claude-sonnet-4-6).");
expect(add.out).toContain("Default model: (provider=anthropic, model_id=claude-sonnet-4-6)");
const parsed = parseToml(
await fs.readFile(projectConfigPath(tmpRoot, DEFAULT_PROJECT_ID), "utf8"),
) as unknown as { default_model: TomlModelRef; models: Array<Record<string, unknown>> };
expect(parsed.default_model).toEqual({
provider: "anthropic",
model_id: "claude-sonnet-4-6",
});
expect(
parsed.models.find((m) => m.provider === "anthropic" && m.model_id === "claude-sonnet-4-6"),
).toBeDefined();
});
it("--provider 显式指定分组:同名上游 id 与预置条目互不冲突(各自独立条目)", async () => {
const add = await runModel([
"add",
"--model-id",
"claude-sonnet-4-6",
"--provider",
"myproxy",
"--base-url",
"https://proxy.example/v1",
"--root",
tmpRoot,
]);
expect(add.code).toBe(0);
expect(add.out).toContain("Added model (provider=myproxy, model_id=claude-sonnet-4-6).");
const list = await runModel(["list", "--root", tmpRoot]);
const line = list.out.split("\n").find((l) => l.includes("myproxy"));
expect(line).toMatch(/myproxy\s+claude-sonnet-4-6/);
expect(line).toContain("base_url=https://proxy.example/v1");
// The pre-existing anthropic entry remains (the (provider, model_id) pair naturally disambiguates).
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 () => {
// 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]);
// A non-catalog id under a first-party vendor group: client_type is not set (AgentHub auto-routes by upstream id).
await runModel([
"add",
"--model-id",
"my-fine-tune",
"--provider",
"deepseek",
"--root",
tmpRoot,
]);
// Gateway group: openai + the gateway's endpoint base URL pre-filled.
await runModel([
"add",
"--model-id",
"acme/some-model",
"--provider",
"openrouter",
"--root",
tmpRoot,
]);
// An explicit --client-type is persisted as-is, not overridden by the default rule.
await runModel([
"add",
"--model-id",
"special-1",
"--provider",
"mylab",
"--client-type",
"verbatim-type",
"--root",
tmpRoot,
]);
const parsed = parseToml(
await fs.readFile(projectConfigPath(tmpRoot, DEFAULT_PROJECT_ID), "utf8"),
) as { models: Array<Record<string, unknown>> };
const by = (p: string, id: string) =>
parsed.models.find((m) => m.provider === p && m.model_id === id)!;
expect(by("custom", "my-openai-proxy").client_type).toBe("openai");
expect(by("mylab", "in-house-1").client_type).toBe("openai");
expect(by("deepseek", "my-fine-tune").client_type).toBeUndefined();
expect(by("openrouter", "acme/some-model").client_type).toBe("openai");
expect(by("openrouter", "acme/some-model").base_url).toBe("https://openrouter.ai/api/v1");
expect(by("mylab", "special-1").client_type).toBe("verbatim-type");
});
it("model default 经 --root 指定根目录设置默认模型(--model-id 上游 id + --provider 成对)", async () => {
const set = await runModel([
"default",
"--model-id",
"deepseek-v4-flash",
"--provider",
"deepseek",
"--root",
tmpRoot,
]);
expect(set.code).toBe(0);
expect(set.out).toContain(
"Default model set to (provider=deepseek, model_id=deepseek-v4-flash).",
);
const parsed = parseToml(
await fs.readFile(projectConfigPath(tmpRoot, DEFAULT_PROJECT_ID), "utf8"),
) as unknown as { default_model: TomlModelRef };
expect(parsed.default_model).toEqual({
provider: "deepseek",
model_id: "deepseek-v4-flash",
});
});
});
describe("model default/vision:--provider 必填,(provider, model_id) 成对引用", () => {
it("缺 --provider:commander 用法报错,非零退出码", 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 () => {
const bad = await runModel([
"default",
"--model-id",
"no-such-model",
"--provider",
"custom",
"--root",
tmpRoot,
]);
expect(bad.code).toBe(1);
expect(bad.err).toContain("(provider=custom, model_id=no-such-model)");
expect(bad.err).toContain("penguin config model list");
// The upstream id matches a pre-existing entry but --provider names the wrong group: also not found (exact pair, no fuzzy matching).
const wrongGroup = await runModel([
"vision",
"--model-id",
"claude-sonnet-4-6",
"--provider",
"openai",
"--root",
tmpRoot,
]);
expect(wrongGroup.code).toBe(1);
expect(wrongGroup.err).toContain("(provider=openai, model_id=claude-sonnet-4-6)");
expect(wrongGroup.err).toContain("penguin config model list");
});
it("model vision 成对引用命中:设置视觉模型(落盘内联表)", async () => {
const ok = await runModel([
"vision",
"--model-id",
"claude-sonnet-4-6",
"--provider",
"anthropic",
"--root",
tmpRoot,
]);
expect(ok.code).toBe(0);
expect(ok.out).toContain(
"Vision model set to (provider=anthropic, model_id=claude-sonnet-4-6).",
);
const parsed = parseToml(
await fs.readFile(projectConfigPath(tmpRoot, DEFAULT_PROJECT_ID), "utf8"),
) as unknown as { vision_model: TomlModelRef };
expect(parsed.vision_model).toEqual({
provider: "anthropic",
model_id: "claude-sonnet-4-6",
});
});
});