@aaroncql/pim-agent 0.0.1 → 0.1.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 (60) hide show
  1. package/README.md +19 -8
  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/bash/capture.test.ts +0 -126
  6. package/src/extensions/bash/format.test.ts +0 -240
  7. package/src/extensions/bash/run.test.ts +0 -262
  8. package/src/extensions/command-picker/ranker.test.ts +0 -46
  9. package/src/extensions/edit/edit.test.ts +0 -285
  10. package/src/extensions/file-picker/catalog.test.ts +0 -263
  11. package/src/extensions/file-picker/index.test.ts +0 -168
  12. package/src/extensions/file-picker/ranker.test.ts +0 -94
  13. package/src/extensions/footer/git.test.ts +0 -76
  14. package/src/extensions/footer/index.test.ts +0 -161
  15. package/src/extensions/footer/segments.test.ts +0 -164
  16. package/src/extensions/glob/glob.test.ts +0 -171
  17. package/src/extensions/glob/index.test.ts +0 -68
  18. package/src/extensions/glob/render.test.ts +0 -126
  19. package/src/extensions/grep/grep.test.ts +0 -387
  20. package/src/extensions/grep/index.test.ts +0 -68
  21. package/src/extensions/grep/render.test.ts +0 -269
  22. package/src/extensions/read/read.test.ts +0 -177
  23. package/src/extensions/read/render.test.ts +0 -61
  24. package/src/extensions/subagent/index.test.ts +0 -44
  25. package/src/extensions/subagent/render.test.ts +0 -292
  26. package/src/extensions/subagent/subagent.test.ts +0 -315
  27. package/src/extensions/system-prompt/prompt.test.ts +0 -64
  28. package/src/extensions/todo/index.test.ts +0 -244
  29. package/src/extensions/todo/render.test.ts +0 -180
  30. package/src/extensions/todo/todo.test.ts +0 -222
  31. package/src/extensions/tps/index.test.ts +0 -254
  32. package/src/extensions/web-fetch/WebViewMarkdownSnapshot.test.ts +0 -119
  33. package/src/extensions/web-fetch/fetch.test.ts +0 -244
  34. package/src/extensions/web-fetch/render.test.ts +0 -56
  35. package/src/extensions/web-search/ExaMcpClient.test.ts +0 -143
  36. package/src/extensions/web-search/render.test.ts +0 -21
  37. package/src/extensions/web-search/search.test.ts +0 -53
  38. package/src/extensions/working-indicator/index.test.ts +0 -21
  39. package/src/extensions/write/render.test.ts +0 -64
  40. package/src/extensions/write/write.test.ts +0 -108
  41. package/src/shared/DiffLines.test.ts +0 -193
  42. package/src/shared/DiffRenderer.test.ts +0 -206
  43. package/src/shared/EditMatcher.test.ts +0 -123
  44. package/src/shared/FileScanner.test.ts +0 -158
  45. package/src/shared/FuzzyMatcher.test.ts +0 -114
  46. package/src/shared/GitignoreFilter.test.ts +0 -64
  47. package/src/shared/Lines.test.ts +0 -25
  48. package/src/shared/McpClient.test.ts +0 -235
  49. package/src/shared/OutputBudget.test.ts +0 -99
  50. package/src/shared/Paths.test.ts +0 -51
  51. package/src/shared/PimSettings.test.ts +0 -90
  52. package/src/shared/Renderer.test.ts +0 -190
  53. package/src/shared/SpillCache.test.ts +0 -94
  54. package/src/shared/Tools.test.ts +0 -392
  55. package/src/telegram/Config.test.ts +0 -275
  56. package/src/telegram/Markdown.test.ts +0 -143
  57. package/src/telegram/Renderer.test.ts +0 -216
  58. package/src/telegram/SessionRegistry.test.ts +0 -89
  59. package/src/telegram/TaskScheduler.test.ts +0 -278
  60. 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
- });