@aaroncql/pim-agent 0.0.1 → 0.2.0

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.
Files changed (84) hide show
  1. package/README.md +94 -66
  2. package/bin/pim.ts +55 -3
  3. package/package.json +20 -5
  4. package/src/extensions/_init/index.ts +3 -2
  5. package/src/extensions/apply-patch/coordinator.ts +49 -0
  6. package/src/extensions/apply-patch/executor.ts +566 -0
  7. package/src/extensions/apply-patch/index.ts +74 -0
  8. package/src/extensions/apply-patch/matcher.ts +66 -0
  9. package/src/extensions/apply-patch/model.ts +34 -0
  10. package/src/extensions/apply-patch/parser.ts +381 -0
  11. package/src/extensions/apply-patch/render.ts +261 -0
  12. package/src/extensions/apply-patch/schema.ts +43 -0
  13. package/src/extensions/apply-patch/types.ts +30 -0
  14. package/src/extensions/bash/index.ts +3 -3
  15. package/src/extensions/edit/index.ts +2 -1
  16. package/src/extensions/glob/index.ts +3 -1
  17. package/src/extensions/glob/schema.ts +2 -1
  18. package/src/extensions/grep/index.ts +3 -1
  19. package/src/extensions/grep/render.ts +18 -4
  20. package/src/extensions/grep/schema.ts +1 -1
  21. package/src/extensions/read/index.ts +36 -9
  22. package/src/extensions/read/render.ts +31 -3
  23. package/src/extensions/subagent/index.ts +4 -1
  24. package/src/extensions/todo/index.ts +4 -3
  25. package/src/extensions/web-search/index.ts +2 -1
  26. package/src/extensions/write/index.ts +2 -1
  27. package/src/shared/PatchSummary.ts +82 -0
  28. package/src/telegram/Renderer.ts +190 -4
  29. package/src/extensions/bash/capture.test.ts +0 -126
  30. package/src/extensions/bash/format.test.ts +0 -240
  31. package/src/extensions/bash/run.test.ts +0 -262
  32. package/src/extensions/command-picker/ranker.test.ts +0 -46
  33. package/src/extensions/edit/edit.test.ts +0 -285
  34. package/src/extensions/file-picker/catalog.test.ts +0 -263
  35. package/src/extensions/file-picker/index.test.ts +0 -168
  36. package/src/extensions/file-picker/ranker.test.ts +0 -94
  37. package/src/extensions/footer/git.test.ts +0 -76
  38. package/src/extensions/footer/index.test.ts +0 -161
  39. package/src/extensions/footer/segments.test.ts +0 -164
  40. package/src/extensions/glob/glob.test.ts +0 -171
  41. package/src/extensions/glob/index.test.ts +0 -68
  42. package/src/extensions/glob/render.test.ts +0 -126
  43. package/src/extensions/grep/grep.test.ts +0 -387
  44. package/src/extensions/grep/index.test.ts +0 -68
  45. package/src/extensions/grep/render.test.ts +0 -269
  46. package/src/extensions/read/read.test.ts +0 -177
  47. package/src/extensions/read/render.test.ts +0 -61
  48. package/src/extensions/subagent/index.test.ts +0 -44
  49. package/src/extensions/subagent/render.test.ts +0 -292
  50. package/src/extensions/subagent/subagent.test.ts +0 -315
  51. package/src/extensions/system-prompt/prompt.test.ts +0 -64
  52. package/src/extensions/todo/index.test.ts +0 -244
  53. package/src/extensions/todo/render.test.ts +0 -180
  54. package/src/extensions/todo/todo.test.ts +0 -222
  55. package/src/extensions/tps/index.test.ts +0 -254
  56. package/src/extensions/web-fetch/WebViewMarkdownSnapshot.test.ts +0 -119
  57. package/src/extensions/web-fetch/fetch.test.ts +0 -244
  58. package/src/extensions/web-fetch/render.test.ts +0 -56
  59. package/src/extensions/web-search/ExaMcpClient.test.ts +0 -143
  60. package/src/extensions/web-search/render.test.ts +0 -21
  61. package/src/extensions/web-search/search.test.ts +0 -53
  62. package/src/extensions/working-indicator/index.test.ts +0 -21
  63. package/src/extensions/write/render.test.ts +0 -64
  64. package/src/extensions/write/write.test.ts +0 -108
  65. package/src/shared/DiffLines.test.ts +0 -193
  66. package/src/shared/DiffRenderer.test.ts +0 -206
  67. package/src/shared/EditMatcher.test.ts +0 -123
  68. package/src/shared/FileScanner.test.ts +0 -158
  69. package/src/shared/FuzzyMatcher.test.ts +0 -114
  70. package/src/shared/GitignoreFilter.test.ts +0 -64
  71. package/src/shared/Lines.test.ts +0 -25
  72. package/src/shared/McpClient.test.ts +0 -235
  73. package/src/shared/OutputBudget.test.ts +0 -99
  74. package/src/shared/Paths.test.ts +0 -51
  75. package/src/shared/PimSettings.test.ts +0 -90
  76. package/src/shared/Renderer.test.ts +0 -190
  77. package/src/shared/SpillCache.test.ts +0 -94
  78. package/src/shared/Tools.test.ts +0 -392
  79. package/src/telegram/Config.test.ts +0 -275
  80. package/src/telegram/Markdown.test.ts +0 -143
  81. package/src/telegram/Renderer.test.ts +0 -216
  82. package/src/telegram/SessionRegistry.test.ts +0 -89
  83. package/src/telegram/TaskScheduler.test.ts +0 -278
  84. package/src/telegram/TaskTool.test.ts +0 -179
