/** * test/link-paths.test.ts — pure findPathMatches matcher (W1). * * Node environment (no DOM): the matcher is deliberately DOM-free so it can be * unit-tested directly and reused by later diff/preview views. */ import { describe, it, expect } from 'vitest' import { findPathMatches, CODE_EXT } from '../public/link-paths.js' describe('findPathMatches — file:line paths', () => { it('matches a src/app.ts:42 path with correct 1-based range', () => { const line = 'see src/app.ts:42 for' const matches = findPathMatches(line) expect(matches).toHaveLength(1) const m = matches[0]! expect(m.text).toBe('src/app.ts:42') expect(m.path).toBe('src/app.ts') expect(m.line).toBe(42) expect(m.column).toBeUndefined() // 'see ' = 4 chars → startX = 5 (1-based), endX inclusive = 5 + 13 - 1 = 17. expect(m.startX).toBe(5) expect(m.endX).toBe(17) expect(line.slice(m.startX - 1, m.endX)).toBe('src/app.ts:42') }) it('parses :line:col into line + column', () => { const matches = findPathMatches('./a/b.tsx:10:5') expect(matches).toHaveLength(1) const m = matches[0]! expect(m.path).toBe('./a/b.tsx') expect(m.line).toBe(10) expect(m.column).toBe(5) }) it('matches a bare filename with an allowlisted extension (README.md)', () => { const matches = findPathMatches('open README.md please') expect(matches).toHaveLength(1) expect(matches[0]!.path).toBe('README.md') expect(matches[0]!.line).toBeUndefined() }) it('matches main.rs:10 via its :line suffix', () => { const matches = findPathMatches('at main.rs:10') expect(matches).toHaveLength(1) expect(matches[0]!.path).toBe('main.rs') expect(matches[0]!.line).toBe(10) }) it('matches an absolute path with a leading slash', () => { const matches = findPathMatches('Error at /home/u/app.ts:42 now') expect(matches).toHaveLength(1) const m = matches[0]! expect(m.path).toBe('/home/u/app.ts') expect(m.line).toBe(42) }) }) describe('findPathMatches — non-paths are rejected', () => { it('does not match a bare domain (example.com)', () => { expect(findPathMatches('visit example.com today')).toHaveLength(0) }) it('does not match a version string (v1.2.3)', () => { expect(findPathMatches('version v1.2.3 released')).toHaveLength(0) }) it('does not match a foo.bar token (unknown extension, no slash/line)', () => { expect(findPathMatches('the foo.bar thing')).toHaveLength(0) }) it('does not match a bare timestamp (12:34)', () => { expect(findPathMatches('at 12:34 today')).toHaveLength(0) }) }) describe('findPathMatches — URL guard', () => { it('does not double-link the path tail of an http URL (WebLinksAddon owns it)', () => { expect(findPathMatches('https://host/path.html')).toHaveLength(0) }) it('does not match a path glued to a URL host (a.ts inside http://x/a.ts)', () => { expect(findPathMatches('http://example.com/src/a.ts')).toHaveLength(0) }) }) describe('findPathMatches — multiple + relative paths', () => { it('returns two disjoint matches for two paths on one line', () => { const matches = findPathMatches('edit src/a.ts:1 and lib/b.rs:2') expect(matches).toHaveLength(2) expect(matches[0]!.path).toBe('src/a.ts') expect(matches[1]!.path).toBe('lib/b.rs') // Ranges are disjoint and ordered. expect(matches[0]!.endX).toBeLessThan(matches[1]!.startX) }) it('keeps a ../ prefix in the path portion', () => { const matches = findPathMatches('see ../lib/util.ts:7') expect(matches).toHaveLength(1) expect(matches[0]!.path).toBe('../lib/util.ts') expect(matches[0]!.line).toBe(7) }) it('matches a nested config.test.ts filename (multi-dot) with the final extension', () => { const matches = findPathMatches('run test/config.test.ts') expect(matches).toHaveLength(1) expect(matches[0]!.path).toBe('test/config.test.ts') }) it('returns [] for a line with no paths', () => { expect(findPathMatches('just some plain words here')).toEqual([]) }) }) describe('CODE_EXT allowlist', () => { it('contains common code extensions and not arbitrary ones', () => { expect(CODE_EXT.has('ts')).toBe(true) expect(CODE_EXT.has('md')).toBe(true) expect(CODE_EXT.has('rs')).toBe(true) expect(CODE_EXT.has('com')).toBe(false) expect(CODE_EXT.has('bar')).toBe(false) }) })