Files
oh-my-pi/packages/coding-agent/test/agent-hub-advisor-scroll.test.ts
T
can1357 c224ef4df5 feat: standardized elision markers and improve transcript viewer robustness
- Standardized elision markers across all tool outputs and filters to use cohesive `[...N [type] elided...]`, `[...Nln elided...]`, and `[...xB elided...]` syntax.
- Updated documentation, prompts, and test expectations to reflect the unified elision format.
- Improved transcript viewer robustness by preventing content aliasing through path-inclusive signature hashing.
- Added logic to clear stale transcript content when associated session files are deleted, accompanied by verifying test cases.
2026-06-19 04:35:18 +02:00

172 lines
5.1 KiB
TypeScript

/**
* Regression: the fullscreen transcript viewer must align the header, body, and
* footer on a single shared gutter. The transcript components carry their own
* 1-column left pad, so the viewer must NOT add a second outer gutter to body
* rows — doing so shifted the content one column right of the "Agent Hub" title
* (the reported "first char off / title shift"). Scrolling must also move the
* visible window.
*/
import { beforeEach, describe, expect, it } from "bun:test";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { AgentTranscriptViewer } from "@oh-my-pi/pi-coding-agent/modes/components/agent-transcript-viewer";
import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { AgentRegistry } from "@oh-my-pi/pi-coding-agent/registry/agent-registry";
import { CURRENT_SESSION_VERSION } from "@oh-my-pi/pi-coding-agent/session/session-entries";
const TS = new Date().toISOString();
function buildJsonl(): string {
const usage = {
input: 1,
output: 1,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 2,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
const lines = [
JSON.stringify({ type: "session", version: CURRENT_SESSION_VERSION, id: "adv", timestamp: TS, cwd: "/tmp" }),
];
lines.push(
JSON.stringify({
type: "message",
id: "u0",
parentId: null,
timestamp: TS,
message: { role: "user", synthetic: true, attribution: "agent", content: "PROMPTMARKER", timestamp: 0 },
}),
);
for (let i = 0; i < 40; i++) {
lines.push(
JSON.stringify({
type: "message",
id: `a${i}`,
parentId: null,
timestamp: TS,
message: {
role: "assistant",
content: [{ type: "text", text: `Reviewing step ${i}.` }],
api: "anthropic-messages",
provider: "anthropic",
model: "gpt-5.5",
usage,
stopReason: "stop",
timestamp: i,
},
}),
);
}
return `${lines.join("\n")}\n`;
}
function makeViewer(file: string) {
const agents = new AgentRegistry();
agents.register({
id: "Main/advisor",
displayName: "advisor",
kind: "advisor",
parentId: "Main",
session: null,
sessionFile: file,
status: "parked",
});
return new AgentTranscriptViewer({
agentId: "Main/advisor",
registry: agents,
ui: { requestRender: () => {}, requestComponentRender: () => {} } as never,
cwd: "/tmp",
expandKeys: ["ctrl+o"],
hubKeys: ["ctrl+s"],
requestRender: () => {},
onClose: () => {},
onHubClose: () => {},
});
}
/** Leading-space count of a stripped line (its content gutter). */
function gutter(line: string): number {
const stripped = Bun.stripANSI(line);
return stripped.length - stripped.trimStart().length;
}
function withViewer(fn: (viewer: AgentTranscriptViewer) => void): void {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "adv-view-"));
const file = path.join(dir, "__advisor.jsonl");
fs.writeFileSync(file, buildJsonl());
try {
fn(makeViewer(file));
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
describe("AgentTranscriptViewer", () => {
beforeEach(async () => {
resetSettingsForTest();
await Settings.init({ inMemory: true });
initTheme();
});
it("aligns the title and body content on the same gutter", () => {
withViewer(viewer => {
viewer.render(80); // populate the scroll view before navigating
viewer.handleInput("g"); // scroll to top so the first message is visible
const lines = viewer.render(80).map(l => Bun.stripANSI(l));
const titleLine = lines.find(l => l.includes("Agent Hub"));
const bodyLine = lines.find(l => l.includes("PROMPTMARKER"));
expect(titleLine).toBeDefined();
expect(bodyLine).toBeDefined();
// The body must not sit one column right of the title.
expect(gutter(bodyLine!)).toBe(gutter(titleLine!));
});
});
it("scrolls the visible window with j/k and g/G", () => {
withViewer(viewer => {
const atBottom = viewer
.render(80)
.map(l => Bun.stripANSI(l))
.join("\n");
viewer.handleInput("g");
const atTop = viewer
.render(80)
.map(l => Bun.stripANSI(l))
.join("\n");
expect(atTop).not.toEqual(atBottom);
expect(atTop).toContain("PROMPTMARKER");
expect(atBottom).not.toContain("PROMPTMARKER");
});
});
it("clears stale content when the transcript file is deleted while open", async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "adv-view-"));
const file = path.join(dir, "__advisor.jsonl");
fs.writeFileSync(file, buildJsonl());
const viewer = makeViewer(file);
const body = () =>
viewer
.render(80)
.map(l => Bun.stripANSI(l))
.join("\n");
try {
viewer.render(80);
viewer.handleInput("g");
expect(body()).toContain("PROMPTMARKER");
fs.rmSync(file);
// Poll until the viewer's own poll timer re-stats and clears (deadline-bounded).
const deadline = Date.now() + 5000;
while (body().includes("PROMPTMARKER") && Date.now() < deadline) {
await Bun.sleep(50);
}
expect(body()).not.toContain("PROMPTMARKER");
} finally {
viewer.dispose();
fs.rmSync(dir, { recursive: true, force: true });
}
});
});