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:
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Session title generation unit tests: prompt shape, sanitization rules, single-shot request
|
||||
* driving (fake LLM), and Session.generateTitle's composition-layer wiring (no real requests sent).
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
assistantText,
|
||||
buildTitlePrompt,
|
||||
emptyTokenCounts,
|
||||
generateTitleWithLLM,
|
||||
sanitizeTitle,
|
||||
Session,
|
||||
thinkingMessage,
|
||||
tokenUsage,
|
||||
userText,
|
||||
} from "../src/index.js";
|
||||
import type {
|
||||
EnvironmentInterface,
|
||||
LLMInterface,
|
||||
LLMOutcome,
|
||||
OmniMessage,
|
||||
SessionMetaPayload,
|
||||
} from "../src/index.js";
|
||||
|
||||
/** A fake LLM: yields the given messages and finishes with the outcome; records the prompt received. */
|
||||
function fakeLLM(
|
||||
outputs: OmniMessage[],
|
||||
outcome: LLMOutcome = { status: "completed" },
|
||||
seenPrompts: string[] = [],
|
||||
): LLMInterface {
|
||||
return {
|
||||
async *streamGenerate({ newMessages }) {
|
||||
const first = newMessages[0];
|
||||
if (first) seenPrompts.push((first.payload as { text: string }).text);
|
||||
for (const msg of outputs) yield msg;
|
||||
return outcome;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const fakeEnvironment: EnvironmentInterface = {
|
||||
listTools: async () => [],
|
||||
// eslint-disable-next-line require-yield
|
||||
executeTool: async function* () {
|
||||
throw new Error("not used");
|
||||
},
|
||||
toolPermission: () => undefined,
|
||||
};
|
||||
|
||||
const META: SessionMetaPayload = {
|
||||
session_id: "session-title-1",
|
||||
provider: "custom",
|
||||
model_id: "m1",
|
||||
model_context_window: 1000,
|
||||
system_prompt: "sp",
|
||||
tools: [],
|
||||
thinking_level: "default",
|
||||
agent_state: "/tmp/state",
|
||||
workspace: "/tmp/w",
|
||||
};
|
||||
|
||||
describe("session-title", () => {
|
||||
it("generateTitleWithLLM:收集模型 text 与用量,清洗后返回", async () => {
|
||||
const seen: string[] = [];
|
||||
const result = await generateTitleWithLLM(
|
||||
fakeLLM(
|
||||
[
|
||||
thinkingMessage("想一下"), // thinking does not count
|
||||
assistantText("「Tailwind 主题配置」。"),
|
||||
tokenUsage(emptyTokenCounts(), { cache_read: 1, cache_write: 2, output: 3, total: 6 }),
|
||||
],
|
||||
{ status: "completed" },
|
||||
seen,
|
||||
),
|
||||
{ userText: "解释 @theme", assistantText: "好的……" },
|
||||
);
|
||||
expect(result.title).toBe("Tailwind 主题配置");
|
||||
expect(result.usage).toEqual({ cache_read: 1, cache_write: 2, output: 3, total: 6 });
|
||||
expect(seen[0]).toBe(buildTitlePrompt("解释 @theme", "好的……"));
|
||||
expect(seen[0]).toContain("SAME language");
|
||||
});
|
||||
|
||||
it("素材为空不发请求;outcome 非 completed 时 title 为 null(usage 保留)", async () => {
|
||||
const seen: string[] = [];
|
||||
const empty = await generateTitleWithLLM(fakeLLM([], { status: "completed" }, seen), {
|
||||
userText: " ",
|
||||
assistantText: "a",
|
||||
});
|
||||
expect(empty).toEqual({ title: null, usage: null });
|
||||
expect(seen).toHaveLength(0);
|
||||
|
||||
const failed = await generateTitleWithLLM(
|
||||
fakeLLM(
|
||||
[
|
||||
assistantText("半截"),
|
||||
tokenUsage(emptyTokenCounts(), { cache_read: 0, cache_write: 0, output: 1, total: 1 }),
|
||||
],
|
||||
{ status: "failed", message: "401" },
|
||||
),
|
||||
{ userText: "u", assistantText: "a" },
|
||||
);
|
||||
expect(failed.title).toBeNull();
|
||||
expect(failed.usage?.total).toBe(1);
|
||||
});
|
||||
|
||||
it("助手素材为空也生成(纯工具轮次):只据用户请求,prompt 省去助手段", async () => {
|
||||
const seen: string[] = [];
|
||||
const result = await generateTitleWithLLM(
|
||||
fakeLLM([assistantText("配置 Tailwind 主题")], { status: "completed" }, seen),
|
||||
{ userText: "帮我配置 @theme", assistantText: "" },
|
||||
);
|
||||
expect(result.title).toBe("配置 Tailwind 主题");
|
||||
expect(seen[0]).toBe(buildTitlePrompt("帮我配置 @theme", ""));
|
||||
expect(seen[0]).not.toContain("[Assistant]");
|
||||
});
|
||||
|
||||
it("sanitizeTitle:剥引号与句读到稳定、折叠空白、超长截断、空返回 null", () => {
|
||||
expect(sanitizeTitle("“ 构建配置 说明 。”")).toBe("构建配置 说明");
|
||||
expect(sanitizeTitle("『标题』!")).toBe("标题");
|
||||
expect(sanitizeTitle(" \n ")).toBeNull();
|
||||
expect(sanitizeTitle("x".repeat(50))).toHaveLength(30);
|
||||
});
|
||||
|
||||
it("Session.generateTitle:经 createBareLLM 发起;未提供工厂时返回 null", async () => {
|
||||
const withFactory = new Session({
|
||||
meta: META,
|
||||
llm: fakeLLM([]),
|
||||
environment: fakeEnvironment,
|
||||
createBareLLM: () => fakeLLM([assistantText("标题 A")]),
|
||||
});
|
||||
expect(
|
||||
await withFactory.generateTitle({ material: { userText: "u", assistantText: "a" } }),
|
||||
).toEqual({
|
||||
title: "标题 A",
|
||||
usage: null,
|
||||
});
|
||||
|
||||
const withoutFactory = new Session({
|
||||
meta: META,
|
||||
llm: fakeLLM([]),
|
||||
environment: fakeEnvironment,
|
||||
});
|
||||
expect(await withoutFactory.generateTitle()).toEqual({
|
||||
title: null,
|
||||
usage: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Session.generateTitle:素材自采(run 收集用户输入与模型正文),无需调用方提供", async () => {
|
||||
const seen: string[] = [];
|
||||
const session = new Session({
|
||||
meta: META,
|
||||
llm: fakeLLM([thinkingMessage("想想"), assistantText("答案正文")]),
|
||||
environment: fakeEnvironment,
|
||||
createBareLLM: () => fakeLLM([assistantText("标题 B")], { status: "completed" }, seen),
|
||||
});
|
||||
for await (const _ of session.run([userText("用户问题")])) {
|
||||
void _; // Drains the output stream; once run finishes, the material is settled
|
||||
}
|
||||
const res = await session.generateTitle();
|
||||
expect(res.title).toBe("标题 B");
|
||||
// Material = the first Task's user text + model text (thinking does not count), matching
|
||||
// buildTitlePrompt's shape.
|
||||
expect(seen[0]).toBe(buildTitlePrompt("用户问题", "答案正文"));
|
||||
|
||||
// No request is sent when no material has been collected (run was never called).
|
||||
const idle = new Session({
|
||||
meta: META,
|
||||
llm: fakeLLM([]),
|
||||
environment: fakeEnvironment,
|
||||
createBareLLM: () => fakeLLM([assistantText("不应产生")]),
|
||||
});
|
||||
expect(await idle.generateTitle()).toEqual({ title: null, usage: null });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user