test: 新增测试代码文件

This commit is contained in:
claude-code-best
2026-04-02 14:44:56 +08:00
parent 9c3803d16b
commit 006ad97fbb
32 changed files with 1102 additions and 68 deletions

View File

@@ -0,0 +1,100 @@
import { describe, expect, test } from "bun:test";
// Test Commander.js option parsing independently from main.tsx initialization.
// main.tsx has heavy bootstrap dependencies; we test the CLI argument parsing
// patterns it uses to ensure correct behavior.
const { Command } = require("/Users/konghayao/code/ai/claude-code/node_modules/.old_modules-13e6b62a502cda34/commander/index.js");
function createTestProgram(): Command {
const program = new Command();
program
.name("claude-code")
.description("CLI test")
.exitOverride() // prevent process.exit during tests
.option("-p, --print", "pipe mode")
.option("--resume", "resume session")
.option("-v, --verbose", "verbose output")
.option("--model <model>", "model to use")
.option("--system-prompt <prompt>", "system prompt")
.option("--allowedTools <tools...>", "allowed tools")
.option("--max-turns <n>", "max conversation turns", parseInt)
.version("1.0.0", "-V, --version", "display version");
return program;
}
describe("CLI arguments: option parsing", () => {
test("no flags returns empty opts", () => {
const program = createTestProgram();
program.parse(["node", "test"]);
expect(program.opts()).toEqual({});
});
test("-p sets print flag", () => {
const program = createTestProgram();
program.parse(["node", "test", "-p"]);
expect(program.opts().print).toBe(true);
});
test("--print is equivalent to -p", () => {
const program = createTestProgram();
program.parse(["node", "test", "--print"]);
expect(program.opts().print).toBe(true);
});
test("--resume sets resume flag", () => {
const program = createTestProgram();
program.parse(["node", "test", "--resume"]);
expect(program.opts().resume).toBe(true);
});
test("-v sets verbose flag", () => {
const program = createTestProgram();
program.parse(["node", "test", "-v"]);
expect(program.opts().verbose).toBe(true);
});
test("--model captures string value", () => {
const program = createTestProgram();
program.parse(["node", "test", "--model", "claude-opus-4-6"]);
expect(program.opts().model).toBe("claude-opus-4-6");
});
test("--system-prompt captures string value", () => {
const program = createTestProgram();
program.parse(["node", "test", "--system-prompt", "Be concise"]);
expect(program.opts().systemPrompt).toBe("Be concise");
});
test("--max-turns parses integer value", () => {
const program = createTestProgram();
program.parse(["node", "test", "--max-turns", "10"]);
expect(program.opts().maxTurns).toBe(10);
});
test("multiple flags can be combined", () => {
const program = createTestProgram();
program.parse(["node", "test", "-p", "-v", "--model", "opus"]);
expect(program.opts().print).toBe(true);
expect(program.opts().verbose).toBe(true);
expect(program.opts().model).toBe("opus");
});
test("--version throws Commander.CommandError with exit code 0", () => {
const program = createTestProgram();
let error: any;
try {
program.parse(["node", "test", "--version"]);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.code).toBe("commander.version");
expect(error.exitCode).toBe(0);
});
test("unknown flags throw CommanderError", () => {
const program = createTestProgram();
expect(() => program.parse(["node", "test", "--nonexistent"])).toThrow();
});
});

View File

