Files
云服务部-叶林立andDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 2a6bf8c929 feat(pi-overflow): 新增统一的新工具结果展示截断扩展
此前截断分散在多层且互相冲突:Pi bash 保留尾部 50KB,RTK 兜底截断又只留
开头 12000 字符(砍掉报错与完整输出路径),pi-chrome 截开头 30K,web_fetch
各 provider 不一致。新扩展统一负责"新结果给模型看多少":
- capture.ts 最先加载暂存原文,index.ts 最后加载(system-prompt 之前)
- 超过 24K 字符时原文落盘 <agentDir>/my-pi/pi-overflow/(700/600、原子写),
  优先取 bash fullOutputPath 完整文件;模型看到 头 + 中段信号行 + 尾 的有界
  视图,行号基于落盘原文、结尾永远可见
- overflow_query:按 id 正则搜索、行区间、jsonPath 只读查询,id 校验防穿越
- 收编:RTK truncate 默认关闭、删除 pi-chrome truncateText、web_fetch 默认
  每页 20000(上限 100000)、web_search max_results 限定 1–20
- pi-compaction 遮蔽带 details.overflow.id 的结果时在占位符提示 overflow_query
- 登记 tools.jsonc / routing / 权限 allow / tool-search 常驻与分组

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-21 20:25:44 -07:00

113 lines
5.5 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runOverflowQuery } from "../src/query.ts";
import { storeEntry } from "../src/store.ts";
import { readOverflowSettings, type OverflowSettings } from "../settings.ts";
function settings(overrides: Partial<OverflowSettings> = {}): OverflowSettings {
return { ...readOverflowSettings("/nonexistent/pi-overflow.json"), ...overrides };
}
async function store(text: string, id = "0123456789ab") {
const root = await mkdtemp(join(tmpdir(), "pi-overflow-query-"));
await storeEntry(root, id, { kind: "text", text }, { tool: "bash", args: { command: "run" } }, 1_000_000);
return root;
}
test("search returns hits with merged context", async () => {
const lines = Array.from({ length: 100 }, (_, i) => (i === 9 || i === 11 || i === 50 ? `line ${i + 1} FAIL` : `line ${i + 1}`));
const root = await store(lines.join("\n"));
const result = await runOverflowQuery(root, { id: "0123456789ab", search: "FAIL", context: 1 }, settings());
assert.ok(!result.error);
assert.match(result.text, /L10: line 10 FAIL/);
assert.match(result.text, /L12: line 12 FAIL/);
assert.match(result.text, /L51: line 51 FAIL/);
// hits at lines 10 and 12 with context 1 merge into one range 9-13
assert.match(result.text, /--- lines 9-13 ---/);
assert.match(result.text, /--- lines 50-52 ---/);
});
test("line-range retrieval returns numbered lines", async () => {
const lines = Array.from({ length: 50 }, (_, i) => `row ${i + 1}`);
const root = await store(lines.join("\n"));
const result = await runOverflowQuery(root, { id: "0123456789ab", lines: { start: 10, end: 15 } }, settings());
assert.match(result.text, /--- lines 10-15 ---/);
assert.match(result.text, /L10: row 10/);
assert.match(result.text, /L15: row 15/);
assert.ok(!result.text.includes("row 16"));
});
test("jsonPath extracts nested values", async () => {
const doc = JSON.stringify({ snapshot: { nodes: [{ name: "a" }, { name: "b" }, { name: "c" }] } });
const root = await store(doc);
const result = await runOverflowQuery(root, { id: "0123456789ab", jsonPath: "snapshot.nodes[2].name" }, settings());
assert.match(result.text, /"c"/);
});
test("invalid id, regex, JSON and path return clear errors without throwing", async () => {
const root = await store('{"a": {"b": 1}}');
assert.ok((await runOverflowQuery(root, { id: "../etc/passwd" }, settings())).error);
assert.ok((await runOverflowQuery(root, { id: "zzzzzzzzzzzz" }, settings())).error);
assert.ok((await runOverflowQuery(root, { id: "0123456789ab", search: "([" }, settings())).error);
const goodPath = await runOverflowQuery(root, { id: "0123456789ab", jsonPath: "a.b" }, settings());
assert.ok(!goodPath.error, "valid path on valid JSON must succeed");
const badPath = await runOverflowQuery(root, { id: "0123456789ab", jsonPath: "a.b.c!!" }, settings());
assert.ok(badPath.error);
const missing = await runOverflowQuery(root, { id: "ffffffffffff" }, settings());
assert.ok(missing.error);
});
test("non-JSON content + jsonPath reports the parse failure", async () => {
const root = await store("not json at all");
const result = await runOverflowQuery(root, { id: "0123456789ab", jsonPath: "a" }, settings());
assert.ok(result.error);
assert.match(result.text, /not valid JSON/);
});
test("output is bounded by thresholdChars and marked truncated", async () => {
const lines = Array.from({ length: 5000 }, (_, i) => `line ${i + 1} pad ${"p".repeat(40)}`);
const root = await store(lines.join("\n"));
const result = await runOverflowQuery(
root,
{ id: "0123456789ab", lines: { start: 1, end: 5000 } },
settings({ thresholdChars: 4000 }),
);
assert.ok(result.text.length <= 4000);
assert.equal(result.truncated, true);
assert.match(result.text, /truncated/);
});
test("lines and search output clip each line to maxLineChars", async () => {
const long = `row ${"q".repeat(2_000)}`;
const root = await store(`first\n${long}\n${long} ERROR\nlast`);
const s = settings({ maxLineChars: 60 });
const lines = await runOverflowQuery(root, { id: "0123456789ab", lines: { start: 2, end: 3 } }, s);
assert.ok(lines.text.includes("q".repeat(56) + "…"));
assert.ok(!lines.text.includes("q".repeat(61)));
const search = await runOverflowQuery(root, { id: "0123456789ab", search: "ERROR", context: 0 }, s);
assert.ok(search.text.includes("q".repeat(56) + "…"));
assert.ok(!search.text.includes("q".repeat(61)));
});
test("search only scans the first 10000 chars of each line", async () => {
const earlyHit = `hit ${"a".repeat(100)} FAIL`;
const lateHit = `${"a".repeat(10_500)} FAIL`;
const root = await store(`${earlyHit}\n${lateHit}\nplain`);
const result = await runOverflowQuery(root, { id: "0123456789ab", search: "FAIL", context: 0 }, settings());
assert.match(result.text, /L1: hit /);
assert.ok(!result.text.includes("L2:"), "match beyond char 10000 must not be reported");
});
test("default query returns metadata and a first-screen view", async () => {
const lines = Array.from({ length: 300 }, (_, i) => `row ${i + 1}`);
const root = await store(lines.join("\n"));
const result = await runOverflowQuery(root, { id: "0123456789ab" }, settings());
assert.ok(!result.error);
assert.match(result.text, /\[pi-overflow id=0123456789ab tool=bash \d+ lines \/ \d+ bytes\]/);
assert.match(result.text, /--- lines 1-40 ---/);
assert.ok(result.text.includes("row 300"), "tail of the stored file must appear");
});