import { describe, expect, it } from "vite-plus/test"; import { collectWrappedTerminalLinkLine, extractTerminalLinks, isTerminalLinkActivation, resolvePathLinkTarget, resolveWrappedTerminalLinkRange, wrappedTerminalLinkRangeIntersectsBufferLine, type TerminalBufferLineLike, } from "./terminal-links"; function createBufferLine(text: string, isWrapped = false): TerminalBufferLineLike { return { isWrapped, translateToString: (trimRight = false) => (trimRight ? text.replace(/\s+$/u, "") : text), }; } describe("extractTerminalLinks", () => { it("finds http urls and path tokens", () => { const line = "failed at https://example.com/docs and src/components/ThreadTerminalDrawer.tsx:42"; expect(extractTerminalLinks(line)).toEqual([ { kind: "url", text: "https://example.com/docs", start: 10, end: 34, }, { kind: "path", text: "src/components/ThreadTerminalDrawer.tsx:42", start: 39, end: 81, }, ]); }); it("trims trailing punctuation from links", () => { const line = "(https://example.com/docs), ./src/main.ts:12."; expect(extractTerminalLinks(line)).toEqual([ { kind: "url", text: "https://example.com/docs", start: 1, end: 25, }, { kind: "path", text: "./src/main.ts:12", start: 28, end: 44, }, ]); }); it("finds Windows absolute paths with forward slashes", () => { const line = "see C:/Users/someone/project/src/file.ts:42 for details"; const path = "C:/Users/someone/project/src/file.ts:42"; const start = line.indexOf(path); expect(extractTerminalLinks(line)).toEqual([ { kind: "path", text: path, start, end: start + path.length, }, ]); }); it("trims trailing punctuation from Windows forward-slash paths", () => { const line = "(C:/tmp/x.ts)."; expect(extractTerminalLinks(line)).toEqual([ { kind: "path", text: "C:/tmp/x.ts", start: 1, end: 12, }, ]); }); }); describe("collectWrappedTerminalLinkLine", () => { it("reconstructs a wrapped line from any physical row", () => { const firstSegment = "see https://example.com/a"; const secondSegment = "/bc?x=1"; const lines = [ createBufferLine("prompt> "), createBufferLine(firstSegment), createBufferLine(secondSegment, true), createBufferLine("done"), ]; const fromFirstRow = collectWrappedTerminalLinkLine(2, (index) => lines[index]); const fromWrappedRow = collectWrappedTerminalLinkLine(3, (index) => lines[index]); expect(fromFirstRow).toEqual({ text: `${firstSegment}${secondSegment}`, segments: [ { bufferLineNumber: 2, text: firstSegment, startIndex: 0, endIndex: firstSegment.length, }, { bufferLineNumber: 3, text: secondSegment, startIndex: firstSegment.length, endIndex: firstSegment.length + secondSegment.length, }, ], }); expect(fromWrappedRow).toEqual(fromFirstRow); }); it("preserves trailing spaces on continued segments for downstream offsets", () => { const firstSegment = "prefix "; const secondSegment = "https://example.com/path"; const lines = [createBufferLine(firstSegment), createBufferLine(secondSegment, true)]; const wrappedLine = collectWrappedTerminalLinkLine(2, (index) => lines[index]); expect(wrappedLine?.text).toBe(`${firstSegment}${secondSegment}`); expect(extractTerminalLinks(wrappedLine?.text ?? "")).toEqual([ { kind: "url", text: secondSegment, start: firstSegment.length, end: firstSegment.length + secondSegment.length, }, ]); }); }); describe("resolveWrappedTerminalLinkRange", () => { it("maps wrapped URL matches back to the correct buffer rows", () => { const prefix = "see "; const firstSegment = `${prefix}https://example.com/a`; const secondSegment = "/bc?x=1"; const lines = [ createBufferLine("prompt> "), createBufferLine(firstSegment), createBufferLine(secondSegment, true), ]; const wrappedLine = collectWrappedTerminalLinkLine(2, (index) => lines[index]); expect(wrappedLine).not.toBeNull(); if (!wrappedLine) { throw new Error("Expected wrapped terminal line to be present."); } const [match] = extractTerminalLinks(wrappedLine.text); expect(match).toEqual({ kind: "url", text: "https://example.com/a/bc?x=1", start: prefix.length, end: firstSegment.length + secondSegment.length, }); if (!match) { throw new Error("Expected wrapped URL match to be present."); } const range = resolveWrappedTerminalLinkRange(wrappedLine, match); expect(range).toEqual({ start: { x: prefix.length + 1, y: 2 }, end: { x: secondSegment.length, y: 3 }, }); expect(wrappedTerminalLinkRangeIntersectsBufferLine(range, 2)).toBe(true); expect(wrappedTerminalLinkRangeIntersectsBufferLine(range, 3)).toBe(true); expect(wrappedTerminalLinkRangeIntersectsBufferLine(range, 4)).toBe(false); }); }); describe("resolvePathLinkTarget", () => { it("resolves relative paths against cwd", () => { expect( resolvePathLinkTarget( "src/components/ThreadTerminalDrawer.tsx:42:7", "/Users/julius/project", ), ).toBe("/Users/julius/project/src/components/ThreadTerminalDrawer.tsx:42:7"); }); it("keeps absolute paths unchanged", () => { expect( resolvePathLinkTarget("/Users/julius/project/src/main.ts:12", "/Users/julius/project"), ).toBe("/Users/julius/project/src/main.ts:12"); }); it("keeps Windows absolute paths with forward slashes unchanged", () => { expect( resolvePathLinkTarget("C:/Users/julius/project/src/main.ts:12", "C:\\Users\\julius\\project"), ).toBe("C:/Users/julius/project/src/main.ts:12"); }); }); describe("isTerminalLinkActivation", () => { it("requires cmd on macOS", () => { expect( isTerminalLinkActivation( { metaKey: true, ctrlKey: false, }, "MacIntel", ), ).toBe(true); expect( isTerminalLinkActivation( { metaKey: false, ctrlKey: true, }, "MacIntel", ), ).toBe(false); }); it("requires ctrl on non-macOS", () => { expect( isTerminalLinkActivation( { metaKey: false, ctrlKey: true, }, "Win32", ), ).toBe(true); expect( isTerminalLinkActivation( { metaKey: true, ctrlKey: false, }, "Linux", ), ).toBe(false); }); });