@@ -0,0 +1,101 @@
import { describe, expect, test } from "bun:test";
import { stripHtmlComments, isMemoryFilePath, getLargeMemoryFiles } from "../../src/utils/claudemd";
import { buildEffectiveSystemPrompt } from "../../src/utils/systemPrompt";
import { createTempDir, cleanupTempDir, writeTempFile } from "../mocks/file-system";
// ─── CLAUDE.md Integration with System Prompt ─────────────────────────
describe("Context build: CLAUDE.md + system prompt integration", () => {
test("buildEffectiveSystemPrompt passes through default prompt", () => {
const result = buildEffectiveSystemPrompt({
defaultSystemPrompt: "You are Claude.",
});
// Result is an array of strings (may be split differently)
const joined = Array.from(result).join("");
expect(joined).toBe("You are Claude.");
});
test("buildEffectiveSystemPrompt handles empty prompts", () => {
const result = buildEffectiveSystemPrompt({
defaultSystemPrompt: "",
});
const joined = Array.from(result).join("");
expect(joined).toBe("");
});
test("buildEffectiveSystemPrompt with overrideSystemPrompt replaces everything", () => {
const result = buildEffectiveSystemPrompt({
defaultSystemPrompt: "Default",
overrideSystemPrompt: "Override",
});
const joined = Array.from(result).join("");
expect(joined).toBe("Override");
});
test("buildEffectiveSystemPrompt with customSystemPrompt replaces default", () => {
const result = buildEffectiveSystemPrompt({
defaultSystemPrompt: "Default",
customSystemPrompt: "Custom",
});
const joined = Array.from(result).join("");
expect(joined).toBe("Custom");
});
test("buildEffectiveSystemPrompt with appendSystemPrompt includes both", () => {
const result = buildEffectiveSystemPrompt({
defaultSystemPrompt: "Main prompt",
appendSystemPrompt: "Appended",
});
const joined = Array.from(result).join("");
expect(joined).toContain("Main prompt");
expect(joined).toContain("Appended");
// Appended should come after main
expect(joined.indexOf("Main prompt")).toBeLessThan(
joined.indexOf("Appended")
);
});
});
// ─── CLAUDE.md Discovery with Real File System ───────────────────────
describe("Context build: CLAUDE.md file system integration", () => {
let tempDir: string;
test("strips HTML comments from CLAUDE.md content", () => {
const input = "<!-- this is a comment -->Actual content";
const { content, stripped } = stripHtmlComments(input);
expect(content).toBe("Actual content");
expect(stripped).toBe(true);
});
test("preserves code blocks when stripping HTML comments", () => {
const input = "```\n<!-- not a real comment -->\n```\nReal text";
const { content } = stripHtmlComments(input);
expect(content).toContain("<!-- not a real comment -->");
expect(content).toContain("Real text");
});
test("isMemoryFilePath correctly identifies CLAUDE.md paths", () => {
expect(isMemoryFilePath("/project/CLAUDE.md")).toBe(true);
expect(isMemoryFilePath("/project/CLAUDE.local.md")).toBe(true);
expect(isMemoryFilePath("/project/.claude/rules/file.md")).toBe(true);
expect(isMemoryFilePath("/project/README.md")).toBe(false);
expect(isMemoryFilePath("/project/src/index.ts")).toBe(false);
});
});
// ─── Large Memory File Filtering ──────────────────────────────────────
describe("Context build: large memory file filtering", () => {
test("getLargeMemoryFiles returns empty for empty input", () => {
expect(getLargeMemoryFiles([])).toEqual([]);
});
test("getLargeMemoryFiles returns empty when all files are small", () => {
const files = [
{ path: "/a/CLAUDE.md", content: "small" },
{ path: "/b/CLAUDE.md", content: "also small" },
];
expect(getLargeMemoryFiles(files)).toEqual([]);
});
});

View File

