fix(coding-agent): stabilize extension and hyperlink tests

This commit is contained in:
can1357
2026-05-21 17:34:58 +09:00
committed by Can Bölük
parent 26b75f34f0
commit 8dc9125b0b
4 changed files with 80 additions and 84 deletions
+2 -3
View File
@@ -68,9 +68,8 @@ describe("isContextOverflow - 400/413 no-body (Cerebras, Mistral, proxy wrappers
// Regression: api.synthetic.new wraps upstream HF 400-no-body in a JSON envelope.
// finalizeErrorMessage transforms the response to "400 status code: {JSON}" where
// the JSON value contains the inner "400 status code (no body)" text.
it("detects wrapped proxy envelope: '400 status code: {\"error\":\"... 400 status code (no body)\"}'", () => {
const errorMessage =
'400 status code: {"error":"Error from inference backend: 400 status code (no body)"}';
it('detects wrapped proxy envelope: \'400 status code: {"error":"... 400 status code (no body)"}\'', () => {
const errorMessage = '400 status code: {"error":"Error from inference backend: 400 status code (no body)"}';
expect(isContextOverflow(createErrorMessage(errorMessage))).toBe(true);
});
@@ -154,7 +154,7 @@ interface LegacyPiMirrorState {
function getMirrorPath(sourcePath: string, state: LegacyPiMirrorState): string {
const extension = path.extname(sourcePath) || ".js";
const digest = Bun.hash(sourcePath).toString(36);
return path.join(state.root, `${digest}${extension}`);
return path.join(state.root, `module-${digest}${extension}`);
}
async function rewriteRelativeImportsForLegacyExtension(
@@ -212,7 +212,7 @@ async function mirrorLegacyPiFile(sourcePath: string, state: LegacyPiMirrorState
}
export async function loadLegacyPiModule(resolvedPath: string): Promise<unknown> {
const root = path.join(os.tmpdir(), "omp-legacy-pi-file", Bun.hash(resolvedPath).toString(36));
const root = path.join(os.tmpdir(), "omp-legacy-pi-file", `entry-${Bun.hash(resolvedPath).toString(36)}`);
await fs.rm(root, { recursive: true, force: true });
const state: LegacyPiMirrorState = { root, seen: new Map() };
const mirroredEntry = await mirrorLegacyPiFile(resolvedPath, state);
@@ -15,7 +15,6 @@ import {
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { getProjectAgentDir, logger, TempDir } from "@oh-my-pi/pi-utils";
import { filterUserScoped } from "./utils/filter-user-extensions";
describe("ExtensionRunner", () => {
let tempDir: TempDir;
@@ -40,11 +39,20 @@ describe("ExtensionRunner", () => {
});
const loadTestExtensions = async (configuredPaths: string[] = []) => {
const result = await discoverAndLoadExtensions(configuredPaths, tempDir.path());
const result = await discoverAndLoadExtensions([extensionsDir, ...configuredPaths], tempDir.path());
const testRoots = [
extensionsDir,
...configuredPaths.map(configuredPath => path.resolve(tempDir.path(), configuredPath)),
];
const isTestScoped = (candidate: string): boolean =>
testRoots.some(root => {
const relative = path.relative(path.resolve(root), path.resolve(candidate));
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
});
return {
...result,
extensions: filterUserScoped(result.extensions),
errors: filterUserScoped(result.errors),
extensions: result.extensions.filter(extension => isTestScoped(extension.path)),
errors: result.errors.filter(error => isTestScoped(error.path)),
};
};
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, spyOn } from "bun:test";
import * as settingsModule from "@oh-my-pi/pi-coding-agent/config/settings";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
import { resetSettingsForTest, Settings, settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { fileHyperlink, isHyperlinkEnabled, tryResolveInternalUrlSync } from "@oh-my-pi/pi-coding-agent/tui/hyperlink";
import * as terminalCaps from "@oh-my-pi/pi-tui";
@@ -7,6 +7,7 @@ import * as terminalCaps from "@oh-my-pi/pi-tui";
const OSC = "\x1b]";
const ST = "\x1b\\";
const LINK_END = `${OSC}8;;${ST}`;
const ORIGINAL_NO_COLOR = Bun.env.NO_COLOR;
/** Extract the hyperlink URI from a wrapped string. Returns undefined if not wrapped. */
function extractLinkUri(text: string): string | undefined {
@@ -19,133 +20,128 @@ function isHyperlinked(text: string): boolean {
return text.includes(`${OSC}8;`) && text.includes(LINK_END);
}
describe("isHyperlinkEnabled", () => {
afterEach(() => {
delete Bun.env.NO_COLOR;
});
/** Set the `tui.hyperlinks` mode via a non-persistent runtime override. */
function setHyperlinkMode(mode: "off" | "auto" | "always"): void {
settings.override("tui.hyperlinks", mode);
}
beforeAll(async () => {
resetSettingsForTest();
await Settings.init({ inMemory: true });
});
afterAll(() => {
resetSettingsForTest();
});
afterEach(() => {
settings.clearOverride("tui.hyperlinks");
if (ORIGINAL_NO_COLOR === undefined) {
delete Bun.env.NO_COLOR;
} else {
Bun.env.NO_COLOR = ORIGINAL_NO_COLOR;
}
});
describe("isHyperlinkEnabled", () => {
it('returns false when mode is "off"', () => {
const spy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "off" : undefined) as never,
);
setHyperlinkMode("off");
expect(isHyperlinkEnabled()).toBe(false);
spy.mockRestore();
});
it('returns true when mode is "always" regardless of TTY', () => {
const spy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "always" : undefined) as never,
);
setHyperlinkMode("always");
expect(isHyperlinkEnabled()).toBe(true);
spy.mockRestore();
});
it("returns false in auto mode when NO_COLOR is set", () => {
const spy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "auto" : undefined) as never,
);
setHyperlinkMode("auto");
Bun.env.NO_COLOR = "1";
expect(isHyperlinkEnabled()).toBe(false);
spy.mockRestore();
});
it("returns false in auto mode when stdout is not a TTY", () => {
const spy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "auto" : undefined) as never,
);
setHyperlinkMode("auto");
const origTTY = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
Object.defineProperty(process.stdout, "isTTY", { value: false, configurable: true });
expect(isHyperlinkEnabled()).toBe(false);
if (origTTY) Object.defineProperty(process.stdout, "isTTY", origTTY);
spy.mockRestore();
try {
Object.defineProperty(process.stdout, "isTTY", { value: false, configurable: true });
expect(isHyperlinkEnabled()).toBe(false);
} finally {
if (origTTY) {
Object.defineProperty(process.stdout, "isTTY", origTTY);
} else {
Reflect.deleteProperty(process.stdout, "isTTY");
}
}
});
it("returns TERMINAL.hyperlinks value in auto mode when conditions are met", () => {
const settingsSpy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "auto" : undefined) as never,
);
setHyperlinkMode("auto");
delete Bun.env.NO_COLOR;
const origTTY = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
// TERMINAL.hyperlinks may be true or false depending on the test runner env;
// what matters is that isHyperlinkEnabled mirrors it.
const expected = terminalCaps.TERMINAL.hyperlinks;
expect(isHyperlinkEnabled()).toBe(expected);
if (origTTY) Object.defineProperty(process.stdout, "isTTY", origTTY);
settingsSpy.mockRestore();
try {
Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
// TERMINAL.hyperlinks may be true or false depending on the test runner env;
// what matters is that isHyperlinkEnabled mirrors it.
const expected = terminalCaps.TERMINAL.hyperlinks;
expect(isHyperlinkEnabled()).toBe(expected);
} finally {
if (origTTY) {
Object.defineProperty(process.stdout, "isTTY", origTTY);
} else {
Reflect.deleteProperty(process.stdout, "isTTY");
}
}
});
});
describe("fileHyperlink", () => {
afterEach(() => {
delete Bun.env.NO_COLOR;
});
it("returns plain text when hyperlinks are disabled (mode=off)", () => {
const spy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "off" : undefined) as never,
);
setHyperlinkMode("off");
const result = fileHyperlink("/Users/foo/bar.ts", "bar.ts");
expect(result).toBe("bar.ts");
spy.mockRestore();
});
it("wraps text in OSC 8 when hyperlinks are enabled (mode=always)", () => {
const spy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "always" : undefined) as never,
);
setHyperlinkMode("always");
const result = fileHyperlink("/Users/foo/bar.ts", "bar.ts");
expect(isHyperlinked(result)).toBe(true);
expect(result).toContain("bar.ts");
spy.mockRestore();
});
it("builds a valid file:// URI with the absolute path", () => {
const spy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "always" : undefined) as never,
);
setHyperlinkMode("always");
const result = fileHyperlink("/Users/foo/bar.ts", "bar.ts");
const uri = extractLinkUri(result);
expect(uri).toMatch(/^file:\/\//);
expect(uri).toContain("bar.ts");
spy.mockRestore();
});
it("encodes spaces in the path", () => {
const spy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "always" : undefined) as never,
);
setHyperlinkMode("always");
const result = fileHyperlink("/Users/foo/my file.ts", "my file.ts");
const uri = extractLinkUri(result);
expect(uri).toContain("%20");
expect(uri).not.toContain(" ");
spy.mockRestore();
});
it("appends line and col as query params when provided", () => {
const spy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "always" : undefined) as never,
);
setHyperlinkMode("always");
const result = fileHyperlink("/Users/foo/bar.ts", "bar.ts", { line: 42, col: 7 });
const uri = extractLinkUri(result);
expect(uri).toContain("line=42");
expect(uri).toContain("col=7");
spy.mockRestore();
});
it("omits query params when line/col are not provided", () => {
const spy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "always" : undefined) as never,
);
setHyperlinkMode("always");
const result = fileHyperlink("/Users/foo/bar.ts", "bar.ts");
const uri = extractLinkUri(result);
expect(uri).not.toContain("?");
spy.mockRestore();
});
it("produces a stable id for the same path", () => {
const spy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "always" : undefined) as never,
);
setHyperlinkMode("always");
const r1 = fileHyperlink("/Users/foo/bar.ts", "bar.ts");
const r2 = fileHyperlink("/Users/foo/bar.ts", "different display text");
// Extract id= from params (between "id=" and next ";")
@@ -153,29 +149,22 @@ describe("fileHyperlink", () => {
const id2 = r2.match(/id=([^;]+)/)?.[1];
expect(id1).toBeDefined();
expect(id1).toBe(id2);
spy.mockRestore();
});
it("does not double-wrap text that already contains an OSC 8 sequence", () => {
const spy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "always" : undefined) as never,
);
setHyperlinkMode("always");
const alreadyWrapped = `${OSC}8;id=abc123;file:///foo/bar.ts${ST}bar.ts${LINK_END}`;
const result = fileHyperlink("/Users/foo/other.ts", alreadyWrapped);
// Should return the already-wrapped text unchanged
expect(result).toBe(alreadyWrapped);
spy.mockRestore();
});
it("preserves ANSI color codes inside the hyperlink", () => {
const spy = spyOn(settingsModule.settings, "get").mockImplementation(
(key: string) => (key === "tui.hyperlinks" ? "always" : undefined) as never,
);
setHyperlinkMode("always");
const colored = "\x1b[32mbar.ts\x1b[0m";
const result = fileHyperlink("/Users/foo/bar.ts", colored);
expect(result).toContain(colored);
expect(isHyperlinked(result)).toBe(true);
spy.mockRestore();
});
});