Path: blob/main/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLITerminalLinkProvider.spec.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { beforeEach, describe, expect, it, vi } from 'vitest';6import type { CancellationToken, Terminal, TerminalLinkContext, Uri } from 'vscode';7import { TestLogService } from '../../../../platform/testing/common/testLogService';8import { CopilotCLITerminalLinkProvider } from '../copilotCLITerminalLinkProvider';910// --- Mocks ---------------------------------------------------------------1112const mockStat = vi.hoisted(() => vi.fn());13const mockReadDirectory = vi.hoisted(() => vi.fn());14const mockShowTextDocument = vi.hoisted(() => vi.fn());15const mockShowQuickPick = vi.hoisted(() => vi.fn());16const mockWorkspaceFolders = vi.hoisted(() => ({ value: undefined as { uri: { fsPath: string; scheme: string } }[] | undefined }));1718vi.mock('vscode', () => ({19Uri: {20file: (path: string) => ({21fsPath: path,22scheme: 'file',23toString: (skipEncoding?: boolean) => skipEncoding ? `file://${path}` : `file://${encodeURI(path)}`,24}),25joinPath: (base: { fsPath: string; scheme: string }, ...segments: string[]) => {26const joined = [base.fsPath, ...segments].join('/');27return {28fsPath: joined,29scheme: base.scheme,30toString: (skipEncoding?: boolean) => skipEncoding ? `file://${joined}` : `file://${encodeURI(joined)}`,31};32},33},34Range: class Range {35constructor(36public readonly startLine: number,37public readonly startCharacter: number,38public readonly endLine: number,39public readonly endCharacter: number,40) { }41},42window: {43showTextDocument: mockShowTextDocument,44showQuickPick: mockShowQuickPick,45},46l10n: {47t: (message: string, ...args: string[]) => message.replace(/\{(\d+)\}/g, (_, i) => args[Number(i)]),48},49FileType: {50Unknown: 0,51File: 1,52Directory: 2,53SymbolicLink: 64,54},55workspace: {56fs: {57stat: mockStat,58readDirectory: mockReadDirectory,59},60get workspaceFolders() {61return mockWorkspaceFolders.value;62},63},64}));6566vi.mock('os', () => ({67homedir: () => '/Users/anthonykim',68}));6970// --- Helpers -------------------------------------------------------------7172const SESSION_UUID = 'ak1234fe-ae47-4c68-8123-f4adef123123';73const SESSION_DIR = `/Users/anthonykim/.copilot/session-state/${SESSION_UUID}`;7475class MockTerminal {76readonly processId = Promise.resolve(123);77readonly name = 'test';78readonly creationOptions = {};79readonly exitStatus = undefined;80readonly state = { isInteractedWith: false, shell: undefined };81readonly selection = undefined;82readonly shellIntegration = undefined;83sendText() { }84show() { }85hide() { }86dispose() { }87}8889function makeTerminal(): Terminal {90return new MockTerminal() as Terminal;91}9293function makeContext(line: string, terminal: Terminal): TerminalLinkContext {94return { line, terminal };95}9697function makeToken(): CancellationToken {98return { isCancellationRequested: false, onCancellationRequested: vi.fn() } as CancellationToken;99}100101function makeCancelledToken(): CancellationToken {102return { isCancellationRequested: true, onCancellationRequested: vi.fn() } as CancellationToken;103}104105// --- Tests ---------------------------------------------------------------106107describe('CopilotCLITerminalLinkProvider', () => {108let provider: CopilotCLITerminalLinkProvider;109let terminal: Terminal;110let sessionDirUri: Uri;111112beforeEach(async () => {113vi.clearAllMocks();114mockWorkspaceFolders.value = undefined;115mockReadDirectory.mockResolvedValue([]);116mockShowQuickPick.mockResolvedValue(undefined);117const vscode = await import('vscode');118119provider = new CopilotCLITerminalLinkProvider(new TestLogService());120terminal = makeTerminal();121sessionDirUri = vscode.Uri.file(SESSION_DIR);122123provider.registerTerminal(terminal);124provider.setSessionDir(terminal, sessionDirUri);125126// By default, stat succeeds (file exists).127mockStat.mockResolvedValue({ type: 1 });128});129130describe('relative paths', () => {131it('should detect files/sample-summary.md', async () => {132const links = await provider.provideTerminalLinks(133makeContext(' Relative: files/sample-summary.md', terminal),134makeToken(),135);136expect(links).toHaveLength(1);137expect(links[0].pathText).toBe('files/sample-summary.md');138expect(links[0].uri?.fsPath).toBe(`${SESSION_DIR}/files/sample-summary.md`);139});140141it('should detect bare files/sample-summary.md at start of line', async () => {142const links = await provider.provideTerminalLinks(143makeContext('files/sample-summary.md', terminal),144makeToken(),145);146expect(links).toHaveLength(1);147expect(links[0].pathText).toBe('files/sample-summary.md');148});149150it('should detect dot-prefixed ./files/sample-summary.md', async () => {151const links = await provider.provideTerminalLinks(152makeContext('./files/sample-summary.md', terminal),153makeToken(),154);155expect(links).toHaveLength(1);156expect(links[0].pathText).toBe('./files/sample-summary.md');157});158159it('should detect standalone plan.md in a sentence', async () => {160const links = await provider.provideTerminalLinks(161makeContext('Created plan.md with next steps', terminal),162makeToken(),163);164expect(links).toHaveLength(1);165expect(links[0].pathText).toBe('plan.md');166expect(links[0].uri?.fsPath).toBe(`${SESSION_DIR}/plan.md`);167});168169it('should detect standalone plan.md with :line:col suffix', async () => {170const links = await provider.provideTerminalLinks(171makeContext('See plan.md:12:3 for details', terminal),172makeToken(),173);174expect(links).toHaveLength(1);175expect(links[0].pathText).toBe('plan.md');176expect(links[0].line).toBe(12);177expect(links[0].col).toBe(3);178});179180it('should detect standalone filenames with 1-character extensions', async () => {181const links = await provider.provideTerminalLinks(182makeContext('Compile main.c next', terminal),183makeToken(),184);185expect(links).toHaveLength(1);186expect(links[0].pathText).toBe('main.c');187});188189it('should not detect numeric tokens like version 1.2', async () => {190const links = await provider.provideTerminalLinks(191makeContext('Version 1.2 is installed', terminal),192makeToken(),193);194expect(links).toHaveLength(0);195});196197it('should resolve bare filename to files/<name> when root file does not exist', async () => {198mockStat.mockImplementation((uri: { fsPath: string }) => {199if (uri.fsPath === `${SESSION_DIR}/todo.md`) {200return Promise.reject(new Error('not found'));201}202if (uri.fsPath === `${SESSION_DIR}/files/todo.md`) {203return Promise.resolve({ type: 1 });204}205return Promise.reject(new Error('not found'));206});207208const links = await provider.provideTerminalLinks(209makeContext('| todo.md | /Users/anthonykim/.copilot/session-state/id/files/todo.md | files/todo.md |', terminal),210makeToken(),211);212213const todoLink = links.find(link => link.pathText === 'todo.md');214expect(todoLink).toBeDefined();215expect(todoLink?.uri?.fsPath).toBe(`${SESSION_DIR}/files/todo.md`);216});217218it('should resolve slash paths relative to files/ when session-root path does not exist', async () => {219mockStat.mockImplementation((uri: { fsPath: string }) => {220if (uri.fsPath === `${SESSION_DIR}/anotherFolderNamehere/thenyourfilehere.txt`) {221return Promise.reject(new Error('not found'));222}223if (uri.fsPath === `${SESSION_DIR}/files/anotherFolderNamehere/thenyourfilehere.txt`) {224return Promise.resolve({ type: 1 });225}226return Promise.reject(new Error('not found'));227});228229const links = await provider.provideTerminalLinks(230makeContext('anotherFolderNamehere/thenyourfilehere.txt', terminal),231makeToken(),232);233234expect(links).toHaveLength(1);235expect(links[0].uri?.fsPath).toBe(`${SESSION_DIR}/files/anotherFolderNamehere/thenyourfilehere.txt`);236});237238it('should resolve bare filename in nested session subdirectories', async () => {239mockStat.mockImplementation((uri: { fsPath: string }) => {240if (uri.fsPath === `${SESSION_DIR}/001-created-session-files-and-path.md`) {241return Promise.reject(new Error('not found'));242}243if (uri.fsPath === `${SESSION_DIR}/files/001-created-session-files-and-path.md`) {244return Promise.reject(new Error('not found'));245}246return Promise.reject(new Error('not found'));247});248249mockReadDirectory.mockImplementation((uri: { fsPath: string }) => {250if (uri.fsPath === SESSION_DIR) {251return Promise.resolve([252['checkpoints', 2],253]);254}255256if (uri.fsPath === `${SESSION_DIR}/checkpoints`) {257return Promise.resolve([258['001-created-session-files-and-path.md', 1],259]);260}261262return Promise.resolve([]);263});264265const links = await provider.provideTerminalLinks(266makeContext('001-created-session-files-and-path.md', terminal),267makeToken(),268);269270expect(links).toHaveLength(1);271expect(links[0].uri?.fsPath).toBe(`${SESSION_DIR}/checkpoints/001-created-session-files-and-path.md`);272});273});274275describe('tilde paths', () => {276it('should expand ~/.copilot/session-state/.../files/sample-summary.md', async () => {277const links = await provider.provideTerminalLinks(278makeContext(` Absolute: ~/.copilot/session-state/${SESSION_UUID}/files/sample-summary.md`, terminal),279makeToken(),280);281expect(links).toHaveLength(1);282expect(links[0].pathText).toContain('~/.copilot/session-state');283expect(links[0].uri?.fsPath).toBe(`/Users/anthonykim/.copilot/session-state/${SESSION_UUID}/files/sample-summary.md`);284});285});286287describe('absolute paths', () => {288it('should skip /Users/anthonykim/.copilot/.../files/sample-summary.md', async () => {289const links = await provider.provideTerminalLinks(290makeContext(` /Users/anthonykim/.copilot/session-state/${SESSION_UUID}/files/sample-summary.md`, terminal),291makeToken(),292);293// Absolute paths are skipped — the built-in detector handles them.294expect(links).toHaveLength(0);295});296});297298describe('trailing punctuation', () => {299it('should strip trailing period from files/sample-summary.md.', async () => {300const links = await provider.provideTerminalLinks(301makeContext('file at files/sample-summary.md.', terminal),302makeToken(),303);304expect(links).toHaveLength(1);305expect(links[0].pathText).toBe('files/sample-summary.md');306});307308it('should strip multiple trailing dots', async () => {309const links = await provider.provideTerminalLinks(310makeContext('files/sample-summary.md...', terminal),311makeToken(),312);313expect(links).toHaveLength(1);314expect(links[0].pathText).toBe('files/sample-summary.md');315});316});317318describe('line and column suffixes', () => {319it('should parse :line:col suffix', async () => {320const links = await provider.provideTerminalLinks(321makeContext('src/foo/bar.ts:10:5', terminal),322makeToken(),323);324expect(links).toHaveLength(1);325expect(links[0].pathText).toBe('src/foo/bar.ts');326expect(links[0].line).toBe(10);327expect(links[0].col).toBe(5);328});329330it('should parse (line, col) suffix', async () => {331const links = await provider.provideTerminalLinks(332makeContext('src/foo/bar.ts(42, 7)', terminal),333makeToken(),334);335expect(links).toHaveLength(1);336expect(links[0].line).toBe(42);337expect(links[0].col).toBe(7);338});339});340341describe('URLs', () => {342it('should skip https:// URLs', async () => {343const links = await provider.provideTerminalLinks(344makeContext('Visit https://example.com/path for info', terminal),345makeToken(),346);347expect(links).toHaveLength(0);348});349});350351describe('guards', () => {352it('should return empty for blank lines', async () => {353const links = await provider.provideTerminalLinks(354makeContext(' ', terminal),355makeToken(),356);357expect(links).toHaveLength(0);358});359360it('should return empty for lines over 2000 chars', async () => {361const longLine = 'files/summary.md ' + 'x'.repeat(2000);362const links = await provider.provideTerminalLinks(363makeContext(longLine, terminal),364makeToken(),365);366expect(links).toHaveLength(0);367});368369it('should cap links at 10 per line', async () => {370const paths = Array.from({ length: 15 }, (_, i) => `dir/file${i}.ts`).join(' ');371const links = await provider.provideTerminalLinks(372makeContext(paths, terminal),373makeToken(),374);375expect(links.length).toBeLessThanOrEqual(10);376});377378it('should skip unregistered terminals with no session dirs', async () => {379const unknownTerminal = makeTerminal();380const links = await provider.provideTerminalLinks(381makeContext('files/summary.md', unknownTerminal),382makeToken(),383);384expect(links).toHaveLength(0);385});386387it('should stop processing when cancellation is requested before path resolution', async () => {388const links = await provider.provideTerminalLinks(389makeContext('files/sample-summary.md', terminal),390makeCancelledToken(),391);392expect(links).toHaveLength(0);393expect(mockStat).not.toHaveBeenCalled();394expect(mockReadDirectory).not.toHaveBeenCalled();395});396});397398describe('cancellation', () => {399it('should stop nested lookup when token is cancelled during traversal', async () => {400const token = makeToken();401const cancellationState = { cancelled: false };402Object.defineProperty(token, 'isCancellationRequested', {403get: () => cancellationState.cancelled,404});405406mockStat.mockRejectedValue(new Error('not found'));407mockReadDirectory.mockImplementation((uri: { fsPath: string }) => {408if (uri.fsPath === SESSION_DIR) {409cancellationState.cancelled = true;410return Promise.resolve([411['checkpoints', 2],412]);413}414415return Promise.resolve([]);416});417418const links = await provider.provideTerminalLinks(419makeContext('001-created-session-files-and-path.md', terminal),420token,421);422423expect(links).toHaveLength(0);424expect(mockReadDirectory).toHaveBeenCalledTimes(1);425});426});427428describe('handleTerminalLink', () => {429it('should prompt when multiple targets exist and open selected target', async () => {430const vscode = await import('vscode');431mockWorkspaceFolders.value = [{ uri: vscode.Uri.file('/workspace/project') }];432433mockStat.mockImplementation((uri: { fsPath: string }) => {434if (uri.fsPath === `${SESSION_DIR}/plan.md`) {435return Promise.resolve({ type: 1 });436}437if (uri.fsPath === '/workspace/project/plan.md') {438return Promise.resolve({ type: 1 });439}440return Promise.reject(new Error('not found'));441});442443mockShowQuickPick.mockImplementation(async (items: Array<{ uri: { fsPath: string }; label: string; description?: string; detail?: string }>) => {444expect(items).toHaveLength(2);445expect(items[0].label).toBe('plan.md');446expect(items[1].label).toBe('plan.md');447expect(items.some(item => item.description === 'session-state/ak1234fe-ae47-4c68-8123-f4adef123123')).toBe(true);448expect(items.some(item => item.description === 'workspace')).toBe(true);449expect(items.every(item => item.detail === undefined)).toBe(true);450return items.find(item => item.uri.fsPath === '/workspace/project/plan.md');451});452453const links = await provider.provideTerminalLinks(454makeContext('plan.md', terminal),455makeToken(),456);457458expect(links).toHaveLength(1);459await provider.handleTerminalLink(links[0]);460expect(mockShowQuickPick).toHaveBeenCalled();461expect(mockShowTextDocument).toHaveBeenCalled();462expect(mockShowTextDocument.mock.calls[0][0].fsPath).toBe('/workspace/project/plan.md');463});464465it('should not open when quick pick is cancelled', async () => {466const vscode = await import('vscode');467mockWorkspaceFolders.value = [{ uri: vscode.Uri.file('/workspace/project') }];468469mockStat.mockImplementation((uri: { fsPath: string }) => {470if (uri.fsPath === `${SESSION_DIR}/plan.md`) {471return Promise.resolve({ type: 1 });472}473if (uri.fsPath === '/workspace/project/plan.md') {474return Promise.resolve({ type: 1 });475}476return Promise.reject(new Error('not found'));477});478479mockShowQuickPick.mockResolvedValue(undefined);480481const links = await provider.provideTerminalLinks(482makeContext('plan.md', terminal),483makeToken(),484);485486expect(links).toHaveLength(1);487await provider.handleTerminalLink(links[0]);488expect(mockShowQuickPick).toHaveBeenCalled();489expect(mockShowTextDocument).not.toHaveBeenCalled();490});491492it('should open directly without prompting when only one target exists', async () => {493const vscode = await import('vscode');494mockWorkspaceFolders.value = [{ uri: vscode.Uri.file('/workspace/project') }];495496mockStat.mockImplementation((uri: { fsPath: string }) => {497if (uri.fsPath === `${SESSION_DIR}/plan.md`) {498return Promise.resolve({ type: 1 });499}500return Promise.reject(new Error('not found'));501});502503const links = await provider.provideTerminalLinks(504makeContext('plan.md', terminal),505makeToken(),506);507508expect(links).toHaveLength(1);509await provider.handleTerminalLink(links[0]);510expect(mockShowQuickPick).not.toHaveBeenCalled();511expect(mockShowTextDocument).toHaveBeenCalledTimes(1);512expect(mockShowTextDocument.mock.calls[0][0].fsPath).toBe(`${SESSION_DIR}/plan.md`);513});514});515516describe('session dir resolution', () => {517it('should resolve via session dir resolver when no cached dir', async () => {518const vscode = await import('vscode');519const freshTerminal = makeTerminal();520provider.registerTerminal(freshTerminal);521provider.setSessionDirResolver(async _t => [vscode.Uri.file(SESSION_DIR)]);522523const links = await provider.provideTerminalLinks(524makeContext('files/demo.md', freshTerminal),525makeToken(),526);527expect(links).toHaveLength(1);528expect(links[0].uri?.fsPath).toBe(`${SESSION_DIR}/files/demo.md`);529});530531it('should fall back to workspace folders when file not in session dir', async () => {532const vscode = await import('vscode');533// stat fails for session dir, succeeds for workspace534mockStat.mockRejectedValueOnce(new Error('not found'))535.mockResolvedValueOnce({ type: 1 });536537mockWorkspaceFolders.value = [{ uri: vscode.Uri.file('/workspace/project') }];538539const links = await provider.provideTerminalLinks(540makeContext('src/index.ts', terminal),541makeToken(),542);543expect(links).toHaveLength(1);544expect(links[0].uri?.fsPath).toBe('/workspace/project/src/index.ts');545});546547// Regression test for https://github.com/microsoft/vscode/issues/301594548// Resolver first returned an unrelated session (only one tracked at the549// time).550it('should not cache stale resolver result when session tracker learns the real session later', async () => {551const vscode = await import('vscode');552const freshTerminal = makeTerminal();553provider.registerTerminal(freshTerminal);554555const staleDir = '/Users/anthonykim/.copilot/session-state/31830812-0221-4389-b6bf-b1d33fe556e2';556const realDir = '/Users/anthonykim/.copilot/session-state/278b1a81-eb86-4a81-bff0-ba68035c1b48';557558// sessionTracker initially only knows about an unrelated session,559// then later also learns the real one for this terminal.560let call = 0;561provider.setSessionDirResolver(async _t => {562call++;563return call === 1564? [vscode.Uri.file(staleDir)]565: [vscode.Uri.file(staleDir), vscode.Uri.file(realDir)];566});567568// files/file-01.md only exists under the real session dir.569mockStat.mockImplementation((uri: { fsPath: string }) => {570if (uri.fsPath === `${realDir}/files/file-01.md`) {571return Promise.resolve({ type: 1 });572}573return Promise.reject(new Error('not found'));574});575576// First hover: only the stale dir is known, file isn't found there.577const first = await provider.provideTerminalLinks(578makeContext('files/file-01.md', freshTerminal),579makeToken(),580);581expect(first).toHaveLength(0);582583// Second hover: resolver must be consulted again and pick up the584// real dir. Previously this returned the stale dir from the cache.585const second = await provider.provideTerminalLinks(586makeContext('files/file-01.md', freshTerminal),587makeToken(),588);589expect(call).toBe(2);590expect(second).toHaveLength(1);591expect(second[0].uri?.fsPath).toBe(`${realDir}/files/file-01.md`);592});593594it('should still consult resolver even when an explicit session dir is cached', async () => {595const vscode = await import('vscode');596const freshTerminal = makeTerminal();597provider.registerTerminal(freshTerminal);598599const oldDir = '/Users/anthonykim/.copilot/session-state/old';600const newDir = '/Users/anthonykim/.copilot/session-state/new';601602// Explicitly cached (e.g. resumed session) but user then started a603// new `copilot` run in the same terminal.604provider.setSessionDir(freshTerminal, vscode.Uri.file(oldDir));605provider.setSessionDirResolver(async _t => [vscode.Uri.file(newDir)]);606607mockStat.mockImplementation((uri: { fsPath: string }) => {608if (uri.fsPath === `${newDir}/files/demo.md`) {609return Promise.resolve({ type: 1 });610}611return Promise.reject(new Error('not found'));612});613614const links = await provider.provideTerminalLinks(615makeContext('files/demo.md', freshTerminal),616makeToken(),617);618expect(links).toHaveLength(1);619expect(links[0].uri?.fsPath).toBe(`${newDir}/files/demo.md`);620});621622// Scenario 1: User quits session X and starts session Y in the SAME623// terminal. The stale cached dir from X's resume should not shadow Y's624// active session dir, even when both have a file with the same name.625it('should prefer active resolver dir over stale cached dir when file exists in both', async () => {626const vscode = await import('vscode');627const freshTerminal = makeTerminal();628provider.registerTerminal(freshTerminal);629630const staleDir = '/Users/anthonykim/.copilot/session-state/ended-session';631const activeDir = '/Users/anthonykim/.copilot/session-state/active-session';632633// Stale cache from a previous resumed session that has since ended.634provider.setSessionDir(freshTerminal, vscode.Uri.file(staleDir));635636// Resolver only returns the active session (stale one was disposed).637provider.setSessionDirResolver(async _t => [vscode.Uri.file(activeDir)]);638639// The file exists in BOTH dirs on disk (session-state persists).640mockStat.mockResolvedValue({ type: 1 });641642const links = await provider.provideTerminalLinks(643makeContext('files/summary.md', freshTerminal),644makeToken(),645);646expect(links).toHaveLength(1);647// Must resolve to the active session dir, not the stale cached one.648expect(links[0].uri?.fsPath).toBe(`${activeDir}/files/summary.md`);649});650651// Scenario 2: Two terminals, each with its own session. The resolver652// returns matching-terminal sessions first for correct isolation.653it('should prefer terminal-matched resolver dirs over unrelated sessions', async () => {654const vscode = await import('vscode');655const terminalA = makeTerminal();656const terminalB = makeTerminal();657provider.registerTerminal(terminalA);658provider.registerTerminal(terminalB);659660const sessionXDir = '/Users/anthonykim/.copilot/session-state/session-x';661const sessionYDir = '/Users/anthonykim/.copilot/session-state/session-y';662663// Terminal-aware resolver: session X belongs to terminal A,664// session Y belongs to terminal B.665provider.setSessionDirResolver(async t => {666if (t === terminalA) {667return [vscode.Uri.file(sessionXDir), vscode.Uri.file(sessionYDir)];668}669return [vscode.Uri.file(sessionYDir), vscode.Uri.file(sessionXDir)];670});671672// The file exists in both session dirs.673mockStat.mockResolvedValue({ type: 1 });674675const linksA = await provider.provideTerminalLinks(676makeContext('files/summary.md', terminalA),677makeToken(),678);679expect(linksA).toHaveLength(1);680expect(linksA[0].uri?.fsPath).toBe(`${sessionXDir}/files/summary.md`);681682const linksB = await provider.provideTerminalLinks(683makeContext('files/summary.md', terminalB),684makeToken(),685);686expect(linksB).toHaveLength(1);687expect(linksB[0].uri?.fsPath).toBe(`${sessionYDir}/files/summary.md`);688});689});690691describe('extensionless files', () => {692it('should detect dir/Makefile', async () => {693const links = await provider.provideTerminalLinks(694makeContext('dir/Makefile', terminal),695makeToken(),696);697expect(links).toHaveLength(1);698expect(links[0].pathText).toBe('dir/Makefile');699});700});701702describe('Windows paths', () => {703it('should detect backslash relative paths', async () => {704const links = await provider.provideTerminalLinks(705makeContext('files\\sample-summary.md', terminal),706makeToken(),707);708expect(links).toHaveLength(1);709expect(links[0].pathText).toBe('files\\sample-summary.md');710});711712it('should expand tilde with backslash (~\\.copilot\\...)', async () => {713const links = await provider.provideTerminalLinks(714makeContext('Create ~\\.copilot\\session-state\\5d9e\\files\\sample-summary.md (+4)', terminal),715makeToken(),716);717expect(links).toHaveLength(1);718expect(links[0].uri?.fsPath).toContain('/Users/anthonykim');719expect(links[0].uri?.fsPath).toContain('.copilot');720});721722it('should skip Windows absolute paths (C:\\...)', async () => {723const links = await provider.provideTerminalLinks(724makeContext('Absolute: C:\\Users\\antho\\.copilot\\files\\sample-summary.md', terminal),725makeToken(),726);727// C:\... matched as \Users\... which starts with \ and is skipped.728expect(links).toHaveLength(0);729});730});731});732733734