@@ -0,0 +1,65 @@
import { describe, expect, test } from "bun:test";
import {
createUserMessage,
createAssistantMessage,
normalizeMessages,
extractTag,
} from "../../src/utils/messages";
// ─── Message Structure ────────────────────────────────────────────────
describe("Message pipeline: message structure", () => {
test("createUserMessage returns a Message with type 'user'", () => {
const msg = createUserMessage("hello");
expect(msg.type).toBe("user");
expect(msg.message.role).toBe("user");
expect(msg.uuid).toBeTruthy();
expect(msg.timestamp).toBeTruthy();
});
test("createAssistantMessage returns a Message with type 'assistant'", () => {
const msg = createAssistantMessage("response");
expect(msg.type).toBe("assistant");
expect(msg.message.role).toBe("assistant");
expect(msg.uuid).toBeTruthy();
});
test("user and assistant messages have different UUIDs", () => {
const user = createUserMessage("hello");
const assistant = createAssistantMessage("response");
expect(user.uuid).not.toBe(assistant.uuid);
});
});
// ─── Tag Extraction ───────────────────────────────────────────────────
describe("Message pipeline: tag extraction", () => {
test("extractTag returns null for non-matching tag", () => {
expect(extractTag("no tags here", "think")).toBeNull();
});
test("extractTag returns null for empty string", () => {
expect(extractTag("", "think")).toBeNull();
});
test("extractTag requires tagName parameter", () => {
// Calling without tagName throws
expect(() => (extractTag as any)("hello")).toThrow();
});
});
// ─── Normalization ────────────────────────────────────────────────────
describe("Message pipeline: normalization", () => {
test("normalizeMessages returns an array", () => {
const msg = createUserMessage("hello");
const result = normalizeMessages([msg]);
expect(Array.isArray(result)).toBe(true);
});
test("normalizeMessages preserves at least one message for simple input", () => {
const msg = createUserMessage("hello");
const result = normalizeMessages([msg]);
expect(result.length).toBeGreaterThanOrEqual(1);
});
});

View File

@@ -0,0 +1,141 @@
import { describe, expect, test } from "bun:test";
import { getAllBaseTools, parseToolPreset, getTools } from "../../src/tools.ts";
import {
findToolByName,
getEmptyToolPermissionContext,
buildTool,
} from "../../src/Tool.ts";
// ─── Tool Registration & Discovery ──────────────────────────────────────
describe("Tool chain: registration and discovery", () => {
test("getAllBaseTools returns a non-empty array of tools", () => {
const tools = getAllBaseTools();
expect(tools.length).toBeGreaterThan(0);
});
test("all base tools have required fields", () => {
const tools = getAllBaseTools();
for (const tool of tools) {
expect(tool.name).toBeTruthy();
expect(tool.description).toBeTruthy();
expect(tool.inputSchema).toBeDefined();
expect(typeof tool.call).toBe("function");
}
});
test("findToolByName finds core tools from the full list", () => {
const tools = getAllBaseTools();
const bash = findToolByName(tools, "Bash");
expect(bash).toBeDefined();
expect(bash!.name).toBe("Bash");
const read = findToolByName(tools, "Read");
expect(read).toBeDefined();
expect(read!.name).toBe("Read");
const edit = findToolByName(tools, "Edit");
expect(edit).toBeDefined();
expect(edit!.name).toBe("Edit");
});
test("findToolByName returns undefined for non-existent tool", () => {
const tools = getAllBaseTools();
expect(findToolByName(tools, "NonExistentTool")).toBeUndefined();
});
test("findToolByName is case-sensitive (exact match only)", () => {
const tools = getAllBaseTools();
expect(findToolByName(tools, "Bash")).toBeDefined();
expect(findToolByName(tools, "bash")).toBeUndefined();
});
test("findToolByName resolves via toolMatchesName", () => {
const tools = getAllBaseTools();
const agent = findToolByName(tools, "Agent");
expect(agent).toBeDefined();
// Verify it can also find by checking name directly
expect(tools.some(t => t.name === "Agent")).toBe(true);
});
test("tool names are unique across the base tool list", () => {
const tools = getAllBaseTools();
const names = tools.map(t => t.name);
expect(new Set(names).size).toBe(names.length);
});
});
// ─── Tool Presets ──────────────────────────────────────────────────────
describe("Tool chain: presets", () => {
test('parseToolPreset("default") returns "default" string', () => {
// parseToolPreset returns a preset name string, not a tool array
expect(parseToolPreset("default")).toBe("default");
});
test("parseToolPreset returns null for unknown preset", () => {
expect(parseToolPreset("nonexistent")).toBeNull();
});
test("parseToolPreset is case-insensitive", () => {
expect(parseToolPreset("DEFAULT")).toBe("default");
});
});
// ─── getTools (with permission context) ────────────────────────────────
describe("Tool chain: getTools with context", () => {
test("getTools returns tools (subset of base tools)", () => {
const allTools = getAllBaseTools();
const ctx = getEmptyToolPermissionContext();
const tools = getTools(ctx);
expect(tools.length).toBeGreaterThan(0);
expect(tools.length).toBeLessThanOrEqual(allTools.length);
});
test("getTools results all have name and call function", () => {
const ctx = getEmptyToolPermissionContext();
const tools = getTools(ctx);
for (const tool of tools) {
expect(tool.name).toBeTruthy();
expect(typeof tool.call).toBe("function");
}
});
});
// ─── buildTool + findToolByName end-to-end ─────────────────────────────
describe("Tool chain: buildTool + findToolByName", () => {
test("a built tool can be found in a custom list", () => {
const customTool = buildTool({
name: "TestTool",
description: "A test tool",
inputSchema: {
type: "object" as const,
properties: { input: { type: "string" } },
required: ["input"],
},
call: async () => ({ output: "test" }),
});
const found = findToolByName([customTool], "TestTool");
expect(found).toBe(customTool);
});
test("built tool defaults are correctly applied", () => {
const tool = buildTool({
name: "MinimalTool",
description: "Minimal",
inputSchema: {
type: "object" as const,
properties: {},
},
call: async () => ({}),
});
expect(tool.isEnabled()).toBe(true);
expect(tool.isConcurrencySafe()).toBe(false);
expect(tool.isReadOnly()).toBe(false);
expect(tool.isDestructive()).toBe(false);
});
});

