Files
claude-code-viewer/src/server/core/claude-code/functions/parseJsonl.test.ts
2025-10-17 20:47:25 +09:00

379 lines
12 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { ErrorJsonl } from "../../types";
import { parseJsonl } from "./parseJsonl";
describe("parseJsonl", () => {
describe("正常系: 有効なJSONLをパースできる", () => {
it("単一のUserエントリをパースできる", () => {
const jsonl = JSON.stringify({
type: "user",
uuid: "550e8400-e29b-41d4-a716-446655440000",
timestamp: "2024-01-01T00:00:00.000Z",
message: { role: "user", content: "Hello" },
isSidechain: false,
userType: "external",
cwd: "/test",
sessionId: "session-1",
version: "1.0.0",
parentUuid: null,
});
const result = parseJsonl(jsonl);
expect(result).toHaveLength(1);
expect(result[0]).toHaveProperty("type", "user");
const entry = result[0];
if (entry && entry.type === "user") {
expect(entry.message.content).toBe("Hello");
}
});
it("単一のSummaryエントリをパースできる", () => {
const jsonl = JSON.stringify({
type: "summary",
summary: "This is a summary",
leafUuid: "550e8400-e29b-41d4-a716-446655440003",
});
const result = parseJsonl(jsonl);
expect(result).toHaveLength(1);
expect(result[0]).toHaveProperty("type", "summary");
const entry = result[0];
if (entry && entry.type === "summary") {
expect(entry.summary).toBe("This is a summary");
}
});
it("複数のエントリをパースできる", () => {
const jsonl = [
JSON.stringify({
type: "user",
uuid: "550e8400-e29b-41d4-a716-446655440000",
timestamp: "2024-01-01T00:00:00.000Z",
message: { role: "user", content: "Hello" },
isSidechain: false,
userType: "external",
cwd: "/test",
sessionId: "session-1",
version: "1.0.0",
parentUuid: null,
}),
JSON.stringify({
type: "summary",
summary: "Test summary",
leafUuid: "550e8400-e29b-41d4-a716-446655440002",
}),
].join("\n");
const result = parseJsonl(jsonl);
expect(result).toHaveLength(2);
expect(result[0]).toHaveProperty("type", "user");
expect(result[1]).toHaveProperty("type", "summary");
});
});
describe("エラー系: 不正なJSON行をErrorJsonlとして返す", () => {
it("無効なJSONを渡すとエラーを投げる", () => {
const jsonl = "invalid json";
// parseJsonl の実装は JSON.parse をそのまま呼び出すため、
// 無効な JSON は例外を投げます
expect(() => parseJsonl(jsonl)).toThrow();
});
it("スキーマに合わないオブジェクトをErrorJsonlとして返す", () => {
const jsonl = JSON.stringify({
type: "unknown",
someField: "value",
});
const result = parseJsonl(jsonl);
expect(result).toHaveLength(1);
const errorEntry = result[0] as ErrorJsonl;
expect(errorEntry.type).toBe("x-error");
expect(errorEntry.lineNumber).toBe(1);
});
it("必須フィールドが欠けているエントリをErrorJsonlとして返す", () => {
const jsonl = JSON.stringify({
type: "user",
uuid: "550e8400-e29b-41d4-a716-446655440000",
// timestamp, message などの必須フィールドが欠けている
});
const result = parseJsonl(jsonl);
expect(result).toHaveLength(1);
const errorEntry = result[0] as ErrorJsonl;
expect(errorEntry.type).toBe("x-error");
expect(errorEntry.lineNumber).toBe(1);
});
it("正常なエントリとエラーエントリを混在して返す", () => {
const jsonl = [
JSON.stringify({
type: "user",
uuid: "550e8400-e29b-41d4-a716-446655440000",
timestamp: "2024-01-01T00:00:00.000Z",
message: { role: "user", content: "Hello" },
isSidechain: false,
userType: "external",
cwd: "/test",
sessionId: "session-1",
version: "1.0.0",
parentUuid: null,
}),
JSON.stringify({ type: "invalid-schema" }),
JSON.stringify({
type: "summary",
summary: "Summary text",
leafUuid: "550e8400-e29b-41d4-a716-446655440001",
}),
].join("\n");
const result = parseJsonl(jsonl);
expect(result).toHaveLength(3);
expect(result[0]).toHaveProperty("type", "user");
expect(result[1]).toHaveProperty("type", "x-error");
expect(result[2]).toHaveProperty("type", "summary");
const errorEntry = result[1] as ErrorJsonl;
expect(errorEntry.lineNumber).toBe(2);
});
});
describe("エッジケース: 空行、トリム、複数エントリ", () => {
it("空文字列を渡すと空配列を返す", () => {
const result = parseJsonl("");
expect(result).toEqual([]);
});
it("空行のみを渡すと空配列を返す", () => {
const result = parseJsonl("\n\n\n");
expect(result).toEqual([]);
});
it("前後の空白をトリムする", () => {
const jsonl = `
${JSON.stringify({
type: "user",
uuid: "550e8400-e29b-41d4-a716-446655440000",
timestamp: "2024-01-01T00:00:00.000Z",
message: { role: "user", content: "Hello" },
isSidechain: false,
userType: "external",
cwd: "/test",
sessionId: "session-1",
version: "1.0.0",
parentUuid: null,
})}
`;
const result = parseJsonl(jsonl);
expect(result).toHaveLength(1);
expect(result[0]).toHaveProperty("type", "user");
});
it("行間の空行を除外する", () => {
const jsonl = [
JSON.stringify({
type: "user",
uuid: "550e8400-e29b-41d4-a716-446655440000",
timestamp: "2024-01-01T00:00:00.000Z",
message: { role: "user", content: "Hello" },
isSidechain: false,
userType: "external",
cwd: "/test",
sessionId: "session-1",
version: "1.0.0",
parentUuid: null,
}),
"",
"",
JSON.stringify({
type: "summary",
summary: "Summary text",
leafUuid: "550e8400-e29b-41d4-a716-446655440001",
}),
].join("\n");
const result = parseJsonl(jsonl);
expect(result).toHaveLength(2);
expect(result[0]).toHaveProperty("type", "user");
expect(result[1]).toHaveProperty("type", "summary");
});
it("空白のみの行を除外する", () => {
const jsonl = [
JSON.stringify({
type: "user",
uuid: "550e8400-e29b-41d4-a716-446655440000",
timestamp: "2024-01-01T00:00:00.000Z",
message: { role: "user", content: "Hello" },
isSidechain: false,
userType: "external",
cwd: "/test",
sessionId: "session-1",
version: "1.0.0",
parentUuid: null,
}),
" ",
"\t",
JSON.stringify({
type: "summary",
summary: "Summary text",
leafUuid: "550e8400-e29b-41d4-a716-446655440001",
}),
].join("\n");
const result = parseJsonl(jsonl);
expect(result).toHaveLength(2);
expect(result[0]).toHaveProperty("type", "user");
expect(result[1]).toHaveProperty("type", "summary");
});
it("多数のエントリを含むJSONLをパースできる", () => {
const entries = Array.from({ length: 100 }, (_, i) => {
return JSON.stringify({
type: "user",
uuid: `550e8400-e29b-41d4-a716-${String(i).padStart(12, "0")}`,
timestamp: new Date(Date.UTC(2024, 0, 1, 0, 0, i)).toISOString(),
message: {
role: "user",
content: `Message ${i}`,
},
isSidechain: false,
userType: "external",
cwd: "/test",
sessionId: "session-1",
version: "1.0.0",
parentUuid:
i > 0
? `550e8400-e29b-41d4-a716-${String(i - 1).padStart(12, "0")}`
: null,
});
});
const jsonl = entries.join("\n");
const result = parseJsonl(jsonl);
expect(result).toHaveLength(100);
expect(result.every((entry) => entry.type === "user")).toBe(true);
});
});
describe("行番号の正確性", () => {
it("スキーマ検証エラー時の行番号が正確に記録される", () => {
const jsonl = [
JSON.stringify({
type: "user",
uuid: "550e8400-e29b-41d4-a716-446655440000",
timestamp: "2024-01-01T00:00:00.000Z",
message: { role: "user", content: "Line 1" },
isSidechain: false,
userType: "external",
cwd: "/test",
sessionId: "session-1",
version: "1.0.0",
parentUuid: null,
}),
JSON.stringify({ type: "invalid", data: "schema error" }),
JSON.stringify({
type: "user",
uuid: "550e8400-e29b-41d4-a716-446655440001",
timestamp: "2024-01-01T00:00:01.000Z",
message: { role: "user", content: "Line 3" },
isSidechain: false,
userType: "external",
cwd: "/test",
sessionId: "session-1",
version: "1.0.0",
parentUuid: null,
}),
JSON.stringify({ type: "another-invalid" }),
].join("\n");
const result = parseJsonl(jsonl);
expect(result).toHaveLength(4);
expect((result[1] as ErrorJsonl).lineNumber).toBe(2);
expect((result[1] as ErrorJsonl).type).toBe("x-error");
expect((result[3] as ErrorJsonl).lineNumber).toBe(4);
expect((result[3] as ErrorJsonl).type).toBe("x-error");
});
it("空行フィルタ後の行番号が正確に記録される", () => {
const jsonl = ["", "", JSON.stringify({ type: "invalid-schema" })].join(
"\n",
);
const result = parseJsonl(jsonl);
expect(result).toHaveLength(1);
// 空行がフィルタされた後のインデックスは0だが、lineNumberは1として記録される
expect((result[0] as ErrorJsonl).lineNumber).toBe(1);
});
});
describe("ConversationSchemaのバリエーション", () => {
it("オプショナルフィールドを含むUserエントリをパースできる", () => {
const jsonl = JSON.stringify({
type: "user",
uuid: "550e8400-e29b-41d4-a716-446655440000",
timestamp: "2024-01-01T00:00:00.000Z",
message: { role: "user", content: "Hello" },
isSidechain: true,
userType: "external",
cwd: "/test",
sessionId: "session-1",
version: "1.0.0",
parentUuid: "550e8400-e29b-41d4-a716-446655440099",
gitBranch: "main",
isMeta: false,
});
const result = parseJsonl(jsonl);
expect(result).toHaveLength(1);
const entry = result[0];
if (entry && entry.type === "user") {
expect(entry.isSidechain).toBe(true);
expect(entry.parentUuid).toBe("550e8400-e29b-41d4-a716-446655440099");
expect(entry.gitBranch).toBe("main");
}
});
it("nullableフィールドがnullのエントリをパースできる", () => {
const jsonl = JSON.stringify({
type: "user",
uuid: "550e8400-e29b-41d4-a716-446655440000",
timestamp: "2024-01-01T00:00:00.000Z",
message: { role: "user", content: "Hello" },
isSidechain: false,
userType: "external",
cwd: "/test",
sessionId: "session-1",
version: "1.0.0",
parentUuid: null,
});
const result = parseJsonl(jsonl);
expect(result).toHaveLength(1);
const entry = result[0];
if (entry && entry.type === "user") {
expect(entry.parentUuid).toBeNull();
}
});
});
});