@arcreflex/agent-transcripts 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -1
- package/README.md +71 -52
- package/package.json +1 -1
- package/scripts/infer-cc-types.prose +87 -0
- package/src/adapters/claude-code.ts +291 -53
- package/src/adapters/index.ts +0 -6
- package/src/archive.ts +267 -0
- package/src/cli.ts +96 -63
- package/src/convert.ts +19 -86
- package/src/parse.ts +0 -3
- package/src/render-html.ts +38 -195
- package/src/render-index.ts +15 -178
- package/src/render.ts +25 -88
- package/src/serve.ts +124 -215
- package/src/title.ts +24 -102
- package/src/types.ts +5 -0
- package/src/utils/naming.ts +8 -13
- package/src/utils/summary.ts +1 -4
- package/src/utils/text.ts +5 -0
- package/src/utils/theme.ts +152 -0
- package/src/utils/tree.ts +85 -1
- package/src/watch.ts +178 -0
- package/test/archive.test.ts +264 -0
- package/test/fixtures/claude/branching.input.jsonl +6 -0
- package/test/fixtures/claude/branching.output.md +25 -0
- package/test/naming.test.ts +98 -0
- package/test/summary.test.ts +144 -0
- package/test/tree.test.ts +217 -0
- package/tsconfig.json +1 -1
- package/src/cache.ts +0 -129
- package/src/sync.ts +0 -294
- package/src/utils/provenance.ts +0 -212
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import {
|
|
6
|
+
archiveSession,
|
|
7
|
+
archiveAll,
|
|
8
|
+
loadEntry,
|
|
9
|
+
saveEntry,
|
|
10
|
+
listEntries,
|
|
11
|
+
isFresh,
|
|
12
|
+
computeContentHash,
|
|
13
|
+
type ArchiveEntry,
|
|
14
|
+
} from "../src/archive.ts";
|
|
15
|
+
import type { Adapter } from "../src/types.ts";
|
|
16
|
+
import { claudeCodeAdapter } from "../src/adapters/claude-code.ts";
|
|
17
|
+
|
|
18
|
+
const fixturesDir = join(import.meta.dir, "fixtures/claude");
|
|
19
|
+
|
|
20
|
+
let tmpDir: string;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
tmpDir = await mkdtemp(join(tmpdir(), "archive-test-"));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("computeContentHash", () => {
|
|
31
|
+
it("returns consistent hash", () => {
|
|
32
|
+
const h1 = computeContentHash("hello");
|
|
33
|
+
const h2 = computeContentHash("hello");
|
|
34
|
+
expect(h1).toBe(h2);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns different hash for different content", () => {
|
|
38
|
+
const h1 = computeContentHash("hello");
|
|
39
|
+
const h2 = computeContentHash("world");
|
|
40
|
+
expect(h1).not.toBe(h2);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("saveEntry / loadEntry", () => {
|
|
45
|
+
it("round-trips an entry", async () => {
|
|
46
|
+
const entry: ArchiveEntry = {
|
|
47
|
+
sessionId: "test-session",
|
|
48
|
+
sourcePath: "/tmp/test.jsonl",
|
|
49
|
+
sourceHash: "abc123",
|
|
50
|
+
adapterName: "claude-code",
|
|
51
|
+
adapterVersion: "claude-code:1",
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
archivedAt: "2025-01-01T00:00:00Z",
|
|
54
|
+
transcripts: [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
await saveEntry(tmpDir, entry);
|
|
58
|
+
const loaded = await loadEntry(tmpDir, "test-session");
|
|
59
|
+
expect(loaded).toEqual(entry);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns undefined for missing entry", async () => {
|
|
63
|
+
const loaded = await loadEntry(tmpDir, "nonexistent");
|
|
64
|
+
expect(loaded).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("isFresh", () => {
|
|
69
|
+
const adapter: Adapter = {
|
|
70
|
+
name: "test",
|
|
71
|
+
version: "test:1",
|
|
72
|
+
discover: async () => [],
|
|
73
|
+
parse: () => [],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const entry: ArchiveEntry = {
|
|
77
|
+
sessionId: "s",
|
|
78
|
+
sourcePath: "/tmp/s.jsonl",
|
|
79
|
+
sourceHash: "hash1",
|
|
80
|
+
adapterName: "test",
|
|
81
|
+
adapterVersion: "test:1",
|
|
82
|
+
schemaVersion: 1,
|
|
83
|
+
archivedAt: "2025-01-01T00:00:00Z",
|
|
84
|
+
transcripts: [],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
it("returns true when all match", () => {
|
|
88
|
+
expect(isFresh(entry, "hash1", adapter)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("returns false when sourceHash differs", () => {
|
|
92
|
+
expect(isFresh(entry, "hash2", adapter)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns false when adapterVersion differs", () => {
|
|
96
|
+
const v2 = { ...adapter, version: "test:2" };
|
|
97
|
+
expect(isFresh(entry, "hash1", v2)).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("returns false when schemaVersion differs", () => {
|
|
101
|
+
const oldSchema = { ...entry, schemaVersion: 0 };
|
|
102
|
+
expect(isFresh(oldSchema, "hash1", adapter)).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("archiveSession", () => {
|
|
107
|
+
it("archives a fixture file", async () => {
|
|
108
|
+
const fixturePath = join(fixturesDir, "basic-conversation.input.jsonl");
|
|
109
|
+
const session = {
|
|
110
|
+
path: fixturePath,
|
|
111
|
+
relativePath: "basic-conversation.input.jsonl",
|
|
112
|
+
mtime: Date.now(),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const { entry, updated } = await archiveSession(
|
|
116
|
+
tmpDir,
|
|
117
|
+
session,
|
|
118
|
+
claudeCodeAdapter,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(updated).toBe(true);
|
|
122
|
+
expect(entry.sessionId).toBe("basic-conversation.input");
|
|
123
|
+
expect(entry.adapterName).toBe("claude-code");
|
|
124
|
+
expect(entry.adapterVersion).toBe("claude-code:1");
|
|
125
|
+
expect(entry.schemaVersion).toBe(1);
|
|
126
|
+
expect(entry.transcripts.length).toBeGreaterThan(0);
|
|
127
|
+
expect(entry.sourceHash).toBeTruthy();
|
|
128
|
+
|
|
129
|
+
// Verify it was persisted
|
|
130
|
+
const loaded = await loadEntry(tmpDir, entry.sessionId);
|
|
131
|
+
expect(loaded).toEqual(entry);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("returns updated: false for unchanged source", async () => {
|
|
135
|
+
const fixturePath = join(fixturesDir, "basic-conversation.input.jsonl");
|
|
136
|
+
const session = {
|
|
137
|
+
path: fixturePath,
|
|
138
|
+
relativePath: "basic-conversation.input.jsonl",
|
|
139
|
+
mtime: Date.now(),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const first = await archiveSession(tmpDir, session, claudeCodeAdapter);
|
|
143
|
+
expect(first.updated).toBe(true);
|
|
144
|
+
|
|
145
|
+
const second = await archiveSession(tmpDir, session, claudeCodeAdapter);
|
|
146
|
+
expect(second.updated).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("updates title when harness summary changes on fresh entry", async () => {
|
|
150
|
+
const fixturePath = join(fixturesDir, "basic-conversation.input.jsonl");
|
|
151
|
+
|
|
152
|
+
// Archive without summary
|
|
153
|
+
const first = await archiveSession(
|
|
154
|
+
tmpDir,
|
|
155
|
+
{ path: fixturePath, relativePath: "x", mtime: Date.now() },
|
|
156
|
+
claudeCodeAdapter,
|
|
157
|
+
);
|
|
158
|
+
expect(first.entry.title).toBeUndefined();
|
|
159
|
+
|
|
160
|
+
// Re-archive with a new harness summary (same content hash)
|
|
161
|
+
const second = await archiveSession(
|
|
162
|
+
tmpDir,
|
|
163
|
+
{
|
|
164
|
+
path: fixturePath,
|
|
165
|
+
relativePath: "x",
|
|
166
|
+
mtime: Date.now(),
|
|
167
|
+
summary: "New harness title",
|
|
168
|
+
},
|
|
169
|
+
claudeCodeAdapter,
|
|
170
|
+
);
|
|
171
|
+
expect(second.updated).toBe(true);
|
|
172
|
+
expect(second.entry.title).toBe("New harness title");
|
|
173
|
+
|
|
174
|
+
// Verify persisted
|
|
175
|
+
const loaded = await loadEntry(tmpDir, first.entry.sessionId);
|
|
176
|
+
expect(loaded?.title).toBe("New harness title");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("preserves existing title when re-archiving", async () => {
|
|
180
|
+
const fixturePath = join(fixturesDir, "basic-conversation.input.jsonl");
|
|
181
|
+
const session = {
|
|
182
|
+
path: fixturePath,
|
|
183
|
+
relativePath: "basic-conversation.input.jsonl",
|
|
184
|
+
mtime: Date.now(),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Archive once
|
|
188
|
+
const { entry } = await archiveSession(tmpDir, session, claudeCodeAdapter);
|
|
189
|
+
|
|
190
|
+
// Manually set a title and save
|
|
191
|
+
entry.title = "My Custom Title";
|
|
192
|
+
await saveEntry(tmpDir, entry);
|
|
193
|
+
|
|
194
|
+
// Force re-archive by bumping adapter version
|
|
195
|
+
const bumpedAdapter = { ...claudeCodeAdapter, version: "claude-code:2" };
|
|
196
|
+
const { entry: reArchived, updated } = await archiveSession(
|
|
197
|
+
tmpDir,
|
|
198
|
+
session,
|
|
199
|
+
bumpedAdapter,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(updated).toBe(true);
|
|
203
|
+
expect(reArchived.title).toBe("My Custom Title");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("listEntries", () => {
|
|
208
|
+
it("returns all entries", async () => {
|
|
209
|
+
const fixtures = [
|
|
210
|
+
"basic-conversation.input.jsonl",
|
|
211
|
+
"with-tools.input.jsonl",
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
for (const f of fixtures) {
|
|
215
|
+
await archiveSession(
|
|
216
|
+
tmpDir,
|
|
217
|
+
{
|
|
218
|
+
path: join(fixturesDir, f),
|
|
219
|
+
relativePath: f,
|
|
220
|
+
mtime: Date.now(),
|
|
221
|
+
},
|
|
222
|
+
claudeCodeAdapter,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const entries = await listEntries(tmpDir);
|
|
227
|
+
expect(entries.length).toBe(2);
|
|
228
|
+
const ids = entries.map((e) => e.sessionId).sort();
|
|
229
|
+
expect(ids).toEqual(
|
|
230
|
+
["basic-conversation.input", "with-tools.input"].sort(),
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("returns empty array for nonexistent dir", async () => {
|
|
235
|
+
const entries = await listEntries("/tmp/nonexistent-archive-dir-xyz");
|
|
236
|
+
expect(entries).toEqual([]);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("archiveAll", () => {
|
|
241
|
+
it("discovers and archives sessions", async () => {
|
|
242
|
+
// Create a source dir with a fixture and a sessions-index.json
|
|
243
|
+
const sourceDir = await mkdtemp(join(tmpdir(), "archive-source-"));
|
|
244
|
+
const fixturePath = join(fixturesDir, "basic-conversation.input.jsonl");
|
|
245
|
+
const content = await Bun.file(fixturePath).text();
|
|
246
|
+
await Bun.write(join(sourceDir, "test-session.jsonl"), content);
|
|
247
|
+
|
|
248
|
+
const result = await archiveAll(tmpDir, sourceDir, [claudeCodeAdapter], {
|
|
249
|
+
quiet: true,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(result.updated.length).toBe(1);
|
|
253
|
+
expect(result.errors.length).toBe(0);
|
|
254
|
+
|
|
255
|
+
// Second run should be all current
|
|
256
|
+
const result2 = await archiveAll(tmpDir, sourceDir, [claudeCodeAdapter], {
|
|
257
|
+
quiet: true,
|
|
258
|
+
});
|
|
259
|
+
expect(result2.updated.length).toBe(0);
|
|
260
|
+
expect(result2.current.length).toBe(1);
|
|
261
|
+
|
|
262
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{"type": "user", "uuid": "msg-1", "message": {"role": "user", "content": "What language should I use for this project?"}, "timestamp": "2024-01-15T10:00:00Z"}
|
|
2
|
+
{"type": "assistant", "uuid": "msg-2", "parentUuid": "msg-1", "message": {"role": "assistant", "content": [{"type": "text", "text": "It depends on your requirements. What kind of project is it?"}]}, "timestamp": "2024-01-15T10:00:05Z"}
|
|
3
|
+
{"type": "user", "uuid": "msg-3a", "parentUuid": "msg-2", "message": {"role": "user", "content": "A CLI tool."}, "timestamp": "2024-01-15T10:01:00Z"}
|
|
4
|
+
{"type": "assistant", "uuid": "msg-4a", "parentUuid": "msg-3a", "message": {"role": "assistant", "content": [{"type": "text", "text": "For a CLI tool, I'd suggest Go or Rust."}]}, "timestamp": "2024-01-15T10:01:05Z"}
|
|
5
|
+
{"type": "user", "uuid": "msg-3b", "parentUuid": "msg-2", "message": {"role": "user", "content": "A web application."}, "timestamp": "2024-01-15T10:02:00Z"}
|
|
6
|
+
{"type": "assistant", "uuid": "msg-4b", "parentUuid": "msg-3b", "message": {"role": "assistant", "content": [{"type": "text", "text": "For a web app, TypeScript with a framework like Next.js is a great choice."}]}, "timestamp": "2024-01-15T10:02:05Z"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Transcript
|
|
2
|
+
|
|
3
|
+
**Source**: `test/fixtures/claude/branching.input.jsonl`
|
|
4
|
+
**Adapter**: claude-code
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## User
|
|
9
|
+
|
|
10
|
+
What language should I use for this project?
|
|
11
|
+
|
|
12
|
+
## Assistant
|
|
13
|
+
|
|
14
|
+
It depends on your requirements. What kind of project is it?
|
|
15
|
+
|
|
16
|
+
> **Other branches**:
|
|
17
|
+
> - `msg-3a` "A CLI tool."
|
|
18
|
+
|
|
19
|
+
## User
|
|
20
|
+
|
|
21
|
+
A web application.
|
|
22
|
+
|
|
23
|
+
## Assistant
|
|
24
|
+
|
|
25
|
+
For a web app, TypeScript with a framework like Next.js is a great choice.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
extractSessionId,
|
|
4
|
+
formatDateTimePrefix,
|
|
5
|
+
generateOutputName,
|
|
6
|
+
} from "../src/utils/naming.ts";
|
|
7
|
+
import type { Transcript } from "../src/types.ts";
|
|
8
|
+
|
|
9
|
+
describe("extractSessionId", () => {
|
|
10
|
+
it("strips .jsonl extension", () => {
|
|
11
|
+
expect(extractSessionId("/path/to/abc-123.jsonl")).toBe("abc-123");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("strips .json extension", () => {
|
|
15
|
+
expect(extractSessionId("/path/to/session.json")).toBe("session");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns 'stdin' for <stdin>", () => {
|
|
19
|
+
expect(extractSessionId("<stdin>")).toBe("stdin");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("handles bare filename", () => {
|
|
23
|
+
expect(extractSessionId("my-session.jsonl")).toBe("my-session");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("handles filename with no extension", () => {
|
|
27
|
+
expect(extractSessionId("/path/to/no-extension")).toBe("no-extension");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("handles deeply nested paths", () => {
|
|
31
|
+
expect(
|
|
32
|
+
extractSessionId(
|
|
33
|
+
"/home/user/.claude/projects/foo/sessions/uuid-here.jsonl",
|
|
34
|
+
),
|
|
35
|
+
).toBe("uuid-here");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("formatDateTimePrefix", () => {
|
|
40
|
+
it("formats a valid ISO timestamp", () => {
|
|
41
|
+
// Use a fixed timestamp and check the format pattern
|
|
42
|
+
const result = formatDateTimePrefix("2024-01-15T14:23:00Z");
|
|
43
|
+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}-\d{4}$/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("falls back to current date for invalid timestamp", () => {
|
|
47
|
+
const result = formatDateTimePrefix("not-a-date");
|
|
48
|
+
// Should still produce a valid format
|
|
49
|
+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}-\d{4}$/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("falls back to current date for empty string", () => {
|
|
53
|
+
const result = formatDateTimePrefix("");
|
|
54
|
+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}-\d{4}$/);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("generateOutputName", () => {
|
|
59
|
+
it("combines datetime prefix and session id", () => {
|
|
60
|
+
const transcript: Transcript = {
|
|
61
|
+
source: { file: "test.jsonl", adapter: "claude-code" },
|
|
62
|
+
metadata: {
|
|
63
|
+
warnings: [],
|
|
64
|
+
messageCount: 1,
|
|
65
|
+
startTime: "2024-01-15T14:23:00Z",
|
|
66
|
+
endTime: "2024-01-15T14:30:00Z",
|
|
67
|
+
},
|
|
68
|
+
messages: [
|
|
69
|
+
{
|
|
70
|
+
type: "user",
|
|
71
|
+
sourceRef: "msg-1",
|
|
72
|
+
timestamp: "2024-01-15T14:23:00Z",
|
|
73
|
+
content: "hello",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const result = generateOutputName(transcript, "/path/to/my-session.jsonl");
|
|
79
|
+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}-\d{4}-my-session$/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("handles transcript with no messages", () => {
|
|
83
|
+
const transcript: Transcript = {
|
|
84
|
+
source: { file: "test.jsonl", adapter: "claude-code" },
|
|
85
|
+
metadata: {
|
|
86
|
+
warnings: [],
|
|
87
|
+
messageCount: 0,
|
|
88
|
+
startTime: "2024-01-01T00:00:00Z",
|
|
89
|
+
endTime: "2024-01-01T00:00:00Z",
|
|
90
|
+
},
|
|
91
|
+
messages: [],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const result = generateOutputName(transcript, "empty.jsonl");
|
|
95
|
+
// No messages → empty timestamp → falls back to current date
|
|
96
|
+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}-\d{4}-empty$/);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { extractToolSummary } from "../src/utils/summary.ts";
|
|
3
|
+
|
|
4
|
+
describe("extractToolSummary", () => {
|
|
5
|
+
describe("Read", () => {
|
|
6
|
+
it("returns file_path", () => {
|
|
7
|
+
expect(extractToolSummary("Read", { file_path: "/foo/bar.ts" })).toBe(
|
|
8
|
+
"/foo/bar.ts",
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("returns empty for missing file_path", () => {
|
|
13
|
+
expect(extractToolSummary("Read", {})).toBe("");
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("Write", () => {
|
|
18
|
+
it("returns file_path", () => {
|
|
19
|
+
expect(extractToolSummary("Write", { file_path: "/out.txt" })).toBe(
|
|
20
|
+
"/out.txt",
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("Edit", () => {
|
|
26
|
+
it("returns file_path", () => {
|
|
27
|
+
expect(extractToolSummary("Edit", { file_path: "/src/index.ts" })).toBe(
|
|
28
|
+
"/src/index.ts",
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("Bash", () => {
|
|
34
|
+
it("prefers description over command", () => {
|
|
35
|
+
expect(
|
|
36
|
+
extractToolSummary("Bash", {
|
|
37
|
+
description: "Run tests",
|
|
38
|
+
command: "bun test",
|
|
39
|
+
}),
|
|
40
|
+
).toBe("Run tests");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("falls back to truncated command", () => {
|
|
44
|
+
const longCmd = "a".repeat(100);
|
|
45
|
+
const result = extractToolSummary("Bash", { command: longCmd });
|
|
46
|
+
expect(result.length).toBe(60);
|
|
47
|
+
expect(result.endsWith("...")).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns empty for no description or command", () => {
|
|
51
|
+
expect(extractToolSummary("Bash", {})).toBe("");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("Grep", () => {
|
|
56
|
+
it("returns pattern with path", () => {
|
|
57
|
+
expect(
|
|
58
|
+
extractToolSummary("Grep", { pattern: "TODO", path: "/src" }),
|
|
59
|
+
).toBe("TODO in /src");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns pattern alone without path", () => {
|
|
63
|
+
expect(extractToolSummary("Grep", { pattern: "TODO" })).toBe("TODO");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("truncates long patterns", () => {
|
|
67
|
+
const long = "x".repeat(100);
|
|
68
|
+
const result = extractToolSummary("Grep", { pattern: long });
|
|
69
|
+
expect(result.length).toBe(80);
|
|
70
|
+
expect(result.endsWith("...")).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("Glob", () => {
|
|
75
|
+
it("returns pattern", () => {
|
|
76
|
+
expect(extractToolSummary("Glob", { pattern: "**/*.ts" })).toBe(
|
|
77
|
+
"**/*.ts",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("WebFetch", () => {
|
|
83
|
+
it("returns url", () => {
|
|
84
|
+
expect(
|
|
85
|
+
extractToolSummary("WebFetch", { url: "https://example.com" }),
|
|
86
|
+
).toBe("https://example.com");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("WebSearch", () => {
|
|
91
|
+
it("returns query", () => {
|
|
92
|
+
expect(
|
|
93
|
+
extractToolSummary("WebSearch", { query: "bun test runner" }),
|
|
94
|
+
).toBe("bun test runner");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("Task", () => {
|
|
99
|
+
it("prefers description", () => {
|
|
100
|
+
expect(
|
|
101
|
+
extractToolSummary("Task", { description: "Research this topic" }),
|
|
102
|
+
).toBe("Research this topic");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("falls back to prompt", () => {
|
|
106
|
+
expect(extractToolSummary("Task", { prompt: "Do the thing" })).toBe(
|
|
107
|
+
"Do the thing",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("truncates long descriptions", () => {
|
|
112
|
+
const long = "y".repeat(100);
|
|
113
|
+
const result = extractToolSummary("Task", { description: long });
|
|
114
|
+
expect(result.length).toBe(60);
|
|
115
|
+
expect(result.endsWith("...")).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("TodoWrite", () => {
|
|
120
|
+
it("returns fixed string", () => {
|
|
121
|
+
expect(extractToolSummary("TodoWrite", {})).toBe("update todos");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("AskUserQuestion", () => {
|
|
126
|
+
it("returns fixed string", () => {
|
|
127
|
+
expect(extractToolSummary("AskUserQuestion", {})).toBe("ask user");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("NotebookEdit", () => {
|
|
132
|
+
it("returns notebook_path", () => {
|
|
133
|
+
expect(
|
|
134
|
+
extractToolSummary("NotebookEdit", { notebook_path: "/nb.ipynb" }),
|
|
135
|
+
).toBe("/nb.ipynb");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("unknown tool", () => {
|
|
140
|
+
it("returns empty string", () => {
|
|
141
|
+
expect(extractToolSummary("SomeUnknownTool", { x: 1 })).toBe("");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|