View File

@@ -0,0 +1,34 @@
export const mockStreamResponse = {
type: "message_start" as const,
message: {
id: "msg_mock_001",
type: "message" as const,
role: "assistant",
content: [],
model: "claude-sonnet-4-20250514",
stop_reason: null,
stop_sequence: null,
usage: { input_tokens: 100, output_tokens: 0 },
},
};
export const mockTextBlock = {
type: "content_block_start" as const,
index: 0,
content_block: { type: "text" as const, text: "Mock response" },
};
export const mockToolUseBlock = {
type: "content_block_start" as const,
index: 1,
content_block: {
type: "tool_use" as const,
id: "toolu_mock_001",
name: "Read",
input: { file_path: "/tmp/test.txt" },
},
};
export const mockEndEvent = {
type: "message_stop" as const,
};

View File

@@ -0,0 +1,32 @@
import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
export async function createTempDir(
prefix = "claude-test-",
): Promise<string> {
return mkdtemp(join(tmpdir(), prefix));
}
export async function cleanupTempDir(dir: string): Promise<void> {
await rm(dir, { recursive: true, force: true });
}
export async function writeTempFile(
dir: string,
name: string,
content: string,
): Promise<string> {
const path = join(dir, name);
await writeFile(path, content, "utf-8");
return path;
}
export async function createTempSubdir(
dir: string,
name: string,
): Promise<string> {
const path = join(dir, name);
await mkdir(path, { recursive: true });
return path;
}

View File

@@ -0,0 +1,3 @@
# Project Instructions
This is a sample CLAUDE.md file for testing purposes.

View File

@@ -0,0 +1,33 @@
{
"userMessage": {
"role": "user",
"content": "Hello, Claude"
},
"assistantMessage": {
"role": "assistant",
"content": [
{ "type": "text", "text": "Hello! How can I help?" }
]
},
"toolUseMessage": {
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_test_001",
"name": "Read",
"input": { "file_path": "/tmp/test.txt" }
}
]
},
"toolResultMessage": {
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_test_001",
"content": "file contents here"
}
]
}
}