mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-19 06:14:19 +01:00
379 lines
12 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|
|
});
|