14252e71cb
Fix all Windows-specific test failures caused by path handling problems
and EBUSY errors from unclosed SQLite database handles.
Root causes fixed:
1. POSIX path assumptions: replaced hard-coded file:///tmp, /repo, etc.
with pathToFileURL/path.resolve/path.join computed expectations
2. shortenPath() now normalizes backslashes to forward slashes after ~
and respects home directory boundaries
3. HistoryStorage.resetInstance() leaked its Database — added #close()
that finalizes all prepared statements and closes the DB
4. AgentStorage gained the same resetInstance()/#close() pattern
5. SqliteAuthCredentialStore.close() leaked one-off prepared statements
from inline this.#db.prepare() calls — wrapped each in try/finally
6. model-cache.ts used a process-global DB even for custom dbPath —
now opens/closes per-call via withModelCacheDb
7. createAgentSession leaked AuthStorage on construction failure —
added ownsAuthStorage cleanup in catch block
8. MnemopiBackend.removeDbFiles() now truly best-effort (catches errors)
9. TempDir retry window expanded from 4x10ms to 40x25ms
10. TempDir prefix convention: non-@ prefixes created dirs relative to
cwd instead of os.tmpdir() — all test temp dirs now use @ prefix
11. Shell-escaped interpolated paths in bash tool tests
12. git core.autocrlf false in autoresearch test repo init
All 522 previously-failing Windows tests now pass.
149 lines
6.1 KiB
TypeScript
149 lines
6.1 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
import * as path from "node:path";
|
|
import * as url from "node:url";
|
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
import { resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
|
import { CustomEditor } from "@oh-my-pi/pi-coding-agent/modes/components/custom-editor";
|
|
import { UserMessageComponent } from "@oh-my-pi/pi-coding-agent/modes/components/user-message";
|
|
import { getEditorTheme, initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
|
|
import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types";
|
|
import { UiHelpers } from "@oh-my-pi/pi-coding-agent/modes/utils/ui-helpers";
|
|
import { Container } from "@oh-my-pi/pi-tui";
|
|
|
|
beforeAll(async () => {
|
|
resetSettingsForTest();
|
|
await Settings.init({ inMemory: true });
|
|
Settings.instance.set("tui.hyperlinks", "always");
|
|
await initTheme(false);
|
|
});
|
|
|
|
afterAll(() => {
|
|
resetSettingsForTest();
|
|
});
|
|
|
|
function render(text: string): string {
|
|
return new UserMessageComponent(text).render(80).join("\n");
|
|
}
|
|
|
|
describe("UserMessageComponent magic-keyword highlighting", () => {
|
|
it("gradient-paints a magic keyword in the rendered (sent) message bubble", () => {
|
|
const raw = render("please orchestrate the rollout");
|
|
// Visible text is preserved.
|
|
expect(Bun.stripANSI(raw)).toContain("please orchestrate the rollout");
|
|
// The keyword is gradient-painted: a per-character foreground sequence is emitted,
|
|
// and the word no longer survives as a contiguous run in the rendered bytes.
|
|
expect(raw).toContain("\x1b[38");
|
|
expect(raw).not.toContain("orchestrate");
|
|
});
|
|
|
|
it("does not paint a keyword inside an inline code span", () => {
|
|
const raw = render("ship the `orchestrate` helper");
|
|
expect(Bun.stripANSI(raw)).toContain("orchestrate");
|
|
// Code spans render through the code style as a single run — the word stays intact.
|
|
expect(raw).toContain("orchestrate");
|
|
});
|
|
|
|
it("does not paint a keyword inside a fenced code block", () => {
|
|
const raw = render("intro\n```\norchestrate\n```");
|
|
expect(Bun.stripANSI(raw)).toContain("orchestrate");
|
|
expect(raw).toContain("orchestrate");
|
|
});
|
|
|
|
it("closes OSC 133 prompt zones without opening a command-output zone", () => {
|
|
const raw = render("first line\nsecond line");
|
|
expect(raw).toContain("\x1b]133;A\x07");
|
|
expect(raw).toContain("\x1b]133;B\x07");
|
|
expect(raw).not.toContain("\x1b]133;C\x07");
|
|
});
|
|
|
|
it("bolds and underlines image references in the rendered message bubble", () => {
|
|
const raw = render("please inspect [Image #1] before continuing");
|
|
expect(Bun.stripANSI(raw)).toContain("[Image #1]");
|
|
expect(raw).toContain("\x1b[1m");
|
|
expect(raw).toContain("\x1b[4m");
|
|
});
|
|
|
|
it("wraps image references in file hyperlinks when a blob path is available", () => {
|
|
const imagePath = path.resolve("/tmp/omp-image.png");
|
|
const imageUri = url.pathToFileURL(path.resolve(imagePath)).href;
|
|
const raw = new UserMessageComponent("please inspect [Image #1]", false, [imagePath]).render(80).join("\n");
|
|
expect(Bun.stripANSI(raw)).toContain("[Image #1]");
|
|
expect(raw).toContain("\x1b]8;id=");
|
|
expect(raw).toContain(imageUri);
|
|
});
|
|
|
|
it("wraps draft editor image references in file hyperlinks when a blob path is available", () => {
|
|
const editor = new CustomEditor(getEditorTheme());
|
|
const imagePath = path.resolve("/tmp/omp-image.png");
|
|
const imageUri = url.pathToFileURL(path.resolve(imagePath)).href;
|
|
editor.imageLinks = [imagePath];
|
|
editor.setText("please inspect [Image #1]");
|
|
const raw = editor.render(80).join("\n");
|
|
expect(Bun.stripANSI(raw)).toContain("[Image #1]");
|
|
expect(raw).toContain("\x1b]8;id=");
|
|
expect(raw).toContain(imageUri);
|
|
});
|
|
|
|
it("rebuilds user messages with image hyperlinks when image links are not precomputed", () => {
|
|
const displayPath = path.resolve("/tmp/abc123.png");
|
|
const displayUri = url.pathToFileURL(path.resolve(displayPath)).href;
|
|
const chatContainer = new Container();
|
|
const sessionManagerMock = {
|
|
putBlobSync: () => ({
|
|
hash: "abc123",
|
|
path: path.resolve("/tmp/abc123"),
|
|
displayPath,
|
|
get ref() {
|
|
return "blob:sha256:abc123";
|
|
},
|
|
}),
|
|
};
|
|
const helpers = new UiHelpers({
|
|
chatContainer,
|
|
getUserMessageText: () => "please inspect [Image #1]",
|
|
sessionManager: sessionManagerMock,
|
|
viewSession: { sessionManager: sessionManagerMock },
|
|
} as unknown as InteractiveModeContext);
|
|
const message: AgentMessage = {
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: "please inspect [Image #1]" },
|
|
{ type: "image", data: Buffer.from("image-bytes").toString("base64"), mimeType: "image/png" },
|
|
],
|
|
attribution: "user",
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
helpers.addMessageToChat(message);
|
|
const component = chatContainer.children.at(-1);
|
|
if (!component) throw new Error("Expected user message component to be appended");
|
|
const raw = component.render(80).join("\n");
|
|
expect(Bun.stripANSI(raw)).toContain("[Image #1]");
|
|
expect(raw).toContain("\x1b]8;id=");
|
|
expect(raw).toContain(displayUri);
|
|
});
|
|
|
|
it("highlights paste markers in the draft editor without a hyperlink", () => {
|
|
const editor = new CustomEditor(getEditorTheme());
|
|
editor.setText("see [Paste #1, +30 lines] now");
|
|
const raw = editor.render(80).join("\n");
|
|
expect(Bun.stripANSI(raw)).toContain("[Paste #1, +30 lines]");
|
|
// The marker label is bold-wrapped (highlighted), unlike surrounding plain text.
|
|
expect(raw).toContain("\x1b[1m[Paste #1, +30 lines]");
|
|
// Paste markers are not clickable, so no OSC-8 hyperlink is emitted (contrast with images).
|
|
expect(raw).not.toContain("\x1b]8;id=");
|
|
});
|
|
|
|
it("hyperlinks the metadata-bearing image marker format", () => {
|
|
const editor = new CustomEditor(getEditorTheme());
|
|
const imagePath = path.resolve("/tmp/omp-image.png");
|
|
const imageUri = url.pathToFileURL(path.resolve(imagePath)).href;
|
|
editor.imageLinks = [imagePath];
|
|
editor.setText("see [Image #1, 800x600] now");
|
|
const raw = editor.render(80).join("\n");
|
|
expect(Bun.stripANSI(raw)).toContain("[Image #1, 800x600]");
|
|
expect(raw).toContain("\x1b]8;id=");
|
|
expect(raw).toContain(imageUri);
|
|
});
|
|
});
|