@@ -1,254 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import { PimSettings } from "../../shared/PimSettings";
4
- import registerTps from "./index";
5
-
6
- type Handler = (event: unknown, ctx: unknown) => unknown;
7
-
8
- type MockPi = {
9
- readonly api: ExtensionAPI;
10
- readonly handlers: Map<string, Handler[]>;
11
- };
12
-
13
- const originalNow = Date.now;
14
- const originalGet = PimSettings.get;
15
-
16
- let now = 0;
17
-
18
- function createPi(): MockPi {
19
- const handlers = new Map<string, Handler[]>();
20
- const api = {
21
- on(event: string, handler: Handler): void {
22
- const existing = handlers.get(event) ?? [];
23
- existing.push(handler);
24
- handlers.set(event, existing);
25
- },
26
- registerCommand(): void {},
27
- } as unknown as ExtensionAPI;
28
-
29
- return { api, handlers };
30
- }
31
-
32
- async function emit(
33
- pi: MockPi,
34
- event: string,
35
- payload: unknown,
36
- ctx: unknown
37
- ): Promise<void> {
38
- for (const handler of pi.handlers.get(event) ?? []) {
39
- await handler(payload, ctx);
40
- }
41
- }
42
-
43
- const assistantMessage = {
44
- role: "assistant",
45
- content: [{ type: "text", text: "hello" }],
46
- api: "openai",
47
- provider: "openai",
48
- model: "test-model",
49
- usage: {
50
- input: 1000,
51
- output: 50,
52
- cacheRead: 5000,
53
- cacheWrite: 100,
54
- totalTokens: 6150,
55
- cost: {
56
- input: 0,
57
- output: 0,
58
- cacheRead: 0,
59
- cacheWrite: 0,
60
- total: 0,
61
- },
62
- },
63
- stopReason: "stop",
64
- timestamp: 1000,
65
- } as const;
66
-
67
- describe("tps extension", () => {
68
- beforeEach(() => {
69
- now = 0;
70
- Date.now = () => now;
71
- Object.defineProperty(PimSettings, "get", {
72
- value: async () => ({ enabled: true }),
73
- });
74
- });
75
-
76
- afterEach(() => {
77
- Date.now = originalNow;
78
- Object.defineProperty(PimSettings, "get", { value: originalGet });
79
- });
80
-
81
- test("reports metrics when stream updates and final message are different objects", async () => {
82
- const pi = createPi();
83
- const notifications: string[] = [];
84
- const ctx = {
85
- hasUI: true,
86
- ui: {
87
- notify(message: string): void {
88
- notifications.push(message);
89
- },
90
- },
91
- };
92
-
93
- registerTps(pi.api);
94
-
95
- await emit(pi, "agent_start", { type: "agent_start" }, ctx);
96
-
97
- now = 1000;
98
- await emit(
99
- pi,
100
- "before_provider_request",
101
- { type: "before_provider_request", payload: {} },
102
- ctx
103
- );
104
-
105
- now = 1150;
106
- await emit(
107
- pi,
108
- "message_update",
109
- {
110
- type: "message_update",
111
- message: { ...assistantMessage },
112
- assistantMessageEvent: {
113
- type: "text_start",
114
- contentIndex: 0,
115
- partial: assistantMessage,
116
- },
117
- },
118
- ctx
119
- );
120
-
121
- now = 1200;
122
- await emit(
123
- pi,
124
- "message_update",
125
- {
126
- type: "message_update",
127
- message: { ...assistantMessage },
128
- assistantMessageEvent: {
129
- type: "text_delta",
130
- contentIndex: 0,
131
- delta: "h",
132
- partial: assistantMessage,
133
- },
134
- },
135
- ctx
136
- );
137
-
138
- now = 2200;
139
- await emit(
140
- pi,
141
- "message_end",
142
- { type: "message_end", message: assistantMessage },
143
- ctx
144
- );
145
- await emit(
146
- pi,
147
- "agent_end",
148
- { type: "agent_end", messages: [assistantMessage] },
149
- ctx
150
- );
151
-
152
- expect(notifications).toEqual([
153
- "Decode: 50.0 tps | Prefill: 5500.0 tps | Cache read: 5,000 | TTFT: 0.20s",
154
- ]);
155
- });
156
-
157
- test("reports once at the end of a multi-turn agent cycle", async () => {
158
- const pi = createPi();
159
- const notifications: string[] = [];
160
- const ctx = {
161
- hasUI: true,
162
- ui: {
163
- notify(message: string): void {
164
- notifications.push(message);
165
- },
166
- },
167
- };
168
-
169
- registerTps(pi.api);
170
-
171
- await emit(pi, "agent_start", { type: "agent_start" }, ctx);
172
-
173
- now = 1000;
174
- await emit(
175
- pi,
176
- "before_provider_request",
177
- { type: "before_provider_request", payload: {} },
178
- ctx
179
- );
180
- now = 1200;
181
- await emit(
182
- pi,
183
- "message_update",
184
- {
185
- type: "message_update",
186
- message: { ...assistantMessage },
187
- assistantMessageEvent: {
188
- type: "thinking_delta",
189
- contentIndex: 0,
190
- delta: "thinking",
191
- partial: assistantMessage,
192
- },
193
- },
194
- ctx
195
- );
196
- now = 2200;
197
- await emit(
198
- pi,
199
- "message_end",
200
- { type: "message_end", message: assistantMessage },
201
- ctx
202
- );
203
- await emit(
204
- pi,
205
- "turn_end",
206
- { type: "turn_end", message: assistantMessage, toolResults: [] },
207
- ctx
208
- );
209
-
210
- now = 3000;
211
- await emit(
212
- pi,
213
- "before_provider_request",
214
- { type: "before_provider_request", payload: {} },
215
- ctx
216
- );
217
- now = 3200;
218
- await emit(
219
- pi,
220
- "message_update",
221
- {
222
- type: "message_update",
223
- message: { ...assistantMessage },
224
- assistantMessageEvent: {
225
- type: "text_delta",
226
- contentIndex: 0,
227
- delta: "h",
228
- partial: assistantMessage,
229
- },
230
- },
231
- ctx
232
- );
233
- now = 4200;
234
- await emit(
235
- pi,
236
- "message_end",
237
- { type: "message_end", message: assistantMessage },
238
- ctx
239
- );
240
-
241
- expect(notifications).toEqual([]);
242
-
243
- await emit(
244
- pi,
245
- "agent_end",
246
- { type: "agent_end", messages: [assistantMessage, assistantMessage] },
247
- ctx
248
- );
249
-
250
- expect(notifications).toEqual([
251
- "Decode: 50.0 tps | Prefill: 5500.0 tps | Cache read: 10,000 | TTFT: 0.20s",
252
- ]);
253
- });
254
- });
@@ -1,119 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import {
3
- createMarkdownSnapshotScript,
4
- renderMarkdownSnapshotTree,
5
- type MarkdownSnapshotElementNode,
6
- type MarkdownSnapshotNode,
7
- } from "./WebViewMarkdownSnapshot";
8
-
9
- const text = (value: string): MarkdownSnapshotNode => ({
10
- type: "text",
11
- text: value,
12
- });
13
-
14
- const element = (
15
- tagName: string,
16
- children: readonly MarkdownSnapshotNode[] = [],
17
- options: Partial<MarkdownSnapshotElementNode> = {}
18
- ): MarkdownSnapshotElementNode => ({
19
- type: "element",
20
- tagName,
21
- children,
22
- ...options,
23
- });
24
-
25
- describe("createMarkdownSnapshotScript", () => {
26
- test("builds a parseable browser snapshot script from stringified functions", () => {
27
- const script = createMarkdownSnapshotScript();
28
-
29
- expect(script).toStartWith("(function captureMarkdownSnapshot");
30
- expect(script).toContain("function renderMarkdownSnapshotTree");
31
- expect(script).toContain("function serializeNode");
32
- expect(script).not.toContain("const renderMarkdownSnapshotTree =");
33
- expect(() => new Function(script)).not.toThrow();
34
- });
35
- });
36
-
37
- describe("renderMarkdownSnapshotTree", () => {
38
- test("renders headings, inline formatting, code, and absolute links", () => {
39
- const root = element("body", [
40
- element("h1", [text("Hello")]),
41
- element("p", [
42
- text("Read "),
43
- element("strong", [text("docs")]),
44
- text(" at "),
45
- element("a", [text("guide")], {
46
- attributes: [{ name: "href", value: "/guide" }],
47
- }),
48
- text(" with "),
49
- element("code", [text("x")], { textContent: "x" }),
50
- ]),
51
- ]);
52
-
53
- expect(renderMarkdownSnapshotTree(root, "https://example.test/base/")).toBe(
54
- [
55
- "# Hello",
56
- "",
57
- "Read **docs** at [guide](https://example.test/guide) with `x`",
58
- ].join("\n")
59
- );
60
- });
61
-
62
- test("renders lists, blockquotes, tables, and fenced code blocks", () => {
63
- const root = element("body", [
64
- element("ul", [
65
- element("li", [text("one")]),
66
- element("li", [text("two")]),
67
- ]),
68
- element("blockquote", [element("p", [text("quoted")])]),
69
- element("table", [
70
- element("tbody", [
71
- element("tr", [
72
- element("th", [text("Name")]),
73
- element("th", [text("Value")]),
74
- ]),
75
- element("tr", [
76
- element("td", [text("A|B")]),
77
- element("td", [text("2")]),
78
- ]),
79
- ]),
80
- ]),
81
- element("pre", [element("code", [text("const x = 1;")])], {
82
- textContent: "const x = 1;",
83
- }),
84
- ]);
85
-
86
- expect(renderMarkdownSnapshotTree(root, "https://example.test/")).toBe(
87
- [
88
- "- one",
89
- "- two",
90
- "",
91
- "> quoted",
92
- "",
93
- "| Name | Value |",
94
- "| --- | --- |",
95
- "| A\\|B | 2 |",
96
- "",
97
- "```",
98
- "const x = 1;",
99
- "```",
100
- ].join("\n")
101
- );
102
- });
103
-
104
- test("skips hidden content and unsafe links", () => {
105
- const root = element("body", [
106
- element("p", [
107
- text("shown"),
108
- element("span", [text("hidden")], { ariaHidden: "true" }),
109
- element("a", [text("unsafe")], {
110
- attributes: [{ name: "href", value: "javascript:alert(1)" }],
111
- }),
112
- ]),
113
- ]);
114
-
115
- expect(renderMarkdownSnapshotTree(root, "https://example.test/")).toBe(
116
- "shown unsafe"
117
- );
118
- });
119
- });
@@ -1,244 +0,0 @@
1
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
- import { mkdtemp, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { SpillCache } from "../../shared/SpillCache";
6
- import {
7
- executeFetch,
8
- formatOutcome,
9
- truncationFooter,
10
- validatePublicUrl,
11
- } from "./fetch";
12
- import { WEB_FETCH_INLINE_BYTES } from "./schema";
13
- import type { JinaReaderClient } from "./JinaReaderClient";
14
- import type { WebViewFetchClient } from "./WebViewFetchClient";
15
-
16
- let previousPimHomeDir: string | undefined;
17
- let testPimHomeDir: string | undefined;
18
-
19
- beforeAll(async () => {
20
- previousPimHomeDir = process.env.PIM_HOME_DIR;
21
- testPimHomeDir = await mkdtemp(join(tmpdir(), "pim-fetch-home-"));
22
- process.env.PIM_HOME_DIR = testPimHomeDir;
23
- });
24
-
25
- afterAll(async () => {
26
- if (previousPimHomeDir === undefined) {
27
- delete process.env.PIM_HOME_DIR;
28
- } else {
29
- process.env.PIM_HOME_DIR = previousPimHomeDir;
30
- }
31
- if (testPimHomeDir) {
32
- await rm(testPimHomeDir, { recursive: true, force: true });
33
- }
34
- });
35
-
36
- describe("validatePublicUrl", () => {
37
- test("accepts http and https", () => {
38
- expect(validatePublicUrl("https://example.com/path")).toBe(
39
- "https://example.com/path"
40
- );
41
- expect(validatePublicUrl("http://example.com")).toBe("http://example.com/");
42
- });
43
-
44
- test("rejects non-http schemes", () => {
45
- expect(() => validatePublicUrl("ftp://example.com")).toThrow(/http/);
46
- expect(() => validatePublicUrl("file:///etc/passwd")).toThrow(/http/);
47
- });
48
-
49
- test("rejects malformed URLs", () => {
50
- expect(() => validatePublicUrl("not a url")).toThrow(/valid/);
51
- });
52
-
53
- test("rejects embedded credentials", () => {
54
- expect(() => validatePublicUrl("https://user:pw@example.com")).toThrow(
55
- /credentials/
56
- );
57
- });
58
-
59
- test("rejects localhost and .local", () => {
60
- expect(() => validatePublicUrl("http://localhost/")).toThrow(/public/);
61
- expect(() => validatePublicUrl("http://printer.local/")).toThrow(/public/);
62
- });
63
-
64
- test("rejects RFC1918 IPv4", () => {
65
- expect(() => validatePublicUrl("http://10.0.0.1/")).toThrow(/public/);
66
- expect(() => validatePublicUrl("http://192.168.1.1/")).toThrow(/public/);
67
- expect(() => validatePublicUrl("http://172.16.0.1/")).toThrow(/public/);
68
- expect(() => validatePublicUrl("http://127.0.0.1/")).toThrow(/public/);
69
- expect(() => validatePublicUrl("http://169.254.0.1/")).toThrow(/public/);
70
- });
71
-
72
- test("rejects IPv6 loopback and link-local", () => {
73
- expect(() => validatePublicUrl("http://[::1]/")).toThrow(/public/);
74
- expect(() => validatePublicUrl("http://[fe80::1]/")).toThrow(/public/);
75
- expect(() => validatePublicUrl("http://[fc00::1]/")).toThrow(/public/);
76
- });
77
-
78
- test("accepts public IPs", () => {
79
- expect(validatePublicUrl("http://8.8.8.8/")).toBe("http://8.8.8.8/");
80
- });
81
- });
82
-
83
- describe("executeFetch", () => {
84
- test("returns remote markdown when available", async () => {
85
- const jina = {
86
- fetchUrl: async () => ({
87
- title: "Remote",
88
- url: "https://example.test/remote",
89
- content: "remote markdown",
90
- }),
91
- } as unknown as JinaReaderClient;
92
- const webView = {
93
- fetchMarkdown: async () => {
94
- throw new Error("Rendered markdown should not be attempted.");
95
- },
96
- } as unknown as WebViewFetchClient;
97
-
98
- const outcome = await executeFetch({
99
- jina,
100
- webView,
101
- url: "https://example.test/",
102
- format: "markdown",
103
- });
104
-
105
- expect(outcome.format).toBe("markdown");
106
- expect(outcome.text).toContain("remote markdown");
107
- });
108
-
109
- test("falls back to rendered markdown when remote markdown fails", async () => {
110
- const jina = {
111
- fetchUrl: async () => {
112
- throw new Error("Request timed out after 20000ms.");
113
- },
114
- } as unknown as JinaReaderClient;
115
- const webView = {
116
- fetchMarkdown: async () => ({
117
- title: "Rendered",
118
- url: "https://example.test/rendered",
119
- content: "# rendered markdown",
120
- }),
121
- fetchHtml: async () => {
122
- throw new Error("HTML should not be attempted for markdown mode.");
123
- },
124
- } as unknown as WebViewFetchClient;
125
-
126
- const outcome = await executeFetch({
127
- jina,
128
- webView,
129
- url: "https://example.test/",
130
- format: "markdown",
131
- });
132
-
133
- expect(outcome.format).toBe("markdown");
134
- expect(outcome.text).toContain("# rendered markdown");
135
- });
136
-
137
- test("throws rendered markdown error when fallback fails", async () => {
138
- const jina = {
139
- fetchUrl: async () => {
140
- throw new Error("remote unavailable");
141
- },
142
- } as unknown as JinaReaderClient;
143
- const webView = {
144
- fetchMarkdown: async () => {
145
- throw new Error("Request failed: unavailable");
146
- },
147
- } as unknown as WebViewFetchClient;
148
-
149
- await expect(
150
- executeFetch({
151
- jina,
152
- webView,
153
- url: "https://example.test/",
154
- format: "markdown",
155
- })
156
- ).rejects.toThrow("Failed to fetch: Request failed: unavailable");
157
- });
158
-
159
- test("returns raw rendered HTML for HTML mode", async () => {
160
- const jina = {
161
- fetchUrl: async () => {
162
- throw new Error("Remote markdown should not be attempted.");
163
- },
164
- } as unknown as JinaReaderClient;
165
- const webView = {
166
- fetchHtml: async () => ({
167
- title: "HTML",
168
- url: "https://example.test/html",
169
- content: "<html><body>Hello</body></html>",
170
- }),
171
- } as unknown as WebViewFetchClient;
172
-
173
- const outcome = await executeFetch({
174
- jina,
175
- webView,
176
- url: "https://example.test/",
177
- format: "html",
178
- });
179
-
180
- expect(outcome.format).toBe("html");
181
- expect(outcome.text).toContain("<html><body>Hello</body></html>");
182
- });
183
- });
184
-
185
- describe("formatOutcome", () => {
186
- const page = {
187
- title: "Example",
188
- url: "https://example.test/page",
189
- content: "hello world",
190
- };
191
-
192
- test("formats untruncated page without spilling", async () => {
193
- const outcome = await formatOutcome(page, "markdown");
194
- expect(outcome.text).toBe(
195
- [
196
- "title: Example",
197
- "url: https://example.test/page",
198
- "format: markdown",
199
- "content:",
200
- "hello world",
201
- ].join("\n")
202
- );
203
- expect(outcome.truncated).toBe(false);
204
- expect(outcome.returnedBytes).toBe(11);
205
- expect(outcome.totalBytes).toBe(11);
206
- expect(outcome.format).toBe("markdown");
207
- expect(outcome.path).toBeNull();
208
- });
209
-
210
- test("spills the full body and points the footer at the resume line over the inline budget", async () => {
211
- // 1 KiB newline-terminated lines: the 32 KiB head holds exactly 32 lines,
212
- // so the footer should resume reading at line 33.
213
- const line = `${"x".repeat(1023)}\n`;
214
- const content = line.repeat(40);
215
- const long = { ...page, content };
216
- const outcome = await formatOutcome(long, "html");
217
- expect(outcome.truncated).toBe(true);
218
- expect(outcome.returnedBytes).toBe(WEB_FETCH_INLINE_BYTES);
219
- expect(outcome.totalBytes).toBe(content.length);
220
- expect(outcome.path).toBeTruthy();
221
- expect(outcome.path!.startsWith(join(SpillCache.dir(), "fetch-"))).toBe(
222
- true
223
- );
224
- expect(outcome.path!.endsWith(".html")).toBe(true);
225
- expect(outcome.text).toContain(
226
- `use read with path=${outcome.path} and start=33 for the rest.]`
227
- );
228
- expect(await Bun.file(outcome.path!).text()).toBe(content);
229
- });
230
- });
231
-
232
- describe("truncationFooter", () => {
233
- test("points at the spill file with a resume line when one was written", () => {
234
- expect(truncationFooter(100, 2000, "/tmp/pim/cache/fetch-abc.md", 7)).toBe(
235
- "[web_fetch tool: showing first 100 bytes of 2000; use read with path=/tmp/pim/cache/fetch-abc.md and start=7 for the rest.]"
236
- );
237
- });
238
-
239
- test("signals the rest is unavailable when the spill failed", () => {
240
- expect(truncationFooter(100, 2000, null, 7)).toBe(
241
- "[web_fetch tool: showing first 100 bytes of 2000; full content unavailable.]"
242
- );
243
- });
244
- });
@@ -1,56 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { formatTitle } from "./render";
3
-
4
- describe("formatTitle", () => {
5
- test("returns placeholder and default format when url is undefined", () => {
6
- expect(formatTitle(undefined, undefined, undefined)).toBe("... (Markdown)");
7
- });
8
-
9
- test("renders default markdown pre-result", () => {
10
- expect(formatTitle("https://example.com", undefined, undefined)).toBe(
11
- "https://example.com (Markdown)"
12
- );
13
- });
14
-
15
- test("renders requested HTML pre-result", () => {
16
- expect(formatTitle("https://example.com", "html", undefined)).toBe(
17
- "https://example.com (HTML)"
18
- );
19
- });
20
-
21
- test("renders size + format label after result arrives", () => {
22
- expect(
23
- formatTitle("https://example.com", undefined, {
24
- format: "markdown",
25
- totalBytes: 23 * 1024,
26
- })
27
- ).toBe("https://example.com (23KB Markdown)");
28
- });
29
-
30
- test("strips trailing zeros and supports two decimals", () => {
31
- expect(
32
- formatTitle("https://example.com", undefined, {
33
- format: "html",
34
- totalBytes: 5355,
35
- })
36
- ).toBe("https://example.com (5.23KB HTML)");
37
- });
38
-
39
- test("renders bytes for tiny payloads", () => {
40
- expect(
41
- formatTitle("https://example.com", undefined, {
42
- format: "markdown",
43
- totalBytes: 512,
44
- })
45
- ).toBe("https://example.com (512B Markdown)");
46
- });
47
-
48
- test("renders MB for large payloads", () => {
49
- expect(
50
- formatTitle("https://example.com", undefined, {
51
- format: "html",
52
- totalBytes: 2.5 * 1024 * 1024,
53
- })
54
- ).toBe("https://example.com (2.5MB HTML)");
55
- });
56
- });