返回 CodeWhale
transcript.test.ts
根目录 / extensions / vscode / src / test / transcript.test.ts
1 import assert from "node:assert/strict";
2 import * as path from "node:path";
3 import { describe, it } from "node:test";
4 import { extractFilePath, isInsideRoot, projectItem, statusForEvent } from "../transcript";
5 import type { ItemRecord } from "../api";
6
7 const agent = (over: Partial<ItemRecord> = {}): ItemRecord => ({
8 id: "i1",
9 kind: "agent_message",
10 summary: "",
11 ...over,
12 });
13
14 describe("projectItem", () => {
15 it("renders the full detail body, not the truncated summary", () => {
16 const stub = "Here is the plan" + "…".repeat(10);
17 const view = projectItem(
18 agent({ status: "completed", summary: stub, detail: "Here is the plan, in full, with steps." }),
19 undefined,
20 );
21 assert.equal(view.summary, "Here is the plan, in full, with steps.");
22 assert.ok(view.html?.includes("in full"));
23 assert.ok(!view.html?.includes("…"));
24 });
25
26 it("falls back to summary when detail is absent on the wire", () => {
27 const view = projectItem(agent({ status: "completed", summary: "short reply" }), undefined);
28 assert.equal(view.summary, "short reply");
29 assert.ok(view.html?.includes("short reply"));
30 });
31
32 it("keeps streamed text when a completion payload carries neither field", () => {
33 const streaming = projectItem(agent({ status: "in_progress" }), {
34 id: "i1",
35 kind: "agent_message",
36 summary: "partial",
37 streamText: "partial",
38 rev: 1,
39 });
40 assert.equal(streaming.streamText, "partial");
41 const done = projectItem(agent({ status: "completed" }), streaming);
42 assert.equal(done.summary, "partial");
43 assert.equal(done.rev, streaming.rev + 1);
44 });
45
46 it("carries a parsed file path onto non-agent items", () => {
47 const view = projectItem(
48 {
49 id: "t1",
50 kind: "file_change",
51 summary: "Edited a file",
52 metadata: { tool_input: JSON.stringify({ file_path: "src/chat.ts" }) },
53 },
54 undefined,
55 );
56 assert.equal(view.filePath, "src/chat.ts");
57 });
58
59 it("merges detail and metadata forward from the existing view", () => {
60 const first = projectItem(
61 { id: "t1", kind: "tool_call", summary: "run", detail: "line one", metadata: { path: "a.ts" } },
62 undefined,
63 );
64 const second = projectItem({ id: "t1", kind: "tool_call", summary: "run" }, first, "item.completed");
65 assert.equal(second.detail, "line one");
66 assert.equal(second.filePath, "a.ts");
67 });
68 });
69
70 describe("extractFilePath", () => {
71 it("reads the path out of a JSON tool_input string", () => {
72 assert.equal(
73 extractFilePath({ tool_input: '{"file_path":"/repo/src/main.rs","content":"x"}' }),
74 "/repo/src/main.rs",
75 );
76 });
77
78 it("reads an already-parsed tool_input object", () => {
79 assert.equal(extractFilePath({ tool_input: { path: "docs/README.md" } }), "docs/README.md");
80 });
81
82 it("prefers a direct metadata path when present", () => {
83 assert.equal(extractFilePath({ path: "direct.ts", tool_input: '{"file_path":"other.ts"}' }), "direct.ts");
84 });
85
86 it("survives malformed or missing tool_input", () => {
87 assert.equal(extractFilePath(undefined), undefined);
88 assert.equal(extractFilePath({}), undefined);
89 assert.equal(extractFilePath({ tool_input: "not json {" }), undefined);
90 assert.equal(extractFilePath({ tool_input: "[]" }), undefined);
91 assert.equal(extractFilePath({ tool_input: '{"command":"ls"}' }), undefined);
92 assert.equal(extractFilePath({ tool_input: '{"file_path":42}' }), undefined);
93 });
94
95 it("refuses paths carrying control characters", () => {
96 assert.equal(extractFilePath({ path: "src/\u0000etc/passwd" }), undefined);
97 assert.equal(extractFilePath({ tool_input: '{"file_path":"a\\u001bb.ts"}' }), undefined);
98 });
99 });
100
101 describe("isInsideRoot", () => {
102 const root = path.resolve("/repo/project");
103
104 it("accepts a workspace-relative path", () => {
105 assert.equal(isInsideRoot(root, "src/chat.ts"), true);
106 assert.equal(isInsideRoot(root, path.join(root, "src", "chat.ts")), true);
107 });
108
109 it("refuses traversal and outside-workspace paths", () => {
110 assert.equal(isInsideRoot(root, "../secrets.env"), false);
111 assert.equal(isInsideRoot(root, "src/../../secrets.env"), false);
112 assert.equal(isInsideRoot(root, path.resolve("/etc/passwd")), false);
113 assert.equal(isInsideRoot(root, root), false);
114 assert.equal(isInsideRoot("", "src/chat.ts"), false);
115 });
116 });
117
118 describe("statusForEvent", () => {
119 it("maps terminal events", () => {
120 assert.equal(statusForEvent("item.completed"), "completed");
121 assert.equal(statusForEvent("item.failed"), "failed");
122 assert.equal(statusForEvent("item.interrupted"), "interrupted");
123 assert.equal(statusForEvent("item.canceled"), "interrupted");
124 assert.equal(statusForEvent("item.started"), "in_progress");
125 });
126 });
127
127 lines TYPESCRIPT