test: add web tool output formatting

This commit is contained in:
alex wiesner
2026-04-09 11:08:52 +01:00
parent 30cfe7e8f1
commit 7db96b025b
3 changed files with 134 additions and 5 deletions

View File

@@ -0,0 +1,53 @@
import test from "node:test";
import assert from "node:assert/strict";
import { formatFetchOutput, formatSearchOutput, truncateText } from "./format.ts";
test("formatSearchOutput renders a compact metadata-only list", () => {
const output = formatSearchOutput({
providerName: "exa-main",
results: [
{
title: "Exa Docs",
url: "https://exa.ai/docs",
publishedDate: "2026-04-09",
author: "Exa",
score: 0.98,
},
],
});
assert.match(output, /Found 1 web result via exa-main:/);
assert.match(output, /Exa Docs/);
assert.match(output, /https:\/\/exa.ai\/docs/);
});
test("truncateText shortens long fetch bodies with an ellipsis", () => {
assert.equal(truncateText("abcdef", 4), "abc…");
assert.equal(truncateText("abc", 10), "abc");
});
test("formatFetchOutput includes both successful and failed URLs", () => {
const output = formatFetchOutput(
{
providerName: "exa-main",
results: [
{
url: "https://good.example",
title: "Good",
text: "This is a very long body that should be truncated in the final output.",
},
{
url: "https://bad.example",
title: null,
error: "429 rate limited",
},
],
},
{ maxCharactersPerResult: 20 },
);
assert.match(output, /Status: ok/);
assert.match(output, /Status: failed/);
assert.match(output, /429 rate limited/);
assert.match(output, /This is a very long…/);
});

View File

@@ -0,0 +1,76 @@
import type { NormalizedFetchResponse, NormalizedSearchResponse } from "./providers/types.ts";
export function truncateText(text: string, maxCharacters = 4000) {
if (text.length <= maxCharacters) {
return text;
}
return `${text.slice(0, Math.max(0, maxCharacters - 1))}`;
}
export function formatSearchOutput(response: NormalizedSearchResponse) {
if (response.results.length === 0) {
return `No web results via ${response.providerName}.`;
}
const lines = [
`Found ${response.results.length} web result${response.results.length === 1 ? "" : "s"} via ${response.providerName}:`,
];
for (const [index, result] of response.results.entries()) {
lines.push(`${index + 1}. ${result.title ?? "(untitled)"}`);
lines.push(` URL: ${result.url}`);
const meta = [result.publishedDate, result.author].filter(Boolean);
if (meta.length > 0) {
lines.push(` Meta: ${meta.join(" • ")}`);
}
if (typeof result.score === "number") {
lines.push(` Score: ${result.score}`);
}
}
return lines.join("\n");
}
export interface FetchFormatOptions {
maxCharactersPerResult?: number;
}
export function formatFetchOutput(response: NormalizedFetchResponse, options: FetchFormatOptions = {}) {
const maxCharactersPerResult = options.maxCharactersPerResult ?? 4000;
const lines = [
`Fetched ${response.results.length} URL${response.results.length === 1 ? "" : "s"} via ${response.providerName}:`,
];
for (const result of response.results) {
lines.push("");
lines.push(`URL: ${result.url}`);
if (result.error) {
lines.push("Status: failed");
lines.push(`Error: ${result.error}`);
continue;
}
lines.push("Status: ok");
if (result.title) {
lines.push(`Title: ${result.title}`);
}
if (result.summary) {
lines.push(`Summary: ${result.summary}`);
}
if (result.highlights?.length) {
lines.push("Highlights:");
for (const highlight of result.highlights) {
lines.push(`- ${highlight}`);
}
}
if (result.text) {
lines.push("Text:");
lines.push(truncateText(result.text, maxCharactersPerResult));
}
}
return lines.join("\n");
}

View File

@@ -737,7 +737,7 @@ git -C /home/alex/dotfiles commit -m "test: add exa web provider adapter"
- Create: `/home/alex/dotfiles/.pi/agent/extensions/web-search/src/format.ts`
- Test: `/home/alex/dotfiles/.pi/agent/extensions/web-search/src/format.test.ts`
- [ ] **Step 1: Write the failing formatter tests**
- [x] **Step 1: Write the failing formatter tests**
Create `/home/alex/dotfiles/.pi/agent/extensions/web-search/src/format.test.ts` with this content:
@@ -797,7 +797,7 @@ test("formatFetchOutput includes both successful and failed URLs", () => {
});
```
- [ ] **Step 2: Run the formatter tests to verify they fail**
- [x] **Step 2: Run the formatter tests to verify they fail**
Run:
@@ -808,7 +808,7 @@ npx tsx --test src/format.test.ts
Expected: `FAIL` with an `ERR_MODULE_NOT_FOUND` error for `./format.ts`.
- [ ] **Step 3: Write the minimal formatter implementation**
- [x] **Step 3: Write the minimal formatter implementation**
Create `/home/alex/dotfiles/.pi/agent/extensions/web-search/src/format.ts` with this content:
@@ -891,7 +891,7 @@ export function formatFetchOutput(response: NormalizedFetchResponse, options: Fe
}
```
- [ ] **Step 4: Run the formatter tests to verify they pass**
- [x] **Step 4: Run the formatter tests to verify they pass**
Run:
@@ -902,7 +902,7 @@ npx tsx --test src/format.test.ts
Expected: `PASS` for all three tests.
- [ ] **Step 5: Commit the formatter layer**
- [x] **Step 5: Commit the formatter layer**
Run: